@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.63
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/AUTHORING.md +268 -9
- package/CHANGELOG.md +8 -0
- package/README.md +1 -0
- package/bin/apifuse-check.ts +84 -2
- package/bin/apifuse-dev.ts +11 -31
- package/bin/apifuse-migrate-deployment.ts +200 -0
- package/bin/apifuse-pack-check.ts +11 -4
- package/bin/apifuse-pack-types.ts +191 -4
- package/bin/apifuse-record.ts +44 -32
- package/bin/apifuse-test.ts +3 -1
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +11 -0
- package/dist/cli/deployment-intent.d.ts +178 -0
- package/dist/cli/deployment-intent.js +277 -0
- package/dist/cli/migrate-deployment-intent.d.ts +132 -0
- package/dist/cli/migrate-deployment-intent.js +1552 -0
- package/dist/cli/migrate-provider-shape.d.ts +5 -1
- package/dist/cli/migrate-provider-shape.js +4 -2
- package/dist/cli/resolver-runtime.d.ts +21 -0
- package/dist/cli/resolver-runtime.js +31 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +5 -0
- package/dist/dev.d.ts +3 -1
- package/dist/dev.js +6 -1
- package/dist/engine.d.ts +35 -1
- package/dist/engine.js +21 -2
- package/dist/error-resolution.d.ts +1 -0
- package/dist/error-resolution.js +54 -0
- package/dist/errors.d.ts +39 -1
- package/dist/errors.js +50 -0
- package/dist/handle-meta.d.ts +18 -2
- package/dist/handle-meta.js +30 -2
- package/dist/handle.d.ts +33 -12
- package/dist/handle.js +69 -24
- package/dist/i18n/error-messages.d.ts +86 -0
- package/dist/i18n/error-messages.js +192 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/keys.d.ts +12 -0
- package/dist/i18n/keys.js +76 -5
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -1
- package/dist/lint.d.ts +10 -0
- package/dist/lint.js +440 -57
- package/dist/provider.d.ts +7 -5
- package/dist/provider.js +4 -2
- package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
- package/dist/runtime/browser-telemetry-binding.js +83 -0
- package/dist/runtime/browser-telemetry.d.ts +76 -0
- package/dist/runtime/browser-telemetry.js +165 -0
- package/dist/runtime/browser.d.ts +3 -0
- package/dist/runtime/browser.js +208 -47
- package/dist/runtime/cache-telemetry.d.ts +71 -0
- package/dist/runtime/cache-telemetry.js +177 -0
- package/dist/runtime/cache.js +67 -7
- package/dist/runtime/capability-response.d.ts +3 -0
- package/dist/runtime/capability-response.js +56 -0
- package/dist/runtime/capability-telemetry.d.ts +46 -0
- package/dist/runtime/capability-telemetry.js +68 -0
- package/dist/runtime/diagnostic-env.d.ts +3 -0
- package/dist/runtime/diagnostic-env.js +10 -1
- package/dist/runtime/engine-mode.d.ts +74 -0
- package/dist/runtime/engine-mode.js +152 -0
- package/dist/runtime/handle.js +15 -10
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +16 -0
- package/dist/runtime/http-telemetry-guard.js +202 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +356 -174
- package/dist/runtime/instrumentation.js +92 -13
- package/dist/runtime/ocr-telemetry.d.ts +82 -0
- package/dist/runtime/ocr-telemetry.js +192 -0
- package/dist/runtime/ocr.js +30 -8
- package/dist/runtime/request-telemetry.d.ts +2 -0
- package/dist/runtime/request-telemetry.js +2 -1
- package/dist/runtime/resolver-runtime-options.d.ts +14 -0
- package/dist/runtime/resolver-runtime-options.js +33 -0
- package/dist/runtime/resolver-shared.d.ts +3 -2
- package/dist/runtime/resolver-shared.js +4 -4
- package/dist/runtime/resolver-telemetry.d.ts +1 -0
- package/dist/runtime/resolver-telemetry.js +1 -1
- package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
- package/dist/runtime/resolver-vendors/browser.js +46 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
- package/dist/runtime/resolver-vendors/hosts.js +35 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
- package/dist/runtime/resolver-vendors/types.d.ts +5 -4
- package/dist/runtime/resolver.d.ts +4 -1
- package/dist/runtime/resolver.js +46 -13
- package/dist/runtime/state-telemetry.d.ts +79 -0
- package/dist/runtime/state-telemetry.js +194 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +26 -7
- package/dist/runtime/stealth-cookies.d.ts +5 -1
- package/dist/runtime/stealth-cookies.js +21 -1
- package/dist/runtime/stealth-owned-headers.d.ts +26 -0
- package/dist/runtime/stealth-owned-headers.js +37 -0
- package/dist/runtime/stealth-telemetry.d.ts +92 -0
- package/dist/runtime/stealth-telemetry.js +179 -0
- package/dist/runtime/stealth.d.ts +4 -1
- package/dist/runtime/stealth.js +228 -85
- package/dist/runtime/stt-telemetry.d.ts +77 -0
- package/dist/runtime/stt-telemetry.js +176 -0
- package/dist/runtime/stt.js +15 -6
- package/dist/server/error-stack-frames.d.ts +35 -0
- package/dist/server/error-stack-frames.js +219 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.js +5 -0
- package/dist/server/sdk-version.d.ts +3 -0
- package/dist/server/sdk-version.js +19 -0
- package/dist/server/self-test.js +1 -18
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +9 -0
- package/dist/server/serve-implementation.d.ts +53 -2
- package/dist/server/serve-implementation.js +369 -123
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +7 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
- package/dist/stateful/stateful-provider-runtime-context.js +1 -0
- package/dist/types.d.ts +94 -1
- package/package.json +6 -1
- package/src/cli/commands.ts +14 -0
- package/src/cli/deployment-intent.ts +364 -0
- package/src/cli/migrate-deployment-intent.ts +1908 -0
- package/src/cli/migrate-provider-shape.ts +10 -4
- package/src/cli/resolver-runtime.ts +72 -0
- package/src/define.ts +13 -0
- package/src/dev.ts +9 -2
- package/src/engine.ts +38 -2
- package/src/error-resolution.ts +58 -0
- package/src/errors.ts +77 -1
- package/src/handle-meta.ts +39 -4
- package/src/handle.ts +121 -38
- package/src/i18n/error-messages.ts +255 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/keys.ts +80 -6
- package/src/index.ts +91 -0
- package/src/lint.ts +552 -57
- package/src/provider.ts +20 -0
- package/src/runtime/browser-telemetry-binding.ts +92 -0
- package/src/runtime/browser-telemetry.ts +289 -0
- package/src/runtime/browser.ts +249 -65
- package/src/runtime/cache-telemetry.ts +247 -0
- package/src/runtime/cache.ts +68 -17
- package/src/runtime/capability-response.ts +53 -0
- package/src/runtime/capability-telemetry.ts +101 -0
- package/src/runtime/diagnostic-env.ts +11 -1
- package/src/runtime/engine-mode.ts +233 -0
- package/src/runtime/handle.ts +20 -10
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +208 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +457 -204
- package/src/runtime/instrumentation.ts +105 -16
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +109 -74
- package/src/runtime/request-telemetry.ts +2 -1
- package/src/runtime/resolver-runtime-options.ts +50 -0
- package/src/runtime/resolver-shared.ts +9 -4
- package/src/runtime/resolver-telemetry.ts +1 -1
- package/src/runtime/resolver-vendors/browser.ts +65 -7
- package/src/runtime/resolver-vendors/hosts.ts +43 -0
- package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
- package/src/runtime/resolver-vendors/types.ts +5 -4
- package/src/runtime/resolver.ts +64 -21
- package/src/runtime/state-telemetry.ts +298 -0
- package/src/runtime/state.ts +46 -45
- package/src/runtime/stealth-cookies.ts +24 -1
- package/src/runtime/stealth-owned-headers.ts +41 -0
- package/src/runtime/stealth-telemetry.ts +304 -0
- package/src/runtime/stealth.ts +355 -150
- package/src/runtime/stt-telemetry.ts +282 -0
- package/src/runtime/stt.ts +93 -71
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +66 -0
- package/src/server/sdk-version.ts +22 -0
- package/src/server/self-test.ts +1 -21
- package/src/server/sensitive-values.ts +14 -0
- package/src/server/serve-implementation.ts +601 -157
- package/src/server/types.ts +7 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
- package/src/stateful/stateful-provider-runtime-context.ts +4 -0
- package/src/types.ts +99 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
package/src/handle.ts
CHANGED
|
@@ -16,12 +16,15 @@ import {
|
|
|
16
16
|
} from "./errors.js";
|
|
17
17
|
import {
|
|
18
18
|
APIFUSE_HANDLE_META_KEY,
|
|
19
|
+
formatHandleIssuers,
|
|
19
20
|
handleFieldDescription,
|
|
20
21
|
HANDLE_KIND_NAME_PATTERN,
|
|
21
22
|
type HandleAccess,
|
|
22
23
|
type HandleFieldMeta,
|
|
24
|
+
type HandleIssuedBy,
|
|
23
25
|
type HandleKindDeclaration,
|
|
24
26
|
type HandleKindType,
|
|
27
|
+
isHandleIssuedBy,
|
|
25
28
|
} from "./handle-meta.js";
|
|
26
29
|
import type { ProviderErrorCategory } from "./observability.js";
|
|
27
30
|
import {
|
|
@@ -31,9 +34,15 @@ import {
|
|
|
31
34
|
} from "./runtime/handle-wordlist.js";
|
|
32
35
|
import type { ProviderStateDurationString } from "./types.js";
|
|
33
36
|
|
|
34
|
-
export type {
|
|
37
|
+
export type {
|
|
38
|
+
HandleAccess,
|
|
39
|
+
HandleFieldMeta,
|
|
40
|
+
HandleIssuedBy,
|
|
41
|
+
HandleKindDeclaration,
|
|
42
|
+
HandleKindType,
|
|
43
|
+
};
|
|
35
44
|
|
|
36
|
-
/** Public
|
|
45
|
+
/** Public kinds whose (max) ttl exceeds this are issued with five words (auto-high). */
|
|
37
46
|
const AUTO_HIGH_TTL_MS = 3_600_000;
|
|
38
47
|
const DEFAULT_BOUND_MAX_ENTRIES = 200;
|
|
39
48
|
const DEFAULT_CURSOR_MAX_VALUE_BYTES = 16_000;
|
|
@@ -76,7 +85,10 @@ export interface DraftKind<
|
|
|
76
85
|
TResultSchema extends ZodType | undefined = ZodType | undefined,
|
|
77
86
|
> extends HandleKindBase<TSchema> {
|
|
78
87
|
readonly type: "draft";
|
|
79
|
-
|
|
88
|
+
/** `"bound"` (default): connection-scoped, 2 words. `"public"`: provider-scoped, 4-5 words. */
|
|
89
|
+
readonly access: HandleAccess;
|
|
90
|
+
/** Public only; `"high"` forces 5 words. Always `"standard"` for bound drafts. */
|
|
91
|
+
readonly strength: HandleStrength;
|
|
80
92
|
readonly ttl: {
|
|
81
93
|
readonly idle: ProviderStateDurationString;
|
|
82
94
|
readonly max: ProviderStateDurationString;
|
|
@@ -87,7 +99,6 @@ export interface DraftKind<
|
|
|
87
99
|
readonly resultTtlMs: number;
|
|
88
100
|
/** Optional schema describing the commit result; type-level only at runtime. */
|
|
89
101
|
readonly result: TResultSchema | undefined;
|
|
90
|
-
readonly wordCount: 2;
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
// `any` here is variance-only: a concrete `CursorKind<{ page: number }>` must be
|
|
@@ -123,6 +134,12 @@ export interface HandleCommitResult<K extends HandleKind> {
|
|
|
123
134
|
export interface HandleContext {
|
|
124
135
|
/** Validates `data` with `kind.schema`, stores it, and returns the canonical handle. */
|
|
125
136
|
create<K extends HandleKind>(kind: K, data: InputOf<K>): Promise<string>;
|
|
137
|
+
/**
|
|
138
|
+
* Same as `create` but returns the full record (`handle`, `status`, `data`,
|
|
139
|
+
* `createdAt`, `expiresAt`) so a provider can expose `expires_at` without
|
|
140
|
+
* recomputing ttl math. `create` is the string-returning shortcut.
|
|
141
|
+
*/
|
|
142
|
+
createRecord<K extends HandleKind>(kind: K, data: InputOf<K>): Promise<HandleRecord<K>>;
|
|
126
143
|
/** Tolerantly parses `handle`, loads the record; drafts get their idle TTL touched. */
|
|
127
144
|
read<K extends HandleKind>(kind: K, handle: string): Promise<HandleRecord<K>>;
|
|
128
145
|
/** CAS update of an active draft (3 retries) with sliding TTL. */
|
|
@@ -242,10 +259,11 @@ export function isHandleError(value: unknown): value is HandleError {
|
|
|
242
259
|
*/
|
|
243
260
|
export function handleRecoverySentence(kind: {
|
|
244
261
|
readonly fieldName: string;
|
|
245
|
-
readonly issuedBy?:
|
|
262
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
246
263
|
}): string {
|
|
247
|
-
|
|
248
|
-
|
|
264
|
+
const issuers = formatHandleIssuers(kind.issuedBy);
|
|
265
|
+
if (issuers) {
|
|
266
|
+
return `Call ${issuers} again and pass the new \`${kind.fieldName}\` exactly as returned.`;
|
|
249
267
|
}
|
|
250
268
|
return `Request a new \`${kind.fieldName}\` and pass it exactly as returned.`;
|
|
251
269
|
}
|
|
@@ -311,8 +329,8 @@ export interface DefineCursorOptions<TSchema extends ZodType> {
|
|
|
311
329
|
readonly maxValueBytes?: number;
|
|
312
330
|
/** Public only; `"high"` forces 5 words. Auto-high when ttl > 1h. */
|
|
313
331
|
readonly strength?: HandleStrength;
|
|
314
|
-
/** Operation key that
|
|
315
|
-
readonly issuedBy?:
|
|
332
|
+
/** Operation key(s) that issue this handle; used in error text and lint. */
|
|
333
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
316
334
|
}
|
|
317
335
|
|
|
318
336
|
export interface DefineDraftOptions<
|
|
@@ -330,11 +348,20 @@ export interface DefineDraftOptions<
|
|
|
330
348
|
};
|
|
331
349
|
/** How long a committed record stays for replay. Default "24h". */
|
|
332
350
|
readonly resultTtl?: ProviderStateDurationString;
|
|
333
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* `"bound"` (default): connection-scoped, 2 words. `"public"`: provider-scoped
|
|
353
|
+
* draft for connectionless providers (`auth: none`), 4-5 words, `maxEntries`
|
|
354
|
+
* required; every lookup failure collapses to `HANDLE_INVALID`.
|
|
355
|
+
*/
|
|
356
|
+
readonly access?: HandleAccess;
|
|
357
|
+
/** Required when public. Bound default 200 per connection. */
|
|
334
358
|
readonly maxEntries?: number;
|
|
335
359
|
/** Default 64,000. */
|
|
336
360
|
readonly maxValueBytes?: number;
|
|
337
|
-
|
|
361
|
+
/** Public only; `"high"` forces 5 words. Auto-high when ttl.max > 1h. */
|
|
362
|
+
readonly strength?: HandleStrength;
|
|
363
|
+
/** Operation key(s) that issue this handle; used in error text and lint. */
|
|
364
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
338
365
|
}
|
|
339
366
|
|
|
340
367
|
function assertKindName(name: unknown, define: string): asserts name is string {
|
|
@@ -359,6 +386,49 @@ function assertPositiveInteger(value: number, label: string, define: string): vo
|
|
|
359
386
|
}
|
|
360
387
|
}
|
|
361
388
|
|
|
389
|
+
function assertAccess(value: unknown, define: string): asserts value is HandleAccess {
|
|
390
|
+
if (value !== "bound" && value !== "public") {
|
|
391
|
+
throw new Error(`${define}: access must be "bound" or "public"; received ${String(value)}.`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function assertStrength(value: unknown, define: string): asserts value is HandleStrength {
|
|
396
|
+
if (value !== "standard" && value !== "high") {
|
|
397
|
+
throw new Error(`${define}: strength must be "standard" or "high"; received ${String(value)}.`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Validates `issuedBy` and returns a frozen copy for lists; `undefined` stays undefined. */
|
|
402
|
+
function normalizeIssuedBy(
|
|
403
|
+
value: unknown,
|
|
404
|
+
define: string,
|
|
405
|
+
name: string,
|
|
406
|
+
): HandleIssuedBy | undefined {
|
|
407
|
+
if (value === undefined) return undefined;
|
|
408
|
+
if (!isHandleIssuedBy(value) || (typeof value === "string" ? value.length === 0 : false)) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`${define}: "${name}" issuedBy must be an operation key or a non-empty list of operation keys; received ${JSON.stringify(value)}.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (typeof value === "string") return value;
|
|
414
|
+
if (value.some((operation) => operation.length === 0)) {
|
|
415
|
+
throw new Error(`${define}: "${name}" issuedBy must not contain empty operation keys.`);
|
|
416
|
+
}
|
|
417
|
+
return Object.freeze([...value]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Word count for a kind: 2 bound; public 4, or 5 when high strength or the lifetime exceeds 1h. */
|
|
421
|
+
function wordCountFor(
|
|
422
|
+
access: HandleAccess,
|
|
423
|
+
strength: HandleStrength,
|
|
424
|
+
lifetimeMs: number,
|
|
425
|
+
): HandleWordCount {
|
|
426
|
+
if (access === "bound") return BOUND_HANDLE_WORD_COUNT;
|
|
427
|
+
return strength === "high" || lifetimeMs > AUTO_HIGH_TTL_MS
|
|
428
|
+
? HIGH_PUBLIC_HANDLE_WORD_COUNT
|
|
429
|
+
: PUBLIC_HANDLE_WORD_COUNT;
|
|
430
|
+
}
|
|
431
|
+
|
|
362
432
|
function createHandleField(meta: HandleFieldMeta): ZodString {
|
|
363
433
|
const described = z.string().min(1).describe(handleFieldDescription(meta));
|
|
364
434
|
// zod v4 `.meta()` replaces the registry entry, so carry the description over.
|
|
@@ -374,17 +444,12 @@ export function defineCursor<TSchema extends ZodType>(
|
|
|
374
444
|
assertKindName(options.name, define);
|
|
375
445
|
const fieldName = options.fieldName ?? `${options.name}_token`;
|
|
376
446
|
assertFieldName(fieldName, define);
|
|
377
|
-
const access:
|
|
378
|
-
|
|
379
|
-
throw new Error(`${define}: access must be "bound" or "public"; received ${String(access)}.`);
|
|
380
|
-
}
|
|
447
|
+
const access: unknown = options.access ?? "bound";
|
|
448
|
+
assertAccess(access, define);
|
|
381
449
|
const ttlMs = parseHandleDurationMs(options.ttl, `"${options.name}" ttl`);
|
|
382
|
-
const strength:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`${define}: strength must be "standard" or "high"; received ${String(strength)}.`,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
450
|
+
const strength: unknown = options.strength ?? "standard";
|
|
451
|
+
assertStrength(strength, define);
|
|
452
|
+
const issuedBy = normalizeIssuedBy(options.issuedBy, define, options.name);
|
|
388
453
|
if (access === "public" && options.maxEntries === undefined) {
|
|
389
454
|
throw new Error(
|
|
390
455
|
`${define}: public cursor "${options.name}" must declare maxEntries (provider-wide live-entry quota).`,
|
|
@@ -394,24 +459,19 @@ export function defineCursor<TSchema extends ZodType>(
|
|
|
394
459
|
assertPositiveInteger(maxEntries, "maxEntries", define);
|
|
395
460
|
const maxValueBytes = options.maxValueBytes ?? DEFAULT_CURSOR_MAX_VALUE_BYTES;
|
|
396
461
|
assertPositiveInteger(maxValueBytes, "maxValueBytes", define);
|
|
397
|
-
const wordCount
|
|
398
|
-
access === "bound"
|
|
399
|
-
? BOUND_HANDLE_WORD_COUNT
|
|
400
|
-
: strength === "high" || ttlMs > AUTO_HIGH_TTL_MS
|
|
401
|
-
? HIGH_PUBLIC_HANDLE_WORD_COUNT
|
|
402
|
-
: PUBLIC_HANDLE_WORD_COUNT;
|
|
462
|
+
const wordCount = wordCountFor(access, strength, ttlMs);
|
|
403
463
|
const meta: HandleFieldMeta = {
|
|
404
464
|
kind: options.name,
|
|
405
465
|
type: "cursor",
|
|
406
466
|
fieldName,
|
|
407
|
-
...(
|
|
467
|
+
...(issuedBy ? { issuedBy } : {}),
|
|
408
468
|
};
|
|
409
469
|
return Object.freeze({
|
|
410
470
|
name: options.name,
|
|
411
471
|
type: "cursor",
|
|
412
472
|
fieldName,
|
|
413
473
|
access,
|
|
414
|
-
issuedBy
|
|
474
|
+
issuedBy,
|
|
415
475
|
schema: options.schema,
|
|
416
476
|
ttl: options.ttl,
|
|
417
477
|
ttlMs,
|
|
@@ -423,7 +483,11 @@ export function defineCursor<TSchema extends ZodType>(
|
|
|
423
483
|
});
|
|
424
484
|
}
|
|
425
485
|
|
|
426
|
-
/**
|
|
486
|
+
/**
|
|
487
|
+
* Defines a mutable draft kind (multi-turn forms with one-shot commit). Bound
|
|
488
|
+
* by default; `access: "public"` opts a connectionless provider into a
|
|
489
|
+
* provider-scoped draft under the public guessing analysis (ADR-0012 D3).
|
|
490
|
+
*/
|
|
427
491
|
export function defineDraft<
|
|
428
492
|
TSchema extends ZodType,
|
|
429
493
|
TResultSchema extends ZodType | undefined = undefined,
|
|
@@ -444,22 +508,40 @@ export function defineDraft<
|
|
|
444
508
|
}
|
|
445
509
|
const resultTtl = options.resultTtl ?? DEFAULT_RESULT_TTL;
|
|
446
510
|
const resultTtlMs = parseHandleDurationMs(resultTtl, `"${options.name}" resultTtl`);
|
|
511
|
+
const access: unknown = options.access ?? "bound";
|
|
512
|
+
assertAccess(access, define);
|
|
513
|
+
const strength: unknown = options.strength ?? "standard";
|
|
514
|
+
assertStrength(strength, define);
|
|
515
|
+
if (access === "bound" && strength !== "standard") {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`${define}: draft "${options.name}" strength applies to public drafts only; bound drafts are always two words.`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const issuedBy = normalizeIssuedBy(options.issuedBy, define, options.name);
|
|
521
|
+
if (access === "public" && options.maxEntries === undefined) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`${define}: public draft "${options.name}" must declare maxEntries (provider-wide live-entry quota).`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
447
526
|
const maxEntries = options.maxEntries ?? DEFAULT_BOUND_MAX_ENTRIES;
|
|
448
527
|
assertPositiveInteger(maxEntries, "maxEntries", define);
|
|
449
528
|
const maxValueBytes = options.maxValueBytes ?? DEFAULT_DRAFT_MAX_VALUE_BYTES;
|
|
450
529
|
assertPositiveInteger(maxValueBytes, "maxValueBytes", define);
|
|
530
|
+
// A public draft's guessable lifetime is its hard `max` ttl (idle only shortens it).
|
|
531
|
+
const wordCount = wordCountFor(access, strength, maxTtlMs);
|
|
451
532
|
const meta: HandleFieldMeta = {
|
|
452
533
|
kind: options.name,
|
|
453
534
|
type: "draft",
|
|
454
535
|
fieldName,
|
|
455
|
-
...(
|
|
536
|
+
...(issuedBy ? { issuedBy } : {}),
|
|
456
537
|
};
|
|
457
538
|
return Object.freeze({
|
|
458
539
|
name: options.name,
|
|
459
540
|
type: "draft",
|
|
460
541
|
fieldName,
|
|
461
|
-
access
|
|
462
|
-
|
|
542
|
+
access,
|
|
543
|
+
strength,
|
|
544
|
+
issuedBy,
|
|
463
545
|
schema: options.schema,
|
|
464
546
|
result: options.result,
|
|
465
547
|
ttl: options.ttl,
|
|
@@ -469,7 +551,7 @@ export function defineDraft<
|
|
|
469
551
|
resultTtlMs,
|
|
470
552
|
maxEntries,
|
|
471
553
|
maxValueBytes,
|
|
472
|
-
wordCount
|
|
554
|
+
wordCount,
|
|
473
555
|
field: () => createHandleField(meta),
|
|
474
556
|
});
|
|
475
557
|
}
|
|
@@ -491,8 +573,8 @@ export interface PickOptions<T> {
|
|
|
491
573
|
readonly field: string;
|
|
492
574
|
/** Item property compared against `value`. Default = `field`. */
|
|
493
575
|
readonly by?: keyof T & string;
|
|
494
|
-
/** Operation that listed the items; used in the error message when known. */
|
|
495
|
-
readonly issuedBy?:
|
|
576
|
+
/** Operation(s) that listed the items; used in the error message when known. */
|
|
577
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
496
578
|
}
|
|
497
579
|
|
|
498
580
|
/**
|
|
@@ -518,8 +600,9 @@ export function pick<T extends Record<string, unknown>>(
|
|
|
518
600
|
const loose = items.filter((item) => keyOf(item).trim().toLowerCase() === folded);
|
|
519
601
|
if (loose.length === 1) return loose[0]!;
|
|
520
602
|
const allowed = items.map(keyOf);
|
|
521
|
-
const
|
|
522
|
-
|
|
603
|
+
const issuers = formatHandleIssuers(options.issuedBy);
|
|
604
|
+
const listed = issuers
|
|
605
|
+
? ` Use the values exactly as listed by ${issuers}.`
|
|
523
606
|
: " Use the values exactly as listed.";
|
|
524
607
|
throw new HandleError(
|
|
525
608
|
"PICK_NOT_OFFERED",
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { sanitizeDiagnosticText } from "../fixture-sanitization.js";
|
|
2
|
+
import type { ProviderLocaleCatalogMap } from "./catalog.js";
|
|
3
|
+
import {
|
|
4
|
+
getProviderLocaleSegments,
|
|
5
|
+
isProviderLocaleKey,
|
|
6
|
+
type ProviderLocale,
|
|
7
|
+
type ProviderLocaleValue,
|
|
8
|
+
} from "./keys.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Values interpolated into a localized provider error message or fix.
|
|
12
|
+
*
|
|
13
|
+
* Strings and finite numbers only. String values are treated as untrusted
|
|
14
|
+
* (upstream bodies frequently end up here), so they are scrubbed and capped by
|
|
15
|
+
* {@link interpolateProviderErrorText} before substitution.
|
|
16
|
+
*/
|
|
17
|
+
export type ProviderErrorMessageParams = Readonly<Record<string, string | number>>;
|
|
18
|
+
|
|
19
|
+
/** Locales the SDK negotiates for client-facing provider text. */
|
|
20
|
+
export const PROVIDER_ERROR_LOCALES = ["en", "ko", "ja"] as const;
|
|
21
|
+
|
|
22
|
+
/** Locale used when the caller asks for nothing the provider catalog covers. */
|
|
23
|
+
export const DEFAULT_PROVIDER_ERROR_LOCALE = "en";
|
|
24
|
+
|
|
25
|
+
/** Catalog namespace for derived `errors.<code>.message` / `errors.<code>.fix` lookups. */
|
|
26
|
+
export const PROVIDER_ERROR_CATALOG_NAMESPACE = "errors";
|
|
27
|
+
|
|
28
|
+
/** Per-value cap applied to every interpolated param, before substitution. */
|
|
29
|
+
export const MAX_INTERPOLATED_PARAM_LENGTH = 200;
|
|
30
|
+
|
|
31
|
+
/** Cap applied to a rendered localized message or fix. */
|
|
32
|
+
export const MAX_LOCALIZED_ERROR_TEXT_LENGTH = 2_000;
|
|
33
|
+
|
|
34
|
+
const TRUNCATION_SUFFIX = "…";
|
|
35
|
+
|
|
36
|
+
// One pass, three alternatives: `{{` and `}}` are the literal-brace escapes and
|
|
37
|
+
// `{name}` is a substitution. Named groups are deliberately not used so the
|
|
38
|
+
// callback can distinguish an escape from a substitution by the matched text.
|
|
39
|
+
const PLACEHOLDER_PATTERN = /\{\{|\}\}|\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
40
|
+
|
|
41
|
+
// Markup openers are removed outright rather than entity-escaped: the envelope
|
|
42
|
+
// is plain text, so an escaped `<` would be as wrong for a terminal as a raw
|
|
43
|
+
// `<` is for an HTML surface. The `&` rule only fires in front of an
|
|
44
|
+
// entity-shaped run, so ordinary prose ("Tom & Jerry") survives while
|
|
45
|
+
// `<script>` cannot be revived by a consumer that decodes entities.
|
|
46
|
+
const MARKUP_OPENER_PATTERN = /[<>`]/g;
|
|
47
|
+
const HTML_ENTITY_AMPERSAND_PATTERN = /&(?=#?[0-9A-Za-z]{1,32};)/g;
|
|
48
|
+
|
|
49
|
+
function isProviderErrorLocale(value: string | undefined): value is ProviderLocale {
|
|
50
|
+
return PROVIDER_ERROR_LOCALES.some((locale) => locale === value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type RankedLanguageRange = {
|
|
54
|
+
readonly primary: string;
|
|
55
|
+
readonly quality: number;
|
|
56
|
+
readonly position: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Negotiates an `Accept-Language` header against {@link PROVIDER_ERROR_LOCALES}.
|
|
61
|
+
*
|
|
62
|
+
* Quality values are honoured (`ko;q=0.1, ja;q=0.9` selects `ja`) and `q=0`
|
|
63
|
+
* means "not acceptable". Ranges are matched on the primary subtag, so `ko-KR`
|
|
64
|
+
* selects `ko`. Returns `undefined` when the header is absent or names no
|
|
65
|
+
* supported locale, so callers can consult a lower-precedence source before
|
|
66
|
+
* defaulting.
|
|
67
|
+
*/
|
|
68
|
+
export function providerLocaleFromAcceptLanguage(
|
|
69
|
+
header: string | null | undefined,
|
|
70
|
+
): ProviderLocale | undefined {
|
|
71
|
+
if (typeof header !== "string" || header.length === 0) return undefined;
|
|
72
|
+
const ranges: RankedLanguageRange[] = [];
|
|
73
|
+
for (const [position, token] of header.split(",").entries()) {
|
|
74
|
+
const [rangeText, ...parameters] = token.split(";");
|
|
75
|
+
const primary = rangeText?.trim().split("-")[0]?.toLowerCase();
|
|
76
|
+
if (!primary) continue;
|
|
77
|
+
const quality = parseQualityValue(parameters);
|
|
78
|
+
if (quality <= 0) continue;
|
|
79
|
+
ranges.push({ primary, quality, position });
|
|
80
|
+
}
|
|
81
|
+
// Stable by construction: equal qualities keep header order.
|
|
82
|
+
ranges.sort((left, right) =>
|
|
83
|
+
left.quality === right.quality ? left.position - right.position : right.quality - left.quality,
|
|
84
|
+
);
|
|
85
|
+
for (const range of ranges) {
|
|
86
|
+
if (isProviderErrorLocale(range.primary)) return range.primary;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseQualityValue(parameters: readonly string[]): number {
|
|
92
|
+
for (const parameter of parameters) {
|
|
93
|
+
const [name, value] = parameter.split("=");
|
|
94
|
+
if (name?.trim().toLowerCase() !== "q") continue;
|
|
95
|
+
const quality = Number.parseFloat(value?.trim() ?? "");
|
|
96
|
+
// A malformed `q` is not a rejection; RFC 9110 treats the range as
|
|
97
|
+
// fully acceptable when no usable quality is present.
|
|
98
|
+
if (!Number.isFinite(quality)) return 1;
|
|
99
|
+
return Math.min(Math.max(quality, 0), 1);
|
|
100
|
+
}
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Reads `accept-language` from a case-insensitive header record. */
|
|
105
|
+
export function acceptLanguageHeaderValue(
|
|
106
|
+
headers: Readonly<Record<string, unknown>> | undefined,
|
|
107
|
+
): string | undefined {
|
|
108
|
+
if (!headers) return undefined;
|
|
109
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
110
|
+
if (name.toLowerCase() === "accept-language" && typeof value === "string") return value;
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Picks the first supported locale across header sources, in precedence order,
|
|
117
|
+
* falling back to {@link DEFAULT_PROVIDER_ERROR_LOCALE}.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveProviderErrorLocale(
|
|
120
|
+
...headerValues: readonly (string | null | undefined)[]
|
|
121
|
+
): ProviderLocale {
|
|
122
|
+
for (const value of headerValues) {
|
|
123
|
+
const locale = providerLocaleFromAcceptLanguage(value);
|
|
124
|
+
if (locale) return locale;
|
|
125
|
+
}
|
|
126
|
+
return DEFAULT_PROVIDER_ERROR_LOCALE;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function truncateText(value: string, limit: number): string {
|
|
130
|
+
return value.length <= limit ? value : `${value.slice(0, limit)}${TRUNCATION_SUFFIX}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Renders one param value as substitution text, or `undefined` when the value
|
|
135
|
+
* cannot be substituted (absent, non-finite, or not a string/number).
|
|
136
|
+
*/
|
|
137
|
+
export function interpolatedParamText(value: unknown): string | undefined {
|
|
138
|
+
if (typeof value === "number") {
|
|
139
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
140
|
+
}
|
|
141
|
+
if (typeof value !== "string") return undefined;
|
|
142
|
+
// sanitizeDiagnosticText scrubs credentials and encodes control, bidi and
|
|
143
|
+
// line-separator characters; the markup pass closes the HTML/markdown hole
|
|
144
|
+
// it deliberately leaves open for log text.
|
|
145
|
+
const scrubbed = sanitizeDiagnosticText(value)
|
|
146
|
+
.replace(MARKUP_OPENER_PATTERN, "")
|
|
147
|
+
.replace(HTML_ENTITY_AMPERSAND_PATTERN, "")
|
|
148
|
+
.replace(/\s+/g, " ")
|
|
149
|
+
.trim();
|
|
150
|
+
return truncateText(scrubbed, MAX_INTERPOLATED_PARAM_LENGTH);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Substitutes `{name}` placeholders in a catalog template.
|
|
155
|
+
*
|
|
156
|
+
* - `{{` and `}}` are the literal `{` and `}` escapes.
|
|
157
|
+
* - A placeholder with no usable param is left verbatim, so an authoring gap is
|
|
158
|
+
* visible in the response instead of silently deleting text.
|
|
159
|
+
* - Substituted text is never rescanned, so a param value containing `{other}`
|
|
160
|
+
* cannot trigger a second round of interpolation.
|
|
161
|
+
*/
|
|
162
|
+
export function interpolateProviderErrorText(
|
|
163
|
+
template: string,
|
|
164
|
+
params?: ProviderErrorMessageParams,
|
|
165
|
+
): string {
|
|
166
|
+
const rendered = template.replace(PLACEHOLDER_PATTERN, (match, name?: string) => {
|
|
167
|
+
if (match === "{{") return "{";
|
|
168
|
+
if (match === "}}") return "}";
|
|
169
|
+
if (name === undefined) return match;
|
|
170
|
+
const substitution = interpolatedParamText(
|
|
171
|
+
params && Object.hasOwn(params, name) ? params[name] : undefined,
|
|
172
|
+
);
|
|
173
|
+
return substitution ?? match;
|
|
174
|
+
});
|
|
175
|
+
return truncateText(rendered, MAX_LOCALIZED_ERROR_TEXT_LENGTH);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function asCatalogText(value: ProviderLocaleValue | undefined): string | undefined {
|
|
179
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readCatalogSegments(
|
|
183
|
+
catalogs: ProviderLocaleCatalogMap,
|
|
184
|
+
segments: readonly string[],
|
|
185
|
+
locale: ProviderLocale,
|
|
186
|
+
fallbackLocale: ProviderLocale,
|
|
187
|
+
): string | undefined {
|
|
188
|
+
return (
|
|
189
|
+
asCatalogText(getProviderLocaleSegments(catalogs[locale] ?? {}, segments)) ??
|
|
190
|
+
asCatalogText(getProviderLocaleSegments(catalogs[fallbackLocale] ?? {}, segments))
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A key candidate considered by {@link localizeProviderErrorText}, in priority order. */
|
|
195
|
+
export type ProviderErrorTextCandidate =
|
|
196
|
+
/** An explicit dot-path key from a throw site or an `errorCodes[]` declaration. */
|
|
197
|
+
| { readonly kind: "key"; readonly key: string | undefined }
|
|
198
|
+
/** The `errors.<code>.<field>` convention; `code` is used verbatim as a segment. */
|
|
199
|
+
| { readonly kind: "derived"; readonly code: string | undefined; readonly field: string };
|
|
200
|
+
|
|
201
|
+
export interface LocalizeProviderErrorTextOptions {
|
|
202
|
+
readonly catalogs?: ProviderLocaleCatalogMap;
|
|
203
|
+
readonly locale: ProviderLocale;
|
|
204
|
+
readonly fallbackLocale?: ProviderLocale;
|
|
205
|
+
readonly candidates: readonly ProviderErrorTextCandidate[];
|
|
206
|
+
readonly params?: ProviderErrorMessageParams;
|
|
207
|
+
/** Literal text returned when no candidate resolves. May be `undefined` for `fix`. */
|
|
208
|
+
readonly fallback: string | undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Resolves client-facing error text against the provider locale catalogs.
|
|
213
|
+
*
|
|
214
|
+
* Candidates are tried in order and each one is resolved in the caller's locale
|
|
215
|
+
* before the `en` catalog, so a more specific key always beats a more specific
|
|
216
|
+
* locale. Nothing here throws: a malformed key, an absent catalog, a
|
|
217
|
+
* non-string catalog value and a missing key all fall through to the next
|
|
218
|
+
* candidate and ultimately to `fallback`.
|
|
219
|
+
*/
|
|
220
|
+
export function localizeProviderErrorText(
|
|
221
|
+
options: LocalizeProviderErrorTextOptions,
|
|
222
|
+
): string | undefined {
|
|
223
|
+
const { catalogs } = options;
|
|
224
|
+
if (!catalogs) return options.fallback;
|
|
225
|
+
const fallbackLocale = options.fallbackLocale ?? DEFAULT_PROVIDER_ERROR_LOCALE;
|
|
226
|
+
for (const candidate of options.candidates) {
|
|
227
|
+
const segments = candidateSegments(candidate);
|
|
228
|
+
if (!segments) continue;
|
|
229
|
+
try {
|
|
230
|
+
const template = readCatalogSegments(catalogs, segments, options.locale, fallbackLocale);
|
|
231
|
+
if (template === undefined) continue;
|
|
232
|
+
return interpolateProviderErrorText(template, options.params);
|
|
233
|
+
} catch {
|
|
234
|
+
// A hostile catalog or params object (throwing getters, exotic proxies)
|
|
235
|
+
// must not turn a provider error into a masked 500. Fall through to the
|
|
236
|
+
// next candidate and ultimately to the English literal.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return options.fallback;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function candidateSegments(candidate: ProviderErrorTextCandidate): readonly string[] | undefined {
|
|
243
|
+
if (candidate.kind === "key") {
|
|
244
|
+
// Explicit keys must satisfy the dot-path grammar; a malformed key is a
|
|
245
|
+
// miss, never a throw at envelope time.
|
|
246
|
+
return isProviderLocaleKey(candidate.key) ? candidate.key.split(".") : undefined;
|
|
247
|
+
}
|
|
248
|
+
if (typeof candidate.code !== "string" || candidate.code.length === 0) return undefined;
|
|
249
|
+
return [PROVIDER_ERROR_CATALOG_NAMESPACE, candidate.code, candidate.field];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The `errors.<code>.<field>` dot path this SDK derives for an undeclared key. */
|
|
253
|
+
export function derivedProviderErrorCatalogPath(code: string, field: "message" | "fix"): string {
|
|
254
|
+
return `${PROVIDER_ERROR_CATALOG_NAMESPACE}.${code}.${field}`;
|
|
255
|
+
}
|
package/src/i18n/index.ts
CHANGED
package/src/i18n/keys.ts
CHANGED
|
@@ -2,8 +2,55 @@ import type { ProviderLocaleKey } from "../types.js";
|
|
|
2
2
|
|
|
3
3
|
export type { ProviderLocale, ProviderLocaleKey } from "../types.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* An ordinary catalog name segment: camelCase, ASCII only.
|
|
7
|
+
*
|
|
8
|
+
* `[A-Z][a-z0-9]+` forces at least one character after each hump, so a
|
|
9
|
+
* screaming acronym (`errorsURL`) is not silently read as camelCase.
|
|
10
|
+
*/
|
|
11
|
+
const NAME_SEGMENT_RE = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*$/;
|
|
12
|
+
|
|
13
|
+
/** An array-index segment, e.g. the `0` in `meta.tags.0`. */
|
|
14
|
+
const INDEX_SEGMENT_RE = /^[0-9]+$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An error-code segment: the thrown `code` used verbatim as a catalog key.
|
|
18
|
+
*
|
|
19
|
+
* Single-case underscore words only — `UPSTREAM_SCHEMA_ERROR`, `BLOCKED`,
|
|
20
|
+
* `reauth_required` — matching the shape the SDK registers and the fleet
|
|
21
|
+
* throws (a census of 91 provider repos found 224 distinct codes: 213
|
|
22
|
+
* SCREAMING_SNAKE, 11 lower_snake, none mixed-case, none with a leading digit).
|
|
23
|
+
* A leading letter is mandatory, so no error code can be spelled like the
|
|
24
|
+
* numeric array-index form and the two segment kinds stay disjoint.
|
|
25
|
+
* The lower_snake arm requires an underscore because a bare lowercase word is
|
|
26
|
+
* already a {@link NAME_SEGMENT_RE} segment.
|
|
27
|
+
*/
|
|
28
|
+
const ERROR_CODE_SEGMENT_RE = /^(?:[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z][a-z0-9]*(?:_[a-z0-9]+)+)$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The only parent under which an error-code segment is legal.
|
|
32
|
+
*
|
|
33
|
+
* Must stay equal to `PROVIDER_ERROR_CATALOG_NAMESPACE` in `error-messages.ts`
|
|
34
|
+
* (the derived-path test in `keys.test.ts` fails if they drift). Anchoring the
|
|
35
|
+
* code vocabulary to this one parent is what keeps the grammar from degrading
|
|
36
|
+
* into "any identifier anywhere": everywhere else a segment is still camelCase
|
|
37
|
+
* or an array index.
|
|
38
|
+
*/
|
|
39
|
+
const ERROR_CODE_PARENT_SEGMENT = "errors";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates one segment in position.
|
|
43
|
+
*
|
|
44
|
+
* `parent` is the preceding segment, or `undefined` for the first segment. The
|
|
45
|
+
* first segment is always a plain name: catalogs are rooted in SDK-owned
|
|
46
|
+
* namespaces (`meta`, `operations`, `auth`, `errors`), never in an index or a
|
|
47
|
+
* code.
|
|
48
|
+
*/
|
|
49
|
+
function isValidProviderLocaleSegment(segment: string, parent: string | undefined): boolean {
|
|
50
|
+
if (parent === undefined) return NAME_SEGMENT_RE.test(segment);
|
|
51
|
+
if (NAME_SEGMENT_RE.test(segment) || INDEX_SEGMENT_RE.test(segment)) return true;
|
|
52
|
+
return parent === ERROR_CODE_PARENT_SEGMENT && ERROR_CODE_SEGMENT_RE.test(segment);
|
|
53
|
+
}
|
|
7
54
|
|
|
8
55
|
export function providerLocaleKey(key: string): ProviderLocaleKey {
|
|
9
56
|
assertProviderLocaleKey(key);
|
|
@@ -11,13 +58,21 @@ export function providerLocaleKey(key: string): ProviderLocaleKey {
|
|
|
11
58
|
}
|
|
12
59
|
|
|
13
60
|
export function isProviderLocaleKey(value: unknown): value is ProviderLocaleKey {
|
|
14
|
-
|
|
61
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
62
|
+
const segments = value.split(".");
|
|
63
|
+
for (const [index, segment] of segments.entries()) {
|
|
64
|
+
// `split(".")` yields "" for an empty segment ("a..b", ".a", "a."), which
|
|
65
|
+
// no segment pattern matches, so those stay rejected.
|
|
66
|
+
if (!isValidProviderLocaleSegment(segment, index === 0 ? undefined : segments[index - 1]))
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
15
70
|
}
|
|
16
71
|
|
|
17
72
|
export function assertProviderLocaleKey(value: unknown): asserts value is ProviderLocaleKey {
|
|
18
73
|
if (!isProviderLocaleKey(value)) {
|
|
19
74
|
throw new Error(
|
|
20
|
-
`Provider locale key must be a dot path such as "meta.description" or "operations.search.description"; received ${JSON.stringify(value)}`,
|
|
75
|
+
`Provider locale key must be a camelCase dot path such as "meta.description" or "operations.search.description", optionally naming an error code under "${ERROR_CODE_PARENT_SEGMENT}." as in "errors.UPSTREAM_SCHEMA_ERROR.message"; received ${JSON.stringify(value)}`,
|
|
21
76
|
);
|
|
22
77
|
}
|
|
23
78
|
}
|
|
@@ -35,9 +90,28 @@ export function getProviderLocalePath(
|
|
|
35
90
|
key: ProviderLocaleKey | string,
|
|
36
91
|
): ProviderLocaleValue | undefined {
|
|
37
92
|
assertProviderLocaleKey(key);
|
|
93
|
+
return getProviderLocaleSegments(catalog, key.split("."));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reads a catalog path from raw segments, bypassing the dot-path grammar.
|
|
98
|
+
*
|
|
99
|
+
* Derived error-message lookups address the catalog by the thrown error code
|
|
100
|
+
* (`errors.<code>.message`). The grammar now accepts the code shapes the SDK
|
|
101
|
+
* registers and the fleet throws, but `code` is provider data: a provider may
|
|
102
|
+
* throw anything, and an unusual code must still resolve its catalog entry
|
|
103
|
+
* rather than be dropped as malformed. Own properties only: a
|
|
104
|
+
* `__proto__`/`constructor` segment must read as a miss, not as the prototype
|
|
105
|
+
* chain.
|
|
106
|
+
*/
|
|
107
|
+
export function getProviderLocaleSegments(
|
|
108
|
+
catalog: ProviderLocaleCatalog,
|
|
109
|
+
segments: readonly string[],
|
|
110
|
+
): ProviderLocaleValue | undefined {
|
|
111
|
+
if (segments.length === 0) return undefined;
|
|
38
112
|
let cursor: unknown = catalog;
|
|
39
|
-
for (const segment of
|
|
40
|
-
if (!isRecord(cursor)) return undefined;
|
|
113
|
+
for (const segment of segments) {
|
|
114
|
+
if (!isRecord(cursor) || !Object.hasOwn(cursor, segment)) return undefined;
|
|
41
115
|
cursor = cursor[segment];
|
|
42
116
|
}
|
|
43
117
|
return isProviderLocaleValue(cursor) ? cursor : undefined;
|