@beignet/core 0.0.36 → 0.0.38

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 (145) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +135 -19
  3. package/dist/client/client.d.ts.map +1 -1
  4. package/dist/client/client.js +6 -28
  5. package/dist/client/client.js.map +1 -1
  6. package/dist/contracts/contract-builder.d.ts +7 -0
  7. package/dist/contracts/contract-builder.d.ts.map +1 -1
  8. package/dist/contracts/contract-builder.js +22 -0
  9. package/dist/contracts/contract-builder.js.map +1 -1
  10. package/dist/contracts/contract-group.d.ts +7 -0
  11. package/dist/contracts/contract-group.d.ts.map +1 -1
  12. package/dist/contracts/contract-group.js +14 -0
  13. package/dist/contracts/contract-group.js.map +1 -1
  14. package/dist/contracts/index.d.ts +4 -0
  15. package/dist/contracts/index.d.ts.map +1 -1
  16. package/dist/contracts/index.js +4 -0
  17. package/dist/contracts/index.js.map +1 -1
  18. package/dist/contracts/lifecycle.d.ts +40 -0
  19. package/dist/contracts/lifecycle.d.ts.map +1 -0
  20. package/dist/contracts/lifecycle.js +174 -0
  21. package/dist/contracts/lifecycle.js.map +1 -0
  22. package/dist/contracts/types.d.ts +5 -0
  23. package/dist/contracts/types.d.ts.map +1 -1
  24. package/dist/contracts/types.js.map +1 -1
  25. package/dist/error-reporting/index.d.ts +42 -1
  26. package/dist/error-reporting/index.d.ts.map +1 -1
  27. package/dist/error-reporting/index.js +60 -0
  28. package/dist/error-reporting/index.js.map +1 -1
  29. package/dist/error-reporting/internal.d.ts +11 -0
  30. package/dist/error-reporting/internal.d.ts.map +1 -0
  31. package/dist/error-reporting/internal.js +36 -0
  32. package/dist/error-reporting/internal.js.map +1 -0
  33. package/dist/events/index.d.ts +9 -4
  34. package/dist/events/index.d.ts.map +1 -1
  35. package/dist/events/index.js +5 -3
  36. package/dist/events/index.js.map +1 -1
  37. package/dist/idempotency/index.d.ts +25 -2
  38. package/dist/idempotency/index.d.ts.map +1 -1
  39. package/dist/idempotency/index.js +35 -5
  40. package/dist/idempotency/index.js.map +1 -1
  41. package/dist/jobs/index.d.ts +26 -4
  42. package/dist/jobs/index.d.ts.map +1 -1
  43. package/dist/jobs/index.js +51 -6
  44. package/dist/jobs/index.js.map +1 -1
  45. package/dist/locks/index.d.ts +19 -1
  46. package/dist/locks/index.d.ts.map +1 -1
  47. package/dist/locks/index.js +27 -12
  48. package/dist/locks/index.js.map +1 -1
  49. package/dist/openapi/index.d.ts +3 -1
  50. package/dist/openapi/index.d.ts.map +1 -1
  51. package/dist/openapi/index.js +14 -2
  52. package/dist/openapi/index.js.map +1 -1
  53. package/dist/outbox/index.d.ts +23 -4
  54. package/dist/outbox/index.d.ts.map +1 -1
  55. package/dist/outbox/index.js +71 -23
  56. package/dist/outbox/index.js.map +1 -1
  57. package/dist/ports/events.d.ts +5 -5
  58. package/dist/ports/events.d.ts.map +1 -1
  59. package/dist/ports/index.d.ts +4 -4
  60. package/dist/ports/unbound.js +1 -1
  61. package/dist/ports/unbound.js.map +1 -1
  62. package/dist/ports/unit-of-work.d.ts +4 -1
  63. package/dist/ports/unit-of-work.d.ts.map +1 -1
  64. package/dist/ports/unit-of-work.js +4 -3
  65. package/dist/ports/unit-of-work.js.map +1 -1
  66. package/dist/query-codec.d.ts +3 -0
  67. package/dist/query-codec.d.ts.map +1 -0
  68. package/dist/query-codec.js +110 -0
  69. package/dist/query-codec.js.map +1 -0
  70. package/dist/server/hooks/cors.d.ts +3 -2
  71. package/dist/server/hooks/cors.d.ts.map +1 -1
  72. package/dist/server/hooks/cors.js +7 -3
  73. package/dist/server/hooks/cors.js.map +1 -1
  74. package/dist/server/hooks/error-reporting.d.ts +8 -0
  75. package/dist/server/hooks/error-reporting.d.ts.map +1 -1
  76. package/dist/server/hooks/error-reporting.js +55 -25
  77. package/dist/server/hooks/error-reporting.js.map +1 -1
  78. package/dist/server/hooks/idempotency.d.ts +1 -1
  79. package/dist/server/hooks/idempotency.d.ts.map +1 -1
  80. package/dist/server/hooks/idempotency.js +30 -9
  81. package/dist/server/hooks/idempotency.js.map +1 -1
  82. package/dist/server/request-executor.d.ts.map +1 -1
  83. package/dist/server/request-executor.js +7 -12
  84. package/dist/server/request-executor.js.map +1 -1
  85. package/dist/server/request-preparation.d.ts.map +1 -1
  86. package/dist/server/request-preparation.js +3 -1
  87. package/dist/server/request-preparation.js.map +1 -1
  88. package/dist/server/response-finalization.d.ts +5 -1
  89. package/dist/server/response-finalization.d.ts.map +1 -1
  90. package/dist/server/response-finalization.js +66 -2
  91. package/dist/server/response-finalization.js.map +1 -1
  92. package/dist/server/server.d.ts.map +1 -1
  93. package/dist/server/server.js +10 -1
  94. package/dist/server/server.js.map +1 -1
  95. package/dist/testing/index.d.ts +5 -5
  96. package/dist/testing/index.d.ts.map +1 -1
  97. package/dist/testing/index.js +9 -4
  98. package/dist/testing/index.js.map +1 -1
  99. package/dist/tracing/index.d.ts +27 -0
  100. package/dist/tracing/index.d.ts.map +1 -1
  101. package/dist/tracing/index.js +96 -0
  102. package/dist/tracing/index.js.map +1 -1
  103. package/dist/uploads/index.d.ts.map +1 -1
  104. package/dist/uploads/index.js +39 -6
  105. package/dist/uploads/index.js.map +1 -1
  106. package/dist/webhooks/index.d.ts +2 -1
  107. package/dist/webhooks/index.d.ts.map +1 -1
  108. package/dist/webhooks/index.js +25 -1
  109. package/dist/webhooks/index.js.map +1 -1
  110. package/package.json +1 -1
  111. package/skills/app-architecture/SKILL.md +19 -1
  112. package/src/client/client.ts +6 -31
  113. package/src/contracts/contract-builder.ts +38 -0
  114. package/src/contracts/contract-group.ts +25 -0
  115. package/src/contracts/index.ts +8 -0
  116. package/src/contracts/lifecycle.ts +236 -0
  117. package/src/contracts/types.ts +5 -0
  118. package/src/error-reporting/index.ts +111 -0
  119. package/src/error-reporting/internal.ts +47 -0
  120. package/src/events/index.ts +20 -4
  121. package/src/idempotency/index.ts +81 -7
  122. package/src/jobs/index.ts +85 -6
  123. package/src/locks/index.ts +65 -16
  124. package/src/openapi/index.ts +22 -2
  125. package/src/outbox/index.ts +119 -27
  126. package/src/ports/events.ts +8 -1
  127. package/src/ports/index.ts +4 -4
  128. package/src/ports/unbound.ts +1 -1
  129. package/src/ports/unit-of-work.ts +10 -3
  130. package/src/query-codec.ts +130 -0
  131. package/src/server/hooks/cors.ts +10 -3
  132. package/src/server/hooks/error-reporting.ts +79 -23
  133. package/src/server/hooks/idempotency.ts +36 -10
  134. package/src/server/request-executor.ts +10 -13
  135. package/src/server/request-preparation.ts +4 -2
  136. package/src/server/response-finalization.ts +94 -5
  137. package/src/server/server.ts +15 -1
  138. package/src/testing/index.ts +25 -14
  139. package/src/tracing/index.ts +120 -0
  140. package/src/uploads/index.ts +44 -6
  141. package/src/webhooks/index.ts +34 -2
  142. package/dist/domain/events.d.ts +0 -44
  143. package/dist/domain/events.d.ts.map +0 -1
  144. package/dist/domain/events.js +0 -24
  145. package/dist/domain/events.js.map +0 -1
