@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.
Files changed (258) hide show
  1. package/AUTHORING.md +268 -9
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +1 -0
  4. package/bin/apifuse-check.ts +84 -2
  5. package/bin/apifuse-dev.ts +11 -31
  6. package/bin/apifuse-migrate-deployment.ts +200 -0
  7. package/bin/apifuse-pack-check.ts +11 -4
  8. package/bin/apifuse-pack-types.ts +191 -4
  9. package/bin/apifuse-record.ts +44 -32
  10. package/bin/apifuse-test.ts +3 -1
  11. package/dist/cli/commands.d.ts +1 -1
  12. package/dist/cli/commands.js +11 -0
  13. package/dist/cli/deployment-intent.d.ts +178 -0
  14. package/dist/cli/deployment-intent.js +277 -0
  15. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  16. package/dist/cli/migrate-deployment-intent.js +1552 -0
  17. package/dist/cli/migrate-provider-shape.d.ts +5 -1
  18. package/dist/cli/migrate-provider-shape.js +4 -2
  19. package/dist/cli/resolver-runtime.d.ts +21 -0
  20. package/dist/cli/resolver-runtime.js +31 -0
  21. package/dist/define.d.ts +3 -0
  22. package/dist/define.js +5 -0
  23. package/dist/dev.d.ts +3 -1
  24. package/dist/dev.js +6 -1
  25. package/dist/engine.d.ts +35 -1
  26. package/dist/engine.js +21 -2
  27. package/dist/error-resolution.d.ts +1 -0
  28. package/dist/error-resolution.js +54 -0
  29. package/dist/errors.d.ts +39 -1
  30. package/dist/errors.js +50 -0
  31. package/dist/handle-meta.d.ts +18 -2
  32. package/dist/handle-meta.js +30 -2
  33. package/dist/handle.d.ts +33 -12
  34. package/dist/handle.js +69 -24
  35. package/dist/i18n/error-messages.d.ts +86 -0
  36. package/dist/i18n/error-messages.js +192 -0
  37. package/dist/i18n/index.d.ts +1 -0
  38. package/dist/i18n/index.js +1 -0
  39. package/dist/i18n/keys.d.ts +12 -0
  40. package/dist/i18n/keys.js +76 -5
  41. package/dist/index.d.ts +10 -2
  42. package/dist/index.js +9 -1
  43. package/dist/lint.d.ts +10 -0
  44. package/dist/lint.js +440 -57
  45. package/dist/provider.d.ts +7 -5
  46. package/dist/provider.js +4 -2
  47. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  48. package/dist/runtime/browser-telemetry-binding.js +83 -0
  49. package/dist/runtime/browser-telemetry.d.ts +76 -0
  50. package/dist/runtime/browser-telemetry.js +165 -0
  51. package/dist/runtime/browser.d.ts +3 -0
  52. package/dist/runtime/browser.js +208 -47
  53. package/dist/runtime/cache-telemetry.d.ts +71 -0
  54. package/dist/runtime/cache-telemetry.js +177 -0
  55. package/dist/runtime/cache.js +67 -7
  56. package/dist/runtime/capability-response.d.ts +3 -0
  57. package/dist/runtime/capability-response.js +56 -0
  58. package/dist/runtime/capability-telemetry.d.ts +46 -0
  59. package/dist/runtime/capability-telemetry.js +68 -0
  60. package/dist/runtime/diagnostic-env.d.ts +3 -0
  61. package/dist/runtime/diagnostic-env.js +10 -1
  62. package/dist/runtime/engine-mode.d.ts +74 -0
  63. package/dist/runtime/engine-mode.js +152 -0
  64. package/dist/runtime/handle.js +15 -10
  65. package/dist/runtime/http-telemetry-binding.d.ts +10 -0
  66. package/dist/runtime/http-telemetry-binding.js +152 -0
  67. package/dist/runtime/http-telemetry-guard.d.ts +16 -0
  68. package/dist/runtime/http-telemetry-guard.js +202 -0
  69. package/dist/runtime/http-telemetry.d.ts +104 -0
  70. package/dist/runtime/http-telemetry.js +190 -0
  71. package/dist/runtime/http.d.ts +31 -0
  72. package/dist/runtime/http.js +356 -174
  73. package/dist/runtime/instrumentation.js +92 -13
  74. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  75. package/dist/runtime/ocr-telemetry.js +192 -0
  76. package/dist/runtime/ocr.js +30 -8
  77. package/dist/runtime/request-telemetry.d.ts +2 -0
  78. package/dist/runtime/request-telemetry.js +2 -1
  79. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  80. package/dist/runtime/resolver-runtime-options.js +33 -0
  81. package/dist/runtime/resolver-shared.d.ts +3 -2
  82. package/dist/runtime/resolver-shared.js +4 -4
  83. package/dist/runtime/resolver-telemetry.d.ts +1 -0
  84. package/dist/runtime/resolver-telemetry.js +1 -1
  85. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  86. package/dist/runtime/resolver-vendors/browser.js +46 -0
  87. package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
  88. package/dist/runtime/resolver-vendors/hosts.js +35 -0
  89. package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
  90. package/dist/runtime/resolver-vendors/types.d.ts +5 -4
  91. package/dist/runtime/resolver.d.ts +4 -1
  92. package/dist/runtime/resolver.js +46 -13
  93. package/dist/runtime/state-telemetry.d.ts +79 -0
  94. package/dist/runtime/state-telemetry.js +194 -0
  95. package/dist/runtime/state.d.ts +3 -0
  96. package/dist/runtime/state.js +26 -7
  97. package/dist/runtime/stealth-cookies.d.ts +5 -1
  98. package/dist/runtime/stealth-cookies.js +21 -1
  99. package/dist/runtime/stealth-owned-headers.d.ts +26 -0
  100. package/dist/runtime/stealth-owned-headers.js +37 -0
  101. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  102. package/dist/runtime/stealth-telemetry.js +179 -0
  103. package/dist/runtime/stealth.d.ts +4 -1
  104. package/dist/runtime/stealth.js +228 -85
  105. package/dist/runtime/stt-telemetry.d.ts +77 -0
  106. package/dist/runtime/stt-telemetry.js +176 -0
  107. package/dist/runtime/stt.js +15 -6
  108. package/dist/server/error-stack-frames.d.ts +35 -0
  109. package/dist/server/error-stack-frames.js +219 -0
  110. package/dist/server/index.d.ts +9 -0
  111. package/dist/server/index.js +5 -0
  112. package/dist/server/sdk-version.d.ts +3 -0
  113. package/dist/server/sdk-version.js +19 -0
  114. package/dist/server/self-test.js +1 -18
  115. package/dist/server/sensitive-values.d.ts +1 -1
  116. package/dist/server/sensitive-values.js +9 -0
  117. package/dist/server/serve-implementation.d.ts +53 -2
  118. package/dist/server/serve-implementation.js +369 -123
  119. package/dist/server/types.d.ts +1 -0
  120. package/dist/server/types.js +7 -0
  121. package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
  122. package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
  123. package/dist/stateful/stateful-provider-runtime-context.js +1 -0
  124. package/dist/types.d.ts +94 -1
  125. package/package.json +6 -1
  126. package/src/cli/commands.ts +14 -0
  127. package/src/cli/deployment-intent.ts +364 -0
  128. package/src/cli/migrate-deployment-intent.ts +1908 -0
  129. package/src/cli/migrate-provider-shape.ts +10 -4
  130. package/src/cli/resolver-runtime.ts +72 -0
  131. package/src/define.ts +13 -0
  132. package/src/dev.ts +9 -2
  133. package/src/engine.ts +38 -2
  134. package/src/error-resolution.ts +58 -0
  135. package/src/errors.ts +77 -1
  136. package/src/handle-meta.ts +39 -4
  137. package/src/handle.ts +121 -38
  138. package/src/i18n/error-messages.ts +255 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/keys.ts +80 -6
  141. package/src/index.ts +91 -0
  142. package/src/lint.ts +552 -57
  143. package/src/provider.ts +20 -0
  144. package/src/runtime/browser-telemetry-binding.ts +92 -0
  145. package/src/runtime/browser-telemetry.ts +289 -0
  146. package/src/runtime/browser.ts +249 -65
  147. package/src/runtime/cache-telemetry.ts +247 -0
  148. package/src/runtime/cache.ts +68 -17
  149. package/src/runtime/capability-response.ts +53 -0
  150. package/src/runtime/capability-telemetry.ts +101 -0
  151. package/src/runtime/diagnostic-env.ts +11 -1
  152. package/src/runtime/engine-mode.ts +233 -0
  153. package/src/runtime/handle.ts +20 -10
  154. package/src/runtime/http-telemetry-binding.ts +162 -0
  155. package/src/runtime/http-telemetry-guard.ts +208 -0
  156. package/src/runtime/http-telemetry.ts +292 -0
  157. package/src/runtime/http.ts +457 -204
  158. package/src/runtime/instrumentation.ts +105 -16
  159. package/src/runtime/ocr-telemetry.ts +308 -0
  160. package/src/runtime/ocr.ts +109 -74
  161. package/src/runtime/request-telemetry.ts +2 -1
  162. package/src/runtime/resolver-runtime-options.ts +50 -0
  163. package/src/runtime/resolver-shared.ts +9 -4
  164. package/src/runtime/resolver-telemetry.ts +1 -1
  165. package/src/runtime/resolver-vendors/browser.ts +65 -7
  166. package/src/runtime/resolver-vendors/hosts.ts +43 -0
  167. package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
  168. package/src/runtime/resolver-vendors/types.ts +5 -4
  169. package/src/runtime/resolver.ts +64 -21
  170. package/src/runtime/state-telemetry.ts +298 -0
  171. package/src/runtime/state.ts +46 -45
  172. package/src/runtime/stealth-cookies.ts +24 -1
  173. package/src/runtime/stealth-owned-headers.ts +41 -0
  174. package/src/runtime/stealth-telemetry.ts +304 -0
  175. package/src/runtime/stealth.ts +355 -150
  176. package/src/runtime/stt-telemetry.ts +282 -0
  177. package/src/runtime/stt.ts +93 -71
  178. package/src/server/error-stack-frames.ts +214 -0
  179. package/src/server/index.ts +66 -0
  180. package/src/server/sdk-version.ts +22 -0
  181. package/src/server/self-test.ts +1 -21
  182. package/src/server/sensitive-values.ts +14 -0
  183. package/src/server/serve-implementation.ts +601 -157
  184. package/src/server/types.ts +7 -0
  185. package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
  186. package/src/stateful/stateful-provider-runtime-context.ts +4 -0
  187. package/src/types.ts +99 -1
  188. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
  189. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
  190. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
  191. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
  192. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
  193. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
  194. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
  195. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
  196. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
  197. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
  198. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
  199. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
  200. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
  201. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
  202. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
  203. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
  204. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
  205. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
  206. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
  207. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
  208. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
  209. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
  210. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
  211. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
  212. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
  213. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
  214. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
  215. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
  216. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
  217. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
  218. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
  219. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
  220. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
  221. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
  222. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
  246. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
  247. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
  248. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
  249. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
  250. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
  251. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
  252. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
  253. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
  254. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
  255. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
  256. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
  257. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
  258. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
