@beignet/core 0.0.31 → 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 (156) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +346 -21
  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/domain/entity.d.ts +37 -6
  7. package/dist/domain/entity.d.ts.map +1 -1
  8. package/dist/domain/entity.js +9 -7
  9. package/dist/domain/entity.js.map +1 -1
  10. package/dist/domain/index.d.ts +4 -4
  11. package/dist/domain/index.d.ts.map +1 -1
  12. package/dist/domain/index.js +4 -4
  13. package/dist/domain/index.js.map +1 -1
  14. package/dist/events/index.d.ts +6 -0
  15. package/dist/events/index.d.ts.map +1 -1
  16. package/dist/events/index.js +20 -10
  17. package/dist/events/index.js.map +1 -1
  18. package/dist/flags/index.d.ts +5 -4
  19. package/dist/flags/index.d.ts.map +1 -1
  20. package/dist/flags/index.js +10 -9
  21. package/dist/flags/index.js.map +1 -1
  22. package/dist/idempotency/index.d.ts +11 -1
  23. package/dist/idempotency/index.d.ts.map +1 -1
  24. package/dist/idempotency/index.js +4 -3
  25. package/dist/idempotency/index.js.map +1 -1
  26. package/dist/jobs/index.d.ts +449 -9
  27. package/dist/jobs/index.d.ts.map +1 -1
  28. package/dist/jobs/index.js +472 -12
  29. package/dist/jobs/index.js.map +1 -1
  30. package/dist/outbox/index.d.ts +170 -3
  31. package/dist/outbox/index.d.ts.map +1 -1
  32. package/dist/outbox/index.js +192 -8
  33. package/dist/outbox/index.js.map +1 -1
  34. package/dist/ports/events.d.ts +2 -2
  35. package/dist/ports/index.d.ts +6 -1
  36. package/dist/ports/index.d.ts.map +1 -1
  37. package/dist/ports/index.js +1 -0
  38. package/dist/ports/index.js.map +1 -1
  39. package/dist/providers/instrumentation.d.ts +4 -0
  40. package/dist/providers/instrumentation.d.ts.map +1 -1
  41. package/dist/providers/instrumentation.js.map +1 -1
  42. package/dist/providers/provider.d.ts +10 -2
  43. package/dist/providers/provider.d.ts.map +1 -1
  44. package/dist/providers/provider.js.map +1 -1
  45. package/dist/schedules/index.d.ts +8 -7
  46. package/dist/schedules/index.d.ts.map +1 -1
  47. package/dist/schedules/index.js +47 -16
  48. package/dist/schedules/index.js.map +1 -1
  49. package/dist/search/index.d.ts +2 -1
  50. package/dist/search/index.d.ts.map +1 -1
  51. package/dist/search/index.js +1 -0
  52. package/dist/search/index.js.map +1 -1
  53. package/dist/server/hooks/index.d.ts +1 -0
  54. package/dist/server/hooks/index.d.ts.map +1 -1
  55. package/dist/server/hooks/index.js +1 -0
  56. package/dist/server/hooks/index.js.map +1 -1
  57. package/dist/server/hooks/rate-limit.d.ts +26 -13
  58. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  59. package/dist/server/hooks/rate-limit.js +28 -34
  60. package/dist/server/hooks/rate-limit.js.map +1 -1
  61. package/dist/server/hooks/security.d.ts +179 -0
  62. package/dist/server/hooks/security.d.ts.map +1 -0
  63. package/dist/server/hooks/security.js +225 -0
  64. package/dist/server/hooks/security.js.map +1 -0
  65. package/dist/server/index.d.ts +8 -0
  66. package/dist/server/index.d.ts.map +1 -1
  67. package/dist/server/index.js +8 -0
  68. package/dist/server/index.js.map +1 -1
  69. package/dist/server/instrumentation.d.ts.map +1 -1
  70. package/dist/server/instrumentation.js +22 -6
  71. package/dist/server/instrumentation.js.map +1 -1
  72. package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
  73. package/dist/server/providers/loadProviderConfig.js +15 -2
  74. package/dist/server/providers/loadProviderConfig.js.map +1 -1
  75. package/dist/server/request-context.d.ts +4 -0
  76. package/dist/server/request-context.d.ts.map +1 -1
  77. package/dist/server/request-context.js +3 -0
  78. package/dist/server/request-context.js.map +1 -1
  79. package/dist/server/runtime-integrity.d.ts +84 -0
  80. package/dist/server/runtime-integrity.d.ts.map +1 -0
  81. package/dist/server/runtime-integrity.js +160 -0
  82. package/dist/server/runtime-integrity.js.map +1 -0
  83. package/dist/server/server.d.ts +10 -0
  84. package/dist/server/server.d.ts.map +1 -1
  85. package/dist/server/server.js +55 -1
  86. package/dist/server/server.js.map +1 -1
  87. package/dist/server/trusted-proxy.d.ts +77 -0
  88. package/dist/server/trusted-proxy.d.ts.map +1 -0
  89. package/dist/server/trusted-proxy.js +129 -0
  90. package/dist/server/trusted-proxy.js.map +1 -0
  91. package/dist/tasks/index.d.ts +6 -7
  92. package/dist/tasks/index.d.ts.map +1 -1
  93. package/dist/tasks/index.js +22 -5
  94. package/dist/tasks/index.js.map +1 -1
  95. package/dist/tenancy/index.d.ts +41 -0
  96. package/dist/tenancy/index.d.ts.map +1 -0
  97. package/dist/tenancy/index.js +34 -0
  98. package/dist/tenancy/index.js.map +1 -0
  99. package/dist/testing/index.d.ts +6 -0
  100. package/dist/testing/index.d.ts.map +1 -1
  101. package/dist/testing/index.js +12 -3
  102. package/dist/testing/index.js.map +1 -1
  103. package/dist/tracing/execution.d.ts +14 -0
  104. package/dist/tracing/execution.d.ts.map +1 -0
  105. package/dist/tracing/execution.js +17 -0
  106. package/dist/tracing/execution.js.map +1 -0
  107. package/dist/tracing/index.d.ts +49 -0
  108. package/dist/tracing/index.d.ts.map +1 -1
  109. package/dist/tracing/index.js +59 -0
  110. package/dist/tracing/index.js.map +1 -1
  111. package/dist/uploads/client.d.ts.map +1 -1
  112. package/dist/uploads/client.js +44 -6
  113. package/dist/uploads/client.js.map +1 -1
  114. package/dist/uploads/index.d.ts +182 -2
  115. package/dist/uploads/index.d.ts.map +1 -1
  116. package/dist/uploads/index.js +468 -76
  117. package/dist/uploads/index.js.map +1 -1
  118. package/dist/webhooks/index.d.ts +56 -4
  119. package/dist/webhooks/index.d.ts.map +1 -1
  120. package/dist/webhooks/index.js +107 -2
  121. package/dist/webhooks/index.js.map +1 -1
  122. package/package.json +5 -1
  123. package/skills/app-architecture/SKILL.md +23 -2
  124. package/src/application/index.ts +88 -56
  125. package/src/domain/entity.ts +61 -13
  126. package/src/domain/index.ts +8 -7
  127. package/src/events/index.ts +27 -14
  128. package/src/flags/index.ts +23 -19
  129. package/src/idempotency/index.ts +17 -3
  130. package/src/jobs/index.ts +1022 -19
  131. package/src/outbox/index.ts +409 -9
  132. package/src/ports/events.ts +2 -2
  133. package/src/ports/index.ts +17 -0
  134. package/src/providers/instrumentation.ts +4 -0
  135. package/src/providers/provider.ts +11 -2
  136. package/src/schedules/index.ts +60 -26
  137. package/src/search/index.ts +3 -1
  138. package/src/server/hooks/index.ts +10 -0
  139. package/src/server/hooks/rate-limit.ts +52 -46
  140. package/src/server/hooks/security.ts +503 -0
  141. package/src/server/index.ts +8 -0
  142. package/src/server/instrumentation.ts +25 -5
  143. package/src/server/providers/loadProviderConfig.ts +19 -2
  144. package/src/server/request-context.ts +7 -0
  145. package/src/server/runtime-integrity.ts +322 -0
  146. package/src/server/server.ts +86 -1
  147. package/src/server/trusted-proxy.ts +249 -0
  148. package/src/tasks/index.ts +29 -12
  149. package/src/tenancy/index.ts +55 -0
  150. package/src/testing/index.ts +19 -3
  151. package/src/tracing/execution.ts +37 -0
  152. package/src/tracing/index.ts +137 -0
  153. package/src/uploads/client.ts +65 -6
  154. package/src/uploads/index.ts +713 -76
  155. package/src/webhooks/index.ts +240 -9
  156. package/src/domain/events.ts +0 -59
