@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.61
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 +17 -16
- package/CHANGELOG.md +4 -0
- package/bin/apifuse-dev.ts +5 -7
- package/bin/apifuse-pack-types.ts +22 -0
- package/bin/apifuse-perf.ts +4 -6
- package/bin/apifuse-record.ts +4 -7
- package/bin/apifuse-submit-check.ts +2 -2
- package/dist/define.d.ts +3 -2
- package/dist/define.js +1 -1
- package/dist/dev.js +3 -1
- package/dist/engine.d.ts +7 -6
- package/dist/engine.js +3 -4
- package/dist/error-resolution.js +21 -4
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +9 -0
- package/dist/handle-meta.d.ts +39 -0
- package/dist/handle-meta.js +29 -0
- package/dist/handle.d.ts +187 -0
- package/dist/handle.js +254 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +352 -47
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +895 -0
- package/dist/runtime/native-network.d.ts +10 -1
- package/dist/runtime/native-network.js +230 -29
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +0 -3
- package/dist/server/serve-implementation.d.ts +4 -11
- package/dist/server/serve-implementation.js +86 -18
- package/dist/testing/run.js +10 -7
- package/dist/types.d.ts +4 -107
- package/package.json +1 -1
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +5 -6
- package/src/error-resolution.ts +21 -4
- package/src/errors.ts +13 -0
- package/src/handle-meta.ts +58 -0
- package/src/handle.ts +529 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/index.ts +26 -13
- package/src/lint.ts +414 -55
- package/src/provider.ts +9 -20
- package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1162 -0
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/state.ts +44 -16
- package/src/server/index.ts +17 -0
- package/src/server/sensitive-values.ts +0 -3
- package/src/server/serve-implementation.ts +87 -32
- package/src/testing/run.ts +11 -7
- package/src/types.ts +4 -137
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice-wordlist.d.ts +0 -9
- package/dist/runtime/choice.d.ts +0 -27
- package/dist/runtime/choice.js +0 -801
- package/src/choice-token.ts +0 -165
- package/src/runtime/choice.ts +0 -1197
package/dist/testing/run.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { ProviderError } from "../errors.js";
|
|
3
3
|
import { createProviderCache } from "../runtime/cache.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createTestHandleContext } from "../runtime/handle.js";
|
|
5
|
+
/**
|
|
6
|
+
* Stable connection scope for the local test harness so bound handles (drafts,
|
|
7
|
+
* default cursors) can be exercised exactly as in a connected production call.
|
|
8
|
+
*/
|
|
9
|
+
const STANDARD_TEST_CONNECTION_ID = "standard-test-connection";
|
|
5
10
|
import { createMemoryProviderRuntimeState } from "../runtime/state.js";
|
|
6
11
|
import { createUnsupportedOcrClient } from "../runtime/ocr.js";
|
|
7
12
|
import { createUnsupportedSttClient } from "../runtime/stt.js";
|
|
@@ -190,7 +195,7 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
|
|
|
190
195
|
getAccessToken: () => undefined,
|
|
191
196
|
getScopes: () => [],
|
|
192
197
|
};
|
|
193
|
-
const request = { headers: {} };
|
|
198
|
+
const request = { headers: {}, connectionId: STANDARD_TEST_CONNECTION_ID };
|
|
194
199
|
const state = createMemoryProviderRuntimeState();
|
|
195
200
|
const nativeEgress = provider.native
|
|
196
201
|
? createNativeEgressAuthorization({ egress: provider.native.network })
|
|
@@ -411,10 +416,9 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
|
|
|
411
416
|
resolver: {
|
|
412
417
|
solve: async () => unsupported("ctx.resolver.solve"),
|
|
413
418
|
},
|
|
414
|
-
|
|
419
|
+
handle: createTestHandleContext({
|
|
415
420
|
providerId: `standard-test-${operationName}`,
|
|
416
421
|
request,
|
|
417
|
-
credential,
|
|
418
422
|
state,
|
|
419
423
|
}),
|
|
420
424
|
};
|
|
@@ -447,7 +451,7 @@ export function createSnapshotContext(rawFixture) {
|
|
|
447
451
|
getAccessToken: () => undefined,
|
|
448
452
|
getScopes: () => [],
|
|
449
453
|
};
|
|
450
|
-
const request = { headers: {} };
|
|
454
|
+
const request = { headers: {}, connectionId: STANDARD_TEST_CONNECTION_ID };
|
|
451
455
|
const state = createMemoryProviderRuntimeState();
|
|
452
456
|
const streamCaptureGroup = findStreamCaptureGroup(rawFixture);
|
|
453
457
|
let nextCaptureItem = 0;
|
|
@@ -521,10 +525,9 @@ export function createSnapshotContext(rawFixture) {
|
|
|
521
525
|
resolver: {
|
|
522
526
|
solve: async () => unsupported("ctx.resolver.solve"),
|
|
523
527
|
},
|
|
524
|
-
|
|
528
|
+
handle: createTestHandleContext({
|
|
525
529
|
providerId: "standard-test",
|
|
526
530
|
request,
|
|
527
|
-
credential,
|
|
528
531
|
state,
|
|
529
532
|
}),
|
|
530
533
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type ms from "ms";
|
|
2
|
+
import type { HandleContext, HandleKind } from "./handle.js";
|
|
2
3
|
import type { HealthScenario } from "./health-scenario.js";
|
|
3
4
|
import type { SerializedCookieJar } from "tough-cookie";
|
|
4
5
|
import type { infer as ZodInfer, ZodType } from "zod";
|
|
@@ -1660,110 +1661,6 @@ export interface ProviderRequestContext {
|
|
|
1660
1661
|
connectionId?: string;
|
|
1661
1662
|
headers: Record<string, string>;
|
|
1662
1663
|
}
|
|
1663
|
-
export interface ProviderChoiceBindingOptions {
|
|
1664
|
-
connection?: boolean;
|
|
1665
|
-
credentialKeys?: readonly string[];
|
|
1666
|
-
}
|
|
1667
|
-
export type ProviderChoiceConsumeMode = "never" | "on-parse" | "explicit";
|
|
1668
|
-
export type ProviderChoiceConsumeResult = {
|
|
1669
|
-
readonly status: "consumed";
|
|
1670
|
-
} | {
|
|
1671
|
-
readonly status: "already-consumed";
|
|
1672
|
-
} | {
|
|
1673
|
-
readonly status: "unsupported";
|
|
1674
|
-
};
|
|
1675
|
-
export type ProviderChoiceExplicitParseResult = {
|
|
1676
|
-
readonly status: "active";
|
|
1677
|
-
readonly payload: Record<string, unknown>;
|
|
1678
|
-
/** Stable, opaque key for provider-owned idempotency records. */
|
|
1679
|
-
readonly replayKey: string;
|
|
1680
|
-
/** Atomically claims a word token. Inline tokens report unsupported. */
|
|
1681
|
-
consume(): Promise<ProviderChoiceConsumeResult>;
|
|
1682
|
-
} | {
|
|
1683
|
-
readonly status: "consumed";
|
|
1684
|
-
/** Use this key to read the provider-owned result before returning an error. */
|
|
1685
|
-
readonly replayKey: string;
|
|
1686
|
-
};
|
|
1687
|
-
export type ProviderChoiceStorageOptions = {
|
|
1688
|
-
readonly mode: "inline";
|
|
1689
|
-
} | {
|
|
1690
|
-
readonly mode: "server";
|
|
1691
|
-
readonly namespace: string;
|
|
1692
|
-
readonly state?: ProviderRuntimeState;
|
|
1693
|
-
readonly ttl?: ProviderStateDurationString;
|
|
1694
|
-
readonly maxEntries: number;
|
|
1695
|
-
readonly maxValueBytes: number;
|
|
1696
|
-
readonly unavailable?: "reject";
|
|
1697
|
-
} | {
|
|
1698
|
-
readonly mode: "auto";
|
|
1699
|
-
readonly namespace: string;
|
|
1700
|
-
readonly state?: ProviderRuntimeState;
|
|
1701
|
-
readonly ttl?: ProviderStateDurationString;
|
|
1702
|
-
readonly maxInlineBytes: number;
|
|
1703
|
-
readonly maxEntries: number;
|
|
1704
|
-
readonly maxValueBytes: number;
|
|
1705
|
-
readonly unavailable?: "reject";
|
|
1706
|
-
};
|
|
1707
|
-
export interface ProviderChoiceIssueOptions<TPayload extends Record<string, unknown>> {
|
|
1708
|
-
prefix: string;
|
|
1709
|
-
purpose: string;
|
|
1710
|
-
payload: TPayload;
|
|
1711
|
-
ttlMs: number;
|
|
1712
|
-
nowMs?: number;
|
|
1713
|
-
bind?: ProviderChoiceBindingOptions;
|
|
1714
|
-
/** Server storage only: standard emits four words; high emits five. */
|
|
1715
|
-
strength?: "standard" | "high";
|
|
1716
|
-
storage?: ProviderChoiceStorageOptions;
|
|
1717
|
-
}
|
|
1718
|
-
export interface ProviderChoiceParseOptions {
|
|
1719
|
-
token: string;
|
|
1720
|
-
prefix: string;
|
|
1721
|
-
purpose: string;
|
|
1722
|
-
ttlMs?: number;
|
|
1723
|
-
nowMs?: number;
|
|
1724
|
-
futureToleranceMs?: number;
|
|
1725
|
-
bind?: ProviderChoiceBindingOptions;
|
|
1726
|
-
storage?: ProviderChoiceStorageOptions;
|
|
1727
|
-
/** Defaults to never, preserving reusable choice-token parse semantics. */
|
|
1728
|
-
consume?: ProviderChoiceConsumeMode;
|
|
1729
|
-
}
|
|
1730
|
-
export interface ProviderChoiceContext {
|
|
1731
|
-
issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload> & {
|
|
1732
|
-
readonly storage?: {
|
|
1733
|
-
readonly mode: "inline";
|
|
1734
|
-
};
|
|
1735
|
-
}): string;
|
|
1736
|
-
issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload> & {
|
|
1737
|
-
readonly storage: Extract<ProviderChoiceStorageOptions, {
|
|
1738
|
-
readonly mode: "server";
|
|
1739
|
-
}>;
|
|
1740
|
-
}): Promise<string>;
|
|
1741
|
-
issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload> & {
|
|
1742
|
-
readonly storage: Extract<ProviderChoiceStorageOptions, {
|
|
1743
|
-
readonly mode: "auto";
|
|
1744
|
-
}>;
|
|
1745
|
-
}): string | Promise<string>;
|
|
1746
|
-
issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload>): string | Promise<string>;
|
|
1747
|
-
parse(options: ProviderChoiceParseOptions & {
|
|
1748
|
-
readonly consume: "explicit";
|
|
1749
|
-
}): Promise<ProviderChoiceExplicitParseResult>;
|
|
1750
|
-
parse(options: ProviderChoiceParseOptions & {
|
|
1751
|
-
readonly storage?: {
|
|
1752
|
-
readonly mode: "inline";
|
|
1753
|
-
};
|
|
1754
|
-
}): Record<string, unknown>;
|
|
1755
|
-
parse(options: ProviderChoiceParseOptions & {
|
|
1756
|
-
readonly storage: Extract<ProviderChoiceStorageOptions, {
|
|
1757
|
-
readonly mode: "server";
|
|
1758
|
-
}>;
|
|
1759
|
-
}): Promise<Record<string, unknown>>;
|
|
1760
|
-
parse(options: ProviderChoiceParseOptions & {
|
|
1761
|
-
readonly storage: Extract<ProviderChoiceStorageOptions, {
|
|
1762
|
-
readonly mode: "auto";
|
|
1763
|
-
}>;
|
|
1764
|
-
}): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
1765
|
-
parse(options: ProviderChoiceParseOptions): Record<string, unknown> | Promise<Record<string, unknown> | ProviderChoiceExplicitParseResult>;
|
|
1766
|
-
}
|
|
1767
1664
|
export interface ContextScratchpad {
|
|
1768
1665
|
get(key: string): unknown;
|
|
1769
1666
|
set(key: string, value: unknown): void;
|
|
@@ -1997,8 +1894,8 @@ export type ProviderContext<TConfig = Record<string, unknown>> = {
|
|
|
1997
1894
|
stt: SttContext;
|
|
1998
1895
|
} : Record<never, never>) & ("resolver" extends keyof TConfig ? {
|
|
1999
1896
|
resolver: ResolverContext;
|
|
2000
|
-
} : Record<never, never>) & ("
|
|
2001
|
-
|
|
1897
|
+
} : Record<never, never>) & ("handle" extends keyof TConfig ? {
|
|
1898
|
+
handle: HandleContext;
|
|
2002
1899
|
} : Record<never, never>);
|
|
2003
1900
|
/** Backwards-compatible name for a declaration-derived provider context. */
|
|
2004
1901
|
export type ProviderContextFor<TConfig> = ProviderContext<TConfig>;
|
|
@@ -2159,7 +2056,7 @@ export interface ProviderDefinition<TContext = ProviderContext> {
|
|
|
2159
2056
|
engine: BrowserEngine;
|
|
2160
2057
|
};
|
|
2161
2058
|
auth?: AuthConfig;
|
|
2162
|
-
|
|
2059
|
+
handle?: readonly HandleKind[];
|
|
2163
2060
|
reviewed?: ProviderReviewed;
|
|
2164
2061
|
access?: ProviderAccessConfig;
|
|
2165
2062
|
secrets?: ProviderSecretDeclaration[];
|
package/package.json
CHANGED
package/src/define.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./native-egress-policy.js";
|
|
22
22
|
import { safeParseSchemaSync } from "./schema.js";
|
|
23
23
|
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
24
|
+
import type { HandleKind } from "./handle.js";
|
|
24
25
|
import type {
|
|
25
26
|
AuthConfig,
|
|
26
27
|
BrowserEngine,
|
|
@@ -601,8 +602,8 @@ export interface ProviderDeclaration {
|
|
|
601
602
|
resolver?: ProviderResolverConfig;
|
|
602
603
|
browser?: { engine: BrowserEngine };
|
|
603
604
|
auth?: AuthConfig;
|
|
604
|
-
/** Declares the
|
|
605
|
-
|
|
605
|
+
/** Declares the handle kinds this provider issues and accepts (ADR-0012). */
|
|
606
|
+
handle?: readonly HandleKind[];
|
|
606
607
|
reviewed?: ProviderReviewed;
|
|
607
608
|
access?: ProviderAccessConfig;
|
|
608
609
|
/** Declares secret requirements; this does not add a `ctx.secrets` member. */
|
|
@@ -3042,7 +3043,7 @@ function finalizeProvider<
|
|
|
3042
3043
|
resolver: config.resolver,
|
|
3043
3044
|
browser: config.browser,
|
|
3044
3045
|
auth: config.auth,
|
|
3045
|
-
|
|
3046
|
+
handle: config.handle,
|
|
3046
3047
|
reviewed: config.reviewed,
|
|
3047
3048
|
access: config.access,
|
|
3048
3049
|
secrets: config.secrets,
|
package/src/dev.ts
CHANGED
|
@@ -17,7 +17,9 @@ export function createDevServer(
|
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
19
|
start: () => {
|
|
20
|
-
|
|
20
|
+
// Local development has no Redis by default; handles (ADR-0012) need a
|
|
21
|
+
// state backend, so fall back to memory state when none is configured.
|
|
22
|
+
void serve(provider, { port, engine: options?.engine, allowMemoryStateFallback: true });
|
|
21
23
|
console.log(
|
|
22
24
|
`[apifuse dev] ${provider.id}@${provider.version} running at http://localhost:${port}`,
|
|
23
25
|
);
|
package/src/engine.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
OTEL_RESOURCE_ATTRIBUTES,
|
|
15
15
|
OTEL_SERVICE_NAME,
|
|
16
16
|
} from "./runtime/otlp.js";
|
|
17
|
+
import type { HandleContext } from "./handle.js";
|
|
17
18
|
import type {
|
|
18
19
|
AuthContext,
|
|
19
20
|
BrowserClient,
|
|
@@ -23,7 +24,6 @@ import type {
|
|
|
23
24
|
NativeContext,
|
|
24
25
|
OcrContext,
|
|
25
26
|
ProviderCache,
|
|
26
|
-
ProviderChoiceContext,
|
|
27
27
|
ProviderContext,
|
|
28
28
|
ProviderDefinition,
|
|
29
29
|
ProviderFilesContext,
|
|
@@ -118,8 +118,8 @@ function isEngineOwnedEngineEnvName(name: string): boolean {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* Hosted runtime settings the engine reads for OCR, STT, cache keying,
|
|
122
|
-
*
|
|
121
|
+
* Hosted runtime settings the engine reads for OCR, STT, cache keying, and
|
|
122
|
+
* the CDP pool. Providers never own them: declarations are rejected
|
|
123
123
|
* and projections drop them, like the vendor credentials above.
|
|
124
124
|
*/
|
|
125
125
|
export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
|
|
@@ -128,7 +128,6 @@ export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
|
|
|
128
128
|
"APIFUSE__OCR__API_KEY",
|
|
129
129
|
"APIFUSE__CLOUDFLARE__ACCOUNT_ID",
|
|
130
130
|
"APIFUSE__CACHE__KEY_PEPPER",
|
|
131
|
-
"APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
|
|
132
131
|
] as const;
|
|
133
132
|
|
|
134
133
|
const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set<string>(ENGINE_OWNED_RUNTIME_ENV_NAMES);
|
|
@@ -237,7 +236,7 @@ export const PROVIDER_CAPABILITY_KEYS = [
|
|
|
237
236
|
"ocr",
|
|
238
237
|
"stt",
|
|
239
238
|
"resolver",
|
|
240
|
-
"
|
|
239
|
+
"handle",
|
|
241
240
|
] as const;
|
|
242
241
|
|
|
243
242
|
export type ProviderCapabilityKey = (typeof PROVIDER_CAPABILITY_KEYS)[number];
|
|
@@ -249,7 +248,7 @@ export type ProviderEngineBindingCandidates = Partial<
|
|
|
249
248
|
readonly credential: CredentialContext;
|
|
250
249
|
readonly files: ProviderFilesContext;
|
|
251
250
|
readonly auth: AuthContext;
|
|
252
|
-
readonly
|
|
251
|
+
readonly handle: HandleContext;
|
|
253
252
|
}
|
|
254
253
|
> & {
|
|
255
254
|
readonly request?: ProviderRequestContext;
|
package/src/error-resolution.ts
CHANGED
|
@@ -20,10 +20,17 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
20
20
|
"refresh_not_supported",
|
|
21
21
|
"RUNTIME_UNSUPPORTED",
|
|
22
22
|
"PROVIDER_STATE_UNSUPPORTED",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
23
|
+
"HANDLE_INVALID",
|
|
24
|
+
"HANDLE_NOT_FOUND",
|
|
25
|
+
"HANDLE_EXPIRED",
|
|
26
|
+
"HANDLE_KIND_MISMATCH",
|
|
27
|
+
"HANDLE_BUSY",
|
|
28
|
+
"HANDLE_COMMITTED",
|
|
29
|
+
"HANDLE_CONNECTION_REQUIRED",
|
|
30
|
+
"HANDLE_STORAGE_UNAVAILABLE",
|
|
31
|
+
"HANDLE_TOO_LARGE",
|
|
32
|
+
"HANDLE_INVALID_DATA",
|
|
33
|
+
"PICK_NOT_OFFERED",
|
|
27
34
|
"unsupported_stealth_cookie_store_version",
|
|
28
35
|
"provider_secret_error",
|
|
29
36
|
"credential_key_error",
|
|
@@ -130,6 +137,16 @@ export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES: ReadonlyMap<string, Provide
|
|
|
130
137
|
["EGRESS_LEASE_INVALID", 409],
|
|
131
138
|
["REPLAY_SESSION_MISMATCH", 409],
|
|
132
139
|
["REPLAY_ALREADY_ATTEMPTED", 409],
|
|
140
|
+
// Handles (ADR-0012): only caller-side faults are mapped. Storage,
|
|
141
|
+
// connection-scope, size, and schema faults stay 500 because the provider
|
|
142
|
+
// or host owns them.
|
|
143
|
+
["HANDLE_INVALID", 400],
|
|
144
|
+
["HANDLE_KIND_MISMATCH", 400],
|
|
145
|
+
["PICK_NOT_OFFERED", 400],
|
|
146
|
+
["HANDLE_NOT_FOUND", 404],
|
|
147
|
+
["HANDLE_EXPIRED", 410],
|
|
148
|
+
["HANDLE_COMMITTED", 409],
|
|
149
|
+
["HANDLE_BUSY", 409],
|
|
133
150
|
["UPSTREAM_ERROR", 502],
|
|
134
151
|
["BLOCKED", 502],
|
|
135
152
|
["OCR_UNAVAILABLE", 503],
|
package/src/errors.ts
CHANGED
|
@@ -12,6 +12,7 @@ const PROVIDER_ERROR_BRAND_VALUE = 1;
|
|
|
12
12
|
const SESSION_EXPIRED_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/session-expired@1");
|
|
13
13
|
const TRANSPORT_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/transport@1");
|
|
14
14
|
const VALIDATION_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/validation@1");
|
|
15
|
+
const HANDLE_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/handle@1");
|
|
15
16
|
|
|
16
17
|
// Defines a non-enumerable, non-writable, non-configurable own data property.
|
|
17
18
|
// Immutable + own means a guard can trust it via a single descriptor read
|
|
@@ -208,6 +209,18 @@ export function isTransportError(value: unknown): value is TransportError {
|
|
|
208
209
|
return isProviderError(value) && hasOwnBrand(value, TRANSPORT_BRAND, true);
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
/** Brands a handle error (ADR-0012) so `isHandleError` works across duplicate SDK module copies. */
|
|
213
|
+
export function brandHandleError(target: object): void {
|
|
214
|
+
defineErrorBrand(target, HANDLE_BRAND, true);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function hasHandleErrorBrand(value: unknown): boolean {
|
|
218
|
+
return (
|
|
219
|
+
hasOwnBrand(value, PROVIDER_ERROR_BRAND, PROVIDER_ERROR_BRAND_VALUE) &&
|
|
220
|
+
hasOwnBrand(value, HANDLE_BRAND, true)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
export function isValidationError(value: unknown): value is ValidationError {
|
|
212
225
|
return (
|
|
213
226
|
isProviderError(value) &&
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, dependency-free definitions for LLM-friendly handles (ADR-0012).
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the runtime so schema helpers, lint, and the runtime can
|
|
5
|
+
* agree on the JSON Schema meta key and the declaration shape without importing
|
|
6
|
+
* each other.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** JSON Schema meta key placed on every schema field created by `kind.field()`. */
|
|
10
|
+
export const APIFUSE_HANDLE_META_KEY = "x-apifuse-handle" as const;
|
|
11
|
+
|
|
12
|
+
export type HandleKindType = "cursor" | "draft";
|
|
13
|
+
export type HandleAccess = "bound" | "public";
|
|
14
|
+
|
|
15
|
+
/** Value stored under {@link APIFUSE_HANDLE_META_KEY} on a handle field. */
|
|
16
|
+
export interface HandleFieldMeta {
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
readonly type: HandleKindType;
|
|
19
|
+
readonly fieldName: string;
|
|
20
|
+
readonly issuedBy?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Structural view of a handle kind as seen by the provider declaration
|
|
25
|
+
* (`handle: [WaitingDraft, PageCursor]`), lint, and the registry. The runtime
|
|
26
|
+
* kinds returned by `defineCursor` / `defineDraft` satisfy this shape.
|
|
27
|
+
*/
|
|
28
|
+
export interface HandleKindDeclaration {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly type: HandleKindType;
|
|
31
|
+
readonly fieldName: string;
|
|
32
|
+
readonly access: HandleAccess;
|
|
33
|
+
readonly issuedBy?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Kind names are lowercase letters only so they never fuse with the word body. */
|
|
37
|
+
export const HANDLE_KIND_NAME_PATTERN = /^[a-z]{2,12}$/;
|
|
38
|
+
|
|
39
|
+
export function isHandleFieldMeta(value: unknown): value is HandleFieldMeta {
|
|
40
|
+
if (typeof value !== "object" || value === null) return false;
|
|
41
|
+
const record = value as Record<string, unknown>;
|
|
42
|
+
return (
|
|
43
|
+
typeof record.kind === "string" &&
|
|
44
|
+
(record.type === "cursor" || record.type === "draft") &&
|
|
45
|
+
typeof record.fieldName === "string" &&
|
|
46
|
+
(record.issuedBy === undefined || typeof record.issuedBy === "string")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The SDK-owned description `kind.field()` emits. Lint exempts a handle field
|
|
52
|
+
* from `describeKey` only while its description is exactly this text; a
|
|
53
|
+
* provider override re-enters the localization rules.
|
|
54
|
+
*/
|
|
55
|
+
export function handleFieldDescription(meta: Pick<HandleFieldMeta, "issuedBy">): string {
|
|
56
|
+
const origin = meta.issuedBy ? ` issued by \`${meta.issuedBy}\`` : "";
|
|
57
|
+
return `Opaque handle${origin}. Copy it exactly as returned; do not edit or shorten it.`;
|
|
58
|
+
}
|