Skip to content

Commit 84bef34

Browse files
Merge pull request #2608 from stripe/prathmesh/merge-node-beta
Merge to beta
2 parents a5394eb + 5eef386 commit 84bef34

16 files changed

+925
-165
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
[![Downloads](https://img.shields.io/npm/dm/stripe.svg)](https://www.npmjs.com/package/stripe)
66
[![Try on RunKit](https://badge.runkitcdn.com/stripe.svg)](https://runkit.com/npm/stripe)
77

8+
> [!TIP]
9+
> Want to chat live with Stripe engineers? Join us on our [Discord server](https://stripe.com/go/discord/node).
10+
811
The Stripe Node library provides convenient access to the Stripe API from
912
applications written in server-side JavaScript.
1013

src/StripeResource.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
getAPIMode,
23
getDataFromArgs,
34
getOptionsFromArgs,
45
makeURLInterpolator,
@@ -16,6 +17,7 @@ import {
1617
UrlInterpolator,
1718
} from './Types.js';
1819
import {HttpClientResponseInterface} from './net/HttpClient.js';
20+
import {coerceV2RequestData, coerceV2ResponseData} from './V2Int64.js';
1921

2022
// Provide extension mechanism for Stripe Resource Sub-Classes
2123
StripeResource.extend = protoExtend;
@@ -209,18 +211,35 @@ StripeResource.prototype = {
209211
return;
210212
}
211213

214+
// Coerce int64_string fields in request body: number → string
215+
const apiMode = getAPIMode(spec.fullPath || spec.path);
216+
if (apiMode === 'v2' && spec.requestSchema && opts.bodyData) {
217+
opts.bodyData = coerceV2RequestData(
218+
opts.bodyData,
219+
spec.requestSchema
220+
) as RequestData;
221+
}
222+
212223
function requestCallback(
213224
err: any,
214225
response: HttpClientResponseInterface
215226
): void {
216227
if (err) {
217228
reject(err);
218229
} else {
219-
resolve(
220-
spec.transformResponseData
221-
? spec.transformResponseData(response)
222-
: response
223-
);
230+
// Coerce int64_string fields in response: string → bigint
231+
try {
232+
if (apiMode === 'v2' && spec.responseSchema) {
233+
coerceV2ResponseData(response, spec.responseSchema);
234+
}
235+
resolve(
236+
spec.transformResponseData
237+
? spec.transformResponseData(response)
238+
: response
239+
);
240+
} catch (e) {
241+
reject(e);
242+
}
224243
}
225244
}
226245

src/Types.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export type BufferedFile = {
1616
type: string;
1717
file: {data: Uint8Array};
1818
};
19+
export type V2RuntimeSchema =
20+
| {kind: 'int64_string'}
21+
| {kind: 'object'; fields: Record<string, V2RuntimeSchema>}
22+
| {kind: 'array'; element: V2RuntimeSchema}
23+
| {kind: 'nullable'; inner: V2RuntimeSchema};
1924
export type MethodSpec = {
2025
method: string;
2126
methodType?: string;
@@ -29,6 +34,8 @@ export type MethodSpec = {
2934
host?: string;
3035
transformResponseData?: (response: HttpClientResponseInterface) => any;
3136
usage?: Array<string>;
37+
requestSchema?: V2RuntimeSchema;
38+
responseSchema?: V2RuntimeSchema;
3239
};
3340
export type MultipartRequestData = RequestData | StreamingFile | BufferedFile;
3441
// rawErrorTypeEnum: The beginning of the section generated from our OpenAPI spec

src/V2Int64.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {V2RuntimeSchema} from './Types.js';
2+
3+
/**
4+
* Coerces outbound V2 request data by converting bigint (or number)
5+
* int64_string fields to strings, matching the wire format expected by the API.
6+
*
7+
* Walks the schema tree and only touches fields that are marked as
8+
* int64_string. All other values are left unchanged.
9+
*/
10+
export const coerceV2RequestData = (
11+
data: unknown,
12+
schema: V2RuntimeSchema
13+
): unknown => {
14+
if (data == null) {
15+
return data;
16+
}
17+
18+
switch (schema.kind) {
19+
case 'int64_string':
20+
return typeof data === 'bigint' || typeof data === 'number'
21+
? String(data)
22+
: data;
23+
24+
case 'object': {
25+
if (typeof data !== 'object' || Array.isArray(data)) {
26+
return data;
27+
}
28+
const obj = data as Record<string, unknown>;
29+
const result: Record<string, unknown> = {};
30+
for (const key of Object.keys(obj)) {
31+
const fieldSchema = schema.fields[key];
32+
result[key] = fieldSchema
33+
? coerceV2RequestData(obj[key], fieldSchema)
34+
: obj[key];
35+
}
36+
return result;
37+
}
38+
39+
case 'array': {
40+
if (!Array.isArray(data)) {
41+
return data;
42+
}
43+
return data.map((element) =>
44+
coerceV2RequestData(element, schema.element)
45+
);
46+
}
47+
48+
case 'nullable':
49+
return coerceV2RequestData(data, schema.inner);
50+
}
51+
};
52+
53+
/**
54+
* Coerces inbound V2 response data by converting string int64_string fields
55+
* to bigints, matching the SDK's public type contract.
56+
*
57+
* Walks the schema tree and only touches fields that are marked as
58+
* int64_string. All other values are left unchanged.
59+
*/
60+
export const coerceV2ResponseData = (
61+
data: unknown,
62+
schema: V2RuntimeSchema
63+
): unknown => {
64+
if (data == null) {
65+
return data;
66+
}
67+
68+
switch (schema.kind) {
69+
case 'int64_string':
70+
if (typeof data === 'string') {
71+
try {
72+
return BigInt(data);
73+
} catch {
74+
throw new Error(
75+
`Failed to coerce int64_string value: expected an integer string, got '${data}'`
76+
);
77+
}
78+
}
79+
return data;
80+
81+
case 'object': {
82+
if (typeof data !== 'object' || Array.isArray(data)) {
83+
return data;
84+
}
85+
const obj = data as Record<string, unknown>;
86+
for (const key of Object.keys(schema.fields)) {
87+
if (key in obj) {
88+
obj[key] = coerceV2ResponseData(obj[key], schema.fields[key]);
89+
}
90+
}
91+
return obj;
92+
}
93+
94+
case 'array': {
95+
if (!Array.isArray(data)) {
96+
return data;
97+
}
98+
for (let i = 0; i < data.length; i++) {
99+
data[i] = coerceV2ResponseData(data[i], schema.element);
100+
}
101+
return data;
102+
}
103+
104+
case 'nullable':
105+
return coerceV2ResponseData(data, schema.inner);
106+
}
107+
};

src/platform/NodePlatformFunctions.ts

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {NodeHttpClient} from '../net/NodeHttpClient.js';
88
import {PlatformFunctions} from './PlatformFunctions.js';
99
import {StripeError} from '../Error.js';
1010
import {concat} from '../utils.js';
11-
import {exec} from 'child_process';
11+
import {arch, release} from 'os';
1212
import {MultipartRequestData, RequestData, BufferedFile} from '../Types.js';
1313

1414
class StreamProcessingError extends StripeError {}
@@ -17,17 +17,6 @@ class StreamProcessingError extends StripeError {}
1717
* Specializes WebPlatformFunctions using APIs available in Node.js.
1818
*/
1919
export class NodePlatformFunctions extends PlatformFunctions {
20-
/** For mocking in tests */
21-
_exec: any;
22-
_UNAME_CACHE: Promise<string | null> | null;
23-
24-
constructor() {
25-
super();
26-
27-
this._exec = exec;
28-
this._UNAME_CACHE = null;
29-
}
30-
3120
/** @override */
3221
uuid4(): string {
3322
// available in: v14.17.x+
@@ -37,31 +26,9 @@ export class NodePlatformFunctions extends PlatformFunctions {
3726
return super.uuid4();
3827
}
3928

40-
/**
41-
* @override
42-
* Node's built in `exec` function sometimes throws outright,
43-
* and sometimes has a callback with an error,
44-
* depending on the type of error.
45-
*
46-
* This unifies that interface by resolving with a null uname
47-
* if an error is encountered.
48-
*/
49-
getUname(): Promise<string | null> {
50-
if (!this._UNAME_CACHE) {
51-
this._UNAME_CACHE = new Promise<string | null>((resolve, reject) => {
52-
try {
53-
this._exec('uname -a', (err: unknown, uname: string | null) => {
54-
if (err) {
55-
return resolve(null);
56-
}
57-
resolve(uname!);
58-
});
59-
} catch (e) {
60-
resolve(null);
61-
}
62-
});
63-
}
64-
return this._UNAME_CACHE;
29+
/** @override */
30+
getPlatformInfo(): string {
31+
return `${process.platform} ${release()} ${arch()}`;
6532
}
6633

6734
/**

src/platform/PlatformFunctions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ export class PlatformFunctions {
2121
}
2222

2323
/**
24-
* Gets uname with Node's built-in `exec` function, if available.
24+
* Returns platform info string for telemetry, or null if unavailable.
2525
*/
26-
getUname(): Promise<string | null> {
27-
throw new Error('getUname not implemented.');
26+
getPlatformInfo(): string | null {
27+
return null;
2828
}
2929

3030
/**

src/platform/WebPlatformFunctions.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ import {MultipartRequestData, RequestData, BufferedFile} from '../Types.js';
88
* Specializes WebPlatformFunctions using APIs available in Web workers.
99
*/
1010
export class WebPlatformFunctions extends PlatformFunctions {
11-
/** @override */
12-
getUname(): Promise<string | null> {
13-
return Promise.resolve(null);
14-
}
15-
1611
/** @override */
1712
createEmitter(): StripeEmitter {
1813
return new StripeEmitter();

src/stripe.core.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ export function createStripe(
7272
Stripe.USER_AGENT = {
7373
bindings_version: Stripe.PACKAGE_VERSION,
7474
lang: 'node',
75-
publisher: 'stripe',
76-
uname: null,
7775
typescript: false,
7876
...determineProcessUserAgentProperties(),
7977
...(aiAgent ? {ai_agent: aiAgent} : {}),
@@ -385,29 +383,31 @@ export function createStripe(
385383
seed: Record<string, string | boolean | null>,
386384
cb: (userAgent: string) => void
387385
): void {
388-
this._platformFunctions.getUname().then((uname: string | null) => {
389-
const userAgent: Record<string, string> = {};
390-
for (const field in seed) {
391-
if (!Object.prototype.hasOwnProperty.call(seed, field)) {
392-
continue;
393-
}
394-
userAgent[field] = encodeURIComponent(seed[field] ?? 'null');
386+
const userAgent: Record<string, string> = {};
387+
for (const field in seed) {
388+
if (!Object.prototype.hasOwnProperty.call(seed, field)) {
389+
continue;
395390
}
391+
userAgent[field] = encodeURIComponent(seed[field] ?? 'null');
392+
}
396393

397-
// URI-encode in case there are unusual characters in the system's uname.
398-
userAgent.uname = encodeURIComponent(uname || 'UNKNOWN');
394+
const platformInfo = this._platformFunctions.getPlatformInfo();
395+
if (platformInfo && this.getTelemetryEnabled()) {
396+
userAgent.platform = encodeURIComponent(platformInfo);
397+
} else {
398+
delete userAgent.platform;
399+
}
399400

400-
const client = this.getApiField('httpClient');
401-
if (client) {
402-
userAgent.httplib = encodeURIComponent(client.getClientName());
403-
}
401+
const client = this.getApiField('httpClient');
402+
if (client) {
403+
userAgent.httplib = encodeURIComponent(client.getClientName());
404+
}
404405

405-
if (this._appInfo) {
406-
userAgent.application = this._appInfo;
407-
}
406+
if (this._appInfo) {
407+
userAgent.application = this._appInfo;
408+
}
408409

409-
cb(JSON.stringify(userAgent));
410-
});
410+
cb(JSON.stringify(userAgent));
411411
},
412412

413413
/**

src/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,6 @@ export function determineProcessUserAgentProperties(): Record<string, string> {
476476
? {}
477477
: {
478478
lang_version: process.version,
479-
platform: process.platform,
480479
};
481480
}
482481

0 commit comments

Comments
 (0)