@@ -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
+ }
@@ -66,6 +66,7 @@ import type {
66
66
  } from "../providers/provider.js";
67
67
  import { createMemorySearch, type MemorySearchPort } from "../search/index.js";
68
68
  import { createAmbientAuditLog } from "../server/audit-context.js";
69
+ import { loadProviderConfig } from "../server/providers/loadProviderConfig.js";
69
70
  import {
70
71
  clearActiveRequestContext,
71
72
  enterActiveRequestContext,
@@ -377,8 +378,10 @@ export function createTestPorts<
377
378
  const errorReporter = createMemoryErrorReporter();
378
379
  const flags = createMemoryFlags();
379
380
  const { jobs, dispatchedJobs } = createRecordingJobDispatcher();
380
- const idempotency = createMemoryIdempotencyStore();
381
381
  const ids = options.ids ?? createUuidIdGenerator();
382
+ const idempotency = createMemoryIdempotencyStore({
383
+ now: () => clock.now(),
384
+ });
382
385
  const logger = createNoopLogger();
383
386
  const locks = createMemoryLocks({
384
387
  now: () => clock.now(),
@@ -386,7 +389,10 @@ export function createTestPorts<
386
389
  });
387
390
  const mailer = createMemoryMailer();
388
391
  const notifications = createMemoryNotificationPort();
389
- const outbox = createMemoryOutbox();
392
+ const outbox = createMemoryOutbox({
393
+ id: () => ids.nextId(),
394
+ now: () => clock.now(),
395
+ });
390
396
  const payments = createMemoryPayments();
391
397
  const rateLimit = createMemoryRateLimiter();
392
398
  const search = createMemorySearch();
@@ -983,6 +989,12 @@ function createUnboundPortGuard<Ports extends AnyPorts>(ports: Ports): Ports {
983
989
  * Options for `installProviderForTest(...)`.
984
990
  */
985
991
  export interface InstallProviderForTestOptions {
992
+ /**
993
+ * Environment variables used to resolve the provider's config through the
994
+ * same loader the server uses (including the provider's `overrides`).
995
+ * Ignored when `config` is passed explicitly.
996
+ */
997
+ env?: Record<string, string | undefined>;
986
998
  /**
987
999
  * Base app ports visible to provider setup, such as `{ devtools }`.
988
1000
  */
@@ -1047,9 +1059,13 @@ export async function installProviderForTest(
1047
1059
  "Pass createServiceContext to installProviderForTest(...) when the test needs a service context.",
1048
1060
  );
1049
1061
  });
1062
+ const config =
1063
+ options.config !== undefined
1064
+ ? options.config
1065
+ : await loadProviderConfig(provider, options.env ?? {}, {});
1050
1066
  const result = await provider.setup({
1051
1067
  ports: basePorts,
1052
- config: options.config,
1068
+ config,
1053
1069
  createServiceContext,
1054
1070
  });
1055
1071
  const ports: Record<string, unknown> = { ...basePorts, ...result.ports };
@@ -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,