@apifuse/provider-sdk 2.2.0-beta.62 → 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 (243) hide show
  1. package/AUTHORING.md +268 -9
  2. package/CHANGELOG.md +4 -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 +187 -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/i18n/error-messages.d.ts +86 -0
  32. package/dist/i18n/error-messages.js +192 -0
  33. package/dist/i18n/index.d.ts +1 -0
  34. package/dist/i18n/index.js +1 -0
  35. package/dist/i18n/keys.d.ts +12 -0
  36. package/dist/i18n/keys.js +76 -5
  37. package/dist/index.d.ts +7 -1
  38. package/dist/index.js +6 -0
  39. package/dist/lint.d.ts +10 -0
  40. package/dist/lint.js +412 -49
  41. package/dist/provider.d.ts +5 -5
  42. package/dist/provider.js +2 -2
  43. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  44. package/dist/runtime/browser-telemetry-binding.js +83 -0
  45. package/dist/runtime/browser-telemetry.d.ts +76 -0
  46. package/dist/runtime/browser-telemetry.js +165 -0
  47. package/dist/runtime/browser.d.ts +3 -0
  48. package/dist/runtime/browser.js +208 -47
  49. package/dist/runtime/cache-telemetry.d.ts +71 -0
  50. package/dist/runtime/cache-telemetry.js +177 -0
  51. package/dist/runtime/cache.js +67 -7
  52. package/dist/runtime/capability-response.d.ts +3 -0
  53. package/dist/runtime/capability-response.js +56 -0
  54. package/dist/runtime/capability-telemetry.d.ts +46 -0
  55. package/dist/runtime/capability-telemetry.js +68 -0
  56. package/dist/runtime/diagnostic-env.d.ts +3 -0
  57. package/dist/runtime/diagnostic-env.js +10 -1
  58. package/dist/runtime/engine-mode.d.ts +74 -0
  59. package/dist/runtime/engine-mode.js +152 -0
  60. package/dist/runtime/http-telemetry-guard.d.ts +10 -0
  61. package/dist/runtime/http-telemetry-guard.js +62 -10
  62. package/dist/runtime/http.js +141 -35
  63. package/dist/runtime/instrumentation.js +92 -13
  64. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  65. package/dist/runtime/ocr-telemetry.js +192 -0
  66. package/dist/runtime/ocr.js +30 -8
  67. package/dist/runtime/request-telemetry.d.ts +2 -0
  68. package/dist/runtime/request-telemetry.js +2 -1
  69. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  70. package/dist/runtime/resolver-runtime-options.js +33 -0
  71. package/dist/runtime/resolver-shared.d.ts +3 -2
  72. package/dist/runtime/resolver-shared.js +4 -4
  73. package/dist/runtime/resolver-telemetry.d.ts +1 -0
  74. package/dist/runtime/resolver-telemetry.js +1 -1
  75. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  76. package/dist/runtime/resolver-vendors/browser.js +46 -0
  77. package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
  78. package/dist/runtime/resolver-vendors/hosts.js +35 -0
  79. package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
  80. package/dist/runtime/resolver-vendors/types.d.ts +5 -4
  81. package/dist/runtime/resolver.d.ts +4 -1
  82. package/dist/runtime/resolver.js +46 -13
  83. package/dist/runtime/state-telemetry.d.ts +79 -0
  84. package/dist/runtime/state-telemetry.js +194 -0
  85. package/dist/runtime/state.d.ts +3 -0
  86. package/dist/runtime/state.js +26 -7
  87. package/dist/runtime/stealth-cookies.d.ts +5 -1
  88. package/dist/runtime/stealth-cookies.js +21 -1
  89. package/dist/runtime/stealth-owned-headers.d.ts +26 -0
  90. package/dist/runtime/stealth-owned-headers.js +37 -0
  91. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  92. package/dist/runtime/stealth-telemetry.js +179 -0
  93. package/dist/runtime/stealth.d.ts +4 -1
  94. package/dist/runtime/stealth.js +228 -85
  95. package/dist/runtime/stt-telemetry.d.ts +77 -0
  96. package/dist/runtime/stt-telemetry.js +176 -0
  97. package/dist/runtime/stt.js +15 -6
  98. package/dist/server/error-stack-frames.d.ts +35 -0
  99. package/dist/server/error-stack-frames.js +219 -0
  100. package/dist/server/index.d.ts +7 -0
  101. package/dist/server/index.js +4 -0
  102. package/dist/server/sdk-version.d.ts +3 -0
  103. package/dist/server/sdk-version.js +19 -0
  104. package/dist/server/self-test.js +1 -18
  105. package/dist/server/sensitive-values.d.ts +1 -1
  106. package/dist/server/sensitive-values.js +9 -0
  107. package/dist/server/serve-implementation.d.ts +51 -2
  108. package/dist/server/serve-implementation.js +337 -90
  109. package/dist/server/types.d.ts +1 -0
  110. package/dist/server/types.js +7 -0
  111. package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
  112. package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
  113. package/dist/stateful/stateful-provider-runtime-context.js +1 -0
  114. package/dist/types.d.ts +94 -1
  115. package/package.json +6 -1
  116. package/src/cli/commands.ts +14 -0
  117. package/src/cli/deployment-intent.ts +364 -0
  118. package/src/cli/migrate-deployment-intent.ts +1908 -0
  119. package/src/cli/migrate-provider-shape.ts +10 -4
  120. package/src/cli/resolver-runtime.ts +72 -0
  121. package/src/define.ts +13 -0
  122. package/src/dev.ts +9 -2
  123. package/src/engine.ts +38 -2
  124. package/src/error-resolution.ts +58 -0
  125. package/src/errors.ts +77 -1
  126. package/src/i18n/error-messages.ts +255 -0
  127. package/src/i18n/index.ts +1 -0
  128. package/src/i18n/keys.ts +80 -6
  129. package/src/index.ts +72 -0
  130. package/src/lint.ts +522 -50
  131. package/src/provider.ts +10 -0
  132. package/src/runtime/browser-telemetry-binding.ts +92 -0
  133. package/src/runtime/browser-telemetry.ts +289 -0
  134. package/src/runtime/browser.ts +249 -65
  135. package/src/runtime/cache-telemetry.ts +247 -0
  136. package/src/runtime/cache.ts +68 -17
  137. package/src/runtime/capability-response.ts +53 -0
  138. package/src/runtime/capability-telemetry.ts +101 -0
  139. package/src/runtime/diagnostic-env.ts +11 -1
  140. package/src/runtime/engine-mode.ts +233 -0
  141. package/src/runtime/http-telemetry-guard.ts +66 -13
  142. package/src/runtime/http.ts +168 -37
  143. package/src/runtime/instrumentation.ts +105 -16
  144. package/src/runtime/ocr-telemetry.ts +308 -0
  145. package/src/runtime/ocr.ts +109 -74
  146. package/src/runtime/request-telemetry.ts +2 -1
  147. package/src/runtime/resolver-runtime-options.ts +50 -0
  148. package/src/runtime/resolver-shared.ts +9 -4
  149. package/src/runtime/resolver-telemetry.ts +1 -1
  150. package/src/runtime/resolver-vendors/browser.ts +65 -7
  151. package/src/runtime/resolver-vendors/hosts.ts +43 -0
  152. package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
  153. package/src/runtime/resolver-vendors/types.ts +5 -4
  154. package/src/runtime/resolver.ts +64 -21
  155. package/src/runtime/state-telemetry.ts +298 -0
  156. package/src/runtime/state.ts +46 -45
  157. package/src/runtime/stealth-cookies.ts +24 -1
  158. package/src/runtime/stealth-owned-headers.ts +41 -0
  159. package/src/runtime/stealth-telemetry.ts +304 -0
  160. package/src/runtime/stealth.ts +355 -150
  161. package/src/runtime/stt-telemetry.ts +282 -0
  162. package/src/runtime/stt.ts +93 -71
  163. package/src/server/error-stack-frames.ts +214 -0
  164. package/src/server/index.ts +53 -0
  165. package/src/server/sdk-version.ts +22 -0
  166. package/src/server/self-test.ts +1 -21
  167. package/src/server/sensitive-values.ts +14 -0
  168. package/src/server/serve-implementation.ts +569 -108
  169. package/src/server/types.ts +7 -0
  170. package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
  171. package/src/stateful/stateful-provider-runtime-context.ts +4 -0
  172. package/src/types.ts +99 -1
  173. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
  174. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
  175. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
  176. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
  177. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
  178. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
  179. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
  180. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
  181. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
  182. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
  183. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
  184. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
  185. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
  186. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
  187. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
  188. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
  189. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
  190. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
  191. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
  192. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
  193. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
  194. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
  195. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
  196. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
  197. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
  198. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
  199. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
  200. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
  201. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
  202. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
  203. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
  204. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
  205. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
  206. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
  207. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
  208. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
  209. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
  210. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
  211. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
  212. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
  213. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
  214. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
  215. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
  216. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
  217. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
  218. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
  219. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
  220. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
  221. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
  222. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
