@beignet/core 0.0.32 → 0.0.33
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/CHANGELOG.md +35 -0
- package/README.md +344 -19
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +63 -47
- package/dist/application/index.js.map +1 -1
- package/dist/events/index.d.ts +6 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +20 -10
- package/dist/events/index.js.map +1 -1
- package/dist/jobs/index.d.ts +415 -4
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +392 -3
- package/dist/jobs/index.js.map +1 -1
- package/dist/outbox/index.d.ts +150 -1
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +170 -1
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/index.d.ts +6 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +4 -0
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/schedules/index.d.ts +8 -7
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +47 -16
- package/dist/schedules/index.js.map +1 -1
- package/dist/server/hooks/index.d.ts +1 -0
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +1 -0
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +26 -13
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +28 -34
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks/security.d.ts +179 -0
- package/dist/server/hooks/security.d.ts.map +1 -0
- package/dist/server/hooks/security.js +225 -0
- package/dist/server/hooks/security.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +22 -6
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +3 -0
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/runtime-integrity.d.ts +84 -0
- package/dist/server/runtime-integrity.d.ts.map +1 -0
- package/dist/server/runtime-integrity.js +160 -0
- package/dist/server/runtime-integrity.js.map +1 -0
- package/dist/server/server.d.ts +10 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +55 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/trusted-proxy.d.ts +77 -0
- package/dist/server/trusted-proxy.d.ts.map +1 -0
- package/dist/server/trusted-proxy.js +129 -0
- package/dist/server/trusted-proxy.js.map +1 -0
- package/dist/tasks/index.d.ts +6 -7
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +22 -5
- package/dist/tasks/index.js.map +1 -1
- package/dist/tenancy/index.d.ts +41 -0
- package/dist/tenancy/index.d.ts.map +1 -0
- package/dist/tenancy/index.js +34 -0
- package/dist/tenancy/index.js.map +1 -0
- package/dist/tracing/execution.d.ts +14 -0
- package/dist/tracing/execution.d.ts.map +1 -0
- package/dist/tracing/execution.js +17 -0
- package/dist/tracing/execution.js.map +1 -0
- package/dist/tracing/index.d.ts +49 -0
- package/dist/tracing/index.d.ts.map +1 -1
- package/dist/tracing/index.js +59 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/uploads/client.d.ts.map +1 -1
- package/dist/uploads/client.js +44 -6
- package/dist/uploads/client.js.map +1 -1
- package/dist/uploads/index.d.ts +71 -0
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +344 -15
- package/dist/uploads/index.js.map +1 -1
- package/dist/webhooks/index.d.ts +55 -4
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +106 -2
- package/dist/webhooks/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/app-architecture/SKILL.md +23 -2
- package/src/application/index.ts +88 -56
- package/src/events/index.ts +27 -14
- package/src/jobs/index.ts +895 -7
- package/src/outbox/index.ts +358 -2
- package/src/ports/index.ts +17 -0
- package/src/providers/instrumentation.ts +4 -0
- package/src/schedules/index.ts +60 -26
- package/src/server/hooks/index.ts +10 -0
- package/src/server/hooks/rate-limit.ts +52 -46
- package/src/server/hooks/security.ts +503 -0
- package/src/server/index.ts +8 -0
- package/src/server/instrumentation.ts +25 -5
- package/src/server/request-context.ts +7 -0
- package/src/server/runtime-integrity.ts +322 -0
- package/src/server/server.ts +86 -1
- package/src/server/trusted-proxy.ts +249 -0
- package/src/tasks/index.ts +29 -12
- package/src/tenancy/index.ts +55 -0
- package/src/tracing/execution.ts +37 -0
- package/src/tracing/index.ts +137 -0
- package/src/uploads/client.ts +65 -6
- package/src/uploads/index.ts +539 -14
- package/src/webhooks/index.ts +238 -9
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { HttpRequestLike } from "./http.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Header source used to resolve a client IP after an app has explicitly opted
|
|
5
|
+
* into trusting its deployment proxy or edge.
|
|
6
|
+
*/
|
|
7
|
+
export type TrustedProxyClientIpSource =
|
|
8
|
+
| "x-forwarded-for-last"
|
|
9
|
+
| "x-forwarded-for-first"
|
|
10
|
+
| "x-real-ip"
|
|
11
|
+
| "cf-connecting-ip"
|
|
12
|
+
| { header: string }
|
|
13
|
+
| ((req: HttpRequestLike) => string | undefined);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Trusted proxy configuration for request metadata.
|
|
17
|
+
*
|
|
18
|
+
* Beignet trusts no forwarding headers by default. Configure this only when
|
|
19
|
+
* the app is always behind a platform or reverse proxy that strips or
|
|
20
|
+
* normalizes these headers before they reach application code.
|
|
21
|
+
*/
|
|
22
|
+
export interface TrustedProxyOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Header source for the end-user client IP. Omit or set to `false` when no
|
|
25
|
+
* client-IP header should be trusted.
|
|
26
|
+
*/
|
|
27
|
+
clientIp?: TrustedProxyClientIpSource | false;
|
|
28
|
+
/**
|
|
29
|
+
* Header that carries the external request protocol.
|
|
30
|
+
*
|
|
31
|
+
* Defaults to `x-forwarded-proto` when trusted proxy handling is enabled.
|
|
32
|
+
*/
|
|
33
|
+
protocolHeader?: string | false;
|
|
34
|
+
/**
|
|
35
|
+
* Header that carries the external request host.
|
|
36
|
+
*
|
|
37
|
+
* Defaults to `x-forwarded-host` when trusted proxy handling is enabled.
|
|
38
|
+
*/
|
|
39
|
+
hostHeader?: string | false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set to `false` or omit the config to trust no forwarding headers.
|
|
44
|
+
*/
|
|
45
|
+
export type TrustedProxyConfig = false | TrustedProxyOptions;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Request metadata after applying an explicit trusted-proxy policy.
|
|
49
|
+
*/
|
|
50
|
+
export interface TrustedRequestInfo {
|
|
51
|
+
/**
|
|
52
|
+
* URL as seen by the app or reconstructed from trusted proxy headers.
|
|
53
|
+
*/
|
|
54
|
+
url: URL;
|
|
55
|
+
/**
|
|
56
|
+
* External request origin.
|
|
57
|
+
*/
|
|
58
|
+
origin: string;
|
|
59
|
+
/**
|
|
60
|
+
* External request protocol without a trailing colon.
|
|
61
|
+
*/
|
|
62
|
+
protocol: "http" | "https";
|
|
63
|
+
/**
|
|
64
|
+
* External request host, including port when present.
|
|
65
|
+
*/
|
|
66
|
+
host: string;
|
|
67
|
+
/**
|
|
68
|
+
* Resolved client IP when a trusted client-IP source is configured.
|
|
69
|
+
*/
|
|
70
|
+
clientIp?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether forwarding headers were eligible to affect this result.
|
|
73
|
+
*/
|
|
74
|
+
trustedProxy: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DEFAULT_PROTOCOL_HEADER = "x-forwarded-proto";
|
|
78
|
+
const DEFAULT_HOST_HEADER = "x-forwarded-host";
|
|
79
|
+
|
|
80
|
+
function splitForwardedHeader(value: string | null): string[] {
|
|
81
|
+
if (!value) return [];
|
|
82
|
+
return value
|
|
83
|
+
.split(",")
|
|
84
|
+
.map((entry) => entry.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function firstHeaderValue(
|
|
89
|
+
req: HttpRequestLike,
|
|
90
|
+
header: string,
|
|
91
|
+
): string | undefined {
|
|
92
|
+
return splitForwardedHeader(req.headers.get(header))[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requireHeaderName(name: string, optionName: string): string {
|
|
96
|
+
const header = name.trim();
|
|
97
|
+
if (!header) {
|
|
98
|
+
throw new Error(`${optionName} must be a non-empty header name.`);
|
|
99
|
+
}
|
|
100
|
+
return header;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeProtocol(
|
|
104
|
+
value: string | undefined,
|
|
105
|
+
): "http" | "https" | undefined {
|
|
106
|
+
if (!value) return undefined;
|
|
107
|
+
const protocol = value.toLowerCase().replace(/:$/, "");
|
|
108
|
+
if (protocol === "http" || protocol === "https") return protocol;
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasInvalidHostCharacter(value: string): boolean {
|
|
113
|
+
for (const char of value) {
|
|
114
|
+
const code = char.charCodeAt(0);
|
|
115
|
+
if (
|
|
116
|
+
code <= 32 ||
|
|
117
|
+
code === 127 ||
|
|
118
|
+
char === "/" ||
|
|
119
|
+
char === "\\" ||
|
|
120
|
+
char === "@" ||
|
|
121
|
+
char === "?" ||
|
|
122
|
+
char === "#"
|
|
123
|
+
) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeHost(
|
|
131
|
+
value: string | undefined,
|
|
132
|
+
protocol: "http" | "https",
|
|
133
|
+
): string | undefined {
|
|
134
|
+
if (!value || hasInvalidHostCharacter(value)) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
return new URL(`${protocol}://${value}`).host;
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function baseRequestUrl(req: HttpRequestLike): URL {
|
|
146
|
+
try {
|
|
147
|
+
return new URL(req.url);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error("req.url must be an absolute URL.");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizedBaseProtocol(url: URL): "http" | "https" {
|
|
154
|
+
const protocol = normalizeProtocol(url.protocol);
|
|
155
|
+
if (protocol) return protocol;
|
|
156
|
+
throw new Error("req.url must use http or https.");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a client IP from a configured trusted proxy source.
|
|
161
|
+
*/
|
|
162
|
+
export function resolveTrustedClientIp(
|
|
163
|
+
req: HttpRequestLike,
|
|
164
|
+
source: TrustedProxyClientIpSource | false | undefined,
|
|
165
|
+
): string | undefined {
|
|
166
|
+
if (!source) return undefined;
|
|
167
|
+
|
|
168
|
+
if (typeof source === "function") {
|
|
169
|
+
return source(req)?.trim() || undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof source === "object") {
|
|
173
|
+
return firstHeaderValue(
|
|
174
|
+
req,
|
|
175
|
+
requireHeaderName(source.header, "trustedProxy.clientIp.header"),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (source === "x-forwarded-for-first" || source === "x-forwarded-for-last") {
|
|
180
|
+
const entries = splitForwardedHeader(req.headers.get("x-forwarded-for"));
|
|
181
|
+
if (entries.length === 0) return undefined;
|
|
182
|
+
return source === "x-forwarded-for-first"
|
|
183
|
+
? entries[0]
|
|
184
|
+
: entries[entries.length - 1];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return firstHeaderValue(req, source);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve request metadata using only app-visible URL data unless a trusted
|
|
192
|
+
* proxy policy is explicitly configured.
|
|
193
|
+
*/
|
|
194
|
+
export function resolveTrustedRequest(
|
|
195
|
+
req: HttpRequestLike,
|
|
196
|
+
config: TrustedProxyConfig | undefined = false,
|
|
197
|
+
): TrustedRequestInfo {
|
|
198
|
+
const baseUrl = baseRequestUrl(req);
|
|
199
|
+
const baseProtocol = normalizedBaseProtocol(baseUrl);
|
|
200
|
+
const baseHost = baseUrl.host;
|
|
201
|
+
|
|
202
|
+
if (!config) {
|
|
203
|
+
return {
|
|
204
|
+
url: baseUrl,
|
|
205
|
+
origin: baseUrl.origin,
|
|
206
|
+
protocol: baseProtocol,
|
|
207
|
+
host: baseHost,
|
|
208
|
+
trustedProxy: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const protocolHeader =
|
|
213
|
+
config.protocolHeader === false
|
|
214
|
+
? undefined
|
|
215
|
+
: requireHeaderName(
|
|
216
|
+
config.protocolHeader ?? DEFAULT_PROTOCOL_HEADER,
|
|
217
|
+
"trustedProxy.protocolHeader",
|
|
218
|
+
);
|
|
219
|
+
const hostHeader =
|
|
220
|
+
config.hostHeader === false
|
|
221
|
+
? undefined
|
|
222
|
+
: requireHeaderName(
|
|
223
|
+
config.hostHeader ?? DEFAULT_HOST_HEADER,
|
|
224
|
+
"trustedProxy.hostHeader",
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const protocol =
|
|
228
|
+
normalizeProtocol(
|
|
229
|
+
protocolHeader ? firstHeaderValue(req, protocolHeader) : undefined,
|
|
230
|
+
) ?? baseProtocol;
|
|
231
|
+
const host =
|
|
232
|
+
normalizeHost(
|
|
233
|
+
hostHeader ? firstHeaderValue(req, hostHeader) : undefined,
|
|
234
|
+
protocol,
|
|
235
|
+
) ?? baseHost;
|
|
236
|
+
const url = new URL(baseUrl);
|
|
237
|
+
url.protocol = `${protocol}:`;
|
|
238
|
+
url.host = host;
|
|
239
|
+
const clientIp = resolveTrustedClientIp(req, config.clientIp);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
url,
|
|
243
|
+
origin: url.origin,
|
|
244
|
+
protocol,
|
|
245
|
+
host,
|
|
246
|
+
...(clientIp !== undefined ? { clientIp } : {}),
|
|
247
|
+
trustedProxy: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
package/src/tasks/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import { runWithResolvedTracingContext } from "../tracing/execution.js";
|
|
3
|
+
import type { TracingPort } from "../tracing/index.js";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Any Standard Schema compatible validator.
|
|
@@ -75,9 +77,7 @@ export interface TaskHandleArgs<T extends TaskDef, Ctx> {
|
|
|
75
77
|
* Parsed task input.
|
|
76
78
|
*/
|
|
77
79
|
input: InferTaskInput<T>;
|
|
78
|
-
/**
|
|
79
|
-
* Handler context.
|
|
80
|
-
*/
|
|
80
|
+
/** Handler context. */
|
|
81
81
|
ctx: Ctx;
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -115,10 +115,10 @@ export interface RunTaskOptions<Ctx> {
|
|
|
115
115
|
* handler runs.
|
|
116
116
|
*/
|
|
117
117
|
input: unknown;
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
/** Handler context or factory resolved inside the task span. */
|
|
119
|
+
ctx: Ctx | (() => MaybePromise<Ctx>);
|
|
120
|
+
/** Runtime tracing port used before a lazy context factory runs. */
|
|
121
|
+
tracing?: TracingPort;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
@@ -260,12 +260,29 @@ export async function runTask<
|
|
|
260
260
|
T extends TaskDef<string, StandardSchema, Ctx>,
|
|
261
261
|
Ctx,
|
|
262
262
|
>(task: T, options: RunTaskOptions<Ctx>): Promise<InferTaskOutput<T>> {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
const traceAttributes = {
|
|
264
|
+
"beignet.task.name": task.name,
|
|
265
|
+
} as const;
|
|
266
|
+
|
|
267
|
+
return await runWithResolvedTracingContext({
|
|
268
|
+
tracing: options.tracing,
|
|
267
269
|
ctx: options.ctx,
|
|
268
|
-
|
|
270
|
+
operation: {
|
|
271
|
+
name: `beignet.task ${task.name}`,
|
|
272
|
+
type: "task",
|
|
273
|
+
kind: "internal",
|
|
274
|
+
attributes: traceAttributes,
|
|
275
|
+
metricAttributes: traceAttributes,
|
|
276
|
+
},
|
|
277
|
+
run: async (ctx) => {
|
|
278
|
+
const parsed = await parseTaskInput(task, options.input);
|
|
279
|
+
return (await task.handle({
|
|
280
|
+
task,
|
|
281
|
+
input: parsed,
|
|
282
|
+
ctx,
|
|
283
|
+
})) as InferTaskOutput<T>;
|
|
284
|
+
},
|
|
285
|
+
});
|
|
269
286
|
}
|
|
270
287
|
|
|
271
288
|
/**
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ActivityTenant } from "../ports/audit.js";
|
|
2
|
+
import { type RequireOptions, requireTenant } from "../ports/auth.js";
|
|
3
|
+
|
|
4
|
+
const tenantScopeBrand = Symbol("TenantScope");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Branded tenant scope token for repository boundaries.
|
|
8
|
+
*
|
|
9
|
+
* Apps still own tenant resolution and tenant data modeling. `TenantScope`
|
|
10
|
+
* carries the already-resolved activity tenant through app-facing repository
|
|
11
|
+
* ports so adapters can apply tenant predicates without accepting arbitrary
|
|
12
|
+
* caller-provided tenant IDs.
|
|
13
|
+
*/
|
|
14
|
+
export type TenantScope = Readonly<{
|
|
15
|
+
id: string;
|
|
16
|
+
tenant: ActivityTenant;
|
|
17
|
+
readonly [tenantScopeBrand]: true;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a tenant scope from a resolved activity tenant.
|
|
22
|
+
*
|
|
23
|
+
* This does not load, create, or validate a tenant record. It only brands the
|
|
24
|
+
* already-resolved tenant for repository and adapter boundaries.
|
|
25
|
+
*/
|
|
26
|
+
export function createTenantScope(tenant: ActivityTenant): TenantScope {
|
|
27
|
+
return {
|
|
28
|
+
id: tenant.id,
|
|
29
|
+
tenant,
|
|
30
|
+
[tenantScopeBrand]: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return a branded tenant scope from `ctx.tenant` or throw.
|
|
36
|
+
*
|
|
37
|
+
* Throws `TenantRequiredError` by default through `requireTenant`. Pass
|
|
38
|
+
* `options.error` to throw an app-owned error instead.
|
|
39
|
+
*/
|
|
40
|
+
export function requireTenantScope(
|
|
41
|
+
ctx: { tenant?: ActivityTenant | null },
|
|
42
|
+
options?: RequireOptions,
|
|
43
|
+
): TenantScope {
|
|
44
|
+
return createTenantScope(requireTenant(ctx, options));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract the stable tenant ID from a tenant scope.
|
|
49
|
+
*
|
|
50
|
+
* Repository adapters should use this helper at the persistence boundary
|
|
51
|
+
* instead of accepting raw tenant IDs from use cases.
|
|
52
|
+
*/
|
|
53
|
+
export function tenantScopeId(scope: TenantScope): string {
|
|
54
|
+
return scope.id;
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveTracingPort,
|
|
3
|
+
runWithTracing,
|
|
4
|
+
type TraceOperation,
|
|
5
|
+
type TraceSpan,
|
|
6
|
+
type TracingPort,
|
|
7
|
+
} from "./index.js";
|
|
8
|
+
|
|
9
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a lazy application context inside a span when the runtime tracing
|
|
13
|
+
* port is available independently from that context.
|
|
14
|
+
*/
|
|
15
|
+
export async function runWithResolvedTracingContext<Ctx, Result>(args: {
|
|
16
|
+
tracing?: TracingPort;
|
|
17
|
+
ctx: Ctx | (() => MaybePromise<Ctx>);
|
|
18
|
+
operation: TraceOperation;
|
|
19
|
+
run(ctx: Ctx, span?: TraceSpan): MaybePromise<Result>;
|
|
20
|
+
}): Promise<Result> {
|
|
21
|
+
const resolveContext = async (): Promise<Ctx> =>
|
|
22
|
+
typeof args.ctx === "function"
|
|
23
|
+
? await (args.ctx as () => MaybePromise<Ctx>)()
|
|
24
|
+
: args.ctx;
|
|
25
|
+
|
|
26
|
+
if (args.tracing) {
|
|
27
|
+
return await runWithTracing(args.tracing, args.operation, async (span) =>
|
|
28
|
+
args.run(await resolveContext(), span),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ctx = await resolveContext();
|
|
33
|
+
const tracing = resolveTracingPort(ctx);
|
|
34
|
+
return await runWithTracing(tracing, args.operation, (span) =>
|
|
35
|
+
args.run(ctx, span),
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/tracing/index.ts
CHANGED
|
@@ -30,6 +30,10 @@ export interface TraceContext {
|
|
|
30
30
|
* W3C traceparent header value.
|
|
31
31
|
*/
|
|
32
32
|
traceparent: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional W3C tracestate value associated with this context.
|
|
35
|
+
*/
|
|
36
|
+
tracestate?: string;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -62,6 +66,137 @@ export interface TraceContextInput {
|
|
|
62
66
|
spanId?: string;
|
|
63
67
|
parentSpanId?: string;
|
|
64
68
|
traceparent?: string;
|
|
69
|
+
tracestate?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Values accepted as tracing attributes. */
|
|
73
|
+
export type TraceAttributeValue =
|
|
74
|
+
| string
|
|
75
|
+
| number
|
|
76
|
+
| boolean
|
|
77
|
+
| readonly string[]
|
|
78
|
+
| readonly number[]
|
|
79
|
+
| readonly boolean[];
|
|
80
|
+
|
|
81
|
+
/** Attributes attached to a traced Beignet operation. */
|
|
82
|
+
export type TraceAttributes = Readonly<Record<string, TraceAttributeValue>>;
|
|
83
|
+
|
|
84
|
+
/** Span kinds understood by tracing providers. */
|
|
85
|
+
export type TraceSpanKind =
|
|
86
|
+
| "internal"
|
|
87
|
+
| "server"
|
|
88
|
+
| "client"
|
|
89
|
+
| "producer"
|
|
90
|
+
| "consumer";
|
|
91
|
+
|
|
92
|
+
/** Stable Beignet operation categories used by tracing and metrics providers. */
|
|
93
|
+
export type TraceOperationType =
|
|
94
|
+
| "request"
|
|
95
|
+
| "useCase"
|
|
96
|
+
| "listener"
|
|
97
|
+
| "job"
|
|
98
|
+
| "schedule"
|
|
99
|
+
| "task"
|
|
100
|
+
| "outbox"
|
|
101
|
+
| "provider";
|
|
102
|
+
|
|
103
|
+
/** Description of one operation to run inside an active trace span. */
|
|
104
|
+
export interface TraceOperation {
|
|
105
|
+
name: string;
|
|
106
|
+
type: TraceOperationType;
|
|
107
|
+
kind?: TraceSpanKind;
|
|
108
|
+
parent?: TraceContextInput;
|
|
109
|
+
/** Attributes attached to the operation span. */
|
|
110
|
+
attributes?: TraceAttributes;
|
|
111
|
+
/**
|
|
112
|
+
* Bounded attributes attached to operation metrics. Values must describe a
|
|
113
|
+
* stable operation dimension, never a request, actor, tenant, or payload.
|
|
114
|
+
*/
|
|
115
|
+
metricAttributes?: TraceAttributes;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Framework-neutral span handle exposed to Beignet execution wrappers. */
|
|
119
|
+
export interface TraceSpan {
|
|
120
|
+
readonly context: TraceContext;
|
|
121
|
+
setAttribute(name: string, value: TraceAttributeValue): void;
|
|
122
|
+
setAttributes(attributes: TraceAttributes): void;
|
|
123
|
+
addEvent(name: string, attributes?: TraceAttributes): void;
|
|
124
|
+
setStatus(status: "ok" | "error"): void;
|
|
125
|
+
recordError(error: unknown): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Optional app port implemented by production tracing integrations. */
|
|
129
|
+
export interface TracingPort {
|
|
130
|
+
current(): TraceContext | undefined;
|
|
131
|
+
startActiveSpan<T>(operation: TraceOperation, run: (span: TraceSpan) => T): T;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
135
|
+
return typeof value === "object" && value !== null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Return whether a value implements `TracingPort`. */
|
|
139
|
+
export function isTracingPort(value: unknown): value is TracingPort {
|
|
140
|
+
return (
|
|
141
|
+
isObject(value) &&
|
|
142
|
+
"current" in value &&
|
|
143
|
+
"startActiveSpan" in value &&
|
|
144
|
+
typeof value.current === "function" &&
|
|
145
|
+
typeof value.startActiveSpan === "function"
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Resolve a tracing port from a direct port, ports object, or app context. */
|
|
150
|
+
export function resolveTracingPort(target: unknown): TracingPort | undefined {
|
|
151
|
+
if (!target) return undefined;
|
|
152
|
+
if (isTracingPort(target)) return target;
|
|
153
|
+
if (!isObject(target)) return undefined;
|
|
154
|
+
|
|
155
|
+
const direct = "tracing" in target ? (target.tracing as unknown) : undefined;
|
|
156
|
+
if (isTracingPort(direct)) return direct;
|
|
157
|
+
|
|
158
|
+
const ports = "ports" in target ? target.ports : undefined;
|
|
159
|
+
if (!ports || ports === target) return undefined;
|
|
160
|
+
return resolveTracingPort(ports);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Resolve trace fields from a trace context or context-like object. */
|
|
164
|
+
export function resolveTraceContextInput(
|
|
165
|
+
target: unknown,
|
|
166
|
+
): TraceContextInput | undefined {
|
|
167
|
+
if (!isObject(target)) return undefined;
|
|
168
|
+
|
|
169
|
+
const context = target as TraceContextInput;
|
|
170
|
+
if (
|
|
171
|
+
context.traceId ||
|
|
172
|
+
context.spanId ||
|
|
173
|
+
context.parentSpanId ||
|
|
174
|
+
context.traceparent ||
|
|
175
|
+
context.tracestate
|
|
176
|
+
) {
|
|
177
|
+
return {
|
|
178
|
+
...(context.traceId ? { traceId: context.traceId } : {}),
|
|
179
|
+
...(context.spanId ? { spanId: context.spanId } : {}),
|
|
180
|
+
...(context.parentSpanId ? { parentSpanId: context.parentSpanId } : {}),
|
|
181
|
+
...(context.traceparent ? { traceparent: context.traceparent } : {}),
|
|
182
|
+
...(context.tracestate ? { tracestate: context.tracestate } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const traceContext = "trace" in target ? target.trace : undefined;
|
|
187
|
+
if (!traceContext || traceContext === target) return undefined;
|
|
188
|
+
return resolveTraceContextInput(traceContext);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Run a callback inside a tracing span when an app tracing port is installed. */
|
|
192
|
+
export function runWithTracing<T>(
|
|
193
|
+
target: unknown,
|
|
194
|
+
operation: TraceOperation,
|
|
195
|
+
run: (span?: TraceSpan) => T,
|
|
196
|
+
): T {
|
|
197
|
+
const tracing = resolveTracingPort(target);
|
|
198
|
+
if (!tracing) return run();
|
|
199
|
+
return tracing.startActiveSpan(operation, (span) => run(span));
|
|
65
200
|
}
|
|
66
201
|
|
|
67
202
|
function createHexId(length: number): string {
|
|
@@ -159,6 +294,7 @@ export function createTraceContext(
|
|
|
159
294
|
spanId,
|
|
160
295
|
traceFlags: parsed?.traceFlags,
|
|
161
296
|
}),
|
|
297
|
+
...(input.tracestate ? { tracestate: input.tracestate } : {}),
|
|
162
298
|
};
|
|
163
299
|
}
|
|
164
300
|
|
|
@@ -172,5 +308,6 @@ export function createChildTraceContext(
|
|
|
172
308
|
traceId: parent.traceId,
|
|
173
309
|
parentSpanId: parent.spanId,
|
|
174
310
|
traceparent: parent.traceparent,
|
|
311
|
+
tracestate: parent.tracestate,
|
|
175
312
|
});
|
|
176
313
|
}
|
package/src/uploads/client.ts
CHANGED
|
@@ -391,6 +391,17 @@ export function createUploadClient<
|
|
|
391
391
|
uploadName: Name,
|
|
392
392
|
prepareOptions: UploadClientPrepareOptions<UploadByName<Registry, Name>>,
|
|
393
393
|
) {
|
|
394
|
+
const fileConstraints = constraints(uploadName);
|
|
395
|
+
const files = await Promise.all(
|
|
396
|
+
prepareOptions.files.map((file) =>
|
|
397
|
+
fileIntentFromFile({
|
|
398
|
+
file,
|
|
399
|
+
constraints: fileConstraints,
|
|
400
|
+
uploadName,
|
|
401
|
+
}),
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
|
|
394
405
|
return requestJson<PrepareUploadResult>({
|
|
395
406
|
fetchImpl,
|
|
396
407
|
url: actionUrl(baseUrl, uploadName, "prepare"),
|
|
@@ -403,7 +414,7 @@ export function createUploadClient<
|
|
|
403
414
|
signal: prepareOptions.signal,
|
|
404
415
|
body: JSON.stringify({
|
|
405
416
|
metadata: prepareOptions.metadata,
|
|
406
|
-
files
|
|
417
|
+
files,
|
|
407
418
|
}),
|
|
408
419
|
},
|
|
409
420
|
});
|
|
@@ -564,12 +575,28 @@ export function createUploadClient<
|
|
|
564
575
|
};
|
|
565
576
|
}
|
|
566
577
|
|
|
567
|
-
function fileIntentFromFile(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
578
|
+
async function fileIntentFromFile(args: {
|
|
579
|
+
file: File;
|
|
580
|
+
constraints: UploadFileConstraints | undefined;
|
|
581
|
+
uploadName: string;
|
|
582
|
+
}): Promise<UploadFileIntent> {
|
|
583
|
+
const contentType =
|
|
584
|
+
normalizeContentType(args.file.type) || "application/octet-stream";
|
|
585
|
+
const intent: UploadFileIntent = {
|
|
586
|
+
name: args.file.name,
|
|
587
|
+
contentType,
|
|
588
|
+
size: args.file.size,
|
|
572
589
|
};
|
|
590
|
+
|
|
591
|
+
const checksumRequirement = args.constraints?.checksum;
|
|
592
|
+
if (checksumRequirement?.algorithm === "sha256") {
|
|
593
|
+
intent.checksum = await createClientUploadChecksum(args.file, {
|
|
594
|
+
uploadName: args.uploadName,
|
|
595
|
+
required: checksumRequirement.required !== false,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return intent;
|
|
573
600
|
}
|
|
574
601
|
|
|
575
602
|
function completeFileInput(file: PreparedUploadResultFile): PreparedUploadFile {
|
|
@@ -577,15 +604,47 @@ function completeFileInput(file: PreparedUploadResultFile): PreparedUploadFile {
|
|
|
577
604
|
name: file.name,
|
|
578
605
|
contentType: file.contentType,
|
|
579
606
|
size: file.size,
|
|
607
|
+
...(file.checksum ? { checksum: file.checksum } : {}),
|
|
580
608
|
uploadId: file.uploadId,
|
|
581
609
|
key: file.key,
|
|
582
610
|
};
|
|
583
611
|
}
|
|
584
612
|
|
|
613
|
+
async function createClientUploadChecksum(
|
|
614
|
+
file: File,
|
|
615
|
+
options: { uploadName: string; required: boolean },
|
|
616
|
+
): Promise<UploadFileIntent["checksum"]> {
|
|
617
|
+
if (!globalThis.crypto?.subtle) {
|
|
618
|
+
if (!options.required) return undefined;
|
|
619
|
+
|
|
620
|
+
throw new UploadClientError({
|
|
621
|
+
operation: "prepare upload",
|
|
622
|
+
uploadName: options.uploadName,
|
|
623
|
+
code: "UPLOAD_CHECKSUM_UNAVAILABLE",
|
|
624
|
+
message: `Upload "${options.uploadName}" requires Web Crypto to compute checksums.`,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
629
|
+
"SHA-256",
|
|
630
|
+
await file.arrayBuffer(),
|
|
631
|
+
);
|
|
632
|
+
return {
|
|
633
|
+
algorithm: "sha256",
|
|
634
|
+
value: [...new Uint8Array(digest)]
|
|
635
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
636
|
+
.join(""),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
585
640
|
function normalizeBaseUrl(baseUrl: string): string {
|
|
586
641
|
return baseUrl.replace(/\/+$/, "");
|
|
587
642
|
}
|
|
588
643
|
|
|
644
|
+
function normalizeContentType(contentType: string): string {
|
|
645
|
+
return contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
646
|
+
}
|
|
647
|
+
|
|
589
648
|
function actionUrl(
|
|
590
649
|
baseUrl: string,
|
|
591
650
|
uploadName: string,
|