package/dist/handle.js CHANGED
@@ -9,9 +9,9 @@
9
9
  */
10
10
  import { z } from "zod";
11
11
  import { brandHandleError, hasHandleErrorBrand, ProviderError, } from "./errors.js";
12
- import { APIFUSE_HANDLE_META_KEY, handleFieldDescription, HANDLE_KIND_NAME_PATTERN, } from "./handle-meta.js";
12
+ import { APIFUSE_HANDLE_META_KEY, formatHandleIssuers, handleFieldDescription, HANDLE_KIND_NAME_PATTERN, isHandleIssuedBy, } from "./handle-meta.js";
13
13
  import { BOUND_HANDLE_WORD_COUNT, HIGH_PUBLIC_HANDLE_WORD_COUNT, PUBLIC_HANDLE_WORD_COUNT, } from "./runtime/handle-wordlist.js";
14
- /** Public cursors whose ttl exceeds this are issued with five words (auto-high). */
14
+ /** Public kinds whose (max) ttl exceeds this are issued with five words (auto-high). */
15
15
  const AUTO_HIGH_TTL_MS = 3_600_000;
16
16
  const DEFAULT_BOUND_MAX_ENTRIES = 200;
17
17
  const DEFAULT_CURSOR_MAX_VALUE_BYTES = 16_000;
