@beignet/core 0.0.32 → 0.0.34
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 +42 -0
- package/README.md +409 -34
- 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/contracts/contract-builder.d.ts +0 -4
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +0 -6
- package/dist/contracts/contract-builder.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/flags/index.d.ts +0 -4
- package/dist/flags/index.d.ts.map +1 -1
- package/dist/flags/index.js +0 -4
- package/dist/flags/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/notifications/index.d.ts +194 -10
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +397 -59
- package/dist/notifications/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/events.d.ts +2 -2
- package/dist/ports/index.d.ts +7 -2
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +2 -1
- 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/providers/provider.d.ts +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 -10
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +75 -11
- 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/testing/index.d.ts +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- 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 -5
- package/skills/app-architecture/SKILL.md +31 -2
- package/src/application/index.ts +88 -56
- package/src/contracts/contract-builder.ts +0 -7
- package/src/events/index.ts +27 -14
- package/src/flags/index.ts +0 -5
- package/src/jobs/index.ts +895 -7
- package/src/notifications/index.ts +678 -70
- package/src/outbox/index.ts +358 -2
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +17 -1
- package/src/providers/instrumentation.ts +4 -0
- package/src/providers/provider.ts +1 -1
- 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 +88 -35
- package/src/server/trusted-proxy.ts +249 -0
- package/src/tasks/index.ts +29 -12
- package/src/tenancy/index.ts +55 -0
- package/src/testing/index.ts +2 -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
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ name the framework area they depend on.
|
|
|
58
58
|
| `@beignet/core/events` | Events and listeners |
|
|
59
59
|
| `@beignet/core/flags` | Feature flag definitions, FlagsPort, memory/static adapters, and helpers |
|
|
60
60
|
| `@beignet/core/idempotency` | Retry-safe command, webhook, and job primitives |
|
|
61
|
-
| `@beignet/core/jobs` | Job definitions, retry policies, and inline job dispatch |
|
|
61
|
+
| `@beignet/core/jobs` | Job definitions, retry policies, timeout guards, execution hooks, execution lease helpers, uniqueness guards, and inline job dispatch |
|
|
62
62
|
| `@beignet/core/locks` | Lease-backed LocksPort, memory adapter, memory provider, and helpers |
|
|
63
63
|
| `@beignet/core/mail` | Mail port, memory mailer, and memory mailer provider |
|
|
64
64
|
| `@beignet/core/memo` | Request-scoped memoization for port lookups |
|
|
@@ -68,14 +68,14 @@ name the framework area they depend on.
|
|
|
68
68
|
| `@beignet/core/payments` | Payments port, memory payments adapter, and memory payments provider |
|
|
69
69
|
| `@beignet/core/pagination` | Offset/cursor page types, normalizers, and result helpers |
|
|
70
70
|
| `@beignet/core/ports` | App-facing ports, auth, audit, policies, cache, storage, logging, and redaction |
|
|
71
|
-
| `@beignet/core/ports/testing` | Port and policy test helpers |
|
|
72
71
|
| `@beignet/core/providers` | Provider lifecycle and instrumentation primitives |
|
|
73
72
|
| `@beignet/core/search` | Search index definitions, SearchPort, memory adapter, memory provider, and helpers |
|
|
74
73
|
| `@beignet/core/schedules` | Schedule primitives |
|
|
75
|
-
| `@beignet/core/server` | Framework-agnostic server runtime and hook helpers |
|
|
74
|
+
| `@beignet/core/server` | Framework-agnostic server runtime, security headers, CSRF, and hook helpers |
|
|
76
75
|
| `@beignet/core/server-only` | Static lint marker for modules that must stay out of client bundles |
|
|
77
76
|
| `@beignet/core/tasks` | Operational task definitions and inline task execution |
|
|
78
|
-
| `@beignet/core/
|
|
77
|
+
| `@beignet/core/tenancy` | Branded tenant scope helpers for repository boundaries |
|
|
78
|
+
| `@beignet/core/testing` | Port and policy assertions, recording adapters, test context factories, memory port fixtures, provider install helper, factories, seeds, and database harnesses |
|
|
79
79
|
| `@beignet/core/tracing` | Dependency-free W3C trace context primitives |
|
|
80
80
|
| `@beignet/core/uploads` | Upload definitions (`createUploads<AppContext>()` app-bound builder), router, signer port, and test signer |
|
|
81
81
|
| `@beignet/core/uploads/client` | Browser upload client for server and direct uploads |
|
|
@@ -98,10 +98,100 @@ Jobs, outbox delivery, and schedule runners use the same terms:
|
|
|
98
98
|
- `attempts` in a retry policy is the maximum total attempts, including the
|
|
99
99
|
first try.
|
|
100
100
|
- `backoff` is the delay before the next retry.
|
|
101
|
+
- `timeout` is the maximum execution window for one handler attempt.
|
|
102
|
+
- `hook` is app-owned behavior that wraps one handler attempt.
|
|
103
|
+
- `execution lease` is a TTL-backed lock around one handler attempt for a
|
|
104
|
+
logical job key.
|
|
101
105
|
- `terminal failure` means the work should not be retried automatically.
|
|
102
106
|
- `dead letter` is a durable terminal delivery state, currently owned by the
|
|
103
107
|
outbox.
|
|
104
108
|
|
|
109
|
+
Jobs may also declare dispatch-time uniqueness and execution leases:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import {
|
|
113
|
+
createJobExecutionLeaseHook,
|
|
114
|
+
createInlineJobDispatcher,
|
|
115
|
+
createJobs,
|
|
116
|
+
createUniqueJobDispatcher,
|
|
117
|
+
type JobDef,
|
|
118
|
+
retry,
|
|
119
|
+
} from "@beignet/core/jobs";
|
|
120
|
+
import type { LocksPort } from "@beignet/core/locks";
|
|
121
|
+
import { z } from "zod";
|
|
122
|
+
|
|
123
|
+
type AppContext = {
|
|
124
|
+
ports: {
|
|
125
|
+
billing: {
|
|
126
|
+
syncAccount(
|
|
127
|
+
accountId: string,
|
|
128
|
+
options?: { signal?: AbortSignal },
|
|
129
|
+
): Promise<void>;
|
|
130
|
+
};
|
|
131
|
+
locks: LocksPort;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const { defineJob } = createJobs<AppContext>();
|
|
136
|
+
|
|
137
|
+
const syncAccountPayloadSchema = z.object({
|
|
138
|
+
accountId: z.string().min(1),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const syncAccountExecutionLease = createJobExecutionLeaseHook<
|
|
142
|
+
JobDef<"billing.sync-account", typeof syncAccountPayloadSchema, AppContext>,
|
|
143
|
+
AppContext
|
|
144
|
+
>({
|
|
145
|
+
locks: ({ ctx }) => ctx.ports.locks,
|
|
146
|
+
key: ({ payload }) => payload.accountId,
|
|
147
|
+
ttl: "5m",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export const SyncAccountJob = defineJob("billing.sync-account", {
|
|
151
|
+
payload: syncAccountPayloadSchema,
|
|
152
|
+
unique: ({ payload }) => ({
|
|
153
|
+
key: payload.accountId,
|
|
154
|
+
ttl: "10m",
|
|
155
|
+
}),
|
|
156
|
+
timeout: "30s",
|
|
157
|
+
retry: retry.exponential({ attempts: 3 }),
|
|
158
|
+
hooks: [syncAccountExecutionLease],
|
|
159
|
+
async handle({ payload, ctx, signal }) {
|
|
160
|
+
await ctx.ports.billing.syncAccount(payload.accountId, { signal });
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export function createJobsPort(ctx: AppContext) {
|
|
165
|
+
return createUniqueJobDispatcher({
|
|
166
|
+
jobs: createInlineJobDispatcher<AppContext>({ ctx }),
|
|
167
|
+
locks: ctx.ports.locks,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`unique` suppresses duplicate dispatches while the resolved lock key's TTL is
|
|
173
|
+
active. It does not replace handler idempotency: providers may still execute a
|
|
174
|
+
queued job more than once after a worker crash or retry.
|
|
175
|
+
|
|
176
|
+
`timeout` bounds each handler attempt. When the timeout expires, Beignet throws
|
|
177
|
+
`JobTimeoutError`, aborts the handler's `signal`, and lets the job retry policy
|
|
178
|
+
decide whether the timeout should retry.
|
|
179
|
+
|
|
180
|
+
`hooks` wrap each handler attempt when the job runs through a Beignet
|
|
181
|
+
dispatcher or worker helper. Runner-level hooks, such as
|
|
182
|
+
`createInlineJobDispatcher({ hooks })`, wrap job-local hooks. Hook failures are
|
|
183
|
+
classified by the same retry policy as handler failures. When a runner can
|
|
184
|
+
report attempt metadata, hooks receive Beignet's one-based `attempt` and
|
|
185
|
+
`maxAttempts` values. Direct `job.handle(...)` calls bypass hooks; use
|
|
186
|
+
`runJobHandler(...)` or a dispatcher when a test needs hook behavior.
|
|
187
|
+
|
|
188
|
+
`createJobExecutionLeaseHook(...)` is the first built-in hook helper. It
|
|
189
|
+
acquires a TTL-backed `LocksPort` lease for one handler attempt, then releases
|
|
190
|
+
best effort in `finally`. It does not start renewal loops, so serverless
|
|
191
|
+
entrypoints can use it with a shared locks provider; the TTL remains the safety
|
|
192
|
+
boundary if the runtime terminates early. Unavailable leases skip by default,
|
|
193
|
+
or can throw `JobExecutionLeaseUnavailableError` for retry classification.
|
|
194
|
+
|
|
105
195
|
Schedules do not own retry policies. They can carry provider attempt metadata
|
|
106
196
|
through `ScheduleRunContext.attempt`, then dispatch jobs or outbox messages when
|
|
107
197
|
the work needs Beignet-managed retry and dead-letter behavior.
|
|
@@ -180,9 +270,12 @@ provider `name` that defaults to `"memory-mailer"`.
|
|
|
180
270
|
`{ notifications: NotificationPort }` backed by
|
|
181
271
|
`createInlineNotificationDispatcher(...)`. Channel handlers receive an app
|
|
182
272
|
service context built lazily through the server context blueprint on each
|
|
183
|
-
send, so registration order does not matter.
|
|
184
|
-
|
|
185
|
-
`
|
|
273
|
+
send, so registration order does not matter. One failed channel does not block
|
|
274
|
+
the remaining channels. Inline sends return ordered `sent`, `skipped`, and
|
|
275
|
+
`failed` results; queued dispatchers also return `queued`. Set
|
|
276
|
+
`failureMode: "throw"` to reject after every channel has run. Options also
|
|
277
|
+
accept an app-owned `preferences` evaluator, the dispatcher's `onError` result
|
|
278
|
+
mapper, and a provider `name` that defaults to `"inline-notifications"`.
|
|
186
279
|
|
|
187
280
|
```typescript
|
|
188
281
|
// server/providers.ts
|
|
@@ -199,8 +292,56 @@ export const providers = [
|
|
|
199
292
|
|
|
200
293
|
Replace `createMemoryMailerProvider(...)` with a real mail provider such as
|
|
201
294
|
`@beignet/provider-mail-resend` for production delivery. Production apps can
|
|
202
|
-
keep the inline
|
|
203
|
-
|
|
295
|
+
keep the inline provider or define a central notification registry and a
|
|
296
|
+
`defineNotificationDeliveryJob(...)`. Install
|
|
297
|
+
`createQueuedNotificationsProvider(...)` after the app's jobs provider to
|
|
298
|
+
enqueue one independently retryable job per channel. Register the delivery job
|
|
299
|
+
with every BullMQ/Inngest worker or outbox registry that can receive it.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// server/notifications.ts
|
|
303
|
+
import {
|
|
304
|
+
defineNotificationDeliveryJob,
|
|
305
|
+
defineNotificationRegistry,
|
|
306
|
+
} from "@beignet/core/notifications";
|
|
307
|
+
import type { AppContext } from "@/app-context";
|
|
308
|
+
import { WelcomeNotification } from "@/features/users/notifications";
|
|
309
|
+
|
|
310
|
+
export const notificationRegistry = defineNotificationRegistry<AppContext>([
|
|
311
|
+
WelcomeNotification,
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
export const DeliverNotificationJob =
|
|
315
|
+
defineNotificationDeliveryJob<AppContext>({
|
|
316
|
+
registry: notificationRegistry,
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// server/index.ts
|
|
322
|
+
import { createQueuedNotificationsProvider } from "@beignet/core/notifications";
|
|
323
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
324
|
+
|
|
325
|
+
export const getServer = createNextServerLoader(async () => {
|
|
326
|
+
const { providers } = await import("./providers");
|
|
327
|
+
const { DeliverNotificationJob } = await import("./notifications");
|
|
328
|
+
|
|
329
|
+
return createNextServer({
|
|
330
|
+
// ...
|
|
331
|
+
providers: [
|
|
332
|
+
...providers,
|
|
333
|
+
createQueuedNotificationsProvider({
|
|
334
|
+
deliveryJob: DeliverNotificationJob,
|
|
335
|
+
}),
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The delivery job defaults to three attempts with exponential backoff. The
|
|
342
|
+
queued dispatcher validates notification payloads before enqueueing and uses
|
|
343
|
+
the app's existing `jobs` port, so the same setup works with direct job
|
|
344
|
+
providers or `createOutboxJobDispatcher(...)`.
|
|
204
345
|
|
|
205
346
|
## Entitlements
|
|
206
347
|
|
|
@@ -215,6 +356,8 @@ import {
|
|
|
215
356
|
type EntitlementDecisionObserver,
|
|
216
357
|
requireEntitlement,
|
|
217
358
|
} from "@beignet/core/entitlements";
|
|
359
|
+
import { createTenant } from "@beignet/core/ports";
|
|
360
|
+
import { createTenantScope } from "@beignet/core/tenancy";
|
|
218
361
|
|
|
219
362
|
function createBillingEntitlements(
|
|
220
363
|
billing: BillingRepository,
|
|
@@ -222,7 +365,10 @@ function createBillingEntitlements(
|
|
|
222
365
|
) {
|
|
223
366
|
return createEntitlements({
|
|
224
367
|
async inspect(input) {
|
|
225
|
-
|
|
368
|
+
if (input.subject.type !== "tenant") return false;
|
|
369
|
+
const account = await billing.findByTenantScope(
|
|
370
|
+
createTenantScope(createTenant(input.subject.id)),
|
|
371
|
+
);
|
|
226
372
|
return account?.status === "active";
|
|
227
373
|
},
|
|
228
374
|
onDecision: recordDecision,
|
|
@@ -353,6 +499,56 @@ Provider adapters may require query fields to be declared in the index
|
|
|
353
499
|
metadata. For Meilisearch, `filters` and `facets` must use
|
|
354
500
|
`filterableAttributes`, and `sort` must use `sortableAttributes`.
|
|
355
501
|
|
|
502
|
+
## Uploads
|
|
503
|
+
|
|
504
|
+
Use `@beignet/core/uploads` for typed file workflows above `StoragePort`.
|
|
505
|
+
Upload definitions own metadata validation, authorization, storage keys, file
|
|
506
|
+
constraints, direct-upload signing, and completion hooks.
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import { createUploads } from "@beignet/core/uploads";
|
|
510
|
+
import { z } from "zod";
|
|
511
|
+
|
|
512
|
+
const { defineUpload } = createUploads<AppContext>();
|
|
513
|
+
|
|
514
|
+
export const issueAttachmentUpload = defineUpload("issues.attachment", {
|
|
515
|
+
metadata: z.object({ issueKey: z.string() }),
|
|
516
|
+
file: {
|
|
517
|
+
contentTypes: ["application/pdf", "text/plain"],
|
|
518
|
+
maxSizeBytes: 5 * 1024 * 1024,
|
|
519
|
+
checksum: { algorithm: "sha256" },
|
|
520
|
+
},
|
|
521
|
+
authorize({ ctx }) {
|
|
522
|
+
return ctx.actor.type === "user";
|
|
523
|
+
},
|
|
524
|
+
key({ metadata, uploadId }) {
|
|
525
|
+
return `issues/${metadata.issueKey}/attachments/${uploadId}`;
|
|
526
|
+
},
|
|
527
|
+
async verifyFile({ ctx, file }) {
|
|
528
|
+
const scan = await ctx.ports.fileScanner.scanObject(file.key);
|
|
529
|
+
|
|
530
|
+
return scan.clean
|
|
531
|
+
? true
|
|
532
|
+
: { valid: false, reason: "Upload did not pass scanning." };
|
|
533
|
+
},
|
|
534
|
+
async onComplete({ ctx, files }) {
|
|
535
|
+
await ctx.ports.issueAttachments.create({
|
|
536
|
+
id: files[0]!.uploadId,
|
|
537
|
+
key: files[0]!.key,
|
|
538
|
+
});
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
For supported media types, uploads verify the declared content type against the
|
|
544
|
+
file signature before completion. Set `contentTypeVerification: false` only for
|
|
545
|
+
workflows that intentionally accept mismatched supported file types. Direct
|
|
546
|
+
uploads can require a SHA-256 checksum with `checksum: { algorithm: "sha256" }`;
|
|
547
|
+
browser clients need a client-safe manifest so `@beignet/core/uploads/client`
|
|
548
|
+
can compute the digest before `prepare`. Use `verifyFile(...)` for app-owned
|
|
549
|
+
scanning, moderation, and quarantine decisions that run after the object exists
|
|
550
|
+
in storage and before `onComplete(...)`.
|
|
551
|
+
|
|
356
552
|
## Webhooks
|
|
357
553
|
|
|
358
554
|
Use `@beignet/core/webhooks` for provider-neutral inbound webhook definitions,
|
|
@@ -378,6 +574,10 @@ export const issueWebhook = defineWebhook("issues.provider", {
|
|
|
378
574
|
secret: process.env.PROVIDER_WEBHOOK_SECRET ?? "",
|
|
379
575
|
signatureHeader: "x-provider-signature",
|
|
380
576
|
signaturePrefix: "sha256=",
|
|
577
|
+
timestamp: {
|
|
578
|
+
header: "x-provider-timestamp",
|
|
579
|
+
toleranceSec: 300,
|
|
580
|
+
},
|
|
381
581
|
}),
|
|
382
582
|
});
|
|
383
583
|
```
|
|
@@ -399,6 +599,13 @@ direction. Attach provider verifiers at the route boundary through the
|
|
|
399
599
|
(`createHmacWebhookVerifier(...)`, `createMemoryWebhookVerifier(...)`) and for
|
|
400
600
|
tests.
|
|
401
601
|
|
|
602
|
+
Generic webhook catalogs reject verified event types that are not declared in
|
|
603
|
+
`events` by default. Set `allowUnknownEvents: true` on
|
|
604
|
+
`createWebhookRoute(...)` or `verifyWebhook(...)` only for broad provider
|
|
605
|
+
endpoints that intentionally acknowledge valid events the app does not handle.
|
|
606
|
+
When a generic HMAC provider signs a timestamp header or payload field, pass
|
|
607
|
+
`timestamp` to reject replayed deliveries outside the configured tolerance.
|
|
608
|
+
|
|
402
609
|
## Provider metadata
|
|
403
610
|
|
|
404
611
|
Reusable provider packages should declare static metadata in `package.json`
|
|
@@ -418,7 +625,7 @@ without importing provider implementation code.
|
|
|
418
625
|
"requiredTables": ["cache_entries"],
|
|
419
626
|
"registration": {
|
|
420
627
|
"required": true,
|
|
421
|
-
"tokens": ["
|
|
628
|
+
"tokens": ["createCacheProvider"]
|
|
422
629
|
},
|
|
423
630
|
"watchers": ["cache"]
|
|
424
631
|
}
|
|
@@ -578,6 +785,45 @@ path with an unregistered method receives a framework-owned `405
|
|
|
578
785
|
METHOD_NOT_ALLOWED` response with an `Allow` header listing the registered
|
|
579
786
|
methods; `HEAD` is intentionally not served by `GET` handlers.
|
|
580
787
|
|
|
788
|
+
### Runtime integrity
|
|
789
|
+
|
|
790
|
+
Workflow artifacts are explicit too. Use `createRuntimeIntegrity(...)` when an
|
|
791
|
+
app should fail startup if a listener, schedule, task, or outbox event/job is
|
|
792
|
+
listed in the app manifest but missing from the runtime registries:
|
|
793
|
+
|
|
794
|
+
```ts
|
|
795
|
+
import {
|
|
796
|
+
createRuntimeIntegrity,
|
|
797
|
+
defineRuntimeManifest,
|
|
798
|
+
defineRuntimeRegistries,
|
|
799
|
+
} from "@beignet/core/server";
|
|
800
|
+
import { postEvents } from "@/features/posts/domain/events";
|
|
801
|
+
import { postJobs } from "@/features/posts/jobs";
|
|
802
|
+
import { postListeners } from "@/features/posts/listeners";
|
|
803
|
+
import { listeners } from "@/server/listeners";
|
|
804
|
+
import { outboxRegistry } from "@/server/outbox";
|
|
805
|
+
|
|
806
|
+
export const runtimeIntegrity = createRuntimeIntegrity({
|
|
807
|
+
manifest: defineRuntimeManifest({
|
|
808
|
+
listeners: [...postListeners],
|
|
809
|
+
outbox: {
|
|
810
|
+
events: [...postEvents],
|
|
811
|
+
jobs: [...postJobs],
|
|
812
|
+
},
|
|
813
|
+
}),
|
|
814
|
+
registries: defineRuntimeRegistries({
|
|
815
|
+
listeners,
|
|
816
|
+
outbox: outboxRegistry,
|
|
817
|
+
}),
|
|
818
|
+
});
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Pass `integrity: runtimeIntegrity` to `createServer(...)` or
|
|
822
|
+
`createNextServer(...)`. The check is pure and serverless-safe: it compares
|
|
823
|
+
imported definitions and registries in memory, without filesystem scanning,
|
|
824
|
+
provider calls, database access, worker startup, or background loops. Use
|
|
825
|
+
`mode: "warn"` to log findings without failing boot.
|
|
826
|
+
|
|
581
827
|
Contract path templates intentionally support concrete segments and
|
|
582
828
|
single-segment params such as `:id` and `[id]`. Framework or platform
|
|
583
829
|
catch-all route files can expose a central Beignet handler, but individual
|
|
@@ -645,7 +891,7 @@ import { createTestContextFactory, createTestPorts } from "@beignet/core/testing
|
|
|
645
891
|
import {
|
|
646
892
|
createTestTenant,
|
|
647
893
|
createTestUserActor,
|
|
648
|
-
} from "@beignet/core/
|
|
894
|
+
} from "@beignet/core/testing";
|
|
649
895
|
|
|
650
896
|
const fixture = createTestPorts<AppContext["ports"]>({
|
|
651
897
|
base: appPorts,
|
|
@@ -785,7 +1031,7 @@ The `service` factory powers two server entrypoints:
|
|
|
785
1031
|
|
|
786
1032
|
- `server.createServiceContext(...)` returns the built context and enters the
|
|
787
1033
|
ambient correlation frame for the rest of the caller's async execution. Use
|
|
788
|
-
it from long-lived runtimes only: servers, workers, and
|
|
1034
|
+
it from long-lived runtimes only: servers, workers, and test runners.
|
|
789
1035
|
- `server.runServiceContext(...)` builds the same context and runs a callback
|
|
790
1036
|
inside a scoped ambient frame, returning the callback's result. Use it from
|
|
791
1037
|
plain scripts such as seeds and one-off maintenance work — the
|
|
@@ -807,18 +1053,47 @@ Both entrypoints require `context.service` in the blueprint, generate fresh
|
|
|
807
1053
|
`tenant` on the ambient request context so audit and instrumentation wrappers
|
|
808
1054
|
observe them at record time.
|
|
809
1055
|
|
|
1056
|
+
### Tenant scopes
|
|
1057
|
+
|
|
1058
|
+
Use `@beignet/core/tenancy` when a repository method should be scoped to the
|
|
1059
|
+
current tenant without accepting arbitrary tenant IDs from callers:
|
|
1060
|
+
|
|
1061
|
+
```ts
|
|
1062
|
+
import {
|
|
1063
|
+
requireTenantScope,
|
|
1064
|
+
tenantScopeId,
|
|
1065
|
+
type TenantScope,
|
|
1066
|
+
} from "@beignet/core/tenancy";
|
|
1067
|
+
|
|
1068
|
+
export interface TodoRepository {
|
|
1069
|
+
create(input: CreateTodoInput, scope: TenantScope): Promise<Todo>;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const scope = requireTenantScope(ctx);
|
|
1073
|
+
await ctx.ports.todos.create(input, scope);
|
|
1074
|
+
|
|
1075
|
+
const tenantId = tenantScopeId(scope); // adapter boundary
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
Apps still own tenant resolution and tenant data modeling. `TenantScope` only
|
|
1079
|
+
brands the already-resolved `ctx.tenant` value for app-facing repository
|
|
1080
|
+
boundaries. `beignet doctor --strict` checks generated tenant-scoped Drizzle
|
|
1081
|
+
repositories, explicit raw `tenantId` repository boundaries, and scoped
|
|
1082
|
+
`tenantId`/`workspaceId` predicates as a conservative drift detector.
|
|
1083
|
+
|
|
810
1084
|
### Testing providers
|
|
811
1085
|
|
|
812
1086
|
Use `installProviderForTest(...)` to run provider setup against test ports
|
|
813
1087
|
without hand-rolling setup, port merge, and lifecycle plumbing:
|
|
814
1088
|
|
|
815
1089
|
```ts
|
|
1090
|
+
import type { CachePort } from "@beignet/core/ports";
|
|
816
1091
|
import { installProviderForTest } from "@beignet/core/testing";
|
|
1092
|
+
import { createRedisCacheProvider } from "@beignet/provider-cache-redis";
|
|
817
1093
|
|
|
818
1094
|
const { ports, result, start, stop } = await installProviderForTest(
|
|
819
|
-
|
|
1095
|
+
createRedisCacheProvider(),
|
|
820
1096
|
{
|
|
821
|
-
ports: { devtools },
|
|
822
1097
|
config: { URL: "redis://localhost:6379" },
|
|
823
1098
|
},
|
|
824
1099
|
);
|
|
@@ -899,7 +1174,7 @@ ORM table objects, or provider SDKs directly.
|
|
|
899
1174
|
|
|
900
1175
|
### Port testing helpers
|
|
901
1176
|
|
|
902
|
-
Use `@beignet/core/
|
|
1177
|
+
Use `@beignet/core/testing` when tests need stable actor, tenant,
|
|
903
1178
|
authorization, or audit assertions:
|
|
904
1179
|
|
|
905
1180
|
```ts
|
|
@@ -909,7 +1184,7 @@ import {
|
|
|
909
1184
|
createTestActivityContext,
|
|
910
1185
|
createTestTenant,
|
|
911
1186
|
createTestUserActor,
|
|
912
|
-
} from "@beignet/core/
|
|
1187
|
+
} from "@beignet/core/testing";
|
|
913
1188
|
|
|
914
1189
|
const activity = createTestActivityContext({
|
|
915
1190
|
actor: createTestUserActor("user_1", { role: "admin" }),
|
|
@@ -963,7 +1238,7 @@ import {
|
|
|
963
1238
|
createRecordingEventBus,
|
|
964
1239
|
createRecordingJobDispatcher,
|
|
965
1240
|
createRecordingProviderInstrumentation,
|
|
966
|
-
} from "@beignet/core/
|
|
1241
|
+
} from "@beignet/core/testing";
|
|
967
1242
|
import { drainOutbox } from "@beignet/core/outbox";
|
|
968
1243
|
import { createProviderInstrumentation } from "@beignet/core/providers";
|
|
969
1244
|
|
|
@@ -1113,6 +1388,29 @@ For caching that must survive across requests, use the explicit tier —
|
|
|
1113
1388
|
`ports.cache.remember` with keys that change when the data changes — and see
|
|
1114
1389
|
the request lifecycle docs for context latency budgets.
|
|
1115
1390
|
|
|
1391
|
+
### Trusted proxy request metadata
|
|
1392
|
+
|
|
1393
|
+
Beignet trusts no forwarding headers by default. Use
|
|
1394
|
+
`resolveTrustedRequest(...)` when app code needs external request metadata
|
|
1395
|
+
behind a platform edge or reverse proxy that strips or normalizes forwarding
|
|
1396
|
+
headers before they reach application code:
|
|
1397
|
+
|
|
1398
|
+
```ts
|
|
1399
|
+
import { resolveTrustedRequest } from "@beignet/core/server";
|
|
1400
|
+
|
|
1401
|
+
const requestInfo = resolveTrustedRequest(req, {
|
|
1402
|
+
clientIp: "x-forwarded-for-last",
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
requestInfo.origin; // "https://app.example.com"
|
|
1406
|
+
requestInfo.clientIp; // resolved only when clientIp is configured
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
The same `trustedProxy` policy is accepted by `createRateLimitHooks(...)` for
|
|
1410
|
+
IP-scoped keys and by `createCsrfHooks(...)` for comparing browser origins
|
|
1411
|
+
against the external host and protocol. Configure it only when every request
|
|
1412
|
+
passes through your trusted edge.
|
|
1413
|
+
|
|
1116
1414
|
### Rate limiting
|
|
1117
1415
|
|
|
1118
1416
|
Use `createRateLimitHooks(...)` from `@beignet/core/server` to enforce
|
|
@@ -1131,14 +1429,15 @@ const server = await createServer<AppContext, AppPorts>({
|
|
|
1131
1429
|
- `global` and `ip` scopes run in `onRequest` before parsing and context
|
|
1132
1430
|
creation; `user` scope runs in `beforeHandle` after route hooks have resolved
|
|
1133
1431
|
identity and `ctx.actor` exists.
|
|
1134
|
-
- `ip` scopes require an explicit `
|
|
1135
|
-
fails `createServer(...)`
|
|
1136
|
-
`ip`-scoped rate limit without
|
|
1137
|
-
configuration error for contracts added
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1432
|
+
- `ip` scopes require an explicit `trustedProxy.clientIp`, `ipSource`, or
|
|
1433
|
+
custom `earlyKey`. The hook's `validate` phase fails `createServer(...)`
|
|
1434
|
+
startup when a registered contract declares an `ip`-scoped rate limit without
|
|
1435
|
+
one, and enforcement throws the same configuration error for contracts added
|
|
1436
|
+
later through `server.route(...)`. Prefer
|
|
1437
|
+
`trustedProxy: { clientIp: "x-forwarded-for-last" }` behind a trusted proxy
|
|
1438
|
+
that appends the socket address, `"x-forwarded-for-first"` when a trusted
|
|
1439
|
+
edge normalizes the header, or
|
|
1440
|
+
`trustedProxy: { clientIp: "cf-connecting-ip" }` for platform headers.
|
|
1142
1441
|
- Pass `ipSource: "none"` to explicitly opt out of client-IP resolution:
|
|
1143
1442
|
Beignet then trusts no forwarding headers and all `ip`-scoped traffic
|
|
1144
1443
|
shares one `ip:unknown` bucket.
|
|
@@ -1230,12 +1529,17 @@ import {
|
|
|
1230
1529
|
createOutboxEventRecorder,
|
|
1231
1530
|
defineOutboxRegistry,
|
|
1232
1531
|
drainOutbox,
|
|
1532
|
+
type OutboxAdminPort,
|
|
1233
1533
|
} from "@beignet/core/outbox";
|
|
1234
1534
|
import {
|
|
1535
|
+
createDrizzleSqliteOutboxAdminPort,
|
|
1235
1536
|
createDrizzleSqliteOutboxPort,
|
|
1236
1537
|
createDrizzleSqliteUnitOfWork,
|
|
1237
1538
|
} from "@beignet/provider-db-drizzle/sqlite";
|
|
1238
1539
|
|
|
1540
|
+
const outboxAdmin: OutboxAdminPort =
|
|
1541
|
+
createDrizzleSqliteOutboxAdminPort(db);
|
|
1542
|
+
|
|
1239
1543
|
const uow = createDrizzleSqliteUnitOfWork({
|
|
1240
1544
|
db,
|
|
1241
1545
|
createTransactionPorts: (tx) => {
|
|
@@ -1265,6 +1569,10 @@ await drainOutbox({
|
|
|
1265
1569
|
The outbox is at-least-once delivery. Use idempotent listeners or jobs when a
|
|
1266
1570
|
duplicate delivery would be harmful.
|
|
1267
1571
|
|
|
1572
|
+
Use `OutboxAdminPort` only from operational contexts. It lets `beignet outbox`
|
|
1573
|
+
list, show, requeue, purge dead-lettered rows, and prune delivered rows without
|
|
1574
|
+
exposing those destructive operations to transaction-scoped use cases.
|
|
1575
|
+
|
|
1268
1576
|
### Schedules
|
|
1269
1577
|
|
|
1270
1578
|
Use `@beignet/core/schedules` to define typed schedules and run them
|
|
@@ -1373,6 +1681,27 @@ const writerAuth = createAuthHooks<AppContext>()({
|
|
|
1373
1681
|
});
|
|
1374
1682
|
```
|
|
1375
1683
|
|
|
1684
|
+
Use `createSecurityHeadersHooks(...)` for the default browser response-header
|
|
1685
|
+
baseline. The hook adds common headers such as `X-Content-Type-Options`,
|
|
1686
|
+
`X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`,
|
|
1687
|
+
`Cross-Origin-Opener-Policy`, and `Cross-Origin-Resource-Policy`; it does not
|
|
1688
|
+
guess your CSP or HSTS policy:
|
|
1689
|
+
|
|
1690
|
+
```ts
|
|
1691
|
+
import { createSecurityHeadersHooks } from "@beignet/core/server";
|
|
1692
|
+
|
|
1693
|
+
const securityHeaders = createSecurityHeadersHooks({
|
|
1694
|
+
contentSecurityPolicy: "default-src 'self'; frame-ancestors 'none'",
|
|
1695
|
+
strictTransportSecurity: {
|
|
1696
|
+
maxAgeSec: 31_536_000,
|
|
1697
|
+
includeSubDomains: true,
|
|
1698
|
+
},
|
|
1699
|
+
});
|
|
1700
|
+
```
|
|
1701
|
+
|
|
1702
|
+
Existing response headers win, so routes that stream files, render HTML, or need
|
|
1703
|
+
a different CSP can set their own policy.
|
|
1704
|
+
|
|
1376
1705
|
Use `createCorsHooks(...)` for app-wide CORS headers. Wildcard origins are
|
|
1377
1706
|
accepted only for non-credentialed requests:
|
|
1378
1707
|
|
|
@@ -1390,6 +1719,31 @@ const browserAppCors = createCorsHooks({
|
|
|
1390
1719
|
apps do not accidentally reflect arbitrary request origins for cookies or
|
|
1391
1720
|
authorization headers.
|
|
1392
1721
|
|
|
1722
|
+
Use `createCsrfHooks(...)` when browser mutations depend on cookies. By default
|
|
1723
|
+
the hook protects unsafe methods by rejecting cross-origin `Origin` or `Referer`
|
|
1724
|
+
headers while still allowing requests that do not carry browser origin headers.
|
|
1725
|
+
Set `allowMissingOrigin: false` and enable token checks for stricter
|
|
1726
|
+
browser-only APIs:
|
|
1727
|
+
|
|
1728
|
+
```ts
|
|
1729
|
+
import { createCsrfHooks } from "@beignet/core/server";
|
|
1730
|
+
|
|
1731
|
+
const csrf = createCsrfHooks({
|
|
1732
|
+
allowMissingOrigin: false,
|
|
1733
|
+
trustedProxy: {},
|
|
1734
|
+
trustedOrigins: ["https://app.example.com"],
|
|
1735
|
+
token: {
|
|
1736
|
+
cookieName: "csrf",
|
|
1737
|
+
headerName: "x-csrf-token",
|
|
1738
|
+
},
|
|
1739
|
+
skip: ({ contract }) => contract.name.startsWith("webhooks."),
|
|
1740
|
+
});
|
|
1741
|
+
```
|
|
1742
|
+
|
|
1743
|
+
Pass `trustedProxy: {}` only when Beignet should compare `Origin` or `Referer`
|
|
1744
|
+
against the external `x-forwarded-host` and `x-forwarded-proto` values written
|
|
1745
|
+
by your trusted edge.
|
|
1746
|
+
|
|
1393
1747
|
### HTTP adapter boundary
|
|
1394
1748
|
|
|
1395
1749
|
`@beignet/core/server` is framework-neutral. It owns route matching, hooks,
|
|
@@ -1471,15 +1825,37 @@ Service contexts created with `server.createServiceContext(...)` receive fresh
|
|
|
1471
1825
|
sets its own `requestId`, headers and recorded events use it.
|
|
1472
1826
|
|
|
1473
1827
|
Trace primitives live in `@beignet/core/tracing` (`TraceContext`,
|
|
1474
|
-
`
|
|
1475
|
-
`
|
|
1476
|
-
dependency-free so app context
|
|
1828
|
+
`TracingPort`, `TraceOperation`, `TraceSpan`, `createTraceContext`,
|
|
1829
|
+
`createChildTraceContext`, `parseTraceparent`, `createTraceparent`,
|
|
1830
|
+
`createTraceId`, `createSpanId`). The module is dependency-free so app context
|
|
1831
|
+
types can be imported from client bundles.
|
|
1832
|
+
|
|
1833
|
+
When final ports include `ports.tracing`, requests execute inside an active
|
|
1834
|
+
`beignet.request <contract>` span. Incoming `traceparent` and `tracestate`
|
|
1835
|
+
continue the trace; if the host already established an active span, Beignet's
|
|
1836
|
+
request span becomes its child. Use cases, listeners, job handlers, schedule
|
|
1837
|
+
handlers, and task handlers create nested active spans through the same port.
|
|
1838
|
+
Install `@beignet/provider-tracing-opentelemetry` to adapt this port to an
|
|
1839
|
+
app-owned OpenTelemetry SDK and emit baseline duration, error, and provider
|
|
1840
|
+
operation metrics.
|
|
1841
|
+
|
|
1842
|
+
Listener, job, schedule, and task runners accept both a lazy `ctx` factory and
|
|
1843
|
+
an explicit `tracing` port. Provider-backed runtimes should pass the installed
|
|
1844
|
+
port so Beignet starts the workflow span before resolving
|
|
1845
|
+
`server.createServiceContext(...)`; the resulting context then inherits the
|
|
1846
|
+
real active span instead of treating local correlation IDs as a remote parent.
|
|
1847
|
+
|
|
1848
|
+
For custom `TraceOperation` values, `attributes` are span-only. Use
|
|
1849
|
+
`metricAttributes` only for bounded operation dimensions such as a contract,
|
|
1850
|
+
use-case, job, schedule, or task name. Never add request, actor, tenant, or
|
|
1851
|
+
payload values to metric attributes.
|
|
1477
1852
|
|
|
1478
1853
|
Use cases created with `createUseCase(...)` are instrumented by default. Each
|
|
1479
|
-
run resolves the instrumentation port from `ctx.ports
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
out
|
|
1854
|
+
run resolves the instrumentation port from `ctx.ports` and records `usecase`
|
|
1855
|
+
lifecycle events plus correlated `error` events for failures. When a tracing
|
|
1856
|
+
port is installed, it also creates an active child span. Pass
|
|
1857
|
+
`instrumentation: false` to opt out of instrumentation events; tracing is
|
|
1858
|
+
controlled by whether the app installs `ports.tracing`.
|
|
1483
1859
|
|
|
1484
1860
|
`createInstrumentedAuditLog({ audit, instrumentation })` from
|
|
1485
1861
|
`@beignet/core/ports` writes durable audit entries first and mirrors sanitized
|
|
@@ -1558,7 +1934,6 @@ getTodo.schema.query; // Query parameter schema
|
|
|
1558
1934
|
getTodo.schema.body; // Request body schema
|
|
1559
1935
|
getTodo.schema.responses; // Response schemas by status code
|
|
1560
1936
|
getTodo.path; // "/api/todos/:id"
|
|
1561
|
-
getTodo.pathTemplate; // "/api/todos/:id" (alias)
|
|
1562
1937
|
getTodo.method; // "GET"
|
|
1563
1938
|
getTodo.metadata; // { auth: "required", ... }
|
|
1564
1939
|
```
|