@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.10

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.
Files changed (213) hide show
  1. package/AUTHORING.md +218 -21
  2. package/CHANGELOG.md +54 -0
  3. package/README.md +147 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +120 -0
  8. package/bin/apifuse-pack-smoke.ts +423 -0
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -29
  155. package/src/ceremonies/index.ts +30 -3
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +134 -2
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -44
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1282 -7
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1726 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -15
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -5
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +945 -185
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1172 -76
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1118 -44
  211. package/src/composite.ts +0 -43
  212. package/src/runtime/tls.ts +0 -425
  213. package/src/types/playwright-stealth.d.ts +0 -9
@@ -0,0 +1,73 @@
1
+ import { createHash, hkdfSync } from "node:crypto";
2
+ const SALT_PREFIX = "apifuse:v1:";
3
+ const OUTPUT_LENGTH = 32;
4
+ const cache = new Map();
5
+ function cacheKey(keyVersion, providerId, purpose) {
6
+ return `${keyVersion}\u0000${providerId}\u0000${purpose}`;
7
+ }
8
+ function computeSalt(purpose) {
9
+ return createHash("sha256")
10
+ .update(SALT_PREFIX + purpose)
11
+ .digest();
12
+ }
13
+ function computeInfo(providerId) {
14
+ return Buffer.from(`provider=${providerId}`, "utf8");
15
+ }
16
+ /** @internal Trusted loaders only; not re-exported to provider-importable paths. */
17
+ export function decodeMasterKey(encoded) {
18
+ if (typeof encoded !== "string" || encoded.length === 0) {
19
+ throw new ConfigurationError("master key is empty; set APIFUSE__KEYRING__MASTER_KEY_V{n} in the external secret manager");
20
+ }
21
+ const normalized = encoded.replace(/-/g, "+").replace(/_/g, "/");
22
+ const padded = normalized.length % 4 === 0
23
+ ? normalized
24
+ : normalized + "=".repeat(4 - (normalized.length % 4));
25
+ // Pre-decode character set guard — Buffer.from silently accepts invalid base64.
26
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(padded)) {
27
+ throw new ConfigurationError("master key is not valid base64");
28
+ }
29
+ const raw = Buffer.from(padded, "base64");
30
+ // Invalid base64 truncates silently; compare against the expected decode length.
31
+ const expectedMinLength = Math.floor((padded.length * 3) / 4) - 2;
32
+ if (raw.length < expectedMinLength) {
33
+ throw new ConfigurationError("master key is not valid base64");
34
+ }
35
+ if (raw.length < 32) {
36
+ throw new ConfigurationError(`master key must be ≥ 32 bytes after base64 decode (got ${raw.length})`);
37
+ }
38
+ return raw;
39
+ }
40
+ export class ConfigurationError extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "ConfigurationError";
44
+ }
45
+ }
46
+ /** @internal Trusted loaders only; not re-exported to provider-importable paths. */
47
+ export function deriveSubkey(masterSecret, providerId, purpose, keyVersion) {
48
+ if (masterSecret.length < 32) {
49
+ throw new ConfigurationError(`master key must be ≥ 32 bytes (got ${masterSecret.length})`);
50
+ }
51
+ if (providerId.length === 0) {
52
+ throw new ConfigurationError("providerId is empty");
53
+ }
54
+ const key = cacheKey(keyVersion, providerId, purpose);
55
+ const cached = cache.get(key);
56
+ if (cached) {
57
+ return cached;
58
+ }
59
+ const salt = computeSalt(purpose);
60
+ const info = computeInfo(providerId);
61
+ const subkey = Buffer.from(hkdfSync("sha256", masterSecret, salt, info, OUTPUT_LENGTH));
62
+ cache.set(key, subkey);
63
+ return subkey;
64
+ }
65
+ /**
66
+ * Invalidate the subkey cache. Used by the master-key rotation worker after a
67
+ * writer version change, and by tests to assert determinism.
68
+ *
69
+ * @internal
70
+ */
71
+ export function invalidateSubkeyCache() {
72
+ cache.clear();
73
+ }
@@ -0,0 +1,25 @@
1
+ export interface KeyRingOptions {
2
+ env: NodeJS.ProcessEnv;
3
+ envPrefix?: string;
4
+ acceptListVar?: string;
5
+ writerVersionVar?: string;
6
+ }
7
+ export interface KeyRingEntry {
8
+ version: number;
9
+ key: Buffer;
10
+ }
11
+ export interface KeyRing {
12
+ accept(version: number): KeyRingEntry;
13
+ activeWriter(): KeyRingEntry;
14
+ versions(): number[];
15
+ purgeVersion(version: number, isActiveInStore: (v: number) => Promise<boolean>): Promise<void>;
16
+ }
17
+ /**
18
+ * @internal Trusted loaders only; not re-exported to provider-importable paths.
19
+ *
20
+ * Loads master keys from the external-secret-manager-injected env (one entry per
21
+ * accepted version). Caller must keep the resulting {@link KeyRing} alive for the
22
+ * process lifetime; rotation requires a restart (or an explicit reload utility
23
+ * added later).
24
+ */
25
+ export declare function loadKeyRing(options: KeyRingOptions): KeyRing;
@@ -0,0 +1,93 @@
1
+ import { ConfigurationError, decodeMasterKey } from "./key-derivation";
2
+ const DEFAULT_KEY_PREFIX = "APIFUSE__KEYRING__MASTER_KEY_V";
3
+ const DEFAULT_ACCEPT_LIST_VAR = "APIFUSE__KEYRING__MASTER_KEY_ACCEPT_LIST";
4
+ const DEFAULT_WRITER_VERSION_VAR = "APIFUSE__KEYRING__MASTER_KEY_WRITER_VERSION";
5
+ function parseAcceptList(raw) {
6
+ if (!raw || raw.trim().length === 0) {
7
+ throw new ConfigurationError(`accept-list env var is empty; expected comma-separated master key versions`);
8
+ }
9
+ const parts = raw
10
+ .split(",")
11
+ .map((s) => s.trim())
12
+ .filter((s) => s.length > 0);
13
+ const versions = [];
14
+ for (const part of parts) {
15
+ const v = Number.parseInt(part, 10);
16
+ if (!Number.isInteger(v) || v <= 0 || String(v) !== part) {
17
+ throw new ConfigurationError(`accept-list contains invalid version "${part}"; expected positive integers`);
18
+ }
19
+ versions.push(v);
20
+ }
21
+ if (versions.length === 0) {
22
+ throw new ConfigurationError("accept-list is empty after parsing");
23
+ }
24
+ return versions;
25
+ }
26
+ function parseWriterVersion(raw, acceptList) {
27
+ if (!raw || raw.trim().length === 0) {
28
+ throw new ConfigurationError("writer-version env var is empty");
29
+ }
30
+ const trimmed = raw.trim();
31
+ const v = Number.parseInt(trimmed, 10);
32
+ if (!Number.isInteger(v) || v <= 0 || String(v) !== trimmed) {
33
+ throw new ConfigurationError(`writer-version "${trimmed}" is not a positive integer`);
34
+ }
35
+ if (!acceptList.includes(v)) {
36
+ throw new ConfigurationError(`writer-version ${v} is not in the accept-list [${acceptList.join(", ")}]`);
37
+ }
38
+ return v;
39
+ }
40
+ /**
41
+ * @internal Trusted loaders only; not re-exported to provider-importable paths.
42
+ *
43
+ * Loads master keys from the external-secret-manager-injected env (one entry per
44
+ * accepted version). Caller must keep the resulting {@link KeyRing} alive for the
45
+ * process lifetime; rotation requires a restart (or an explicit reload utility
46
+ * added later).
47
+ */
48
+ export function loadKeyRing(options) {
49
+ const env = options.env;
50
+ const prefix = options.envPrefix ?? DEFAULT_KEY_PREFIX;
51
+ const acceptListVar = options.acceptListVar ?? DEFAULT_ACCEPT_LIST_VAR;
52
+ const writerVersionVar = options.writerVersionVar ?? DEFAULT_WRITER_VERSION_VAR;
53
+ const acceptList = parseAcceptList(env[acceptListVar]);
54
+ const writerVersion = parseWriterVersion(env[writerVersionVar], acceptList);
55
+ const entries = new Map();
56
+ for (const version of acceptList) {
57
+ const raw = env[`${prefix}${version}`];
58
+ if (raw === undefined) {
59
+ throw new ConfigurationError(`${prefix}${version} is missing (listed in accept-list)`);
60
+ }
61
+ const key = decodeMasterKey(raw);
62
+ entries.set(version, { version, key });
63
+ }
64
+ return {
65
+ accept(version) {
66
+ const entry = entries.get(version);
67
+ if (!entry) {
68
+ throw new ConfigurationError(`version ${version} is not accepted; accept-list is [${acceptList.join(", ")}]`);
69
+ }
70
+ return entry;
71
+ },
72
+ activeWriter() {
73
+ const entry = entries.get(writerVersion);
74
+ if (!entry) {
75
+ throw new ConfigurationError(`writer version ${writerVersion} is missing from accept-list entries`);
76
+ }
77
+ return entry;
78
+ },
79
+ versions() {
80
+ return [...acceptList];
81
+ },
82
+ async purgeVersion(version, isActiveInStore) {
83
+ if (!entries.has(version)) {
84
+ return;
85
+ }
86
+ const active = await isActiveInStore(version);
87
+ if (active) {
88
+ throw new ConfigurationError(`cannot purge master-key version ${version}: active connection rows still reference it`);
89
+ }
90
+ entries.delete(version);
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @internal Trusted loaders only; not re-exported to provider-importable paths.
3
+ *
4
+ * Returns `provider:{HMAC16(contextNamespaceSubkey, providerId)}:{sessionId}`
5
+ * per the `context-namespace` HKDF purpose. Knowing `providerId` alone is
6
+ * insufficient to reconstruct the namespace — the HMAC requires the derived
7
+ * subkey, which is never exposed outside trusted code.
8
+ */
9
+ export declare function deriveContextNamespace(masterSecret: Buffer, providerId: string, sessionId: string, keyVersion: number): string;
@@ -0,0 +1,19 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { deriveSubkey } from "./key-derivation";
3
+ const HMAC_HEX_LENGTH = 16;
4
+ /**
5
+ * @internal Trusted loaders only; not re-exported to provider-importable paths.
6
+ *
7
+ * Returns `provider:{HMAC16(contextNamespaceSubkey, providerId)}:{sessionId}`
8
+ * per the `context-namespace` HKDF purpose. Knowing `providerId` alone is
9
+ * insufficient to reconstruct the namespace — the HMAC requires the derived
10
+ * subkey, which is never exposed outside trusted code.
11
+ */
12
+ export function deriveContextNamespace(masterSecret, providerId, sessionId, keyVersion) {
13
+ if (sessionId.length === 0) {
14
+ throw new Error("sessionId is empty");
15
+ }
16
+ const subkey = deriveSubkey(masterSecret, providerId, "context-namespace", keyVersion);
17
+ const hmac = createHmac("sha256", subkey).update(providerId).digest("hex");
18
+ return `provider:${hmac.slice(0, HMAC_HEX_LENGTH)}:${sessionId}`;
19
+ }
@@ -0,0 +1,39 @@
1
+ import type { TraceSpan } from "../types";
2
+ export interface OTLPExportOptions {
3
+ endpoint: string;
4
+ headers?: Record<string, string>;
5
+ timeout?: number;
6
+ }
7
+ export declare function spansToOTLP(spans: TraceSpan[], resourceAttributes?: Record<string, string>): {
8
+ resourceSpans: Array<{
9
+ resource: {
10
+ attributes: Array<{
11
+ key: string;
12
+ value: Record<string, string>;
13
+ }>;
14
+ };
15
+ scopeSpans: Array<{
16
+ scope: {
17
+ name: string;
18
+ version: string;
19
+ };
20
+ spans: Array<{
21
+ attributes: Array<{
22
+ key: string;
23
+ value: Record<string, string | number | boolean>;
24
+ }>;
25
+ endTimeUnixNano: string;
26
+ kind: number;
27
+ name: string;
28
+ parentSpanId?: string;
29
+ spanId: string;
30
+ startTimeUnixNano: string;
31
+ status: {
32
+ code: number;
33
+ };
34
+ traceId: string;
35
+ }>;
36
+ }>;
37
+ }>;
38
+ };
39
+ export declare function exportSpansOTLP(spans: TraceSpan[], options: OTLPExportOptions, resourceAttributes?: Record<string, string>): Promise<void>;
@@ -0,0 +1,103 @@
1
+ let nextTraceId = 1n;
2
+ let replayableTraceId = null;
3
+ function createBatchSignature(spans, resourceAttributes) {
4
+ return JSON.stringify({
5
+ resourceAttributes: resourceAttributes ?? null,
6
+ spans,
7
+ });
8
+ }
9
+ function createTraceId(signature) {
10
+ if (replayableTraceId?.signature === signature) {
11
+ const traceId = replayableTraceId.traceId;
12
+ replayableTraceId = null;
13
+ return traceId;
14
+ }
15
+ const traceId = nextTraceId.toString(16).padStart(32, "0");
16
+ nextTraceId += 1n;
17
+ replayableTraceId = { signature, traceId };
18
+ return traceId;
19
+ }
20
+ function normalizeHexId(value, length) {
21
+ if (!value) {
22
+ return undefined;
23
+ }
24
+ const normalized = value.replace(/[^a-fA-F0-9]/g, "").toLowerCase();
25
+ return normalized.padStart(length, "0").slice(-length);
26
+ }
27
+ function toAttributeValue(value) {
28
+ if (typeof value === "string") {
29
+ return { stringValue: value };
30
+ }
31
+ if (typeof value === "number") {
32
+ return { doubleValue: value };
33
+ }
34
+ if (typeof value === "boolean") {
35
+ return { boolValue: value };
36
+ }
37
+ return { stringValue: String(value) };
38
+ }
39
+ export function spansToOTLP(spans, resourceAttributes) {
40
+ const traceId = createTraceId(createBatchSignature(spans, resourceAttributes));
41
+ return {
42
+ resourceSpans: [
43
+ {
44
+ resource: {
45
+ attributes: Object.entries(resourceAttributes ?? {}).map(([key, value]) => ({
46
+ key,
47
+ value: { stringValue: value },
48
+ })),
49
+ },
50
+ scopeSpans: [
51
+ {
52
+ scope: {
53
+ name: "apifuse-provider-sdk",
54
+ version: "0.1.0",
55
+ },
56
+ spans: spans.map((span) => ({
57
+ traceId,
58
+ spanId: normalizeHexId(span.id, 16) ?? "0000000000000001",
59
+ parentSpanId: normalizeHexId(span.parentId, 16),
60
+ name: span.name,
61
+ kind: 2,
62
+ startTimeUnixNano: String(span.startedAt * 1_000_000),
63
+ endTimeUnixNano: String(span.endedAt * 1_000_000),
64
+ status: { code: span.status === "ok" ? 1 : 2 },
65
+ attributes: Object.entries(span.attributes ?? {}).map(([key, value]) => ({
66
+ key,
67
+ value: toAttributeValue(value),
68
+ })),
69
+ })),
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ export async function exportSpansOTLP(spans, options, resourceAttributes) {
77
+ if (spans.length === 0) {
78
+ return;
79
+ }
80
+ const controller = new AbortController();
81
+ const timer = setTimeout(() => controller.abort(), options.timeout ?? 5_000);
82
+ try {
83
+ const response = await fetch(options.endpoint, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ ...options.headers,
88
+ },
89
+ body: JSON.stringify(spansToOTLP(spans, resourceAttributes)),
90
+ signal: controller.signal,
91
+ });
92
+ if (!response.ok) {
93
+ throw new Error(`HTTP ${response.status}`);
94
+ }
95
+ }
96
+ catch (error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ console.warn("[apifuse] OTLP export failed:", message);
99
+ }
100
+ finally {
101
+ clearTimeout(timer);
102
+ }
103
+ }
@@ -0,0 +1,12 @@
1
+ import type { Span } from "./trace";
2
+ export type PerfStats = {
3
+ p50: number;
4
+ p95: number;
5
+ p99: number;
6
+ avg: number;
7
+ min: number;
8
+ max: number;
9
+ };
10
+ export declare function computePercentile(sortedValues: number[], p: number): number;
11
+ export declare function computeStats(durations: number[]): PerfStats;
12
+ export declare function groupSpansByName(allSpans: Span[][]): Map<string, number[]>;
@@ -0,0 +1,52 @@
1
+ export function computePercentile(sortedValues, p) {
2
+ if (sortedValues.length === 0) {
3
+ return 0;
4
+ }
5
+ if (sortedValues.length === 1) {
6
+ return sortedValues[0] ?? 0;
7
+ }
8
+ const percentile = Math.min(100, Math.max(0, p));
9
+ const position = (percentile / 100) * (sortedValues.length - 1);
10
+ const lowerIndex = Math.floor(position);
11
+ const upperIndex = Math.ceil(position);
12
+ const lower = sortedValues[lowerIndex] ?? 0;
13
+ const upper = sortedValues[upperIndex] ?? lower;
14
+ if (lowerIndex === upperIndex) {
15
+ return lower;
16
+ }
17
+ const weight = position - lowerIndex;
18
+ return lower + (upper - lower) * weight;
19
+ }
20
+ export function computeStats(durations) {
21
+ if (durations.length === 0) {
22
+ return {
23
+ p50: 0,
24
+ p95: 0,
25
+ p99: 0,
26
+ avg: 0,
27
+ min: 0,
28
+ max: 0,
29
+ };
30
+ }
31
+ const sorted = [...durations].sort((a, b) => a - b);
32
+ const total = sorted.reduce((sum, value) => sum + value, 0);
33
+ return {
34
+ p50: computePercentile(sorted, 50),
35
+ p95: computePercentile(sorted, 95),
36
+ p99: computePercentile(sorted, 99),
37
+ avg: total / sorted.length,
38
+ min: sorted[0] ?? 0,
39
+ max: sorted[sorted.length - 1] ?? 0,
40
+ };
41
+ }
42
+ export function groupSpansByName(allSpans) {
43
+ const grouped = new Map();
44
+ for (const spans of allSpans) {
45
+ for (const span of spans) {
46
+ const durations = grouped.get(span.name) ?? [];
47
+ durations.push(span.duration_ms);
48
+ grouped.set(span.name, durations);
49
+ }
50
+ }
51
+ return grouped;
52
+ }
@@ -0,0 +1,12 @@
1
+ export interface PrevalidateResult {
2
+ valid: boolean;
3
+ errors?: Array<{
4
+ path: string;
5
+ message: string;
6
+ }>;
7
+ }
8
+ type JsonSchema = Record<string, unknown>;
9
+ export declare function prevalidate(schema: JsonSchema, data: unknown, options?: {
10
+ timeoutMs?: number;
11
+ }): PrevalidateResult;
12
+ export {};
@@ -0,0 +1,173 @@
1
+ import Ajv from "ajv";
2
+ import { RE2 } from "re2-wasm";
3
+ const DEFAULT_TIMEOUT_MS = 500;
4
+ function now() {
5
+ return Date.now();
6
+ }
7
+ function cloneWithoutPatterns(value) {
8
+ if (Array.isArray(value)) {
9
+ return value.map((entry) => cloneWithoutPatterns(entry));
10
+ }
11
+ if (!value || typeof value !== "object") {
12
+ return value;
13
+ }
14
+ const cloned = {};
15
+ for (const [key, entry] of Object.entries(value)) {
16
+ if (key === "pattern") {
17
+ continue;
18
+ }
19
+ cloned[key] = cloneWithoutPatterns(entry);
20
+ }
21
+ return cloned;
22
+ }
23
+ function buildAjv() {
24
+ return new Ajv({ allErrors: true, strict: true, strictSchema: true });
25
+ }
26
+ function createTimeoutGuard(timeoutMs) {
27
+ const startedAt = now();
28
+ return () => {
29
+ if (now() - startedAt > timeoutMs) {
30
+ throw new Error("prevalidation_timeout");
31
+ }
32
+ };
33
+ }
34
+ function formatInstancePath(path) {
35
+ return path.length > 0 ? path : "$";
36
+ }
37
+ function appendPath(basePath, segment) {
38
+ if (segment.startsWith("[")) {
39
+ return `${basePath}${segment}`;
40
+ }
41
+ return basePath === "$" ? `${basePath}.${segment}` : `${basePath}.${segment}`;
42
+ }
43
+ function isRecord(value) {
44
+ return !!value && typeof value === "object" && !Array.isArray(value);
45
+ }
46
+ function collectPatternErrors(schema, data, path, guard, errors) {
47
+ guard();
48
+ if (!isRecord(schema)) {
49
+ return;
50
+ }
51
+ if (typeof schema.pattern === "string" && typeof data === "string") {
52
+ try {
53
+ const regex = new RE2(schema.pattern, "u");
54
+ if (!regex.test(data)) {
55
+ errors.push({
56
+ path,
57
+ message: `must match pattern ${schema.pattern}`,
58
+ });
59
+ }
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : "Invalid RE2 pattern";
63
+ errors.push({ path, message });
64
+ }
65
+ }
66
+ if (schema.$ref !== undefined) {
67
+ return;
68
+ }
69
+ if (Array.isArray(schema.allOf)) {
70
+ for (const entry of schema.allOf) {
71
+ collectPatternErrors(entry, data, path, guard, errors);
72
+ }
73
+ }
74
+ if (Array.isArray(schema.anyOf)) {
75
+ for (const entry of schema.anyOf) {
76
+ collectPatternErrors(entry, data, path, guard, errors);
77
+ }
78
+ }
79
+ if (Array.isArray(schema.oneOf)) {
80
+ for (const entry of schema.oneOf) {
81
+ collectPatternErrors(entry, data, path, guard, errors);
82
+ }
83
+ }
84
+ if (isRecord(schema.not)) {
85
+ collectPatternErrors(schema.not, data, path, guard, errors);
86
+ }
87
+ if (isRecord(schema.if)) {
88
+ collectPatternErrors(schema.if, data, path, guard, errors);
89
+ }
90
+ if (isRecord(schema.then)) {
91
+ collectPatternErrors(schema.then, data, path, guard, errors);
92
+ }
93
+ if (isRecord(schema.else)) {
94
+ collectPatternErrors(schema.else, data, path, guard, errors);
95
+ }
96
+ if (Array.isArray(data) && schema.items !== undefined) {
97
+ for (const [index, item] of data.entries()) {
98
+ collectPatternErrors(schema.items, item, appendPath(path, `[${index}]`), guard, errors);
99
+ }
100
+ }
101
+ if (!isRecord(data)) {
102
+ return;
103
+ }
104
+ if (isRecord(schema.properties)) {
105
+ for (const [key, childSchema] of Object.entries(schema.properties)) {
106
+ if (key in data) {
107
+ collectPatternErrors(childSchema, data[key], appendPath(path, key), guard, errors);
108
+ }
109
+ }
110
+ }
111
+ if (isRecord(schema.patternProperties)) {
112
+ for (const [pattern, childSchema] of Object.entries(schema.patternProperties)) {
113
+ const keyPattern = new RE2(pattern, "u");
114
+ for (const [key, value] of Object.entries(data)) {
115
+ guard();
116
+ if (keyPattern.test(key)) {
117
+ collectPatternErrors(childSchema, value, appendPath(path, key), guard, errors);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ if (schema.additionalProperties && isRecord(schema.additionalProperties)) {
123
+ const declaredKeys = isRecord(schema.properties)
124
+ ? new Set(Object.keys(schema.properties))
125
+ : new Set();
126
+ for (const [key, value] of Object.entries(data)) {
127
+ if (!declaredKeys.has(key)) {
128
+ collectPatternErrors(schema.additionalProperties, value, appendPath(path, key), guard, errors);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ export function prevalidate(schema, data, options = {}) {
134
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
135
+ const guard = createTimeoutGuard(timeoutMs);
136
+ try {
137
+ guard();
138
+ const ajv = buildAjv();
139
+ const strippedSchema = cloneWithoutPatterns(schema);
140
+ if (!strippedSchema || typeof strippedSchema !== "object") {
141
+ return {
142
+ valid: false,
143
+ errors: [{ path: "$", message: "Invalid schema" }],
144
+ };
145
+ }
146
+ const validate = ajv.compile(strippedSchema);
147
+ const schemaValid = validate(data);
148
+ const errors = validate.errors?.map((error) => ({
149
+ path: formatInstancePath(error.instancePath),
150
+ message: error.message ?? "Invalid value",
151
+ })) ?? [];
152
+ collectPatternErrors(schema, data, "$", guard, errors);
153
+ if (!schemaValid || errors.length > 0) {
154
+ return { valid: false, errors };
155
+ }
156
+ return { valid: true };
157
+ }
158
+ catch (error) {
159
+ if (error instanceof Error && error.message === "prevalidation_timeout") {
160
+ return {
161
+ valid: false,
162
+ errors: [
163
+ {
164
+ path: "$",
165
+ message: `Prevalidation timed out after ${timeoutMs}ms`,
166
+ },
167
+ ],
168
+ };
169
+ }
170
+ const message = error instanceof Error ? error.message : "Prevalidation failed";
171
+ return { valid: false, errors: [{ path: "$", message }] };
172
+ }
173
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProviderDefinition } from "../types";
2
+ export declare function getProviderBaseUrl(provider: ProviderDefinition): string | undefined;
@@ -0,0 +1,11 @@
1
+ export function getProviderBaseUrl(provider) {
2
+ const operations = Object.values(provider.operations);
3
+ for (const operation of operations) {
4
+ const baseUrl = operation.upstream
5
+ ?.baseUrl;
6
+ if (typeof baseUrl === "string" && baseUrl.length > 0) {
7
+ return baseUrl;
8
+ }
9
+ }
10
+ return undefined;
11
+ }
@@ -0,0 +1,21 @@
1
+ import { TransportError } from "../errors";
2
+ export declare const PROXY_AUTH_IP_DENIED_CODE = "PROXY_AUTH_IP_DENIED";
3
+ export declare const PROXY_AUTH_IP_DENIED_MESSAGE = "Proxy source IP is not authorized. Add the runtime egress IP to the proxy provider allowlist.";
4
+ export declare const PROXY_EDGE_AUTH_REJECTED_CODE = "PROXY_EDGE_AUTH_REJECTED";
5
+ export declare const PROXY_EDGE_AUTH_REJECTED_MESSAGE = "Proxy provider rejected a candidate endpoint during authentication. The SDK will retry or refresh the proxy pool when safe.";
6
+ export declare const PROXY_POOL_STALE_CODE = "PROXY_POOL_STALE";
7
+ export declare const PROXY_EDGE_TLS_REJECTED_CODE = "PROXY_EDGE_TLS_REJECTED";
8
+ export declare const PROXY_POOL_EXHAUSTED_CODE = "PROXY_POOL_EXHAUSTED";
9
+ export declare const PROXY_POOL_EXHAUSTED_MESSAGE = "Proxy provider pool was exhausted. The SDK refreshed the proxy allocation, but all candidate endpoints failed.";
10
+ export declare function isProxyAuthIpDeniedMessage(message: string): boolean;
11
+ export declare function createProxyAuthIpDeniedError(cause?: Error): TransportError;
12
+ export declare function isProxyEdgeAuthRejectedMessage(message: string): boolean;
13
+ export declare function createProxyEdgeAuthRejectedError(cause?: Error): TransportError;
14
+ export declare function isProxyPoolStaleStatus(status: number): boolean;
15
+ export declare function isProxyEdgeTlsRejectedResponse(status: number, evidence: string): boolean;
16
+ export declare function isProxyPoolStaleMessage(message: string): boolean;
17
+ export declare function isProxyPoolRefreshableError(error: unknown): boolean;
18
+ export declare const isProxyPoolStaleError: typeof isProxyPoolRefreshableError;
19
+ export declare function createProxyPoolStaleError(status: number, cause?: Error): TransportError;
20
+ export declare function createProxyEdgeTlsRejectedError(status: number, cause?: Error): TransportError;
21
+ export declare function createProxyPoolExhaustedError(cause?: Error): TransportError;