@mandujs/core 0.53.2 → 0.54.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/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,274 +1,274 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @mandujs/core/auth/tokens — store tests.
|
|
3
|
-
*
|
|
4
|
-
* Each case builds a fresh `:memory:` store (gated on Bun.SQL availability)
|
|
5
|
-
* with `gcSchedule: false` so no real cron fires during tests. Closed in
|
|
6
|
-
* `afterEach`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
createAuthTokenStore,
|
|
13
|
-
type AuthTokenStore,
|
|
14
|
-
type TokenRecord,
|
|
15
|
-
} from "../tokens";
|
|
16
|
-
|
|
17
|
-
// ─── Gate on Bun.SQL + Bun.CryptoHasher ─────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const hasBunSql = (() => {
|
|
20
|
-
const g = globalThis as unknown as {
|
|
21
|
-
Bun?: { SQL?: unknown; CryptoHasher?: unknown };
|
|
22
|
-
};
|
|
23
|
-
return typeof g.Bun?.SQL === "function" && typeof g.Bun?.CryptoHasher === "function";
|
|
24
|
-
})();
|
|
25
|
-
const describeIfBun = hasBunSql ? describe : describe.skip;
|
|
26
|
-
|
|
27
|
-
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
const SECRET = "tokens-test-secret-at-least-32-bytes-long!";
|
|
30
|
-
|
|
31
|
-
describeIfBun("@mandujs/core/auth/tokens — createAuthTokenStore", () => {
|
|
32
|
-
let store: AuthTokenStore;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
store = createAuthTokenStore({
|
|
36
|
-
secret: SECRET,
|
|
37
|
-
dbPath: ":memory:",
|
|
38
|
-
gcSchedule: false,
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(async () => {
|
|
43
|
-
await store.close();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("mint → consume roundtrip returns the record with userId + purpose", async () => {
|
|
47
|
-
const { token, record } = await store.mint("verify-email", "u-1", {
|
|
48
|
-
email: "a@b.com",
|
|
49
|
-
});
|
|
50
|
-
expect(token).toContain(".");
|
|
51
|
-
expect(record.userId).toBe("u-1");
|
|
52
|
-
expect(record.purpose).toBe("verify-email");
|
|
53
|
-
expect(record.consumedAt).toBeNull();
|
|
54
|
-
expect(record.tokenHash).toMatch(/^[0-9a-f]{64}$/);
|
|
55
|
-
|
|
56
|
-
const consumed = await store.consume("verify-email", token);
|
|
57
|
-
expect(consumed).not.toBeNull();
|
|
58
|
-
expect(consumed!.userId).toBe("u-1");
|
|
59
|
-
expect(consumed!.id).toBe(record.id);
|
|
60
|
-
expect(consumed!.consumedAt).toBeTypeOf("number");
|
|
61
|
-
expect(consumed!.consumedAt).not.toBeNull();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("consuming twice: second call returns null (single-use)", async () => {
|
|
65
|
-
const { token } = await store.mint("verify-email", "u-2", { email: "a@b.com" });
|
|
66
|
-
const first = await store.consume("verify-email", token);
|
|
67
|
-
expect(first).not.toBeNull();
|
|
68
|
-
const second = await store.consume("verify-email", token);
|
|
69
|
-
expect(second).toBeNull();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("expired token returns null", async () => {
|
|
73
|
-
// Zero TTL means expires_at == now at insert. The insert completes at
|
|
74
|
-
// T, and consume() reads `now > T` → treated as expired. We sleep a
|
|
75
|
-
// few ms to be defensive against same-millisecond flakiness.
|
|
76
|
-
const shortLived = createAuthTokenStore({
|
|
77
|
-
secret: SECRET,
|
|
78
|
-
dbPath: ":memory:",
|
|
79
|
-
gcSchedule: false,
|
|
80
|
-
ttlSecondsByPurpose: { "verify-email": 0 },
|
|
81
|
-
});
|
|
82
|
-
try {
|
|
83
|
-
const { token } = await shortLived.mint("verify-email", "u-3");
|
|
84
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
85
|
-
const result = await shortLived.consume("verify-email", token);
|
|
86
|
-
expect(result).toBeNull();
|
|
87
|
-
} finally {
|
|
88
|
-
await shortLived.close();
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("tampered nonce returns null (hash mismatch)", async () => {
|
|
93
|
-
const { token } = await store.mint("verify-email", "u-4");
|
|
94
|
-
const [id, nonce] = token.split(".");
|
|
95
|
-
// Flip one character in the nonce. Keep length the same to exercise
|
|
96
|
-
// the byte-wise hash compare (length mismatch would short-circuit).
|
|
97
|
-
const flipped = (nonce!.charAt(0) === "A" ? "B" : "A") + nonce!.slice(1);
|
|
98
|
-
const tampered = `${id}.${flipped}`;
|
|
99
|
-
const result = await store.consume("verify-email", tampered);
|
|
100
|
-
expect(result).toBeNull();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("tampered id (pointing to real row) still returns null — hash mismatch", async () => {
|
|
104
|
-
const { token: a } = await store.mint("verify-email", "u-5a");
|
|
105
|
-
const { token: b } = await store.mint("verify-email", "u-5b");
|
|
106
|
-
const aNonce = a.split(".")[1]!;
|
|
107
|
-
const bId = b.split(".")[0]!;
|
|
108
|
-
// Forged token: use B's id with A's nonce. B's stored hash was
|
|
109
|
-
// computed against B's nonce, so the compare fails.
|
|
110
|
-
const forged = `${bId}.${aNonce}`;
|
|
111
|
-
const result = await store.consume("verify-email", forged);
|
|
112
|
-
expect(result).toBeNull();
|
|
113
|
-
|
|
114
|
-
// Sanity: the real B token still consumes cleanly afterwards. The
|
|
115
|
-
// forged attempt did NOT mark B as consumed.
|
|
116
|
-
const real = await store.consume("verify-email", b);
|
|
117
|
-
expect(real).not.toBeNull();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("wrong purpose returns null (purpose binds to the hash)", async () => {
|
|
121
|
-
const { token } = await store.mint("verify-email", "u-6");
|
|
122
|
-
const result = await store.consume("reset-password", token);
|
|
123
|
-
expect(result).toBeNull();
|
|
124
|
-
|
|
125
|
-
// Real purpose still works — wrong-purpose attempt did NOT mark it
|
|
126
|
-
// consumed.
|
|
127
|
-
const real = await store.consume("verify-email", token);
|
|
128
|
-
expect(real).not.toBeNull();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("unknown id returns null without touching other rows", async () => {
|
|
132
|
-
const { token } = await store.mint("verify-email", "u-7");
|
|
133
|
-
// Use a random UUID as the id so the row simply doesn't exist.
|
|
134
|
-
const fakeId = crypto.randomUUID();
|
|
135
|
-
const nonce = token.split(".")[1]!;
|
|
136
|
-
const bogus = `${fakeId}.${nonce}`;
|
|
137
|
-
const result = await store.consume("verify-email", bogus);
|
|
138
|
-
expect(result).toBeNull();
|
|
139
|
-
|
|
140
|
-
// Real token still usable.
|
|
141
|
-
const real = await store.consume("verify-email", token);
|
|
142
|
-
expect(real).not.toBeNull();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("gcNow() sweeps expired + consumed rows and returns the count", async () => {
|
|
146
|
-
// Consume one row to create a "consumed" candidate.
|
|
147
|
-
const { token: consumedToken } = await store.mint("verify-email", "u-8a");
|
|
148
|
-
await store.consume("verify-email", consumedToken);
|
|
149
|
-
|
|
150
|
-
// Build a short-lived entry via a second store on the same (in-memory)
|
|
151
|
-
// semantics — we can't share a :memory: DB across handles, so mint
|
|
152
|
-
// into a different short-TTL store to prove the delete query works.
|
|
153
|
-
const expiring = createAuthTokenStore({
|
|
154
|
-
secret: SECRET,
|
|
155
|
-
dbPath: ":memory:",
|
|
156
|
-
gcSchedule: false,
|
|
157
|
-
ttlSecondsByPurpose: { "verify-email": 0 },
|
|
158
|
-
});
|
|
159
|
-
try {
|
|
160
|
-
await expiring.mint("verify-email", "u-8b");
|
|
161
|
-
await expiring.mint("verify-email", "u-8c");
|
|
162
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
163
|
-
const deleted = await expiring.gcNow();
|
|
164
|
-
expect(deleted).toBe(2);
|
|
165
|
-
// Second sweep is a no-op.
|
|
166
|
-
expect(await expiring.gcNow()).toBe(0);
|
|
167
|
-
} finally {
|
|
168
|
-
await expiring.close();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// The original store has exactly one row (consumed); gcNow removes it.
|
|
172
|
-
const cleaned = await store.gcNow();
|
|
173
|
-
expect(cleaned).toBe(1);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("meta round-trips as JSON for verify-email tokens", async () => {
|
|
177
|
-
const { token, record: minted } = await store.mint("verify-email", "u-9", {
|
|
178
|
-
email: "alice@example.com",
|
|
179
|
-
signupSource: "marketing-page",
|
|
180
|
-
});
|
|
181
|
-
expect(minted.meta?.email).toBe("alice@example.com");
|
|
182
|
-
|
|
183
|
-
const consumed = await store.consume("verify-email", token);
|
|
184
|
-
expect(consumed).not.toBeNull();
|
|
185
|
-
expect(consumed!.meta?.email).toBe("alice@example.com");
|
|
186
|
-
expect(consumed!.meta?.signupSource).toBe("marketing-page");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("sequential consumes serialise through the UPDATE predicate — only the first wins", async () => {
|
|
190
|
-
// True wall-clock concurrency is not expressible against a single
|
|
191
|
-
// SQLite connection (Bun.SQL surfaces "cannot start a transaction
|
|
192
|
-
// within a transaction" when two callers share the handle). The
|
|
193
|
-
// atomicity guarantee we actually care about is observable
|
|
194
|
-
// sequentially: consume #2 must see `consumed_at IS NOT NULL` from
|
|
195
|
-
// consume #1's committed transaction and return null.
|
|
196
|
-
//
|
|
197
|
-
// The defense-in-depth `UPDATE … WHERE consumed_at IS NULL` predicate
|
|
198
|
-
// is what guards against a hypothetical interleaving if multiple
|
|
199
|
-
// writer connections ever land in the future (e.g., Postgres).
|
|
200
|
-
const { token } = await store.mint("verify-email", "u-10");
|
|
201
|
-
const a = await store.consume("verify-email", token);
|
|
202
|
-
const b = await store.consume("verify-email", token);
|
|
203
|
-
const successes: TokenRecord[] = [];
|
|
204
|
-
if (a) successes.push(a);
|
|
205
|
-
if (b) successes.push(b);
|
|
206
|
-
expect(successes).toHaveLength(1);
|
|
207
|
-
expect(a!.consumedAt).not.toBeNull();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("malformed token (no dot) returns null without throwing", async () => {
|
|
211
|
-
// Exercise the `parseToken` no-dot path, the empty-string path, and
|
|
212
|
-
// the multi-dot path — all must collapse to null.
|
|
213
|
-
const nodot = await store.consume("verify-email", "not-a-token");
|
|
214
|
-
expect(nodot).toBeNull();
|
|
215
|
-
|
|
216
|
-
const empty = await store.consume("verify-email", "");
|
|
217
|
-
expect(empty).toBeNull();
|
|
218
|
-
|
|
219
|
-
const multiDot = await store.consume("verify-email", "a.b.c");
|
|
220
|
-
expect(multiDot).toBeNull();
|
|
221
|
-
|
|
222
|
-
const leadingDot = await store.consume("verify-email", ".nonce");
|
|
223
|
-
expect(leadingDot).toBeNull();
|
|
224
|
-
|
|
225
|
-
const trailingDot = await store.consume("verify-email", "id.");
|
|
226
|
-
expect(trailingDot).toBeNull();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("close() then mint/consume/gcNow rejects with a clear error", async () => {
|
|
230
|
-
const tempStore = createAuthTokenStore({
|
|
231
|
-
secret: SECRET,
|
|
232
|
-
dbPath: ":memory:",
|
|
233
|
-
gcSchedule: false,
|
|
234
|
-
});
|
|
235
|
-
// Populate one row so gcNow has something to scan.
|
|
236
|
-
await tempStore.mint("verify-email", "u-11");
|
|
237
|
-
await tempStore.close();
|
|
238
|
-
|
|
239
|
-
await expect(tempStore.mint("verify-email", "u-12")).rejects.toThrow(
|
|
240
|
-
/store is closed/,
|
|
241
|
-
);
|
|
242
|
-
await expect(tempStore.consume("verify-email", "x.y")).rejects.toThrow(
|
|
243
|
-
/store is closed/,
|
|
244
|
-
);
|
|
245
|
-
await expect(tempStore.gcNow()).rejects.toThrow(/store is closed/);
|
|
246
|
-
// close() is idempotent.
|
|
247
|
-
await expect(tempStore.close()).resolves.toBeUndefined();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("empty secret is rejected synchronously at construction", () => {
|
|
251
|
-
expect(() =>
|
|
252
|
-
createAuthTokenStore({ secret: "", dbPath: ":memory:", gcSchedule: false }),
|
|
253
|
-
).toThrow(/secret/);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("invalid table name is rejected synchronously at construction", () => {
|
|
257
|
-
expect(() =>
|
|
258
|
-
createAuthTokenStore({
|
|
259
|
-
secret: SECRET,
|
|
260
|
-
dbPath: ":memory:",
|
|
261
|
-
table: "bad-name; DROP TABLE users",
|
|
262
|
-
gcSchedule: false,
|
|
263
|
-
}),
|
|
264
|
-
).toThrow(/table name/);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("reset-password token consumed at the wrong purpose returns null", async () => {
|
|
268
|
-
const { token } = await store.mint("reset-password", "u-13");
|
|
269
|
-
const wrongPurpose = await store.consume("verify-email", token);
|
|
270
|
-
expect(wrongPurpose).toBeNull();
|
|
271
|
-
const rightPurpose = await store.consume("reset-password", token);
|
|
272
|
-
expect(rightPurpose).not.toBeNull();
|
|
273
|
-
});
|
|
274
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @mandujs/core/auth/tokens — store tests.
|
|
3
|
+
*
|
|
4
|
+
* Each case builds a fresh `:memory:` store (gated on Bun.SQL availability)
|
|
5
|
+
* with `gcSchedule: false` so no real cron fires during tests. Closed in
|
|
6
|
+
* `afterEach`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createAuthTokenStore,
|
|
13
|
+
type AuthTokenStore,
|
|
14
|
+
type TokenRecord,
|
|
15
|
+
} from "../tokens";
|
|
16
|
+
|
|
17
|
+
// ─── Gate on Bun.SQL + Bun.CryptoHasher ─────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const hasBunSql = (() => {
|
|
20
|
+
const g = globalThis as unknown as {
|
|
21
|
+
Bun?: { SQL?: unknown; CryptoHasher?: unknown };
|
|
22
|
+
};
|
|
23
|
+
return typeof g.Bun?.SQL === "function" && typeof g.Bun?.CryptoHasher === "function";
|
|
24
|
+
})();
|
|
25
|
+
const describeIfBun = hasBunSql ? describe : describe.skip;
|
|
26
|
+
|
|
27
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const SECRET = "tokens-test-secret-at-least-32-bytes-long!";
|
|
30
|
+
|
|
31
|
+
describeIfBun("@mandujs/core/auth/tokens — createAuthTokenStore", () => {
|
|
32
|
+
let store: AuthTokenStore;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
store = createAuthTokenStore({
|
|
36
|
+
secret: SECRET,
|
|
37
|
+
dbPath: ":memory:",
|
|
38
|
+
gcSchedule: false,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await store.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("mint → consume roundtrip returns the record with userId + purpose", async () => {
|
|
47
|
+
const { token, record } = await store.mint("verify-email", "u-1", {
|
|
48
|
+
email: "a@b.com",
|
|
49
|
+
});
|
|
50
|
+
expect(token).toContain(".");
|
|
51
|
+
expect(record.userId).toBe("u-1");
|
|
52
|
+
expect(record.purpose).toBe("verify-email");
|
|
53
|
+
expect(record.consumedAt).toBeNull();
|
|
54
|
+
expect(record.tokenHash).toMatch(/^[0-9a-f]{64}$/);
|
|
55
|
+
|
|
56
|
+
const consumed = await store.consume("verify-email", token);
|
|
57
|
+
expect(consumed).not.toBeNull();
|
|
58
|
+
expect(consumed!.userId).toBe("u-1");
|
|
59
|
+
expect(consumed!.id).toBe(record.id);
|
|
60
|
+
expect(consumed!.consumedAt).toBeTypeOf("number");
|
|
61
|
+
expect(consumed!.consumedAt).not.toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("consuming twice: second call returns null (single-use)", async () => {
|
|
65
|
+
const { token } = await store.mint("verify-email", "u-2", { email: "a@b.com" });
|
|
66
|
+
const first = await store.consume("verify-email", token);
|
|
67
|
+
expect(first).not.toBeNull();
|
|
68
|
+
const second = await store.consume("verify-email", token);
|
|
69
|
+
expect(second).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("expired token returns null", async () => {
|
|
73
|
+
// Zero TTL means expires_at == now at insert. The insert completes at
|
|
74
|
+
// T, and consume() reads `now > T` → treated as expired. We sleep a
|
|
75
|
+
// few ms to be defensive against same-millisecond flakiness.
|
|
76
|
+
const shortLived = createAuthTokenStore({
|
|
77
|
+
secret: SECRET,
|
|
78
|
+
dbPath: ":memory:",
|
|
79
|
+
gcSchedule: false,
|
|
80
|
+
ttlSecondsByPurpose: { "verify-email": 0 },
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
const { token } = await shortLived.mint("verify-email", "u-3");
|
|
84
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
85
|
+
const result = await shortLived.consume("verify-email", token);
|
|
86
|
+
expect(result).toBeNull();
|
|
87
|
+
} finally {
|
|
88
|
+
await shortLived.close();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("tampered nonce returns null (hash mismatch)", async () => {
|
|
93
|
+
const { token } = await store.mint("verify-email", "u-4");
|
|
94
|
+
const [id, nonce] = token.split(".");
|
|
95
|
+
// Flip one character in the nonce. Keep length the same to exercise
|
|
96
|
+
// the byte-wise hash compare (length mismatch would short-circuit).
|
|
97
|
+
const flipped = (nonce!.charAt(0) === "A" ? "B" : "A") + nonce!.slice(1);
|
|
98
|
+
const tampered = `${id}.${flipped}`;
|
|
99
|
+
const result = await store.consume("verify-email", tampered);
|
|
100
|
+
expect(result).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("tampered id (pointing to real row) still returns null — hash mismatch", async () => {
|
|
104
|
+
const { token: a } = await store.mint("verify-email", "u-5a");
|
|
105
|
+
const { token: b } = await store.mint("verify-email", "u-5b");
|
|
106
|
+
const aNonce = a.split(".")[1]!;
|
|
107
|
+
const bId = b.split(".")[0]!;
|
|
108
|
+
// Forged token: use B's id with A's nonce. B's stored hash was
|
|
109
|
+
// computed against B's nonce, so the compare fails.
|
|
110
|
+
const forged = `${bId}.${aNonce}`;
|
|
111
|
+
const result = await store.consume("verify-email", forged);
|
|
112
|
+
expect(result).toBeNull();
|
|
113
|
+
|
|
114
|
+
// Sanity: the real B token still consumes cleanly afterwards. The
|
|
115
|
+
// forged attempt did NOT mark B as consumed.
|
|
116
|
+
const real = await store.consume("verify-email", b);
|
|
117
|
+
expect(real).not.toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("wrong purpose returns null (purpose binds to the hash)", async () => {
|
|
121
|
+
const { token } = await store.mint("verify-email", "u-6");
|
|
122
|
+
const result = await store.consume("reset-password", token);
|
|
123
|
+
expect(result).toBeNull();
|
|
124
|
+
|
|
125
|
+
// Real purpose still works — wrong-purpose attempt did NOT mark it
|
|
126
|
+
// consumed.
|
|
127
|
+
const real = await store.consume("verify-email", token);
|
|
128
|
+
expect(real).not.toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("unknown id returns null without touching other rows", async () => {
|
|
132
|
+
const { token } = await store.mint("verify-email", "u-7");
|
|
133
|
+
// Use a random UUID as the id so the row simply doesn't exist.
|
|
134
|
+
const fakeId = crypto.randomUUID();
|
|
135
|
+
const nonce = token.split(".")[1]!;
|
|
136
|
+
const bogus = `${fakeId}.${nonce}`;
|
|
137
|
+
const result = await store.consume("verify-email", bogus);
|
|
138
|
+
expect(result).toBeNull();
|
|
139
|
+
|
|
140
|
+
// Real token still usable.
|
|
141
|
+
const real = await store.consume("verify-email", token);
|
|
142
|
+
expect(real).not.toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("gcNow() sweeps expired + consumed rows and returns the count", async () => {
|
|
146
|
+
// Consume one row to create a "consumed" candidate.
|
|
147
|
+
const { token: consumedToken } = await store.mint("verify-email", "u-8a");
|
|
148
|
+
await store.consume("verify-email", consumedToken);
|
|
149
|
+
|
|
150
|
+
// Build a short-lived entry via a second store on the same (in-memory)
|
|
151
|
+
// semantics — we can't share a :memory: DB across handles, so mint
|
|
152
|
+
// into a different short-TTL store to prove the delete query works.
|
|
153
|
+
const expiring = createAuthTokenStore({
|
|
154
|
+
secret: SECRET,
|
|
155
|
+
dbPath: ":memory:",
|
|
156
|
+
gcSchedule: false,
|
|
157
|
+
ttlSecondsByPurpose: { "verify-email": 0 },
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
await expiring.mint("verify-email", "u-8b");
|
|
161
|
+
await expiring.mint("verify-email", "u-8c");
|
|
162
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
163
|
+
const deleted = await expiring.gcNow();
|
|
164
|
+
expect(deleted).toBe(2);
|
|
165
|
+
// Second sweep is a no-op.
|
|
166
|
+
expect(await expiring.gcNow()).toBe(0);
|
|
167
|
+
} finally {
|
|
168
|
+
await expiring.close();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The original store has exactly one row (consumed); gcNow removes it.
|
|
172
|
+
const cleaned = await store.gcNow();
|
|
173
|
+
expect(cleaned).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("meta round-trips as JSON for verify-email tokens", async () => {
|
|
177
|
+
const { token, record: minted } = await store.mint("verify-email", "u-9", {
|
|
178
|
+
email: "alice@example.com",
|
|
179
|
+
signupSource: "marketing-page",
|
|
180
|
+
});
|
|
181
|
+
expect(minted.meta?.email).toBe("alice@example.com");
|
|
182
|
+
|
|
183
|
+
const consumed = await store.consume("verify-email", token);
|
|
184
|
+
expect(consumed).not.toBeNull();
|
|
185
|
+
expect(consumed!.meta?.email).toBe("alice@example.com");
|
|
186
|
+
expect(consumed!.meta?.signupSource).toBe("marketing-page");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("sequential consumes serialise through the UPDATE predicate — only the first wins", async () => {
|
|
190
|
+
// True wall-clock concurrency is not expressible against a single
|
|
191
|
+
// SQLite connection (Bun.SQL surfaces "cannot start a transaction
|
|
192
|
+
// within a transaction" when two callers share the handle). The
|
|
193
|
+
// atomicity guarantee we actually care about is observable
|
|
194
|
+
// sequentially: consume #2 must see `consumed_at IS NOT NULL` from
|
|
195
|
+
// consume #1's committed transaction and return null.
|
|
196
|
+
//
|
|
197
|
+
// The defense-in-depth `UPDATE … WHERE consumed_at IS NULL` predicate
|
|
198
|
+
// is what guards against a hypothetical interleaving if multiple
|
|
199
|
+
// writer connections ever land in the future (e.g., Postgres).
|
|
200
|
+
const { token } = await store.mint("verify-email", "u-10");
|
|
201
|
+
const a = await store.consume("verify-email", token);
|
|
202
|
+
const b = await store.consume("verify-email", token);
|
|
203
|
+
const successes: TokenRecord[] = [];
|
|
204
|
+
if (a) successes.push(a);
|
|
205
|
+
if (b) successes.push(b);
|
|
206
|
+
expect(successes).toHaveLength(1);
|
|
207
|
+
expect(a!.consumedAt).not.toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("malformed token (no dot) returns null without throwing", async () => {
|
|
211
|
+
// Exercise the `parseToken` no-dot path, the empty-string path, and
|
|
212
|
+
// the multi-dot path — all must collapse to null.
|
|
213
|
+
const nodot = await store.consume("verify-email", "not-a-token");
|
|
214
|
+
expect(nodot).toBeNull();
|
|
215
|
+
|
|
216
|
+
const empty = await store.consume("verify-email", "");
|
|
217
|
+
expect(empty).toBeNull();
|
|
218
|
+
|
|
219
|
+
const multiDot = await store.consume("verify-email", "a.b.c");
|
|
220
|
+
expect(multiDot).toBeNull();
|
|
221
|
+
|
|
222
|
+
const leadingDot = await store.consume("verify-email", ".nonce");
|
|
223
|
+
expect(leadingDot).toBeNull();
|
|
224
|
+
|
|
225
|
+
const trailingDot = await store.consume("verify-email", "id.");
|
|
226
|
+
expect(trailingDot).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("close() then mint/consume/gcNow rejects with a clear error", async () => {
|
|
230
|
+
const tempStore = createAuthTokenStore({
|
|
231
|
+
secret: SECRET,
|
|
232
|
+
dbPath: ":memory:",
|
|
233
|
+
gcSchedule: false,
|
|
234
|
+
});
|
|
235
|
+
// Populate one row so gcNow has something to scan.
|
|
236
|
+
await tempStore.mint("verify-email", "u-11");
|
|
237
|
+
await tempStore.close();
|
|
238
|
+
|
|
239
|
+
await expect(tempStore.mint("verify-email", "u-12")).rejects.toThrow(
|
|
240
|
+
/store is closed/,
|
|
241
|
+
);
|
|
242
|
+
await expect(tempStore.consume("verify-email", "x.y")).rejects.toThrow(
|
|
243
|
+
/store is closed/,
|
|
244
|
+
);
|
|
245
|
+
await expect(tempStore.gcNow()).rejects.toThrow(/store is closed/);
|
|
246
|
+
// close() is idempotent.
|
|
247
|
+
await expect(tempStore.close()).resolves.toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("empty secret is rejected synchronously at construction", () => {
|
|
251
|
+
expect(() =>
|
|
252
|
+
createAuthTokenStore({ secret: "", dbPath: ":memory:", gcSchedule: false }),
|
|
253
|
+
).toThrow(/secret/);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("invalid table name is rejected synchronously at construction", () => {
|
|
257
|
+
expect(() =>
|
|
258
|
+
createAuthTokenStore({
|
|
259
|
+
secret: SECRET,
|
|
260
|
+
dbPath: ":memory:",
|
|
261
|
+
table: "bad-name; DROP TABLE users",
|
|
262
|
+
gcSchedule: false,
|
|
263
|
+
}),
|
|
264
|
+
).toThrow(/table name/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("reset-password token consumed at the wrong purpose returns null", async () => {
|
|
268
|
+
const { token } = await store.mint("reset-password", "u-13");
|
|
269
|
+
const wrongPurpose = await store.consume("verify-email", token);
|
|
270
|
+
expect(wrongPurpose).toBeNull();
|
|
271
|
+
const rightPurpose = await store.consume("reset-password", token);
|
|
272
|
+
expect(rightPurpose).not.toBeNull();
|
|
273
|
+
});
|
|
274
|
+
});
|