@apifuse/provider-sdk 2.2.0-beta.63 → 2.2.0-beta.65
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 +107 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/bin/apifuse-check.ts +17 -2
- package/bin/apifuse-submit-check.ts +9 -5
- package/dist/handle-meta.d.ts +36 -1
- package/dist/handle-meta.js +16 -0
- package/dist/handle.d.ts +23 -5
- package/dist/handle.js +54 -8
- package/dist/health-scenario.d.ts +38 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/lint.d.ts +5 -0
- package/dist/lint.js +582 -33
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/handle.js +9 -1
- package/dist/runtime/ocr.js +5 -0
- package/dist/runtime-boundary-lint.d.ts +137 -0
- package/dist/runtime-boundary-lint.js +590 -0
- package/dist/types.d.ts +29 -0
- package/dist/typescript-module.d.ts +39 -0
- package/dist/typescript-module.js +72 -0
- package/package.json +1 -1
- package/src/handle-meta.ts +51 -1
- package/src/handle.ts +92 -10
- package/src/health-scenario.ts +38 -0
- package/src/index.ts +14 -0
- package/src/lint.ts +668 -41
- package/src/provider.ts +2 -0
- package/src/runtime/handle.ts +9 -1
- package/src/runtime/ocr.ts +5 -0
- package/src/runtime-boundary-lint.ts +707 -0
- package/src/types.ts +29 -0
- package/src/typescript-module.ts +100 -0
package/AUTHORING.md
CHANGED
|
@@ -136,6 +136,8 @@ Hosted runtime settings are engine-owned as well: every `APIFUSE__CDP_POOL__*`
|
|
|
136
136
|
variable, the Cloudflare OCR/STT tokens and account identifier,
|
|
137
137
|
`APIFUSE__OCR__API_KEY`, and `APIFUSE__CACHE__KEY_PEPPER`. `defineProvider`
|
|
138
138
|
rejects them in `secrets`; the engine reads them from its own environment.
|
|
139
|
+
Provider code must not probe those names either — to learn whether OCR is
|
|
140
|
+
configured, read `ctx.ocr.available` (see the OCR section below).
|
|
139
141
|
|
|
140
142
|
### Engine mode
|
|
141
143
|
|
|
@@ -455,6 +457,40 @@ Best-practice rules:
|
|
|
455
457
|
- Submission checks and health checks must not invoke live STT by default.
|
|
456
458
|
Provide explicit smoke evidence when a provider depends on audio OTP behavior.
|
|
457
459
|
|
|
460
|
+
### OCR runtime capability: availability is the SDK's verdict
|
|
461
|
+
|
|
462
|
+
Declare `ocr: { mode: "required" | "optional" }` and call `ctx.ocr` from
|
|
463
|
+
operation handlers or auth-flow handlers. Whether a working backend is wired
|
|
464
|
+
in is decided by the SDK, never by provider code: `ctx.ocr.available` is
|
|
465
|
+
`true` when `createOcrClientFromEnv` found a configured backend and `false`
|
|
466
|
+
for the unsupported/error client that undeclared or misconfigured providers
|
|
467
|
+
receive. Branch on that flag for behavior that depends on OCR being present
|
|
468
|
+
(for example, whether a CAPTCHA field is required from the user), and keep the
|
|
469
|
+
`OCR_UNAVAILABLE` / `UNSUPPORTED_OCR_BACKEND` error fallback for the call
|
|
470
|
+
itself: `available: true` promises a configured backend, not a successful
|
|
471
|
+
upstream call.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
async start(ctx) {
|
|
475
|
+
const image = await fetchCaptcha(ctx);
|
|
476
|
+
return ctx.auth.nextForm({
|
|
477
|
+
turnId: "credentials",
|
|
478
|
+
fields: {
|
|
479
|
+
captchaText: { type: "string", required: !ctx.ocr.available, /* ... */ },
|
|
480
|
+
},
|
|
481
|
+
data: { captchaImage: image, ocrConfigured: ctx.ocr.available },
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Do not re-implement the predicate by reading `APIFUSE__OCR__*` or
|
|
487
|
+
`APIFUSE__CLOUDFLARE__ACCOUNT_ID`: those names are engine-owned, the
|
|
488
|
+
runtime-boundary lint reports the read, and a drift between the copy and the
|
|
489
|
+
SDK's own selection logic downgrades every login to the manual path silently.
|
|
490
|
+
|
|
491
|
+
A custom `OcrContext` override (`serve({ ocr })`, `createFlowContext({ ocr })`,
|
|
492
|
+
or a hand-written test double) must set `available` explicitly as a plain data
|
|
493
|
+
property; the SDK's own clients do, and `bindOcrTelemetry` preserves it.
|
|
458
494
|
|
|
459
495
|
### Health journey DX for SMS/payment flows
|
|
460
496
|
|
|
@@ -857,6 +893,77 @@ Note the asymmetry: the gate treats whitespace-only values as missing, but
|
|
|
857
893
|
`ctx.env.get()` still returns the raw value to handlers — trim at the point of
|
|
858
894
|
use if the upstream is whitespace-sensitive.
|
|
859
895
|
|
|
896
|
+
### Runtime boundary: no `process.env`, `node:fs`, `child_process`, or raw `fetch()`
|
|
897
|
+
|
|
898
|
+
Provider runtime code reaches the outside world through the context only:
|
|
899
|
+
`ctx.http` / `ctx.stealth` for HTTP, `ctx.browser` for browser work,
|
|
900
|
+
`ctx.env.get()` / `ctx.credential` for configuration and secrets, `ctx.files` /
|
|
901
|
+
`ctx.cache` / `ctx.state` for data that must outlive a request. Ambient Node
|
|
902
|
+
and Bun globals bypass `allowedHosts`, proxy policy, retries, redaction, the
|
|
903
|
+
secret presence gate, and telemetry. `apifuse check` reports them at **error**
|
|
904
|
+
level — a finding fails `apifuse check` and `apifuse submit-check`, in the
|
|
905
|
+
official and the bounty `standalone` mode alike (the level was `warn` for one
|
|
906
|
+
release as a migration window; the window closed once the fleet's request-path
|
|
907
|
+
hits were fixed or acknowledged — a provider that still carries one sees
|
|
908
|
+
`apifuse check` fail on its next SDK bump and fixes or acknowledges it in that
|
|
909
|
+
same PR):
|
|
910
|
+
|
|
911
|
+
- `process-env-direct-read` — `process.env.X`, `process.env["X"]`, a bare
|
|
912
|
+
`process.env` object use, `Bun.env`. The `APIFUSE__RUNTIME__*` bootstrap
|
|
913
|
+
family (`APIFUSE__RUNTIME__PORT`, `…POD_ID`, `…POD_ENDPOINT`) is exempt
|
|
914
|
+
everywhere; every other value is declared in `defineProvider` under
|
|
915
|
+
`secrets: [{ name, required }]` and read with `ctx.env.get(name)`. The names
|
|
916
|
+
visible to `ctx.env.get` are exactly the declared `secrets[].name` (minus
|
|
917
|
+
engine-owned names; an `oauth2_proxied` auth flow additionally sees
|
|
918
|
+
`APIFUSE__AUTH_PROXY__URL`). `env: true` (or `env: {}`) is the capability
|
|
919
|
+
declaration that turns the `ctx.env` binding on — a type *and* runtime gate,
|
|
920
|
+
since touching `ctx.env` without it throws `PROVIDER_CAPABILITY_UNDECLARED`
|
|
921
|
+
— and does not widen the readable names. Test/E2E toggles are not secrets
|
|
922
|
+
and cannot move to `ctx.env`: replace them with test doubles (inject
|
|
923
|
+
`ctx.env` / `ctx.http`) or recorded fixtures.
|
|
924
|
+
- `node-runtime-module-import` — value imports of `fs`, `fs/promises`, `net`,
|
|
925
|
+
`tls`, `dgram`, `http`, `https`, `http2`, `child_process` (with or without
|
|
926
|
+
`node:`), and `Bun.spawn` / `Bun.spawnSync` / ``Bun.$` ` `` / `Bun.file` /
|
|
927
|
+
`Bun.write`. Type-only imports (`import type`, or every binding inline `type`) are ignored; `node:path`, `node:crypto`,
|
|
928
|
+
`node:url` are fine.
|
|
929
|
+
- `direct-fetch-call` — the global `fetch()` (also `globalThis.fetch`).
|
|
930
|
+
`ctx.stealth.fetch()` never matches, nor does a call to a `fetch` binding
|
|
931
|
+
declared in an enclosing scope (parameter, variable, import, catch clause).
|
|
932
|
+
The check is lexical: a wrapper that takes `fetch` as a parameter does not
|
|
933
|
+
hide a global `fetch()` in another function of the same file.
|
|
934
|
+
|
|
935
|
+
Scope is runtime source only: tests, recorded fixtures, `.d.ts`, the root
|
|
936
|
+
`dev.ts` / `start.ts` / `deploy.ts` entrypoints, and the `scripts/`, `tools/`,
|
|
937
|
+
`bin/` directories at the provider root are not scanned. Keep fixture
|
|
938
|
+
recorders, smoke scripts, and other operator tooling under `scripts/`, and do
|
|
939
|
+
not import from there in request-path modules. `apifuse check` also leaves out
|
|
940
|
+
any directory that is itself a git checkout (a linked worktree such as
|
|
941
|
+
`.worktree/<branch>/`, a nested clone, an initialised submodule): its files are
|
|
942
|
+
another revision, checked in that checkout, not this provider's runtime source.
|
|
943
|
+
|
|
944
|
+
Acknowledge a deliberate exception on the finding line or the comment-only
|
|
945
|
+
line above it with `// @apifuse-allow <rule>: <reason>`; the finding then
|
|
946
|
+
appears as an `INFO` audit line instead of an error, and the check passes.
|
|
947
|
+
|
|
948
|
+
```ts
|
|
949
|
+
// Before: request-path module reads the ambient environment and the disk
|
|
950
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
951
|
+
const serviceKey = process.env.EXAMPLE_SERVICE_KEY;
|
|
952
|
+
if (process.env.EXAMPLE_DIAG_TAP_DIR) {
|
|
953
|
+
await writeFile(`${process.env.EXAMPLE_DIAG_TAP_DIR}/booking.json`, JSON.stringify(response));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// After: the credential is a declared secret read through the context —
|
|
957
|
+
// defineProvider({
|
|
958
|
+
// env: true, // turns the ctx.env capability on (type + runtime gate)
|
|
959
|
+
// secrets: [{ name: "APIFUSE__PROVIDER__EXAMPLE__SERVICE_KEY", required: true }], // the readable names
|
|
960
|
+
// })
|
|
961
|
+
// — and the ad-hoc diagnostic tap is deleted: a toggle that is not a secret has
|
|
962
|
+
// no place in ctx.env; observability goes through ctx.trace instead.
|
|
963
|
+
const serviceKey = ctx.env.get("APIFUSE__PROVIDER__EXAMPLE__SERVICE_KEY");
|
|
964
|
+
const response = await ctx.trace.span("booking.confirm", () => confirmBooking(ctx, serviceKey));
|
|
965
|
+
```
|
|
966
|
+
|
|
860
967
|
### Credentials forced into query parameters
|
|
861
968
|
|
|
862
969
|
Prefer an authorization header or request body whenever the upstream supports
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.65
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 4731ac76145caf85a6c340ad0c446371bd24b052.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.64
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 0505a938340d08db3ea3e2302cab5f6b5dd2b4e5.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.63
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit a9b85acc5af4c40ec59774b0d1e864e7ed21828e.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vooy Korea, Inc. (주식회사 부이코리아)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/apifuse-check.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
import { z } from "zod";
|
|
@@ -226,6 +226,21 @@ function isScannableProviderSourceFile(relativePath: string): boolean {
|
|
|
226
226
|
);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* A directory under the provider root that is itself a git checkout — a linked
|
|
231
|
+
* worktree (`.worktree/<branch>/`, whose `.git` is a file pointing at the main
|
|
232
|
+
* repository), a nested clone (`.git` directory), or an initialised submodule.
|
|
233
|
+
* Its files are another revision of some repository, not this provider's
|
|
234
|
+
* runtime source: an older copy of `upstream/` or `scripts/` there would be
|
|
235
|
+
* linted as if it were request-path code at the root (the tooling-directory
|
|
236
|
+
* exemption is anchored at the provider root), and its `apifuse check` is run
|
|
237
|
+
* in that checkout, not here. Git refuses to track a path named `.git`, so the
|
|
238
|
+
* marker cannot be planted in a committed tree to hide a file from the scan.
|
|
239
|
+
*/
|
|
240
|
+
function isNestedCheckout(directory: string): boolean {
|
|
241
|
+
return existsSync(join(directory, ".git"));
|
|
242
|
+
}
|
|
243
|
+
|
|
229
244
|
function collectProviderSourceFiles(providerRoot: string): Record<string, string> {
|
|
230
245
|
const sources: Record<string, string> = {};
|
|
231
246
|
// `.agents`/`.apifuse` are deliberately not skipped: managed content there
|
|
@@ -236,7 +251,7 @@ function collectProviderSourceFiles(providerRoot: string): Record<string, string
|
|
|
236
251
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
237
252
|
const path = resolve(directory, entry.name);
|
|
238
253
|
if (entry.isDirectory()) {
|
|
239
|
-
if (!skipDirectories.has(entry.name)) {
|
|
254
|
+
if (!skipDirectories.has(entry.name) && !isNestedCheckout(path)) {
|
|
240
255
|
visit(path);
|
|
241
256
|
}
|
|
242
257
|
continue;
|
|
@@ -25,18 +25,22 @@ import {
|
|
|
25
25
|
hasStreamEvidenceMarker,
|
|
26
26
|
parseStreamEvidenceRecord,
|
|
27
27
|
} from "../src/stream-evidence.js";
|
|
28
|
+
import { getTypeScript } from "../src/typescript-module.js";
|
|
28
29
|
import { type CheckResult, PROMPT_ASSETS_CHECK_MESSAGE, runChecks } from "./apifuse-check.js";
|
|
29
30
|
import { hasSubstantiveDelimitedTextStructure } from "./submit-check-delimited-text.js";
|
|
30
31
|
import { hasSubstantiveXmlStructure } from "./submit-check-xml.js";
|
|
31
32
|
|
|
32
|
-
const ts: typeof import("typescript") =
|
|
33
|
+
const ts: typeof import("typescript") = loadTypeScript();
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
// Shares the lint rules' loader so a workspace whose `typescript` is the 7.x
|
|
36
|
+
// native shell (no compiler API) falls back to `@typescript/typescript6`
|
|
37
|
+
// instead of failing inside the first parse.
|
|
38
|
+
function loadTypeScript(): typeof import("typescript") {
|
|
35
39
|
try {
|
|
36
|
-
return
|
|
37
|
-
} catch {
|
|
40
|
+
return getTypeScript();
|
|
41
|
+
} catch (error) {
|
|
38
42
|
console.error(
|
|
39
|
-
|
|
43
|
+
`apifuse submit-check requires the TypeScript compiler API; install it in the workspace running the CLI (bun add -d typescript). ${error instanceof Error ? error.message : String(error)}`,
|
|
40
44
|
);
|
|
41
45
|
process.exit(1);
|
|
42
46
|
}
|
package/dist/handle-meta.d.ts
CHANGED
|
@@ -15,25 +15,60 @@ export type HandleAccess = "bound" | "public";
|
|
|
15
15
|
* `search-address` and `reverse-geocode` both issuing `location_token`).
|
|
16
16
|
*/
|
|
17
17
|
export type HandleIssuedBy = string | readonly string[];
|
|
18
|
-
/**
|
|
18
|
+
/** The schema side a handle field appears on. */
|
|
19
|
+
export type HandleFieldDirection = "input" | "output";
|
|
20
|
+
/**
|
|
21
|
+
* Direction-specific property keys for one handle kind, for the operations
|
|
22
|
+
* whose caller-facing contract names the same handle differently coming in and
|
|
23
|
+
* going out (the platform canon's `cursor` in, `next_cursor` out). The two keys
|
|
24
|
+
* are two spellings of ONE handle: the value a caller reads from `output` is
|
|
25
|
+
* the value it must send back under `input`, unedited.
|
|
26
|
+
*/
|
|
27
|
+
export interface HandleFieldNames {
|
|
28
|
+
readonly input: string;
|
|
29
|
+
readonly output: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Value stored under {@link APIFUSE_HANDLE_META_KEY} on a handle field.
|
|
33
|
+
*
|
|
34
|
+
* `fieldName` is the INPUT property key — the name a caller sends the handle
|
|
35
|
+
* back under, and the name every runtime error message quotes. `outputFieldName`
|
|
36
|
+
* is present only when the output key differs, so a kind with one name emits
|
|
37
|
+
* exactly the meta it always did.
|
|
38
|
+
*/
|
|
19
39
|
export interface HandleFieldMeta {
|
|
20
40
|
readonly kind: string;
|
|
21
41
|
readonly type: HandleKindType;
|
|
22
42
|
readonly fieldName: string;
|
|
43
|
+
readonly outputFieldName?: string;
|
|
23
44
|
readonly issuedBy?: HandleIssuedBy;
|
|
24
45
|
}
|
|
25
46
|
/**
|
|
26
47
|
* Structural view of a handle kind as seen by the provider declaration
|
|
27
48
|
* (`handle: [WaitingDraft, PageCursor]`), lint, and the registry. The runtime
|
|
28
49
|
* kinds returned by `defineCursor` / `defineDraft` satisfy this shape.
|
|
50
|
+
*
|
|
51
|
+
* `fieldName` / `outputFieldName` follow the {@link HandleFieldMeta} rule:
|
|
52
|
+
* `fieldName` is the input key, `outputFieldName` is set only when the output
|
|
53
|
+
* key differs.
|
|
29
54
|
*/
|
|
30
55
|
export interface HandleKindDeclaration {
|
|
31
56
|
readonly name: string;
|
|
32
57
|
readonly type: HandleKindType;
|
|
33
58
|
readonly fieldName: string;
|
|
59
|
+
readonly outputFieldName?: string;
|
|
34
60
|
readonly access: HandleAccess;
|
|
35
61
|
readonly issuedBy?: HandleIssuedBy;
|
|
36
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The property key a handle kind claims on one side of an operation.
|
|
65
|
+
*
|
|
66
|
+
* This is the single place that resolves a direction to a key, so lint, the
|
|
67
|
+
* runtime, and error text cannot disagree about which name belongs where.
|
|
68
|
+
*/
|
|
69
|
+
export declare function handleFieldNameFor(kind: Pick<HandleKindDeclaration, "fieldName" | "outputFieldName">, direction: HandleFieldDirection): string;
|
|
70
|
+
/** True when a kind names the same handle differently on input and output. */
|
|
71
|
+
export declare function handleHasDirectionalFieldNames(kind: Pick<HandleKindDeclaration, "fieldName" | "outputFieldName">): boolean;
|
|
37
72
|
/** Kind names are lowercase letters only so they never fuse with the word body. */
|
|
38
73
|
export declare const HANDLE_KIND_NAME_PATTERN: RegExp;
|
|
39
74
|
/** True for a single operation key or a non-empty list of operation keys. */
|
package/dist/handle-meta.js
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** JSON Schema meta key placed on every schema field created by `kind.field()`. */
|
|
9
9
|
export const APIFUSE_HANDLE_META_KEY = "x-apifuse-handle";
|
|
10
|
+
/**
|
|
11
|
+
* The property key a handle kind claims on one side of an operation.
|
|
12
|
+
*
|
|
13
|
+
* This is the single place that resolves a direction to a key, so lint, the
|
|
14
|
+
* runtime, and error text cannot disagree about which name belongs where.
|
|
15
|
+
*/
|
|
16
|
+
export function handleFieldNameFor(kind, direction) {
|
|
17
|
+
if (direction === "input")
|
|
18
|
+
return kind.fieldName;
|
|
19
|
+
return kind.outputFieldName ?? kind.fieldName;
|
|
20
|
+
}
|
|
21
|
+
/** True when a kind names the same handle differently on input and output. */
|
|
22
|
+
export function handleHasDirectionalFieldNames(kind) {
|
|
23
|
+
return kind.outputFieldName !== undefined && kind.outputFieldName !== kind.fieldName;
|
|
24
|
+
}
|
|
10
25
|
/** Kind names are lowercase letters only so they never fuse with the word body. */
|
|
11
26
|
export const HANDLE_KIND_NAME_PATTERN = /^[a-z]{2,12}$/;
|
|
12
27
|
/** True for a single operation key or a non-empty list of operation keys. */
|
|
@@ -43,6 +58,7 @@ export function isHandleFieldMeta(value) {
|
|
|
43
58
|
return (typeof record.kind === "string" &&
|
|
44
59
|
(record.type === "cursor" || record.type === "draft") &&
|
|
45
60
|
typeof record.fieldName === "string" &&
|
|
61
|
+
(record.outputFieldName === undefined || typeof record.outputFieldName === "string") &&
|
|
46
62
|
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy)));
|
|
47
63
|
}
|
|
48
64
|
/**
|
package/dist/handle.d.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type ZodString, type ZodType, z } from "zod";
|
|
11
11
|
import { ProviderError, type ProviderErrorOptions } from "./errors.js";
|
|
12
|
-
import { type HandleAccess, type HandleFieldMeta, type HandleIssuedBy, type HandleKindDeclaration, type HandleKindType } from "./handle-meta.js";
|
|
12
|
+
import { type HandleAccess, type HandleFieldDirection, type HandleFieldMeta, type HandleFieldNames, type HandleIssuedBy, type HandleKindDeclaration, type HandleKindType } from "./handle-meta.js";
|
|
13
13
|
import type { ProviderStateDurationString } from "./types.js";
|
|
14
|
-
export type { HandleAccess, HandleFieldMeta, HandleIssuedBy, HandleKindDeclaration, HandleKindType, };
|
|
14
|
+
export type { HandleAccess, HandleFieldDirection, HandleFieldMeta, HandleFieldNames, HandleIssuedBy, HandleKindDeclaration, HandleKindType, };
|
|
15
15
|
export type HandleStrength = "standard" | "high";
|
|
16
16
|
/** Words issued per handle: 2 for bound kinds, 4 for public, 5 for high-strength public. */
|
|
17
17
|
export type HandleWordCount = 2 | 4 | 5;
|
|
@@ -30,6 +30,10 @@ export interface HandleKindBase<TSchema extends ZodType = ZodType> extends Handl
|
|
|
30
30
|
/**
|
|
31
31
|
* Schema for the handle field, usable in output and input positions. Carries
|
|
32
32
|
* the `x-apifuse-handle` meta and an SDK-owned English description.
|
|
33
|
+
*
|
|
34
|
+
* One schema serves both directions even when the kind declares separate
|
|
35
|
+
* `input` / `output` property keys: the meta carries both names and lint
|
|
36
|
+
* checks the key against the side the field actually appears on.
|
|
33
37
|
*/
|
|
34
38
|
field(): ZodString;
|
|
35
39
|
}
|
|
@@ -131,6 +135,7 @@ export declare function isHandleError(value: unknown): value is HandleError;
|
|
|
131
135
|
*/
|
|
132
136
|
export declare function handleRecoverySentence(kind: {
|
|
133
137
|
readonly fieldName: string;
|
|
138
|
+
readonly outputFieldName?: string;
|
|
134
139
|
readonly issuedBy?: HandleIssuedBy;
|
|
135
140
|
}): string;
|
|
136
141
|
/** Parses a `ProviderStateDurationString` to milliseconds; throws on invalid or non-positive input. */
|
|
@@ -138,8 +143,16 @@ export declare function parseHandleDurationMs(value: ProviderStateDurationString
|
|
|
138
143
|
export interface DefineCursorOptions<TSchema extends ZodType> {
|
|
139
144
|
/** Kind name, `/^[a-z]{2,12}$/`; becomes the handle prefix. */
|
|
140
145
|
readonly name: string;
|
|
141
|
-
/**
|
|
142
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Schema property key carrying the handle. Default `${name}_token`.
|
|
148
|
+
*
|
|
149
|
+
* Pass `{ input, output }` when the caller-facing contract names the same
|
|
150
|
+
* handle differently in each direction — the canon's `{ input: "cursor",
|
|
151
|
+
* output: "next_cursor" }`. Both keys must be given; the handle's identity
|
|
152
|
+
* stays the kind, so a value issued under `output` is still only valid when
|
|
153
|
+
* sent back under `input`.
|
|
154
|
+
*/
|
|
155
|
+
readonly fieldName?: string | HandleFieldNames;
|
|
143
156
|
readonly schema: TSchema;
|
|
144
157
|
readonly ttl: ProviderStateDurationString;
|
|
145
158
|
/** `"bound"` (default): connection-scoped, 2 words. `"public"`: provider-scoped, 4-5 words. */
|
|
@@ -155,7 +168,12 @@ export interface DefineCursorOptions<TSchema extends ZodType> {
|
|
|
155
168
|
}
|
|
156
169
|
export interface DefineDraftOptions<TSchema extends ZodType, TResultSchema extends ZodType | undefined> {
|
|
157
170
|
readonly name: string;
|
|
158
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Schema property key carrying the handle. Default `${name}_token`. Accepts
|
|
173
|
+
* `{ input, output }` for a contract that names the handle differently in
|
|
174
|
+
* each direction; see {@link DefineCursorOptions.fieldName}.
|
|
175
|
+
*/
|
|
176
|
+
readonly fieldName?: string | HandleFieldNames;
|
|
159
177
|
readonly schema: TSchema;
|
|
160
178
|
/** Optional schema of the commit result (type-level). */
|
|
161
179
|
readonly result?: TResultSchema;
|
package/dist/handle.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { brandHandleError, hasHandleErrorBrand, ProviderError, } from "./errors.js";
|
|
12
|
-
import { APIFUSE_HANDLE_META_KEY, formatHandleIssuers, handleFieldDescription, HANDLE_KIND_NAME_PATTERN, isHandleIssuedBy, } from "./handle-meta.js";
|
|
12
|
+
import { APIFUSE_HANDLE_META_KEY, formatHandleIssuers, handleFieldDescription, handleHasDirectionalFieldNames, HANDLE_KIND_NAME_PATTERN, isHandleIssuedBy, } from "./handle-meta.js";
|
|
13
13
|
import { BOUND_HANDLE_WORD_COUNT, HIGH_PUBLIC_HANDLE_WORD_COUNT, PUBLIC_HANDLE_WORD_COUNT, } from "./runtime/handle-wordlist.js";
|
|
14
14
|
/** Public kinds whose (max) ttl exceeds this are issued with five words (auto-high). */
|
|
15
15
|
const AUTO_HIGH_TTL_MS = 3_600_000;
|
|
@@ -64,6 +64,16 @@ export function isHandleError(value) {
|
|
|
64
64
|
*/
|
|
65
65
|
export function handleRecoverySentence(kind) {
|
|
66
66
|
const issuers = formatHandleIssuers(kind.issuedBy);
|
|
67
|
+
// When the kind is issued under one key and accepted under another, naming
|
|
68
|
+
// only one of them tells the LLM to look for a field that is not there. Name
|
|
69
|
+
// the output key it reads and the input key it writes, in that order.
|
|
70
|
+
if (handleHasDirectionalFieldNames(kind)) {
|
|
71
|
+
const issued = kind.outputFieldName;
|
|
72
|
+
if (issuers) {
|
|
73
|
+
return `Call ${issuers} again and pass the new \`${issued}\` back as \`${kind.fieldName}\`, exactly as returned.`;
|
|
74
|
+
}
|
|
75
|
+
return `Request a new \`${issued}\` and pass it back as \`${kind.fieldName}\`, exactly as returned.`;
|
|
76
|
+
}
|
|
67
77
|
if (issuers) {
|
|
68
78
|
return `Call ${issuers} again and pass the new \`${kind.fieldName}\` exactly as returned.`;
|
|
69
79
|
}
|
|
@@ -111,10 +121,44 @@ function assertKindName(name, define) {
|
|
|
111
121
|
throw new Error(`${define}: kind name must match ${HANDLE_KIND_NAME_PATTERN} (2-12 lowercase letters, no digits or separators) so it never fuses with the word body; received ${JSON.stringify(name)}.`);
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
|
-
function assertFieldName(fieldName, define) {
|
|
115
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(fieldName)) {
|
|
116
|
-
throw new Error(`${define}:
|
|
124
|
+
function assertFieldName(fieldName, define, label) {
|
|
125
|
+
if (typeof fieldName !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(fieldName)) {
|
|
126
|
+
throw new Error(`${define}: ${label} must be a plain identifier usable as a schema property key; received ${JSON.stringify(fieldName)}.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolves the `fieldName` option to the input key and, when the two differ,
|
|
131
|
+
* the output key.
|
|
132
|
+
*
|
|
133
|
+
* The string form names one key for both directions and is unchanged. The
|
|
134
|
+
* object form requires BOTH keys: a half-declaration would leave one side
|
|
135
|
+
* silently defaulting to `${name}_token`, which is the kind of mismatch this
|
|
136
|
+
* option exists to make explicit. `outputFieldName` is left undefined when the
|
|
137
|
+
* two keys are equal so a one-name kind emits exactly the meta it always did.
|
|
138
|
+
*/
|
|
139
|
+
function resolveFieldNames(value, define, name) {
|
|
140
|
+
if (value === undefined)
|
|
141
|
+
return { fieldName: `${name}_token` };
|
|
142
|
+
if (typeof value === "string") {
|
|
143
|
+
assertFieldName(value, define, "fieldName");
|
|
144
|
+
return { fieldName: value };
|
|
145
|
+
}
|
|
146
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
147
|
+
throw new Error(`${define}: "${name}" fieldName must be a property key or { input, output }; received ${JSON.stringify(value)}.`);
|
|
148
|
+
}
|
|
149
|
+
const record = value;
|
|
150
|
+
const extra = Object.keys(record).filter((key) => key !== "input" && key !== "output");
|
|
151
|
+
if (extra.length > 0) {
|
|
152
|
+
throw new Error(`${define}: "${name}" fieldName accepts only "input" and "output"; received extra ${extra.map((key) => JSON.stringify(key)).join(", ")}.`);
|
|
153
|
+
}
|
|
154
|
+
if (record.input === undefined || record.output === undefined) {
|
|
155
|
+
throw new Error(`${define}: "${name}" fieldName must declare both "input" and "output" when the two directions differ; declaring one would leave the other defaulting to "${name}_token".`);
|
|
117
156
|
}
|
|
157
|
+
assertFieldName(record.input, define, "fieldName.input");
|
|
158
|
+
assertFieldName(record.output, define, "fieldName.output");
|
|
159
|
+
if (record.input === record.output)
|
|
160
|
+
return { fieldName: record.input };
|
|
161
|
+
return { fieldName: record.input, outputFieldName: record.output };
|
|
118
162
|
}
|
|
119
163
|
function assertPositiveInteger(value, label, define) {
|
|
120
164
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -163,8 +207,7 @@ function createHandleField(meta) {
|
|
|
163
207
|
export function defineCursor(options) {
|
|
164
208
|
const define = "defineCursor";
|
|
165
209
|
assertKindName(options.name, define);
|
|
166
|
-
const fieldName = options.fieldName
|
|
167
|
-
assertFieldName(fieldName, define);
|
|
210
|
+
const { fieldName, outputFieldName } = resolveFieldNames(options.fieldName, define, options.name);
|
|
168
211
|
const access = options.access ?? "bound";
|
|
169
212
|
assertAccess(access, define);
|
|
170
213
|
const ttlMs = parseHandleDurationMs(options.ttl, `"${options.name}" ttl`);
|
|
@@ -183,12 +226,14 @@ export function defineCursor(options) {
|
|
|
183
226
|
kind: options.name,
|
|
184
227
|
type: "cursor",
|
|
185
228
|
fieldName,
|
|
229
|
+
...(outputFieldName ? { outputFieldName } : {}),
|
|
186
230
|
...(issuedBy ? { issuedBy } : {}),
|
|
187
231
|
};
|
|
188
232
|
return Object.freeze({
|
|
189
233
|
name: options.name,
|
|
190
234
|
type: "cursor",
|
|
191
235
|
fieldName,
|
|
236
|
+
outputFieldName,
|
|
192
237
|
access,
|
|
193
238
|
issuedBy,
|
|
194
239
|
schema: options.schema,
|
|
@@ -209,8 +254,7 @@ export function defineCursor(options) {
|
|
|
209
254
|
export function defineDraft(options) {
|
|
210
255
|
const define = "defineDraft";
|
|
211
256
|
assertKindName(options.name, define);
|
|
212
|
-
const fieldName = options.fieldName
|
|
213
|
-
assertFieldName(fieldName, define);
|
|
257
|
+
const { fieldName, outputFieldName } = resolveFieldNames(options.fieldName, define, options.name);
|
|
214
258
|
if (!options.ttl || typeof options.ttl !== "object") {
|
|
215
259
|
throw new Error(`${define}: draft "${options.name}" ttl must be { idle, max }.`);
|
|
216
260
|
}
|
|
@@ -242,12 +286,14 @@ export function defineDraft(options) {
|
|
|
242
286
|
kind: options.name,
|
|
243
287
|
type: "draft",
|
|
244
288
|
fieldName,
|
|
289
|
+
...(outputFieldName ? { outputFieldName } : {}),
|
|
245
290
|
...(issuedBy ? { issuedBy } : {}),
|
|
246
291
|
};
|
|
247
292
|
return Object.freeze({
|
|
248
293
|
name: options.name,
|
|
249
294
|
type: "draft",
|
|
250
295
|
fieldName,
|
|
296
|
+
outputFieldName,
|
|
251
297
|
access,
|
|
252
298
|
strength,
|
|
253
299
|
issuedBy,
|
|
@@ -1562,6 +1562,44 @@ export type OperationResult = {
|
|
|
1562
1562
|
data: JsonValue;
|
|
1563
1563
|
request_id: string;
|
|
1564
1564
|
duration_ms: number;
|
|
1565
|
+
/**
|
|
1566
|
+
* Whether the monitor's operation step was answered from a stale-if-error
|
|
1567
|
+
* cache serve — the runtime's projection of the response envelope's
|
|
1568
|
+
* `meta.stale`.
|
|
1569
|
+
*
|
|
1570
|
+
* An operation that reads its upstream through `ctx.cache.getOrSet(...,
|
|
1571
|
+
* { staleIfErrorMs })` answers an upstream outage with HTTP 200 and a
|
|
1572
|
+
* well-formed, fully schema-valid body for the whole stale window. Every
|
|
1573
|
+
* assertion over `status_code` and `data` therefore passes, and the probe
|
|
1574
|
+
* rolls up `ok` while the upstream is down. This boolean is the only thing
|
|
1575
|
+
* in the step result that separates that from a live read.
|
|
1576
|
+
*
|
|
1577
|
+
* Always present, so a reference to it resolves to `false` rather than
|
|
1578
|
+
* raising `reference_unresolvable`; a response whose `meta` is malformed
|
|
1579
|
+
* fails the step rather than projecting as fresh.
|
|
1580
|
+
*
|
|
1581
|
+
* Providers that serve stale-if-error should guard on it, so an outage the
|
|
1582
|
+
* cache is absorbing stops reading as green:
|
|
1583
|
+
*
|
|
1584
|
+
* ```ts
|
|
1585
|
+
* {
|
|
1586
|
+
* id: "guard-freshness",
|
|
1587
|
+
* result: "freshness-guarded",
|
|
1588
|
+
* kind: "guard",
|
|
1589
|
+
* condition: {
|
|
1590
|
+
* kind: "predicate",
|
|
1591
|
+
* operator: "not_equals",
|
|
1592
|
+
* actual: { ref: { namespace: "steps", binding: "case", path: ["served_stale_cache"] } },
|
|
1593
|
+
* expected: true,
|
|
1594
|
+
* },
|
|
1595
|
+
* onFail: {
|
|
1596
|
+
* attribute: [{ operationId, status: "degraded", reasonCode: "expected_absence", reasonKey }],
|
|
1597
|
+
* stop: "scenario",
|
|
1598
|
+
* },
|
|
1599
|
+
* }
|
|
1600
|
+
* ```
|
|
1601
|
+
*/
|
|
1602
|
+
served_stale_cache: boolean;
|
|
1565
1603
|
candidate?: {
|
|
1566
1604
|
attempts: number;
|
|
1567
1605
|
selected_index: number | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -13,14 +13,16 @@ export * from "./engine.js";
|
|
|
13
13
|
export * from "./observability.js";
|
|
14
14
|
export * from "./user-input.js";
|
|
15
15
|
export * from "./i18n/index.js";
|
|
16
|
-
export { type LintDiagnostic, lintOperation, lintProvider, } from "./lint.js";
|
|
16
|
+
export { type LintDiagnostic, lintOperation, lintProvider, type ProviderLintInformation, type ProviderLintResult, } from "./lint.js";
|
|
17
|
+
export { type LintRuntimeBoundarySourcesOptions, lintRuntimeBoundarySources, RUNTIME_BOUNDARY_RULES, type RuntimeBoundaryRule, } from "./runtime-boundary-lint.js";
|
|
18
|
+
export { TYPESCRIPT_COMPILER_MODULE_SPECIFIERS, type TypeScriptCompilerModuleLike, } from "./typescript-module.js";
|
|
17
19
|
export * from "./recipes/gov-api.js";
|
|
18
20
|
export * from "./recipes/rest-api.js";
|
|
19
21
|
export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
|
|
20
22
|
export type { BrowserClientOptions } from "./runtime/browser.js";
|
|
21
23
|
export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, type ProviderCacheOptions, } from "./runtime/cache.js";
|
|
22
24
|
export * from "./handle.js";
|
|
23
|
-
export { APIFUSE_HANDLE_META_KEY, HANDLE_KIND_NAME_PATTERN, handleFieldDescription, isHandleFieldMeta, } from "./handle-meta.js";
|
|
25
|
+
export { APIFUSE_HANDLE_META_KEY, HANDLE_KIND_NAME_PATTERN, handleFieldDescription, handleFieldNameFor, handleHasDirectionalFieldNames, isHandleFieldMeta, } from "./handle-meta.js";
|
|
24
26
|
export { type CreateHandleContextOptions, type CreateTestHandleContextOptions, createHandleContext, createTestHandleContext, type HandleNormalizationClass, type NormalizedHandle, normalizeHandle, } from "./runtime/handle.js";
|
|
25
27
|
export { type CreateCredentialContextOptions, createCredentialContext, } from "./runtime/credential.js";
|
|
26
28
|
export { createEnvContext } from "./runtime/env.js";
|
package/dist/index.js
CHANGED
|
@@ -12,12 +12,14 @@ export * from "./observability.js";
|
|
|
12
12
|
export * from "./user-input.js";
|
|
13
13
|
export * from "./i18n/index.js";
|
|
14
14
|
export { lintOperation, lintProvider, } from "./lint.js";
|
|
15
|
+
export { lintRuntimeBoundarySources, RUNTIME_BOUNDARY_RULES, } from "./runtime-boundary-lint.js";
|
|
16
|
+
export { TYPESCRIPT_COMPILER_MODULE_SPECIFIERS, } from "./typescript-module.js";
|
|
15
17
|
export * from "./recipes/gov-api.js";
|
|
16
18
|
export * from "./recipes/rest-api.js";
|
|
17
19
|
export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
|
|
18
20
|
export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, } from "./runtime/cache.js";
|
|
19
21
|
export * from "./handle.js";
|
|
20
|
-
export { APIFUSE_HANDLE_META_KEY, HANDLE_KIND_NAME_PATTERN, handleFieldDescription, isHandleFieldMeta, } from "./handle-meta.js";
|
|
22
|
+
export { APIFUSE_HANDLE_META_KEY, HANDLE_KIND_NAME_PATTERN, handleFieldDescription, handleFieldNameFor, handleHasDirectionalFieldNames, isHandleFieldMeta, } from "./handle-meta.js";
|
|
21
23
|
export { createHandleContext, createTestHandleContext, normalizeHandle, } from "./runtime/handle.js";
|
|
22
24
|
export { createCredentialContext, } from "./runtime/credential.js";
|
|
23
25
|
export { createEnvContext } from "./runtime/env.js";
|
package/dist/lint.d.ts
CHANGED
|
@@ -29,11 +29,16 @@ export type ProviderLintMode = "official" | "standalone";
|
|
|
29
29
|
type ProviderLintOptions = {
|
|
30
30
|
mode?: ProviderLintMode;
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* A non-blocking audit line: a finding the author acknowledged with
|
|
34
|
+
* `// @apifuse-allow <rule>: <reason>`, kept visible in `apifuse check` output.
|
|
35
|
+
*/
|
|
32
36
|
export interface ProviderLintInformation {
|
|
33
37
|
rule: string;
|
|
34
38
|
message: string;
|
|
35
39
|
field?: string;
|
|
36
40
|
}
|
|
41
|
+
/** Diagnostics plus the acknowledged findings that accompany them. */
|
|
37
42
|
export interface ProviderLintResult {
|
|
38
43
|
diagnostics: LintDiagnostic[];
|
|
39
44
|
information: ProviderLintInformation[];
|