okengine 0.20.0 → 0.21.1

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 (104) hide show
  1. package/package.json +3 -7
  2. package/site/content/docs/client/calling.mdx +117 -29
  3. package/site/content/docs/client/index.mdx +3 -3
  4. package/site/content/docs/elements/flow/http.mdx +18 -15
  5. package/site/content/docs/elements/flow/index.mdx +26 -18
  6. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  7. package/site/content/docs/elements/store/files.mdx +11 -5
  8. package/site/content/docs/elements/store/index.mdx +2 -3
  9. package/site/content/docs/elements/store/kv.mdx +8 -2
  10. package/site/content/docs/elements/store/sql.mdx +9 -6
  11. package/site/content/docs/elements/vault/index.mdx +11 -8
  12. package/site/content/docs/elements/vault/secrets.mdx +6 -3
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/recipes/rustfs.mdx +1 -1
  15. package/site/content/docs/reference/configuration.mdx +1 -1
  16. package/site/content/docs/reference/errors.mdx +199 -25
  17. package/site/content/docs/reference/fx.mdx +6 -1
  18. package/site/content/docs/understand/the-architecture.mdx +2 -2
  19. package/site/content/docs/understand/try-it.mdx +758 -25
  20. package/src/cli/dev-app-runner.ts +2 -1
  21. package/src/cli/dev.test.ts +105 -2
  22. package/src/cli/dev.ts +37 -2
  23. package/src/cli/start.ts +2 -1
  24. package/src/client/create.ts +14 -27
  25. package/src/client/explain.test.ts +252 -0
  26. package/src/client/explain.ts +272 -0
  27. package/src/client/live.test.ts +44 -0
  28. package/src/client/live.ts +44 -101
  29. package/src/client/notes-contract.test.ts +10 -0
  30. package/src/client/sse.ts +26 -68
  31. package/src/client/stream.ts +25 -67
  32. package/src/client/transport.test.ts +67 -0
  33. package/src/client/transport.ts +51 -112
  34. package/src/client/types.ts +17 -6
  35. package/src/client/wire.ts +119 -0
  36. package/src/client-react/live-resource.ts +6 -2
  37. package/src/compiler/aot.ts +3 -32
  38. package/src/compiler/dynamic.ts +13 -11
  39. package/src/compiler/interpret.ts +45 -0
  40. package/src/compiler/response.ts +17 -27
  41. package/src/console/server/invoke-user-flow.test.ts +8 -2
  42. package/src/console/server/invoke-user-flow.ts +12 -18
  43. package/src/console/server/security.gate.test.ts +1 -1
  44. package/src/console/ui-next/dist/assets/{access-page-DFLu0wTA.js → access-page-Bgt9bq2r.js} +1 -1
  45. package/src/console/ui-next/dist/assets/{agent-disclosure-DGscxaF5.js → agent-disclosure-B43CXZZR.js} +1 -1
  46. package/src/console/ui-next/dist/assets/{cache-glyph-BGmRZk7d.js → cache-glyph-92uM5MO7.js} +1 -1
  47. package/src/console/ui-next/dist/assets/{call-pii-button--feUYxvG.js → call-pii-button-DtGVPtcs.js} +1 -1
  48. package/src/console/ui-next/dist/assets/{collapsible-JWvpaiGY.js → collapsible-DN7l6zmC.js} +1 -1
  49. package/src/console/ui-next/dist/assets/{duration-tone-D9yCJG4n.js → duration-tone-BmIR9FV8.js} +1 -1
  50. package/src/console/ui-next/dist/assets/{flows-page-Bs6MD9GB.js → flows-page-BMHs-IzK.js} +1 -1
  51. package/src/console/ui-next/dist/assets/{highlighted-json-xH8MrEnv.js → highlighted-json-BlAEVgNW.js} +1 -1
  52. package/src/console/ui-next/dist/assets/{http-method-C4vB6ZIw.js → http-method-BDf7OAHv.js} +1 -1
  53. package/src/console/ui-next/dist/assets/{index-yTCY4AcS.js → index-BKpaes3n.js} +3 -3
  54. package/src/console/ui-next/dist/assets/{observability-page-BxJ3R6dU.js → observability-page-WnVLI-0j.js} +1 -1
  55. package/src/console/ui-next/dist/assets/{replica-lag-QRKB_IE8.js → replica-lag-B3GLNVfF.js} +1 -1
  56. package/src/console/ui-next/dist/assets/{request-meta-DqZ-fMu5.js → request-meta-DMbnAe3f.js} +1 -1
  57. package/src/console/ui-next/dist/assets/{store-page-Dixb6L7a.js → store-page-KvFDingJ.js} +1 -1
  58. package/src/console/ui-next/dist/assets/{trace-detail-sheet-CazhjtiU.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
  59. package/src/console/ui-next/dist/assets/{tree-expand-toggle-DlnqYKfr.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{units-page-BXTLjU2-.js → units-page-C4NdNuxP.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{vault-page-39KR__bc.js → vault-page-CBYl0LW_.js} +1 -1
  62. package/src/console/ui-next/dist/index.html +1 -1
  63. package/src/docker/docker.test.ts +3 -3
  64. package/src/docker/images-config.test.ts +4 -4
  65. package/src/docker/stack-id.test.ts +1 -1
  66. package/src/elements/store/files-errors.test.ts +149 -0
  67. package/src/elements/store/files-errors.ts +189 -0
  68. package/src/elements/store/kv-errors.test.ts +98 -0
  69. package/src/elements/store/kv-errors.ts +139 -0
  70. package/src/elements/store/resource.ts +11 -7
  71. package/src/elements/store/runtime.ts +18 -13
  72. package/src/elements/store/sql-errors.test.ts +197 -0
  73. package/src/elements/store/sql-errors.ts +294 -0
  74. package/src/elements/store/sql-session.test.ts +52 -0
  75. package/src/elements/store/sql-session.ts +26 -4
  76. package/src/elements/store/store-errors.ts +47 -0
  77. package/src/http.ts +9 -1
  78. package/src/i18n/catalogs/ar.ts +18 -0
  79. package/src/i18n/catalogs/en.ts +18 -0
  80. package/src/index.ts +9 -1
  81. package/src/kernel/app.ts +23 -4
  82. package/src/kernel/builtin-errors.test.ts +117 -0
  83. package/src/kernel/builtin-errors.ts +129 -0
  84. package/src/kernel/call.test.ts +182 -0
  85. package/src/kernel/client-descriptor.test.ts +78 -0
  86. package/src/kernel/client-descriptor.ts +23 -0
  87. package/src/kernel/errors-text.ts +99 -0
  88. package/src/kernel/errors-vault.ts +16 -0
  89. package/src/kernel/errors.registry.test.ts +7 -0
  90. package/src/kernel/errors.ts +184 -159
  91. package/src/kernel/fail-helpers.ts +34 -0
  92. package/src/kernel/fx-sql-handle.ts +305 -0
  93. package/src/kernel/fx.test.ts +8 -0
  94. package/src/kernel/fx.ts +49 -335
  95. package/src/kernel/index.ts +12 -1
  96. package/src/kernel/json-result.ts +59 -0
  97. package/src/kernel/project-out.ts +6 -1
  98. package/src/okid-extended.ts +175 -0
  99. package/src/okid-shared.ts +103 -0
  100. package/src/okid.ts +30 -213
  101. package/src/release/build-lib.ts +9 -0
  102. package/src/runtime/dev-request-log.ts +29 -11
  103. package/src/term.test.ts +76 -0
  104. package/src/term.ts +166 -3
@@ -0,0 +1,175 @@
1
+ /**
2
+ * `okid({ … })` options path — sortable, prefix, and alphabet toggles.
3
+ *
4
+ * Kept off the kernel edge profile. `okid()` and `okid(length)` never load it.
5
+ */
6
+
7
+ import {
8
+ assertLength,
9
+ OKID_ALPHABET,
10
+ OKID_DEFAULT_LENGTH,
11
+ OKID_LOOKALIKE_CHARS,
12
+ OKID_MAX_PREFIX_LENGTH,
13
+ OKID_MIN_LENGTH,
14
+ OKID_SORTABLE_ALPHABET,
15
+ OKID_SORTABLE_MIN_LENGTH,
16
+ type OkidOptions,
17
+ } from "./okid-shared.ts";
18
+
19
+ /** Character groups addressable through {@link OkidOptions} toggles. */
20
+ const GROUPS = {
21
+ numbers: "0123456789",
22
+ lowercase: "abcdefghijklmnopqrstuvwxyz",
23
+ uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
24
+ symbols: "-_",
25
+ } as const;
26
+
27
+ /** Resolved alphabet + encoding metadata for one options combination. */
28
+ interface ResolvedAlphabet {
29
+ readonly chars: string;
30
+ readonly size: number;
31
+ /** Bitmask covering `size` values (`size` is always a power of two here). */
32
+ readonly mask: number;
33
+ }
34
+
35
+ /** Memoized resolutions keyed by the toggle bitmask (32 combinations max). */
36
+ const ALPHABET_CACHE = new Map<number, ResolvedAlphabet>();
37
+
38
+ /**
39
+ * Resolve a toggle combination to an alphabet and rejection-sampling mask.
40
+ *
41
+ * @param numbers - Include `0-9`
42
+ * @param lowercase - Include `a-z`
43
+ * @param uppercase - Include `A-Z`
44
+ * @param symbols - Include `-` and `_`
45
+ * @param lookAlikes - Include visually confusable characters
46
+ */
47
+ function resolveAlphabet(
48
+ numbers: boolean,
49
+ lowercase: boolean,
50
+ uppercase: boolean,
51
+ symbols: boolean,
52
+ lookAlikes: boolean,
53
+ ): ResolvedAlphabet {
54
+ const key =
55
+ (numbers ? 1 : 0) |
56
+ (lowercase ? 2 : 0) |
57
+ (uppercase ? 4 : 0) |
58
+ (symbols ? 8 : 0) |
59
+ (lookAlikes ? 0 : 16);
60
+ const cached = ALPHABET_CACHE.get(key);
61
+ if (cached) return cached;
62
+
63
+ let chars = "";
64
+ if (numbers) chars += GROUPS.numbers;
65
+ if (lowercase) chars += GROUPS.lowercase;
66
+ if (uppercase) chars += GROUPS.uppercase;
67
+ if (symbols) chars += GROUPS.symbols;
68
+ if (!chars) {
69
+ throw new RangeError("okid: alphabet is empty — enable at least one character group");
70
+ }
71
+ if (!lookAlikes) {
72
+ chars = [...chars].filter((c) => !OKID_LOOKALIKE_CHARS.includes(c)).join("");
73
+ }
74
+
75
+ // Round up to a power of two for mask-based rejection sampling: bytes below
76
+ // `size` map uniformly, bytes above are discarded and re-drawn — unbiased at
77
+ // every alphabet size, unlike naive modulo.
78
+ const rawSize = chars.length;
79
+ const size = 1 << Math.ceil(Math.log2(rawSize));
80
+ const resolved: ResolvedAlphabet = { chars, size: rawSize, mask: size - 1 };
81
+ ALPHABET_CACHE.set(key, resolved);
82
+ return resolved;
83
+ }
84
+
85
+ /**
86
+ * Encode one random byte stream into `length` characters of `alphabet`.
87
+ *
88
+ * @param alphabet - Resolved charset
89
+ * @param length - Output length
90
+ */
91
+ function encodeAlphabet(alphabet: ResolvedAlphabet, length: number): string {
92
+ const { chars, size, mask } = alphabet;
93
+ const bytes = new Uint8Array(length + Math.ceil(length >> 2));
94
+ crypto.getRandomValues(bytes.subarray(0, length));
95
+ let out = "";
96
+ let i = 0;
97
+ while (out.length < length && i < bytes.length) {
98
+ const byte = bytes[i++]!;
99
+ if ((byte & mask) < size) out += chars[byte & mask];
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /**
105
+ * Pack epoch-ms into exactly 8 codepoint-ordered characters (48 bits).
106
+ *
107
+ * @param nowMs - Epoch milliseconds
108
+ */
109
+ function encodeTimestamp(nowMs: number): string {
110
+ let t = nowMs % 2 ** 48;
111
+ let out = "";
112
+ for (let i = 0; i < 8; i++) {
113
+ out = OKID_SORTABLE_ALPHABET[t & 63]! + out;
114
+ t = Math.floor(t / 64);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Assert a semantic prefix is within bounds and stays on the URL-safe alphabet.
121
+ *
122
+ * @param prefix - Caller-supplied label
123
+ */
124
+ function assertPrefix(prefix: string): void {
125
+ if (prefix.length > OKID_MAX_PREFIX_LENGTH) {
126
+ throw new RangeError(
127
+ `okid: prefix length ${prefix.length} exceeds max ${OKID_MAX_PREFIX_LENGTH}`,
128
+ );
129
+ }
130
+ for (const char of prefix) {
131
+ if (!OKID_ALPHABET.includes(char)) {
132
+ throw new RangeError(
133
+ `okid: prefix contains invalid character ${JSON.stringify(char)} — use characters from OKID_ALPHABET`,
134
+ );
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Generate an id from an options object.
141
+ *
142
+ * @param options - Length, prefix, sortable, and alphabet toggles
143
+ */
144
+ export function okidWithOptions(options: OkidOptions): string {
145
+ const {
146
+ length = OKID_DEFAULT_LENGTH,
147
+ prefix = "",
148
+ sortable = false,
149
+ lowercase = true,
150
+ uppercase = true,
151
+ numbers = true,
152
+ symbols = true,
153
+ lookAlikes = true,
154
+ } = options;
155
+
156
+ if (prefix) assertPrefix(prefix);
157
+
158
+ let body: string;
159
+ if (sortable) {
160
+ assertLength(length, OKID_SORTABLE_MIN_LENGTH, "length");
161
+ // Time ordering requires lexicographic encoding, which requires the full
162
+ // codepoint-ordered alphabet — partial subsets cannot preserve both the
163
+ // caller's charset choice AND cross-ms ordering, so toggles are ignored.
164
+ const alphabet = resolveAlphabet(true, true, true, true, true);
165
+ body = encodeTimestamp(Date.now()) + encodeAlphabet(alphabet, length - 8);
166
+ } else {
167
+ assertLength(length, OKID_MIN_LENGTH, "length");
168
+ body = encodeAlphabet(
169
+ resolveAlphabet(numbers, lowercase, uppercase, symbols, lookAlikes),
170
+ length,
171
+ );
172
+ }
173
+
174
+ return prefix ? prefix + body : body;
175
+ }
@@ -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
  }
@@ -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" },
@@ -56,6 +56,8 @@ export type DevRequestLogInput = {
56
56
  readonly ms: number;
57
57
  /** Human failure detail (error.message / code) for 4xx/5xx. */
58
58
  readonly detail?: string;
59
+ /** Failure `error.code` for the title chip. */
60
+ readonly errorCode?: string;
59
61
  };
60
62
 
61
63
  /**
@@ -85,16 +87,19 @@ export function isSilentDevRequest(method: string, path: string, surface?: DevLo
85
87
  *
86
88
  * @param response - HTTP response (cloned; original body stays readable)
87
89
  */
88
- export async function failureDetailFromResponse(response: Response): Promise<string | undefined> {
89
- if (response.status < 400) return undefined;
90
+ export async function failureEnvelopeFromResponse(
91
+ response: Response,
92
+ ): Promise<{ readonly detail?: string; readonly code?: string }> {
93
+ if (response.status < 400) return {};
90
94
  try {
91
95
  const body: unknown = await response.clone().json();
92
- if (body === null || typeof body !== "object" || !("error" in body)) return undefined;
96
+ if (body === null || typeof body !== "object" || !("error" in body)) return {};
93
97
  const error = (body as { error: unknown }).error;
94
- if (error === null || typeof error !== "object") return undefined;
98
+ if (error === null || typeof error !== "object") return {};
95
99
  const rec = error as { message?: unknown; code?: unknown; data?: unknown };
100
+ const code = typeof rec.code === "string" ? rec.code : undefined;
96
101
  if (typeof rec.message === "string" && rec.message.trim().length > 0) {
97
- return rec.message.trim();
102
+ return { detail: rec.message.trim(), ...(code ? { code } : {}) };
98
103
  }
99
104
  if (
100
105
  rec.data !== null &&
@@ -103,14 +108,25 @@ export async function failureDetailFromResponse(response: Response): Promise<str
103
108
  typeof (rec.data as { reason: unknown }).reason === "string"
104
109
  ) {
105
110
  const reason = (rec.data as { reason: string }).reason;
106
- const code = typeof rec.code === "string" ? rec.code : "Error";
107
- return `${code}: ${reason}`;
111
+ return {
112
+ detail: `${code ?? "Error"}: ${reason}`,
113
+ ...(code ? { code } : {}),
114
+ };
108
115
  }
109
- if (typeof rec.code === "string") return rec.code;
116
+ if (code) return { detail: code, code };
110
117
  } catch {
111
118
  // non-JSON error bodies stay status-only
112
119
  }
113
- return undefined;
120
+ return {};
121
+ }
122
+
123
+ /**
124
+ * Extract a short failure detail from an OKE JSON envelope body.
125
+ *
126
+ * @param response - HTTP response (cloned; original body stays readable)
127
+ */
128
+ export async function failureDetailFromResponse(response: Response): Promise<string | undefined> {
129
+ return (await failureEnvelopeFromResponse(response)).detail;
114
130
  }
115
131
 
116
132
  /**
@@ -135,6 +151,7 @@ export function logDevRequest(input: DevRequestLogInput): void {
135
151
  status: input.status,
136
152
  ms: input.ms,
137
153
  detail: input.detail,
154
+ errorCode: input.errorCode,
138
155
  }),
139
156
  );
140
157
  }
@@ -167,7 +184,7 @@ export async function timedDevFetch(
167
184
  const url = new URL(request.url);
168
185
  const method = request.method.toUpperCase();
169
186
  const response = await handle(request);
170
- const detail = await failureDetailFromResponse(response);
187
+ const envelope = await failureEnvelopeFromResponse(response);
171
188
  logDevRequest({
172
189
  surface: options.surface ?? currentDevSurface(),
173
190
  method,
@@ -176,7 +193,8 @@ export async function timedDevFetch(
176
193
  runId: options.resolveRunId?.(request, response),
177
194
  status: response.status,
178
195
  ms: Math.round(performance.now() - started),
179
- detail,
196
+ detail: envelope.detail,
197
+ errorCode: envelope.code,
180
198
  });
181
199
  return response;
182
200
  }