okengine 0.21.0 → 0.22.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 +4 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +10 -0
- package/package.json +22 -25
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +28 -15
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/dev.test.ts +105 -2
- package/src/cli/dev.ts +37 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/index.ts +9 -1
- package/src/cli/load-config.images.test.ts +10 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/create.ts +31 -27
- package/src/client/live.ts +44 -101
- package/src/client/sse.ts +20 -66
- package/src/client/stream.ts +25 -67
- package/src/client/transport.test.ts +225 -0
- package/src/client/transport.ts +158 -90
- package/src/client/types.ts +29 -3
- package/src/client/wire.ts +119 -0
- package/src/compiler/aot.ts +3 -32
- package/src/compiler/dynamic.ts +13 -11
- package/src/compiler/effects-infer.ts +726 -16
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +113 -12
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/interpret.ts +45 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.js → access-page-Ze6ckjdd.js} +2 -2
- package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-BCC-DxKT.js → cache-glyph-rL0lHxat.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-Deo4PP18.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.js → collapsible-CoJ6amHf.js} +1 -1
- package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
- package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
- package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
- package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-gT1lsWQK.js → flows-page-D2E7BtKK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-CPYBzDh0.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.js → http-method-DTPevKmM.js} +1 -1
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
- package/src/console/ui-next/dist/assets/{replica-lag-C4QdAF7J.js → replica-lag-BM27lXld.js} +3 -3
- package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.js → request-meta-BjFT7DNl.js} +1 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Dcpo4Us_.js → trace-detail-sheet-Cmitq3da.js} +2 -2
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-BwZ9YjTW.js → vault-page-BOMr0go9.js} +2 -2
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/journal-postgres.ts +168 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.ts +20 -4
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +4 -0
- package/src/i18n/catalogs/en.ts +4 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/app.ts +200 -20
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +19 -1
- package/src/kernel/builtin-errors.ts +8 -0
- package/src/kernel/client-descriptor.test.ts +78 -0
- package/src/kernel/client-descriptor.ts +24 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +99 -0
- package/src/kernel/errors.ts +90 -138
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx-sql-handle.ts +305 -0
- package/src/kernel/fx.ts +42 -330
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +56 -0
- package/src/kernel/json-result.ts +59 -0
- package/src/kernel/project-out.ts +6 -1
- package/src/manifest/types.ts +8 -0
- package/src/okid-extended.ts +175 -0
- package/src/okid-shared.ts +103 -0
- package/src/okid.ts +30 -213
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/build-lib.ts +9 -0
- package/src/release/http-graph.test.ts +25 -0
- package/src/release/http-graph.ts +90 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +105 -14
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-CSKumwS2.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-CwoV56jn.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/index-Cul17AcV.js +0 -63
- package/src/console/ui-next/dist/assets/observability-page-oY9vdYBk.js +0 -4
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-CL0D9dOq.js +0 -41
- package/src/console/ui-next/dist/assets/units-page-1PlT19ft.js +0 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKID constants and the options-path encoder.
|
|
3
|
+
*
|
|
4
|
+
* The default `okid()` / `okid(length)` hot path does not import the
|
|
5
|
+
* alphabet resolver — only these constants and {@link assertLength}.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default `okid()` / `okid(length)` alphabet.
|
|
10
|
+
*
|
|
11
|
+
* Same 64-character set and order as
|
|
12
|
+
* `resolveAlphabet(true, true, true, true, true)` (numbers, lowercase,
|
|
13
|
+
* uppercase, symbols). Power-of-two size, so a bitmask is unbiased.
|
|
14
|
+
*/
|
|
15
|
+
export const OKID_DEFAULT_CHARS =
|
|
16
|
+
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Characters removed when `lookAlikes` is disabled (human transcription).
|
|
20
|
+
*/
|
|
21
|
+
export const OKID_LOOKALIKE_CHARS = "1lI0Oouv5Ss";
|
|
22
|
+
|
|
23
|
+
/** Default URL-safe alphabet: Base64URL charset in its conventional order. */
|
|
24
|
+
export const OKID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Codepoint-ordered variant of {@link OKID_ALPHABET}: identical characters,
|
|
28
|
+
* sorted by code unit so plain string comparison equals value comparison.
|
|
29
|
+
* The default Base64URL order is NOT lexicographic (`_` sorts between `Z`
|
|
30
|
+
* and `a`), which would silently break time ordering — so the sortable
|
|
31
|
+
* encoder always uses this order.
|
|
32
|
+
*
|
|
33
|
+
* Kept as a literal (not `[...OKID_ALPHABET].sort().join("")`) so the public
|
|
34
|
+
* JSR API stays fast-type / explicitly typed.
|
|
35
|
+
*/
|
|
36
|
+
export const OKID_SORTABLE_ALPHABET: string =
|
|
37
|
+
"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
|
|
38
|
+
|
|
39
|
+
/** Default id length — 21 chars × 6 bits = 126 bits of entropy. */
|
|
40
|
+
export const OKID_DEFAULT_LENGTH = 21;
|
|
41
|
+
|
|
42
|
+
/** Shortest permitted id length (8 chars ≈ 48 bits entropy floor). */
|
|
43
|
+
export const OKID_MIN_LENGTH = 8;
|
|
44
|
+
|
|
45
|
+
/** Longest permitted id length. */
|
|
46
|
+
export const OKID_MAX_LENGTH = 128;
|
|
47
|
+
|
|
48
|
+
/** Sortable ids need ≥ 16: 8 timestamp chars alone would leave no randomness. */
|
|
49
|
+
export const OKID_SORTABLE_MIN_LENGTH = 16;
|
|
50
|
+
|
|
51
|
+
/** Longest permitted semantic {@link OkidOptions.prefix}. */
|
|
52
|
+
export const OKID_MAX_PREFIX_LENGTH = 32;
|
|
53
|
+
|
|
54
|
+
/** Options for {@link okid}. All alphabet toggles default to included. */
|
|
55
|
+
export interface OkidOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Generated body length (default {@link OKID_DEFAULT_LENGTH}). Does not
|
|
58
|
+
* include {@link prefix} — the returned string is `prefix + body`.
|
|
59
|
+
*/
|
|
60
|
+
readonly length?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Fixed semantic label prepended to the body (e.g. `"usr_"`, `"evt_"`).
|
|
63
|
+
* Characters must belong to {@link OKID_ALPHABET}; max
|
|
64
|
+
* {@link OKID_MAX_PREFIX_LENGTH}. Empty / omitted means no label.
|
|
65
|
+
*/
|
|
66
|
+
readonly prefix?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Prefix the body with a 48-bit epoch-ms timestamp (exactly 8 chars) so
|
|
69
|
+
* lexicographic order tracks creation time across milliseconds. Alphabet
|
|
70
|
+
* toggles are ignored under this mode — see {@link OKID_SORTABLE_ALPHABET}.
|
|
71
|
+
* Combines with {@link prefix}: `prefix + timestamp + random`.
|
|
72
|
+
*/
|
|
73
|
+
readonly sortable?: boolean;
|
|
74
|
+
/** Include `a-z` (default true). */
|
|
75
|
+
readonly lowercase?: boolean;
|
|
76
|
+
/** Include `A-Z` (default true). */
|
|
77
|
+
readonly uppercase?: boolean;
|
|
78
|
+
/** Include `0-9` (default true). */
|
|
79
|
+
readonly numbers?: boolean;
|
|
80
|
+
/** Include `-` and `_` (default true). */
|
|
81
|
+
readonly symbols?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Include visually confusable characters (`1lI0Oouv5Ss`, default true).
|
|
84
|
+
* Set `false` to drop them for human transcription.
|
|
85
|
+
*/
|
|
86
|
+
readonly lookAlikes?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Assert `length` is a valid integer within bounds for the requested mode.
|
|
91
|
+
*
|
|
92
|
+
* @param length - Requested length
|
|
93
|
+
* @param min - Mode-specific minimum
|
|
94
|
+
* @param label - Option name used in the error message
|
|
95
|
+
*/
|
|
96
|
+
export function assertLength(length: number, min: number, label: string): void {
|
|
97
|
+
if (!Number.isInteger(length)) {
|
|
98
|
+
throw new RangeError(`okid: ${label} must be an integer, got ${length}`);
|
|
99
|
+
}
|
|
100
|
+
if (length < min || length > OKID_MAX_LENGTH) {
|
|
101
|
+
throw new RangeError(`okid: ${label} ${length} is out of range [${min}, ${OKID_MAX_LENGTH}]`);
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/okid.ts
CHANGED
|
@@ -52,193 +52,33 @@
|
|
|
52
52
|
* @module
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
import { lazyRequire } from "./kernel/lazy-require.ts";
|
|
56
|
+
import {
|
|
57
|
+
assertLength,
|
|
58
|
+
OKID_DEFAULT_CHARS,
|
|
59
|
+
OKID_DEFAULT_LENGTH,
|
|
60
|
+
OKID_MIN_LENGTH,
|
|
61
|
+
type OkidOptions,
|
|
62
|
+
} from "./okid-shared.ts";
|
|
63
|
+
|
|
64
|
+
export type { OkidOptions } from "./okid-shared.ts";
|
|
65
|
+
export {
|
|
66
|
+
OKID_ALPHABET,
|
|
67
|
+
OKID_DEFAULT_LENGTH,
|
|
68
|
+
OKID_LOOKALIKE_CHARS,
|
|
69
|
+
OKID_MAX_LENGTH,
|
|
70
|
+
OKID_MAX_PREFIX_LENGTH,
|
|
71
|
+
OKID_MIN_LENGTH,
|
|
72
|
+
OKID_SORTABLE_ALPHABET,
|
|
73
|
+
OKID_SORTABLE_MIN_LENGTH,
|
|
74
|
+
} from "./okid-shared.ts";
|
|
62
75
|
|
|
63
76
|
/**
|
|
64
|
-
*
|
|
77
|
+
* Load sortable / prefix / alphabet-toggle encoding.
|
|
78
|
+
* Computed stem so the edge profile does not inline it.
|
|
65
79
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/** Default URL-safe alphabet: Base64URL charset in its conventional order. */
|
|
69
|
-
export const OKID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Codepoint-ordered variant of {@link OKID_ALPHABET}: identical characters,
|
|
73
|
-
* sorted by code unit so plain string comparison equals value comparison.
|
|
74
|
-
* The default Base64URL order is NOT lexicographic (`_` sorts between `Z`
|
|
75
|
-
* and `a`), which would silently break time ordering — so the sortable
|
|
76
|
-
* encoder always uses this order.
|
|
77
|
-
*
|
|
78
|
-
* Kept as a literal (not `[...OKID_ALPHABET].sort().join("")`) so the public
|
|
79
|
-
* JSR API stays fast-type / explicitly typed.
|
|
80
|
-
*/
|
|
81
|
-
export const OKID_SORTABLE_ALPHABET: string =
|
|
82
|
-
"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
|
|
83
|
-
|
|
84
|
-
/** Default id length — 21 chars × 6 bits = 126 bits of entropy. */
|
|
85
|
-
export const OKID_DEFAULT_LENGTH = 21;
|
|
86
|
-
|
|
87
|
-
/** Shortest permitted id length (8 chars ≈ 48 bits entropy floor). */
|
|
88
|
-
export const OKID_MIN_LENGTH = 8;
|
|
89
|
-
|
|
90
|
-
/** Longest permitted id length. */
|
|
91
|
-
export const OKID_MAX_LENGTH = 128;
|
|
92
|
-
|
|
93
|
-
/** Sortable ids need ≥ 16: 8 timestamp chars alone would leave no randomness. */
|
|
94
|
-
export const OKID_SORTABLE_MIN_LENGTH = 16;
|
|
95
|
-
|
|
96
|
-
/** Longest permitted semantic {@link OkidOptions.prefix}. */
|
|
97
|
-
export const OKID_MAX_PREFIX_LENGTH = 32;
|
|
98
|
-
|
|
99
|
-
/** Options for {@link okid}. All alphabet toggles default to included. */
|
|
100
|
-
export interface OkidOptions {
|
|
101
|
-
/**
|
|
102
|
-
* Generated body length (default {@link OKID_DEFAULT_LENGTH}). Does not
|
|
103
|
-
* include {@link prefix} — the returned string is `prefix + body`.
|
|
104
|
-
*/
|
|
105
|
-
readonly length?: number;
|
|
106
|
-
/**
|
|
107
|
-
* Fixed semantic label prepended to the body (e.g. `"usr_"`, `"evt_"`).
|
|
108
|
-
* Characters must belong to {@link OKID_ALPHABET}; max
|
|
109
|
-
* {@link OKID_MAX_PREFIX_LENGTH}. Empty / omitted means no label.
|
|
110
|
-
*/
|
|
111
|
-
readonly prefix?: string;
|
|
112
|
-
/**
|
|
113
|
-
* Prefix the body with a 48-bit epoch-ms timestamp (exactly 8 chars) so
|
|
114
|
-
* lexicographic order tracks creation time across milliseconds. Alphabet
|
|
115
|
-
* toggles are ignored under this mode — see {@link OKID_SORTABLE_ALPHABET}.
|
|
116
|
-
* Combines with {@link prefix}: `prefix + timestamp + random`.
|
|
117
|
-
*/
|
|
118
|
-
readonly sortable?: boolean;
|
|
119
|
-
/** Include `a-z` (default true). */
|
|
120
|
-
readonly lowercase?: boolean;
|
|
121
|
-
/** Include `A-Z` (default true). */
|
|
122
|
-
readonly uppercase?: boolean;
|
|
123
|
-
/** Include `0-9` (default true). */
|
|
124
|
-
readonly numbers?: boolean;
|
|
125
|
-
/** Include `-` and `_` (default true). */
|
|
126
|
-
readonly symbols?: boolean;
|
|
127
|
-
/**
|
|
128
|
-
* Include visually confusable characters (`1lI0Oouv5Ss`, default true).
|
|
129
|
-
* Set `false` to drop them for human transcription.
|
|
130
|
-
*/
|
|
131
|
-
readonly lookAlikes?: boolean;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Resolved alphabet + encoding metadata for one options combination. */
|
|
135
|
-
interface ResolvedAlphabet {
|
|
136
|
-
readonly chars: string;
|
|
137
|
-
readonly size: number;
|
|
138
|
-
/** Bitmask covering `size` values (`size` is always a power of two here). */
|
|
139
|
-
readonly mask: number;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Memoized resolutions keyed by the toggle bitmask (32 combinations max). */
|
|
143
|
-
const ALPHABET_CACHE = new Map<number, ResolvedAlphabet>();
|
|
144
|
-
|
|
145
|
-
function resolveAlphabet(
|
|
146
|
-
numbers: boolean,
|
|
147
|
-
lowercase: boolean,
|
|
148
|
-
uppercase: boolean,
|
|
149
|
-
symbols: boolean,
|
|
150
|
-
lookAlikes: boolean,
|
|
151
|
-
): ResolvedAlphabet {
|
|
152
|
-
const key =
|
|
153
|
-
(numbers ? 1 : 0) |
|
|
154
|
-
(lowercase ? 2 : 0) |
|
|
155
|
-
(uppercase ? 4 : 0) |
|
|
156
|
-
(symbols ? 8 : 0) |
|
|
157
|
-
(lookAlikes ? 0 : 16);
|
|
158
|
-
const cached = ALPHABET_CACHE.get(key);
|
|
159
|
-
if (cached) return cached;
|
|
160
|
-
|
|
161
|
-
let chars = "";
|
|
162
|
-
if (numbers) chars += GROUPS.numbers;
|
|
163
|
-
if (lowercase) chars += GROUPS.lowercase;
|
|
164
|
-
if (uppercase) chars += GROUPS.uppercase;
|
|
165
|
-
if (symbols) chars += GROUPS.symbols;
|
|
166
|
-
if (!chars) {
|
|
167
|
-
throw new RangeError("okid: alphabet is empty — enable at least one character group");
|
|
168
|
-
}
|
|
169
|
-
if (!lookAlikes) {
|
|
170
|
-
chars = [...chars].filter((c) => !OKID_LOOKALIKE_CHARS.includes(c)).join("");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Round up to a power of two for mask-based rejection sampling: bytes below
|
|
174
|
-
// `size` map uniformly, bytes above are discarded and re-drawn — unbiased at
|
|
175
|
-
// every alphabet size, unlike naive modulo.
|
|
176
|
-
const rawSize = chars.length;
|
|
177
|
-
const size = 1 << Math.ceil(Math.log2(rawSize));
|
|
178
|
-
const resolved: ResolvedAlphabet = { chars, size: rawSize, mask: size - 1 };
|
|
179
|
-
ALPHABET_CACHE.set(key, resolved);
|
|
180
|
-
return resolved;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Encode one random byte stream into `length` characters of `alphabet`. */
|
|
184
|
-
function encode(alphabet: ResolvedAlphabet, length: number): string {
|
|
185
|
-
const { chars, size, mask } = alphabet;
|
|
186
|
-
const bytes = new Uint8Array(length + Math.ceil(length >> 2));
|
|
187
|
-
crypto.getRandomValues(bytes.subarray(0, length));
|
|
188
|
-
let out = "";
|
|
189
|
-
let i = 0;
|
|
190
|
-
while (out.length < length && i < bytes.length) {
|
|
191
|
-
const byte = bytes[i++]!;
|
|
192
|
-
if ((byte & mask) < size) out += chars[byte & mask];
|
|
193
|
-
}
|
|
194
|
-
return out;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Pack epoch-ms into exactly 8 codepoint-ordered characters (48 bits). */
|
|
198
|
-
function encodeTimestamp(nowMs: number): string {
|
|
199
|
-
let t = nowMs % 2 ** 48;
|
|
200
|
-
let out = "";
|
|
201
|
-
for (let i = 0; i < 8; i++) {
|
|
202
|
-
out = OKID_SORTABLE_ALPHABET[t & 63]! + out;
|
|
203
|
-
t = Math.floor(t / 64);
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Assert `length` is a valid integer within bounds for the requested mode.
|
|
210
|
-
*
|
|
211
|
-
* @param length - Requested length
|
|
212
|
-
* @param min - Mode-specific minimum
|
|
213
|
-
* @param label - Option name used in the error message
|
|
214
|
-
*/
|
|
215
|
-
function assertLength(length: number, min: number, label: string): void {
|
|
216
|
-
if (!Number.isInteger(length)) {
|
|
217
|
-
throw new RangeError(`okid: ${label} must be an integer, got ${length}`);
|
|
218
|
-
}
|
|
219
|
-
if (length < min || length > OKID_MAX_LENGTH) {
|
|
220
|
-
throw new RangeError(`okid: ${label} ${length} is out of range [${min}, ${OKID_MAX_LENGTH}]`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Assert a semantic prefix is within bounds and stays on the URL-safe alphabet.
|
|
226
|
-
*
|
|
227
|
-
* @param prefix - Caller-supplied label
|
|
228
|
-
*/
|
|
229
|
-
function assertPrefix(prefix: string): void {
|
|
230
|
-
if (prefix.length > OKID_MAX_PREFIX_LENGTH) {
|
|
231
|
-
throw new RangeError(
|
|
232
|
-
`okid: prefix length ${prefix.length} exceeds max ${OKID_MAX_PREFIX_LENGTH}`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
for (const char of prefix) {
|
|
236
|
-
if (!OKID_ALPHABET.includes(char)) {
|
|
237
|
-
throw new RangeError(
|
|
238
|
-
`okid: prefix contains invalid character ${JSON.stringify(char)} — use characters from OKID_ALPHABET`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
80
|
+
function loadOkidExtended(): typeof import("./okid-extended.ts") {
|
|
81
|
+
return lazyRequire(import.meta.dir, ["okid", "extended"].join("-"));
|
|
242
82
|
}
|
|
243
83
|
|
|
244
84
|
/**
|
|
@@ -256,34 +96,11 @@ function assertPrefix(prefix: string): void {
|
|
|
256
96
|
export function okid(options: number | OkidOptions = OKID_DEFAULT_LENGTH): string {
|
|
257
97
|
if (typeof options === "number") {
|
|
258
98
|
assertLength(options, OKID_MIN_LENGTH, "length");
|
|
259
|
-
|
|
99
|
+
const bytes = new Uint8Array(options);
|
|
100
|
+
crypto.getRandomValues(bytes);
|
|
101
|
+
let out = "";
|
|
102
|
+
for (let i = 0; i < options; i++) out += OKID_DEFAULT_CHARS[bytes[i]! & 63]!;
|
|
103
|
+
return out;
|
|
260
104
|
}
|
|
261
|
-
|
|
262
|
-
const {
|
|
263
|
-
length = OKID_DEFAULT_LENGTH,
|
|
264
|
-
prefix = "",
|
|
265
|
-
sortable = false,
|
|
266
|
-
lowercase = true,
|
|
267
|
-
uppercase = true,
|
|
268
|
-
numbers = true,
|
|
269
|
-
symbols = true,
|
|
270
|
-
lookAlikes = true,
|
|
271
|
-
} = options;
|
|
272
|
-
|
|
273
|
-
if (prefix) assertPrefix(prefix);
|
|
274
|
-
|
|
275
|
-
let body: string;
|
|
276
|
-
if (sortable) {
|
|
277
|
-
assertLength(length, OKID_SORTABLE_MIN_LENGTH, "length");
|
|
278
|
-
// Time ordering requires lexicographic encoding, which requires the full
|
|
279
|
-
// codepoint-ordered alphabet — partial subsets cannot preserve both the
|
|
280
|
-
// caller's charset choice AND cross-ms ordering, so toggles are ignored.
|
|
281
|
-
const alphabet = resolveAlphabet(true, true, true, true, true);
|
|
282
|
-
body = encodeTimestamp(Date.now()) + encode(alphabet, length - 8);
|
|
283
|
-
} else {
|
|
284
|
-
assertLength(length, OKID_MIN_LENGTH, "length");
|
|
285
|
-
body = encode(resolveAlphabet(numbers, lowercase, uppercase, symbols, lookAlikes), length);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return prefix ? prefix + body : body;
|
|
105
|
+
return loadOkidExtended().okidWithOptions(options);
|
|
289
106
|
}
|
|
@@ -18,7 +18,7 @@ import { otp } from "./otp.ts";
|
|
|
18
18
|
import { magicLink } from "./magic-link.ts";
|
|
19
19
|
|
|
20
20
|
const SECRET = "test-secret-at-least-16";
|
|
21
|
-
const MAILPIT_IMAGE = "axllent/mailpit:v1.31.
|
|
21
|
+
const MAILPIT_IMAGE = "axllent/mailpit:v1.31.2";
|
|
22
22
|
|
|
23
23
|
afterEach(() => {
|
|
24
24
|
resetBindings();
|
package/src/plugins/otp.test.ts
CHANGED
|
@@ -117,7 +117,71 @@ describe("otp() provider mode boot", () => {
|
|
|
117
117
|
const body = (await res.json()) as { data: { ok: true; devOtp?: string } };
|
|
118
118
|
expect(body.data.ok).toBe(true);
|
|
119
119
|
expect(body.data.devOtp).toBeUndefined();
|
|
120
|
-
expect(app.router.match("POST", "/auth/otp/resend")).
|
|
120
|
+
expect(app.router.match("POST", "/auth/otp/resend")).toBeTruthy();
|
|
121
|
+
await app.stop();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("SMS resend issues a new request id, honors cooldown, and rejects another channel", async () => {
|
|
125
|
+
let now = 1_000_000;
|
|
126
|
+
const requestIds: string[] = [];
|
|
127
|
+
const verifications = createVerificationStore();
|
|
128
|
+
const driver = mockOtpSmsDriver();
|
|
129
|
+
const transport = driver.smsTransport as unknown as SmsOtpTransport;
|
|
130
|
+
const original = transport.sendOtp.bind(transport);
|
|
131
|
+
transport.sendOtp = async (opts) => {
|
|
132
|
+
requestIds.push(opts.requestId);
|
|
133
|
+
return original(opts);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const app = oke({
|
|
137
|
+
name: `otp-provider-resend-${crypto.randomUUID()}`,
|
|
138
|
+
env: "test",
|
|
139
|
+
registry: "ignore",
|
|
140
|
+
gate: { auth: { secret: SECRET } },
|
|
141
|
+
channel: { drivers: [driver] },
|
|
142
|
+
fx: { now: () => now },
|
|
143
|
+
}).plug(
|
|
144
|
+
otp({
|
|
145
|
+
mode: "provider",
|
|
146
|
+
resendCooldownMs: 60_000,
|
|
147
|
+
verifications,
|
|
148
|
+
now: () => now,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
await app.boot({ env: "test" });
|
|
152
|
+
|
|
153
|
+
const phone = "+15551234567";
|
|
154
|
+
expect((await app.fetch(jsonPost("/auth/otp/request", { phone }))).status).toBe(200);
|
|
155
|
+
const first = findActiveVerification(verifications, `otp:${phone}`, now);
|
|
156
|
+
expect(first?.value.startsWith("provider:")).toBe(true);
|
|
157
|
+
expect(first?.sealedOtp).toBeNull();
|
|
158
|
+
const firstId = first?.value.slice("provider:".length);
|
|
159
|
+
|
|
160
|
+
const cooled = await app.fetch(jsonPost("/auth/otp/resend", { phone }));
|
|
161
|
+
const cooledBody = (await cooled.json()) as { error?: { data?: { reason?: string } } };
|
|
162
|
+
expect(cooledBody.error?.data?.reason).toBe("resend_cooldown");
|
|
163
|
+
|
|
164
|
+
const other = await app.fetch(jsonPost("/auth/otp/resend", { phone, channel: "email" }));
|
|
165
|
+
const otherBody = (await other.json()) as { error?: { data?: { reason?: string } } };
|
|
166
|
+
expect(otherBody.error?.data?.reason).toBe("invalid_channel");
|
|
167
|
+
|
|
168
|
+
now += 60_001;
|
|
169
|
+
const resent = await app.fetch(jsonPost("/auth/otp/resend", { phone, channel: "sms" }));
|
|
170
|
+
expect(resent.status).toBe(200);
|
|
171
|
+
const resentBody = (await resent.json()) as { data: { channel: string; devOtp?: string } };
|
|
172
|
+
expect(resentBody.data.channel).toBe("sms");
|
|
173
|
+
expect(resentBody.data.devOtp).toBeUndefined();
|
|
174
|
+
|
|
175
|
+
const second = findActiveVerification(verifications, `otp:${phone}`, now);
|
|
176
|
+
const secondId = second?.value.slice("provider:".length);
|
|
177
|
+
expect(secondId).toBeDefined();
|
|
178
|
+
expect(secondId).not.toBe(firstId);
|
|
179
|
+
if (firstId === undefined || secondId === undefined) {
|
|
180
|
+
throw new Error("expected provider request ids");
|
|
181
|
+
}
|
|
182
|
+
expect(requestIds).toEqual([firstId, secondId]);
|
|
183
|
+
expect(findActiveVerification(verifications, `otp:${phone}`, now)?.sealedOtp).toBeNull();
|
|
184
|
+
|
|
121
185
|
await app.stop();
|
|
122
186
|
});
|
|
123
187
|
});
|
package/src/plugins/otp.ts
CHANGED
|
@@ -77,7 +77,8 @@ export interface OtpProviderModeOptions extends OtpBaseOptions {
|
|
|
77
77
|
readonly channels?: never;
|
|
78
78
|
/** Forbidden in provider mode — code never exists server-side. */
|
|
79
79
|
readonly exposeDevOtp?: never;
|
|
80
|
-
|
|
80
|
+
/** Spacing between provider SMS resends. Default 60s. */
|
|
81
|
+
readonly resendCooldownMs?: number;
|
|
81
82
|
readonly from?: never;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -216,10 +217,7 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
216
217
|
const identities = resolveSharedIdentities(opts);
|
|
217
218
|
const verifications = opts.verifications ?? createVerificationStore();
|
|
218
219
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
219
|
-
const resendCooldownMs =
|
|
220
|
-
opts.mode === "app"
|
|
221
|
-
? (opts.resendCooldownMs ?? DEFAULT_RESEND_COOLDOWN_MS)
|
|
222
|
-
: DEFAULT_RESEND_COOLDOWN_MS;
|
|
220
|
+
const resendCooldownMs = opts.resendCooldownMs ?? DEFAULT_RESEND_COOLDOWN_MS;
|
|
223
221
|
const channels = opts.mode === "app" ? opts.channels : ([] as const);
|
|
224
222
|
const exposeDevOtp = opts.mode === "app" && opts.exposeDevOtp === true;
|
|
225
223
|
const from = opts.mode === "app" ? opts.from : undefined;
|
|
@@ -288,8 +286,61 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
288
286
|
attempts: 0,
|
|
289
287
|
phone,
|
|
290
288
|
sealedOtp: null,
|
|
289
|
+
lastDeliveredAt: now,
|
|
290
|
+
lastChannel: "sms",
|
|
291
|
+
});
|
|
292
|
+
return { ok: true as const, channel: "sms" as const };
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const resendContract = {
|
|
297
|
+
in: z.object({
|
|
298
|
+
phone: z.string().min(8),
|
|
299
|
+
lang: z.enum(["en", "ar"]).optional(),
|
|
300
|
+
channel: z.enum(["sms", "whatsapp", "email"]).optional(),
|
|
301
|
+
}),
|
|
302
|
+
out: requestOut,
|
|
303
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const resend = flow("auth.resendOtp", {
|
|
307
|
+
plane: "user",
|
|
308
|
+
effects: { sends: ["sms-otp"] },
|
|
309
|
+
do: async (input, fx) => {
|
|
310
|
+
if (input.channel !== undefined && input.channel !== "sms") {
|
|
311
|
+
return fail("AuthFailed", { reason: "invalid_channel" });
|
|
312
|
+
}
|
|
313
|
+
const phone = input.phone.trim();
|
|
314
|
+
if (!E164.test(phone)) return fail("AuthFailed", { reason: "invalid_phone" });
|
|
315
|
+
const now = runtime.now();
|
|
316
|
+
const row = findActiveVerification(verifications, `otp:${phone}`, now);
|
|
317
|
+
if (!row || row.sealedOtp) {
|
|
318
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
319
|
+
}
|
|
320
|
+
if (row.lastDeliveredAt !== undefined && now - row.lastDeliveredAt < resendCooldownMs) {
|
|
321
|
+
return fail("AuthFailed", { reason: "resend_cooldown" });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const requestId = okid();
|
|
325
|
+
await fx.sendOtp({
|
|
326
|
+
to: phone,
|
|
327
|
+
requestId,
|
|
328
|
+
...(input.lang ? { lang: input.lang } : {}),
|
|
329
|
+
});
|
|
330
|
+
invalidateVerifications(verifications, `otp:${phone}`, now);
|
|
331
|
+
putVerification(verifications, {
|
|
332
|
+
id: okid(),
|
|
333
|
+
identifier: `otp:${phone}`,
|
|
334
|
+
value: `provider:${requestId}`,
|
|
335
|
+
expiresAt: now + ttlMs,
|
|
336
|
+
createdAt: now,
|
|
337
|
+
consumedAt: null,
|
|
338
|
+
attempts: 0,
|
|
339
|
+
phone,
|
|
340
|
+
sealedOtp: null,
|
|
341
|
+
lastDeliveredAt: now,
|
|
342
|
+
lastChannel: "sms",
|
|
291
343
|
});
|
|
292
|
-
// Provider mode: resend-via-different-channel is impossible — provider owns the code.
|
|
293
344
|
return { ok: true as const, channel: "sms" as const };
|
|
294
345
|
},
|
|
295
346
|
});
|
|
@@ -373,7 +424,8 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
373
424
|
return plugin("otp", { version: "0.0.1", config: configSnapshot })
|
|
374
425
|
.needs("auth")
|
|
375
426
|
.binding(bindPublicAuth("/otp/request", request, "otp", requestContract))
|
|
376
|
-
.binding(bindPublicAuth("/otp/verify", verify, "otp", verifyContract))
|
|
427
|
+
.binding(bindPublicAuth("/otp/verify", verify, "otp", verifyContract))
|
|
428
|
+
.binding(bindPublicAuth("/otp/resend", resend, "otp", resendContract));
|
|
377
429
|
}
|
|
378
430
|
|
|
379
431
|
// ── App mode ───────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute budgets with room under the cap also fail at 2× the last value.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
absoluteBudgetOk,
|
|
8
|
+
absoluteRegressionCeiling,
|
|
9
|
+
formatBudgetsReport,
|
|
10
|
+
type BudgetsSnapshot,
|
|
11
|
+
} from "./measure.ts";
|
|
12
|
+
import {
|
|
13
|
+
ABSOLUTE_REGRESSION_RATIO,
|
|
14
|
+
CLIENT_BUDGET_BYTES,
|
|
15
|
+
COLD_START_BUDGET_MS,
|
|
16
|
+
CONSOLE_BUDGET_BYTES,
|
|
17
|
+
KERNEL_EDGE_BUDGET_BYTES,
|
|
18
|
+
} from "./limits.ts";
|
|
19
|
+
|
|
20
|
+
describe("absolute regression ceilings", () => {
|
|
21
|
+
test("ratio is 2× so a confirmed doubling fails and a single noisy run can be re-measured", () => {
|
|
22
|
+
expect(ABSOLUTE_REGRESSION_RATIO).toBe(2);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("cold start and console initial load grow a ceiling; kernel, client, and a zero baseline do not", () => {
|
|
26
|
+
expect(absoluteRegressionCeiling(undefined, COLD_START_BUDGET_MS, "ms")).toBeUndefined();
|
|
27
|
+
expect(absoluteRegressionCeiling(0, 1, "ms")).toBeUndefined();
|
|
28
|
+
expect(absoluteRegressionCeiling(8.915, COLD_START_BUDGET_MS, "ms")).toBe(17.83);
|
|
29
|
+
expect(absoluteRegressionCeiling(353_621, CONSOLE_BUDGET_BYTES, "bytes")).toBe(707_242);
|
|
30
|
+
expect(absoluteRegressionCeiling(12_556, KERNEL_EDGE_BUDGET_BYTES, "bytes")).toBeUndefined();
|
|
31
|
+
expect(absoluteRegressionCeiling(4598, CLIENT_BUDGET_BYTES, "bytes")).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("ok requires both the cap and the ceiling", () => {
|
|
35
|
+
expect(absoluteBudgetOk(8.915, COLD_START_BUDGET_MS, 17.83)).toBe(true);
|
|
36
|
+
expect(absoluteBudgetOk(17.83, COLD_START_BUDGET_MS, 17.83)).toBe(false);
|
|
37
|
+
expect(absoluteBudgetOk(20, COLD_START_BUDGET_MS, 17.83)).toBe(false);
|
|
38
|
+
expect(absoluteBudgetOk(80, COLD_START_BUDGET_MS, undefined)).toBe(false);
|
|
39
|
+
expect(absoluteBudgetOk(12_556, KERNEL_EDGE_BUDGET_BYTES, undefined)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("the CI log prints the 2× ceiling and the absolute cap", () => {
|
|
43
|
+
const snapshot: BudgetsSnapshot = {
|
|
44
|
+
measuredAt: "2026-09-23T00:00:00.000Z",
|
|
45
|
+
version: "0.0.0",
|
|
46
|
+
budgets: [
|
|
47
|
+
{
|
|
48
|
+
id: "coldStartMedianMs",
|
|
49
|
+
label: "Cold start on Bun",
|
|
50
|
+
value: 20,
|
|
51
|
+
limit: COLD_START_BUDGET_MS,
|
|
52
|
+
regressionLimit: 17.83,
|
|
53
|
+
unit: "ms",
|
|
54
|
+
gate: "absolute",
|
|
55
|
+
group: "core",
|
|
56
|
+
ok: false,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
const report = formatBudgetsReport(snapshot);
|
|
61
|
+
expect(report).toContain("[FAIL] Cold start on Bun: 20.000 ms < 17.830 ms (cap 75.000 ms)");
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/release/build-lib.ts
CHANGED
|
@@ -26,6 +26,7 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
|
|
|
26
26
|
// Lazy sync chunks — loaded via requirePackageModule / computed require
|
|
27
27
|
// when the feature actually runs (auth, list page, auto-cache).
|
|
28
28
|
{ src: "src/kernel/fx-runtime.ts", out: "dist/fx-runtime.js" },
|
|
29
|
+
{ src: "src/kernel/client-descriptor.ts", out: "dist/client-descriptor.js" },
|
|
29
30
|
{ src: "src/auth/config.ts", out: "dist/auth-config.js" },
|
|
30
31
|
{ src: "src/kernel/fx-auth-keys.ts", out: "dist/fx-auth-keys.js" },
|
|
31
32
|
{ src: "src/kernel/fx-auth-tenants.ts", out: "dist/fx-auth-tenants.js" },
|
|
@@ -37,6 +38,12 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
|
|
|
37
38
|
{ src: "src/kernel/errors-live-resume.ts", out: "dist/errors-live-resume.js" },
|
|
38
39
|
{ src: "src/kernel/errors-channel.ts", out: "dist/errors-channel.js" },
|
|
39
40
|
{ src: "src/kernel/errors-tenant.ts", out: "dist/errors-tenant.js" },
|
|
41
|
+
{ src: "src/kernel/errors-text.ts", out: "dist/errors-text.js" },
|
|
42
|
+
{ src: "src/kernel/fx-sql-handle.ts", out: "dist/fx-sql-handle.js" },
|
|
43
|
+
{ src: "src/kernel/concurrency.ts", out: "dist/concurrency.js" },
|
|
44
|
+
{ src: "src/compiler/aot.ts", out: "dist/aot.js" },
|
|
45
|
+
{ src: "src/runs/window.ts", out: "dist/window.js" },
|
|
46
|
+
{ src: "src/okid-extended.ts", out: "dist/okid-extended.js" },
|
|
40
47
|
{ src: "src/kernel/clock-durable.ts", out: "dist/clock-durable.js" },
|
|
41
48
|
{ src: "src/kernel/clock-reconcile.ts", out: "dist/clock-reconcile.js" },
|
|
42
49
|
{ src: "src/kernel/clock-per-tenant-name.ts", out: "dist/clock-per-tenant-name.js" },
|
|
@@ -44,6 +51,8 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
|
|
|
44
51
|
{ src: "src/kernel/list-page.ts", out: "dist/list-page.js" },
|
|
45
52
|
{ src: "src/kernel/http-resource.ts", out: "dist/http-resource.js" },
|
|
46
53
|
{ src: "src/elements/store/cache.ts", out: "dist/store-cache.js" },
|
|
54
|
+
{ src: "src/elements/store/search-runtime.ts", out: "dist/search-runtime.js" },
|
|
55
|
+
{ src: "src/elements/store/schema-decl.ts", out: "dist/schema-decl.js" },
|
|
47
56
|
{ src: "src/i18n/messages.ts", out: "dist/messages.js" },
|
|
48
57
|
{ src: "src/i18n/failure-message.ts", out: "dist/failure-message.js" },
|
|
49
58
|
{ src: "src/okid.ts", out: "dist/okid.js" },
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `okengine/http` must not statically import heavy modules.
|
|
3
|
+
* Forbidden ids live in {@link HTTP_STATIC_GRAPH_FORBIDDEN}.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { forbiddenHttpGraphHits, readStaticGraphSpecifiers } from "./http-graph.ts";
|
|
8
|
+
|
|
9
|
+
describe("okengine/http static import graph", () => {
|
|
10
|
+
test("zod and the hybrid-search runtime are not in the graph", async () => {
|
|
11
|
+
const specifiers = await readStaticGraphSpecifiers();
|
|
12
|
+
expect(specifiers.length).toBeGreaterThan(10);
|
|
13
|
+
expect(forbiddenHttpGraphHits(specifiers)).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("the forbidden list matches zod and search-runtime, and nothing nearby", () => {
|
|
17
|
+
expect(forbiddenHttpGraphHits(["node_modules/zod/index.js"])).toEqual(["zod"]);
|
|
18
|
+
expect(forbiddenHttpGraphHits(["zod"])).toEqual(["zod"]);
|
|
19
|
+
expect(forbiddenHttpGraphHits(["src/elements/store/search-runtime.ts"])).toEqual([
|
|
20
|
+
"hybrid-search runtime",
|
|
21
|
+
]);
|
|
22
|
+
expect(forbiddenHttpGraphHits(["src/elements/store/search-lsh.ts"])).toEqual([]);
|
|
23
|
+
expect(forbiddenHttpGraphHits(["src/kernel/app.ts"])).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
});
|