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.
Files changed (160) hide show
  1. package/AGENTS.md +4 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +10 -0
  6. package/package.json +22 -25
  7. package/site/content/docs/ai/skills.mdx +1 -1
  8. package/site/content/docs/client/calling.mdx +27 -19
  9. package/site/content/docs/client/index.mdx +1 -1
  10. package/site/content/docs/elements/channel/email.mdx +1 -1
  11. package/site/content/docs/elements/channel/index.mdx +1 -1
  12. package/site/content/docs/elements/flow/http.mdx +4 -4
  13. package/site/content/docs/elements/flow/index.mdx +3 -3
  14. package/site/content/docs/elements/flow/meta.json +1 -1
  15. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  16. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  17. package/site/content/docs/plugins/otp.mdx +31 -31
  18. package/site/content/docs/recipes/mailpit.mdx +1 -1
  19. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  20. package/site/content/docs/recipes/pgdog.mdx +1 -1
  21. package/site/content/docs/reference/configuration.mdx +2 -2
  22. package/site/content/docs/reference/errors.mdx +12 -2
  23. package/site/content/docs/reference/fx.mdx +28 -15
  24. package/site/content/docs/reference/idempotency.mdx +192 -0
  25. package/site/content/docs/reference/index.mdx +6 -1
  26. package/site/content/docs/reference/meta.json +2 -1
  27. package/site/content/docs/understand/meta.json +1 -1
  28. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  29. package/site/content/docs/understand/the-architecture.mdx +14 -12
  30. package/site/content/docs/understand/try-it.mdx +3 -3
  31. package/src/cli/competitor-mention-removal.test.ts +3 -3
  32. package/src/cli/dev.test.ts +105 -2
  33. package/src/cli/dev.ts +37 -2
  34. package/src/cli/docker-cli.test.ts +1 -1
  35. package/src/cli/index.ts +9 -1
  36. package/src/cli/load-config.images.test.ts +10 -10
  37. package/src/cli/load-config.ts +1 -1
  38. package/src/cli/vault-cmd.test.ts +23 -0
  39. package/src/cli/vault-cmd.ts +7 -0
  40. package/src/client/create.ts +31 -27
  41. package/src/client/live.ts +44 -101
  42. package/src/client/sse.ts +20 -66
  43. package/src/client/stream.ts +25 -67
  44. package/src/client/transport.test.ts +225 -0
  45. package/src/client/transport.ts +158 -90
  46. package/src/client/types.ts +29 -3
  47. package/src/client/wire.ts +119 -0
  48. package/src/compiler/aot.ts +3 -32
  49. package/src/compiler/dynamic.ts +13 -11
  50. package/src/compiler/effects-infer.ts +726 -16
  51. package/src/compiler/extract.test.ts +37 -0
  52. package/src/compiler/extract.ts +113 -12
  53. package/src/compiler/fixtures/skyport.expected.json +24 -0
  54. package/src/compiler/fx-follow.test.ts +164 -0
  55. package/src/compiler/fx-index.ts +399 -0
  56. package/src/compiler/interpret.ts +45 -0
  57. package/src/console/server/flows-invoke.test.ts +2 -2
  58. package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.js → access-page-Ze6ckjdd.js} +2 -2
  59. package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
  60. package/src/console/ui-next/dist/assets/{cache-glyph-BCC-DxKT.js → cache-glyph-rL0lHxat.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-Deo4PP18.js} +1 -1
  62. package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.js → collapsible-CoJ6amHf.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
  65. package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
  66. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
  67. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
  68. package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
  69. package/src/console/ui-next/dist/assets/{flows-page-gT1lsWQK.js → flows-page-D2E7BtKK.js} +1 -1
  70. package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-CPYBzDh0.js} +1 -1
  71. package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.js → http-method-DTPevKmM.js} +1 -1
  72. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
  73. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
  74. package/src/console/ui-next/dist/assets/{replica-lag-C4QdAF7J.js → replica-lag-BM27lXld.js} +3 -3
  75. package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.js → request-meta-BjFT7DNl.js} +1 -1
  76. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
  77. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
  78. package/src/console/ui-next/dist/assets/{trace-detail-sheet-Dcpo4Us_.js → trace-detail-sheet-Cmitq3da.js} +2 -2
  79. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
  80. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
  81. package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
  82. package/src/console/ui-next/dist/assets/{vault-page-BwZ9YjTW.js → vault-page-BOMr0go9.js} +2 -2
  83. package/src/console/ui-next/dist/index.html +2 -2
  84. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
  85. package/src/docker/docker.test.ts +13 -13
  86. package/src/docker/images-config.test.ts +12 -12
  87. package/src/docker/stack-id.test.ts +1 -1
  88. package/src/drivers/journal-postgres.ts +168 -3
  89. package/src/drivers/meilisearch.ts +2 -0
  90. package/src/drivers/vault-builtin.test.ts +10 -0
  91. package/src/drivers/vault-builtin.ts +13 -1
  92. package/src/drivers/vault-remote-bag.ts +5 -1
  93. package/src/drivers/vault-types.ts +6 -1
  94. package/src/elements/store/emit-drizzle.ts +2 -2
  95. package/src/elements/store/live-default.test.ts +2 -0
  96. package/src/elements/store/live-http.test.ts +1 -0
  97. package/src/elements/store/resource-list-docs.test.ts +2 -2
  98. package/src/elements/store/resource.test.ts +3 -3
  99. package/src/elements/store/schema-decl.test.ts +1 -0
  100. package/src/elements/store/sql-condition.ts +24 -7
  101. package/src/elements/store/sql-session.ts +20 -4
  102. package/src/elements/store/upsert-app.test.ts +1 -1
  103. package/src/elements/vault/audit.test.ts +137 -0
  104. package/src/elements/vault/audit.ts +106 -13
  105. package/src/elements/vault/boot-chain.ts +9 -1
  106. package/src/elements/vault/builtin-adapter.ts +33 -3
  107. package/src/i18n/catalogs/ar.ts +4 -0
  108. package/src/i18n/catalogs/en.ts +4 -0
  109. package/src/kernel/abort-scope.ts +33 -2
  110. package/src/kernel/app.ts +200 -20
  111. package/src/kernel/auto-cache.test.ts +6 -6
  112. package/src/kernel/boot-bind/vault.ts +1 -0
  113. package/src/kernel/boot.ts +19 -1
  114. package/src/kernel/builtin-errors.ts +8 -0
  115. package/src/kernel/client-descriptor.test.ts +78 -0
  116. package/src/kernel/client-descriptor.ts +24 -0
  117. package/src/kernel/concurrency.test.ts +33 -0
  118. package/src/kernel/effects-stamping.test.ts +2 -2
  119. package/src/kernel/errors-compiler.ts +20 -0
  120. package/src/kernel/errors-text.ts +99 -0
  121. package/src/kernel/errors.ts +90 -138
  122. package/src/kernel/external-effects.test.ts +36 -0
  123. package/src/kernel/flow.ts +24 -0
  124. package/src/kernel/fx-fetch.ts +5 -1
  125. package/src/kernel/fx-sql-handle.ts +305 -0
  126. package/src/kernel/fx.ts +42 -330
  127. package/src/kernel/idempotency-store.ts +578 -0
  128. package/src/kernel/idempotency.test.ts +556 -0
  129. package/src/kernel/idempotency.ts +304 -0
  130. package/src/kernel/index.ts +1 -0
  131. package/src/kernel/journal.ts +56 -0
  132. package/src/kernel/json-result.ts +59 -0
  133. package/src/kernel/project-out.ts +6 -1
  134. package/src/manifest/types.ts +8 -0
  135. package/src/okid-extended.ts +175 -0
  136. package/src/okid-shared.ts +103 -0
  137. package/src/okid.ts +30 -213
  138. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  139. package/src/plugins/otp.test.ts +65 -1
  140. package/src/plugins/otp.ts +59 -7
  141. package/src/release/absolute-regression.test.ts +63 -0
  142. package/src/release/build-lib.ts +9 -0
  143. package/src/release/http-graph.test.ts +25 -0
  144. package/src/release/http-graph.ts +90 -0
  145. package/src/release/index.ts +3 -0
  146. package/src/release/limits.ts +17 -2
  147. package/src/release/measure.ts +105 -14
  148. package/src/test/create-test-app.test.ts +3 -1
  149. package/src/test/create-test-app.ts +40 -0
  150. package/src/test/live-signals.test.ts +1 -0
  151. package/src/test/provisions.integration.test.ts +1 -0
  152. package/src/test/tenant-isolation.test.ts +1 -0
  153. package/src/console/ui-next/dist/assets/agent-disclosure-CSKumwS2.js +0 -1
  154. package/src/console/ui-next/dist/assets/duration-tone-CwoV56jn.js +0 -9
  155. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  156. package/src/console/ui-next/dist/assets/index-Cul17AcV.js +0 -63
  157. package/src/console/ui-next/dist/assets/observability-page-oY9vdYBk.js +0 -4
  158. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  159. package/src/console/ui-next/dist/assets/store-page-CL0D9dOq.js +0 -41
  160. 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
- /** Character groups addressable through {@link OkidOptions} toggles. */
56
- const GROUPS = {
57
- numbers: "0123456789",
58
- lowercase: "abcdefghijklmnopqrstuvwxyz",
59
- uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
60
- symbols: "-_",
61
- } as const;
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
- * Characters removed when `lookAlikes` is disabled (human transcription).
77
+ * Load sortable / prefix / alphabet-toggle encoding.
78
+ * Computed stem so the edge profile does not inline it.
65
79
  */
66
- export const OKID_LOOKALIKE_CHARS = "1lI0Oouv5Ss";
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
- return encode(resolveAlphabet(true, true, true, true, true), options);
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.1";
21
+ const MAILPIT_IMAGE = "axllent/mailpit:v1.31.2";
22
22
 
23
23
  afterEach(() => {
24
24
  resetBindings();
@@ -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")).toBeFalsy();
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
  });
@@ -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
- readonly resendCooldownMs?: never;
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
+ });
@@ -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
+ });