@@ -38,6 +38,7 @@ export declare const OperationRequestSchema: z.ZodObject<{
38
38
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
39
39
  externalRef: z.ZodString;
40
40
  }, z.core.$strip>>;
41
+ tenantId: z.ZodOptional<z.ZodString>;
41
42
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
42
43
  trace: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
43
44
  }, z.core.$strip>;
@@ -21,6 +21,13 @@ export const OperationRequestSchema = z.object({
21
21
  input: z.record(z.string(), z.unknown()),
22
22
  connectionId: z.string().optional(),
23
23
  connection: OperationConnectionSchema.optional(),
24
+ /**
25
+ * Gateway-asserted principal scope: the organization id for customer
26
+ * subjects, the service-account id for platform service accounts. Absent
27
+ * on direct calls and self-test. Trusted exactly like `connectionId`; it is
28
+ * never derived from caller headers or `input`.
29
+ */
30
+ tenantId: z.string().optional(),
24
31
  headers: z.record(z.string(), z.string()).optional(),
25
32
  trace: z.record(z.string(), z.string()).optional(),
26
33
  });
@@ -145,6 +145,7 @@ function buildForwardingEnvelope(input) {
145
145
  requestId: input.request.requestId,
146
146
  input: input.request.input,
147
147
  connection: metadata.connection,
148
+ ...(metadata.tenantId ? { tenantId: metadata.tenantId } : {}),
148
149
  headers: sanitizeForwardedHeaders(metadata.headers),
149
150
  ...(metadata.trace ? { trace: metadata.trace } : {}),
150
151
  },
