@tetrascience-npm/request 0.2.0-beta.106.2 → 0.2.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/dist/cli/generate-client.js +0 -0
- package/dist/cli/generate-schemas.js +0 -0
- package/dist/cli/init-client.d.ts +3 -0
- package/dist/cli/init-client.d.ts.map +1 -0
- package/dist/cli/init-client.js +133 -0
- package/dist/client/install-tracking.d.ts +18 -0
- package/dist/client/install-tracking.d.ts.map +1 -0
- package/dist/client/install-tracking.js +88 -0
- package/dist/client/middleware.d.ts +18 -0
- package/dist/client/middleware.d.ts.map +1 -0
- package/dist/client/middleware.js +71 -0
- package/dist/client/sanitize-url.d.ts +3 -0
- package/dist/client/sanitize-url.d.ts.map +1 -0
- package/dist/client/sanitize-url.js +14 -0
- package/dist/server/express-middleware.d.ts +41 -0
- package/dist/server/express-middleware.d.ts.map +1 -0
- package/dist/server/express-middleware.js +52 -0
- package/dist/server/middleware.d.ts +27 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +55 -0
- package/dist/server/validation.d.ts +40 -0
- package/dist/server/validation.d.ts.map +1 -0
- package/dist/server/validation.js +45 -0
- package/dist/shared/auth-middleware.d.ts +45 -0
- package/dist/shared/auth-middleware.d.ts.map +1 -0
- package/dist/shared/auth-middleware.js +84 -0
- package/dist/shared/safe-response.d.ts +22 -0
- package/dist/shared/safe-response.d.ts.map +1 -0
- package/dist/shared/safe-response.js +41 -0
- package/dist/shared/validation.d.ts +40 -0
- package/dist/shared/validation.d.ts.map +1 -0
- package/dist/shared/validation.js +39 -0
- package/package.json +1 -1
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-client.d.ts","sourceRoot":"","sources":["../../src/cli/init-client.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* @deprecated Use `generate-service-client` instead.
|
|
39
|
+
*
|
|
40
|
+
* Scaffolds a typed OpenAPI client directory for a TetraScience service.
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* init-service-client --name @tetrascience/my-service-client --spec ../openapi/spec.yaml [--out client]
|
|
44
|
+
*/
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const templates_1 = require("./templates");
|
|
48
|
+
function parseArgs() {
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
let packageName;
|
|
51
|
+
let specPath;
|
|
52
|
+
let outDir = 'client';
|
|
53
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
|
+
if (args[i] === '--name' && args[i + 1]) {
|
|
55
|
+
packageName = args[++i];
|
|
56
|
+
}
|
|
57
|
+
else if (args[i] === '--spec' && args[i + 1]) {
|
|
58
|
+
specPath = args[++i];
|
|
59
|
+
}
|
|
60
|
+
else if (args[i] === '--out' && args[i + 1]) {
|
|
61
|
+
outDir = args[++i];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!packageName) {
|
|
65
|
+
console.error('Error: --name <@tetrascience/my-service-client> is required');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (!specPath) {
|
|
69
|
+
console.error('Error: --spec <relative-path-to-openapi-yaml> is required');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
return { packageName, specPath, outDir };
|
|
73
|
+
}
|
|
74
|
+
function generatePackageJson(packageName, specPath) {
|
|
75
|
+
const pkg = {
|
|
76
|
+
name: packageName,
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
description: `Typed client for the ${(0, templates_1.deriveServiceName)(packageName)} service API`,
|
|
79
|
+
main: 'dist/index.js',
|
|
80
|
+
types: 'dist/index.d.ts',
|
|
81
|
+
files: ['dist'],
|
|
82
|
+
scripts: {
|
|
83
|
+
generate: `openapi-typescript ${specPath} -o generated/schema.ts`,
|
|
84
|
+
'generate:schemas': `generate-request-schemas --spec ${specPath} --out generated`,
|
|
85
|
+
build: 'yarn generate && yarn generate:schemas && tsc --project tsconfig.build.json',
|
|
86
|
+
},
|
|
87
|
+
dependencies: {
|
|
88
|
+
'@tetrascience/request-middleware': '^0.2.0',
|
|
89
|
+
'openapi-fetch': '^0.17.0',
|
|
90
|
+
zod: '^3.24.0',
|
|
91
|
+
},
|
|
92
|
+
devDependencies: {
|
|
93
|
+
'@types/js-yaml': '^4.0.9',
|
|
94
|
+
'js-yaml': '^4.1.1',
|
|
95
|
+
'openapi-typescript': '^7.6.0',
|
|
96
|
+
'openapi-zod-client': '^1.18.3',
|
|
97
|
+
'ts-node': '^10.9.2',
|
|
98
|
+
typescript: '^5.7.3',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
102
|
+
}
|
|
103
|
+
function main() {
|
|
104
|
+
console.warn('WARNING: init-service-client is deprecated. Use generate-service-client instead.\n');
|
|
105
|
+
const { packageName, specPath, outDir } = parseArgs();
|
|
106
|
+
const resolvedOut = path.resolve(outDir);
|
|
107
|
+
const serviceName = (0, templates_1.deriveServiceName)(packageName);
|
|
108
|
+
if (fs.existsSync(resolvedOut) && fs.readdirSync(resolvedOut).length > 0) {
|
|
109
|
+
console.error(`Error: output directory "${outDir}" already exists and is not empty`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
fs.mkdirSync(resolvedOut, { recursive: true });
|
|
113
|
+
fs.mkdirSync(path.join(resolvedOut, 'generated'), { recursive: true });
|
|
114
|
+
const files = {
|
|
115
|
+
'package.json': generatePackageJson(packageName, specPath),
|
|
116
|
+
'tsconfig.build.json': (0, templates_1.generateTsConfig)(),
|
|
117
|
+
'index.ts': (0, templates_1.generateIndexTs)(serviceName),
|
|
118
|
+
'.gitignore': (0, templates_1.generateClientGitIgnore)(),
|
|
119
|
+
};
|
|
120
|
+
for (const [name, content] of Object.entries(files)) {
|
|
121
|
+
const filePath = path.join(resolvedOut, name);
|
|
122
|
+
fs.writeFileSync(filePath, content);
|
|
123
|
+
console.log(` created ${path.relative(process.cwd(), filePath)}`);
|
|
124
|
+
}
|
|
125
|
+
console.log(`\nScaffolded ${packageName} in ${outDir}/`);
|
|
126
|
+
console.log(`\nNext steps:`);
|
|
127
|
+
console.log(` cd ${outDir}`);
|
|
128
|
+
console.log(` yarn install`);
|
|
129
|
+
console.log(` yarn build`);
|
|
130
|
+
}
|
|
131
|
+
if (require.main === module) {
|
|
132
|
+
main();
|
|
133
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ClientTrackingOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Install request tracking on the global `fetch` function.
|
|
4
|
+
*
|
|
5
|
+
* Every `fetch()` call will automatically get a `ts-request-id` header
|
|
6
|
+
* and optional logging. Returns an uninstall function to restore the
|
|
7
|
+
* original `fetch`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import {installRequestMiddleware, createConsoleLogger} from '@tetrascience/request-middleware/client';
|
|
12
|
+
*
|
|
13
|
+
* installRequestMiddleware({logger: createConsoleLogger({prefix: 'my-app'})});
|
|
14
|
+
* // All fetch() calls now include ts-request-id
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function installRequestMiddleware(options?: ClientTrackingOptions): () => void;
|
|
18
|
+
//# sourceMappingURL=install-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-tracking.d.ts","sourceRoot":"","sources":["../../src/client/install-tracking.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,SAAS,CAAC;AA8BnD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,IAAI,CAoDpF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installRequestMiddleware = installRequestMiddleware;
|
|
4
|
+
const constants_1 = require("../shared/constants");
|
|
5
|
+
const generate_request_id_1 = require("../shared/generate-request-id");
|
|
6
|
+
function sanitizeUrl(url) {
|
|
7
|
+
try {
|
|
8
|
+
return new URL(url).pathname;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return url.split(/[?#]/)[0];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const SESSION_COOKIE_MAX_AGE = 30 * 60; // 30 minutes, refreshed on each request
|
|
15
|
+
/**
|
|
16
|
+
* Get or create a session ID from cookies.
|
|
17
|
+
* Creates a new UUID and stores it as a cookie if not already present.
|
|
18
|
+
* Refreshes the cookie expiry on each call (sliding window).
|
|
19
|
+
*/
|
|
20
|
+
function getOrCreateSessionId() {
|
|
21
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${constants_1.SESSION_ID_HEADER}=([^;]*)`));
|
|
22
|
+
if (match) {
|
|
23
|
+
// Refresh expiry
|
|
24
|
+
document.cookie = `${constants_1.SESSION_ID_HEADER}=${match[1]}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
25
|
+
return match[1];
|
|
26
|
+
}
|
|
27
|
+
const sessionId = (0, generate_request_id_1.generateRequestId)();
|
|
28
|
+
document.cookie = `${constants_1.SESSION_ID_HEADER}=${sessionId}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
29
|
+
return sessionId;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Install request tracking on the global `fetch` function.
|
|
33
|
+
*
|
|
34
|
+
* Every `fetch()` call will automatically get a `ts-request-id` header
|
|
35
|
+
* and optional logging. Returns an uninstall function to restore the
|
|
36
|
+
* original `fetch`.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import {installRequestMiddleware, createConsoleLogger} from '@tetrascience/request-middleware/client';
|
|
41
|
+
*
|
|
42
|
+
* installRequestMiddleware({logger: createConsoleLogger({prefix: 'my-app'})});
|
|
43
|
+
* // All fetch() calls now include ts-request-id
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function installRequestMiddleware(options) {
|
|
47
|
+
const { logger } = options ?? {};
|
|
48
|
+
const originalFetch = globalThis.fetch;
|
|
49
|
+
globalThis.fetch = (input, init) => {
|
|
50
|
+
// Merge headers: start from the Request's headers (if input is a Request),
|
|
51
|
+
// then layer on init?.headers, then add tracking headers.
|
|
52
|
+
const inputHeaders = input instanceof Request ? input.headers : undefined;
|
|
53
|
+
const headers = new Headers(inputHeaders);
|
|
54
|
+
if (init?.headers) {
|
|
55
|
+
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
56
|
+
}
|
|
57
|
+
if (!headers.has(constants_1.REQUEST_ID_HEADER)) {
|
|
58
|
+
headers.set(constants_1.REQUEST_ID_HEADER, (0, generate_request_id_1.generateRequestId)());
|
|
59
|
+
}
|
|
60
|
+
const requestId = headers.get(constants_1.REQUEST_ID_HEADER);
|
|
61
|
+
if (!headers.has(constants_1.SESSION_ID_HEADER) && typeof document !== 'undefined') {
|
|
62
|
+
headers.set(constants_1.SESSION_ID_HEADER, getOrCreateSessionId());
|
|
63
|
+
}
|
|
64
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
65
|
+
logger?.debug('Outgoing request', {
|
|
66
|
+
requestId,
|
|
67
|
+
path: sanitizeUrl(url),
|
|
68
|
+
});
|
|
69
|
+
return originalFetch(input, { ...init, headers }).then((response) => {
|
|
70
|
+
logger?.debug('Response received', {
|
|
71
|
+
requestId,
|
|
72
|
+
path: sanitizeUrl(url),
|
|
73
|
+
status: response.status,
|
|
74
|
+
});
|
|
75
|
+
return response;
|
|
76
|
+
}, (error) => {
|
|
77
|
+
logger?.error('Request failed', {
|
|
78
|
+
requestId,
|
|
79
|
+
path: sanitizeUrl(url),
|
|
80
|
+
error: error instanceof Error ? error.message : String(error),
|
|
81
|
+
});
|
|
82
|
+
throw error;
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
return () => {
|
|
86
|
+
globalThis.fetch = originalFetch;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Middleware } from 'openapi-fetch';
|
|
2
|
+
import type { ClientTrackingOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Create request tracking middleware for openapi-fetch clients (browser).
|
|
5
|
+
*
|
|
6
|
+
* Composes tracing (request ID injection) with request/response logging.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import createClient from 'openapi-fetch';
|
|
11
|
+
* import {createRequestTrackingMiddleware} from '@tetrascience/request-middleware/client';
|
|
12
|
+
*
|
|
13
|
+
* const client = createClient<paths>({baseUrl: '/api'});
|
|
14
|
+
* client.use(createRequestTrackingMiddleware());
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function createRequestTrackingMiddleware(options?: ClientTrackingOptions): Middleware;
|
|
18
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/client/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAK9C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,SAAS,CAAC;AAWnD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,UAAU,CAkD3F"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequestTrackingMiddleware = createRequestTrackingMiddleware;
|
|
4
|
+
const constants_1 = require("../shared/constants");
|
|
5
|
+
const tracing_1 = require("../shared/middleware/tracing");
|
|
6
|
+
const sanitize_url_1 = require("./sanitize-url");
|
|
7
|
+
/**
|
|
8
|
+
* Read ts-session-id from cookies if available (browser only).
|
|
9
|
+
*/
|
|
10
|
+
function getSessionIdFromCookie() {
|
|
11
|
+
if (typeof document === 'undefined')
|
|
12
|
+
return undefined;
|
|
13
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${constants_1.SESSION_ID_HEADER}=([^;]*)`));
|
|
14
|
+
return match?.[1];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create request tracking middleware for openapi-fetch clients (browser).
|
|
18
|
+
*
|
|
19
|
+
* Composes tracing (request ID injection) with request/response logging.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import createClient from 'openapi-fetch';
|
|
24
|
+
* import {createRequestTrackingMiddleware} from '@tetrascience/request-middleware/client';
|
|
25
|
+
*
|
|
26
|
+
* const client = createClient<paths>({baseUrl: '/api'});
|
|
27
|
+
* client.use(createRequestTrackingMiddleware());
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function createRequestTrackingMiddleware(options) {
|
|
31
|
+
const { logger } = options ?? {};
|
|
32
|
+
const tracingMiddleware = (0, tracing_1.createTracingMiddleware)({
|
|
33
|
+
requestId: options?.requestId,
|
|
34
|
+
sessionId: options?.sessionId ?? getSessionIdFromCookie,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
onRequest(params) {
|
|
38
|
+
// Delegate header injection to shared tracing middleware
|
|
39
|
+
tracingMiddleware.onRequest(params);
|
|
40
|
+
const { request, schemaPath } = params;
|
|
41
|
+
const requestId = request.headers.get(constants_1.REQUEST_ID_HEADER);
|
|
42
|
+
logger?.debug('Outgoing request', {
|
|
43
|
+
requestId,
|
|
44
|
+
method: request.method,
|
|
45
|
+
path: (0, sanitize_url_1.sanitizeUrl)(request.url),
|
|
46
|
+
schemaPath,
|
|
47
|
+
});
|
|
48
|
+
return request;
|
|
49
|
+
},
|
|
50
|
+
onResponse({ request, response, schemaPath }) {
|
|
51
|
+
const requestId = request.headers.get(constants_1.REQUEST_ID_HEADER);
|
|
52
|
+
logger?.debug('Response received', {
|
|
53
|
+
requestId,
|
|
54
|
+
method: request.method,
|
|
55
|
+
path: (0, sanitize_url_1.sanitizeUrl)(request.url),
|
|
56
|
+
schemaPath,
|
|
57
|
+
status: response.status,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
onError({ request, error, schemaPath }) {
|
|
61
|
+
const requestId = request.headers.get(constants_1.REQUEST_ID_HEADER);
|
|
62
|
+
logger?.error('Request failed', {
|
|
63
|
+
requestId,
|
|
64
|
+
method: request.method,
|
|
65
|
+
path: (0, sanitize_url_1.sanitizeUrl)(request.url),
|
|
66
|
+
schemaPath,
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-url.d.ts","sourceRoot":"","sources":["../../src/client/sanitize-url.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ/C"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeUrl = sanitizeUrl;
|
|
4
|
+
/** Extract pathname from a URL, stripping query string and hash. */
|
|
5
|
+
function sanitizeUrl(url) {
|
|
6
|
+
try {
|
|
7
|
+
// Try absolute URL first
|
|
8
|
+
return new URL(url).pathname;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// Relative URL — strip query string and hash manually
|
|
12
|
+
return url.split(/[?#]/)[0];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Express-compatible types so we don't depend on @types/express.
|
|
3
|
+
*/
|
|
4
|
+
interface IncomingRequest {
|
|
5
|
+
headers: Record<string, string | string[] | undefined>;
|
|
6
|
+
cookies?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
interface ServerResponse {
|
|
9
|
+
setHeader?(name: string, value: string): void;
|
|
10
|
+
}
|
|
11
|
+
type NextFunction = () => void;
|
|
12
|
+
type ExpressMiddleware = (req: IncomingRequest, res: ServerResponse, next: NextFunction) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Express middleware that sets up request context for tracing and auth.
|
|
15
|
+
*
|
|
16
|
+
* Reads from incoming headers and cookies:
|
|
17
|
+
* - `ts-request-id` — generates UUID if missing
|
|
18
|
+
* - `ts-session-id` — generates UUID if missing, sets cookie for persistence
|
|
19
|
+
* - `x-org-slug` — from header or cookie
|
|
20
|
+
* - `ts-auth-token` — from header or cookie
|
|
21
|
+
*
|
|
22
|
+
* All downstream code can access the context via `getRequestContext()`.
|
|
23
|
+
* Generated clients auto-resolve auth and tracing from this context.
|
|
24
|
+
*
|
|
25
|
+
* Requires `cookie-parser` middleware to run before this middleware
|
|
26
|
+
* so that `req.cookies` is populated.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import express from 'express';
|
|
31
|
+
* import cookieParser from 'cookie-parser';
|
|
32
|
+
* import { createRequestMiddleware } from '@tetrascience/request-middleware/server';
|
|
33
|
+
*
|
|
34
|
+
* const app = express();
|
|
35
|
+
* app.use(cookieParser());
|
|
36
|
+
* app.use(createRequestMiddleware());
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function createRequestMiddleware(): ExpressMiddleware;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=express-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express-middleware.d.ts","sourceRoot":"","sources":["../../src/server/express-middleware.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,UAAU,eAAe;IACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AACD,UAAU,cAAc;IACvB,SAAS,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9C;AACD,KAAK,YAAY,GAAG,MAAM,IAAI,CAAC;AAC/B,KAAK,iBAAiB,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;AAIjG;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,uBAAuB,IAAI,iBAAiB,CAwB3D"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequestMiddleware = createRequestMiddleware;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const constants_1 = require("../shared/constants");
|
|
6
|
+
const request_context_1 = require("./request-context");
|
|
7
|
+
const SESSION_COOKIE_MAX_AGE = 30 * 60; // 30 minutes, matching the browser cookie
|
|
8
|
+
/**
|
|
9
|
+
* Express middleware that sets up request context for tracing and auth.
|
|
10
|
+
*
|
|
11
|
+
* Reads from incoming headers and cookies:
|
|
12
|
+
* - `ts-request-id` — generates UUID if missing
|
|
13
|
+
* - `ts-session-id` — generates UUID if missing, sets cookie for persistence
|
|
14
|
+
* - `x-org-slug` — from header or cookie
|
|
15
|
+
* - `ts-auth-token` — from header or cookie
|
|
16
|
+
*
|
|
17
|
+
* All downstream code can access the context via `getRequestContext()`.
|
|
18
|
+
* Generated clients auto-resolve auth and tracing from this context.
|
|
19
|
+
*
|
|
20
|
+
* Requires `cookie-parser` middleware to run before this middleware
|
|
21
|
+
* so that `req.cookies` is populated.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import express from 'express';
|
|
26
|
+
* import cookieParser from 'cookie-parser';
|
|
27
|
+
* import { createRequestMiddleware } from '@tetrascience/request-middleware/server';
|
|
28
|
+
*
|
|
29
|
+
* const app = express();
|
|
30
|
+
* app.use(cookieParser());
|
|
31
|
+
* app.use(createRequestMiddleware());
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
function createRequestMiddleware() {
|
|
35
|
+
return (req, res, next) => {
|
|
36
|
+
const requestId = req.headers[constants_1.REQUEST_ID_HEADER] || (0, crypto_1.randomUUID)();
|
|
37
|
+
// Session: header → cookie → generate
|
|
38
|
+
let sessionId = req.headers[constants_1.SESSION_ID_HEADER]
|
|
39
|
+
|| req.cookies?.[constants_1.SESSION_ID_HEADER];
|
|
40
|
+
if (!sessionId) {
|
|
41
|
+
sessionId = (0, crypto_1.randomUUID)();
|
|
42
|
+
}
|
|
43
|
+
// Always refresh the session cookie (sliding expiry)
|
|
44
|
+
res.setHeader?.('Set-Cookie', `${constants_1.SESSION_ID_HEADER}=${sessionId}; Path=/; Max-Age=${SESSION_COOKIE_MAX_AGE}; HttpOnly; SameSite=Lax`);
|
|
45
|
+
// Auth: header → cookie (optional, may not be present for background jobs)
|
|
46
|
+
const orgSlug = req.headers[constants_1.ORG_SLUG_HEADER]
|
|
47
|
+
|| req.cookies?.[constants_1.ORG_SLUG_HEADER];
|
|
48
|
+
const authToken = req.headers[constants_1.AUTH_TOKEN_HEADER]
|
|
49
|
+
|| req.cookies?.[constants_1.AUTH_TOKEN_HEADER];
|
|
50
|
+
(0, request_context_1.runWithRequestContext)({ requestId, sessionId, orgSlug, authToken }, () => next());
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Middleware } from 'openapi-fetch';
|
|
2
|
+
import type { RequestTrackingMiddlewareOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Create openapi-fetch middleware that injects TetraScience service headers
|
|
5
|
+
* on every outgoing request.
|
|
6
|
+
*
|
|
7
|
+
* Reads request context from AsyncLocalStorage to propagate:
|
|
8
|
+
* - ts-request-id (from context or generates new UUID)
|
|
9
|
+
* - ts-initiating-service-name
|
|
10
|
+
* - ts-internal-api-key
|
|
11
|
+
* - x-org-slug (passthrough from incoming request context)
|
|
12
|
+
* - ts-auth-token (passthrough from incoming request context)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import createClient from 'openapi-fetch';
|
|
17
|
+
* import {createRequestTrackingMiddleware} from '@tetrascience/request-middleware/server';
|
|
18
|
+
*
|
|
19
|
+
* const client = createClient<paths>({baseUrl: env.DATA_APPS_ENDPOINT});
|
|
20
|
+
* client.use(createRequestTrackingMiddleware({
|
|
21
|
+
* serviceName: 'my-service',
|
|
22
|
+
* internalApiKey: process.env.INTERNAL_API_KEY,
|
|
23
|
+
* }));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createRequestTrackingMiddleware(options: RequestTrackingMiddlewareOptions): Middleware;
|
|
27
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/server/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAU9C,OAAO,KAAK,EAAC,gCAAgC,EAAC,MAAM,SAAS,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,gCAAgC,GAAG,UAAU,CA+BrG"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequestTrackingMiddleware = createRequestTrackingMiddleware;
|
|
4
|
+
const constants_1 = require("../shared/constants");
|
|
5
|
+
const request_context_1 = require("./request-context");
|
|
6
|
+
/**
|
|
7
|
+
* Create openapi-fetch middleware that injects TetraScience service headers
|
|
8
|
+
* on every outgoing request.
|
|
9
|
+
*
|
|
10
|
+
* Reads request context from AsyncLocalStorage to propagate:
|
|
11
|
+
* - ts-request-id (from context or generates new UUID)
|
|
12
|
+
* - ts-initiating-service-name
|
|
13
|
+
* - ts-internal-api-key
|
|
14
|
+
* - x-org-slug (passthrough from incoming request context)
|
|
15
|
+
* - ts-auth-token (passthrough from incoming request context)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import createClient from 'openapi-fetch';
|
|
20
|
+
* import {createRequestTrackingMiddleware} from '@tetrascience/request-middleware/server';
|
|
21
|
+
*
|
|
22
|
+
* const client = createClient<paths>({baseUrl: env.DATA_APPS_ENDPOINT});
|
|
23
|
+
* client.use(createRequestTrackingMiddleware({
|
|
24
|
+
* serviceName: 'my-service',
|
|
25
|
+
* internalApiKey: process.env.INTERNAL_API_KEY,
|
|
26
|
+
* }));
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function createRequestTrackingMiddleware(options) {
|
|
30
|
+
const { serviceName, internalApiKey, getRequestId: getReqId = request_context_1.getRequestId } = options;
|
|
31
|
+
return {
|
|
32
|
+
onRequest({ request }) {
|
|
33
|
+
const context = (0, request_context_1.getRequestContext)();
|
|
34
|
+
const requestId = getReqId();
|
|
35
|
+
if (!request.headers.has(constants_1.REQUEST_ID_HEADER)) {
|
|
36
|
+
request.headers.set(constants_1.REQUEST_ID_HEADER, requestId);
|
|
37
|
+
}
|
|
38
|
+
if (!request.headers.has(constants_1.INITIATING_SERVICE_NAME_HEADER)) {
|
|
39
|
+
request.headers.set(constants_1.INITIATING_SERVICE_NAME_HEADER, serviceName);
|
|
40
|
+
}
|
|
41
|
+
const apiKey = typeof internalApiKey === 'function' ? internalApiKey() : internalApiKey;
|
|
42
|
+
if (apiKey && !request.headers.has(constants_1.INTERNAL_API_KEY_HEADER)) {
|
|
43
|
+
request.headers.set(constants_1.INTERNAL_API_KEY_HEADER, apiKey);
|
|
44
|
+
}
|
|
45
|
+
// Passthrough from incoming request context
|
|
46
|
+
if (context.orgSlug && !request.headers.has(constants_1.ORG_SLUG_HEADER)) {
|
|
47
|
+
request.headers.set(constants_1.ORG_SLUG_HEADER, context.orgSlug);
|
|
48
|
+
}
|
|
49
|
+
if (context.authToken && !request.headers.has(constants_1.AUTH_TOKEN_HEADER)) {
|
|
50
|
+
request.headers.set(constants_1.AUTH_TOKEN_HEADER, context.authToken);
|
|
51
|
+
}
|
|
52
|
+
return request;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Middleware } from 'openapi-fetch';
|
|
2
|
+
/**
|
|
3
|
+
* Any object with a `.parse()` method, compatible with both Zod 3 and Zod 4.
|
|
4
|
+
*/
|
|
5
|
+
export interface Parseable {
|
|
6
|
+
parse(data: unknown): unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A map of "METHOD /path" → schema for request body validation.
|
|
10
|
+
*
|
|
11
|
+
* Values must have a `.parse()` method (any Zod version works).
|
|
12
|
+
*
|
|
13
|
+
* Keys use the OpenAPI path template format, e.g.:
|
|
14
|
+
* "POST /v1/dataapps/apps/manage"
|
|
15
|
+
* "PUT /v1/dataapps/apps/{id}/labels"
|
|
16
|
+
*/
|
|
17
|
+
export type RequestBodySchemaMap = Record<string, Parseable>;
|
|
18
|
+
export interface RequestValidationMiddlewareOptions {
|
|
19
|
+
/** Map of "METHOD /path" → Zod schema for request body validation. */
|
|
20
|
+
schemas: RequestBodySchemaMap;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create openapi-fetch middleware that validates request bodies against
|
|
24
|
+
* Zod schemas before the request is sent over the wire.
|
|
25
|
+
*
|
|
26
|
+
* If validation fails, a ZodError is thrown and the request is never sent.
|
|
27
|
+
* Endpoints without a matching schema are passed through without validation.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import createClient from 'openapi-fetch';
|
|
32
|
+
* import {createRequestValidationMiddleware} from '@tetrascience/request-middleware/server';
|
|
33
|
+
* import {requestBodySchemas} from './generated/request-schemas';
|
|
34
|
+
*
|
|
35
|
+
* const client = createClient<paths>({baseUrl: '...'});
|
|
36
|
+
* client.use(createRequestValidationMiddleware({schemas: requestBodySchemas}));
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function createRequestValidationMiddleware(options: RequestValidationMiddlewareOptions): Middleware;
|
|
40
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/server/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAE7D,MAAM,WAAW,kCAAkC;IAClD,sEAAsE;IACtE,OAAO,EAAE,oBAAoB,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,kCAAkC,GAAG,UAAU,CA2BzG"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequestValidationMiddleware = createRequestValidationMiddleware;
|
|
4
|
+
/**
|
|
5
|
+
* Create openapi-fetch middleware that validates request bodies against
|
|
6
|
+
* Zod schemas before the request is sent over the wire.
|
|
7
|
+
*
|
|
8
|
+
* If validation fails, a ZodError is thrown and the request is never sent.
|
|
9
|
+
* Endpoints without a matching schema are passed through without validation.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import createClient from 'openapi-fetch';
|
|
14
|
+
* import {createRequestValidationMiddleware} from '@tetrascience/request-middleware/server';
|
|
15
|
+
* import {requestBodySchemas} from './generated/request-schemas';
|
|
16
|
+
*
|
|
17
|
+
* const client = createClient<paths>({baseUrl: '...'});
|
|
18
|
+
* client.use(createRequestValidationMiddleware({schemas: requestBodySchemas}));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function createRequestValidationMiddleware(options) {
|
|
22
|
+
const { schemas } = options;
|
|
23
|
+
return {
|
|
24
|
+
async onRequest({ request, schemaPath }) {
|
|
25
|
+
const key = `${request.method} ${schemaPath}`;
|
|
26
|
+
const schema = schemas[key];
|
|
27
|
+
if (!schema)
|
|
28
|
+
return request;
|
|
29
|
+
const cloned = request.clone();
|
|
30
|
+
const rawText = await cloned.text();
|
|
31
|
+
if (rawText === '') {
|
|
32
|
+
return request;
|
|
33
|
+
}
|
|
34
|
+
let body;
|
|
35
|
+
try {
|
|
36
|
+
body = JSON.parse(rawText);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
schema.parse(body);
|
|
42
|
+
return request;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Middleware } from 'openapi-fetch';
|
|
2
|
+
import type { InternalAuth, DirectAuth, TracingOptions } from './client-types';
|
|
3
|
+
/**
|
|
4
|
+
* Create middleware that injects tracing headers: request ID, session ID,
|
|
5
|
+
* and service name.
|
|
6
|
+
*
|
|
7
|
+
* All values are optional. `requestId` falls back to a new UUID if not
|
|
8
|
+
* provided. Values can be static strings or functions resolved per-request.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* client.use(createTracingMiddleware({
|
|
13
|
+
* requestId: () => getRequestId(),
|
|
14
|
+
* sessionId: () => getRequestContext().sessionId,
|
|
15
|
+
* serviceName: 'my-service',
|
|
16
|
+
* }))
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function createTracingMiddleware(options?: TracingOptions): Middleware;
|
|
20
|
+
/**
|
|
21
|
+
* Create middleware that injects internal (service-to-service) auth headers.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* client.use(createInternalAuthMiddleware({
|
|
26
|
+
* internalApiKey: process.env.INTERNAL_API_KEY,
|
|
27
|
+
* orgSlug: () => getRequestContext().orgSlug,
|
|
28
|
+
* authToken: () => getRequestContext().authToken,
|
|
29
|
+
* }))
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function createInternalAuthMiddleware(auth: InternalAuth): Middleware;
|
|
33
|
+
/**
|
|
34
|
+
* Create middleware that injects direct (user/browser) auth headers.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* client.use(createDirectAuthMiddleware({
|
|
39
|
+
* token: jwt,
|
|
40
|
+
* orgSlug: 'my-org',
|
|
41
|
+
* }))
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function createDirectAuthMiddleware(auth: DirectAuth): Middleware;
|
|
45
|
+
//# sourceMappingURL=auth-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-middleware.d.ts","sourceRoot":"","sources":["../../src/shared/auth-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAE9C,OAAO,KAAK,EAAC,YAAY,EAAE,UAAU,EAAE,cAAc,EAAC,MAAM,gBAAgB,CAAC;AAsB7E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,UAAU,CAS5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU,CAS3E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAQvE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTracingMiddleware = createTracingMiddleware;
|
|
4
|
+
exports.createInternalAuthMiddleware = createInternalAuthMiddleware;
|
|
5
|
+
exports.createDirectAuthMiddleware = createDirectAuthMiddleware;
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const generate_request_id_1 = require("./generate-request-id");
|
|
8
|
+
function resolve(value) {
|
|
9
|
+
return typeof value === 'function' ? value() : value;
|
|
10
|
+
}
|
|
11
|
+
function setIfAbsent(request, header, value) {
|
|
12
|
+
const resolved = resolve(value);
|
|
13
|
+
if (resolved && !request.headers.has(header)) {
|
|
14
|
+
request.headers.set(header, resolved);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create middleware that injects tracing headers: request ID, session ID,
|
|
19
|
+
* and service name.
|
|
20
|
+
*
|
|
21
|
+
* All values are optional. `requestId` falls back to a new UUID if not
|
|
22
|
+
* provided. Values can be static strings or functions resolved per-request.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* client.use(createTracingMiddleware({
|
|
27
|
+
* requestId: () => getRequestId(),
|
|
28
|
+
* sessionId: () => getRequestContext().sessionId,
|
|
29
|
+
* serviceName: 'my-service',
|
|
30
|
+
* }))
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function createTracingMiddleware(options) {
|
|
34
|
+
return {
|
|
35
|
+
onRequest({ request }) {
|
|
36
|
+
setIfAbsent(request, constants_1.REQUEST_ID_HEADER, resolve(options?.requestId) ?? (0, generate_request_id_1.generateRequestId)());
|
|
37
|
+
setIfAbsent(request, constants_1.SESSION_ID_HEADER, options?.sessionId);
|
|
38
|
+
setIfAbsent(request, constants_1.INITIATING_SERVICE_NAME_HEADER, options?.serviceName);
|
|
39
|
+
return request;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create middleware that injects internal (service-to-service) auth headers.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* client.use(createInternalAuthMiddleware({
|
|
49
|
+
* internalApiKey: process.env.INTERNAL_API_KEY,
|
|
50
|
+
* orgSlug: () => getRequestContext().orgSlug,
|
|
51
|
+
* authToken: () => getRequestContext().authToken,
|
|
52
|
+
* }))
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function createInternalAuthMiddleware(auth) {
|
|
56
|
+
return {
|
|
57
|
+
onRequest({ request }) {
|
|
58
|
+
setIfAbsent(request, constants_1.INTERNAL_API_KEY_HEADER, auth.internalApiKey);
|
|
59
|
+
setIfAbsent(request, constants_1.ORG_SLUG_HEADER, auth.orgSlug);
|
|
60
|
+
setIfAbsent(request, constants_1.AUTH_TOKEN_HEADER, auth.authToken);
|
|
61
|
+
return request;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create middleware that injects direct (user/browser) auth headers.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* client.use(createDirectAuthMiddleware({
|
|
71
|
+
* token: jwt,
|
|
72
|
+
* orgSlug: 'my-org',
|
|
73
|
+
* }))
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
function createDirectAuthMiddleware(auth) {
|
|
77
|
+
return {
|
|
78
|
+
onRequest({ request }) {
|
|
79
|
+
setIfAbsent(request, constants_1.AUTH_TOKEN_HEADER, auth.token);
|
|
80
|
+
setIfAbsent(request, constants_1.ORG_SLUG_HEADER, auth.orgSlug);
|
|
81
|
+
return request;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Middleware } from 'openapi-fetch';
|
|
2
|
+
/**
|
|
3
|
+
* Create openapi-fetch middleware that prevents crashes when the server
|
|
4
|
+
* returns a non-JSON body (e.g. plain text "success") on a successful response.
|
|
5
|
+
*
|
|
6
|
+
* openapi-fetch's default parser calls `response.json()` which throws on
|
|
7
|
+
* non-JSON bodies. This middleware detects that case and wraps the body in
|
|
8
|
+
* a `{ message: "<text>" }` JSON envelope so parsing succeeds.
|
|
9
|
+
*
|
|
10
|
+
* 204 No Content responses are left untouched (no body to parse).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import createClient from 'openapi-fetch';
|
|
15
|
+
* import {createSafeResponseMiddleware} from '@tetrascience/request-middleware';
|
|
16
|
+
*
|
|
17
|
+
* const client = createClient<paths>({baseUrl: '...'});
|
|
18
|
+
* client.use(createSafeResponseMiddleware());
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function createSafeResponseMiddleware(): Middleware;
|
|
22
|
+
//# sourceMappingURL=safe-response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-response.d.ts","sourceRoot":"","sources":["../../src/shared/safe-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,IAAI,UAAU,CAkBzD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSafeResponseMiddleware = createSafeResponseMiddleware;
|
|
4
|
+
/**
|
|
5
|
+
* Create openapi-fetch middleware that prevents crashes when the server
|
|
6
|
+
* returns a non-JSON body (e.g. plain text "success") on a successful response.
|
|
7
|
+
*
|
|
8
|
+
* openapi-fetch's default parser calls `response.json()` which throws on
|
|
9
|
+
* non-JSON bodies. This middleware detects that case and wraps the body in
|
|
10
|
+
* a `{ message: "<text>" }` JSON envelope so parsing succeeds.
|
|
11
|
+
*
|
|
12
|
+
* 204 No Content responses are left untouched (no body to parse).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import createClient from 'openapi-fetch';
|
|
17
|
+
* import {createSafeResponseMiddleware} from '@tetrascience/request-middleware';
|
|
18
|
+
*
|
|
19
|
+
* const client = createClient<paths>({baseUrl: '...'});
|
|
20
|
+
* client.use(createSafeResponseMiddleware());
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function createSafeResponseMiddleware() {
|
|
24
|
+
return {
|
|
25
|
+
async onResponse({ response }) {
|
|
26
|
+
const contentType = response.headers.get('content-type') || '';
|
|
27
|
+
const isJson = contentType.includes('/json') || contentType.includes('+json');
|
|
28
|
+
if (response.ok && response.status !== 204 && !isJson) {
|
|
29
|
+
const text = await response.clone().text();
|
|
30
|
+
const headers = new Headers(response.headers);
|
|
31
|
+
headers.set('content-type', 'application/json');
|
|
32
|
+
return new Response(JSON.stringify({ message: text }), {
|
|
33
|
+
status: response.status,
|
|
34
|
+
statusText: response.statusText,
|
|
35
|
+
headers,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Middleware } from 'openapi-fetch';
|
|
2
|
+
/**
|
|
3
|
+
* Any object with a `.parse()` method, compatible with both Zod 3 and Zod 4.
|
|
4
|
+
*/
|
|
5
|
+
export interface Parseable {
|
|
6
|
+
parse(data: unknown): unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A map of "METHOD /path" → schema for request body validation.
|
|
10
|
+
*
|
|
11
|
+
* Values must have a `.parse()` method (any Zod version works).
|
|
12
|
+
*
|
|
13
|
+
* Keys use the OpenAPI path template format, e.g.:
|
|
14
|
+
* "POST /v1/dataapps/apps/manage"
|
|
15
|
+
* "PUT /v1/dataapps/apps/{id}/labels"
|
|
16
|
+
*/
|
|
17
|
+
export type RequestBodySchemaMap = Record<string, Parseable>;
|
|
18
|
+
export interface RequestValidationMiddlewareOptions {
|
|
19
|
+
/** Map of "METHOD /path" → Zod schema for request body validation. */
|
|
20
|
+
schemas: RequestBodySchemaMap;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create openapi-fetch middleware that validates request bodies against
|
|
24
|
+
* Zod schemas before the request is sent over the wire.
|
|
25
|
+
*
|
|
26
|
+
* If validation fails, a ZodError is thrown and the request is never sent.
|
|
27
|
+
* Endpoints without a matching schema are passed through without validation.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import createClient from 'openapi-fetch';
|
|
32
|
+
* import {createRequestValidationMiddleware} from '@tetrascience/request-middleware';
|
|
33
|
+
* import {requestBodySchemas} from './generated/request-schemas';
|
|
34
|
+
*
|
|
35
|
+
* const client = createClient<paths>({baseUrl: '...'});
|
|
36
|
+
* client.use(createRequestValidationMiddleware({schemas: requestBodySchemas}));
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function createRequestValidationMiddleware(options: RequestValidationMiddlewareOptions): Middleware;
|
|
40
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/shared/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAE7D,MAAM,WAAW,kCAAkC;IAClD,sEAAsE;IACtE,OAAO,EAAE,oBAAoB,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,kCAAkC,GAAG,UAAU,CAqBzG"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequestValidationMiddleware = createRequestValidationMiddleware;
|
|
4
|
+
/**
|
|
5
|
+
* Create openapi-fetch middleware that validates request bodies against
|
|
6
|
+
* Zod schemas before the request is sent over the wire.
|
|
7
|
+
*
|
|
8
|
+
* If validation fails, a ZodError is thrown and the request is never sent.
|
|
9
|
+
* Endpoints without a matching schema are passed through without validation.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import createClient from 'openapi-fetch';
|
|
14
|
+
* import {createRequestValidationMiddleware} from '@tetrascience/request-middleware';
|
|
15
|
+
* import {requestBodySchemas} from './generated/request-schemas';
|
|
16
|
+
*
|
|
17
|
+
* const client = createClient<paths>({baseUrl: '...'});
|
|
18
|
+
* client.use(createRequestValidationMiddleware({schemas: requestBodySchemas}));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function createRequestValidationMiddleware(options) {
|
|
22
|
+
const { schemas } = options;
|
|
23
|
+
return {
|
|
24
|
+
async onRequest({ request, schemaPath }) {
|
|
25
|
+
const key = `${request.method} ${schemaPath}`;
|
|
26
|
+
const schema = schemas[key];
|
|
27
|
+
if (!schema)
|
|
28
|
+
return request;
|
|
29
|
+
const cloned = request.clone();
|
|
30
|
+
const rawText = await cloned.text();
|
|
31
|
+
if (rawText === '') {
|
|
32
|
+
return request;
|
|
33
|
+
}
|
|
34
|
+
const body = JSON.parse(rawText);
|
|
35
|
+
schema.parse(body);
|
|
36
|
+
return request;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
package/package.json
CHANGED