@@ -63,8 +63,9 @@ export function isHandleError(value) {
63
63
  * knows the next action without provider-specific wording.
64
64
  */
65
65
  export function handleRecoverySentence(kind) {
66
- if (kind.issuedBy) {
67
- return `Call \`${kind.issuedBy}\` again and pass the new \`${kind.fieldName}\` exactly as returned.`;
66
+ const issuers = formatHandleIssuers(kind.issuedBy);
67
+ if (issuers) {
68
+ return `Call ${issuers} again and pass the new \`${kind.fieldName}\` exactly as returned.`;
68
69
  }
69
70
  return `Request a new \`${kind.fieldName}\` and pass it exactly as returned.`;
70
71
  }
@@ -120,6 +121,38 @@ function assertPositiveInteger(value, label, define) {
120
121
  throw new Error(`${define}: ${label} must be a positive integer; received ${String(value)}.`);
121
122
  }
122
123
  }
124
+ function assertAccess(value, define) {
125
+ if (value !== "bound" && value !== "public") {
126
+ throw new Error(`${define}: access must be "bound" or "public"; received ${String(value)}.`);
127
+ }
128
+ }
129
+ function assertStrength(value, define) {
130
+ if (value !== "standard" && value !== "high") {
131
+ throw new Error(`${define}: strength must be "standard" or "high"; received ${String(value)}.`);
132
+ }
133
+ }
134
+ /** Validates `issuedBy` and returns a frozen copy for lists; `undefined` stays undefined. */
135
+ function normalizeIssuedBy(value, define, name) {
136
+ if (value === undefined)
137
+ return undefined;
138
+ if (!isHandleIssuedBy(value) || (typeof value === "string" ? value.length === 0 : false)) {
139
+ throw new Error(`${define}: "${name}" issuedBy must be an operation key or a non-empty list of operation keys; received ${JSON.stringify(value)}.`);
140
+ }
141
+ if (typeof value === "string")
142
+ return value;
143
+ if (value.some((operation) => operation.length === 0)) {
144
+ throw new Error(`${define}: "${name}" issuedBy must not contain empty operation keys.`);
145
+ }
146
+ return Object.freeze([...value]);
147
+ }
148
+ /** Word count for a kind: 2 bound; public 4, or 5 when high strength or the lifetime exceeds 1h. */
149
+ function wordCountFor(access, strength, lifetimeMs) {
150
+ if (access === "bound")
151
+ return BOUND_HANDLE_WORD_COUNT;
152
+ return strength === "high" || lifetimeMs > AUTO_HIGH_TTL_MS
153
+ ? HIGH_PUBLIC_HANDLE_WORD_COUNT
154
+ : PUBLIC_HANDLE_WORD_COUNT;
155
+ }
123
156
  function createHandleField(meta) {
124
157
  const described = z.string().min(1).describe(handleFieldDescription(meta));
125
158
  // zod v4 `.meta()` replaces the registry entry, so carry the description over.
@@ -133,14 +166,11 @@ export function defineCursor(options) {
133
166
  const fieldName = options.fieldName ?? `${options.name}_token`;
134
167
  assertFieldName(fieldName, define);
135
168
  const access = options.access ?? "bound";
136
- if (access !== "bound" && access !== "public") {
137
- throw new Error(`${define}: access must be "bound" or "public"; received ${String(access)}.`);
138
- }
169
+ assertAccess(access, define);
139
170
  const ttlMs = parseHandleDurationMs(options.ttl, `"${options.name}" ttl`);
140
171
  const strength = options.strength ?? "standard";
141
- if (strength !== "standard" && strength !== "high") {
142
- throw new Error(`${define}: strength must be "standard" or "high"; received ${String(strength)}.`);
143
- }
172
+ assertStrength(strength, define);
173
+ const issuedBy = normalizeIssuedBy(options.issuedBy, define, options.name);
144
174
  if (access === "public" && options.maxEntries === undefined) {
145
175
  throw new Error(`${define}: public cursor "${options.name}" must declare maxEntries (provider-wide live-entry quota).`);
146
176
  }
@@ -148,23 +178,19 @@ export function defineCursor(options) {
148
178
  assertPositiveInteger(maxEntries, "maxEntries", define);
149
179
  const maxValueBytes = options.maxValueBytes ?? DEFAULT_CURSOR_MAX_VALUE_BYTES;
150
180
  assertPositiveInteger(maxValueBytes, "maxValueBytes", define);
151
- const wordCount = access === "bound"
152
- ? BOUND_HANDLE_WORD_COUNT
153
- : strength === "high" || ttlMs > AUTO_HIGH_TTL_MS
154
- ? HIGH_PUBLIC_HANDLE_WORD_COUNT
155
- : PUBLIC_HANDLE_WORD_COUNT;
181
+ const wordCount = wordCountFor(access, strength, ttlMs);
156
182
  const meta = {
157
183
  kind: options.name,
158
184
  type: "cursor",
159
185
  fieldName,
160
- ...(options.issuedBy ? { issuedBy: options.issuedBy } : {}),
186
+ ...(issuedBy ? { issuedBy } : {}),
161
187
  };
162
188
  return Object.freeze({
163
189
  name: options.name,
164
190
  type: "cursor",
165
191
  fieldName,
166
192
  access,
167
- issuedBy: options.issuedBy,
193
+ issuedBy,
168
194
  schema: options.schema,
169
195
  ttl: options.ttl,
170
196
  ttlMs,
@@ -175,7 +201,11 @@ export function defineCursor(options) {
175
201
  field: () => createHandleField(meta),
176
202
  });
177
203
  }
178
- /** Defines a mutable, always-bound draft kind (multi-turn forms with one-shot commit). */
204
+ /**
205
+ * Defines a mutable draft kind (multi-turn forms with one-shot commit). Bound
206
+ * by default; `access: "public"` opts a connectionless provider into a
207
+ * provider-scoped draft under the public guessing analysis (ADR-0012 D3).
208
+ */
179
209
  export function defineDraft(options) {
180
210
  const define = "defineDraft";
181
211
  assertKindName(options.name, define);
@@ -191,22 +221,36 @@ export function defineDraft(options) {
191
221
  }
192
222
  const resultTtl = options.resultTtl ?? DEFAULT_RESULT_TTL;
193
223
  const resultTtlMs = parseHandleDurationMs(resultTtl, `"${options.name}" resultTtl`);
224
+ const access = options.access ?? "bound";
225
+ assertAccess(access, define);
226
+ const strength = options.strength ?? "standard";
227
+ assertStrength(strength, define);
228
+ if (access === "bound" && strength !== "standard") {
229
+ throw new Error(`${define}: draft "${options.name}" strength applies to public drafts only; bound drafts are always two words.`);
230
+ }
231
+ const issuedBy = normalizeIssuedBy(options.issuedBy, define, options.name);
232
+ if (access === "public" && options.maxEntries === undefined) {
233
+ throw new Error(`${define}: public draft "${options.name}" must declare maxEntries (provider-wide live-entry quota).`);
234
+ }
194
235
  const maxEntries = options.maxEntries ?? DEFAULT_BOUND_MAX_ENTRIES;
195
236
  assertPositiveInteger(maxEntries, "maxEntries", define);
196
237
  const maxValueBytes = options.maxValueBytes ?? DEFAULT_DRAFT_MAX_VALUE_BYTES;
197
238
  assertPositiveInteger(maxValueBytes, "maxValueBytes", define);
239
+ // A public draft's guessable lifetime is its hard `max` ttl (idle only shortens it).
240
+ const wordCount = wordCountFor(access, strength, maxTtlMs);
198
241
  const meta = {
199
242
  kind: options.name,
200
243
  type: "draft",
201
244
  fieldName,
202
- ...(options.issuedBy ? { issuedBy: options.issuedBy } : {}),
245
+ ...(issuedBy ? { issuedBy } : {}),
203
246
  };
204
247
  return Object.freeze({
205
248
  name: options.name,
206
249
  type: "draft",
207
250
  fieldName,
208
- access: "bound",
209
- issuedBy: options.issuedBy,
251
+ access,
252
+ strength,
253
+ issuedBy,
210
254
  schema: options.schema,
211
255
  result: options.result,
212
256
  ttl: options.ttl,
@@ -216,7 +260,7 @@ export function defineDraft(options) {
216
260
  resultTtlMs,
217
261
  maxEntries,
218
262
  maxValueBytes,
219
- wordCount: BOUND_HANDLE_WORD_COUNT,
263
+ wordCount,
220
264
  field: () => createHandleField(meta),
221
265
  });
222
266
  }
@@ -247,8 +291,9 @@ export function pick(items, value, options) {
247
291
  if (loose.length === 1)
248
292
  return loose[0];
249
293
  const allowed = items.map(keyOf);
250
- const listed = options.issuedBy
251
- ? ` Use the values exactly as listed by \`${options.issuedBy}\`.`
294
+ const issuers = formatHandleIssuers(options.issuedBy);
295
+ const listed = issuers
296
+ ? ` Use the values exactly as listed by ${issuers}.`
252
297
  : " Use the values exactly as listed.";
253
298
  throw new HandleError("PICK_NOT_OFFERED", `\`${options.field}\` must be one of: ${allowed.join(", ")}.${listed}`, { details: { field: options.field, allowed } });
254
299
  }
@@ -0,0 +1,86 @@
1
+ import type { ProviderLocaleCatalogMap } from "./catalog.js";
2
+ import { type ProviderLocale } from "./keys.js";
3
+ /**
4
+ * Values interpolated into a localized provider error message or fix.
5
+ *
6
+ * Strings and finite numbers only. String values are treated as untrusted
7
+ * (upstream bodies frequently end up here), so they are scrubbed and capped by
8
+ * {@link interpolateProviderErrorText} before substitution.
9
+ */
10
+ export type ProviderErrorMessageParams = Readonly<Record<string, string | number>>;
11
+ /** Locales the SDK negotiates for client-facing provider text. */
12
+ export declare const PROVIDER_ERROR_LOCALES: readonly ["en", "ko", "ja"];
13
+ /** Locale used when the caller asks for nothing the provider catalog covers. */
14
+ export declare const DEFAULT_PROVIDER_ERROR_LOCALE = "en";
15
+ /** Catalog namespace for derived `errors.<code>.message` / `errors.<code>.fix` lookups. */
16
+ export declare const PROVIDER_ERROR_CATALOG_NAMESPACE = "errors";
17
+ /** Per-value cap applied to every interpolated param, before substitution. */
18
+ export declare const MAX_INTERPOLATED_PARAM_LENGTH = 200;
19
+ /** Cap applied to a rendered localized message or fix. */
20
+ export declare const MAX_LOCALIZED_ERROR_TEXT_LENGTH = 2000;
21
+ /**
22
+ * Negotiates an `Accept-Language` header against {@link PROVIDER_ERROR_LOCALES}.
23
+ *
24
+ * Quality values are honoured (`ko;q=0.1, ja;q=0.9` selects `ja`) and `q=0`
25
+ * means "not acceptable". Ranges are matched on the primary subtag, so `ko-KR`
26
+ * selects `ko`. Returns `undefined` when the header is absent or names no
27
+ * supported locale, so callers can consult a lower-precedence source before
28
+ * defaulting.
29
+ */
30
+ export declare function providerLocaleFromAcceptLanguage(header: string | null | undefined): ProviderLocale | undefined;
31
+ /** Reads `accept-language` from a case-insensitive header record. */
32
+ export declare function acceptLanguageHeaderValue(headers: Readonly<Record<string, unknown>> | undefined): string | undefined;
33
+ /**
34
+ * Picks the first supported locale across header sources, in precedence order,
35
+ * falling back to {@link DEFAULT_PROVIDER_ERROR_LOCALE}.
36
+ */
37
+ export declare function resolveProviderErrorLocale(...headerValues: readonly (string | null | undefined)[]): ProviderLocale;
38
+ /**
39
+ * Renders one param value as substitution text, or `undefined` when the value
40
+ * cannot be substituted (absent, non-finite, or not a string/number).
41
+ */
42
+ export declare function interpolatedParamText(value: unknown): string | undefined;
43
+ /**
44
+ * Substitutes `{name}` placeholders in a catalog template.
45
+ *
46
+ * - `{{` and `}}` are the literal `{` and `}` escapes.
47
+ * - A placeholder with no usable param is left verbatim, so an authoring gap is
48
+ * visible in the response instead of silently deleting text.
49
+ * - Substituted text is never rescanned, so a param value containing `{other}`
50
+ * cannot trigger a second round of interpolation.
51
+ */
52
+ export declare function interpolateProviderErrorText(template: string, params?: ProviderErrorMessageParams): string;
53
+ /** A key candidate considered by {@link localizeProviderErrorText}, in priority order. */
54
+ export type ProviderErrorTextCandidate =
55
+ /** An explicit dot-path key from a throw site or an `errorCodes[]` declaration. */
56
+ {
57
+ readonly kind: "key";
58
+ readonly key: string | undefined;
59
+ }
60
+ /** The `errors.<code>.<field>` convention; `code` is used verbatim as a segment. */
61
+ | {
62
+ readonly kind: "derived";
63
+ readonly code: string | undefined;
64
+ readonly field: string;
65
+ };
66
+ export interface LocalizeProviderErrorTextOptions {
67
+ readonly catalogs?: ProviderLocaleCatalogMap;
68
+ readonly locale: ProviderLocale;
69
+ readonly fallbackLocale?: ProviderLocale;
70
+ readonly candidates: readonly ProviderErrorTextCandidate[];
71
+ readonly params?: ProviderErrorMessageParams;
72
+ /** Literal text returned when no candidate resolves. May be `undefined` for `fix`. */
73
+ readonly fallback: string | undefined;
74
+ }
75
+ /**
76
+ * Resolves client-facing error text against the provider locale catalogs.
77
+ *
78
+ * Candidates are tried in order and each one is resolved in the caller's locale
79
+ * before the `en` catalog, so a more specific key always beats a more specific
80
+ * locale. Nothing here throws: a malformed key, an absent catalog, a
81
+ * non-string catalog value and a missing key all fall through to the next
82
+ * candidate and ultimately to `fallback`.
83
+ */
84
+ export declare function localizeProviderErrorText(options: LocalizeProviderErrorTextOptions): string | undefined;
85
+ /** The `errors.<code>.<field>` dot path this SDK derives for an undeclared key. */
86
+ export declare function derivedProviderErrorCatalogPath(code: string, field: "message" | "fix"): string;
@@ -0,0 +1,192 @@
1
+ import { sanitizeDiagnosticText } from "../fixture-sanitization.js";
2
+ import { getProviderLocaleSegments, isProviderLocaleKey, } from "./keys.js";
3
+ /** Locales the SDK negotiates for client-facing provider text. */
4
+ export const PROVIDER_ERROR_LOCALES = ["en", "ko", "ja"];
5
+ /** Locale used when the caller asks for nothing the provider catalog covers. */
6
+ export const DEFAULT_PROVIDER_ERROR_LOCALE = "en";
7
+ /** Catalog namespace for derived `errors.<code>.message` / `errors.<code>.fix` lookups. */
8
+ export const PROVIDER_ERROR_CATALOG_NAMESPACE = "errors";
9
+ /** Per-value cap applied to every interpolated param, before substitution. */
10
+ export const MAX_INTERPOLATED_PARAM_LENGTH = 200;
11
+ /** Cap applied to a rendered localized message or fix. */
12
+ export const MAX_LOCALIZED_ERROR_TEXT_LENGTH = 2_000;
13
+ const TRUNCATION_SUFFIX = "…";
14
+ // One pass, three alternatives: `{{` and `}}` are the literal-brace escapes and
15
+ // `{name}` is a substitution. Named groups are deliberately not used so the
16
+ // callback can distinguish an escape from a substitution by the matched text.
17
+ const PLACEHOLDER_PATTERN = /\{\{|\}\}|\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
18
+ // Markup openers are removed outright rather than entity-escaped: the envelope
19
+ // is plain text, so an escaped `&lt;` would be as wrong for a terminal as a raw
20
+ // `<` is for an HTML surface. The `&` rule only fires in front of an
21
+ // entity-shaped run, so ordinary prose ("Tom & Jerry") survives while
22
+ // `&lt;script&gt;` cannot be revived by a consumer that decodes entities.
23
+ const MARKUP_OPENER_PATTERN = /[<>`]/g;
24
+ const HTML_ENTITY_AMPERSAND_PATTERN = /&(?=#?[0-9A-Za-z]{1,32};)/g;
25
+ function isProviderErrorLocale(value) {
26
+ return PROVIDER_ERROR_LOCALES.some((locale) => locale === value);
27
+ }
28
+ /**
29
+ * Negotiates an `Accept-Language` header against {@link PROVIDER_ERROR_LOCALES}.
30
+ *
31
+ * Quality values are honoured (`ko;q=0.1, ja;q=0.9` selects `ja`) and `q=0`
32
+ * means "not acceptable". Ranges are matched on the primary subtag, so `ko-KR`
33
+ * selects `ko`. Returns `undefined` when the header is absent or names no
34
+ * supported locale, so callers can consult a lower-precedence source before
35
+ * defaulting.
36
+ */
37
+ export function providerLocaleFromAcceptLanguage(header) {
38
+ if (typeof header !== "string" || header.length === 0)
39
+ return undefined;
40
+ const ranges = [];
41
+ for (const [position, token] of header.split(",").entries()) {
42
+ const [rangeText, ...parameters] = token.split(";");
43
+ const primary = rangeText?.trim().split("-")[0]?.toLowerCase();
44
+ if (!primary)
45
+ continue;
46
+ const quality = parseQualityValue(parameters);
47
+ if (quality <= 0)
48
+ continue;
49
+ ranges.push({ primary, quality, position });
50
+ }
51
+ // Stable by construction: equal qualities keep header order.
52
+ ranges.sort((left, right) => left.quality === right.quality ? left.position - right.position : right.quality - left.quality);
53
+ for (const range of ranges) {
54
+ if (isProviderErrorLocale(range.primary))
55
+ return range.primary;
56
+ }
57
+ return undefined;
58
+ }
59
+ function parseQualityValue(parameters) {
60
+ for (const parameter of parameters) {
61
+ const [name, value] = parameter.split("=");
62
+ if (name?.trim().toLowerCase() !== "q")
63
+ continue;
64
+ const quality = Number.parseFloat(value?.trim() ?? "");
65
+ // A malformed `q` is not a rejection; RFC 9110 treats the range as
66
+ // fully acceptable when no usable quality is present.
67
+ if (!Number.isFinite(quality))
68
+ return 1;
69
+ return Math.min(Math.max(quality, 0), 1);
70
+ }
71
+ return 1;
72
+ }
73
+ /** Reads `accept-language` from a case-insensitive header record. */
74
+ export function acceptLanguageHeaderValue(headers) {
75
+ if (!headers)
76
+ return undefined;
77
+ for (const [name, value] of Object.entries(headers)) {
78
+ if (name.toLowerCase() === "accept-language" && typeof value === "string")
79
+ return value;
80
+ }
81
+ return undefined;
82
+ }
83
+ /**
84
+ * Picks the first supported locale across header sources, in precedence order,
85
+ * falling back to {@link DEFAULT_PROVIDER_ERROR_LOCALE}.
86
+ */
87
+ export function resolveProviderErrorLocale(...headerValues) {
88
+ for (const value of headerValues) {
89
+ const locale = providerLocaleFromAcceptLanguage(value);
90
+ if (locale)
91
+ return locale;
92
+ }
93
+ return DEFAULT_PROVIDER_ERROR_LOCALE;
94
+ }
95
+ function truncateText(value, limit) {
96
+ return value.length <= limit ? value : `${value.slice(0, limit)}${TRUNCATION_SUFFIX}`;
97
+ }
98
+ /**
99
+ * Renders one param value as substitution text, or `undefined` when the value
100
+ * cannot be substituted (absent, non-finite, or not a string/number).
101
+ */
102
+ export function interpolatedParamText(value) {
103
+ if (typeof value === "number") {
104
+ return Number.isFinite(value) ? String(value) : undefined;
105
+ }
106
+ if (typeof value !== "string")
107
+ return undefined;
108
+ // sanitizeDiagnosticText scrubs credentials and encodes control, bidi and
109
+ // line-separator characters; the markup pass closes the HTML/markdown hole
110
+ // it deliberately leaves open for log text.
111
+ const scrubbed = sanitizeDiagnosticText(value)
112
+ .replace(MARKUP_OPENER_PATTERN, "")
113
+ .replace(HTML_ENTITY_AMPERSAND_PATTERN, "")
114
+ .replace(/\s+/g, " ")
115
+ .trim();
116
+ return truncateText(scrubbed, MAX_INTERPOLATED_PARAM_LENGTH);
117
+ }
118
+ /**
119
+ * Substitutes `{name}` placeholders in a catalog template.
120
+ *
121
+ * - `{{` and `}}` are the literal `{` and `}` escapes.
122
+ * - A placeholder with no usable param is left verbatim, so an authoring gap is
123
+ * visible in the response instead of silently deleting text.
124
+ * - Substituted text is never rescanned, so a param value containing `{other}`
125
+ * cannot trigger a second round of interpolation.
126
+ */
127
+ export function interpolateProviderErrorText(template, params) {
128
+ const rendered = template.replace(PLACEHOLDER_PATTERN, (match, name) => {
129
+ if (match === "{{")
130
+ return "{";
131
+ if (match === "}}")
132
+ return "}";
133
+ if (name === undefined)
134
+ return match;
135
+ const substitution = interpolatedParamText(params && Object.hasOwn(params, name) ? params[name] : undefined);
136
+ return substitution ?? match;
137
+ });
138
+ return truncateText(rendered, MAX_LOCALIZED_ERROR_TEXT_LENGTH);
139
+ }
140
+ function asCatalogText(value) {
141
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
142
+ }
143
+ function readCatalogSegments(catalogs, segments, locale, fallbackLocale) {
144
+ return (asCatalogText(getProviderLocaleSegments(catalogs[locale] ?? {}, segments)) ??
145
+ asCatalogText(getProviderLocaleSegments(catalogs[fallbackLocale] ?? {}, segments)));
146
+ }
147
+ /**
148
+ * Resolves client-facing error text against the provider locale catalogs.
149
+ *
150
+ * Candidates are tried in order and each one is resolved in the caller's locale
151
+ * before the `en` catalog, so a more specific key always beats a more specific
152
+ * locale. Nothing here throws: a malformed key, an absent catalog, a
153
+ * non-string catalog value and a missing key all fall through to the next
154
+ * candidate and ultimately to `fallback`.
155
+ */
156
+ export function localizeProviderErrorText(options) {
157
+ const { catalogs } = options;
158
+ if (!catalogs)
159
+ return options.fallback;
160
+ const fallbackLocale = options.fallbackLocale ?? DEFAULT_PROVIDER_ERROR_LOCALE;
161
+ for (const candidate of options.candidates) {
162
+ const segments = candidateSegments(candidate);
163
+ if (!segments)
164
+ continue;
165
+ try {
166
+ const template = readCatalogSegments(catalogs, segments, options.locale, fallbackLocale);
167
+ if (template === undefined)
168
+ continue;
169
+ return interpolateProviderErrorText(template, options.params);
170
+ }
171
+ catch {
172
+ // A hostile catalog or params object (throwing getters, exotic proxies)
173
+ // must not turn a provider error into a masked 500. Fall through to the
174
+ // next candidate and ultimately to the English literal.
175
+ }
176
+ }
177
+ return options.fallback;
178
+ }
179
+ function candidateSegments(candidate) {
180
+ if (candidate.kind === "key") {
181
+ // Explicit keys must satisfy the dot-path grammar; a malformed key is a
182
+ // miss, never a throw at envelope time.
183
+ return isProviderLocaleKey(candidate.key) ? candidate.key.split(".") : undefined;
184
+ }
185
+ if (typeof candidate.code !== "string" || candidate.code.length === 0)
186
+ return undefined;
187
+ return [PROVIDER_ERROR_CATALOG_NAMESPACE, candidate.code, candidate.field];
188
+ }
189
+ /** The `errors.<code>.<field>` dot path this SDK derives for an undeclared key. */
190
+ export function derivedProviderErrorCatalogPath(code, field) {
191
+ return `${PROVIDER_ERROR_CATALOG_NAMESPACE}.${code}.${field}`;
192
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./catalog.js";
2
+ export * from "./error-messages.js";
2
3
  export * from "./keys.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./catalog.js";
2
+ export * from "./error-messages.js";
2
3
  export * from "./keys.js";
@@ -5,6 +5,18 @@ export declare function isProviderLocaleKey(value: unknown): value is ProviderLo
5
5
  export declare function assertProviderLocaleKey(value: unknown): asserts value is ProviderLocaleKey;
6
6
  export declare function qualifyProviderLocaleKey(providerId: string, key: ProviderLocaleKey | string): string;
7
7
  export declare function getProviderLocalePath(catalog: ProviderLocaleCatalog, key: ProviderLocaleKey | string): ProviderLocaleValue | undefined;
8
+ /**
9
+ * Reads a catalog path from raw segments, bypassing the dot-path grammar.
10
+ *
11
+ * Derived error-message lookups address the catalog by the thrown error code
12
+ * (`errors.<code>.message`). The grammar now accepts the code shapes the SDK
13
+ * registers and the fleet throws, but `code` is provider data: a provider may
14
+ * throw anything, and an unusual code must still resolve its catalog entry
15
+ * rather than be dropped as malformed. Own properties only: a
16
+ * `__proto__`/`constructor` segment must read as a miss, not as the prototype
17
+ * chain.
18
+ */
19
+ export declare function getProviderLocaleSegments(catalog: ProviderLocaleCatalog, segments: readonly string[]): ProviderLocaleValue | undefined;
8
20
  export type ProviderLocaleValue = string | readonly string[];
9
21
  export type ProviderLocaleCatalog = Record<string, unknown>;
10
22
  export declare function isProviderLocaleValue(value: unknown): value is ProviderLocaleValue;
package/dist/i18n/keys.js CHANGED
@@ -1,14 +1,69 @@
1
- const PROVIDER_LOCALE_KEY_RE = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*|\.[0-9]+)*$/;
1
+ /**
2
+ * An ordinary catalog name segment: camelCase, ASCII only.
3
+ *
4
+ * `[A-Z][a-z0-9]+` forces at least one character after each hump, so a
5
+ * screaming acronym (`errorsURL`) is not silently read as camelCase.
6
+ */
7
+ const NAME_SEGMENT_RE = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*$/;
8
+ /** An array-index segment, e.g. the `0` in `meta.tags.0`. */
9
+ const INDEX_SEGMENT_RE = /^[0-9]+$/;
10
+ /**
11
+ * An error-code segment: the thrown `code` used verbatim as a catalog key.
12
+ *
13
+ * Single-case underscore words only — `UPSTREAM_SCHEMA_ERROR`, `BLOCKED`,
14
+ * `reauth_required` — matching the shape the SDK registers and the fleet
15
+ * throws (a census of 91 provider repos found 224 distinct codes: 213
16
+ * SCREAMING_SNAKE, 11 lower_snake, none mixed-case, none with a leading digit).
17
+ * A leading letter is mandatory, so no error code can be spelled like the
18
+ * numeric array-index form and the two segment kinds stay disjoint.
19
+ * The lower_snake arm requires an underscore because a bare lowercase word is
20
+ * already a {@link NAME_SEGMENT_RE} segment.
21
+ */
22
+ const ERROR_CODE_SEGMENT_RE = /^(?:[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z][a-z0-9]*(?:_[a-z0-9]+)+)$/;
23
+ /**
24
+ * The only parent under which an error-code segment is legal.
25
+ *
26
+ * Must stay equal to `PROVIDER_ERROR_CATALOG_NAMESPACE` in `error-messages.ts`
27
+ * (the derived-path test in `keys.test.ts` fails if they drift). Anchoring the
28
+ * code vocabulary to this one parent is what keeps the grammar from degrading
29
+ * into "any identifier anywhere": everywhere else a segment is still camelCase
30
+ * or an array index.
31
+ */
32
+ const ERROR_CODE_PARENT_SEGMENT = "errors";
33
+ /**
34
+ * Validates one segment in position.
35
+ *
36
+ * `parent` is the preceding segment, or `undefined` for the first segment. The
37
+ * first segment is always a plain name: catalogs are rooted in SDK-owned
38
+ * namespaces (`meta`, `operations`, `auth`, `errors`), never in an index or a
39
+ * code.
40
+ */
41
+ function isValidProviderLocaleSegment(segment, parent) {
42
+ if (parent === undefined)
43
+ return NAME_SEGMENT_RE.test(segment);
44
+ if (NAME_SEGMENT_RE.test(segment) || INDEX_SEGMENT_RE.test(segment))
45
+ return true;
46
+ return parent === ERROR_CODE_PARENT_SEGMENT && ERROR_CODE_SEGMENT_RE.test(segment);
47
+ }
2
48
  export function providerLocaleKey(key) {
3
49
  assertProviderLocaleKey(key);
4
50
  return key;
5
51
  }
6
52
  export function isProviderLocaleKey(value) {
7
- return typeof value === "string" && PROVIDER_LOCALE_KEY_RE.test(value);
53
+ if (typeof value !== "string" || value.length === 0)
54
+ return false;
55
+ const segments = value.split(".");
56
+ for (const [index, segment] of segments.entries()) {
57
+ // `split(".")` yields "" for an empty segment ("a..b", ".a", "a."), which
58
+ // no segment pattern matches, so those stay rejected.
59
+ if (!isValidProviderLocaleSegment(segment, index === 0 ? undefined : segments[index - 1]))
60
+ return false;
61
+ }
62
+ return true;
8
63
  }
9
64
  export function assertProviderLocaleKey(value) {
10
65
  if (!isProviderLocaleKey(value)) {
11
- throw new Error(`Provider locale key must be a dot path such as "meta.description" or "operations.search.description"; received ${JSON.stringify(value)}`);
66
+ throw new Error(`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)}`);
12
67
  }
13
68
  }
14
69
  export function qualifyProviderLocaleKey(providerId, key) {
@@ -17,9 +72,25 @@ export function qualifyProviderLocaleKey(providerId, key) {
17
72
  }
18
73
  export function getProviderLocalePath(catalog, key) {
19
74
  assertProviderLocaleKey(key);
75
+ return getProviderLocaleSegments(catalog, key.split("."));
76
+ }
77
+ /**
78
+ * Reads a catalog path from raw segments, bypassing the dot-path grammar.
79
+ *
80
+ * Derived error-message lookups address the catalog by the thrown error code
81
+ * (`errors.<code>.message`). The grammar now accepts the code shapes the SDK
82
+ * registers and the fleet throws, but `code` is provider data: a provider may
83
+ * throw anything, and an unusual code must still resolve its catalog entry
84
+ * rather than be dropped as malformed. Own properties only: a
85
+ * `__proto__`/`constructor` segment must read as a miss, not as the prototype
86
+ * chain.
87
+ */
88
+ export function getProviderLocaleSegments(catalog, segments) {
89
+ if (segments.length === 0)
90
+ return undefined;
20
91
  let cursor = catalog;
21
- for (const segment of key.split(".")) {
22
- if (!isRecord(cursor))
92
+ for (const segment of segments) {
93
+ if (!isRecord(cursor) || !Object.hasOwn(cursor, segment))
23
94
  return undefined;
24
95
  cursor = cursor[segment];
25
96
  }