@@ -160,12 +161,33 @@ function forwardedMetadata(request) {
160
161
  }
161
162
  const operationRequest = forwardingContext.operationRequest;
162
163
  const connection = OperationConnectionSchema.parse(operationRequest?.connection);
164
+ const tenantId = forwardedTenantId(operationRequest?.tenantId);
163
165
  return {
164
166
  connection,
167
+ ...(tenantId !== undefined ? { tenantId } : {}),
165
168
  ...(operationRequest?.headers ? { headers: operationRequest.headers } : {}),
166
169
  ...(operationRequest?.trace ? { trace: operationRequest.trace } : {}),
167
170
  };
168
171
  }
172
+ /**
173
+ * The runtime context is consumer-supplied and typed `unknown`, and only its
174
+ * `requestId` is guarded on read. A malformed principal scope must fail at the
175
+ * source with a forwarding error — the owner would otherwise reject the whole
176
+ * forwarded operation, and dropping it silently would downgrade a per-principal
177
+ * policy to the shared scope. `""` is not a principal (same rule as ids on the
178
+ * ingress envelope), so it is absent rather than invalid.
179
+ */
180
+ function forwardedTenantId(value) {
181
+ if (value === undefined || value === "")
182
+ return undefined;
183
+ if (typeof value !== "string") {
184
+ throw new StatefulOwnerForwardingError({
185
+ code: "STATEFUL_FORWARDING_CONTEXT_INVALID",
186
+ message: "Stateful operation forwarding requires a string tenantId when it is present.",
187
+ });
188
+ }
189
+ return value;
190
+ }
169
191
  function sanitizeForwardedHeaders(headers) {
170
192
  if (!headers)
171
193
  return {};
@@ -5,6 +5,8 @@ export interface StatefulForwardingRuntimeContext {
5
5
  readonly operationRequest?: {
6
6
  readonly requestId: string;
7
7
  readonly connection?: unknown;
8
+ /** Gateway-asserted principal scope; forwarded verbatim to the owner pod. */
9
+ readonly tenantId?: string;
8
10
  readonly headers?: Record<string, string>;
9
11
  readonly trace?: Record<string, string>;
10
12
  };
@@ -24,6 +26,7 @@ export declare function withStatefulLocalProviderContext(providerContext: unknow
24
26
  export declare function statefulForwardingContextFromProviderRequest(request: {
25
27
  readonly requestId: string;
26
28
  readonly connection?: unknown;
29
+ readonly tenantId?: string;
27
30
  readonly headers?: Record<string, string>;
28
31
  readonly trace?: Record<string, string>;
29
32
  }): StatefulForwardingRuntimeContext;
@@ -16,6 +16,7 @@ export function statefulForwardingContextFromProviderRequest(request) {
16
16
  operationRequest: {
17
17
  requestId: request.requestId,
18
18
  ...(request.connection !== undefined ? { connection: request.connection } : {}),
19
+ ...(request.tenantId ? { tenantId: request.tenantId } : {}),
19
20
  ...(request.headers ? { headers: request.headers } : {}),
20
21
  ...(request.trace ? { trace: request.trace } : {}),
21
22
  },
package/dist/types.d.ts CHANGED
@@ -782,8 +782,17 @@ export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number
782
782
  export interface OperationErrorCode {
783
783
  code: string;
784
784
  status?: ProviderErrorStatus;
785
+ /** Developer-facing English prose for docs and MCP tool text. Never localized. */
785
786
  description: string;
786
787
  retryable?: boolean;
788
+ /**
789
+ * Locale catalog key for the client-facing `message` of every error thrown
790
+ * with this `code`. A throw site's own `messageKey` wins; when neither is
791
+ * present the SDK derives `errors.<code>.message`.
792
+ */
793
+ messageKey?: ProviderLocaleKeyInput;
794
+ /** Locale catalog key for the client-facing `fix`. Derived default: `errors.<code>.fix`. */
795
+ fixKey?: ProviderLocaleKeyInput;
787
796
  }
788
797
  export type StealthPlatform = "macos" | "windows" | "linux" | "android" | "ios";
789
798
  export type StealthBrowser = "chrome" | "firefox" | "safari";
@@ -1039,8 +1048,44 @@ export interface HttpRetrySummary {
1039
1048
  lastErrorCode?: string;
1040
1049
  lastStatus?: number;
1041
1050
  }
1051
+ /** Per-attempt input for a `RequestOptions.headers` factory. */
1052
+ export interface HttpAttemptContext {
1053
+ /**
1054
+ * 1-based number of this request build: the first request is `1`, its first
1055
+ * retry `2`. Only attempts that reach header resolution count; a failed
1056
+ * proxy allocation or a skipped duplicate proxy offset builds no request
1057
+ * and is not numbered.
1058
+ */
1059
+ attempt: number;
1060
+ /**
1061
+ * Resolved request URL: `baseUrl` + path + `params` + `sensitiveParams`.
1062
+ * It is the exact URL the attempt is issued against, so bind signature
1063
+ * targets to it rather than to a URL the caller rebuilds. Strip the query
1064
+ * and fragment for fields that are defined without them — RFC 9449 §4.2
1065
+ * requires that of DPoP `htu`. It carries any sensitive query values, so
1066
+ * never log it and never copy it verbatim into a signed field that a
1067
+ * verifier logs.
1068
+ */
1069
+ url: string;
1070
+ /** Normalized upper-case HTTP method of the request. */
1071
+ method: Uppercase<HttpMethod>;
1072
+ }
1073
+ /**
1074
+ * Builds the request headers for one issued attempt. Managed retry calls it
1075
+ * again for every retry, so request-bound single-use proofs (DPoP `jti`,
1076
+ * HTTP message signatures, HMAC nonces) can be re-minted instead of replayed.
1077
+ * A throw is raised as a non-retryable `TransportError` with code
1078
+ * `http_header_factory_failed`.
1079
+ */
1080
+ export type HttpHeadersFactory = (attempt: HttpAttemptContext) => Record<string, string> | Promise<Record<string, string>>;
1042
1081
  export interface RequestOptions {
1043
- headers?: Record<string, string>;
1082
+ /**
1083
+ * Request headers, or a factory resolved once per issued attempt (see
1084
+ * `HttpHeadersFactory`). The factory runs after proxy resolution, before the
1085
+ * first redirect hop of that attempt; redirect hops reuse the attempt's
1086
+ * headers. `ctx.stealth` declares its own header option and is not affected.
1087
+ */
1088
+ headers?: Record<string, string> | HttpHeadersFactory;
1044
1089
  params?: RequestParams;
1045
1090
  /**
1046
1091
  * Query parameters whose values contain credentials or other secret material.
@@ -1048,7 +1093,23 @@ export interface RequestOptions {
1048
1093
  */
1049
1094
  sensitiveParams?: Record<string, string>;
1050
1095
  proxy?: string;
1096
+ /**
1097
+ * Per-attempt timeout for the response-header phase. It is disarmed once a
1098
+ * successful response's headers arrive, so it never bounds consumption of
1099
+ * that body — on the buffered path as well as on streams; use `signal` for
1100
+ * that. A non-2xx body the transport discards on the caller's behalf is
1101
+ * still drained under this deadline, so a wedged error body cannot hang the
1102
+ * attempt.
1103
+ */
1051
1104
  timeout?: number;
1105
+ /**
1106
+ * Cancels this request only — its retries, its backoff sleep and its
1107
+ * response-body consumption — leaving the client's other in-flight requests
1108
+ * untouched. Merged with the client-level `signal`, so a rejection reads as
1109
+ * `transport_cancelled` either way; inspect `signal.reason` to tell a
1110
+ * per-request abort from a fleet-wide one.
1111
+ */
1112
+ signal?: AbortSignal;
1052
1113
  /**
1053
1114
  * Defaults to true. Set to false when callers need to inspect upstream
1054
1115
  * non-2xx bodies themselves instead of converting them to TransportError.
@@ -1073,6 +1134,13 @@ export interface HttpRedirectPolicy {
1073
1134
  export type HttpRedirectFailureReason = Exclude<RedirectRunReason, "completed">;
1074
1135
  export type HttpMethod = "HEAD" | "head" | "GET" | "get" | "POST" | "post" | "PUT" | "put" | "DELETE" | "delete" | "OPTIONS" | "options" | "TRACE" | "trace" | "PATCH" | "patch";
1075
1136
  export interface StealthFetchOptions extends Omit<RequestOptions, "redirectPolicy" | "headers"> {
1137
+ /**
1138
+ * Not supported by the stealth transport, which observes only the
1139
+ * client-level `signal` passed to `createStealthClient`. Typed as `never` so
1140
+ * a per-request signal fails to compile instead of being silently ignored;
1141
+ * threading one through the stealth session is a separate change.
1142
+ */
1143
+ signal?: never;
1076
1144
  /**
1077
1145
  * Defaults to true. Set to false when callers need to inspect upstream
1078
1146
  * non-2xx bodies themselves instead of converting them to TransportError.
@@ -1659,6 +1727,16 @@ export interface CredentialContext {
1659
1727
  }
1660
1728
  export interface ProviderRequestContext {
1661
1729
  connectionId?: string;
1730
+ /**
1731
+ * Gateway-asserted principal scope of the operation request: the
1732
+ * organization id for customer subjects, the service-account id for
1733
+ * platform service accounts. `undefined` on direct calls and self-test
1734
+ * (unlike `FlowContext.tenantId`, which is coerced to `""`); an empty
1735
+ * string on the wire is normalised to `undefined` like `connectionId`.
1736
+ * Key per-principal policy (quota, fairness, correlation) for
1737
+ * connectionless operations on it. It does not scope `ctx.state`.
1738
+ */
1739
+ tenantId?: string;
1662
1740
  headers: Record<string, string>;
1663
1741
  }
1664
1742
  export interface ContextScratchpad {
@@ -1915,10 +1993,17 @@ export interface AuthConfig {
1915
1993
  proxied?: ProxiedOAuthConfig;
1916
1994
  flow?: AuthFlowDefinition;
1917
1995
  }
1996
+ /** Who provisions a declared secret: the platform (`apifuse`) or the provider author (`contributor`). */
1997
+ export type ProviderSecretIssuer = "apifuse" | "contributor";
1918
1998
  export interface ProviderSecretDeclaration {
1919
1999
  name: string;
1920
2000
  description?: string;
1921
2001
  required?: boolean;
2002
+ /**
2003
+ * Optional today; ADR-0011 D3 makes it required once every deployed provider
2004
+ * declares it. Metadata only: it does not change which secrets are projected.
2005
+ */
2006
+ issuer?: ProviderSecretIssuer;
1922
2007
  }
1923
2008
  export interface CredentialDeclaration {
1924
2009
  keys: string[];
@@ -2000,6 +2085,14 @@ export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOu
2000
2085
  * optional. Note that the deployment `runtime` axis
2001
2086
  * (`shared`/`dedicated`/`browser`) is distinct from the provider execution
2002
2087
  * `runtime` (`standard`/`shared`/`browser`).
2088
+ *
2089
+ * This key is the ONLY authored deployment surface. Declare it inside the
2090
+ * `defineProvider({...})` literal, not on a spread default export and not
2091
+ * in a standalone `deploy.ts` (legacy; `apifuse migrate-deployment` hoists
2092
+ * and deletes it). Write only what differs from the runtime profile —
2093
+ * shared `25m/128Mi`, browser `200m/256Mi`, `language: "typescript"`,
2094
+ * `replicas: 1`, HPA enabled 1-1 at 70% CPU — so most providers declare
2095
+ * nothing and `apifuse check` warns on restated defaults.
2003
2096
  */
2004
2097
  export interface ProviderDeploymentOverrides {
2005
2098
  runtime?: "shared" | "dedicated" | "browser";
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.62",
2
+ "version": "2.2.0-beta.63",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -15,9 +15,14 @@
15
15
  "src",
16
16
  "!src/__tests__",
17
17
  "!src/__tests__/**",
18
+ "!src/**/__tests__",
19
+ "!src/**/__tests__/**",
18
20
  "!src/**/*.test.ts",
21
+ "!src/**/*.spec.ts",
19
22
  "!src/index.test.ts",
20
23
  "bin",
24
+ "!bin/__tests__",
25
+ "!bin/__tests__/**",
21
26
  "README.md",
22
27
  "AUTHORING.md",
23
28
  "CHANGELOG.md",
@@ -5,6 +5,7 @@ export type ApifuseCommandName =
5
5
  | "sync-assets"
6
6
  | "migrate-shape"
7
7
  | "migrate-operation-declaration"
8
+ | "migrate-deployment"
8
9
  | "submit-check"
9
10
  | "bounty-check"
10
11
  | "record"
@@ -76,6 +77,18 @@ export const COMMAND_MANIFEST: Record<
76
77
  ],
77
78
  modulePath: "./apifuse-migrate-operation-declaration",
78
79
  },
80
+ "migrate-deployment": {
81
+ name: "migrate-deployment",
82
+ summary:
83
+ "Retire a legacy deploy.ts: hoist its non-default values into the defineProvider() deployment key and delete the file.",
84
+ usage:
85
+ "apifuse migrate-deployment [path] [--check] [--json] [--drop-codeowners-lock]",
86
+ examples: [
87
+ "apifuse migrate-deployment .",
88
+ "apifuse migrate-deployment . --check --json",
89
+ ],
90
+ modulePath: "./apifuse-migrate-deployment",
91
+ },
79
92
  "submit-check": {
80
93
  name: "submit-check",
81
94
  summary:
@@ -136,6 +149,7 @@ export const COMMAND_ORDER: ApifuseCommandName[] = [
136
149
  "sync-assets",
137
150
  "migrate-shape",
138
151
  "migrate-operation-declaration",
152
+ "migrate-deployment",
139
153
  "submit-check",
140
154
  "record",
141
155
  "test",
@@ -0,0 +1,364 @@
1
+ import { z } from "zod";
2
+
3
+ import type { ProviderDeploymentOverrides } from "../types.js";
4
+
5
+ /**
6
+ * Deployment-intent helpers shared by `apifuse check` and
7
+ * `apifuse migrate-deployment`.
8
+ *
9
+ * Deployment intent has exactly one authored home: the optional `deployment`
10
+ * key on `defineProvider()`. The APIFuse registry builder is the only reader;
11
+ * it resolves omitted fields from a runtime profile and rejects anything it
12
+ * cannot resolve. This module mirrors that profile table so the CLI can
13
+ * (a) subtract profile defaults when hoisting a legacy `deploy.ts` into the
14
+ * key and (b) warn when a declared key restates a default. The registry
15
+ * stays canonical — keep the constants below identical to
16
+ * `resolveProviderDeploymentConfig` in `@apifuse/provider-registry`.
17
+ */
18
+
19
+ export type DeploymentRuntime = "shared" | "dedicated" | "browser";
20
+ export type DeploymentLanguage = "typescript" | "python";
21
+
22
+ export interface DeploymentResources {
23
+ cpu: string;
24
+ memory: string;
25
+ }
26
+
27
+ export interface DeploymentHpa {
28
+ enabled: boolean;
29
+ minReplicas?: number;
30
+ maxReplicas?: number;
31
+ targetCPUUtilizationPercentage?: number;
32
+ }
33
+
34
+ /** Fully resolved deployment view: what the platform materializes for a provider. */
35
+ export interface ResolvedDeploymentIntent {
36
+ runtime: DeploymentRuntime;
37
+ language: DeploymentLanguage;
38
+ replicas: number;
39
+ hpa: DeploymentHpa;
40
+ resources: DeploymentResources;
41
+ cache?: { redis?: { enabled: boolean; url?: string } };
42
+ network?: { additionalTcpPorts: number[] };
43
+ buildContext: string;
44
+ }
45
+
46
+ /**
47
+ * Runtime-profile defaults. `dedicated` carries no resource profile and
48
+ * therefore always needs explicit `resources`.
49
+ */
50
+ export const DEPLOYMENT_PROFILE_DEFAULTS = Object.freeze({
51
+ runtime: "shared" as DeploymentRuntime,
52
+ language: "typescript" as DeploymentLanguage,
53
+ replicas: 1,
54
+ hpa: Object.freeze({
55
+ enabled: true,
56
+ minReplicas: 1,
57
+ maxReplicas: 1,
58
+ targetCPUUtilizationPercentage: 70,
59
+ }) as Readonly<DeploymentHpa>,
60
+ buildContext: ".",
61
+ resources: Object.freeze({
62
+ shared: Object.freeze({ cpu: "25m", memory: "128Mi" }),
63
+ browser: Object.freeze({ cpu: "200m", memory: "256Mi" }),
64
+ dedicated: undefined,
65
+ }) as Readonly<Record<DeploymentRuntime, Readonly<DeploymentResources> | undefined>>,
66
+ });
67
+
68
+ const BUILD_CONTEXT_CONSTRAINT =
69
+ 'only "." is accepted; external provider images always build from the materialized checkout root';
70
+
71
+ const hpaSchema = z
72
+ .object({
73
+ enabled: z.boolean(),
74
+ minReplicas: z.number().int().positive().optional(),
75
+ maxReplicas: z.number().int().positive().optional(),
76
+ targetCPUUtilizationPercentage: z.number().int().positive().optional(),
77
+ })
78
+ .strict();
79
+
80
+ const resourcesSchema = z.object({ cpu: z.string().min(1), memory: z.string().min(1) }).strict();
81
+
82
+ const cacheSchema = z
83
+ .object({
84
+ redis: z
85
+ .object({ enabled: z.boolean(), url: z.string().min(1).optional() })
86
+ .strict()
87
+ .optional(),
88
+ })
89
+ .strict();
90
+
91
+ const networkSchema = z
92
+ .object({
93
+ additionalTcpPorts: z.array(z.number().int().min(1).max(65535)).default([]),
94
+ })
95
+ .strict();
96
+
97
+ const buildContextSchema = z
98
+ .string()
99
+ .min(1)
100
+ .refine((value) => value === ".", { message: BUILD_CONTEXT_CONSTRAINT });
101
+
102
+ /**
103
+ * The standalone `deploy.ts` contract (legacy). Mirrors the registry's
104
+ * `ProviderDeploymentConfigSchema`, including its own defaults — note that a
105
+ * legacy file without `hpa` resolved to `{ enabled: false }`, not the
106
+ * profile HPA, so the two default tables are deliberately different.
107
+ */
108
+ export const legacyDeploymentConfigSchema = z
109
+ .object({
110
+ runtime: z.enum(["shared", "dedicated", "browser"]),
111
+ language: z.enum(["typescript", "python"]),
112
+ replicas: z.number().int().positive().default(1),
113
+ hpa: hpaSchema.default({ enabled: false }),
114
+ resources: resourcesSchema,
115
+ cache: cacheSchema.optional(),
116
+ network: networkSchema.optional(),
117
+ buildContext: buildContextSchema.optional(),
118
+ })
119
+ .strict();
120
+
121
+ /** The authored `deployment` key: every field optional (registry `ProviderDeploymentOverridesSchema`). */
122
+ export const deploymentOverridesSchema = z
123
+ .object({
124
+ runtime: z.enum(["shared", "dedicated", "browser"]).optional(),
125
+ language: z.enum(["typescript", "python"]).optional(),
126
+ replicas: z.number().int().positive().optional(),
127
+ hpa: hpaSchema.optional(),
128
+ resources: resourcesSchema.optional(),
129
+ cache: cacheSchema.optional(),
130
+ network: networkSchema.optional(),
131
+ buildContext: buildContextSchema.optional(),
132
+ })
133
+ .strict();
134
+
135
+ export type DeploymentParse<T> =
136
+ | { readonly ok: true; readonly value: T }
137
+ | { readonly ok: false; readonly reason: string };
138
+
139
+ function formatZodIssues(error: z.ZodError): string {
140
+ return error.issues
141
+ .map((issue) => {
142
+ const path = issue.path.map(String).join(".");
143
+ return path ? `${path}: ${issue.message}` : issue.message;
144
+ })
145
+ .join("; ");
146
+ }
147
+
148
+ /** Parse a legacy `deploy.ts` default export into the resolved view. */
149
+ export function normalizeLegacyDeployment(
150
+ value: unknown,
151
+ ): DeploymentParse<ResolvedDeploymentIntent> {
152
+ const parsed = legacyDeploymentConfigSchema.safeParse(value);
153
+ if (!parsed.success) {
154
+ return {
155
+ ok: false,
156
+ reason: `deploy.ts is not a valid deployment config: ${formatZodIssues(parsed.error)}`,
157
+ };
158
+ }
159
+ const legacy = parsed.data;
160
+ return {
161
+ ok: true,
162
+ value: {
163
+ runtime: legacy.runtime,
164
+ language: legacy.language,
165
+ replicas: legacy.replicas,
166
+ hpa: legacy.hpa,
167
+ resources: legacy.resources,
168
+ ...(legacy.cache ? { cache: legacy.cache } : {}),
169
+ ...(legacy.network ? { network: legacy.network } : {}),
170
+ buildContext: legacy.buildContext ?? DEPLOYMENT_PROFILE_DEFAULTS.buildContext,
171
+ },
172
+ };
173
+ }
174
+
175
+ /** Parse an authored `deployment` key against the strict overrides contract. */
176
+ export function parseDeploymentIntent(
177
+ value: unknown,
178
+ ): DeploymentParse<ProviderDeploymentOverrides> {
179
+ const parsed = deploymentOverridesSchema.safeParse(value);
180
+ if (!parsed.success) {
181
+ return { ok: false, reason: `deployment key is invalid: ${formatZodIssues(parsed.error)}` };
182
+ }
183
+ return { ok: true, value: parsed.data };
184
+ }
185
+
186
+ /**
187
+ * Resolve an authored key against the runtime profiles, per field;
188
+ * `resources` and `hpa` resolve as whole objects (never deep-merged).
189
+ * Throws when the intent cannot resolve (dedicated runtime without
190
+ * resources, non-default buildContext) — the registry rejects the same.
191
+ */
192
+ export function resolveDeploymentIntent(
193
+ intent: ProviderDeploymentOverrides = {},
194
+ ): ResolvedDeploymentIntent {
195
+ const runtime = intent.runtime ?? DEPLOYMENT_PROFILE_DEFAULTS.runtime;
196
+ const resources = intent.resources ?? DEPLOYMENT_PROFILE_DEFAULTS.resources[runtime];
197
+ if (!resources) {
198
+ throw new Error(
199
+ `resources: runtime "${runtime}" has no profile default; declare explicit resources { cpu, memory }`,
200
+ );
201
+ }
202
+ if (
203
+ intent.buildContext !== undefined &&
204
+ intent.buildContext !== DEPLOYMENT_PROFILE_DEFAULTS.buildContext
205
+ ) {
206
+ throw new Error(`buildContext: ${BUILD_CONTEXT_CONSTRAINT}`);
207
+ }
208
+ return {
209
+ runtime,
210
+ language: intent.language ?? DEPLOYMENT_PROFILE_DEFAULTS.language,
211
+ replicas: intent.replicas ?? DEPLOYMENT_PROFILE_DEFAULTS.replicas,
212
+ hpa: intent.hpa ? { ...intent.hpa } : { ...DEPLOYMENT_PROFILE_DEFAULTS.hpa },
213
+ resources: { ...resources },
214
+ ...(intent.cache ? { cache: cloneJson(intent.cache) } : {}),
215
+ ...(intent.network
216
+ ? { network: { additionalTcpPorts: [...(intent.network.additionalTcpPorts ?? [])] } }
217
+ : {}),
218
+ buildContext: intent.buildContext ?? DEPLOYMENT_PROFILE_DEFAULTS.buildContext,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Two resolved views describe the same deployment when they are deep-equal
224
+ * after dropping empty shells (`network: { additionalTcpPorts: [] }`,
225
+ * `cache: {}`) that carry no intent — the manifests they render are
226
+ * byte-identical.
227
+ */
228
+ export function deploymentIntentsEquivalent(
229
+ left: ResolvedDeploymentIntent,
230
+ right: ResolvedDeploymentIntent,
231
+ ): boolean {
232
+ return deepEqual(canonicalResolvedDeployment(left), canonicalResolvedDeployment(right));
233
+ }
234
+
235
+ export function canonicalResolvedDeployment(
236
+ resolved: ResolvedDeploymentIntent,
237
+ ): ResolvedDeploymentIntent {
238
+ const { cache, network, ...rest } = resolved;
239
+ const canonical: ResolvedDeploymentIntent = { ...rest };
240
+ if (cache?.redis !== undefined) canonical.cache = { redis: { ...cache.redis } };
241
+ if (network !== undefined && network.additionalTcpPorts.length > 0) {
242
+ canonical.network = { additionalTcpPorts: [...network.additionalTcpPorts] };
243
+ }
244
+ return canonical;
245
+ }
246
+
247
+ /**
248
+ * The smallest `deployment` key that resolves to `resolved`: profile
249
+ * defaults are omitted, `hpa`/`resources` are kept whole when they differ
250
+ * (the resolver never deep-merges them), and empty shells are dropped.
251
+ * Callers prove the result with
252
+ * `deploymentIntentsEquivalent(resolveDeploymentIntent(key), resolved)`.
253
+ */
254
+ export function subtractDeploymentDefaults(
255
+ resolved: ResolvedDeploymentIntent,
256
+ ): ProviderDeploymentOverrides {
257
+ const intent: ProviderDeploymentOverrides = {};
258
+ if (resolved.runtime !== DEPLOYMENT_PROFILE_DEFAULTS.runtime) intent.runtime = resolved.runtime;
259
+ if (resolved.language !== DEPLOYMENT_PROFILE_DEFAULTS.language)
260
+ intent.language = resolved.language;
261
+ if (resolved.replicas !== DEPLOYMENT_PROFILE_DEFAULTS.replicas)
262
+ intent.replicas = resolved.replicas;
263
+ if (!deepEqual(resolved.hpa, DEPLOYMENT_PROFILE_DEFAULTS.hpa)) intent.hpa = { ...resolved.hpa };
264
+ const profileResources = DEPLOYMENT_PROFILE_DEFAULTS.resources[resolved.runtime];
265
+ if (profileResources === undefined || !deepEqual(resolved.resources, profileResources)) {
266
+ intent.resources = { ...resolved.resources };
267
+ }
268
+ if (resolved.cache?.redis !== undefined) intent.cache = { redis: { ...resolved.cache.redis } };
269
+ if (resolved.network !== undefined && resolved.network.additionalTcpPorts.length > 0) {
270
+ intent.network = { additionalTcpPorts: [...resolved.network.additionalTcpPorts] };
271
+ }
272
+ // buildContext: "." is the only accepted value, so it is never authored.
273
+ return intent;
274
+ }
275
+
276
+ /** Authored order for emitted keys: the registry contract's field order. */
277
+ export const DEPLOYMENT_INTENT_FIELD_ORDER: readonly (keyof ProviderDeploymentOverrides)[] = [
278
+ "runtime",
279
+ "language",
280
+ "replicas",
281
+ "hpa",
282
+ "resources",
283
+ "cache",
284
+ "network",
285
+ "buildContext",
286
+ ];
287
+
288
+ export interface RedundantDeploymentField {
289
+ /** Dotted path under `deployment`, e.g. `hpa` or `network.additionalTcpPorts`. */
290
+ readonly path: string;
291
+ readonly message: string;
292
+ }
293
+
294
+ /**
295
+ * Fields of an authored `deployment` key that restate what the runtime
296
+ * profile already resolves (ADR-0009 "derived values are not re-stated").
297
+ * Accepts an unvalidated value because `apifuse check` inspects the loaded
298
+ * definition, whose key the SDK passes through verbatim.
299
+ */
300
+ export function redundantDeploymentDefaults(intent: unknown): RedundantDeploymentField[] {
301
+ if (!isRecord(intent)) return [];
302
+ const redundant: RedundantDeploymentField[] = [];
303
+ const flag = (path: string, detail: string): void => {
304
+ redundant.push({ path, message: `deployment.${path} ${detail}; omit it` });
305
+ };
306
+ if (intent.runtime === DEPLOYMENT_PROFILE_DEFAULTS.runtime) {
307
+ flag("runtime", `restates the profile default ("${DEPLOYMENT_PROFILE_DEFAULTS.runtime}")`);
308
+ }
309
+ if (intent.language === DEPLOYMENT_PROFILE_DEFAULTS.language) {
310
+ flag("language", `restates the profile default ("${DEPLOYMENT_PROFILE_DEFAULTS.language}")`);
311
+ }
312
+ if (intent.replicas === DEPLOYMENT_PROFILE_DEFAULTS.replicas) {
313
+ flag("replicas", `restates the profile default (${DEPLOYMENT_PROFILE_DEFAULTS.replicas})`);
314
+ }
315
+ if (deepEqual(intent.hpa, DEPLOYMENT_PROFILE_DEFAULTS.hpa)) {
316
+ flag("hpa", "restates the profile default (enabled, 1-1 replicas, 70% CPU target)");
317
+ }
318
+ const runtime =
319
+ typeof intent.runtime === "string" && intent.runtime in DEPLOYMENT_PROFILE_DEFAULTS.resources
320
+ ? (intent.runtime as DeploymentRuntime)
321
+ : DEPLOYMENT_PROFILE_DEFAULTS.runtime;
322
+ const profileResources = DEPLOYMENT_PROFILE_DEFAULTS.resources[runtime];
323
+ if (profileResources !== undefined && deepEqual(intent.resources, profileResources)) {
324
+ flag(
325
+ "resources",
326
+ `restates the "${runtime}" profile default (${profileResources.cpu}/${profileResources.memory})`,
327
+ );
328
+ }
329
+ if (intent.buildContext === DEPLOYMENT_PROFILE_DEFAULTS.buildContext) {
330
+ flag("buildContext", 'restates the only accepted value (".")');
331
+ }
332
+ if (isRecord(intent.network)) {
333
+ const ports = intent.network.additionalTcpPorts;
334
+ if (ports === undefined || (Array.isArray(ports) && ports.length === 0)) {
335
+ flag("network", "declares no additional TCP ports (empty shell)");
336
+ }
337
+ }
338
+ if (isRecord(intent.cache) && intent.cache.redis === undefined) {
339
+ flag("cache", "declares no redis intent (empty shell)");
340
+ }
341
+ return redundant;
342
+ }
343
+
344
+ /** Structural equality for JSON-shaped values, key order insensitive. */
345
+ export function deepEqual(left: unknown, right: unknown): boolean {
346
+ if (left === right) return true;
347
+ if (Array.isArray(left) || Array.isArray(right)) {
348
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
349
+ return left.every((item, index) => deepEqual(item, right[index]));
350
+ }
351
+ if (!isRecord(left) || !isRecord(right)) return false;
352
+ const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
353
+ const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
354
+ if (leftKeys.length !== rightKeys.length) return false;
355
+ return leftKeys.every((key) => Object.hasOwn(right, key) && deepEqual(left[key], right[key]));
356
+ }
357
+
358
+ function isRecord(value: unknown): value is Record<string, unknown> {
359
+ return typeof value === "object" && value !== null && !Array.isArray(value);
360
+ }
361
+
362
+ function cloneJson<T>(value: T): T {
363
+ return JSON.parse(JSON.stringify(value)) as T;
364
+ }