@@ -9,6 +9,29 @@
9
9
  */
10
10
 
11
11
  const TRACEPARENT_PATTERN = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
12
+ const MAX_TRACESTATE_LENGTH = 512;
13
+ const MAX_TRACESTATE_MEMBERS = 32;
14
+ const TRACESTATE_SIMPLE_KEY_PATTERN = /^[a-z][a-z0-9_*/-]{0,255}$/;
15
+ const TRACESTATE_VENDOR_KEY_PATTERN =
16
+ /^[a-z0-9][a-z0-9_*/-]{0,240}@[a-z][a-z0-9_*/-]{0,13}$/;
17
+ const TRACESTATE_VALUE_PATTERN =
18
+ /^[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]$/;
19
+
20
+ /** Current version of Beignet's durable trace carrier. */
21
+ export const TRACE_CARRIER_VERSION = 1 as const;
22
+
23
+ /**
24
+ * Vendor-neutral trace context stored in durable messages and transport
25
+ * envelopes. Unknown versions and malformed values are ignored by consumers.
26
+ */
27
+ export interface TraceCarrier {
28
+ /** Carrier schema version. */
29
+ readonly version: typeof TRACE_CARRIER_VERSION;
30
+ /** W3C traceparent value captured at the producing boundary. */
31
+ readonly traceparent: string;
32
+ /** Optional W3C tracestate value captured at the producing boundary. */
33
+ readonly tracestate?: string;
34
+ }
12
35
 
