@cosmicdrift/kumiko-framework 0.362.0 → 0.364.0
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/AGENTS.md +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +14 -5
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +9 -9
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- package/src/changes.json +251 -8
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/security-baseline. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# security-baseline
|
|
4
|
+
|
|
5
|
+
securityBaselineFeatures() preset: sessions, crypto-shredding, rate-limiting, audit — plus the NODE_ENV=production boot warning when one is missing.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Security baseline
|
|
10
|
+
|
|
11
|
+
Mount `securityBaselineFeatures()` instead of hand-picking the four features
|
|
12
|
+
every prod app needs for basic account/security hygiene: `sessions`
|
|
13
|
+
(revocable JWTs), `crypto-shredding` (operator-triggered subject-key erase),
|
|
14
|
+
`rate-limiting` (ops-side bucket-status query, dispatcher wiring is
|
|
15
|
+
automatic), and `audit` (tenant-scoped audit trail). Forgetting one of them
|
|
16
|
+
used to be silent; now the boot validator warns at `NODE_ENV=production`.
|
|
17
|
+
|
|
18
|
+
## What it shows
|
|
19
|
+
|
|
20
|
+
- **`securityBaselineFeatures()`** (`@cosmicdrift/kumiko-bundled-features/presets`) —
|
|
21
|
+
returns the four features in one call, fresh instances per call.
|
|
22
|
+
- **`warnOnMissingSecurityBaseline`** — wired into `validateBoot` (fw#2857).
|
|
23
|
+
At `NODE_ENV=production`, boot compares mounted feature names against
|
|
24
|
+
`SECURITY_BASELINE_FEATURE_NAMES` and logs a `console.warn` naming whatever
|
|
25
|
+
is missing. It never throws — the baseline is a strong recommendation, not
|
|
26
|
+
a hard requirement — and it is silent outside production.
|
|
27
|
+
- **`includeSessions: false`** — an optional opt-out for dropping `sessions`
|
|
28
|
+
from the preset explicitly. Not required for combining with
|
|
29
|
+
`dsgvoSelfServiceFeatures()` any more: both mount `createSessionsFeature()`
|
|
30
|
+
with no-arg options, and the framework's `dedupeFeatures()` collapses the
|
|
31
|
+
two identical instances at boot instead of throwing.
|
|
32
|
+
|
|
33
|
+
## Feature composition
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
config → tenant's r.requires("config") dependency
|
|
37
|
+
user → cross-tenant identity (sessions' r.requires target)
|
|
38
|
+
tenant → memberships (audit's r.requires target)
|
|
39
|
+
auth-foundation → tokenVerifier extension point (sessions' r.requires target)
|
|
40
|
+
sessions → revocable JWTs
|
|
41
|
+
crypto-shredding → operator-triggered subject-key erase
|
|
42
|
+
rate-limiting → ops-side rate-limit status query
|
|
43
|
+
audit → tenant-scoped audit trail
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## When to reach for it
|
|
47
|
+
|
|
48
|
+
Any app that also mounts `dsgvoSelfServiceFeatures()` — that preset already
|
|
49
|
+
mounts `sessions` as part of its own require-chain. Combine both directly;
|
|
50
|
+
`dedupeFeatures()` collapses the two identical `sessions` instances at boot:
|
|
51
|
+
|
|
52
|
+
```ts illustration
|
|
53
|
+
import { dsgvoSelfServiceFeatures, securityBaselineFeatures } from "@cosmicdrift/kumiko-bundled-features/presets";
|
|
54
|
+
|
|
55
|
+
export const APP_FEATURES = [
|
|
56
|
+
// ...config, user, tenant, auth-foundation...
|
|
57
|
+
...dsgvoSelfServiceFeatures(),
|
|
58
|
+
...securityBaselineFeatures(),
|
|
59
|
+
];
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For an app that does not mount `dsgvoSelfServiceFeatures()`, use
|
|
63
|
+
`securityBaselineFeatures()` with its default options, as this recipe does.
|
|
64
|
+
|
|
65
|
+
Dedupe keeps the first mounted instance. Late-bound state such as
|
|
66
|
+
`bindAutoRevokeOnPasswordChange` must be bound on the instance returned by
|
|
67
|
+
`composeFeatures`/`createApp`, not on a locally held handle to the dropped one:
|
|
68
|
+
the dropped instance's hooks never run and password changes silently stop
|
|
69
|
+
revoking sessions.
|
|
70
|
+
|
|
71
|
+
A `createSessionsFeature({ ... })` mounted with different options than the
|
|
72
|
+
preset's own no-arg instance still produces a clear boot error — dedupe only
|
|
73
|
+
collapses provably interchangeable instances, never silently picks one:
|
|
74
|
+
|
|
75
|
+
```ts illustration
|
|
76
|
+
export const APP_FEATURES = [
|
|
77
|
+
// ...
|
|
78
|
+
createSessionsFeature({ expiresInMs: 1000 }),
|
|
79
|
+
...securityBaselineFeatures(),
|
|
80
|
+
// throws: Duplicate feature: "sessions" mounted twice with different
|
|
81
|
+
// options — mount it once or pass identical options
|
|
82
|
+
];
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tests
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bun test samples/recipes/security-baseline/src/__tests__/feature.test.ts
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Three cases: `APP_FEATURES` boots clean; at `NODE_ENV=production` it produces
|
|
92
|
+
no security-baseline warning; dropping `crypto-shredding`/`rate-limiting`/
|
|
93
|
+
`audit` (`sessions` stays mounted — `auth-foundation`'s own bootCheck requires
|
|
94
|
+
a sessionStore) at `NODE_ENV=production` produces exactly one warning and
|
|
95
|
+
still does not throw.
|
|
96
|
+
|
|
97
|
+
## Source files
|
|
98
|
+
|
|
99
|
+
### src/__tests__/feature.test.ts
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// Security-Baseline Sample — Test
|
|
103
|
+
// Proves: APP_FEATURES boots clean, and the NODE_ENV=production boot warning
|
|
104
|
+
// fires only when the baseline features are missing, never for APP_FEATURES.
|
|
105
|
+
|
|
106
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
107
|
+
import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
108
|
+
import { APP_FEATURES } from "../feature";
|
|
109
|
+
|
|
110
|
+
function securityBaselineWarnings(calls: readonly unknown[][]): unknown[][] {
|
|
111
|
+
return calls.filter(
|
|
112
|
+
(call) => typeof call[0] === "string" && call[0].includes("security baseline"),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe("security-baseline sample", () => {
|
|
117
|
+
test("APP_FEATURES boots without throwing", () => {
|
|
118
|
+
expect(() => validateBoot(APP_FEATURES)).not.toThrow();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("NODE_ENV=production + APP_FEATURES → no security-baseline warning", () => {
|
|
122
|
+
const originalNodeEnv = process.env["NODE_ENV"];
|
|
123
|
+
process.env["NODE_ENV"] = "production";
|
|
124
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
validateBoot(APP_FEATURES);
|
|
128
|
+
expect(securityBaselineWarnings(warnSpy.mock.calls)).toHaveLength(0);
|
|
129
|
+
} finally {
|
|
130
|
+
warnSpy.mockRestore();
|
|
131
|
+
if (originalNodeEnv === undefined) {
|
|
132
|
+
delete process.env["NODE_ENV"];
|
|
133
|
+
} else {
|
|
134
|
+
process.env["NODE_ENV"] = originalNodeEnv;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("NODE_ENV=production + baseline features missing → warns but does not throw", () => {
|
|
140
|
+
const originalNodeEnv = process.env["NODE_ENV"];
|
|
141
|
+
process.env["NODE_ENV"] = "production";
|
|
142
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
143
|
+
// Keep "sessions" mounted — auth-foundation's own bootCheck requires a
|
|
144
|
+
// tokenVerifier or sessionStore, and dropping it would fail boot for an
|
|
145
|
+
// unrelated reason before warnOnMissingSecurityBaseline ever runs.
|
|
146
|
+
const withoutBaseline = APP_FEATURES.filter(
|
|
147
|
+
(f) => !["crypto-shredding", "rate-limiting", "audit"].includes(f.name),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
expect(() => validateBoot(withoutBaseline)).not.toThrow();
|
|
152
|
+
expect(securityBaselineWarnings(warnSpy.mock.calls)).toHaveLength(1);
|
|
153
|
+
} finally {
|
|
154
|
+
warnSpy.mockRestore();
|
|
155
|
+
if (originalNodeEnv === undefined) {
|
|
156
|
+
delete process.env["NODE_ENV"];
|
|
157
|
+
} else {
|
|
158
|
+
process.env["NODE_ENV"] = originalNodeEnv;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### src/feature.ts
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// Security-Baseline Sample
|
|
169
|
+
// Shows: mounting securityBaselineFeatures() (sessions, crypto-shredding,
|
|
170
|
+
// rate-limiting, audit) and how it silences the NODE_ENV=production boot
|
|
171
|
+
// warning that fires when one of those four is missing.
|
|
172
|
+
|
|
173
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
174
|
+
import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
175
|
+
import { securityBaselineFeatures } from "@cosmicdrift/kumiko-bundled-features/presets";
|
|
176
|
+
import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
177
|
+
import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
178
|
+
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
179
|
+
|
|
180
|
+
export const APP_FEATURES: FeatureDefinition[] = [
|
|
181
|
+
createConfigFeature(),
|
|
182
|
+
createUserFeature(),
|
|
183
|
+
createTenantFeature(),
|
|
184
|
+
authFoundationFeature,
|
|
185
|
+
...securityBaselineFeatures(),
|
|
186
|
+
];
|
|
187
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/seed-migration. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# seed-migration
|
|
4
|
+
|
|
5
|
+
ES-Operations seed-migrations — file-based diff-and-apply für Aggregate-Updates die idempotent-Seeder nicht erfassen.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Seed-Migration
|
|
10
|
+
|
|
11
|
+
File-based migrations for Event-Sourcing operations — like `kumiko schema apply`, but for aggregate-state-changes after the initial seed.
|
|
12
|
+
|
|
13
|
+
## What it shows
|
|
14
|
+
|
|
15
|
+
- **`seedsDir`-Option** in `runProdApp({ seedsDir: "./seeds" })` — beim Boot werden pending Files in chronologischer Reihenfolge angewendet und in `kumiko_es_operations` markiert.
|
|
16
|
+
- **`SeedMigration`-Interface** — default-Export einer seed-File mit `description` + `run(ctx)`. ctx liefert `systemWriteAs` (System-User bypassed Access-Check) + Read-Helpers (`findUserByEmail`, `findMembershipsOfUser`, `findTenants`, `findTemplateResources`).
|
|
17
|
+
- **Idempotency** — Marker landet nach Erfolg in der Tracking-Tabelle, zweiter Boot skipped applied Seeds.
|
|
18
|
+
- **Tx-Atomicity** — Jede Migration läuft in eigener Transaction; Failure rollt zurück + bricht Boot ab (kein Partial-Apply).
|
|
19
|
+
- **Chronologische File-IDs** — Filename `<date>-<slug>.ts` (z.B. `2026-05-20-fix-admin-roles.ts`) ist die ID.
|
|
20
|
+
|
|
21
|
+
### Phase 1.5 hinzugefügt
|
|
22
|
+
|
|
23
|
+
- **`tenantIdOverride`** in `ctx.systemWriteAs(qn, payload, tenantId)` — Pflicht wenn das Aggregate in einem Tenant-Stream lebt (sonst `version_conflict`).
|
|
24
|
+
- **Dry-Run-Validator** — Runner parsed seed-files vor Run + checked alle handler-QNs gegen registry. camelCase-typos & andere QN-Drift werden BEFORE the write erkannt.
|
|
25
|
+
- **`scripts/smoke.ts`** — copy-paste-Template für lokalen Pre-Push-Smoke. Bun-runnable, validiert Module-Load + QN-Resolution + Access offline. **Pflicht-Pattern vor Push.**
|
|
26
|
+
- **Docker-COPY-Pflicht** dokumentiert in `framework/src/es-ops/README.md`.
|
|
27
|
+
|
|
28
|
+
## Lokaler Smoke vor Push (Pflicht)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Im App-Root:
|
|
32
|
+
bun samples/recipes/seed-migration/scripts/smoke.ts --seeds-dir ./seeds
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Erwarteter Output:
|
|
36
|
+
```
|
|
37
|
+
seed: 2026-05-20-fix-admin-roles.ts
|
|
38
|
+
✓ loads: "ergänze TenantAdmin-Rolle"
|
|
39
|
+
✓ "tenant:write:update-member-roles" registered + accessible
|
|
40
|
+
|
|
41
|
+
✓ all 1 seed-file(s) pass smoke.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Bei typo / drift fail-t mit klarer message + exit-code 1 — CI nutzt es als prä-build-step. App-Author muss in `smoke.ts` den `features`-Array gegen die eigene App-Feature-Set tauschen (siehe TODO im File).
|
|
45
|
+
|
|
46
|
+
### Archive template rows
|
|
47
|
+
|
|
48
|
+
`findTemplateResources({ slug?, kind?, status?, locale?, tenantId? })` replaces a raw `SELECT … FROM read_template_resources`. It defaults to the system tenant and returns `[]` when template-resolver is not mounted:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
for (const t of await ctx.findTemplateResources({ kind: "mail", status: "active" })) {
|
|
52
|
+
await ctx.systemWriteAs("template-resolver:write:archive", { id: t.id }, SYSTEM_TENANT_ID, [
|
|
53
|
+
"SystemAdmin",
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## When to reach for it
|
|
59
|
+
|
|
60
|
+
Du hast einen idempotent-Seeder der bei initialer Erstellung „Insert wenn nicht existiert" macht (z.B. `auth.admin.memberships`), und dann ändern sich die Soll-Daten — neue Rolle, anderer Tier, korrigierte Bezeichnung. Idempotent-Seeder skippen die existing Rows → DB driftet vom Code-Stand ab.
|
|
61
|
+
|
|
62
|
+
Seed-Migrations sind der saubere Fix: ein File schreibt das gewünschte Update einmalig in den Event-Store, Projection läuft automatisch.
|
|
63
|
+
|
|
64
|
+
## Don't reach for it when
|
|
65
|
+
|
|
66
|
+
- **Initial Seeding** (erste Daten beim leeren Stack). Dafür gibt es `r.config({seeds})` + `options.seeds`-Array — idempotent-by-design durch deterministische Aggregate-IDs.
|
|
67
|
+
- **Schema-Migrations**. Dafür gibt es `kumiko schema generate`. Seed-migrations sind
|
|
68
|
+
Data-Layer, nicht Schema-Layer.
|
|
69
|
+
- **Read-only Operationen / Reports**. Dafür gibt es App-spezifische Routes / Queries.
|
|
70
|
+
|
|
71
|
+
## How it works
|
|
72
|
+
|
|
73
|
+
1. Bei `runProdApp`-Boot scannt das Framework `seedsDir` nach `*.ts`-Files.
|
|
74
|
+
2. SELECT `id` FROM `kumiko_es_operations` WHERE `operation_type='seed-migration'` → applied-Set.
|
|
75
|
+
3. Pending = files-on-disk **minus** applied-set, sortiert chronologisch.
|
|
76
|
+
4. Pro pending: Tx auf, `migration.run(ctx)` (kann beliebige Handler via `systemWriteAs` rufen), Marker einfügen, Tx commit.
|
|
77
|
+
5. Failure → Tx rollback, kein Marker, Boot bricht ab. Operator fixt + Retry.
|
|
78
|
+
|
|
79
|
+
## CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Scaffold neue Migration mit Datum-Prefix
|
|
83
|
+
bunx kumiko ops seed:new fix-admin-roles
|
|
84
|
+
# → seeds/2026-05-20-fix-admin-roles.ts
|
|
85
|
+
|
|
86
|
+
# Was applied, was pending
|
|
87
|
+
bunx kumiko ops seed:status
|
|
88
|
+
|
|
89
|
+
# (Phase 1.5) Direct apply ohne Boot
|
|
90
|
+
bunx kumiko ops seed:apply [--dry-run]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Skippable im Notfall
|
|
94
|
+
|
|
95
|
+
Wenn eine kaputte Migration den Boot blockiert: setze `skippable: true` im seed-File und beim nächsten Boot `KUMIKO_SKIP_ES_OPS_<sanitized-id>=1` env-var. Marker wird nicht geschrieben → beim nächsten Boot ohne Flag würde der Seed wieder laufen. NICHT als Standard-Workflow — Recovery-only.
|
|
96
|
+
|
|
97
|
+
## See also
|
|
98
|
+
|
|
99
|
+
- **Driver-Use-Case:** `seeds/2026-02-01-fix-admin-roles.ts` in this recipe
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/session-revocation. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# session-revocation
|
|
4
|
+
|
|
5
|
+
createSessionCallbacks + sessionChecker — server-side session revocation via JWT jti.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Sample: Session Revocation
|
|
10
|
+
|
|
11
|
+
**I want JWTs to be revocable server-side — logout, password change, or admin force-logout should invalidate outstanding tokens before they expire.**
|
|
12
|
+
|
|
13
|
+
## What this sample shows
|
|
14
|
+
|
|
15
|
+
The production wiring shape: an app builds `createSessionCallbacks()` against a live DB handle, then hands the creator, revoker, and checker to `buildServer({ auth: ... })`. From there the framework handles the rest — login persists a `sid`, the middleware checks it on every request, logout flips the DB row, and a forged JWT without a matching session is rejected.
|
|
16
|
+
|
|
17
|
+
## The wiring in 3 sentences
|
|
18
|
+
|
|
19
|
+
1. `createSessionCallbacks({ db })` returns `sessionCreator`, `sessionRevoker`, and `sessionChecker` backed by `store_user_sessions`.
|
|
20
|
+
2. `buildServer({ auth: { sessionCreator, sessionRevoker, sessionChecker, ... } })` links the JWT `jti` claim to that table on every request.
|
|
21
|
+
3. After logout (or mass-revoke on password change), the same JWT returns 401 even though the signature is still valid.
|
|
22
|
+
|
|
23
|
+
## What to copy into your app
|
|
24
|
+
|
|
25
|
+
```ts illustration
|
|
26
|
+
const db = createDbConnection(config.databaseUrl);
|
|
27
|
+
const callbacks = createSessionCallbacks({ db });
|
|
28
|
+
|
|
29
|
+
const server = buildServer({
|
|
30
|
+
registry: buildRegistry([
|
|
31
|
+
createSessionsFeature({
|
|
32
|
+
autoRevokeOnPasswordChange: callbacks.sessionMassRevoker,
|
|
33
|
+
}),
|
|
34
|
+
// ... other features
|
|
35
|
+
]),
|
|
36
|
+
context: { db, /* ... */ },
|
|
37
|
+
jwtSecret: process.env.JWT_SECRET,
|
|
38
|
+
auth: {
|
|
39
|
+
membershipQuery: "tenant:query:memberships",
|
|
40
|
+
loginHandler: "auth-email-password:write:login",
|
|
41
|
+
sessionCreator: callbacks.sessionCreator,
|
|
42
|
+
sessionRevoker: callbacks.sessionRevoker,
|
|
43
|
+
sessionChecker: callbacks.sessionChecker,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Design rules
|
|
49
|
+
|
|
50
|
+
1. **Sessions are a feature, not built-in.** Without `createSessionsFeature()` and wired callbacks, the app issues plain stateless JWTs — valid until expiry, no revocation path.
|
|
51
|
+
2. **Storage is pluggable.** This sample uses the default DB-backed impl; swap in Redis or Memcached by implementing the same `SessionCreator` / `SessionRevoker` / `SessionChecker` signatures.
|
|
52
|
+
3. **`jti` is the link.** When a checker is wired, the middleware never trusts the JWT alone — it confirms the session row is still live.
|
|
53
|
+
|
|
54
|
+
## Tests
|
|
55
|
+
|
|
56
|
+
- **End-to-end:** login → authenticated query → logout → same token rejected with 401
|
|
57
|
+
- **Wiring contract:** copies the `buildServer({ auth: ... })` block from the sample comment verbatim
|
|
58
|
+
|
|
59
|
+
## Source files
|
|
60
|
+
|
|
61
|
+
### src/__tests__/feature.integration.test.ts
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// Session Revocation Sample — Integration Test
|
|
65
|
+
//
|
|
66
|
+
// Drives the wiring from the sample's comment snippet end-to-end against
|
|
67
|
+
// real HTTP + DB + Redis. If a reader copies the buildServer({...}) block
|
|
68
|
+
// verbatim, the resulting app behaves like this test asserts.
|
|
69
|
+
|
|
70
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
71
|
+
import {
|
|
72
|
+
AuthHandlers,
|
|
73
|
+
createAuthEmailPasswordFeature,
|
|
74
|
+
hashPassword,
|
|
75
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
76
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
77
|
+
import {
|
|
78
|
+
configValuesTable,
|
|
79
|
+
createConfigFeature,
|
|
80
|
+
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
81
|
+
import {
|
|
82
|
+
type SessionCallbacks,
|
|
83
|
+
userSessionEntity,
|
|
84
|
+
userSessionTable,
|
|
85
|
+
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
86
|
+
import {
|
|
87
|
+
sessionCallbacksFromLateBound,
|
|
88
|
+
withMintedSession,
|
|
89
|
+
} from "@cosmicdrift/kumiko-bundled-features/sessions/testing";
|
|
90
|
+
import {
|
|
91
|
+
createTenantFeature,
|
|
92
|
+
tenantEntity,
|
|
93
|
+
tenantMembershipsTable,
|
|
94
|
+
} from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
95
|
+
import { seedTenantMembership } from "@cosmicdrift/kumiko-bundled-features/tenant/seeding";
|
|
96
|
+
import {
|
|
97
|
+
createUserFeature,
|
|
98
|
+
UserHandlers,
|
|
99
|
+
userEntity,
|
|
100
|
+
userTable,
|
|
101
|
+
} from "@cosmicdrift/kumiko-bundled-features/user";
|
|
102
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
103
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
104
|
+
import {
|
|
105
|
+
setupTestStack,
|
|
106
|
+
type TestStack,
|
|
107
|
+
TestUsers,
|
|
108
|
+
testTenantId,
|
|
109
|
+
unsafeCreateEntityTable,
|
|
110
|
+
unsafePushTables,
|
|
111
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
112
|
+
import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
|
|
113
|
+
import * as jose from "jose";
|
|
114
|
+
import { createSessionCallbacks, createSessionsFeature } from "../feature";
|
|
115
|
+
|
|
116
|
+
const TENANT: TenantId = testTenantId(1);
|
|
117
|
+
|
|
118
|
+
let stack: TestStack;
|
|
119
|
+
const callbacks = createLateBoundHolder<SessionCallbacks>("session-callbacks");
|
|
120
|
+
|
|
121
|
+
beforeAll(async () => {
|
|
122
|
+
const bound = sessionCallbacksFromLateBound(callbacks);
|
|
123
|
+
stack = await setupTestStack({
|
|
124
|
+
features: [
|
|
125
|
+
createConfigFeature(),
|
|
126
|
+
createUserFeature(),
|
|
127
|
+
createTenantFeature(),
|
|
128
|
+
createAuthEmailPasswordFeature(),
|
|
129
|
+
authFoundationFeature,
|
|
130
|
+
createSessionsFeature(),
|
|
131
|
+
],
|
|
132
|
+
authConfig: {
|
|
133
|
+
...bound.asAuthConfig(),
|
|
134
|
+
membershipQuery: "tenant:query:memberships",
|
|
135
|
+
loginHandler: AuthHandlers.login,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
callbacks.set(createSessionCallbacks({ db: stack.db }));
|
|
139
|
+
|
|
140
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
141
|
+
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
142
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
143
|
+
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterAll(async () => {
|
|
147
|
+
await stack.cleanup();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
beforeEach(async () => {
|
|
151
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userSessionTable.tableName}"`);
|
|
152
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
153
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("session-revocation sample wiring", () => {
|
|
157
|
+
test("end-to-end: login, use, logout, reject", async () => {
|
|
158
|
+
const hash = await hashPassword("super-long-password");
|
|
159
|
+
const actor = await withMintedSession(
|
|
160
|
+
(user, meta) => callbacks.get().sessionCreator(user, meta),
|
|
161
|
+
TestUsers.systemAdmin,
|
|
162
|
+
);
|
|
163
|
+
const created = await stack.http.writeOk<{ id: string }>(
|
|
164
|
+
UserHandlers.create,
|
|
165
|
+
{ email: "demo@example.com", passwordHash: hash, displayName: "Demo" },
|
|
166
|
+
actor,
|
|
167
|
+
);
|
|
168
|
+
await seedTenantMembership(stack.db, {
|
|
169
|
+
userId: created.id,
|
|
170
|
+
tenantId: TENANT,
|
|
171
|
+
roles: ["User"],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const loginRes = await stack.http.raw("POST", "/api/auth/login", {
|
|
175
|
+
email: "demo@example.com",
|
|
176
|
+
password: "super-long-password",
|
|
177
|
+
});
|
|
178
|
+
expect(loginRes.status).toBe(200);
|
|
179
|
+
const { token } = (await loginRes.json()) as { token: string };
|
|
180
|
+
expect(jose.decodeJwt(token).jti).toBeTypeOf("string");
|
|
181
|
+
|
|
182
|
+
const beforeLogout = await stack.http.raw(
|
|
183
|
+
"POST",
|
|
184
|
+
"/api/query",
|
|
185
|
+
{ type: "user:query:user:me", payload: {} },
|
|
186
|
+
{ Authorization: `Bearer ${token}` },
|
|
187
|
+
);
|
|
188
|
+
expect(beforeLogout.status).toBe(200);
|
|
189
|
+
|
|
190
|
+
const logout = await stack.http.raw("POST", "/api/auth/logout", undefined, {
|
|
191
|
+
Authorization: `Bearer ${token}`,
|
|
192
|
+
});
|
|
193
|
+
expect(logout.status).toBe(200);
|
|
194
|
+
|
|
195
|
+
const afterLogout = await stack.http.raw(
|
|
196
|
+
"POST",
|
|
197
|
+
"/api/query",
|
|
198
|
+
{ type: "user:query:user:me", payload: {} },
|
|
199
|
+
{ Authorization: `Bearer ${token}` },
|
|
200
|
+
);
|
|
201
|
+
expect(afterLogout.status).toBe(401);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### src/feature.ts
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// Session Revocation Sample
|
|
210
|
+
//
|
|
211
|
+
// Shows the production-wiring shape: an app builds `createSessionCallbacks()`
|
|
212
|
+
// against a live DB handle, then hands the creator/revoker/checker to
|
|
213
|
+
// `buildServer({ auth: ... })`. From there the framework handles the rest —
|
|
214
|
+
// login persists a sid, the middleware checks it on every request, logout
|
|
215
|
+
// flips the DB row, a forged JWT without a matching session is rejected.
|
|
216
|
+
//
|
|
217
|
+
// What to copy into your own app:
|
|
218
|
+
//
|
|
219
|
+
// const db = createDbConnection(config.databaseUrl);
|
|
220
|
+
// const callbacks = createSessionCallbacks({ db });
|
|
221
|
+
// const server = buildServer({
|
|
222
|
+
// registry: buildRegistry([
|
|
223
|
+
// // Pass the mass-revoker into the sessions feature to wire
|
|
224
|
+
// // "password-change signs you out everywhere" — the feature registers
|
|
225
|
+
// // a cross-feature entity-hook on the user entity that runs it.
|
|
226
|
+
// createSessionsFeature({
|
|
227
|
+
// autoRevokeOnPasswordChange: callbacks.sessionMassRevoker,
|
|
228
|
+
// }),
|
|
229
|
+
// // ... other features
|
|
230
|
+
// ]),
|
|
231
|
+
// context: { db, ... },
|
|
232
|
+
// jwtSecret: config.jwtSecret,
|
|
233
|
+
// auth: {
|
|
234
|
+
// membershipQuery: "tenant:query:memberships",
|
|
235
|
+
// loginHandler: "auth-email-password:write:login",
|
|
236
|
+
// sessionCreator: callbacks.sessionCreator,
|
|
237
|
+
// sessionRevoker: callbacks.sessionRevoker,
|
|
238
|
+
// sessionChecker: callbacks.sessionChecker,
|
|
239
|
+
// },
|
|
240
|
+
// });
|
|
241
|
+
//
|
|
242
|
+
// Design rules the sample demonstrates:
|
|
243
|
+
//
|
|
244
|
+
// 1. Sessions are a feature, not built-in. An app that doesn't register
|
|
245
|
+
// `createSessionsFeature()` (or doesn't wire callbacks) issues plain
|
|
246
|
+
// stateless JWTs — valid until expiry, no revocation path.
|
|
247
|
+
// 2. Session storage is abstracted behind the three callback signatures.
|
|
248
|
+
// This sample uses the default DB-backed impl; you could swap in a
|
|
249
|
+
// Redis- or Memcached-backed version by wiring your own callbacks that
|
|
250
|
+
// match `SessionCreator`/`SessionRevoker`/`SessionChecker`.
|
|
251
|
+
// 3. The `jti` claim on the JWT is the link between stateless token and
|
|
252
|
+
// stateful server. The middleware never trusts the JWT alone when a
|
|
253
|
+
// checker is wired — it confirms the sid is still live.
|
|
254
|
+
|
|
255
|
+
export {
|
|
256
|
+
createSessionCallbacks,
|
|
257
|
+
createSessionsFeature,
|
|
258
|
+
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
259
|
+
```
|