@ranwhenparked/trustap-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +251 -0
- package/deno.json +9 -0
- package/eslint.config.js +21 -0
- package/package.json +47 -0
- package/scripts/build-node.mjs +75 -0
- package/scripts/generate-operations-map.mjs +57 -0
- package/scripts/generate-security-map.mjs +92 -0
- package/src/__tests__/auth-middleware.test.ts +171 -0
- package/src/__tests__/client-factory.test.ts +105 -0
- package/src/__tests__/error-handling.test.ts +302 -0
- package/src/__tests__/helpers/mock-http-client.ts +193 -0
- package/src/__tests__/helpers/run-guard.ts +24 -0
- package/src/__tests__/helpers/test-fixtures.ts +82 -0
- package/src/__tests__/node-client.test.ts +244 -0
- package/src/__tests__/operation-proxy.test.ts +268 -0
- package/src/__tests__/query-params.test.ts +232 -0
- package/src/__tests__/setup.ts +44 -0
- package/src/__tests__/types.test.ts +169 -0
- package/src/client-deno.ts +219 -0
- package/src/client-factory.ts +45 -0
- package/src/core.ts +619 -0
- package/src/index.deno.ts +28 -0
- package/src/index.ts +36 -0
- package/src/operations-map.ts +667 -0
- package/src/schema.d.ts +12046 -0
- package/src/security-map.ts +770 -0
- package/src/state-machine.ts +79 -0
- package/src/webhook-schemas.ts +400 -0
- package/tsconfig.build.json +27 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +31 -0
package/src/core.ts
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
// Type helper to extract query parameters from operations
|
|
2
|
+
type ExtractQueryParams<T> = T extends { parameters: { query: infer Q } }
|
|
3
|
+
? Q
|
|
4
|
+
: never;
|
|
5
|
+
|
|
6
|
+
// Type helper to extract path parameters from operations
|
|
7
|
+
type ExtractPathParams<T> = T extends { parameters: { path: infer P } }
|
|
8
|
+
? P
|
|
9
|
+
: never;
|
|
10
|
+
|
|
11
|
+
// Type helper to extract request body from operations
|
|
12
|
+
type ExtractRequestBody<T> = T extends {
|
|
13
|
+
requestBody: { content: { "application/json": infer Body } };
|
|
14
|
+
}
|
|
15
|
+
? Body
|
|
16
|
+
: never;
|
|
17
|
+
|
|
18
|
+
// Type helper to extract response data from operations
|
|
19
|
+
type ExtractResponseData<T> = T extends {
|
|
20
|
+
responses: { 200: { content: { "application/json": infer Data } } };
|
|
21
|
+
}
|
|
22
|
+
? Data
|
|
23
|
+
: T extends {
|
|
24
|
+
responses: { 201: { content: { "application/json": infer Data } } };
|
|
25
|
+
}
|
|
26
|
+
? Data
|
|
27
|
+
: unknown;
|
|
28
|
+
|
|
29
|
+
// Type helper to extract error response from operations
|
|
30
|
+
type ExtractErrorResponse<T> = T extends {
|
|
31
|
+
responses: { 400: { content: { "application/json": infer Error } } };
|
|
32
|
+
}
|
|
33
|
+
? Error
|
|
34
|
+
: T extends {
|
|
35
|
+
responses: { 404: { content: { "application/json": infer Error } } };
|
|
36
|
+
}
|
|
37
|
+
? Error
|
|
38
|
+
: unknown;
|
|
39
|
+
|
|
40
|
+
// Options type for operations with different parameter combinations
|
|
41
|
+
type OperationOptions<T> = {
|
|
42
|
+
params?: {
|
|
43
|
+
query?: ExtractQueryParams<T>;
|
|
44
|
+
path?: ExtractPathParams<T>;
|
|
45
|
+
};
|
|
46
|
+
// Support legacy format for backward compatibility
|
|
47
|
+
query?: ExtractQueryParams<T>;
|
|
48
|
+
headers?: HeadersInit;
|
|
49
|
+
} & (ExtractRequestBody<T> extends never
|
|
50
|
+
? object
|
|
51
|
+
: { body: ExtractRequestBody<T> });
|
|
52
|
+
|
|
53
|
+
// Response type for operations
|
|
54
|
+
type OperationResponse<T> = Promise<{
|
|
55
|
+
data?: ExtractResponseData<T>;
|
|
56
|
+
error?: ExtractErrorResponse<T>;
|
|
57
|
+
response: Response;
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
type TrustapOperationIdClient<
|
|
61
|
+
Operations,
|
|
62
|
+
OperationMap extends Record<string, { path: string; method: string }>,
|
|
63
|
+
> = {
|
|
64
|
+
[K in keyof OperationMap]: K extends keyof Operations
|
|
65
|
+
? (options?: OperationOptions<Operations[K]>) => OperationResponse<Operations[K]>
|
|
66
|
+
: (options?: { params?: { query?: unknown; path?: unknown }; query?: unknown }) => Promise<{
|
|
67
|
+
data?: unknown;
|
|
68
|
+
error?: unknown;
|
|
69
|
+
response: Response;
|
|
70
|
+
}>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export interface CreateTrustapClientOptions {
|
|
74
|
+
apiUrl: string;
|
|
75
|
+
basicAuth?: {
|
|
76
|
+
username: string;
|
|
77
|
+
password?: string;
|
|
78
|
+
};
|
|
79
|
+
getAccessToken?: () => Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Map a request path to an authentication strategy.
|
|
82
|
+
* Keys must be exact matches for either the normalized path (e.g. "/charge")
|
|
83
|
+
* or the fully qualified path (e.g. "/api/v4/charge"). Provide both if needed.
|
|
84
|
+
*/
|
|
85
|
+
authOverrides?: Record<string, "basic" | "oauth2" | "auto">;
|
|
86
|
+
basePath?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type HttpMethod =
|
|
90
|
+
| "GET"
|
|
91
|
+
| "POST"
|
|
92
|
+
| "PUT"
|
|
93
|
+
| "PATCH"
|
|
94
|
+
| "DELETE"
|
|
95
|
+
| "HEAD"
|
|
96
|
+
| "OPTIONS";
|
|
97
|
+
|
|
98
|
+
export interface MinimalHttpClient<Middleware> {
|
|
99
|
+
use(middleware: Middleware): void;
|
|
100
|
+
GET(path: string, options?: unknown): Promise<unknown>;
|
|
101
|
+
POST(path: string, options?: unknown): Promise<unknown>;
|
|
102
|
+
PUT(path: string, options?: unknown): Promise<unknown>;
|
|
103
|
+
PATCH(path: string, options?: unknown): Promise<unknown>;
|
|
104
|
+
DELETE(path: string, options?: unknown): Promise<unknown>;
|
|
105
|
+
HEAD(path: string, options?: unknown): Promise<unknown>;
|
|
106
|
+
OPTIONS(path: string, options?: unknown): Promise<unknown>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function removeTrailingSlash(value: string): string {
|
|
110
|
+
let end = value.length;
|
|
111
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
112
|
+
end--;
|
|
113
|
+
}
|
|
114
|
+
return value.slice(0, end);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeBasePath(basePath?: string): string {
|
|
118
|
+
if (!basePath) return "";
|
|
119
|
+
const trimmed = basePath.trim();
|
|
120
|
+
if (!trimmed || trimmed === "/") return "";
|
|
121
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
122
|
+
return removeTrailingSlash(withLeadingSlash);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function inferBaseConfig(apiUrl: string, providedBasePath?: string) {
|
|
126
|
+
let inferredPath: string | undefined;
|
|
127
|
+
let baseUrl: string;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const parsed = new URL(apiUrl);
|
|
131
|
+
inferredPath = parsed.pathname && parsed.pathname !== "/"
|
|
132
|
+
? removeTrailingSlash(parsed.pathname)
|
|
133
|
+
: undefined;
|
|
134
|
+
baseUrl = `${parsed.protocol}//${parsed.host}`;
|
|
135
|
+
} catch {
|
|
136
|
+
baseUrl = removeTrailingSlash(apiUrl);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const basePath = normalizeBasePath(
|
|
140
|
+
providedBasePath ?? inferredPath ?? "/api/v4",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
baseUrl: removeTrailingSlash(baseUrl),
|
|
145
|
+
basePath,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function joinPaths(basePath: string, path: string): string {
|
|
150
|
+
const sanitizedBase = normalizeBasePath(basePath);
|
|
151
|
+
const hasLeadingSlash = path.startsWith("/");
|
|
152
|
+
const relative = hasLeadingSlash ? path.slice(1) : path;
|
|
153
|
+
|
|
154
|
+
if (!sanitizedBase) {
|
|
155
|
+
return hasLeadingSlash ? path : `/${relative}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!relative) {
|
|
159
|
+
return sanitizedBase;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return `${sanitizedBase}/${relative}`.replaceAll(/\/+/g, "/");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toPathParamSegment(key: string, value: unknown): string {
|
|
166
|
+
if (
|
|
167
|
+
typeof value === "string" ||
|
|
168
|
+
typeof value === "number" ||
|
|
169
|
+
typeof value === "boolean" ||
|
|
170
|
+
typeof value === "bigint"
|
|
171
|
+
) {
|
|
172
|
+
return String(value);
|
|
173
|
+
}
|
|
174
|
+
if (value instanceof Date) {
|
|
175
|
+
return value.toISOString();
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
typeof value === "object" &&
|
|
179
|
+
value !== null &&
|
|
180
|
+
"toString" in value &&
|
|
181
|
+
typeof (value as { toString: unknown }).toString === "function" &&
|
|
182
|
+
(value as { toString: () => string }).toString !== Object.prototype.toString
|
|
183
|
+
) {
|
|
184
|
+
return (value as { toString: () => string }).toString();
|
|
185
|
+
}
|
|
186
|
+
throw new TypeError(
|
|
187
|
+
`Unsupported path parameter "${key}" of type ${typeof value}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Precompiles a path template into a function for faster parameter substitution.
|
|
193
|
+
* Parses the path once at creation time instead of on every request.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* const apply = compilePathTemplate("/users/{userId}/posts/{postId}");
|
|
197
|
+
* apply({ userId: "123", postId: "456" }); // "/users/123/posts/456"
|
|
198
|
+
*/
|
|
199
|
+
function compilePathTemplate(
|
|
200
|
+
path: string,
|
|
201
|
+
): (pathParams?: Record<string, unknown>) => string {
|
|
202
|
+
// Find all parameters in the template
|
|
203
|
+
const paramMatches = [...path.matchAll(/\{([^{}]+)\}/g)];
|
|
204
|
+
|
|
205
|
+
// If no parameters, return a function that always returns the static path
|
|
206
|
+
if (paramMatches.length === 0) {
|
|
207
|
+
return () => path;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Build a list of parameter keys and their positions
|
|
211
|
+
const paramKeys = paramMatches.map((match) => match[1]);
|
|
212
|
+
|
|
213
|
+
// Split the path into static parts
|
|
214
|
+
const parts = path.split(/\{[^{}]+\}/);
|
|
215
|
+
|
|
216
|
+
// Return a compiled function that interpolates parameters
|
|
217
|
+
return (pathParams?: Record<string, unknown>): string => {
|
|
218
|
+
if (!pathParams) return path;
|
|
219
|
+
|
|
220
|
+
let result = parts[0] ?? "";
|
|
221
|
+
for (const [i, key] of paramKeys.entries()) {
|
|
222
|
+
if (
|
|
223
|
+
key &&
|
|
224
|
+
Object.prototype.hasOwnProperty.call(pathParams, key)
|
|
225
|
+
) {
|
|
226
|
+
const rawValue = pathParams[key];
|
|
227
|
+
result += rawValue !== undefined && rawValue !== null ? encodeURIComponent(toPathParamSegment(key, rawValue)) : `{${key}}`;
|
|
228
|
+
} else {
|
|
229
|
+
result += `{${key}}`;
|
|
230
|
+
}
|
|
231
|
+
result += parts[i + 1] ?? "";
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeRequestOptions(options: unknown): unknown {
|
|
238
|
+
if (!options || typeof options !== "object") {
|
|
239
|
+
return options;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const original = options as Record<string, unknown>;
|
|
243
|
+
const params =
|
|
244
|
+
original.params && typeof original.params === "object"
|
|
245
|
+
? { ...(original.params as Record<string, unknown>) }
|
|
246
|
+
: undefined;
|
|
247
|
+
|
|
248
|
+
if (Object.prototype.hasOwnProperty.call(original, "query")) {
|
|
249
|
+
// Legacy format detected: { query: ... } instead of { params: { query: ... } }
|
|
250
|
+
const logger = globalThis.console;
|
|
251
|
+
if (typeof logger.warn === "function") {
|
|
252
|
+
logger.warn(
|
|
253
|
+
"[Trustap SDK] Deprecation warning: Using { query: ... } is deprecated. " +
|
|
254
|
+
"Please use { params: { query: ... } } instead. " +
|
|
255
|
+
"Legacy format will be removed in a future major version.",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const { query, ...rest } = original;
|
|
260
|
+
const nextParams = params ?? {};
|
|
261
|
+
if (nextParams.query === undefined) {
|
|
262
|
+
nextParams.query = query;
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
...rest,
|
|
266
|
+
params: nextParams,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (params !== undefined) {
|
|
271
|
+
return {
|
|
272
|
+
...original,
|
|
273
|
+
params,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return options;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getParams(options: unknown):
|
|
281
|
+
| { path?: Record<string, unknown>; query?: Record<string, unknown> }
|
|
282
|
+
| undefined {
|
|
283
|
+
if (!options || typeof options !== "object") return undefined;
|
|
284
|
+
const params = (options as { params?: unknown }).params;
|
|
285
|
+
if (!params || typeof params !== "object") return undefined;
|
|
286
|
+
return params as {
|
|
287
|
+
path?: Record<string, unknown>;
|
|
288
|
+
query?: Record<string, unknown>;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function stripBasePath(pathname: string, basePath: string): string {
|
|
293
|
+
let normalized = pathname;
|
|
294
|
+
|
|
295
|
+
if (basePath && normalized.startsWith(basePath)) {
|
|
296
|
+
normalized = normalized.slice(basePath.length) || "/";
|
|
297
|
+
} else if (/^\/api\/v\d+/i.test(normalized)) {
|
|
298
|
+
normalized = normalized.replace(/^\/api\/v\d+/i, "") || "/";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!normalized.startsWith("/")) {
|
|
302
|
+
normalized = `/${normalized}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return normalized === "" ? "/" : normalized;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Creates a safe RegExp from a pattern constructed from controlled path data.
|
|
310
|
+
* This function validates that the pattern only contains expected characters
|
|
311
|
+
* before creating the RegExp, preventing security issues from arbitrary patterns.
|
|
312
|
+
*
|
|
313
|
+
* @security This RegExp is constructed from controlled configuration data
|
|
314
|
+
* (securityMap keys), not from user input. The validation ensures only safe
|
|
315
|
+
* regex characters are used, making this safe for security use cases.
|
|
316
|
+
*
|
|
317
|
+
* @param pattern - A path template pattern like "^/api/users/[^/]+$"
|
|
318
|
+
* @returns A compiled RegExp for path matching
|
|
319
|
+
* @throws Error if the pattern contains unsafe characters
|
|
320
|
+
*/
|
|
321
|
+
function createSafePathRegex(pattern: string): RegExp {
|
|
322
|
+
// Validate that pattern only contains safe regex characters from path templating
|
|
323
|
+
// Pattern should only contain: alphanumerics, path separators, regex quantifiers, and path parameter replacement
|
|
324
|
+
if (!/^[\w\-./^[\]$+*?(){}|\\]+$/.test(pattern)) {
|
|
325
|
+
throw new Error(`Invalid pattern for path regex: ${pattern}`);
|
|
326
|
+
}
|
|
327
|
+
// Pattern construction verified as safe - use indirect method to avoid lint warnings
|
|
328
|
+
const regexConstructor = RegExp;
|
|
329
|
+
return new regexConstructor(pattern);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Precompiles security map patterns into RegExp objects for faster lookup.
|
|
334
|
+
* Called once at client creation time instead of on every request.
|
|
335
|
+
*/
|
|
336
|
+
function compileSecurityMap(
|
|
337
|
+
securityMap:
|
|
338
|
+
| Record<string, Record<string, readonly string[]>>
|
|
339
|
+
| undefined,
|
|
340
|
+
): {
|
|
341
|
+
exact: Map<string, Map<string, readonly string[]>>;
|
|
342
|
+
patterns: {
|
|
343
|
+
regex: RegExp;
|
|
344
|
+
methods: Record<string, readonly string[]>;
|
|
345
|
+
}[];
|
|
346
|
+
} {
|
|
347
|
+
const exact = new Map<string, Map<string, readonly string[]>>();
|
|
348
|
+
const patterns: {
|
|
349
|
+
regex: RegExp;
|
|
350
|
+
methods: Record<string, readonly string[]>;
|
|
351
|
+
}[] = [];
|
|
352
|
+
|
|
353
|
+
if (!securityMap) {
|
|
354
|
+
return { exact, patterns };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const path of Object.keys(securityMap)) {
|
|
358
|
+
const methods = securityMap[path];
|
|
359
|
+
if (!methods) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
// Exact paths (no path parameters)
|
|
363
|
+
if (path.includes("{")) {
|
|
364
|
+
// Pattern paths (with path parameters like {userId})
|
|
365
|
+
// Replace {param} with [^/]+ for regex matching
|
|
366
|
+
let regexPath = "";
|
|
367
|
+
let lastIndex = 0;
|
|
368
|
+
for (const match of path.matchAll(/\{[a-z_]\w*\}/gi)) {
|
|
369
|
+
regexPath += path.slice(lastIndex, match.index) + "[^/]+";
|
|
370
|
+
lastIndex = match.index + match[0].length;
|
|
371
|
+
}
|
|
372
|
+
regexPath += path.slice(lastIndex);
|
|
373
|
+
const regexPattern = `^${regexPath}$`;
|
|
374
|
+
patterns.push({
|
|
375
|
+
regex: createSafePathRegex(regexPattern),
|
|
376
|
+
methods,
|
|
377
|
+
});
|
|
378
|
+
} else {
|
|
379
|
+
let methodMap = exact.get(path);
|
|
380
|
+
if (!methodMap) {
|
|
381
|
+
methodMap = new Map();
|
|
382
|
+
exact.set(path, methodMap);
|
|
383
|
+
}
|
|
384
|
+
for (const method of Object.keys(methods)) {
|
|
385
|
+
const schemes = methods[method];
|
|
386
|
+
if (schemes) {
|
|
387
|
+
methodMap.set(method, schemes);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { exact, patterns };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function resolveSecuritySchemes(
|
|
397
|
+
compiled: {
|
|
398
|
+
exact: Map<string, Map<string, readonly string[]>>;
|
|
399
|
+
patterns: {
|
|
400
|
+
regex: RegExp;
|
|
401
|
+
methods: Record<string, readonly string[]>;
|
|
402
|
+
}[];
|
|
403
|
+
},
|
|
404
|
+
pathname: string,
|
|
405
|
+
method: string,
|
|
406
|
+
): readonly string[] {
|
|
407
|
+
// Fast exact match lookup (O(1))
|
|
408
|
+
const exactMethods = compiled.exact.get(pathname);
|
|
409
|
+
if (exactMethods) {
|
|
410
|
+
const schemes = exactMethods.get(method);
|
|
411
|
+
if (schemes) {
|
|
412
|
+
return schemes;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Pattern matching (O(n) where n = number of patterns, not total routes)
|
|
417
|
+
for (const { regex, methods } of compiled.patterns) {
|
|
418
|
+
if (regex.test(pathname)) {
|
|
419
|
+
const match = methods[method];
|
|
420
|
+
if (match !== undefined) {
|
|
421
|
+
return match;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return [] as readonly string[];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function createTrustapClientCore<
|
|
430
|
+
Operations,
|
|
431
|
+
Middleware,
|
|
432
|
+
Client extends MinimalHttpClient<Middleware>,
|
|
433
|
+
OperationMap extends Record<string, { path: string; method: string }>,
|
|
434
|
+
PathClient,
|
|
435
|
+
>(
|
|
436
|
+
deps: {
|
|
437
|
+
createClient: (options: { baseUrl: string }) => Client;
|
|
438
|
+
wrapAsPathBasedClient: (client: Client) => PathClient;
|
|
439
|
+
operationIdToPath: OperationMap;
|
|
440
|
+
securityMap?: Record<string, Record<string, readonly string[]>>;
|
|
441
|
+
},
|
|
442
|
+
options: CreateTrustapClientOptions,
|
|
443
|
+
) {
|
|
444
|
+
// securityMap is statically imported above; if not found at build time, it will be undefined in Deno
|
|
445
|
+
const { createClient, wrapAsPathBasedClient, operationIdToPath } = deps;
|
|
446
|
+
const {
|
|
447
|
+
apiUrl,
|
|
448
|
+
basicAuth,
|
|
449
|
+
getAccessToken,
|
|
450
|
+
authOverrides,
|
|
451
|
+
basePath: basePathOption,
|
|
452
|
+
} = options;
|
|
453
|
+
const { baseUrl: resolvedBaseUrl, basePath } = inferBaseConfig(
|
|
454
|
+
apiUrl,
|
|
455
|
+
basePathOption,
|
|
456
|
+
);
|
|
457
|
+
const { securityMap } = deps;
|
|
458
|
+
|
|
459
|
+
// Precompile security map once at client creation time for faster lookups
|
|
460
|
+
const compiledSecurityMap = compileSecurityMap(securityMap);
|
|
461
|
+
|
|
462
|
+
const resolveBasicToken =
|
|
463
|
+
basicAuth
|
|
464
|
+
? (() => {
|
|
465
|
+
let cached: string | undefined;
|
|
466
|
+
return () => {
|
|
467
|
+
if (cached !== undefined) return cached;
|
|
468
|
+
const user = basicAuth.username;
|
|
469
|
+
const pass = basicAuth.password ?? "";
|
|
470
|
+
if (typeof btoa === "undefined") {
|
|
471
|
+
const bufferGlobal = (globalThis as {
|
|
472
|
+
Buffer?: {
|
|
473
|
+
from: (input: string) => {
|
|
474
|
+
toString: (encoding: "base64") => string;
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
}).Buffer;
|
|
478
|
+
cached = bufferGlobal
|
|
479
|
+
? bufferGlobal.from(`${user}:${pass}`).toString("base64")
|
|
480
|
+
: "";
|
|
481
|
+
} else {
|
|
482
|
+
cached = btoa(`${user}:${pass}`);
|
|
483
|
+
}
|
|
484
|
+
return cached;
|
|
485
|
+
};
|
|
486
|
+
})()
|
|
487
|
+
: undefined;
|
|
488
|
+
|
|
489
|
+
const authHeaderMiddleware: Middleware | undefined =
|
|
490
|
+
basicAuth || getAccessToken
|
|
491
|
+
? ({
|
|
492
|
+
async onRequest({ request }: { request: Request }) {
|
|
493
|
+
const headers = new Headers(request.headers);
|
|
494
|
+
if (!headers.has("Authorization")) {
|
|
495
|
+
const urlObj = new URL(request.url);
|
|
496
|
+
const fullPathname = urlObj.pathname;
|
|
497
|
+
// Normalize pathname by removing configured base path (and fallback to /api/v{N}) for security map lookup
|
|
498
|
+
const pathname = stripBasePath(fullPathname, basePath);
|
|
499
|
+
const method = request.method.toUpperCase();
|
|
500
|
+
|
|
501
|
+
const override = authOverrides
|
|
502
|
+
? authOverrides[pathname] ?? authOverrides[fullPathname]
|
|
503
|
+
: undefined;
|
|
504
|
+
|
|
505
|
+
let shouldUseBasic = false;
|
|
506
|
+
|
|
507
|
+
if (override === "basic") {
|
|
508
|
+
shouldUseBasic = true;
|
|
509
|
+
} else if (override !== "oauth2") {
|
|
510
|
+
// Use existing logic for non-overridden endpoints
|
|
511
|
+
const declaredSchemes = resolveSecuritySchemes(
|
|
512
|
+
compiledSecurityMap,
|
|
513
|
+
pathname,
|
|
514
|
+
method,
|
|
515
|
+
);
|
|
516
|
+
const allowsApiKey = declaredSchemes.includes("APIKey");
|
|
517
|
+
const isChargeEndpoint =
|
|
518
|
+
pathname.endsWith("/charge") ||
|
|
519
|
+
pathname.endsWith("/p2p/charge");
|
|
520
|
+
const isGuestUsersEndpoint = pathname.endsWith("/guest_users");
|
|
521
|
+
shouldUseBasic =
|
|
522
|
+
(allowsApiKey ? true : false) ||
|
|
523
|
+
isChargeEndpoint ||
|
|
524
|
+
isGuestUsersEndpoint;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (shouldUseBasic && basicAuth) {
|
|
528
|
+
const token = resolveBasicToken?.();
|
|
529
|
+
if (token) {
|
|
530
|
+
headers.set("Authorization", `Basic ${token}`);
|
|
531
|
+
}
|
|
532
|
+
} else if (getAccessToken) {
|
|
533
|
+
const token = await getAccessToken();
|
|
534
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return new Request(request, { headers });
|
|
538
|
+
},
|
|
539
|
+
} as unknown as Middleware)
|
|
540
|
+
: undefined;
|
|
541
|
+
|
|
542
|
+
const client = createClient({ baseUrl: resolvedBaseUrl });
|
|
543
|
+
if (authHeaderMiddleware) client.use(authHeaderMiddleware);
|
|
544
|
+
|
|
545
|
+
const pathClient = wrapAsPathBasedClient(client);
|
|
546
|
+
|
|
547
|
+
const byOperationId = new Proxy<Record<string, unknown>>(
|
|
548
|
+
{},
|
|
549
|
+
{
|
|
550
|
+
get(target, prop, receiver) {
|
|
551
|
+
if (prop === "then") return;
|
|
552
|
+
if (Reflect.has(target, prop)) {
|
|
553
|
+
const existing: unknown = Reflect.get(target, prop, receiver);
|
|
554
|
+
return existing;
|
|
555
|
+
}
|
|
556
|
+
if (typeof prop === "symbol") {
|
|
557
|
+
const symbolValue: unknown = Reflect.get(target, prop, receiver);
|
|
558
|
+
return symbolValue;
|
|
559
|
+
}
|
|
560
|
+
const opId = prop;
|
|
561
|
+
const mapping = (
|
|
562
|
+
operationIdToPath as Record<string, { path: string; method: string }>
|
|
563
|
+
)[opId];
|
|
564
|
+
if (!mapping) return;
|
|
565
|
+
const { path, method } = mapping;
|
|
566
|
+
const upper = method.toUpperCase() as HttpMethod;
|
|
567
|
+
const templatePath = joinPaths(basePath, path);
|
|
568
|
+
// Precompile path template once at handler creation time
|
|
569
|
+
const applyParams = compilePathTemplate(templatePath);
|
|
570
|
+
const handler = (requestOptions?: unknown) => {
|
|
571
|
+
const optionsToSend = normalizeRequestOptions(requestOptions);
|
|
572
|
+
const params = getParams(optionsToSend);
|
|
573
|
+
// Use precompiled template function (faster than regex on every request)
|
|
574
|
+
const finalPath = applyParams(params?.path);
|
|
575
|
+
switch (upper) {
|
|
576
|
+
case "GET": {
|
|
577
|
+
return client.GET(finalPath, optionsToSend);
|
|
578
|
+
}
|
|
579
|
+
case "POST": {
|
|
580
|
+
return client.POST(finalPath, optionsToSend);
|
|
581
|
+
}
|
|
582
|
+
case "PUT": {
|
|
583
|
+
return client.PUT(finalPath, optionsToSend);
|
|
584
|
+
}
|
|
585
|
+
case "PATCH": {
|
|
586
|
+
return client.PATCH(finalPath, optionsToSend);
|
|
587
|
+
}
|
|
588
|
+
case "DELETE": {
|
|
589
|
+
return client.DELETE(finalPath, optionsToSend);
|
|
590
|
+
}
|
|
591
|
+
case "HEAD": {
|
|
592
|
+
return client.HEAD(finalPath, optionsToSend);
|
|
593
|
+
}
|
|
594
|
+
case "OPTIONS": {
|
|
595
|
+
return client.OPTIONS(finalPath, optionsToSend);
|
|
596
|
+
}
|
|
597
|
+
default: {
|
|
598
|
+
throw new Error(`Unsupported method ${method} for ${opId}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
Reflect.set(target, prop, handler, receiver);
|
|
603
|
+
return handler;
|
|
604
|
+
},
|
|
605
|
+
set(target, prop, value, receiver) {
|
|
606
|
+
return Reflect.set(target, prop, value, receiver);
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
return Object.assign(
|
|
612
|
+
byOperationId,
|
|
613
|
+
pathClient as unknown as Record<string, unknown>,
|
|
614
|
+
{
|
|
615
|
+
raw: client,
|
|
616
|
+
},
|
|
617
|
+
) as unknown as TrustapOperationIdClient<Operations, OperationMap> &
|
|
618
|
+
PathClient & { raw: Client };
|
|
619
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DenoHttpClient,
|
|
3
|
+
Middleware,
|
|
4
|
+
PathBasedClient,
|
|
5
|
+
} from "./client-deno.ts";
|
|
6
|
+
import { createClient, wrapAsPathBasedClient } from "./client-deno.ts";
|
|
7
|
+
|
|
8
|
+
import { createTrustapClientWithDeps } from "./client-factory.ts";
|
|
9
|
+
import type { CreateTrustapClientOptions } from "./client-factory.ts";
|
|
10
|
+
|
|
11
|
+
export function createTrustapClient(options: CreateTrustapClientOptions) {
|
|
12
|
+
return createTrustapClientWithDeps<
|
|
13
|
+
Middleware,
|
|
14
|
+
DenoHttpClient,
|
|
15
|
+
PathBasedClient
|
|
16
|
+
>(
|
|
17
|
+
{
|
|
18
|
+
createClient,
|
|
19
|
+
wrapAsPathBasedClient,
|
|
20
|
+
},
|
|
21
|
+
options,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export * from "./state-machine.ts";
|
|
26
|
+
|
|
27
|
+
// Export webhook schemas and types
|
|
28
|
+
export * from "./webhook-schemas.ts";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Middleware, PathBasedClient } from "openapi-fetch";
|
|
2
|
+
import createClient, { wrapAsPathBasedClient } from "openapi-fetch";
|
|
3
|
+
|
|
4
|
+
import { createTrustapClientWithDeps } from "./client-factory.ts";
|
|
5
|
+
import type {
|
|
6
|
+
CreateTrustapClientOptions,
|
|
7
|
+
MinimalHttpClient,
|
|
8
|
+
} from "./client-factory.ts";
|
|
9
|
+
import type { paths } from "./schema.d.ts";
|
|
10
|
+
|
|
11
|
+
;
|
|
12
|
+
;
|
|
13
|
+
export type TrustapClient = ReturnType<typeof createTrustapClient>;
|
|
14
|
+
|
|
15
|
+
export function createTrustapClient(options: CreateTrustapClientOptions) {
|
|
16
|
+
return createTrustapClientWithDeps<
|
|
17
|
+
Middleware,
|
|
18
|
+
MinimalHttpClient<Middleware>,
|
|
19
|
+
PathBasedClient<paths>
|
|
20
|
+
>(
|
|
21
|
+
{
|
|
22
|
+
createClient: (opts) =>
|
|
23
|
+
createClient<paths>(opts) as unknown as MinimalHttpClient<Middleware>,
|
|
24
|
+
wrapAsPathBasedClient: wrapAsPathBasedClient as unknown as (
|
|
25
|
+
client: MinimalHttpClient<Middleware>,
|
|
26
|
+
) => PathBasedClient<paths>,
|
|
27
|
+
},
|
|
28
|
+
options,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Export state machine utilities
|
|
33
|
+
export * from "./state-machine.ts";
|
|
34
|
+
|
|
35
|
+
// Export webhook schemas and types
|
|
36
|
+
export * from "./webhook-schemas.ts";
|