@apifuse/provider-sdk 2.2.0-beta.52 → 2.2.0-beta.54
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 +8 -0
- package/dist/cli/migrate-operation-declaration.d.ts +1 -1
- package/dist/cli/migrate-operation-declaration.js +126 -5
- package/dist/i18n/operation-locale-namespace.d.ts +2 -0
- package/dist/i18n/operation-locale-namespace.js +7 -0
- package/dist/runtime/auth-flow.d.ts +1 -0
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/trace.d.ts +7 -0
- package/dist/runtime/trace.js +24 -1
- package/dist/server/serve-implementation.d.ts +4 -0
- package/dist/server/serve-implementation.js +576 -321
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +17 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +8 -0
- package/src/cli/migrate-operation-declaration.ts +208 -6
- package/src/i18n/operation-locale-namespace.ts +10 -0
- package/src/runtime/auth-flow.ts +3 -0
- package/src/runtime/trace.ts +37 -1
- package/src/server/serve-implementation.ts +759 -586
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.54
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 4dccd0cd9d3443cde4edebf98ffc6c11beecb9ea.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.53
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 40a916bac16bb2b2bbda7fc4a96132d10ca103bf.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.52
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit 6e64ae0ad2e735b3de0c115f5ea29517efcf4ff2.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type OperationDeclarationRefusalReason = "no_safety" | "safety_conflict" | "locale_key_conflict" | "connection_mode_conflict" | "execution_conflict" | "approval_conflict" | "non_literal" | "unsupported_member" | "factory_composed_operations" | "source_syntax" | "codemod_syntax" | "missing_english_locale" | "operation_id_unresolved" | "examples_conflict" | "locale_todo_conflict";
|
|
1
|
+
export type OperationDeclarationRefusalReason = "no_safety" | "safety_conflict" | "locale_key_conflict" | "connection_mode_conflict" | "execution_conflict" | "approval_conflict" | "non_literal" | "unsupported_member" | "factory_composed_operations" | "source_syntax" | "codemod_syntax" | "missing_english_locale" | "operation_id_unresolved" | "examples_conflict" | "invalid_locale_key" | "locale_todo_conflict";
|
|
2
2
|
export type OperationDeclarationRefusal = {
|
|
3
3
|
readonly file: string;
|
|
4
4
|
readonly operationKey: string;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { assertProviderLocaleKey } from "../i18n/keys.js";
|
|
4
|
+
import { operationIdToLocaleNamespace } from "../i18n/operation-locale-namespace.js";
|
|
3
5
|
const ts = await loadTypeScript();
|
|
4
6
|
async function loadTypeScript() {
|
|
5
7
|
try {
|
|
@@ -81,6 +83,10 @@ export function migrateOperationDeclaration(sourceText, fileName, options = {})
|
|
|
81
83
|
}
|
|
82
84
|
if (refusals.length > 0)
|
|
83
85
|
return { status: "refused", refusals };
|
|
86
|
+
const invalidLocaleKey = findInvalidLocaleTodo(todos, fileName);
|
|
87
|
+
if (invalidLocaleKey !== undefined) {
|
|
88
|
+
return { status: "refused", refusals: [invalidLocaleKey] };
|
|
89
|
+
}
|
|
84
90
|
if (edits.length === 0) {
|
|
85
91
|
return {
|
|
86
92
|
status: "unchanged",
|
|
@@ -221,7 +227,20 @@ function planOperationMigration(source, fileName, site, constObjects, constArray
|
|
|
221
227
|
if (merged.insert !== undefined)
|
|
222
228
|
addInsertion(insertions, "docs", merged.insert);
|
|
223
229
|
}
|
|
224
|
-
const
|
|
230
|
+
const title = planTitleLocale(top.byName.get("title"), top.byName.get("titleKey"), docs.get("titleKey"), fileName, site, localeFiles);
|
|
231
|
+
if ("refusal" in title)
|
|
232
|
+
return title;
|
|
233
|
+
const localeNamespace = top.byName.get("inputExamples") === undefined
|
|
234
|
+
? { namespace: site.operationKey }
|
|
235
|
+
: resolveOperationLocaleNamespace([
|
|
236
|
+
top.byName.get("titleKey"),
|
|
237
|
+
docs.get("titleKey"),
|
|
238
|
+
top.byName.get("descriptionKey"),
|
|
239
|
+
docs.get("descriptionKey"),
|
|
240
|
+
], fileName, site);
|
|
241
|
+
if ("refusal" in localeNamespace)
|
|
242
|
+
return localeNamespace;
|
|
243
|
+
const examples = planExamples(top.byName.get("inputExamples"), top.byName.get("examples"), fileName, site, source, constArrays, localeFiles, localeNamespace.namespace);
|
|
225
244
|
if ("refusal" in examples)
|
|
226
245
|
return examples;
|
|
227
246
|
const edits = [...examples.edits];
|
|
@@ -245,7 +264,66 @@ function planOperationMigration(source, fileName, site, constObjects, constArray
|
|
|
245
264
|
if (!edits.some((existing) => rangesOverlap(existing, edit)))
|
|
246
265
|
edits.push(edit);
|
|
247
266
|
}
|
|
248
|
-
return { edits, localeTodos: examples.localeTodos };
|
|
267
|
+
return { edits, localeTodos: [...title.localeTodos, ...examples.localeTodos] };
|
|
268
|
+
}
|
|
269
|
+
function planTitleLocale(title, flatTitleKey, nestedTitleKey, fileName, site, localeFiles) {
|
|
270
|
+
if (title === undefined)
|
|
271
|
+
return { localeTodos: [] };
|
|
272
|
+
const originalProse = literalString(title.initializer);
|
|
273
|
+
if (originalProse === undefined) {
|
|
274
|
+
return nonLiteral(fileName, site.operationKey, "title must be a string literal so its authored prose can be preserved in the English locale catalog.");
|
|
275
|
+
}
|
|
276
|
+
if (!site.operationIdProven) {
|
|
277
|
+
return {
|
|
278
|
+
refusal: refusal(fileName, site.operationKey, "operation_id_unresolved", "title requires an exact operation id proven from a static operations map."),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (!localeFiles.includes("locales/en.json")) {
|
|
282
|
+
return {
|
|
283
|
+
refusal: refusal(fileName, site.operationKey, "missing_english_locale", "title cannot be migrated because locales/en.json does not exist."),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
let selectedTitleKey = flatTitleKey ?? nestedTitleKey;
|
|
287
|
+
if (flatTitleKey !== undefined && nestedTitleKey !== undefined) {
|
|
288
|
+
const same = equivalentLiteral(flatTitleKey.initializer, nestedTitleKey.initializer);
|
|
289
|
+
if (same === undefined) {
|
|
290
|
+
return nonLiteral(fileName, site.operationKey, "titleKey must be literal when both top-level and nested declarations exist.");
|
|
291
|
+
}
|
|
292
|
+
if (!same) {
|
|
293
|
+
return {
|
|
294
|
+
refusal: refusal(fileName, site.operationKey, "locale_key_conflict", "Top-level titleKey conflicts with nested titleKey."),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
selectedTitleKey = flatTitleKey;
|
|
298
|
+
}
|
|
299
|
+
const explicitTitleKey = selectedTitleKey === undefined ? undefined : literalString(selectedTitleKey.initializer);
|
|
300
|
+
if (selectedTitleKey !== undefined && explicitTitleKey === undefined) {
|
|
301
|
+
return nonLiteral(fileName, site.operationKey, "titleKey must be a string literal so the title locale destination is provable.");
|
|
302
|
+
}
|
|
303
|
+
let selectedTitleLocaleKey;
|
|
304
|
+
if (explicitTitleKey !== undefined) {
|
|
305
|
+
selectedTitleLocaleKey = explicitTitleKey;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
try {
|
|
309
|
+
selectedTitleLocaleKey = `operations.${operationIdToLocaleNamespace(site.operationKey)}.title`;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
refusal: invalidLocaleKeyRefusal(fileName, site.operationKey, `operations.${site.operationKey}.title`, error),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
localeTodos: [
|
|
319
|
+
{
|
|
320
|
+
localeFile: "locales/en.json",
|
|
321
|
+
operationKey: site.operationKey,
|
|
322
|
+
key: selectedTitleLocaleKey,
|
|
323
|
+
originalProse,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
249
327
|
}
|
|
250
328
|
function resolveRiskClass(top, annotations, toolRouter, fileName, operationKey) {
|
|
251
329
|
const topRisk = top.get("riskClass");
|
|
@@ -381,7 +459,7 @@ function mergeFlatAndNested(field, flat, nested, fileName, operationKey, source,
|
|
|
381
459
|
}
|
|
382
460
|
return {};
|
|
383
461
|
}
|
|
384
|
-
function planExamples(inputExamples, existingExamples, fileName, site, source, constArrays, localeFiles) {
|
|
462
|
+
function planExamples(inputExamples, existingExamples, fileName, site, source, constArrays, localeFiles, localeNamespace) {
|
|
385
463
|
if (inputExamples === undefined)
|
|
386
464
|
return { edits: [], localeTodos: [] };
|
|
387
465
|
if (existingExamples !== undefined) {
|
|
@@ -440,7 +518,7 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, c
|
|
|
440
518
|
if (scenarioProse === undefined) {
|
|
441
519
|
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].scenario must be a string literal.`);
|
|
442
520
|
}
|
|
443
|
-
const scenarioKey = `operations.${
|
|
521
|
+
const scenarioKey = `operations.${localeNamespace}.examples.${index}.scenario`;
|
|
444
522
|
edits.push(replaceExampleLocaleMember(scenario, "scenarioKey", scenarioKey, source));
|
|
445
523
|
for (const localeFile of localeFiles) {
|
|
446
524
|
todos.push({
|
|
@@ -456,7 +534,7 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, c
|
|
|
456
534
|
if (rationaleProse === undefined) {
|
|
457
535
|
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].rationale must be a string literal.`);
|
|
458
536
|
}
|
|
459
|
-
const rationaleKey = `operations.${
|
|
537
|
+
const rationaleKey = `operations.${localeNamespace}.examples.${index}.rationale`;
|
|
460
538
|
edits.push(replaceExampleLocaleMember(rationale, "rationaleKey", rationaleKey, source));
|
|
461
539
|
for (const localeFile of localeFiles) {
|
|
462
540
|
todos.push({
|
|
@@ -470,6 +548,34 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, c
|
|
|
470
548
|
}
|
|
471
549
|
return { edits, localeTodos: todos };
|
|
472
550
|
}
|
|
551
|
+
function resolveOperationLocaleNamespace(members, fileName, site) {
|
|
552
|
+
const authoredNamespaces = new Set();
|
|
553
|
+
for (const member of members) {
|
|
554
|
+
const localeKey = literalString(member?.initializer);
|
|
555
|
+
if (localeKey === undefined)
|
|
556
|
+
continue;
|
|
557
|
+
const segments = localeKey.split(".");
|
|
558
|
+
if (segments[0] === "operations" && segments[1] !== undefined) {
|
|
559
|
+
authoredNamespaces.add(segments[1]);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (authoredNamespaces.size > 1) {
|
|
563
|
+
return {
|
|
564
|
+
refusal: refusal(fileName, site.operationKey, "locale_key_conflict", `Operation titleKey and descriptionKey declarations use different locale namespaces: ${[...authoredNamespaces].join(", ")}.`),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const authored = authoredNamespaces.values().next().value;
|
|
568
|
+
if (authored !== undefined)
|
|
569
|
+
return { namespace: authored };
|
|
570
|
+
try {
|
|
571
|
+
return { namespace: operationIdToLocaleNamespace(site.operationKey) };
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
return {
|
|
575
|
+
refusal: invalidLocaleKeyRefusal(fileName, site.operationKey, `operations.${site.operationKey}.examples`, error),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
473
579
|
function replaceExampleLocaleMember(member, newName, localeKey, source) {
|
|
474
580
|
return {
|
|
475
581
|
start: member.property.getStart(source),
|
|
@@ -1049,6 +1155,21 @@ function firstSyntaxError(source) {
|
|
|
1049
1155
|
function refusal(file, operationKey, reason, detail) {
|
|
1050
1156
|
return { file, operationKey, reason, detail };
|
|
1051
1157
|
}
|
|
1158
|
+
function findInvalidLocaleTodo(todos, fileName) {
|
|
1159
|
+
for (const todo of todos) {
|
|
1160
|
+
try {
|
|
1161
|
+
assertProviderLocaleKey(todo.key);
|
|
1162
|
+
}
|
|
1163
|
+
catch (error) {
|
|
1164
|
+
return invalidLocaleKeyRefusal(fileName, todo.operationKey, todo.key, error);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
function invalidLocaleKeyRefusal(fileName, operationKey, localeKey, error) {
|
|
1170
|
+
const validatorDetail = error instanceof Error ? error.message : String(error);
|
|
1171
|
+
return refusal(fileName, operationKey, "invalid_locale_key", `Refusing to write invalid provider locale key ${JSON.stringify(localeKey)}: ${validatorDetail}`);
|
|
1172
|
+
}
|
|
1052
1173
|
function nonLiteral(fileName, operationKey, detail) {
|
|
1053
1174
|
return {
|
|
1054
1175
|
refusal: refusal(fileName, operationKey, "non_literal", detail),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { assertProviderLocaleKey } from "./keys.js";
|
|
2
|
+
/** Canonical locale-catalog namespace for a URL-safe provider operation id. */
|
|
3
|
+
export function operationIdToLocaleNamespace(operationId) {
|
|
4
|
+
const namespace = operationId.replace(/[-_]([a-z0-9])/g, (_separator, character) => character.toUpperCase());
|
|
5
|
+
assertProviderLocaleKey(`operations.${namespace}.description`);
|
|
6
|
+
return namespace;
|
|
7
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createAuthFlowHelpers } from "../auth.js";
|
|
|
3
3
|
import { createUnsupportedOcrClient } from "./ocr.js";
|
|
4
4
|
import { createUnsupportedResolverClient } from "./resolver-shared.js";
|
|
5
5
|
import { createUnsupportedSttClient } from "./stt.js";
|
|
6
|
+
import { createTraceContext } from "./trace.js";
|
|
6
7
|
function normalizeAllowedKeys(allowedKeys) {
|
|
7
8
|
return new Set(allowedKeys.filter((key) => key.trim().length > 0));
|
|
8
9
|
}
|
|
@@ -39,6 +40,7 @@ export function createFlowContext(options) {
|
|
|
39
40
|
externalRef: options.externalRef,
|
|
40
41
|
tenantId: options.tenantId,
|
|
41
42
|
providerId: options.providerId,
|
|
43
|
+
trace: options.trace ?? createTraceContext(),
|
|
42
44
|
http: options.http,
|
|
43
45
|
state: options.state,
|
|
44
46
|
stealth: options.stealth,
|
package/dist/runtime/trace.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface CreateTraceContextOptions {
|
|
|
10
10
|
onSpan?: (span: Span) => void;
|
|
11
11
|
exportOptions?: OTLPExportOptions;
|
|
12
12
|
resourceAttributes?: Record<string, string>;
|
|
13
|
+
/** W3C-compatible 32-character lowercase hexadecimal trace id used for export. */
|
|
14
|
+
traceId?: string;
|
|
13
15
|
/**
|
|
14
16
|
* Applied to a detached copy of each span immediately before OTLP export; never touches
|
|
15
17
|
* getSpans() or onSpan. Returning nothing (or throwing) drops that export batch.
|
|
@@ -25,6 +27,11 @@ export interface TraceRecorder {
|
|
|
25
27
|
runSpan<T>(name: string, fn: () => Promise<T> | T, options?: SpanHookOptions<T>): Promise<T>;
|
|
26
28
|
}
|
|
27
29
|
export declare const TRACE_RECORDER: unique symbol;
|
|
30
|
+
/** Updates request metadata before its pending root span completes and exports. */
|
|
31
|
+
export declare function updateTraceContextExportMetadata(trace: BaseTraceContext, input: {
|
|
32
|
+
traceId?: string;
|
|
33
|
+
resourceAttributes?: Record<string, string>;
|
|
34
|
+
}): void;
|
|
28
35
|
export declare function resolveTraceContextOptions(config?: TraceConfig): CreateTraceContextOptions;
|
|
29
36
|
export declare function getTraceRecorder(trace: BaseTraceContext): TraceRecorder | null;
|
|
30
37
|
export declare function createTraceContext(options?: CreateTraceContextOptions): TraceContext;
|
package/dist/runtime/trace.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { exportSpansOTLP } from "./otlp.js";
|
|
3
3
|
export const TRACE_RECORDER = Symbol.for("@apifuse/provider-sdk/runtime/trace-recorder");
|
|
4
|
+
const TRACE_EXPORT_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/trace-export-metadata");
|
|
5
|
+
function assertValidTraceId(traceId) {
|
|
6
|
+
if (!/^[0-9a-f]{32}$/.test(traceId) || /^0{32}$/.test(traceId)) {
|
|
7
|
+
throw new TypeError("traceId must be 32 lowercase hexadecimal characters and non-zero");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Updates request metadata before its pending root span completes and exports. */
|
|
11
|
+
export function updateTraceContextExportMetadata(trace, input) {
|
|
12
|
+
trace[TRACE_EXPORT_METADATA]?.update(input);
|
|
13
|
+
}
|
|
4
14
|
function buildOTLPExportOptions(config) {
|
|
5
15
|
if (config?.exporter !== "otlp") {
|
|
6
16
|
return undefined;
|
|
@@ -67,6 +77,8 @@ export function getTraceRecorder(trace) {
|
|
|
67
77
|
return trace[TRACE_RECORDER] ?? null;
|
|
68
78
|
}
|
|
69
79
|
export function createTraceContext(options = {}) {
|
|
80
|
+
if (options.traceId !== undefined)
|
|
81
|
+
assertValidTraceId(options.traceId);
|
|
70
82
|
const maxSpans = options.maxSpans ?? 1000;
|
|
71
83
|
const completed = [];
|
|
72
84
|
const activeSpanStorage = new AsyncLocalStorage();
|
|
@@ -79,7 +91,7 @@ export function createTraceContext(options = {}) {
|
|
|
79
91
|
: undefined;
|
|
80
92
|
// One trace id per context so every export batch of this request shares it and
|
|
81
93
|
// two processes can never mint the same id.
|
|
82
|
-
|
|
94
|
+
let exportTraceId = options.traceId ?? crypto.randomUUID().replace(/-/g, "");
|
|
83
95
|
let exportScheduled = false;
|
|
84
96
|
// One pending batch per context: roots completing before the flush share it, and a span is
|
|
85
97
|
// handed to the exporter exactly once, so later roots never re-send earlier spans.
|
|
@@ -168,6 +180,17 @@ export function createTraceContext(options = {}) {
|
|
|
168
180
|
return completed.map((entry) => ({ ...entry.span }));
|
|
169
181
|
},
|
|
170
182
|
[TRACE_RECORDER]: recorder,
|
|
183
|
+
[TRACE_EXPORT_METADATA]: {
|
|
184
|
+
update(input) {
|
|
185
|
+
if (input.traceId !== undefined) {
|
|
186
|
+
assertValidTraceId(input.traceId);
|
|
187
|
+
exportTraceId = input.traceId;
|
|
188
|
+
}
|
|
189
|
+
if (input.resourceAttributes !== undefined && exportResourceAttributes) {
|
|
190
|
+
Object.assign(exportResourceAttributes, input.resourceAttributes);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
},
|
|
171
194
|
};
|
|
172
195
|
return traceContext;
|
|
173
196
|
}
|
|
@@ -79,6 +79,10 @@ type ProviderServerLogEventBase = ProviderRequestCost & {
|
|
|
79
79
|
kind: "operation" | "auth";
|
|
80
80
|
route: string;
|
|
81
81
|
requestId?: string;
|
|
82
|
+
connectionId?: string;
|
|
83
|
+
flowId?: string;
|
|
84
|
+
tenantId?: string;
|
|
85
|
+
requestedProviderId?: string;
|
|
82
86
|
status: number;
|
|
83
87
|
proxy?: ProxyTelemetryLogPayload;
|
|
84
88
|
};
|