@ericminassian/platform 0.0.0-stage → 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/dist/app.d.ts +10 -0
- package/dist/app.js +23 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/cookies.d.ts +10 -0
- package/dist/auth/cookies.js +37 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/index.d.ts +22 -0
- package/dist/auth/index.js +217 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/pkce.d.ts +16 -0
- package/dist/auth/pkce.js +32 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/tokens.d.ts +17 -0
- package/dist/auth/tokens.js +142 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/bindings.d.ts +13 -0
- package/dist/bindings.js +32 -0
- package/dist/bindings.js.map +1 -0
- package/dist/clients.d.ts +38 -0
- package/dist/clients.js +65 -0
- package/dist/clients.js.map +1 -0
- package/dist/context.d.ts +31 -0
- package/dist/context.js +33 -0
- package/dist/context.js.map +1 -0
- package/dist/db.d.ts +97 -0
- package/dist/db.js +297 -0
- package/dist/db.js.map +1 -0
- package/dist/events.d.ts +9 -0
- package/dist/events.js +28 -0
- package/dist/events.js.map +1 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.js +82 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/queue.d.ts +7 -0
- package/dist/queue.js +50 -0
- package/dist/queue.js.map +1 -0
- package/dist/secrets.d.ts +8 -0
- package/dist/secrets.js +42 -0
- package/dist/secrets.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +232 -0
- package/dist/server.js.map +1 -0
- package/dist/storage.d.ts +30 -0
- package/dist/storage.js +108 -0
- package/dist/storage.js.map +1 -0
- package/dist/telemetry.d.ts +42 -0
- package/dist/telemetry.js +218 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +54 -0
- package/dist/testing.js.map +1 -0
- package/dist/worker.d.ts +73 -0
- package/dist/worker.js +179 -0
- package/dist/worker.js.map +1 -0
- package/package.json +58 -3
- package/src/app.ts +32 -0
- package/src/auth/cookies.ts +39 -0
- package/src/auth/index.ts +246 -0
- package/src/auth/pkce.ts +43 -0
- package/src/auth/tokens.ts +174 -0
- package/src/bindings.ts +35 -0
- package/src/clients.ts +98 -0
- package/src/context.ts +56 -0
- package/src/db.ts +380 -0
- package/src/events.ts +42 -0
- package/src/http.ts +82 -0
- package/src/index.ts +40 -0
- package/src/queue.ts +67 -0
- package/src/secrets.ts +57 -0
- package/src/server.ts +274 -0
- package/src/storage.ts +143 -0
- package/src/telemetry.ts +260 -0
- package/src/testing.ts +73 -0
- package/src/worker.ts +291 -0
- package/README.md +0 -3
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import type { AuthBindings, PlatformUser } from '@ericminassian/platform-core';
|
|
3
|
+
import { createRemoteJWKSet, type JWTPayload, errors as joseErrors, jwtVerify } from 'jose';
|
|
4
|
+
import { client } from '../clients.ts';
|
|
5
|
+
|
|
6
|
+
export interface Tokens {
|
|
7
|
+
idToken: string;
|
|
8
|
+
refreshToken?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
|
|
12
|
+
|
|
13
|
+
function jwks(uri: string) {
|
|
14
|
+
let set = jwksCache.get(uri);
|
|
15
|
+
if (!set) {
|
|
16
|
+
set = createRemoteJWKSet(new URL(uri), { cooldownDuration: 30_000, cacheMaxAge: 3_600_000 });
|
|
17
|
+
jwksCache.set(uri, set);
|
|
18
|
+
}
|
|
19
|
+
return set;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Verify a Cognito ID (or access) token for this app's client and map it to a user. */
|
|
23
|
+
export async function verifyToken(auth: AuthBindings, token: string): Promise<PlatformUser> {
|
|
24
|
+
const { payload } = await jwtVerify(token, jwks(auth.jwksUri), { issuer: auth.issuer });
|
|
25
|
+
const use = payload.token_use;
|
|
26
|
+
if (use === 'id') {
|
|
27
|
+
const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
28
|
+
if (!aud.includes(auth.clientId))
|
|
29
|
+
throw new joseErrors.JWTClaimValidationFailed('unexpected "aud" claim value', payload, 'aud');
|
|
30
|
+
} else if (use === 'access') {
|
|
31
|
+
if (payload.client_id !== auth.clientId)
|
|
32
|
+
throw new joseErrors.JWTClaimValidationFailed(
|
|
33
|
+
'unexpected "client_id" claim value',
|
|
34
|
+
payload,
|
|
35
|
+
'client_id',
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
throw new joseErrors.JWTClaimValidationFailed('unexpected "token_use" claim value', payload, 'token_use');
|
|
39
|
+
}
|
|
40
|
+
return toUser(payload);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isExpiredError(e: unknown): boolean {
|
|
44
|
+
return e instanceof joseErrors.JWTExpired;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function toUser(p: JWTPayload): PlatformUser {
|
|
48
|
+
const str = (k: string) => (typeof p[k] === 'string' ? (p[k] as string) : undefined);
|
|
49
|
+
const name =
|
|
50
|
+
str('name') ?? ([str('given_name'), str('family_name')].filter(Boolean).join(' ') || undefined);
|
|
51
|
+
const groups = Array.isArray(p['cognito:groups']) ? (p['cognito:groups'] as unknown[]).map(String) : [];
|
|
52
|
+
const user: PlatformUser = { id: String(p.sub), groups };
|
|
53
|
+
const email = str('email');
|
|
54
|
+
if (email) user.email = email;
|
|
55
|
+
if (p.email_verified !== undefined)
|
|
56
|
+
user.emailVerified = p.email_verified === true || p.email_verified === 'true';
|
|
57
|
+
if (name) user.name = name;
|
|
58
|
+
return user;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------- hosted login (cognito mode)
|
|
62
|
+
|
|
63
|
+
async function tokenRequest(auth: AuthBindings, params: Record<string, string>): Promise<Tokens> {
|
|
64
|
+
const res = await fetch(`${auth.loginUrl.replace(/\/$/, '')}/oauth2/token`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
67
|
+
body: new URLSearchParams({ client_id: auth.clientId, ...params }),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
throw new Error(`Cognito token endpoint returned ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
71
|
+
const json = (await res.json()) as { id_token?: string; refresh_token?: string };
|
|
72
|
+
if (!json.id_token) throw new Error('Cognito token endpoint returned no id_token');
|
|
73
|
+
return { idToken: json.id_token, ...(json.refresh_token ? { refreshToken: json.refresh_token } : {}) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function exchangeCode(
|
|
77
|
+
auth: AuthBindings,
|
|
78
|
+
code: string,
|
|
79
|
+
verifier: string,
|
|
80
|
+
redirectUri: string,
|
|
81
|
+
): Promise<Tokens> {
|
|
82
|
+
return tokenRequest(auth, {
|
|
83
|
+
grant_type: 'authorization_code',
|
|
84
|
+
code,
|
|
85
|
+
code_verifier: verifier,
|
|
86
|
+
redirect_uri: redirectUri,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function refreshTokens(auth: AuthBindings, refreshToken: string): Promise<Tokens> {
|
|
91
|
+
if (auth.mode === 'local') {
|
|
92
|
+
const { InitiateAuthCommand } = await import('@aws-sdk/client-cognito-identity-provider');
|
|
93
|
+
const out = await (await client('cognito')).send(
|
|
94
|
+
new InitiateAuthCommand({
|
|
95
|
+
ClientId: auth.clientId,
|
|
96
|
+
AuthFlow: 'REFRESH_TOKEN_AUTH',
|
|
97
|
+
AuthParameters: { REFRESH_TOKEN: refreshToken },
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
const id = out.AuthenticationResult?.IdToken;
|
|
101
|
+
if (!id) throw new Error('refresh returned no id token');
|
|
102
|
+
return { idToken: id };
|
|
103
|
+
}
|
|
104
|
+
return tokenRequest(auth, { grant_type: 'refresh_token', refresh_token: refreshToken });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------- local sign-in (plat dev)
|
|
108
|
+
|
|
109
|
+
function errorName(e: unknown): string {
|
|
110
|
+
return (e as { name?: string })?.name ?? '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `plat dev` sign-in against the AWS Emulator's Cognito (no hosted UI there):
|
|
115
|
+
* create the user if needed, set a throwaway password, and authenticate.
|
|
116
|
+
*/
|
|
117
|
+
export async function localSignIn(auth: AuthBindings, email: string, name?: string): Promise<Tokens> {
|
|
118
|
+
const cognito = await client('cognito');
|
|
119
|
+
const {
|
|
120
|
+
AdminCreateUserCommand,
|
|
121
|
+
AdminInitiateAuthCommand,
|
|
122
|
+
AdminSetUserPasswordCommand,
|
|
123
|
+
InitiateAuthCommand,
|
|
124
|
+
} = await import('@aws-sdk/client-cognito-identity-provider');
|
|
125
|
+
try {
|
|
126
|
+
await cognito.send(
|
|
127
|
+
new AdminCreateUserCommand({
|
|
128
|
+
UserPoolId: auth.userPoolId,
|
|
129
|
+
Username: email,
|
|
130
|
+
MessageAction: 'SUPPRESS',
|
|
131
|
+
UserAttributes: [
|
|
132
|
+
{ Name: 'email', Value: email },
|
|
133
|
+
{ Name: 'email_verified', Value: 'true' },
|
|
134
|
+
...(name ? [{ Name: 'name', Value: name }] : []),
|
|
135
|
+
],
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (errorName(e) !== 'UsernameExistsException') throw e;
|
|
140
|
+
}
|
|
141
|
+
const password = `Pp1!${randomBytes(18).toString('base64url')}`;
|
|
142
|
+
await cognito.send(
|
|
143
|
+
new AdminSetUserPasswordCommand({
|
|
144
|
+
UserPoolId: auth.userPoolId,
|
|
145
|
+
Username: email,
|
|
146
|
+
Password: password,
|
|
147
|
+
Permanent: true,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
const params = { USERNAME: email, PASSWORD: password };
|
|
151
|
+
let result: { IdToken?: string; RefreshToken?: string } | undefined;
|
|
152
|
+
try {
|
|
153
|
+
const out = await cognito.send(
|
|
154
|
+
new AdminInitiateAuthCommand({
|
|
155
|
+
UserPoolId: auth.userPoolId,
|
|
156
|
+
ClientId: auth.clientId,
|
|
157
|
+
AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
|
|
158
|
+
AuthParameters: params,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
result = out.AuthenticationResult;
|
|
162
|
+
} catch {
|
|
163
|
+
const out = await cognito.send(
|
|
164
|
+
new InitiateAuthCommand({
|
|
165
|
+
ClientId: auth.clientId,
|
|
166
|
+
AuthFlow: 'USER_PASSWORD_AUTH',
|
|
167
|
+
AuthParameters: params,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
result = out.AuthenticationResult;
|
|
171
|
+
}
|
|
172
|
+
if (!result?.IdToken) throw new Error('Local sign-in returned no tokens');
|
|
173
|
+
return { idToken: result.IdToken, ...(result.RefreshToken ? { refreshToken: result.RefreshToken } : {}) };
|
|
174
|
+
}
|
package/src/bindings.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BINDINGS_ENV, type Bindings, decodeBindings } from '@ericminassian/platform-core';
|
|
2
|
+
|
|
3
|
+
let cached: Bindings | undefined;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The stage's bindings, decoded lazily from `PLATFORM_BINDINGS` on first use
|
|
7
|
+
* so importing the SDK never throws; using a resource without bindings does.
|
|
8
|
+
*/
|
|
9
|
+
export function getBindings(): Bindings {
|
|
10
|
+
if (!cached) cached = decodeBindings(process.env[BINDINGS_ENV]);
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Bindings if present, otherwise undefined (telemetry, health). */
|
|
15
|
+
export function tryBindings(): Bindings | undefined {
|
|
16
|
+
try {
|
|
17
|
+
return getBindings();
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Override bindings (tests). Pass undefined to re-read the environment. */
|
|
24
|
+
export function setBindings(b: Bindings | undefined): void {
|
|
25
|
+
cached = b;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function region(): string {
|
|
29
|
+
return tryBindings()?.region ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? 'us-east-1';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** True under `plat dev` / `plat test` (AWS Emulator). */
|
|
33
|
+
export function isEmulator(): boolean {
|
|
34
|
+
return Boolean(process.env.AWS_ENDPOINT_URL);
|
|
35
|
+
}
|
package/src/clients.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
|
|
2
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
import type { EventBridgeClient } from '@aws-sdk/client-eventbridge';
|
|
4
|
+
import type { LambdaClient } from '@aws-sdk/client-lambda';
|
|
5
|
+
import type { S3Client } from '@aws-sdk/client-s3';
|
|
6
|
+
import type { SQSClient } from '@aws-sdk/client-sqs';
|
|
7
|
+
import type { SSMClient } from '@aws-sdk/client-ssm';
|
|
8
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
9
|
+
import { isEmulator, region } from './bindings.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AWS clients, created on first use. Under `plat dev` / `plat test` the
|
|
13
|
+
* standard `AWS_ENDPOINT_URL` points every client at the AWS Emulator and
|
|
14
|
+
* `NODE_EXTRA_CA_CERTS` trusts its certificate; nothing else differs.
|
|
15
|
+
*
|
|
16
|
+
* Only DynamoDB is imported eagerly (most apps use it, and `db.raw()` is
|
|
17
|
+
* synchronous). Everything else is a dynamic import, which esbuild keeps in
|
|
18
|
+
* the same bundle but evaluates lazily — so cold starts only pay for the
|
|
19
|
+
* services a request actually touches.
|
|
20
|
+
*/
|
|
21
|
+
export interface Clients {
|
|
22
|
+
ddb: DynamoDBDocumentClient;
|
|
23
|
+
sqs: SQSClient;
|
|
24
|
+
events: EventBridgeClient;
|
|
25
|
+
s3: S3Client;
|
|
26
|
+
ssm: SSMClient;
|
|
27
|
+
lambda: LambdaClient;
|
|
28
|
+
cognito: CognitoIdentityProviderClient;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type LazyClientName = Exclude<keyof Clients, 'ddb'>;
|
|
32
|
+
|
|
33
|
+
/** Minimal client shape (tests may substitute `{ send }` fakes). */
|
|
34
|
+
export interface SendClient {
|
|
35
|
+
send(command: unknown): Promise<unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const instances: Partial<Record<keyof Clients, unknown>> = {};
|
|
39
|
+
const pending: Partial<Record<keyof Clients, Promise<unknown>>> = {};
|
|
40
|
+
const overrides: Partial<Record<keyof Clients, SendClient>> = {};
|
|
41
|
+
|
|
42
|
+
function config(): { region: string; endpoint?: string } {
|
|
43
|
+
const endpoint = process.env.AWS_ENDPOINT_URL;
|
|
44
|
+
return { region: region(), ...(endpoint ? { endpoint } : {}) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lazyFactories: { [K in LazyClientName]: () => Promise<Clients[K]> } = {
|
|
48
|
+
sqs: async () => new (await import('@aws-sdk/client-sqs')).SQSClient(config()),
|
|
49
|
+
events: async () => new (await import('@aws-sdk/client-eventbridge')).EventBridgeClient(config()),
|
|
50
|
+
s3: async () =>
|
|
51
|
+
new (await import('@aws-sdk/client-s3')).S3Client({
|
|
52
|
+
...config(),
|
|
53
|
+
...(isEmulator() ? { forcePathStyle: true } : {}),
|
|
54
|
+
}),
|
|
55
|
+
ssm: async () => new (await import('@aws-sdk/client-ssm')).SSMClient(config()),
|
|
56
|
+
lambda: async () => new (await import('@aws-sdk/client-lambda')).LambdaClient(config()),
|
|
57
|
+
cognito: async () =>
|
|
58
|
+
new (await import('@aws-sdk/client-cognito-identity-provider')).CognitoIdentityProviderClient(config()),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The DynamoDB document client (synchronous). */
|
|
62
|
+
export function ddbClient(): DynamoDBDocumentClient {
|
|
63
|
+
const o = overrides.ddb;
|
|
64
|
+
if (o) return o as unknown as DynamoDBDocumentClient;
|
|
65
|
+
let c = instances.ddb as DynamoDBDocumentClient | undefined;
|
|
66
|
+
if (!c) {
|
|
67
|
+
c = DynamoDBDocumentClient.from(new DynamoDBClient(config()), {
|
|
68
|
+
marshallOptions: { removeUndefinedValues: true, convertClassInstanceToMap: true },
|
|
69
|
+
});
|
|
70
|
+
instances.ddb = c;
|
|
71
|
+
}
|
|
72
|
+
return c;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function client<K extends LazyClientName>(name: K): Promise<Clients[K]> {
|
|
76
|
+
const o = overrides[name];
|
|
77
|
+
if (o) return o as unknown as Clients[K];
|
|
78
|
+
const existing = instances[name];
|
|
79
|
+
if (existing) return existing as Clients[K];
|
|
80
|
+
pending[name] ??= lazyFactories[name]().then((c) => {
|
|
81
|
+
instances[name] = c;
|
|
82
|
+
return c;
|
|
83
|
+
});
|
|
84
|
+
return pending[name] as Promise<Clients[K]>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Substitute a client (tests). `undefined` removes the override. */
|
|
88
|
+
export function setClient(name: keyof Clients, fake: SendClient | undefined): void {
|
|
89
|
+
if (fake) overrides[name] = fake;
|
|
90
|
+
else delete overrides[name];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Drop cached clients (tests that change region/endpoint between runs). */
|
|
94
|
+
export function resetClients(): void {
|
|
95
|
+
for (const k of Object.keys(instances) as (keyof Clients)[]) delete instances[k];
|
|
96
|
+
for (const k of Object.keys(pending) as (keyof Clients)[]) delete pending[k];
|
|
97
|
+
for (const k of Object.keys(overrides) as (keyof Clients)[]) delete overrides[k];
|
|
98
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { newSpanId, newTraceId, type PlatformUser } from '@ericminassian/platform-core';
|
|
3
|
+
|
|
4
|
+
/** Per-request / per-job context carried through async calls. */
|
|
5
|
+
export interface RequestContext {
|
|
6
|
+
trace: string;
|
|
7
|
+
/** Span id of the root (request or job). */
|
|
8
|
+
span: string;
|
|
9
|
+
/** performance.now() when the root started, for span offsets. */
|
|
10
|
+
startedAt: number;
|
|
11
|
+
/** Set-Cookie values produced while handling the request (e.g. token refresh). */
|
|
12
|
+
cookies: string[];
|
|
13
|
+
/** Matched route pattern, set by the router integration. */
|
|
14
|
+
route?: string;
|
|
15
|
+
/** Resolved user (undefined = not looked up yet, null = signed out). */
|
|
16
|
+
user?: PlatformUser | null;
|
|
17
|
+
/** Job name for error attribution in workers. */
|
|
18
|
+
job?: string;
|
|
19
|
+
/** Error a framework handled itself (e.g. a Hono app's onError), for telemetry. */
|
|
20
|
+
error?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const storage = new AsyncLocalStorage<RequestContext>();
|
|
24
|
+
|
|
25
|
+
export function currentContext(): RequestContext | undefined {
|
|
26
|
+
return storage.getStore();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function runWithContext<T>(ctx: RequestContext, fn: () => T): T {
|
|
30
|
+
return storage.run(ctx, fn);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function newContext(trace?: string | null): RequestContext {
|
|
34
|
+
return { trace: trace || newTraceId(), span: newSpanId(), startedAt: performance.now(), cookies: [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Trace id of the current request/job, or a fresh one outside any context. */
|
|
38
|
+
export function currentTrace(): string {
|
|
39
|
+
return storage.getStore()?.trace ?? newTraceId();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parse a W3C `traceparent` header (`00-<32 hex trace>-<16 hex span>-<flags>`). */
|
|
43
|
+
export function parseTraceparent(header: string | null | undefined): string | undefined {
|
|
44
|
+
if (!header) return undefined;
|
|
45
|
+
const m = /^[\da-f]{2}-([\da-f]{32})-[\da-f]{16}-[\da-f]{2}$/i.exec(header.trim());
|
|
46
|
+
return m?.[1]?.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Record the route pattern (e.g. `/api/todos/:id`) for the current request's
|
|
51
|
+
* telemetry. Hono apps get this automatically; other frameworks can call it.
|
|
52
|
+
*/
|
|
53
|
+
export function setRoute(pattern: string): void {
|
|
54
|
+
const ctx = storage.getStore();
|
|
55
|
+
if (ctx) ctx.route = pattern;
|
|
56
|
+
}
|