@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.
- package/AUTHORING.md +268 -9
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/bin/apifuse-check.ts +84 -2
- package/bin/apifuse-dev.ts +11 -31
- package/bin/apifuse-migrate-deployment.ts +200 -0
- package/bin/apifuse-pack-check.ts +11 -4
- package/bin/apifuse-pack-types.ts +187 -4
- package/bin/apifuse-record.ts +44 -32
- package/bin/apifuse-test.ts +3 -1
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +11 -0
- package/dist/cli/deployment-intent.d.ts +178 -0
- package/dist/cli/deployment-intent.js +277 -0
- package/dist/cli/migrate-deployment-intent.d.ts +132 -0
- package/dist/cli/migrate-deployment-intent.js +1552 -0
- package/dist/cli/migrate-provider-shape.d.ts +5 -1
- package/dist/cli/migrate-provider-shape.js +4 -2
- package/dist/cli/resolver-runtime.d.ts +21 -0
- package/dist/cli/resolver-runtime.js +31 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +5 -0
- package/dist/dev.d.ts +3 -1
- package/dist/dev.js +6 -1
- package/dist/engine.d.ts +35 -1
- package/dist/engine.js +21 -2
- package/dist/error-resolution.d.ts +1 -0
- package/dist/error-resolution.js +54 -0
- package/dist/errors.d.ts +39 -1
- package/dist/errors.js +50 -0
- package/dist/i18n/error-messages.d.ts +86 -0
- package/dist/i18n/error-messages.js +192 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/keys.d.ts +12 -0
- package/dist/i18n/keys.js +76 -5
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -0
- package/dist/lint.d.ts +10 -0
- package/dist/lint.js +412 -49
- package/dist/provider.d.ts +5 -5
- package/dist/provider.js +2 -2
- package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
- package/dist/runtime/browser-telemetry-binding.js +83 -0
- package/dist/runtime/browser-telemetry.d.ts +76 -0
- package/dist/runtime/browser-telemetry.js +165 -0
- package/dist/runtime/browser.d.ts +3 -0
- package/dist/runtime/browser.js +208 -47
- package/dist/runtime/cache-telemetry.d.ts +71 -0
- package/dist/runtime/cache-telemetry.js +177 -0
- package/dist/runtime/cache.js +67 -7
- package/dist/runtime/capability-response.d.ts +3 -0
- package/dist/runtime/capability-response.js +56 -0
- package/dist/runtime/capability-telemetry.d.ts +46 -0
- package/dist/runtime/capability-telemetry.js +68 -0
- package/dist/runtime/diagnostic-env.d.ts +3 -0
- package/dist/runtime/diagnostic-env.js +10 -1
- package/dist/runtime/engine-mode.d.ts +74 -0
- package/dist/runtime/engine-mode.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +10 -0
- package/dist/runtime/http-telemetry-guard.js +62 -10
- package/dist/runtime/http.js +141 -35
- package/dist/runtime/instrumentation.js +92 -13
- package/dist/runtime/ocr-telemetry.d.ts +82 -0
- package/dist/runtime/ocr-telemetry.js +192 -0
- package/dist/runtime/ocr.js +30 -8
- package/dist/runtime/request-telemetry.d.ts +2 -0
- package/dist/runtime/request-telemetry.js +2 -1
- package/dist/runtime/resolver-runtime-options.d.ts +14 -0
- package/dist/runtime/resolver-runtime-options.js +33 -0
- package/dist/runtime/resolver-shared.d.ts +3 -2
- package/dist/runtime/resolver-shared.js +4 -4
- package/dist/runtime/resolver-telemetry.d.ts +1 -0
- package/dist/runtime/resolver-telemetry.js +1 -1
- package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
- package/dist/runtime/resolver-vendors/browser.js +46 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
- package/dist/runtime/resolver-vendors/hosts.js +35 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
- package/dist/runtime/resolver-vendors/types.d.ts +5 -4
- package/dist/runtime/resolver.d.ts +4 -1
- package/dist/runtime/resolver.js +46 -13
- package/dist/runtime/state-telemetry.d.ts +79 -0
- package/dist/runtime/state-telemetry.js +194 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +26 -7
- package/dist/runtime/stealth-cookies.d.ts +5 -1
- package/dist/runtime/stealth-cookies.js +21 -1
- package/dist/runtime/stealth-owned-headers.d.ts +26 -0
- package/dist/runtime/stealth-owned-headers.js +37 -0
- package/dist/runtime/stealth-telemetry.d.ts +92 -0
- package/dist/runtime/stealth-telemetry.js +179 -0
- package/dist/runtime/stealth.d.ts +4 -1
- package/dist/runtime/stealth.js +228 -85
- package/dist/runtime/stt-telemetry.d.ts +77 -0
- package/dist/runtime/stt-telemetry.js +176 -0
- package/dist/runtime/stt.js +15 -6
- package/dist/server/error-stack-frames.d.ts +35 -0
- package/dist/server/error-stack-frames.js +219 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +4 -0
- package/dist/server/sdk-version.d.ts +3 -0
- package/dist/server/sdk-version.js +19 -0
- package/dist/server/self-test.js +1 -18
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +9 -0
- package/dist/server/serve-implementation.d.ts +51 -2
- package/dist/server/serve-implementation.js +337 -90
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +7 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
- package/dist/stateful/stateful-provider-runtime-context.js +1 -0
- package/dist/types.d.ts +94 -1
- package/package.json +6 -1
- package/src/cli/commands.ts +14 -0
- package/src/cli/deployment-intent.ts +364 -0
- package/src/cli/migrate-deployment-intent.ts +1908 -0
- package/src/cli/migrate-provider-shape.ts +10 -4
- package/src/cli/resolver-runtime.ts +72 -0
- package/src/define.ts +13 -0
- package/src/dev.ts +9 -2
- package/src/engine.ts +38 -2
- package/src/error-resolution.ts +58 -0
- package/src/errors.ts +77 -1
- package/src/i18n/error-messages.ts +255 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/keys.ts +80 -6
- package/src/index.ts +72 -0
- package/src/lint.ts +522 -50
- package/src/provider.ts +10 -0
- package/src/runtime/browser-telemetry-binding.ts +92 -0
- package/src/runtime/browser-telemetry.ts +289 -0
- package/src/runtime/browser.ts +249 -65
- package/src/runtime/cache-telemetry.ts +247 -0
- package/src/runtime/cache.ts +68 -17
- package/src/runtime/capability-response.ts +53 -0
- package/src/runtime/capability-telemetry.ts +101 -0
- package/src/runtime/diagnostic-env.ts +11 -1
- package/src/runtime/engine-mode.ts +233 -0
- package/src/runtime/http-telemetry-guard.ts +66 -13
- package/src/runtime/http.ts +168 -37
- package/src/runtime/instrumentation.ts +105 -16
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +109 -74
- package/src/runtime/request-telemetry.ts +2 -1
- package/src/runtime/resolver-runtime-options.ts +50 -0
- package/src/runtime/resolver-shared.ts +9 -4
- package/src/runtime/resolver-telemetry.ts +1 -1
- package/src/runtime/resolver-vendors/browser.ts +65 -7
- package/src/runtime/resolver-vendors/hosts.ts +43 -0
- package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
- package/src/runtime/resolver-vendors/types.ts +5 -4
- package/src/runtime/resolver.ts +64 -21
- package/src/runtime/state-telemetry.ts +298 -0
- package/src/runtime/state.ts +46 -45
- package/src/runtime/stealth-cookies.ts +24 -1
- package/src/runtime/stealth-owned-headers.ts +41 -0
- package/src/runtime/stealth-telemetry.ts +304 -0
- package/src/runtime/stealth.ts +355 -150
- package/src/runtime/stt-telemetry.ts +282 -0
- package/src/runtime/stt.ts +93 -71
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +53 -0
- package/src/server/sdk-version.ts +22 -0
- package/src/server/self-test.ts +1 -21
- package/src/server/sensitive-values.ts +14 -0
- package/src/server/serve-implementation.ts +569 -108
- package/src/server/types.ts +7 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
- package/src/stateful/stateful-provider-runtime-context.ts +4 -0
- package/src/types.ts +99 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
package/dist/server/types.d.ts
CHANGED
|
@@ -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>;
|
package/dist/server/types.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|
package/src/cli/commands.ts
CHANGED
|
@@ -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
|
+
}
|