13
36
  /**
14
37
  * Trace context used to correlate related activity.
@@ -161,6 +184,103 @@ export function resolveTracingPort(target: unknown): TracingPort | undefined {
161
184
  return resolveTracingPort(ports);
162
185
  }
163
186
 
187
+ function validTracestate(value: unknown): string | undefined {
188
+ if (typeof value !== "string") return undefined;
189
+ const normalized = value.trim();
190
+ if (normalized.length === 0 || normalized.length > MAX_TRACESTATE_LENGTH) {
191
+ return undefined;
192
+ }
193
+
194
+ const members = normalized.split(",");
195
+ if (members.length > MAX_TRACESTATE_MEMBERS) return undefined;
196
+
197
+ const keys = new Set<string>();
198
+ for (const rawMember of members) {
199
+ const member = rawMember.trim();
200
+ const separator = member.indexOf("=");
201
+ if (separator <= 0) return undefined;
202
+
203
+ const key = member.slice(0, separator).trim();
204
+ const memberValue = member.slice(separator + 1).trimStart();
205
+ if (
206
+ (!TRACESTATE_SIMPLE_KEY_PATTERN.test(key) &&
207
+ !TRACESTATE_VENDOR_KEY_PATTERN.test(key)) ||
208
+ !TRACESTATE_VALUE_PATTERN.test(memberValue) ||
209
+ keys.has(key)
210
+ ) {
211
+ return undefined;
212
+ }
213
+ keys.add(key);
214
+ }
215
+
216
+ return normalized;
217
+ }
218
+
219
+ /**
220
+ * Parse an untrusted durable trace carrier.
221
+ *
222
+ * Invalid carriers return `undefined`; trace metadata must never prevent
223
+ * message delivery.
224
+ */
225
+ export function parseTraceCarrier(value: unknown): TraceCarrier | undefined {
226
+ if (!isObject(value) || value.version !== TRACE_CARRIER_VERSION) {
227
+ return undefined;
228
+ }
229
+
230
+ const parsed = parseTraceparent(
231
+ typeof value.traceparent === "string" ? value.traceparent : undefined,
232
+ );
233
+ if (!parsed) return undefined;
234
+
235
+ if (value.tracestate !== undefined) {
236
+ const tracestate = validTracestate(value.tracestate);
237
+ if (!tracestate) return undefined;
238
+ return {
239
+ version: TRACE_CARRIER_VERSION,
240
+ traceparent: parsed.traceparent,
241
+ tracestate,
242
+ };
243
+ }
244
+
245
+ return {
246
+ version: TRACE_CARRIER_VERSION,
247
+ traceparent: parsed.traceparent,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Capture the current trace context from a tracing port, ports object, app
253
+ * context, or explicit trace context. Returns `undefined` when no valid
254
+ * context is available.
255
+ */
256
+ export function captureTraceCarrier(target: unknown): TraceCarrier | undefined {
257
+ let context: TraceContextInput | undefined;
258
+ try {
259
+ context = resolveTraceContextInput(target);
260
+ } catch {
261
+ // Context-like inputs may be proxy-backed and reject unknown properties.
262
+ }
263
+
264
+ try {
265
+ context = resolveTracingPort(target)?.current() ?? context;
266
+ } catch {
267
+ // Tracing is best-effort and must not block the owning operation.
268
+ }
269
+
270
+ try {
271
+ if (!context?.traceparent) return undefined;
272
+
273
+ return parseTraceCarrier({
274
+ version: TRACE_CARRIER_VERSION,
275
+ traceparent: context.traceparent,
276
+ ...(context.tracestate ? { tracestate: context.tracestate } : {}),
277
+ });
278
+ } catch {
279
+ // Trace contexts are app-provided and remain best-effort inputs.
280
+ return undefined;
281
+ }
282
+ }
283
+
164
284
  /** Resolve trace fields from a trace context or context-like object. */
165
285
  export function resolveTraceContextInput(
166
286
  target: unknown,
@@ -1036,6 +1036,7 @@ export function createUploadRouter<Ctx>(
1036
1036
  assertFiles(definition, intents, { requireChecksum: false });
1037
1037
 
1038
1038
  const completed: CompletedUploadFile[] = [];
1039
+ const storedKeys: string[] = [];
1039
1040
  for (const [index, file] of webFiles.entries()) {
1040
1041
  const intent = intents[index];
1041
1042
  if (!intent) continue;
@@ -1072,18 +1073,24 @@ export function createUploadRouter<Ctx>(
1072
1073
  metadata: storageMetadata,
1073
1074
  visibility: definition.file.visibility ?? "private",
1074
1075
  });
1076
+ storedKeys.push(key);
1075
1077
  const completedFile = {
1076
1078
  ...verifiedIntent,
1077
1079
  uploadId,
1078
1080
  key,
1079
1081
  object,
1080
1082
  };
1081
- await assertVerifiedFile(definition, {
1082
- ctx,
1083
- metadata,
1084
- file: completedFile,
1085
- storage: options.storage,
1086
- });
1083
+ try {
1084
+ await assertVerifiedFile(definition, {
1085
+ ctx,
1086
+ metadata,
1087
+ file: completedFile,
1088
+ storage: options.storage,
1089
+ });
1090
+ } catch (error) {
1091
+ await cleanupRejectedServerUpload(uploadName, storedKeys);
1092
+ throw error;
1093
+ }
1087
1094
  completed.push(completedFile);
1088
1095
  }
1089
1096
 
@@ -1114,6 +1121,37 @@ export function createUploadRouter<Ctx>(
1114
1121
  }
1115
1122
  }
1116
1123
 
1124
+ async function cleanupRejectedServerUpload(
1125
+ uploadName: string,
1126
+ keys: readonly string[],
1127
+ ): Promise<void> {
1128
+ const failures: Array<{ key: string; error: string }> = [];
1129
+
1130
+ await Promise.all(
1131
+ keys.map(async (key) => {
1132
+ try {
1133
+ await options.storage.delete(key);
1134
+ } catch (error) {
1135
+ failures.push({
1136
+ key,
1137
+ error: error instanceof Error ? error.message : String(error),
1138
+ });
1139
+ }
1140
+ }),
1141
+ );
1142
+
1143
+ if (failures.length === 0) return;
1144
+ instrumentation.custom({
1145
+ name: "upload.server.cleanup.failed",
1146
+ label: "Rejected upload cleanup failed",
1147
+ summary: uploadName,
1148
+ details: {
1149
+ uploadName,
1150
+ failures,
1151
+ },
1152
+ });
1153
+ }
1154
+
1117
1155
  function recordFailure(
1118
1156
  name: string,
1119
1157
  uploadName: string,
@@ -133,7 +133,8 @@ export type HmacWebhookTimestampFormat =
133
133
  export type HmacWebhookTimestampOptions =
134
134
  | {
135
135
  /**
136
- * Header that carries the provider event timestamp.
136
+ * Header that carries the provider event timestamp. Header timestamps
137
+ * are authenticated as `<timestamp>.<rawBody>`.
137
138
  */
138
139
  header: string;
139
140
  payloadPath?: never;
@@ -461,7 +462,12 @@ export function createHmacWebhookVerifier(
461
462
  });
462
463
  }
463
464
 
464
- const expected = await hmacHex(algorithm, options.secret, input.rawBody);
465
+ const signedBody = hmacSignedBody({
466
+ input,
467
+ provider: options.provider,
468
+ timestamp,
469
+ });
470
+ const expected = await hmacHex(algorithm, options.secret, signedBody);
465
471
  const actual = normalizeSignature(signature, options.signaturePrefix);
466
472
  if (!(await timingSafeStringEqual(actual, expected))) {
467
473
  throw new WebhookVerificationError({
@@ -573,6 +579,32 @@ function rawBodyText(rawBody: WebhookRawBody): string {
573
579
  return new TextDecoder().decode(rawBodyBytes(rawBody));
574
580
  }
575
581
 
582
+ function hmacSignedBody(args: {
583
+ input: VerifyWebhookInput;
584
+ provider: string | undefined;
585
+ timestamp: NormalizedHmacTimestampOptions | undefined;
586
+ }): WebhookRawBody {
587
+ if (!args.timestamp || args.timestamp.source === "payload") {
588
+ return args.input.rawBody;
589
+ }
590
+
591
+ const value = args.input.headers?.[args.timestamp.key];
592
+ if (value === undefined || value === null || value === "") {
593
+ throw new WebhookVerificationError({
594
+ message: `Missing ${args.timestamp.key} header.`,
595
+ provider: args.provider,
596
+ code: "missing_timestamp",
597
+ });
598
+ }
599
+
600
+ const prefix = new TextEncoder().encode(`${value}.`);
601
+ const body = rawBodyBytes(args.input.rawBody);
602
+ const signed = new Uint8Array(prefix.byteLength + body.byteLength);
603
+ signed.set(prefix);
604
+ signed.set(body, prefix.byteLength);
605
+ return signed;
606
+ }
607
+
576
608
  function parseJsonBody(rawBody: WebhookRawBody): unknown {
577
609
  try {
578
610
  return JSON.parse(rawBodyText(rawBody));
@@ -1,44 +0,0 @@
1
- import type { EventDef, InferEventPayload, StandardSchema } from "../events/index.js";
2
- /**
3
- * Domain event definition with a stable name and payload schema.
4
- */
5
- export type DomainEventDef<Name extends string = string, Payload extends StandardSchema = StandardSchema> = EventDef<Name, Payload>;
6
- /**
7
- * Infer the payload type from a DomainEventDef.
8
- *
9
- * @example
10
- * ```ts
11
- * const UserRegistered = defineDomainEvent(
12
- * "user.registered",
13
- * z.object({
14
- * userId: z.string(),
15
- * email: z.string().email(),
16
- * })
17
- * );
18
- *
19
- * type Payload = InferEventPayload<typeof UserRegistered>;
20
- * // { userId: string; email: string }
21
- * ```
22
- */
23
- export type { InferEventPayload };
24
- /**
25
- * Create a new domain event definition.
26
- *
27
- * This is a domain-focused alias around `defineEvent(...)` for applications
28
- * that separate domain events from integration events.
29
- *
30
- * @example
31
- * ```ts
32
- * const UserRegistered = defineDomainEvent(
33
- * "user.registered",
34
- * z.object({
35
- * userId: z.string(),
36
- * email: z.string().email(),
37
- * })
38
- * );
39
- *
40
- * type UserRegistered = typeof UserRegistered;
41
- * ```
42
- */
43
- export declare function defineDomainEvent<Name extends string, Payload extends StandardSchema>(name: Name, payload: Payload): DomainEventDef<Name, Payload>;
44
- //# sourceMappingURL=events.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/domain/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACf,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,MAAM,MAAM,cAAc,CACxB,IAAI,SAAS,MAAM,GAAG,MAAM,EAC5B,OAAO,SAAS,cAAc,GAAG,cAAc,IAC7C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAE5B;;;;;;;;;;;;;;;;GAgBG;AACH,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAElC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,SAAS,MAAM,EACnB,OAAO,SAAS,cAAc,EAC9B,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAE7D"}
@@ -1,24 +0,0 @@
1
- import { defineEvent } from "../events/index.js";
2
- /**
3
- * Create a new domain event definition.
4
- *
5
- * This is a domain-focused alias around `defineEvent(...)` for applications
6
- * that separate domain events from integration events.
7
- *
8
- * @example
9
- * ```ts
10
- * const UserRegistered = defineDomainEvent(
11
- * "user.registered",
12
- * z.object({
13
- * userId: z.string(),
14
- * email: z.string().email(),
15
- * })
16
- * );
17
- *
18
- * type UserRegistered = typeof UserRegistered;
19
- * ```
20
- */
21
- export function defineDomainEvent(name, payload) {
22
- return defineEvent(name, { payload });
23
- }
24
- //# sourceMappingURL=events.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/domain/events.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA6BjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,iBAAiB,CAG/B,IAAU,EAAE,OAAgB;IAC5B,OAAO,WAAW,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;AACxC,CAAC"}