@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.62
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/CHANGELOG.md +4 -0
- package/bin/apifuse-pack-types.ts +16 -12
- package/dist/handle-meta.d.ts +18 -2
- package/dist/handle-meta.js +30 -2
- package/dist/handle.d.ts +33 -12
- package/dist/handle.js +69 -24
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lint.js +28 -8
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +2 -0
- package/dist/runtime/handle.js +15 -10
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +6 -0
- package/dist/runtime/http-telemetry-guard.js +150 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +222 -146
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/serve-implementation.d.ts +2 -0
- package/dist/server/serve-implementation.js +34 -35
- package/package.json +1 -1
- package/src/handle-meta.ts +39 -4
- package/src/handle.ts +121 -38
- package/src/index.ts +19 -0
- package/src/lint.ts +30 -7
- package/src/provider.ts +10 -0
- package/src/runtime/handle.ts +20 -10
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +155 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +295 -173
- package/src/server/index.ts +13 -0
- package/src/server/serve-implementation.ts +41 -58
package/src/handle-meta.ts
CHANGED
|
@@ -12,12 +12,19 @@ export const APIFUSE_HANDLE_META_KEY = "x-apifuse-handle" as const;
|
|
|
12
12
|
export type HandleKindType = "cursor" | "draft";
|
|
13
13
|
export type HandleAccess = "bound" | "public";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Operation key(s) that issue a handle. A single string is the common case; a
|
|
17
|
+
* list names every operation whose output carries the field (for example
|
|
18
|
+
* `search-address` and `reverse-geocode` both issuing `location_token`).
|
|
19
|
+
*/
|
|
20
|
+
export type HandleIssuedBy = string | readonly string[];
|
|
21
|
+
|
|
15
22
|
/** Value stored under {@link APIFUSE_HANDLE_META_KEY} on a handle field. */
|
|
16
23
|
export interface HandleFieldMeta {
|
|
17
24
|
readonly kind: string;
|
|
18
25
|
readonly type: HandleKindType;
|
|
19
26
|
readonly fieldName: string;
|
|
20
|
-
readonly issuedBy?:
|
|
27
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -30,12 +37,39 @@ export interface HandleKindDeclaration {
|
|
|
30
37
|
readonly type: HandleKindType;
|
|
31
38
|
readonly fieldName: string;
|
|
32
39
|
readonly access: HandleAccess;
|
|
33
|
-
readonly issuedBy?:
|
|
40
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/** Kind names are lowercase letters only so they never fuse with the word body. */
|
|
37
44
|
export const HANDLE_KIND_NAME_PATTERN = /^[a-z]{2,12}$/;
|
|
38
45
|
|
|
46
|
+
/** True for a single operation key or a non-empty list of operation keys. */
|
|
47
|
+
export function isHandleIssuedBy(value: unknown): value is HandleIssuedBy {
|
|
48
|
+
if (typeof value === "string") return true;
|
|
49
|
+
return (
|
|
50
|
+
Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Normalizes `issuedBy` to a list of operation keys (empty when undefined). */
|
|
55
|
+
export function handleIssuerList(issuedBy: HandleIssuedBy | undefined): readonly string[] {
|
|
56
|
+
if (issuedBy === undefined) return [];
|
|
57
|
+
return typeof issuedBy === "string" ? [issuedBy] : issuedBy;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Renders the issuing operation(s) as inline code for error and description
|
|
62
|
+
* text: `` `a` ``, `` `a` or `b` ``, `` `a`, `b`, or `c` ``. Undefined when no
|
|
63
|
+
* issuer is declared.
|
|
64
|
+
*/
|
|
65
|
+
export function formatHandleIssuers(issuedBy: HandleIssuedBy | undefined): string | undefined {
|
|
66
|
+
const issuers = handleIssuerList(issuedBy).map((operation) => `\`${operation}\``);
|
|
67
|
+
if (issuers.length === 0) return undefined;
|
|
68
|
+
if (issuers.length === 1) return issuers[0];
|
|
69
|
+
if (issuers.length === 2) return `${issuers[0]} or ${issuers[1]}`;
|
|
70
|
+
return `${issuers.slice(0, -1).join(", ")}, or ${issuers[issuers.length - 1]}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
export function isHandleFieldMeta(value: unknown): value is HandleFieldMeta {
|
|
40
74
|
if (typeof value !== "object" || value === null) return false;
|
|
41
75
|
const record = value as Record<string, unknown>;
|
|
@@ -43,7 +77,7 @@ export function isHandleFieldMeta(value: unknown): value is HandleFieldMeta {
|
|
|
43
77
|
typeof record.kind === "string" &&
|
|
44
78
|
(record.type === "cursor" || record.type === "draft") &&
|
|
45
79
|
typeof record.fieldName === "string" &&
|
|
46
|
-
(record.issuedBy === undefined ||
|
|
80
|
+
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy))
|
|
47
81
|
);
|
|
48
82
|
}
|
|
49
83
|
|
|
@@ -53,6 +87,7 @@ export function isHandleFieldMeta(value: unknown): value is HandleFieldMeta {
|
|
|
53
87
|
* provider override re-enters the localization rules.
|
|
54
88
|
*/
|
|
55
89
|
export function handleFieldDescription(meta: Pick<HandleFieldMeta, "issuedBy">): string {
|
|
56
|
-
const
|
|
90
|
+
const issuers = formatHandleIssuers(meta.issuedBy);
|
|
91
|
+
const origin = issuers ? ` issued by ${issuers}` : "";
|
|
57
92
|
return `Opaque handle${origin}. Copy it exactly as returned; do not edit or shorten it.`;
|
|
58
93
|
}
|
package/src/handle.ts
CHANGED
|
@@ -16,12 +16,15 @@ import {
|
|
|
16
16
|
} from "./errors.js";
|
|
17
17
|
import {
|
|
18
18
|
APIFUSE_HANDLE_META_KEY,
|
|
19
|
+
formatHandleIssuers,
|
|
19
20
|
handleFieldDescription,
|
|
20
21
|
HANDLE_KIND_NAME_PATTERN,
|
|
21
22
|
type HandleAccess,
|
|
22
23
|
type HandleFieldMeta,
|
|
24
|
+
type HandleIssuedBy,
|
|
23
25
|
type HandleKindDeclaration,
|
|
24
26
|
type HandleKindType,
|
|
27
|
+
isHandleIssuedBy,
|
|
25
28
|
} from "./handle-meta.js";
|
|
26
29
|
import type { ProviderErrorCategory } from "./observability.js";
|
|
27
30
|
import {
|
|
@@ -31,9 +34,15 @@ import {
|
|
|
31
34
|
} from "./runtime/handle-wordlist.js";
|
|
32
35
|
import type { ProviderStateDurationString } from "./types.js";
|
|
33
36
|
|
|
34
|
-
export type {
|
|
37
|
+
export type {
|
|
38
|
+
HandleAccess,
|
|
39
|
+
HandleFieldMeta,
|
|
40
|
+
HandleIssuedBy,
|
|
41
|
+
HandleKindDeclaration,
|
|
42
|
+
HandleKindType,
|
|
43
|
+
};
|
|
35
44
|
|
|
36
|
-
/** Public
|
|
45
|
+
/** Public kinds whose (max) ttl exceeds this are issued with five words (auto-high). */
|
|
37
46
|
const AUTO_HIGH_TTL_MS = 3_600_000;
|
|
38
47
|
const DEFAULT_BOUND_MAX_ENTRIES = 200;
|
|
39
48
|
const DEFAULT_CURSOR_MAX_VALUE_BYTES = 16_000;
|
|
@@ -76,7 +85,10 @@ export interface DraftKind<
|
|
|
76
85
|
TResultSchema extends ZodType | undefined = ZodType | undefined,
|
|
77
86
|
> extends HandleKindBase<TSchema> {
|
|
78
87
|
readonly type: "draft";
|
|
79
|
-
|
|
88
|
+
/** `"bound"` (default): connection-scoped, 2 words. `"public"`: provider-scoped, 4-5 words. */
|
|
89
|
+
readonly access: HandleAccess;
|
|
90
|
+
/** Public only; `"high"` forces 5 words. Always `"standard"` for bound drafts. */
|
|
91
|
+
readonly strength: HandleStrength;
|
|
80
92
|
readonly ttl: {
|
|
81
93
|
readonly idle: ProviderStateDurationString;
|
|
82
94
|
readonly max: ProviderStateDurationString;
|
|
@@ -87,7 +99,6 @@ export interface DraftKind<
|
|
|
87
99
|
readonly resultTtlMs: number;
|
|
88
100
|
/** Optional schema describing the commit result; type-level only at runtime. */
|
|
89
101
|
readonly result: TResultSchema | undefined;
|
|
90
|
-
readonly wordCount: 2;
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
// `any` here is variance-only: a concrete `CursorKind<{ page: number }>` must be
|
|
@@ -123,6 +134,12 @@ export interface HandleCommitResult<K extends HandleKind> {
|
|
|
123
134
|
export interface HandleContext {
|
|
124
135
|
/** Validates `data` with `kind.schema`, stores it, and returns the canonical handle. */
|
|
125
136
|
create<K extends HandleKind>(kind: K, data: InputOf<K>): Promise<string>;
|
|
137
|
+
/**
|
|
138
|
+
* Same as `create` but returns the full record (`handle`, `status`, `data`,
|
|
139
|
+
* `createdAt`, `expiresAt`) so a provider can expose `expires_at` without
|
|
140
|
+
* recomputing ttl math. `create` is the string-returning shortcut.
|
|
141
|
+
*/
|
|
142
|
+
createRecord<K extends HandleKind>(kind: K, data: InputOf<K>): Promise<HandleRecord<K>>;
|
|
126
143
|
/** Tolerantly parses `handle`, loads the record; drafts get their idle TTL touched. */
|
|
127
144
|
read<K extends HandleKind>(kind: K, handle: string): Promise<HandleRecord<K>>;
|
|
128
145
|
/** CAS update of an active draft (3 retries) with sliding TTL. */
|
|
@@ -242,10 +259,11 @@ export function isHandleError(value: unknown): value is HandleError {
|
|
|
242
259
|
*/
|
|
243
260
|
export function handleRecoverySentence(kind: {
|
|
244
261
|
readonly fieldName: string;
|
|
245
|
-
readonly issuedBy?:
|
|
262
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
246
263
|
}): string {
|
|
247
|
-
|
|
248
|
-
|
|
264
|
+
const issuers = formatHandleIssuers(kind.issuedBy);
|
|
265
|
+
if (issuers) {
|
|
266
|
+
return `Call ${issuers} again and pass the new \`${kind.fieldName}\` exactly as returned.`;
|
|
249
267
|
}
|
|
250
268
|
return `Request a new \`${kind.fieldName}\` and pass it exactly as returned.`;
|
|
251
269
|
}
|
|
@@ -311,8 +329,8 @@ export interface DefineCursorOptions<TSchema extends ZodType> {
|
|
|
311
329
|
readonly maxValueBytes?: number;
|
|
312
330
|
/** Public only; `"high"` forces 5 words. Auto-high when ttl > 1h. */
|
|
313
331
|
readonly strength?: HandleStrength;
|
|
314
|
-
/** Operation key that
|
|
315
|
-
readonly issuedBy?:
|
|
332
|
+
/** Operation key(s) that issue this handle; used in error text and lint. */
|
|
333
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
316
334
|
}
|
|
317
335
|
|
|
318
336
|
export interface DefineDraftOptions<
|
|
@@ -330,11 +348,20 @@ export interface DefineDraftOptions<
|
|
|
330
348
|
};
|
|
331
349
|
/** How long a committed record stays for replay. Default "24h". */
|
|
332
350
|
readonly resultTtl?: ProviderStateDurationString;
|
|
333
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* `"bound"` (default): connection-scoped, 2 words. `"public"`: provider-scoped
|
|
353
|
+
* draft for connectionless providers (`auth: none`), 4-5 words, `maxEntries`
|
|
354
|
+
* required; every lookup failure collapses to `HANDLE_INVALID`.
|
|
355
|
+
*/
|
|
356
|
+
readonly access?: HandleAccess;
|
|
357
|
+
/** Required when public. Bound default 200 per connection. */
|
|
334
358
|
readonly maxEntries?: number;
|
|
335
359
|
/** Default 64,000. */
|
|
336
360
|
readonly maxValueBytes?: number;
|
|
337
|
-
|
|
361
|
+
/** Public only; `"high"` forces 5 words. Auto-high when ttl.max > 1h. */
|
|
362
|
+
readonly strength?: HandleStrength;
|
|
363
|
+
/** Operation key(s) that issue this handle; used in error text and lint. */
|
|
364
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
338
365
|
}
|
|
339
366
|
|
|
340
367
|
function assertKindName(name: unknown, define: string): asserts name is string {
|
|
@@ -359,6 +386,49 @@ function assertPositiveInteger(value: number, label: string, define: string): vo
|
|
|
359
386
|
}
|
|
360
387
|
}
|
|
361
388
|
|
|
389
|
+
function assertAccess(value: unknown, define: string): asserts value is HandleAccess {
|
|
390
|
+
if (value !== "bound" && value !== "public") {
|
|
391
|
+
throw new Error(`${define}: access must be "bound" or "public"; received ${String(value)}.`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function assertStrength(value: unknown, define: string): asserts value is HandleStrength {
|
|
396
|
+
if (value !== "standard" && value !== "high") {
|
|
397
|
+
throw new Error(`${define}: strength must be "standard" or "high"; received ${String(value)}.`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Validates `issuedBy` and returns a frozen copy for lists; `undefined` stays undefined. */
|
|
402
|
+
function normalizeIssuedBy(
|
|
403
|
+
value: unknown,
|
|
404
|
+
define: string,
|
|
405
|
+
name: string,
|
|
406
|
+
): HandleIssuedBy | undefined {
|
|
407
|
+
if (value === undefined) return undefined;
|
|
408
|
+
if (!isHandleIssuedBy(value) || (typeof value === "string" ? value.length === 0 : false)) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`${define}: "${name}" issuedBy must be an operation key or a non-empty list of operation keys; received ${JSON.stringify(value)}.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (typeof value === "string") return value;
|
|
414
|
+
if (value.some((operation) => operation.length === 0)) {
|
|
415
|
+
throw new Error(`${define}: "${name}" issuedBy must not contain empty operation keys.`);
|
|
416
|
+
}
|
|
417
|
+
return Object.freeze([...value]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Word count for a kind: 2 bound; public 4, or 5 when high strength or the lifetime exceeds 1h. */
|
|
421
|
+
function wordCountFor(
|
|
422
|
+
access: HandleAccess,
|
|
423
|
+
strength: HandleStrength,
|
|
424
|
+
lifetimeMs: number,
|
|
425
|
+
): HandleWordCount {
|
|
426
|
+
if (access === "bound") return BOUND_HANDLE_WORD_COUNT;
|
|
427
|
+
return strength === "high" || lifetimeMs > AUTO_HIGH_TTL_MS
|
|
428
|
+
? HIGH_PUBLIC_HANDLE_WORD_COUNT
|
|
429
|
+
: PUBLIC_HANDLE_WORD_COUNT;
|
|
430
|
+
}
|
|
431
|
+
|
|
362
432
|
function createHandleField(meta: HandleFieldMeta): ZodString {
|
|
363
433
|
const described = z.string().min(1).describe(handleFieldDescription(meta));
|
|
364
434
|
// zod v4 `.meta()` replaces the registry entry, so carry the description over.
|
|
@@ -374,17 +444,12 @@ export function defineCursor<TSchema extends ZodType>(
|
|
|
374
444
|
assertKindName(options.name, define);
|
|
375
445
|
const fieldName = options.fieldName ?? `${options.name}_token`;
|
|
376
446
|
assertFieldName(fieldName, define);
|
|
377
|
-
const access:
|
|
378
|
-
|
|
379
|
-
throw new Error(`${define}: access must be "bound" or "public"; received ${String(access)}.`);
|
|
380
|
-
}
|
|
447
|
+
const access: unknown = options.access ?? "bound";
|
|
448
|
+
assertAccess(access, define);
|
|
381
449
|
const ttlMs = parseHandleDurationMs(options.ttl, `"${options.name}" ttl`);
|
|
382
|
-
const strength:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`${define}: strength must be "standard" or "high"; received ${String(strength)}.`,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
450
|
+
const strength: unknown = options.strength ?? "standard";
|
|
451
|
+
assertStrength(strength, define);
|
|
452
|
+
const issuedBy = normalizeIssuedBy(options.issuedBy, define, options.name);
|
|
388
453
|
if (access === "public" && options.maxEntries === undefined) {
|
|
389
454
|
throw new Error(
|
|
390
455
|
`${define}: public cursor "${options.name}" must declare maxEntries (provider-wide live-entry quota).`,
|
|
@@ -394,24 +459,19 @@ export function defineCursor<TSchema extends ZodType>(
|
|
|
394
459
|
assertPositiveInteger(maxEntries, "maxEntries", define);
|
|
395
460
|
const maxValueBytes = options.maxValueBytes ?? DEFAULT_CURSOR_MAX_VALUE_BYTES;
|
|
396
461
|
assertPositiveInteger(maxValueBytes, "maxValueBytes", define);
|
|
397
|
-
const wordCount
|
|
398
|
-
access === "bound"
|
|
399
|
-
? BOUND_HANDLE_WORD_COUNT
|
|
400
|
-
: strength === "high" || ttlMs > AUTO_HIGH_TTL_MS
|
|
401
|
-
? HIGH_PUBLIC_HANDLE_WORD_COUNT
|
|
402
|
-
: PUBLIC_HANDLE_WORD_COUNT;
|
|
462
|
+
const wordCount = wordCountFor(access, strength, ttlMs);
|
|
403
463
|
const meta: HandleFieldMeta = {
|
|
404
464
|
kind: options.name,
|
|
405
465
|
type: "cursor",
|
|
406
466
|
fieldName,
|
|
407
|
-
...(
|
|
467
|
+
...(issuedBy ? { issuedBy } : {}),
|
|
408
468
|
};
|
|
409
469
|
return Object.freeze({
|
|
410
470
|
name: options.name,
|
|
411
471
|
type: "cursor",
|
|
412
472
|
fieldName,
|
|
413
473
|
access,
|
|
414
|
-
issuedBy
|
|
474
|
+
issuedBy,
|
|
415
475
|
schema: options.schema,
|
|
416
476
|
ttl: options.ttl,
|
|
417
477
|
ttlMs,
|
|
@@ -423,7 +483,11 @@ export function defineCursor<TSchema extends ZodType>(
|
|
|
423
483
|
});
|
|
424
484
|
}
|
|
425
485
|
|
|
426
|
-
/**
|
|
486
|
+
/**
|
|
487
|
+
* Defines a mutable draft kind (multi-turn forms with one-shot commit). Bound
|
|
488
|
+
* by default; `access: "public"` opts a connectionless provider into a
|
|
489
|
+
* provider-scoped draft under the public guessing analysis (ADR-0012 D3).
|
|
490
|
+
*/
|
|
427
491
|
export function defineDraft<
|
|
428
492
|
TSchema extends ZodType,
|
|
429
493
|
TResultSchema extends ZodType | undefined = undefined,
|
|
@@ -444,22 +508,40 @@ export function defineDraft<
|
|
|
444
508
|
}
|
|
445
509
|
const resultTtl = options.resultTtl ?? DEFAULT_RESULT_TTL;
|
|
446
510
|
const resultTtlMs = parseHandleDurationMs(resultTtl, `"${options.name}" resultTtl`);
|
|
511
|
+
const access: unknown = options.access ?? "bound";
|
|
512
|
+
assertAccess(access, define);
|
|
513
|
+
const strength: unknown = options.strength ?? "standard";
|
|
514
|
+
assertStrength(strength, define);
|
|
515
|
+
if (access === "bound" && strength !== "standard") {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`${define}: draft "${options.name}" strength applies to public drafts only; bound drafts are always two words.`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const issuedBy = normalizeIssuedBy(options.issuedBy, define, options.name);
|
|
521
|
+
if (access === "public" && options.maxEntries === undefined) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`${define}: public draft "${options.name}" must declare maxEntries (provider-wide live-entry quota).`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
447
526
|
const maxEntries = options.maxEntries ?? DEFAULT_BOUND_MAX_ENTRIES;
|
|
448
527
|
assertPositiveInteger(maxEntries, "maxEntries", define);
|
|
449
528
|
const maxValueBytes = options.maxValueBytes ?? DEFAULT_DRAFT_MAX_VALUE_BYTES;
|
|
450
529
|
assertPositiveInteger(maxValueBytes, "maxValueBytes", define);
|
|
530
|
+
// A public draft's guessable lifetime is its hard `max` ttl (idle only shortens it).
|
|
531
|
+
const wordCount = wordCountFor(access, strength, maxTtlMs);
|
|
451
532
|
const meta: HandleFieldMeta = {
|
|
452
533
|
kind: options.name,
|
|
453
534
|
type: "draft",
|
|
454
535
|
fieldName,
|
|
455
|
-
...(
|
|
536
|
+
...(issuedBy ? { issuedBy } : {}),
|
|
456
537
|
};
|
|
457
538
|
return Object.freeze({
|
|
458
539
|
name: options.name,
|
|
459
540
|
type: "draft",
|
|
460
541
|
fieldName,
|
|
461
|
-
access
|
|
462
|
-
|
|
542
|
+
access,
|
|
543
|
+
strength,
|
|
544
|
+
issuedBy,
|
|
463
545
|
schema: options.schema,
|
|
464
546
|
result: options.result,
|
|
465
547
|
ttl: options.ttl,
|
|
@@ -469,7 +551,7 @@ export function defineDraft<
|
|
|
469
551
|
resultTtlMs,
|
|
470
552
|
maxEntries,
|
|
471
553
|
maxValueBytes,
|
|
472
|
-
wordCount
|
|
554
|
+
wordCount,
|
|
473
555
|
field: () => createHandleField(meta),
|
|
474
556
|
});
|
|
475
557
|
}
|
|
@@ -491,8 +573,8 @@ export interface PickOptions<T> {
|
|
|
491
573
|
readonly field: string;
|
|
492
574
|
/** Item property compared against `value`. Default = `field`. */
|
|
493
575
|
readonly by?: keyof T & string;
|
|
494
|
-
/** Operation that listed the items; used in the error message when known. */
|
|
495
|
-
readonly issuedBy?:
|
|
576
|
+
/** Operation(s) that listed the items; used in the error message when known. */
|
|
577
|
+
readonly issuedBy?: HandleIssuedBy;
|
|
496
578
|
}
|
|
497
579
|
|
|
498
580
|
/**
|
|
@@ -518,8 +600,9 @@ export function pick<T extends Record<string, unknown>>(
|
|
|
518
600
|
const loose = items.filter((item) => keyOf(item).trim().toLowerCase() === folded);
|
|
519
601
|
if (loose.length === 1) return loose[0]!;
|
|
520
602
|
const allowed = items.map(keyOf);
|
|
521
|
-
const
|
|
522
|
-
|
|
603
|
+
const issuers = formatHandleIssuers(options.issuedBy);
|
|
604
|
+
const listed = issuers
|
|
605
|
+
? ` Use the values exactly as listed by ${issuers}.`
|
|
523
606
|
: " Use the values exactly as listed.";
|
|
524
607
|
throw new HandleError(
|
|
525
608
|
"PICK_NOT_OFFERED",
|
package/src/index.ts
CHANGED
|
@@ -139,6 +139,12 @@ export {
|
|
|
139
139
|
type ProviderCacheOptions,
|
|
140
140
|
} from "./runtime/cache.js";
|
|
141
141
|
export * from "./handle.js";
|
|
142
|
+
export {
|
|
143
|
+
APIFUSE_HANDLE_META_KEY,
|
|
144
|
+
HANDLE_KIND_NAME_PATTERN,
|
|
145
|
+
handleFieldDescription,
|
|
146
|
+
isHandleFieldMeta,
|
|
147
|
+
} from "./handle-meta.js";
|
|
142
148
|
export {
|
|
143
149
|
type CreateHandleContextOptions,
|
|
144
150
|
type CreateTestHandleContextOptions,
|
|
@@ -244,6 +250,7 @@ export {
|
|
|
244
250
|
MISSING_SECRET_CODE,
|
|
245
251
|
} from "./runtime/secrets.js";
|
|
246
252
|
export {
|
|
253
|
+
createMemoryProviderRuntimeState,
|
|
247
254
|
createUnsupportedProviderRuntimeState,
|
|
248
255
|
UnsupportedProviderStateError,
|
|
249
256
|
} from "./runtime/state.js";
|
|
@@ -537,6 +544,18 @@ export * from "./utils/parse.js";
|
|
|
537
544
|
export * from "./utils/text.js";
|
|
538
545
|
export * from "./utils/transform.js";
|
|
539
546
|
|
|
547
|
+
export {
|
|
548
|
+
HttpTelemetryCollector,
|
|
549
|
+
type HttpTelemetrySink,
|
|
550
|
+
type HttpTelemetryRequestSink,
|
|
551
|
+
type HttpAttemptTelemetryEvent,
|
|
552
|
+
type HttpAttemptSample,
|
|
553
|
+
type HttpTelemetryErrorCode,
|
|
554
|
+
type HttpTelemetryRetryPayload,
|
|
555
|
+
type HttpTelemetryLogPayload,
|
|
556
|
+
type HttpTelemetryHeaderPayload,
|
|
557
|
+
} from "./runtime/http-telemetry.js";
|
|
558
|
+
|
|
540
559
|
export {
|
|
541
560
|
NativeTelemetryCollector,
|
|
542
561
|
type NativeAttemptSample,
|
package/src/lint.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
type HandleFieldMeta,
|
|
12
12
|
type HandleKindDeclaration,
|
|
13
13
|
handleFieldDescription,
|
|
14
|
+
handleIssuerList,
|
|
15
|
+
isHandleIssuedBy,
|
|
14
16
|
isHandleFieldMeta,
|
|
15
17
|
} from "./handle-meta.js";
|
|
16
18
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
@@ -1534,7 +1536,7 @@ function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
|
|
|
1534
1536
|
typeof record.name === "string" &&
|
|
1535
1537
|
(record.type === "cursor" || record.type === "draft") &&
|
|
1536
1538
|
typeof record.fieldName === "string" &&
|
|
1537
|
-
(record.issuedBy === undefined ||
|
|
1539
|
+
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy))
|
|
1538
1540
|
);
|
|
1539
1541
|
});
|
|
1540
1542
|
}
|
|
@@ -1542,8 +1544,9 @@ function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
|
|
|
1542
1544
|
/**
|
|
1543
1545
|
* ADR-0012 handle rules: every `x-apifuse-handle` schema field must belong to
|
|
1544
1546
|
* a declared kind under its declared property key; every declared kind must be
|
|
1545
|
-
* both issued (some output) and accepted (some input); `issuedBy`
|
|
1546
|
-
* operation
|
|
1547
|
+
* both issued (some output) and accepted (some input); every `issuedBy` entry
|
|
1548
|
+
* must be an operation key and at least one of them must output the field; and
|
|
1549
|
+
* `ctx.handle` needs `state`.
|
|
1547
1550
|
*/
|
|
1548
1551
|
function lintHandleDeclarations(provider: {
|
|
1549
1552
|
id?: string;
|
|
@@ -1636,19 +1639,39 @@ function lintHandleDeclarations(provider: {
|
|
|
1636
1639
|
}
|
|
1637
1640
|
|
|
1638
1641
|
if (kind.issuedBy === undefined) continue;
|
|
1639
|
-
|
|
1642
|
+
const issuers = handleIssuerList(kind.issuedBy);
|
|
1643
|
+
const known = issuers.filter((operationKey) => Object.hasOwn(operations, operationKey));
|
|
1644
|
+
for (const operationKey of issuers) {
|
|
1645
|
+
if (known.includes(operationKey)) continue;
|
|
1640
1646
|
diagnostics.push({
|
|
1641
1647
|
rule: "handle-issued-by",
|
|
1642
1648
|
level: "error",
|
|
1643
1649
|
field,
|
|
1644
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${
|
|
1650
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${operationKey}", which is not an operation key.`,
|
|
1645
1651
|
});
|
|
1646
|
-
}
|
|
1652
|
+
}
|
|
1653
|
+
if (known.length === 0) continue;
|
|
1654
|
+
const issuing = known.filter((operationKey) =>
|
|
1655
|
+
outputKindsByOperation.get(operationKey)?.has(kind.name),
|
|
1656
|
+
);
|
|
1657
|
+
if (issuing.length > 0) continue;
|
|
1658
|
+
if (known.length === 1) {
|
|
1659
|
+
diagnostics.push({
|
|
1660
|
+
rule: "handle-issued-by",
|
|
1661
|
+
level: "error",
|
|
1662
|
+
field,
|
|
1663
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind.fieldName}" handle field.`,
|
|
1664
|
+
});
|
|
1665
|
+
} else {
|
|
1647
1666
|
diagnostics.push({
|
|
1648
1667
|
rule: "handle-issued-by",
|
|
1649
1668
|
level: "error",
|
|
1650
1669
|
field,
|
|
1651
|
-
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy
|
|
1670
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy ${known
|
|
1671
|
+
.map((operationKey) => `"${operationKey}"`)
|
|
1672
|
+
.join(
|
|
1673
|
+
", ",
|
|
1674
|
+
)}, but none of those operations' outputs has a "${kind.fieldName}" handle field. At least one listed issuer must output it.`,
|
|
1652
1675
|
});
|
|
1653
1676
|
}
|
|
1654
1677
|
}
|
package/src/provider.ts
CHANGED
|
@@ -154,6 +154,12 @@ export {
|
|
|
154
154
|
qualifyProviderLocaleKey,
|
|
155
155
|
} from "./i18n/index.js";
|
|
156
156
|
export * from "./handle.js";
|
|
157
|
+
export {
|
|
158
|
+
APIFUSE_HANDLE_META_KEY,
|
|
159
|
+
HANDLE_KIND_NAME_PATTERN,
|
|
160
|
+
handleFieldDescription,
|
|
161
|
+
isHandleFieldMeta,
|
|
162
|
+
} from "./handle-meta.js";
|
|
157
163
|
export {
|
|
158
164
|
type CreateHandleContextOptions,
|
|
159
165
|
type CreateTestHandleContextOptions,
|
|
@@ -163,6 +169,10 @@ export {
|
|
|
163
169
|
type NormalizedHandle,
|
|
164
170
|
normalizeHandle,
|
|
165
171
|
} from "./runtime/handle.js";
|
|
172
|
+
export {
|
|
173
|
+
createMemoryProviderRuntimeState,
|
|
174
|
+
createUnsupportedProviderRuntimeState,
|
|
175
|
+
} from "./runtime/state.js";
|
|
166
176
|
export {
|
|
167
177
|
APIFUSE_CONTENT_PROVENANCE_META_KEY,
|
|
168
178
|
APIFUSE_CONTENT_TRUST_META_KEY,
|
package/src/runtime/handle.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Storage layout: one state namespace per kind, `handle.<kind>`, scoped by the
|
|
5
5
|
* kind's access (bound → connection scope via `state.forConnection`, public →
|
|
6
|
-
* provider scope). The state key is the
|
|
6
|
+
* provider scope; both cursors and drafts may be either). The state key is the
|
|
7
|
+
* canonical handle string. Records hold
|
|
7
8
|
* the validated data plus status/timestamps; the handle string itself carries
|
|
8
9
|
* no payload and no signature.
|
|
9
10
|
*/
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
isDraftKind,
|
|
27
28
|
type ResultOf,
|
|
28
29
|
} from "../handle.js";
|
|
30
|
+
import { formatHandleIssuers } from "../handle-meta.js";
|
|
29
31
|
import type {
|
|
30
32
|
ProviderRequestContext,
|
|
31
33
|
ProviderRuntimeState,
|
|
@@ -246,9 +248,8 @@ export function formatNormalizationClasses(classes: readonly string[]): string {
|
|
|
246
248
|
// ---------------------------------------------------------------------------
|
|
247
249
|
|
|
248
250
|
function publicCollapsedMessage(kind: HandleKind): string {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
: "Request a new one.";
|
|
251
|
+
const issuers = formatHandleIssuers(kind.issuedBy);
|
|
252
|
+
const again = issuers ? `Call ${issuers} again to get a new one.` : "Request a new one.";
|
|
252
253
|
return `\`${kind.fieldName}\` is not valid or has expired. ${again}`;
|
|
253
254
|
}
|
|
254
255
|
|
|
@@ -257,7 +258,7 @@ function normalizationError(kind: HandleKind, failure: NormalizationFailure): Ha
|
|
|
257
258
|
return new HandleError(
|
|
258
259
|
"HANDLE_KIND_MISMATCH",
|
|
259
260
|
`\`${kind.fieldName}\` must be a \`${kind.name}_...\` handle as returned by ${
|
|
260
|
-
kind.issuedBy
|
|
261
|
+
formatHandleIssuers(kind.issuedBy) ?? "the issuing operation"
|
|
261
262
|
}. ${handleRecoverySentence(kind)}`,
|
|
262
263
|
{ kind: kind.name, details: { field: kind.fieldName } },
|
|
263
264
|
);
|
|
@@ -352,7 +353,7 @@ function committedError(kind: HandleKind): HandleError {
|
|
|
352
353
|
function connectionRequiredError(kind: HandleKind, operation: HandleOperation): HandleError {
|
|
353
354
|
return new HandleError(
|
|
354
355
|
"HANDLE_CONNECTION_REQUIRED",
|
|
355
|
-
`Handle kind "${kind.name}" is bound to a connection, but request.connectionId is missing for ${operation}. Bound handles require a connection-scoped request; use access: "public" for connection-less
|
|
356
|
+
`Handle kind "${kind.name}" is bound to a connection, but request.connectionId is missing for ${operation}. Bound handles require a connection-scoped request; use access: "public" for connection-less kinds.`,
|
|
356
357
|
{ kind: kind.name },
|
|
357
358
|
);
|
|
358
359
|
}
|
|
@@ -738,8 +739,11 @@ export function createHandleContext(options: CreateHandleContextOptions): Handle
|
|
|
738
739
|
};
|
|
739
740
|
}
|
|
740
741
|
|
|
741
|
-
async function
|
|
742
|
-
|
|
742
|
+
async function createRecord<K extends HandleKind>(
|
|
743
|
+
kind: K,
|
|
744
|
+
data: InputOf<K>,
|
|
745
|
+
): Promise<HandleRecord<K>> {
|
|
746
|
+
return await instrumented<HandleRecord<K>>(kind, "create", async (report) => {
|
|
743
747
|
const validated = validateData(kind, "create", data);
|
|
744
748
|
const namespace = resolveNamespace(kind, "create");
|
|
745
749
|
const now = nowMs();
|
|
@@ -766,7 +770,9 @@ export function createHandleContext(options: CreateHandleContextOptions): Handle
|
|
|
766
770
|
const result = await storage(kind, () =>
|
|
767
771
|
namespace.compareAndSet(canonical, 0, record, { ttl: msDuration(writeTtlMs) }),
|
|
768
772
|
);
|
|
769
|
-
if (result.ok)
|
|
773
|
+
if (result.ok) {
|
|
774
|
+
return { value: toHandleRecord(kind, canonical, record), outcome: "success" };
|
|
775
|
+
}
|
|
770
776
|
}
|
|
771
777
|
throw storageUnavailableError(
|
|
772
778
|
kind,
|
|
@@ -775,6 +781,10 @@ export function createHandleContext(options: CreateHandleContextOptions): Handle
|
|
|
775
781
|
});
|
|
776
782
|
}
|
|
777
783
|
|
|
784
|
+
async function create<K extends HandleKind>(kind: K, data: InputOf<K>): Promise<string> {
|
|
785
|
+
return (await createRecord(kind, data)).handle;
|
|
786
|
+
}
|
|
787
|
+
|
|
778
788
|
async function read<K extends HandleKind>(kind: K, handle: string): Promise<HandleRecord<K>> {
|
|
779
789
|
return await instrumented<HandleRecord<K>>(kind, "read", async (report) => {
|
|
780
790
|
const loaded = await load(kind, "read", handle, report);
|
|
@@ -1139,7 +1149,7 @@ export function createHandleContext(options: CreateHandleContextOptions): Handle
|
|
|
1139
1149
|
});
|
|
1140
1150
|
}
|
|
1141
1151
|
|
|
1142
|
-
return { create, read, update, commit, discard };
|
|
1152
|
+
return { create, createRecord, read, update, commit, discard };
|
|
1143
1153
|
}
|
|
1144
1154
|
|
|
1145
1155
|
/**
|