@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +344 -19
  3. package/dist/application/index.d.ts.map +1 -1
  4. package/dist/application/index.js +63 -47
  5. package/dist/application/index.js.map +1 -1
  6. package/dist/events/index.d.ts +6 -0
  7. package/dist/events/index.d.ts.map +1 -1
  8. package/dist/events/index.js +20 -10
  9. package/dist/events/index.js.map +1 -1
  10. package/dist/jobs/index.d.ts +415 -4
  11. package/dist/jobs/index.d.ts.map +1 -1
  12. package/dist/jobs/index.js +392 -3
  13. package/dist/jobs/index.js.map +1 -1
  14. package/dist/outbox/index.d.ts +150 -1
  15. package/dist/outbox/index.d.ts.map +1 -1
  16. package/dist/outbox/index.js +170 -1
  17. package/dist/outbox/index.js.map +1 -1
  18. package/dist/ports/index.d.ts +6 -1
  19. package/dist/ports/index.d.ts.map +1 -1
  20. package/dist/ports/index.js +1 -0
  21. package/dist/ports/index.js.map +1 -1
  22. package/dist/providers/instrumentation.d.ts +4 -0
  23. package/dist/providers/instrumentation.d.ts.map +1 -1
  24. package/dist/providers/instrumentation.js.map +1 -1
  25. package/dist/schedules/index.d.ts +8 -7
  26. package/dist/schedules/index.d.ts.map +1 -1
  27. package/dist/schedules/index.js +47 -16
  28. package/dist/schedules/index.js.map +1 -1
  29. package/dist/server/hooks/index.d.ts +1 -0
  30. package/dist/server/hooks/index.d.ts.map +1 -1
  31. package/dist/server/hooks/index.js +1 -0
  32. package/dist/server/hooks/index.js.map +1 -1
  33. package/dist/server/hooks/rate-limit.d.ts +26 -13
  34. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  35. package/dist/server/hooks/rate-limit.js +28 -34
  36. package/dist/server/hooks/rate-limit.js.map +1 -1
  37. package/dist/server/hooks/security.d.ts +179 -0
  38. package/dist/server/hooks/security.d.ts.map +1 -0
  39. package/dist/server/hooks/security.js +225 -0
  40. package/dist/server/hooks/security.js.map +1 -0
  41. package/dist/server/index.d.ts +8 -0
  42. package/dist/server/index.d.ts.map +1 -1
  43. package/dist/server/index.js +8 -0
  44. package/dist/server/index.js.map +1 -1
  45. package/dist/server/instrumentation.d.ts.map +1 -1
  46. package/dist/server/instrumentation.js +22 -6
  47. package/dist/server/instrumentation.js.map +1 -1
  48. package/dist/server/request-context.d.ts +4 -0
  49. package/dist/server/request-context.d.ts.map +1 -1
  50. package/dist/server/request-context.js +3 -0
  51. package/dist/server/request-context.js.map +1 -1
  52. package/dist/server/runtime-integrity.d.ts +84 -0
  53. package/dist/server/runtime-integrity.d.ts.map +1 -0
  54. package/dist/server/runtime-integrity.js +160 -0
  55. package/dist/server/runtime-integrity.js.map +1 -0
  56. package/dist/server/server.d.ts +10 -0
  57. package/dist/server/server.d.ts.map +1 -1
  58. package/dist/server/server.js +55 -1
  59. package/dist/server/server.js.map +1 -1
  60. package/dist/server/trusted-proxy.d.ts +77 -0
  61. package/dist/server/trusted-proxy.d.ts.map +1 -0
  62. package/dist/server/trusted-proxy.js +129 -0
  63. package/dist/server/trusted-proxy.js.map +1 -0
  64. package/dist/tasks/index.d.ts +6 -7
  65. package/dist/tasks/index.d.ts.map +1 -1
  66. package/dist/tasks/index.js +22 -5
  67. package/dist/tasks/index.js.map +1 -1
  68. package/dist/tenancy/index.d.ts +41 -0
  69. package/dist/tenancy/index.d.ts.map +1 -0
  70. package/dist/tenancy/index.js +34 -0
  71. package/dist/tenancy/index.js.map +1 -0
  72. package/dist/tracing/execution.d.ts +14 -0
  73. package/dist/tracing/execution.d.ts.map +1 -0
  74. package/dist/tracing/execution.js +17 -0
  75. package/dist/tracing/execution.js.map +1 -0
  76. package/dist/tracing/index.d.ts +49 -0
  77. package/dist/tracing/index.d.ts.map +1 -1
  78. package/dist/tracing/index.js +59 -0
  79. package/dist/tracing/index.js.map +1 -1
  80. package/dist/uploads/client.d.ts.map +1 -1
  81. package/dist/uploads/client.js +44 -6
  82. package/dist/uploads/client.js.map +1 -1
  83. package/dist/uploads/index.d.ts +71 -0
  84. package/dist/uploads/index.d.ts.map +1 -1
  85. package/dist/uploads/index.js +344 -15
  86. package/dist/uploads/index.js.map +1 -1
  87. package/dist/webhooks/index.d.ts +55 -4
  88. package/dist/webhooks/index.d.ts.map +1 -1
  89. package/dist/webhooks/index.js +106 -2
  90. package/dist/webhooks/index.js.map +1 -1
  91. package/package.json +5 -1
  92. package/skills/app-architecture/SKILL.md +23 -2
  93. package/src/application/index.ts +88 -56
  94. package/src/events/index.ts +27 -14
  95. package/src/jobs/index.ts +895 -7
  96. package/src/outbox/index.ts +358 -2
  97. package/src/ports/index.ts +17 -0
  98. package/src/providers/instrumentation.ts +4 -0
  99. package/src/schedules/index.ts +60 -26
  100. package/src/server/hooks/index.ts +10 -0
  101. package/src/server/hooks/rate-limit.ts +52 -46
  102. package/src/server/hooks/security.ts +503 -0
  103. package/src/server/index.ts +8 -0
  104. package/src/server/instrumentation.ts +25 -5
  105. package/src/server/request-context.ts +7 -0
  106. package/src/server/runtime-integrity.ts +322 -0
  107. package/src/server/server.ts +86 -1
  108. package/src/server/trusted-proxy.ts +249 -0
  109. package/src/tasks/index.ts +29 -12
  110. package/src/tenancy/index.ts +55 -0
  111. package/src/tracing/execution.ts +37 -0
  112. package/src/tracing/index.ts +137 -0
  113. package/src/uploads/client.ts +65 -6
  114. package/src/uploads/index.ts +539 -14
  115. 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
+ }
@@ -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
- * Handler context.
120
- */
121
- ctx: Ctx;
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 parsed = await parseTaskInput(task, options.input);
264
- return (await task.handle({
265
- task,
266
- input: parsed,
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
- })) as InferTaskOutput<T>;
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
+ }
@@ -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
  }
@@ -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: prepareOptions.files.map(fileIntentFromFile),
417
+ files,
407
418
  }),
408
419
  },
409
420
  });
@@ -564,12 +575,28 @@ export function createUploadClient<
564
575
  };
565
576
  }
566
577
 
567
- function fileIntentFromFile(file: File): UploadFileIntent {
568
- return {
569
- name: file.name,
570
- contentType: file.type || "application/octet-stream",
571
- size: file.size,
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,