@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
|
@@ -118,21 +118,139 @@ wolverine womb woolworker workbasket wound wrangle wreckage wristwatch wrongdoin
|
|
|
118
118
|
yacht yahoo yard yearbook yesterday yiddish yield yo-yo yodel yogurt yuppie zealot zebra zeppelin
|
|
119
119
|
zestfully zigzagged zillion zipping zirconium zodiac zombie zookeeper zucchini
|
|
120
120
|
`;
|
|
121
|
-
export const
|
|
122
|
-
export const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
export const HANDLE_WORDLIST_SIZE = 1_296;
|
|
122
|
+
export const HANDLE_WORD_ENTROPY_BITS = Math.log2(HANDLE_WORDLIST_SIZE);
|
|
123
|
+
/**
|
|
124
|
+
* Bound handles live under `state.forConnection(connectionId)`: another
|
|
125
|
+
* connection cannot address them at all, so no guessing analysis applies. The
|
|
126
|
+
* only requirement is uniqueness among live handles of one connection
|
|
127
|
+
* (1,296^2 = 1.68M keys, CAS-if-absent with retries), which two words satisfy.
|
|
128
|
+
*/
|
|
129
|
+
export const BOUND_HANDLE_WORD_COUNT = 2;
|
|
130
|
+
/** Public handles are addressable by anyone; ADR 0006 §2 guessing numbers apply. */
|
|
131
|
+
export const PUBLIC_HANDLE_WORD_COUNT = 4;
|
|
132
|
+
/** Used when a public kind declares `strength: "high"` or its ttl exceeds one hour. */
|
|
133
|
+
export const HIGH_PUBLIC_HANDLE_WORD_COUNT = 5;
|
|
134
|
+
export const PUBLIC_HANDLE_ENTROPY_BITS = PUBLIC_HANDLE_WORD_COUNT * HANDLE_WORD_ENTROPY_BITS;
|
|
135
|
+
export const HIGH_PUBLIC_HANDLE_ENTROPY_BITS = HIGH_PUBLIC_HANDLE_WORD_COUNT * HANDLE_WORD_ENTROPY_BITS;
|
|
127
136
|
export const EFF_SHORT_WORDLIST_2 = Object.freeze(EFF_SHORT_WORDLIST_2_TEXT.trim().split(/\s+/));
|
|
128
137
|
const EFF_SHORT_WORD_SET = new Set(EFF_SHORT_WORDLIST_2);
|
|
129
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Words the runtime may issue. The handle grammar joins words with `-`, so the
|
|
140
|
+
* one hyphenated official entry (`yo-yo`) would fuse with the separator and is
|
|
141
|
+
* never issued. The official list itself stays embedded unchanged.
|
|
142
|
+
*/
|
|
143
|
+
export const ISSUABLE_HANDLE_WORDS = Object.freeze(EFF_SHORT_WORDLIST_2.filter((word) => !word.includes("-")));
|
|
144
|
+
export function handleWordAt(index) {
|
|
130
145
|
const word = EFF_SHORT_WORDLIST_2[index];
|
|
131
146
|
if (word === undefined) {
|
|
132
|
-
throw new RangeError(`
|
|
147
|
+
throw new RangeError(`Handle word index is out of range: ${index}`);
|
|
133
148
|
}
|
|
134
149
|
return word;
|
|
135
150
|
}
|
|
136
|
-
export function
|
|
151
|
+
export function isHandleWord(value) {
|
|
137
152
|
return EFF_SHORT_WORD_SET.has(value);
|
|
138
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Symmetric-deletion index (SymSpell, max distance 1). Two strings at
|
|
156
|
+
* Levenshtein distance <= 1 always share a form reachable by deleting at most
|
|
157
|
+
* one character from each side, so a lookup of the query and its one-deletion
|
|
158
|
+
* variants yields every candidate; a real distance check then removes the false
|
|
159
|
+
* positives (e.g. transpositions). ~10k entries, O(len) per lookup.
|
|
160
|
+
*/
|
|
161
|
+
/** Shortest and longest words in the list (3 and 10 for the official list). */
|
|
162
|
+
export const MIN_HANDLE_WORD_LENGTH = Math.min(...EFF_SHORT_WORDLIST_2.map((word) => word.length));
|
|
163
|
+
export const MAX_HANDLE_WORD_LENGTH = Math.max(...EFF_SHORT_WORDLIST_2.map((word) => word.length));
|
|
164
|
+
const DELETION_INDEX = (() => {
|
|
165
|
+
const index = new Map();
|
|
166
|
+
const add = (form, word) => {
|
|
167
|
+
const bucket = index.get(form);
|
|
168
|
+
if (bucket === undefined)
|
|
169
|
+
index.set(form, [word]);
|
|
170
|
+
else if (!bucket.includes(word))
|
|
171
|
+
bucket.push(word);
|
|
172
|
+
};
|
|
173
|
+
for (const word of EFF_SHORT_WORDLIST_2) {
|
|
174
|
+
add(word, word);
|
|
175
|
+
for (const variant of oneDeletionVariants(word))
|
|
176
|
+
add(variant, word);
|
|
177
|
+
}
|
|
178
|
+
return index;
|
|
179
|
+
})();
|
|
180
|
+
function oneDeletionVariants(value) {
|
|
181
|
+
const variants = [];
|
|
182
|
+
for (let position = 0; position < value.length; position += 1) {
|
|
183
|
+
variants.push(value.slice(0, position) + value.slice(position + 1));
|
|
184
|
+
}
|
|
185
|
+
return variants;
|
|
186
|
+
}
|
|
187
|
+
/** True when Levenshtein distance between `left` and `right` is at most 1. */
|
|
188
|
+
function withinEditDistanceOne(left, right) {
|
|
189
|
+
if (left === right)
|
|
190
|
+
return true;
|
|
191
|
+
const lengthDelta = left.length - right.length;
|
|
192
|
+
if (lengthDelta > 1 || lengthDelta < -1)
|
|
193
|
+
return false;
|
|
194
|
+
if (lengthDelta === 0) {
|
|
195
|
+
let mismatches = 0;
|
|
196
|
+
for (let position = 0; position < left.length; position += 1) {
|
|
197
|
+
if (left[position] !== right[position]) {
|
|
198
|
+
mismatches += 1;
|
|
199
|
+
if (mismatches > 1)
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
const longer = lengthDelta > 0 ? left : right;
|
|
206
|
+
const shorter = lengthDelta > 0 ? right : left;
|
|
207
|
+
let longIndex = 0;
|
|
208
|
+
let shortIndex = 0;
|
|
209
|
+
let skipped = false;
|
|
210
|
+
while (shortIndex < shorter.length) {
|
|
211
|
+
if (longer[longIndex] === shorter[shortIndex]) {
|
|
212
|
+
longIndex += 1;
|
|
213
|
+
shortIndex += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (skipped)
|
|
217
|
+
return false;
|
|
218
|
+
skipped = true;
|
|
219
|
+
longIndex += 1;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolves one handle segment to a wordlist entry: the exact word, or the
|
|
225
|
+
* unique word at edit distance <= `maxDistance`. Returns null when no word or
|
|
226
|
+
* more than one word qualifies. The official list has pairwise edit distance
|
|
227
|
+
* >= 3, so at distance 1 every non-null answer is unique; the uniqueness check
|
|
228
|
+
* is kept as a guard so a wordlist change cannot silently make normalization
|
|
229
|
+
* map one input onto two different canonical handles.
|
|
230
|
+
*/
|
|
231
|
+
export function findHandleWordWithinDistance(segment, maxDistance) {
|
|
232
|
+
if (segment.length === 0)
|
|
233
|
+
return null;
|
|
234
|
+
if (EFF_SHORT_WORD_SET.has(segment))
|
|
235
|
+
return segment;
|
|
236
|
+
if (maxDistance !== 1)
|
|
237
|
+
return null;
|
|
238
|
+
// A segment that is more than one character shorter or longer than every
|
|
239
|
+
// word cannot be within distance 1 of any word. Rejecting it here also bounds
|
|
240
|
+
// the variant generation below (which is O(len²) in memory) against
|
|
241
|
+
// client-supplied garbage of arbitrary length.
|
|
242
|
+
if (segment.length < MIN_HANDLE_WORD_LENGTH - maxDistance ||
|
|
243
|
+
segment.length > MAX_HANDLE_WORD_LENGTH + maxDistance) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const matches = new Set();
|
|
247
|
+
for (const form of [segment, ...oneDeletionVariants(segment)]) {
|
|
248
|
+
for (const candidate of DELETION_INDEX.get(form) ?? []) {
|
|
249
|
+
if (withinEditDistanceOne(segment, candidate))
|
|
250
|
+
matches.add(candidate);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (matches.size !== 1)
|
|
254
|
+
return null;
|
|
255
|
+
return matches.values().next().value ?? null;
|
|
256
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type HandleContext, type HandleKind, type HandleStatus, type HandleTelemetryEvent } from "../handle.js";
|
|
2
|
+
import type { ProviderRequestContext, ProviderRuntimeState } from "../types.js";
|
|
3
|
+
declare const RECORD_VERSION = 1;
|
|
4
|
+
export interface CreateHandleContextOptions {
|
|
5
|
+
readonly providerId: string;
|
|
6
|
+
readonly request?: ProviderRequestContext;
|
|
7
|
+
readonly state?: ProviderRuntimeState;
|
|
8
|
+
/** Clock used for record timestamps and TTL math; defaults to `Date.now`. */
|
|
9
|
+
readonly nowMs?: () => number;
|
|
10
|
+
/** Receives allowlisted metadata only; handle values, data, and results are never included. */
|
|
11
|
+
readonly onTelemetry?: (event: HandleTelemetryEvent) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Lease held (and renewed) by a `committing` claim while upstream work runs.
|
|
14
|
+
* Defaults to 5 minutes; exposed so tests can exercise renewal quickly.
|
|
15
|
+
*/
|
|
16
|
+
readonly commitLeaseMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface CreateTestHandleContextOptions {
|
|
19
|
+
readonly providerId?: string;
|
|
20
|
+
readonly state?: ProviderRuntimeState;
|
|
21
|
+
readonly request?: ProviderRequestContext;
|
|
22
|
+
readonly nowMs?: () => number;
|
|
23
|
+
readonly onTelemetry?: (event: HandleTelemetryEvent) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Persisted record shape (spec §6). `expires_at_ms` is the logical expiry the
|
|
27
|
+
* caller sees (sliding for drafts); `max_expires_at_ms` is the hard lifetime.
|
|
28
|
+
* Records are stored past their logical expiry for a grace window so an expired
|
|
29
|
+
* handle can be told apart from an unknown one (`HANDLE_EXPIRED` vs
|
|
30
|
+
* `HANDLE_NOT_FOUND`); after the grace window they are simply gone.
|
|
31
|
+
*/
|
|
32
|
+
export type StoredHandleRecord = {
|
|
33
|
+
readonly v: typeof RECORD_VERSION;
|
|
34
|
+
readonly kind: string;
|
|
35
|
+
readonly type: "cursor" | "draft";
|
|
36
|
+
/** `discarded` is an internal tombstone written by `discard`; reads treat it as absent. */
|
|
37
|
+
readonly status: HandleStatus | "discarded";
|
|
38
|
+
readonly data: unknown;
|
|
39
|
+
readonly result?: unknown;
|
|
40
|
+
readonly created_at_ms: number;
|
|
41
|
+
readonly expires_at_ms: number;
|
|
42
|
+
readonly max_expires_at_ms: number;
|
|
43
|
+
readonly committed_at_ms?: number;
|
|
44
|
+
/** Owner token of a `committing` claim; lets the claimant recognise its record after an ambiguous write. */
|
|
45
|
+
readonly claim_id?: string;
|
|
46
|
+
};
|
|
47
|
+
export type HandleNormalizationClass = "case" | "whitespace" | "separator" | "punctuation" | "typo";
|
|
48
|
+
export interface NormalizedHandle {
|
|
49
|
+
readonly canonical: string;
|
|
50
|
+
readonly words: readonly string[];
|
|
51
|
+
readonly normalization: readonly HandleNormalizationClass[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Tolerant parse of a raw handle string into its canonical form. Throws
|
|
55
|
+
* `HANDLE_KIND_MISMATCH` when the string does not start with the kind name and
|
|
56
|
+
* `HANDLE_INVALID` when the body cannot be resolved to the kind's word count.
|
|
57
|
+
*/
|
|
58
|
+
export declare function normalizeHandle(kind: HandleKind, raw: string): {
|
|
59
|
+
canonical: string;
|
|
60
|
+
words: string[];
|
|
61
|
+
normalization: string[];
|
|
62
|
+
};
|
|
63
|
+
export declare function formatNormalizationClasses(classes: readonly string[]): string;
|
|
64
|
+
export declare function createHandleContext(options: CreateHandleContextOptions): HandleContext;
|
|
65
|
+
/**
|
|
66
|
+
* Handle context for unit tests: memory state, a fixed connection, and an
|
|
67
|
+
* optional fake clock. Share one `state` between two contexts with different
|
|
68
|
+
* `request.connectionId`s to exercise bound isolation.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createTestHandleContext(options?: CreateTestHandleContextOptions): HandleContext;
|
|
71
|
+
export {};
|