@tetrascience-npm/request 0.2.0-beta.106.2
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 +169 -0
- package/client.d.ts +1 -0
- package/client.js +1 -0
- package/dist/cli/generate-client.d.ts +3 -0
- package/dist/cli/generate-client.d.ts.map +1 -0
- package/dist/cli/generate-client.js +208 -0
- package/dist/cli/generate-schemas.d.ts +7 -0
- package/dist/cli/generate-schemas.d.ts.map +1 -0
- package/dist/cli/generate-schemas.js +210 -0
- package/dist/cli/templates.d.ts +27 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +165 -0
- package/dist/client/console-logger.d.ts +10 -0
- package/dist/client/console-logger.d.ts.map +1 -0
- package/dist/client/console-logger.js +42 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +19 -0
- package/dist/client/install-middleware.d.ts +31 -0
- package/dist/client/install-middleware.d.ts.map +1 -0
- package/dist/client/install-middleware.js +117 -0
- package/dist/client/types.d.ts +13 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +24 -0
- package/dist/server/request-context.d.ts +27 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +42 -0
- package/dist/server/request-middleware.d.ts +14 -0
- package/dist/server/request-middleware.d.ts.map +1 -0
- package/dist/server/request-middleware.js +82 -0
- package/dist/server/types.d.ts +13 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/shared/client-types.d.ts +63 -0
- package/dist/shared/client-types.d.ts.map +1 -0
- package/dist/shared/client-types.js +7 -0
- package/dist/shared/constants.d.ts +7 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +9 -0
- package/dist/shared/generate-request-id.d.ts +10 -0
- package/dist/shared/generate-request-id.d.ts.map +1 -0
- package/dist/shared/generate-request-id.js +21 -0
- package/dist/shared/index.d.ts +8 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +29 -0
- package/dist/shared/middleware/auth.d.ts +39 -0
- package/dist/shared/middleware/auth.d.ts.map +1 -0
- package/dist/shared/middleware/auth.js +164 -0
- package/dist/shared/middleware/default.d.ts +29 -0
- package/dist/shared/middleware/default.d.ts.map +1 -0
- package/dist/shared/middleware/default.js +67 -0
- package/dist/shared/middleware/index.d.ts +6 -0
- package/dist/shared/middleware/index.d.ts.map +1 -0
- package/dist/shared/middleware/index.js +21 -0
- package/dist/shared/middleware/safe-response.d.ts +22 -0
- package/dist/shared/middleware/safe-response.d.ts.map +1 -0
- package/dist/shared/middleware/safe-response.js +41 -0
- package/dist/shared/middleware/tracing.d.ts +23 -0
- package/dist/shared/middleware/tracing.d.ts.map +1 -0
- package/dist/shared/middleware/tracing.js +67 -0
- package/dist/shared/middleware/utils.d.ts +4 -0
- package/dist/shared/middleware/utils.d.ts.map +1 -0
- package/dist/shared/middleware/utils.js +13 -0
- package/dist/shared/middleware/validation.d.ts +43 -0
- package/dist/shared/middleware/validation.d.ts.map +1 -0
- package/dist/shared/middleware/validation.js +42 -0
- package/dist/shared/sanitize-url.d.ts +3 -0
- package/dist/shared/sanitize-url.d.ts.map +1 -0
- package/dist/shared/sanitize-url.js +12 -0
- package/dist/shared/types.d.ts +10 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/package.json +98 -0
- package/server.d.ts +1 -0
- package/server.js +1 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared template functions used by init-client and generate-client CLIs.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.deriveServiceName = deriveServiceName;
|
|
7
|
+
exports.generateTsConfig = generateTsConfig;
|
|
8
|
+
exports.generateIndexTs = generateIndexTs;
|
|
9
|
+
exports.generateClientPackageJson = generateClientPackageJson;
|
|
10
|
+
exports.generateClientGitIgnore = generateClientGitIgnore;
|
|
11
|
+
exports.generateYarnRc = generateYarnRc;
|
|
12
|
+
/**
|
|
13
|
+
* Derive a PascalCase service name from the npm package name.
|
|
14
|
+
* @example "@tetrascience/data-apps-client" -> "DataApps"
|
|
15
|
+
* @example "@tetrascience/pipeline-client" -> "Pipeline"
|
|
16
|
+
*/
|
|
17
|
+
function deriveServiceName(packageName) {
|
|
18
|
+
const bare = packageName.replace(/^@[^/]+\//, '').replace(/-client$/, '');
|
|
19
|
+
return bare
|
|
20
|
+
.split('-')
|
|
21
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
22
|
+
.join('');
|
|
23
|
+
}
|
|
24
|
+
function generateTsConfig() {
|
|
25
|
+
const config = {
|
|
26
|
+
compilerOptions: {
|
|
27
|
+
target: 'ES2020',
|
|
28
|
+
module: 'node16',
|
|
29
|
+
moduleResolution: 'node16',
|
|
30
|
+
lib: ['ES2020'],
|
|
31
|
+
declaration: true,
|
|
32
|
+
strict: true,
|
|
33
|
+
esModuleInterop: true,
|
|
34
|
+
skipLibCheck: true,
|
|
35
|
+
outDir: 'dist',
|
|
36
|
+
rootDir: '.',
|
|
37
|
+
},
|
|
38
|
+
include: ['index.ts', 'generated/**/*.ts'],
|
|
39
|
+
exclude: ['node_modules', 'dist'],
|
|
40
|
+
};
|
|
41
|
+
return JSON.stringify(config, null, 2) + '\n';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* PascalCase a variant name for use as a type name.
|
|
45
|
+
* @example "internal" -> "Internal", "public-api" -> "PublicApi"
|
|
46
|
+
*/
|
|
47
|
+
function pascalCase(s) {
|
|
48
|
+
return s
|
|
49
|
+
.split(/[-_]/)
|
|
50
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
51
|
+
.join('');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Generate index.ts for a client package.
|
|
55
|
+
*
|
|
56
|
+
* @param serviceName - PascalCase service name (e.g. "DataApps")
|
|
57
|
+
* @param specVariants - null for single-spec, or an array of variant names (e.g. ["internal", "public"])
|
|
58
|
+
*/
|
|
59
|
+
function generateIndexTs(serviceName, specVariants) {
|
|
60
|
+
const factoryName = `create${serviceName}Client`;
|
|
61
|
+
const typeName = `${serviceName}Client`;
|
|
62
|
+
const optionsName = `${serviceName}ClientOptions`;
|
|
63
|
+
if (!specVariants || specVariants.length === 0) {
|
|
64
|
+
// Single-spec template
|
|
65
|
+
return `import { createClient, applyDefaultMiddleware } from '@tetrascience-npm/request'
|
|
66
|
+
import type { ServiceClientOptions } from '@tetrascience-npm/request'
|
|
67
|
+
import type { paths } from './generated/schema'
|
|
68
|
+
import { requestBodySchemas } from './generated/request-schemas'
|
|
69
|
+
|
|
70
|
+
export type * from './generated/schema'
|
|
71
|
+
export type { InternalAuth, DirectAuth, ServiceClientOptions } from '@tetrascience-npm/request'
|
|
72
|
+
|
|
73
|
+
export type ${optionsName} = ServiceClientOptions
|
|
74
|
+
|
|
75
|
+
export function ${factoryName}(options: ${optionsName}) {
|
|
76
|
+
const client = createClient<paths>({
|
|
77
|
+
baseUrl: options.baseUrl,
|
|
78
|
+
headers: options.headers,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
applyDefaultMiddleware(client, options, requestBodySchemas)
|
|
82
|
+
|
|
83
|
+
return client
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ${typeName} = ReturnType<typeof ${factoryName}>
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
// Multi-spec template
|
|
90
|
+
const pathImports = specVariants
|
|
91
|
+
.map((v) => `import type { paths as ${pascalCase(v)}Paths } from './generated/${v}.schema'`)
|
|
92
|
+
.join('\n');
|
|
93
|
+
const schemaImports = specVariants
|
|
94
|
+
.map((v) => `import { requestBodySchemas as ${v}Schemas } from './generated/${v}.request-schemas'`)
|
|
95
|
+
.join('\n');
|
|
96
|
+
const pathTypes = specVariants.map((v) => `${pascalCase(v)}Paths`);
|
|
97
|
+
const pathUnion = pathTypes.join(' | ');
|
|
98
|
+
const defaultPath = pathTypes[0];
|
|
99
|
+
const pathExports = specVariants.map((v) => ` ${pascalCase(v)}Paths`).join(',\n');
|
|
100
|
+
const schemaMerge = specVariants.map((v) => `...${v}Schemas`).join(', ');
|
|
101
|
+
return `import { createClient, applyDefaultMiddleware } from '@tetrascience-npm/request'
|
|
102
|
+
import type { ServiceClientOptions } from '@tetrascience-npm/request'
|
|
103
|
+
${pathImports}
|
|
104
|
+
${schemaImports}
|
|
105
|
+
|
|
106
|
+
export type {
|
|
107
|
+
${pathExports},
|
|
108
|
+
}
|
|
109
|
+
export type { InternalAuth, DirectAuth, ServiceClientOptions } from '@tetrascience-npm/request'
|
|
110
|
+
|
|
111
|
+
const allSchemas = { ${schemaMerge} }
|
|
112
|
+
|
|
113
|
+
export type ${optionsName} = ServiceClientOptions
|
|
114
|
+
|
|
115
|
+
export function ${factoryName}<P extends ${pathUnion} = ${defaultPath}>(options: ${optionsName}) {
|
|
116
|
+
const client = createClient<P>({
|
|
117
|
+
baseUrl: options.baseUrl,
|
|
118
|
+
headers: options.headers,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
applyDefaultMiddleware(client, options, allSchemas)
|
|
122
|
+
|
|
123
|
+
return client
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type ${typeName}<P extends ${pathUnion} = ${defaultPath}> = ReturnType<typeof ${factoryName}<P>>
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
function generateClientPackageJson(config) {
|
|
130
|
+
const pkg = {
|
|
131
|
+
name: config.packageName,
|
|
132
|
+
version: config.version,
|
|
133
|
+
description: config.description || `Typed client for the ${config.serviceName} service API`,
|
|
134
|
+
main: 'dist/index.js',
|
|
135
|
+
types: 'dist/index.d.ts',
|
|
136
|
+
files: ['dist'],
|
|
137
|
+
scripts: {
|
|
138
|
+
build: 'tsc --project tsconfig.build.json',
|
|
139
|
+
prepack: 'yarn build',
|
|
140
|
+
},
|
|
141
|
+
dependencies: {
|
|
142
|
+
'@tetrascience-npm/request': config.middlewareVersion,
|
|
143
|
+
'openapi-fetch': '^0.17.0',
|
|
144
|
+
zod: '^3.24.0',
|
|
145
|
+
},
|
|
146
|
+
devDependencies: {
|
|
147
|
+
typescript: '^5.7.3',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
151
|
+
}
|
|
152
|
+
function generateClientGitIgnore() {
|
|
153
|
+
return `node_modules/
|
|
154
|
+
dist/
|
|
155
|
+
.yarn/
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
function generateYarnRc() {
|
|
159
|
+
return `nodeLinker: node-modules
|
|
160
|
+
|
|
161
|
+
npmScopes:
|
|
162
|
+
tetrascience:
|
|
163
|
+
npmRegistryServer: "https://tetrascience.jfrog.io/artifactory/api/npm/ts-npm-virtual/"
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RequestTrackingLogger } from '../shared/types';
|
|
2
|
+
import type { ConsoleLoggerOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Create a structured console logger for request tracking.
|
|
5
|
+
*
|
|
6
|
+
* Outputs structured log entries to the browser console, including the
|
|
7
|
+
* requestId from metadata for correlation with backend logs.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createConsoleLogger(options?: ConsoleLoggerOptions): RequestTrackingLogger;
|
|
10
|
+
//# sourceMappingURL=console-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-logger.d.ts","sourceRoot":"","sources":["../../src/client/console-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,iBAAiB,CAAC;AAC3D,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,SAAS,CAAC;AAKlD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,qBAAqB,CA8BzF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createConsoleLogger = createConsoleLogger;
|
|
4
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
5
|
+
/**
|
|
6
|
+
* Create a structured console logger for request tracking.
|
|
7
|
+
*
|
|
8
|
+
* Outputs structured log entries to the browser console, including the
|
|
9
|
+
* requestId from metadata for correlation with backend logs.
|
|
10
|
+
*/
|
|
11
|
+
function createConsoleLogger(options) {
|
|
12
|
+
const minLevel = LOG_LEVELS[options?.level ?? 'info'];
|
|
13
|
+
const prefix = options?.prefix;
|
|
14
|
+
const formatMessage = (message) => (prefix ? `[${prefix}] ${message}` : message);
|
|
15
|
+
const shouldLog = (level) => LOG_LEVELS[level] >= minLevel;
|
|
16
|
+
const log = (method, message, meta) => {
|
|
17
|
+
if (meta !== undefined) {
|
|
18
|
+
console[method](formatMessage(message), meta);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console[method](formatMessage(message));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
debug(message, meta) {
|
|
26
|
+
if (shouldLog('debug'))
|
|
27
|
+
log('debug', message, meta);
|
|
28
|
+
},
|
|
29
|
+
info(message, meta) {
|
|
30
|
+
if (shouldLog('info'))
|
|
31
|
+
log('info', message, meta);
|
|
32
|
+
},
|
|
33
|
+
warn(message, meta) {
|
|
34
|
+
if (shouldLog('warn'))
|
|
35
|
+
log('warn', message, meta);
|
|
36
|
+
},
|
|
37
|
+
error(message, meta) {
|
|
38
|
+
if (shouldLog('error'))
|
|
39
|
+
log('error', message, meta);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./console-logger"), exports);
|
|
19
|
+
__exportStar(require("./install-middleware"), exports);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ClientTrackingOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Install request middleware on the global `fetch` function.
|
|
4
|
+
*
|
|
5
|
+
* Automatically injects `ts-request-id` and `ts-session-id` headers
|
|
6
|
+
* on every outgoing request. Session ID is persisted in a cookie
|
|
7
|
+
* with a 30-minute sliding expiry.
|
|
8
|
+
*
|
|
9
|
+
* Safe to call multiple times — subsequent calls return the existing
|
|
10
|
+
* uninstall function without re-patching (important for Module Federation
|
|
11
|
+
* where multiple micro-frontends share `globalThis`).
|
|
12
|
+
*
|
|
13
|
+
* **Limitations:**
|
|
14
|
+
* - Only patches `fetch()`. Libraries using `XMLHttpRequest` or `axios`
|
|
15
|
+
* are not covered.
|
|
16
|
+
* - Code that captures `window.fetch` before this runs (e.g.
|
|
17
|
+
* `const myFetch = window.fetch` at import time) will hold a
|
|
18
|
+
* reference to the unpatched fetch.
|
|
19
|
+
*
|
|
20
|
+
* @returns An uninstall function that restores the original `fetch`
|
|
21
|
+
* and clears the session cookie.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { installRequestMiddleware } from '@tetrascience-npm/request/client'
|
|
26
|
+
*
|
|
27
|
+
* installRequestMiddleware()
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function installRequestMiddleware(options?: ClientTrackingOptions): () => void;
|
|
31
|
+
//# sourceMappingURL=install-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-middleware.d.ts","sourceRoot":"","sources":["../../src/client/install-middleware.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,SAAS,CAAC;AAqCnD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,IAAI,CA+DpF"}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
const sanitize_url_1 = require("../shared/sanitize-url");
|
|
7
|
+
const SESSION_COOKIE_MAX_AGE = 30 * 60; // 30 minutes, refreshed on each request
|
|
8
|
+
const INSTALLED_KEY = '__ts_request_middleware_installed__';
|
|
9
|
+
/**
|
|
10
|
+
* Get or create a session ID from cookies.
|
|
11
|
+
* Creates a new UUID and stores it as a cookie if not already present.
|
|
12
|
+
* Refreshes the cookie expiry on each call (sliding window).
|
|
13
|
+
*
|
|
14
|
+
* The cookie is written without a Domain attribute — the server's
|
|
15
|
+
* createRequestMiddleware() sets the correct cross-subdomain Domain
|
|
16
|
+
* (from COOKIE_DOMAIN env var) on the response. The browser-set cookie
|
|
17
|
+
* is just a fallback for the first request before the server responds.
|
|
18
|
+
*/
|
|
19
|
+
function getOrCreateSessionId() {
|
|
20
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${constants_1.SESSION_ID_HEADER}=([^;]*)`));
|
|
21
|
+
if (match) {
|
|
22
|
+
// Refresh expiry
|
|
23
|
+
document.cookie = `${constants_1.SESSION_ID_HEADER}=${match[1]}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;
|
|
24
|
+
return match[1];
|
|
25
|
+
}
|
|
26
|
+
const sessionId = (0, generate_request_id_1.generateRequestId)();
|
|
27
|
+
document.cookie = `${constants_1.SESSION_ID_HEADER}=${sessionId}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;
|
|
28
|
+
return sessionId;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Clear the session cookie.
|
|
32
|
+
*/
|
|
33
|
+
function clearSessionCookie() {
|
|
34
|
+
if (typeof document !== 'undefined') {
|
|
35
|
+
document.cookie = `${constants_1.SESSION_ID_HEADER}=; path=/; max-age=0; SameSite=Lax; Secure`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Install request middleware on the global `fetch` function.
|
|
40
|
+
*
|
|
41
|
+
* Automatically injects `ts-request-id` and `ts-session-id` headers
|
|
42
|
+
* on every outgoing request. Session ID is persisted in a cookie
|
|
43
|
+
* with a 30-minute sliding expiry.
|
|
44
|
+
*
|
|
45
|
+
* Safe to call multiple times — subsequent calls return the existing
|
|
46
|
+
* uninstall function without re-patching (important for Module Federation
|
|
47
|
+
* where multiple micro-frontends share `globalThis`).
|
|
48
|
+
*
|
|
49
|
+
* **Limitations:**
|
|
50
|
+
* - Only patches `fetch()`. Libraries using `XMLHttpRequest` or `axios`
|
|
51
|
+
* are not covered.
|
|
52
|
+
* - Code that captures `window.fetch` before this runs (e.g.
|
|
53
|
+
* `const myFetch = window.fetch` at import time) will hold a
|
|
54
|
+
* reference to the unpatched fetch.
|
|
55
|
+
*
|
|
56
|
+
* @returns An uninstall function that restores the original `fetch`
|
|
57
|
+
* and clears the session cookie.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { installRequestMiddleware } from '@tetrascience-npm/request/client'
|
|
62
|
+
*
|
|
63
|
+
* installRequestMiddleware()
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function installRequestMiddleware(options) {
|
|
67
|
+
// Guard against double-install (Module Federation, multiple micro-frontends)
|
|
68
|
+
const existing = globalThis[INSTALLED_KEY];
|
|
69
|
+
if (existing) {
|
|
70
|
+
return existing.uninstall;
|
|
71
|
+
}
|
|
72
|
+
const { logger } = options ?? {};
|
|
73
|
+
const originalFetch = globalThis.fetch;
|
|
74
|
+
globalThis.fetch = (input, init) => {
|
|
75
|
+
// Merge headers: start from the Request's headers (if input is a Request),
|
|
76
|
+
// then layer on init?.headers, then add tracking headers.
|
|
77
|
+
const inputHeaders = input instanceof Request ? input.headers : undefined;
|
|
78
|
+
const headers = new Headers(inputHeaders);
|
|
79
|
+
if (init?.headers) {
|
|
80
|
+
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
81
|
+
}
|
|
82
|
+
if (!headers.has(constants_1.REQUEST_ID_HEADER)) {
|
|
83
|
+
headers.set(constants_1.REQUEST_ID_HEADER, (0, generate_request_id_1.generateRequestId)());
|
|
84
|
+
}
|
|
85
|
+
const requestId = headers.get(constants_1.REQUEST_ID_HEADER);
|
|
86
|
+
if (!headers.has(constants_1.SESSION_ID_HEADER) && typeof document !== 'undefined') {
|
|
87
|
+
headers.set(constants_1.SESSION_ID_HEADER, getOrCreateSessionId());
|
|
88
|
+
}
|
|
89
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
90
|
+
logger?.debug('Outgoing request', {
|
|
91
|
+
requestId,
|
|
92
|
+
path: (0, sanitize_url_1.sanitizeUrl)(url),
|
|
93
|
+
});
|
|
94
|
+
return originalFetch(input, { ...init, headers }).then((response) => {
|
|
95
|
+
logger?.debug('Response received', {
|
|
96
|
+
requestId,
|
|
97
|
+
path: (0, sanitize_url_1.sanitizeUrl)(url),
|
|
98
|
+
status: response.status,
|
|
99
|
+
});
|
|
100
|
+
return response;
|
|
101
|
+
}, (error) => {
|
|
102
|
+
logger?.error('Request failed', {
|
|
103
|
+
requestId,
|
|
104
|
+
path: (0, sanitize_url_1.sanitizeUrl)(url),
|
|
105
|
+
error: error instanceof Error ? error.message : String(error),
|
|
106
|
+
});
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
const uninstall = () => {
|
|
111
|
+
globalThis.fetch = originalFetch;
|
|
112
|
+
clearSessionCookie();
|
|
113
|
+
delete globalThis[INSTALLED_KEY];
|
|
114
|
+
};
|
|
115
|
+
globalThis[INSTALLED_KEY] = { uninstall };
|
|
116
|
+
return uninstall;
|
|
117
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestTrackingLogger } from '../shared/types';
|
|
2
|
+
export interface ConsoleLoggerOptions {
|
|
3
|
+
/** Prefix prepended to all log messages. */
|
|
4
|
+
prefix?: string;
|
|
5
|
+
/** Minimum log level. Defaults to 'info'. */
|
|
6
|
+
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
7
|
+
}
|
|
8
|
+
/** Options for installRequestTracking (browser global fetch patching). */
|
|
9
|
+
export interface ClientTrackingOptions {
|
|
10
|
+
/** If provided, logs each outgoing request/response with the request ID. */
|
|
11
|
+
logger?: RequestTrackingLogger;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,iBAAiB,CAAC;AAE3D,MAAM,WAAW,oBAAoB;IACpC,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAC5C;AAED,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB;IACrC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAC/B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./shared"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AAIrC,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.REQUEST_ID_HEADER = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
__exportStar(require("./request-context"), exports);
|
|
20
|
+
__exportStar(require("./request-middleware"), exports);
|
|
21
|
+
// Constant re-exported for convenience (commonly used with request context)
|
|
22
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
23
|
+
var constants_1 = require("../shared/constants");
|
|
24
|
+
Object.defineProperty(exports, "REQUEST_ID_HEADER", { enumerable: true, get: function () { return constants_1.REQUEST_ID_HEADER; } });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RequestContext } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Get the current request context.
|
|
4
|
+
* Returns an empty object if no context has been set.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getRequestContext(): Partial<RequestContext>;
|
|
7
|
+
/**
|
|
8
|
+
* Run a function within a request context. The context is scoped to the
|
|
9
|
+
* callback and its async descendants, providing proper isolation.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* app.use((req, res, next) => {
|
|
14
|
+
* runWithRequestContext({ requestId }, () => next())
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function runWithRequestContext<T>(context: RequestContext, fn: () => T): T;
|
|
19
|
+
/**
|
|
20
|
+
* Run a function with no request context. Useful for testing isolation.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runWithoutRequestContext<T>(fn: () => T): T;
|
|
23
|
+
/**
|
|
24
|
+
* Get the request ID from the current context, or generate a new UUID.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getRequestId(): string;
|
|
27
|
+
//# sourceMappingURL=request-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,SAAS,CAAC;AAI5C;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAAC,cAAc,CAAC,CAE3D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEhF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE1D;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRequestContext = getRequestContext;
|
|
4
|
+
exports.runWithRequestContext = runWithRequestContext;
|
|
5
|
+
exports.runWithoutRequestContext = runWithoutRequestContext;
|
|
6
|
+
exports.getRequestId = getRequestId;
|
|
7
|
+
const async_hooks_1 = require("async_hooks");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
|
|
10
|
+
/**
|
|
11
|
+
* Get the current request context.
|
|
12
|
+
* Returns an empty object if no context has been set.
|
|
13
|
+
*/
|
|
14
|
+
function getRequestContext() {
|
|
15
|
+
return asyncLocalStorage.getStore() ?? {};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run a function within a request context. The context is scoped to the
|
|
19
|
+
* callback and its async descendants, providing proper isolation.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* app.use((req, res, next) => {
|
|
24
|
+
* runWithRequestContext({ requestId }, () => next())
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function runWithRequestContext(context, fn) {
|
|
29
|
+
return asyncLocalStorage.run(context, fn);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run a function with no request context. Useful for testing isolation.
|
|
33
|
+
*/
|
|
34
|
+
function runWithoutRequestContext(fn) {
|
|
35
|
+
return asyncLocalStorage.exit(fn);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the request ID from the current context, or generate a new UUID.
|
|
39
|
+
*/
|
|
40
|
+
function getRequestId() {
|
|
41
|
+
return getRequestContext().requestId ?? (0, crypto_1.randomUUID)();
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
|
7
|
+
interface ServerResponse {
|
|
8
|
+
setHeader?(name: string, value: string): void;
|
|
9
|
+
}
|
|
10
|
+
type NextFunction = () => void;
|
|
11
|
+
type ExpressMiddleware = (req: IncomingRequest, res: ServerResponse, next: NextFunction) => void;
|
|
12
|
+
export declare function createRequestMiddleware(): ExpressMiddleware;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=request-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-middleware.d.ts","sourceRoot":"","sources":["../../src/server/request-middleware.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,UAAU,eAAe;IACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACvD;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;AAwDjG,wBAAgB,uBAAuB,IAAI,iBAAiB,CAyB3D"}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
* Parse a named value from the Cookie header.
|
|
10
|
+
*/
|
|
11
|
+
function getCookie(req, name) {
|
|
12
|
+
const header = req.headers.cookie;
|
|
13
|
+
if (!header)
|
|
14
|
+
return undefined;
|
|
15
|
+
const match = header.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
16
|
+
return match?.[1];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Express middleware that sets up request context for tracing and auth.
|
|
20
|
+
*
|
|
21
|
+
* Reads from incoming headers and cookies:
|
|
22
|
+
* - `ts-request-id` — generates UUID if missing
|
|
23
|
+
* - `ts-session-id` — generates UUID if missing, sets cookie for persistence
|
|
24
|
+
* - `x-org-slug` — from header or cookie
|
|
25
|
+
* - `ts-auth-token` — from header or cookie
|
|
26
|
+
*
|
|
27
|
+
* Parses cookies internally — no need for `cookie-parser`.
|
|
28
|
+
*
|
|
29
|
+
* All downstream code can access the context via `getRequestContext()`.
|
|
30
|
+
* Generated clients auto-resolve auth and tracing from this context.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import express from 'express';
|
|
35
|
+
* import { createRequestMiddleware } from '@tetrascience-npm/request/server';
|
|
36
|
+
*
|
|
37
|
+
* const app = express();
|
|
38
|
+
* app.use(createRequestMiddleware());
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Derive cookie domain from env vars (same logic as TDP's getCookieDomain).
|
|
43
|
+
* COOKIE_DOMAIN is set on data apps; SERVICE_URI on the TDP web service.
|
|
44
|
+
*/
|
|
45
|
+
function deriveServerCookieDomain() {
|
|
46
|
+
const cookieDomain = process.env.COOKIE_DOMAIN;
|
|
47
|
+
if (cookieDomain)
|
|
48
|
+
return cookieDomain.startsWith('.') ? cookieDomain : '.' + cookieDomain;
|
|
49
|
+
const serviceUri = process.env.SERVICE_URI;
|
|
50
|
+
if (serviceUri) {
|
|
51
|
+
try {
|
|
52
|
+
return '.' + new URL(serviceUri).hostname;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// fall through
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function createRequestMiddleware() {
|
|
61
|
+
const cookieDomain = deriveServerCookieDomain();
|
|
62
|
+
return (req, res, next) => {
|
|
63
|
+
const requestId = req.headers[constants_1.REQUEST_ID_HEADER] || (0, crypto_1.randomUUID)();
|
|
64
|
+
// Session: header → cookie → generate
|
|
65
|
+
let sessionId = req.headers[constants_1.SESSION_ID_HEADER]
|
|
66
|
+
|| getCookie(req, constants_1.SESSION_ID_HEADER);
|
|
67
|
+
if (!sessionId) {
|
|
68
|
+
sessionId = (0, crypto_1.randomUUID)();
|
|
69
|
+
}
|
|
70
|
+
// Always refresh the session cookie (sliding expiry)
|
|
71
|
+
let setCookie = `${constants_1.SESSION_ID_HEADER}=${sessionId}; Path=/; Max-Age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;
|
|
72
|
+
if (cookieDomain)
|
|
73
|
+
setCookie += `; Domain=${cookieDomain}`;
|
|
74
|
+
res.setHeader?.('Set-Cookie', setCookie);
|
|
75
|
+
// Auth: header → cookie (optional, may not be present for background jobs)
|
|
76
|
+
const orgSlug = req.headers[constants_1.ORG_SLUG_HEADER]
|
|
77
|
+
|| getCookie(req, constants_1.ORG_SLUG_HEADER);
|
|
78
|
+
const authToken = req.headers[constants_1.AUTH_TOKEN_HEADER]
|
|
79
|
+
|| getCookie(req, constants_1.AUTH_TOKEN_HEADER);
|
|
80
|
+
(0, request_context_1.runWithRequestContext)({ requestId, sessionId, orgSlug, authToken }, () => next());
|
|
81
|
+
};
|
|
82
|
+
}
|