@stellar-agent-services/provider-kit 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +508 -2
- package/dist/approve/assets/index-BazIJAI2.css +1 -0
- package/dist/approve/assets/index-CL28cSJy.js +420 -0
- package/dist/approve/favicon.svg +1 -0
- package/dist/approve/index.html +18 -0
- package/dist/cli/approval-view.d.ts +80 -0
- package/dist/cli/approval-view.d.ts.map +1 -0
- package/dist/cli/approval-view.js +5 -0
- package/dist/cli/approval-view.js.map +1 -0
- package/dist/cli/approval.d.ts +132 -0
- package/dist/cli/approval.d.ts.map +1 -0
- package/dist/cli/approval.js +542 -0
- package/dist/cli/approval.js.map +1 -0
- package/dist/cli/check.d.ts +3 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +137 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/dev-account.d.ts +28 -0
- package/dist/cli/dev-account.d.ts.map +1 -0
- package/dist/cli/dev-account.js +62 -0
- package/dist/cli/dev-account.js.map +1 -0
- package/dist/cli/dev.d.ts +27 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +226 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/discovery.d.ts +20 -0
- package/dist/cli/discovery.d.ts.map +1 -0
- package/dist/cli/discovery.js +202 -0
- package/dist/cli/discovery.js.map +1 -0
- package/dist/cli/env-file.d.ts +16 -0
- package/dist/cli/env-file.d.ts.map +1 -0
- package/dist/cli/env-file.js +77 -0
- package/dist/cli/env-file.js.map +1 -0
- package/dist/cli/fly.d.ts +28 -0
- package/dist/cli/fly.d.ts.map +1 -0
- package/dist/cli/fly.js +92 -0
- package/dist/cli/fly.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +11 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +170 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/launch.d.ts +21 -0
- package/dist/cli/launch.d.ts.map +1 -0
- package/dist/cli/launch.js +627 -0
- package/dist/cli/launch.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +158 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/owner-chain.d.ts +88 -0
- package/dist/cli/owner-chain.d.ts.map +1 -0
- package/dist/cli/owner-chain.js +284 -0
- package/dist/cli/owner-chain.js.map +1 -0
- package/dist/cli/owner-steps.d.ts +33 -0
- package/dist/cli/owner-steps.d.ts.map +1 -0
- package/dist/cli/owner-steps.js +121 -0
- package/dist/cli/owner-steps.js.map +1 -0
- package/dist/cli/processes.d.ts +47 -0
- package/dist/cli/processes.d.ts.map +1 -0
- package/dist/cli/processes.js +129 -0
- package/dist/cli/processes.js.map +1 -0
- package/dist/cli/reconcile.d.ts +3 -0
- package/dist/cli/reconcile.d.ts.map +1 -0
- package/dist/cli/reconcile.js +76 -0
- package/dist/cli/reconcile.js.map +1 -0
- package/dist/cli/register.d.ts +9 -0
- package/dist/cli/register.d.ts.map +1 -0
- package/dist/cli/register.js +209 -0
- package/dist/cli/register.js.map +1 -0
- package/dist/cli/request-validation.d.ts +5 -0
- package/dist/cli/request-validation.d.ts.map +1 -0
- package/dist/cli/request-validation.js +144 -0
- package/dist/cli/request-validation.js.map +1 -0
- package/dist/cli/serve.d.ts +25 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +73 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/shared.d.ts +142 -0
- package/dist/cli/shared.d.ts.map +1 -0
- package/dist/cli/shared.js +110 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli/status.d.ts +32 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +248 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/target.d.ts +14 -0
- package/dist/cli/target.d.ts.map +1 -0
- package/dist/cli/target.js +43 -0
- package/dist/cli/target.js.map +1 -0
- package/dist/cli/trustline.d.ts +3 -0
- package/dist/cli/trustline.d.ts.map +1 -0
- package/dist/cli/trustline.js +114 -0
- package/dist/cli/trustline.js.map +1 -0
- package/dist/cli/try.d.ts +54 -0
- package/dist/cli/try.d.ts.map +1 -0
- package/dist/cli/try.js +450 -0
- package/dist/cli/try.js.map +1 -0
- package/dist/cli/tunnel.d.ts +13 -0
- package/dist/cli/tunnel.d.ts.map +1 -0
- package/dist/cli/tunnel.js +43 -0
- package/dist/cli/tunnel.js.map +1 -0
- package/dist/cli/validators.d.ts +6 -0
- package/dist/cli/validators.d.ts.map +1 -0
- package/dist/cli/validators.js +14 -0
- package/dist/cli/validators.js.map +1 -0
- package/dist/define-service.d.ts +71 -0
- package/dist/define-service.d.ts.map +1 -0
- package/dist/define-service.js +86 -0
- package/dist/define-service.js.map +1 -0
- package/dist/gateway/config.d.ts +88 -0
- package/dist/gateway/config.d.ts.map +1 -0
- package/dist/gateway/config.js +201 -0
- package/dist/gateway/config.js.map +1 -0
- package/dist/gateway/openapi.d.ts +69 -0
- package/dist/gateway/openapi.d.ts.map +1 -0
- package/dist/gateway/openapi.js +677 -0
- package/dist/gateway/openapi.js.map +1 -0
- package/dist/gateway/upstream.d.ts +15 -0
- package/dist/gateway/upstream.d.ts.map +1 -0
- package/dist/gateway/upstream.js +95 -0
- package/dist/gateway/upstream.js.map +1 -0
- package/dist/http/bounded.d.ts +3 -0
- package/dist/http/bounded.d.ts.map +1 -0
- package/dist/http/bounded.js +23 -0
- package/dist/http/bounded.js.map +1 -0
- package/dist/http/node-server.d.ts +15 -0
- package/dist/http/node-server.d.ts.map +1 -0
- package/dist/http/node-server.js +120 -0
- package/dist/http/node-server.js.map +1 -0
- package/dist/http/rate-limit.d.ts +11 -0
- package/dist/http/rate-limit.d.ts.map +1 -0
- package/dist/http/rate-limit.js +35 -0
- package/dist/http/rate-limit.js.map +1 -0
- package/dist/http/router.d.ts +29 -0
- package/dist/http/router.d.ts.map +1 -0
- package/dist/http/router.js +329 -0
- package/dist/http/router.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/memory-store.d.ts +3 -0
- package/dist/jobs/memory-store.d.ts.map +1 -0
- package/dist/jobs/memory-store.js +121 -0
- package/dist/jobs/memory-store.js.map +1 -0
- package/dist/jobs/reconcile.d.ts +22 -0
- package/dist/jobs/reconcile.d.ts.map +1 -0
- package/dist/jobs/reconcile.js +82 -0
- package/dist/jobs/reconcile.js.map +1 -0
- package/dist/jobs/recovery.d.ts +19 -0
- package/dist/jobs/recovery.d.ts.map +1 -0
- package/dist/jobs/recovery.js +65 -0
- package/dist/jobs/recovery.js.map +1 -0
- package/dist/jobs/runner.d.ts +38 -0
- package/dist/jobs/runner.d.ts.map +1 -0
- package/dist/jobs/runner.js +194 -0
- package/dist/jobs/runner.js.map +1 -0
- package/dist/jobs/scheduler.d.ts +66 -0
- package/dist/jobs/scheduler.d.ts.map +1 -0
- package/dist/jobs/scheduler.js +197 -0
- package/dist/jobs/scheduler.js.map +1 -0
- package/dist/jobs/sqlite-store.d.ts +8 -0
- package/dist/jobs/sqlite-store.d.ts.map +1 -0
- package/dist/jobs/sqlite-store.js +231 -0
- package/dist/jobs/sqlite-store.js.map +1 -0
- package/dist/jobs/store.d.ts +118 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +36 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +99 -0
- package/dist/manifest.js.map +1 -0
- package/dist/payments/chain.d.ts +23 -0
- package/dist/payments/chain.d.ts.map +1 -0
- package/dist/payments/chain.js +108 -0
- package/dist/payments/chain.js.map +1 -0
- package/dist/payments/facilitator.d.ts +44 -0
- package/dist/payments/facilitator.d.ts.map +1 -0
- package/dist/payments/facilitator.js +125 -0
- package/dist/payments/facilitator.js.map +1 -0
- package/dist/payments/onchain.d.ts +36 -0
- package/dist/payments/onchain.d.ts.map +1 -0
- package/dist/payments/onchain.js +191 -0
- package/dist/payments/onchain.js.map +1 -0
- package/dist/payments/payload-checks.d.ts +22 -0
- package/dist/payments/payload-checks.d.ts.map +1 -0
- package/dist/payments/payload-checks.js +113 -0
- package/dist/payments/payload-checks.js.map +1 -0
- package/dist/payments/settlement.d.ts +22 -0
- package/dist/payments/settlement.d.ts.map +1 -0
- package/dist/payments/settlement.js +65 -0
- package/dist/payments/settlement.js.map +1 -0
- package/dist/payments/x402-server.d.ts +8 -0
- package/dist/payments/x402-server.d.ts.map +1 -0
- package/dist/payments/x402-server.js +74 -0
- package/dist/payments/x402-server.js.map +1 -0
- package/dist/provider.d.ts +63 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +177 -0
- package/dist/provider.js.map +1 -0
- package/dist/registration.d.ts +86 -0
- package/dist/registration.d.ts.map +1 -0
- package/dist/registration.js +162 -0
- package/dist/registration.js.map +1 -0
- package/dist/testing.d.ts +13 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +48 -0
- package/dist/testing.js.map +1 -0
- package/dist/validation-request.d.ts +36 -0
- package/dist/validation-request.d.ts.map +1 -0
- package/dist/validation-request.js +101 -0
- package/dist/validation-request.js.map +1 -0
- package/package.json +55 -4
- package/schemas/gateway-config.schema.json +140 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
// Gateway services from an OpenAPI document, for `provider-kit init --openapi` (docs/provider-setup-guide.md §3a).
|
|
2
|
+
//
|
|
3
|
+
// The gateway POSTs a job's input to one fixed URL as a JSON object, and takes a JSON object back
|
|
4
|
+
// (gateway/upstream.ts). So an operation becomes a service when it is a POST that takes a JSON object or no body,
|
|
5
|
+
// has no path parameters and no required query, header or cookie parameters, and answers 2xx with a JSON object.
|
|
6
|
+
// Every other operation is skipped, with the reason.
|
|
7
|
+
//
|
|
8
|
+
// Schemas become what the standard asks for [S4]–[S6]: JSON Schema 2020-12, with the body's own schema at the
|
|
9
|
+
// root and the components it uses under $defs. OpenAPI's own keywords are rewritten (nullable, example, boolean
|
|
10
|
+
// exclusiveMinimum and exclusiveMaximum), readOnly properties leave inputs and writeOnly ones leave outputs. Each
|
|
11
|
+
// service is then checked as `provider-kit serve` checks it, and skipped with the problem when it fails.
|
|
12
|
+
//
|
|
13
|
+
// The rest are first guesses for the developer to review: the price, the charge policy, names from paths, titles
|
|
14
|
+
// and descriptions from the document, and the API's own credentials as "env:" headers, kept out of the file.
|
|
15
|
+
// OpenAPI 3.0, 3.1 and 3.2, as JSON or YAML.
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { Keypair } from "@stellar/stellar-sdk";
|
|
18
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
19
|
+
import { parse as parseYaml } from "yaml";
|
|
20
|
+
import { defineService } from "../define-service.js";
|
|
21
|
+
import { readBounded } from "../http/bounded.js";
|
|
22
|
+
import { createProvider } from "../provider.js";
|
|
23
|
+
/**
|
|
24
|
+
* What a generated service starts with, as `provider-kit init` writes it. The developer sets the real values. A call
|
|
25
|
+
* expected to take seconds is charged only if it succeeds: on-success suits work under half a minute (spec §3.2).
|
|
26
|
+
*/
|
|
27
|
+
export const FIRST_GUESS = {
|
|
28
|
+
price: "0.01",
|
|
29
|
+
charge: "on-success",
|
|
30
|
+
expectedDurationSeconds: 5,
|
|
31
|
+
capacity: { concurrency: 2, maxQueueWaitSeconds: 60 },
|
|
32
|
+
};
|
|
33
|
+
/** The description a service gets when the document has none, as in `provider-kit init`'s starter config. */
|
|
34
|
+
export const DESCRIPTION_TO_WRITE = "What it does, when to use it, and its limits (input size, languages, what it won't do).";
|
|
35
|
+
/** The provider description init writes when it has none. */
|
|
36
|
+
export const PROVIDER_DESCRIPTION_TO_WRITE = "What you offer, in a sentence or two.";
|
|
37
|
+
/** The most a document read from a URL may be. */
|
|
38
|
+
const MAX_DOCUMENT_BYTES = 10 * 1024 * 1024;
|
|
39
|
+
/** A provider has at most this many services (the manifest schema's services maxItems). */
|
|
40
|
+
const MAX_SERVICES = 25;
|
|
41
|
+
const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace", "query"];
|
|
42
|
+
/** Header parameters OpenAPI describes elsewhere, and ignores as parameters. */
|
|
43
|
+
const IGNORED_HEADERS = new Set(["accept", "content-type", "authorization"]);
|
|
44
|
+
/** OpenAPI's own schema keywords, which JSON Schema 2020-12 doesn't have, and identifiers that would move $ref's base. */
|
|
45
|
+
const OPENAPI_ONLY = new Set(["nullable", "example", "xml", "externalDocs", "discriminator", "$schema", "$id"]);
|
|
46
|
+
/** Keywords whose value is a schema or an array of schemas. */
|
|
47
|
+
const SUBSCHEMAS = new Set([
|
|
48
|
+
"items",
|
|
49
|
+
"prefixItems",
|
|
50
|
+
"additionalProperties",
|
|
51
|
+
"not",
|
|
52
|
+
"if",
|
|
53
|
+
"then",
|
|
54
|
+
"else",
|
|
55
|
+
"contains",
|
|
56
|
+
"propertyNames",
|
|
57
|
+
"unevaluatedItems",
|
|
58
|
+
"unevaluatedProperties",
|
|
59
|
+
"contentSchema",
|
|
60
|
+
"allOf",
|
|
61
|
+
"anyOf",
|
|
62
|
+
"oneOf",
|
|
63
|
+
]);
|
|
64
|
+
/** Keywords whose value maps names to schemas. */
|
|
65
|
+
const SCHEMA_MAPS = new Set(["properties", "patternProperties", "dependentSchemas", "$defs", "definitions"]);
|
|
66
|
+
/** The document can't be read or used at all. */
|
|
67
|
+
export class OpenApiError extends Error {
|
|
68
|
+
name = "OpenApiError";
|
|
69
|
+
}
|
|
70
|
+
/** Why one operation can't be a service. */
|
|
71
|
+
class Skip extends Error {
|
|
72
|
+
}
|
|
73
|
+
/** Reads an OpenAPI 3 document, JSON or YAML, from a file or an http(s) URL. `url` is where it came from, after redirects. */
|
|
74
|
+
export async function readOpenApi(source, fetcher = fetch) {
|
|
75
|
+
if (!/^https?:\/\//i.test(source)) {
|
|
76
|
+
let text;
|
|
77
|
+
try {
|
|
78
|
+
text = await readFile(source, "utf8");
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new OpenApiError(`${source} can't be read: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
return { document: parseDocument(text, source) };
|
|
84
|
+
}
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
response = await fetcher(source, { headers: { Accept: "application/json, application/yaml;q=0.9, */*;q=0.5" }, signal: AbortSignal.timeout(30_000) });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new OpenApiError(`${source} can't be read: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
await response.body?.cancel();
|
|
94
|
+
throw new OpenApiError(`${source} answered ${response.status}`);
|
|
95
|
+
}
|
|
96
|
+
const bytes = await readBounded(response.body, MAX_DOCUMENT_BYTES);
|
|
97
|
+
if (bytes === undefined)
|
|
98
|
+
throw new OpenApiError(`${source} is larger than ${MAX_DOCUMENT_BYTES / 1024 / 1024} MB`);
|
|
99
|
+
return { document: parseDocument(new TextDecoder().decode(bytes), source), url: response.url === "" ? source : response.url };
|
|
100
|
+
}
|
|
101
|
+
function parseDocument(text, source) {
|
|
102
|
+
let document;
|
|
103
|
+
try {
|
|
104
|
+
document = JSON.parse(text);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
try {
|
|
108
|
+
document = parseYaml(text);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new OpenApiError(`${source} is neither JSON nor YAML: ${error.message.split("\n")[0]}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!isObject(document))
|
|
115
|
+
throw new OpenApiError(`${source} isn't an OpenAPI document`);
|
|
116
|
+
const version = document["openapi"];
|
|
117
|
+
if (typeof version !== "string") {
|
|
118
|
+
if (typeof document["swagger"] === "string") {
|
|
119
|
+
throw new OpenApiError(`${source} is Swagger ${document["swagger"]} (OpenAPI 2). Convert it to OpenAPI 3 first, such as in the Swagger Editor: Edit, then Convert to OpenAPI 3.`);
|
|
120
|
+
}
|
|
121
|
+
throw new OpenApiError(`${source} isn't an OpenAPI document: it names no "openapi" version`);
|
|
122
|
+
}
|
|
123
|
+
if (!/^3\.[0-2](\.|$)/.test(version))
|
|
124
|
+
throw new OpenApiError(`${source} is OpenAPI ${version}; 3.0, 3.1 and 3.2 are supported`);
|
|
125
|
+
return document;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The gateway services an OpenAPI document describes. `url` is where the document came from, to resolve a relative
|
|
129
|
+
* server URL against; `upstream` is the API's base URL, which wins over the document's servers.
|
|
130
|
+
*/
|
|
131
|
+
export async function gatewayFromOpenApi(document, options = {}) {
|
|
132
|
+
const info = isObject(document["info"]) ? document["info"] : {};
|
|
133
|
+
const title = line(info["title"], 120) ?? "API";
|
|
134
|
+
const version = line(info["version"], 60);
|
|
135
|
+
const description = prose(info["description"], 500) ?? prose(info["summary"], 500);
|
|
136
|
+
const gateway = {
|
|
137
|
+
api: { title, ...(version === undefined ? {} : { version }), openapi: String(document["openapi"]) },
|
|
138
|
+
provider: { name: title, ...(description === undefined ? {} : { description }) },
|
|
139
|
+
services: [],
|
|
140
|
+
skipped: [],
|
|
141
|
+
credentials: [],
|
|
142
|
+
undescribed: [],
|
|
143
|
+
warnings: [],
|
|
144
|
+
};
|
|
145
|
+
const names = new Set();
|
|
146
|
+
const paths = isObject(document["paths"]) ? document["paths"] : {};
|
|
147
|
+
for (const [path, value] of Object.entries(paths)) {
|
|
148
|
+
if (!path.startsWith("/"))
|
|
149
|
+
continue;
|
|
150
|
+
let item;
|
|
151
|
+
try {
|
|
152
|
+
item = resolved(document, value);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (!(error instanceof Skip))
|
|
156
|
+
throw error;
|
|
157
|
+
gateway.skipped.push({ method: "*", path, reason: error.message });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const method of METHODS) {
|
|
161
|
+
const operation = item[method];
|
|
162
|
+
if (!isObject(operation))
|
|
163
|
+
continue;
|
|
164
|
+
const skip = (reason) => gateway.skipped.push({ method: method.toUpperCase(), path, reason });
|
|
165
|
+
if (method !== "post") {
|
|
166
|
+
skip("the gateway calls the API with POST");
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (operation["deprecated"] === true) {
|
|
170
|
+
skip("it is deprecated");
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Servers may be set per operation and per path, over the document's.
|
|
174
|
+
const servers = operation["servers"] ?? item["servers"] ?? document["servers"];
|
|
175
|
+
let generated;
|
|
176
|
+
try {
|
|
177
|
+
generated = generate(document, path, item, operation, () => upstreamBase(servers, options));
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (!(error instanceof Skip))
|
|
181
|
+
throw error;
|
|
182
|
+
skip(error.message);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const { service } = generated;
|
|
186
|
+
service.name = unique(service.name, names);
|
|
187
|
+
const problem = await problemWith(service);
|
|
188
|
+
if (problem !== undefined) {
|
|
189
|
+
skip(problem);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (gateway.services.length >= MAX_SERVICES) {
|
|
193
|
+
skip(`a provider has at most ${MAX_SERVICES} services; this one can replace another`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
names.add(service.name);
|
|
197
|
+
gateway.services.push(service);
|
|
198
|
+
if (!generated.described)
|
|
199
|
+
gateway.undescribed.push(service.name);
|
|
200
|
+
for (const credential of generated.credentials) {
|
|
201
|
+
if (!gateway.credentials.some((known) => known.name === credential.name))
|
|
202
|
+
gateway.credentials.push(credential);
|
|
203
|
+
}
|
|
204
|
+
if (generated.warning !== undefined)
|
|
205
|
+
gateway.warnings.push(`${service.name}: ${generated.warning}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return gateway;
|
|
209
|
+
}
|
|
210
|
+
/** One POST operation as a gateway service. Throws Skip when the gateway can't call it. */
|
|
211
|
+
function generate(document, path, item, operation, upstream) {
|
|
212
|
+
if (/\{[^}]*\}/.test(path))
|
|
213
|
+
throw new Skip(`it has a path parameter (${/\{[^}]*\}/.exec(path)[0]}); the gateway calls one fixed URL`);
|
|
214
|
+
for (const value of [...list(item["parameters"]), ...list(operation["parameters"])]) {
|
|
215
|
+
const parameter = resolved(document, value);
|
|
216
|
+
const where = String(parameter["in"]);
|
|
217
|
+
const name = String(parameter["name"]);
|
|
218
|
+
if (where === "path")
|
|
219
|
+
throw new Skip(`it has a path parameter (${name}); the gateway calls one fixed URL`);
|
|
220
|
+
if (parameter["required"] === true && !(where === "header" && IGNORED_HEADERS.has(name.toLowerCase()))) {
|
|
221
|
+
throw new Skip(`it needs the ${where} parameter "${name}"; the gateway sends only a JSON body`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
let inputSchema = { type: "object", properties: {}, additionalProperties: false };
|
|
225
|
+
let inputExample = {};
|
|
226
|
+
if (operation["requestBody"] !== undefined) {
|
|
227
|
+
const body = resolved(document, operation["requestBody"]);
|
|
228
|
+
const media = jsonMedia(body["content"], "input");
|
|
229
|
+
if (media === undefined)
|
|
230
|
+
throw new Skip(`it takes ${firstMediaType(body["content"]) ?? "a body of no stated type"}, not JSON`);
|
|
231
|
+
inputSchema = objectRoot(toJsonSchema(document, media["schema"] ?? {}, "input"), "input");
|
|
232
|
+
inputExample = exampleOf(document, media);
|
|
233
|
+
}
|
|
234
|
+
let outputSchema = { type: "object" };
|
|
235
|
+
let outputExample;
|
|
236
|
+
const success = successResponse(operation["responses"]);
|
|
237
|
+
if (success !== undefined) {
|
|
238
|
+
const response = resolved(document, success.response);
|
|
239
|
+
if (!isObject(response["content"]) || Object.keys(response["content"]).length === 0) {
|
|
240
|
+
throw new Skip(`it answers ${success.code} with no content; a service answers with a JSON object`);
|
|
241
|
+
}
|
|
242
|
+
const media = jsonMedia(response["content"], "output");
|
|
243
|
+
if (media === undefined)
|
|
244
|
+
throw new Skip(`it answers with ${firstMediaType(response["content"])}, not JSON`);
|
|
245
|
+
outputSchema = objectRoot(toJsonSchema(document, media["schema"] ?? {}, "output"), "output");
|
|
246
|
+
outputExample = exampleOf(document, media);
|
|
247
|
+
}
|
|
248
|
+
const auth = credentialsFor(document, operation);
|
|
249
|
+
const summary = line(operation["summary"], 120);
|
|
250
|
+
const ownDescription = prose(operation["description"], 1000);
|
|
251
|
+
const tags = tagsFor(operation["tags"]);
|
|
252
|
+
// An example pair only when both halves match their schemas [S9].
|
|
253
|
+
const example = isObject(inputExample) && isObject(outputExample) && matches(inputSchema, inputExample) && matches(outputSchema, outputExample);
|
|
254
|
+
const service = {
|
|
255
|
+
name: nameFor(path, operation["operationId"]),
|
|
256
|
+
...(summary === undefined ? {} : { title: summary }),
|
|
257
|
+
description: ownDescription ?? prose(operation["summary"], 1000) ?? DESCRIPTION_TO_WRITE,
|
|
258
|
+
price: FIRST_GUESS.price,
|
|
259
|
+
charge: FIRST_GUESS.charge,
|
|
260
|
+
expectedDurationSeconds: FIRST_GUESS.expectedDurationSeconds,
|
|
261
|
+
...(tags.length === 0 ? {} : { tags }),
|
|
262
|
+
inputSchema,
|
|
263
|
+
outputSchema,
|
|
264
|
+
...(example ? { examples: [{ input: inputExample, output: outputExample }] } : {}),
|
|
265
|
+
capacity: { ...FIRST_GUESS.capacity },
|
|
266
|
+
upstream: { url: `${upstream().replace(/\/+$/, "")}${path}`, ...(Object.keys(auth.headers).length === 0 ? {} : { headers: auth.headers }) },
|
|
267
|
+
};
|
|
268
|
+
return { service, described: ownDescription !== undefined, credentials: auth.credentials, ...(auth.warning === undefined ? {} : { warning: auth.warning }) };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A schema from the document as JSON Schema 2020-12: the schema itself at the root, never a reference to it [S4],
|
|
272
|
+
* and every component it uses, however deep, under $defs, so it refers to nothing outside itself [S5].
|
|
273
|
+
*/
|
|
274
|
+
function toJsonSchema(document, schema, direction) {
|
|
275
|
+
const components = isObject(document["components"]) && isObject(document["components"]["schemas"]) ? document["components"]["schemas"] : {};
|
|
276
|
+
const defs = new Map();
|
|
277
|
+
const queue = [];
|
|
278
|
+
const reference = (pointer) => {
|
|
279
|
+
if (!pointer.startsWith("#"))
|
|
280
|
+
throw new Skip(`its schema refers to another document (${pointer}); bundle the document into one file first`);
|
|
281
|
+
const tokens = pointer.startsWith("#/") ? pointer.slice(2).split("/").map(unescapeToken) : [];
|
|
282
|
+
const name = tokens[2];
|
|
283
|
+
if (tokens[0] !== "components" || tokens[1] !== "schemas" || name === undefined)
|
|
284
|
+
throw new Skip(`its schema refers to ${pointer}, outside components/schemas`);
|
|
285
|
+
if (!(name in components))
|
|
286
|
+
throw new Skip(`its schema refers to ${pointer}, which the document doesn't have`);
|
|
287
|
+
let def = defs.get(name);
|
|
288
|
+
if (def === undefined) {
|
|
289
|
+
def = { key: defKey(name, defs) };
|
|
290
|
+
defs.set(name, def);
|
|
291
|
+
queue.push(name);
|
|
292
|
+
}
|
|
293
|
+
return ["#", "$defs", def.key, ...tokens.slice(3)].map((token, index) => (index < 2 ? token : escapeToken(token))).join("/");
|
|
294
|
+
};
|
|
295
|
+
const convert = (value) => {
|
|
296
|
+
if (Array.isArray(value))
|
|
297
|
+
return value.map(convert);
|
|
298
|
+
if (!isObject(value))
|
|
299
|
+
return value;
|
|
300
|
+
const out = {};
|
|
301
|
+
for (const [key, child] of Object.entries(value)) {
|
|
302
|
+
if (key.startsWith("x-") || OPENAPI_ONLY.has(key))
|
|
303
|
+
continue;
|
|
304
|
+
if (key === "$ref" && typeof child === "string")
|
|
305
|
+
out[key] = reference(child);
|
|
306
|
+
else if (SCHEMA_MAPS.has(key) && isObject(child))
|
|
307
|
+
out[key] = Object.fromEntries(Object.entries(child).map(([name, schema]) => [name, convert(schema)]));
|
|
308
|
+
else if (SUBSCHEMAS.has(key))
|
|
309
|
+
out[key] = convert(child);
|
|
310
|
+
// Everything else, such as enum, const, default and examples, is data: copied as it is.
|
|
311
|
+
else
|
|
312
|
+
out[key] = child;
|
|
313
|
+
}
|
|
314
|
+
return rewrite(value, out, direction);
|
|
315
|
+
};
|
|
316
|
+
const root = convert(inlined(document, schema));
|
|
317
|
+
// The schemas `true` and `{}` accept anything; `false` accepts nothing.
|
|
318
|
+
if (root === true)
|
|
319
|
+
return {};
|
|
320
|
+
if (!isObject(root))
|
|
321
|
+
throw new Skip(direction === "input" ? "its body schema accepts nothing" : "its answer schema accepts nothing");
|
|
322
|
+
for (let name = queue.shift(); name !== undefined; name = queue.shift())
|
|
323
|
+
defs.get(name).schema = convert(components[name]);
|
|
324
|
+
if (defs.size > 0) {
|
|
325
|
+
root["$defs"] = { ...(isObject(root["$defs"]) ? root["$defs"] : {}), ...Object.fromEntries([...defs.values()].map((def) => [def.key, def.schema])) };
|
|
326
|
+
}
|
|
327
|
+
return root;
|
|
328
|
+
}
|
|
329
|
+
/** OpenAPI's ways of saying things, said the JSON Schema 2020-12 way. `original` is the schema as the document wrote it. */
|
|
330
|
+
function rewrite(original, out, direction) {
|
|
331
|
+
// OpenAPI 3.0 writes one example as `example`; JSON Schema keeps a list in `examples`.
|
|
332
|
+
if (original["example"] !== undefined && out["examples"] === undefined)
|
|
333
|
+
out["examples"] = [original["example"]];
|
|
334
|
+
if (out["examples"] !== undefined && !Array.isArray(out["examples"]))
|
|
335
|
+
delete out["examples"];
|
|
336
|
+
// OpenAPI 3.0's boolean exclusiveMinimum and exclusiveMaximum qualify minimum and maximum.
|
|
337
|
+
for (const [exclusive, bound] of [
|
|
338
|
+
["exclusiveMinimum", "minimum"],
|
|
339
|
+
["exclusiveMaximum", "maximum"],
|
|
340
|
+
]) {
|
|
341
|
+
if (typeof out[exclusive] !== "boolean")
|
|
342
|
+
continue;
|
|
343
|
+
if (out[exclusive] === true && typeof out[bound] === "number") {
|
|
344
|
+
out[exclusive] = out[bound];
|
|
345
|
+
delete out[bound];
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
delete out[exclusive];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// A client never sends readOnly properties, and never gets writeOnly ones back.
|
|
352
|
+
const properties = out["properties"];
|
|
353
|
+
if (isObject(properties)) {
|
|
354
|
+
const hidden = direction === "input" ? "readOnly" : "writeOnly";
|
|
355
|
+
const dropped = Object.keys(properties).filter((name) => {
|
|
356
|
+
const property = properties[name];
|
|
357
|
+
return isObject(property) && property[hidden] === true;
|
|
358
|
+
});
|
|
359
|
+
for (const name of dropped)
|
|
360
|
+
delete properties[name];
|
|
361
|
+
const required = out["required"];
|
|
362
|
+
if (dropped.length > 0 && Array.isArray(required))
|
|
363
|
+
out["required"] = required.filter((name) => typeof name !== "string" || !dropped.includes(name));
|
|
364
|
+
}
|
|
365
|
+
// OpenAPI 3.0's nullable: null is allowed as well.
|
|
366
|
+
if (original["nullable"] === true) {
|
|
367
|
+
const type = out["type"];
|
|
368
|
+
if (typeof type === "string") {
|
|
369
|
+
out["type"] = [type, "null"];
|
|
370
|
+
const values = out["enum"];
|
|
371
|
+
if (Array.isArray(values) && !values.includes(null))
|
|
372
|
+
out["enum"] = [...values, null];
|
|
373
|
+
}
|
|
374
|
+
else if (type === undefined) {
|
|
375
|
+
return { anyOf: [out, { type: "null" }] };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
/** The schema with "type": "object" first at its root [S4]: the gateway sends and takes JSON objects only. */
|
|
381
|
+
function objectRoot(schema, direction) {
|
|
382
|
+
const { type, ...rest } = schema;
|
|
383
|
+
if (type === "object" || type === undefined || (Array.isArray(type) && type.includes("object")))
|
|
384
|
+
return { type: "object", ...rest };
|
|
385
|
+
const kinds = (Array.isArray(type) ? type : [type]).map((kind) => KINDS[String(kind)] ?? String(kind)).join(" or ");
|
|
386
|
+
throw new Skip(direction === "input" ? `its body is ${kinds}, not a JSON object` : `it answers with ${kinds}, not a JSON object`);
|
|
387
|
+
}
|
|
388
|
+
const KINDS = {
|
|
389
|
+
array: "an array",
|
|
390
|
+
string: "a string",
|
|
391
|
+
number: "a number",
|
|
392
|
+
integer: "an integer",
|
|
393
|
+
boolean: "true or false",
|
|
394
|
+
null: "null",
|
|
395
|
+
};
|
|
396
|
+
/** A reference at the root replaced with what it refers to, keeping the keywords beside it. */
|
|
397
|
+
function inlined(document, schema) {
|
|
398
|
+
const seen = new Set();
|
|
399
|
+
let current = schema;
|
|
400
|
+
while (isObject(current) && typeof current["$ref"] === "string") {
|
|
401
|
+
const pointer = current["$ref"];
|
|
402
|
+
if (seen.has(pointer))
|
|
403
|
+
throw new Skip(`its schema refers to itself and nothing else (${pointer})`);
|
|
404
|
+
seen.add(pointer);
|
|
405
|
+
const { $ref: _, ...beside } = current;
|
|
406
|
+
const target = pointed(document, pointer, "its schema");
|
|
407
|
+
current = isObject(target) ? { ...target, ...beside } : target;
|
|
408
|
+
}
|
|
409
|
+
return current;
|
|
410
|
+
}
|
|
411
|
+
/** What a $ref points to, outside schemas: parameters, request bodies, responses, examples, path items. */
|
|
412
|
+
function resolved(document, value) {
|
|
413
|
+
const seen = new Set();
|
|
414
|
+
let current = value;
|
|
415
|
+
while (isObject(current) && typeof current["$ref"] === "string") {
|
|
416
|
+
const pointer = current["$ref"];
|
|
417
|
+
if (seen.has(pointer))
|
|
418
|
+
throw new Skip(`it refers to itself (${pointer})`);
|
|
419
|
+
seen.add(pointer);
|
|
420
|
+
current = pointed(document, pointer);
|
|
421
|
+
}
|
|
422
|
+
return isObject(current) ? current : {};
|
|
423
|
+
}
|
|
424
|
+
/** What a local $ref points to. `what` names the referrer in the reason when it can't be followed. */
|
|
425
|
+
function pointed(document, pointer, what = "it") {
|
|
426
|
+
if (!pointer.startsWith("#"))
|
|
427
|
+
throw new Skip(`${what} refers to another document (${pointer}); bundle the document into one file first`);
|
|
428
|
+
if (!pointer.startsWith("#/"))
|
|
429
|
+
throw new Skip(`${what} refers to ${pointer}, which isn't a JSON pointer into the document`);
|
|
430
|
+
let current = document;
|
|
431
|
+
for (const token of pointer.slice(2).split("/").map(unescapeToken)) {
|
|
432
|
+
const next = isObject(current) ? current[token] : Array.isArray(current) ? current[Number(token)] : undefined;
|
|
433
|
+
if (next === undefined)
|
|
434
|
+
throw new Skip(`${what} refers to ${pointer}, which the document doesn't have`);
|
|
435
|
+
current = next;
|
|
436
|
+
}
|
|
437
|
+
return current;
|
|
438
|
+
}
|
|
439
|
+
/** The JSON media type of a body or an answer: application/json and +json types; for answers, also any type. */
|
|
440
|
+
function jsonMedia(content, direction) {
|
|
441
|
+
if (!isObject(content))
|
|
442
|
+
return undefined;
|
|
443
|
+
const entries = Object.entries(content).filter((entry) => isObject(entry[1]));
|
|
444
|
+
const json = entries.find(([type]) => /^application\/(?:[\w.+-]+\+)?json\s*(?:;|$)/i.test(type.trim()));
|
|
445
|
+
return (json ?? (direction === "output" ? entries.find(([type]) => type.trim() === "*/*") : undefined))?.[1];
|
|
446
|
+
}
|
|
447
|
+
function firstMediaType(content) {
|
|
448
|
+
return isObject(content) ? Object.keys(content)[0] : undefined;
|
|
449
|
+
}
|
|
450
|
+
/** The answer a service returns: 200, else the first other 2xx. */
|
|
451
|
+
function successResponse(responses) {
|
|
452
|
+
if (!isObject(responses))
|
|
453
|
+
return undefined;
|
|
454
|
+
const code = Object.keys(responses)
|
|
455
|
+
.filter((candidate) => /^2(\d\d|XX)$/i.test(candidate))
|
|
456
|
+
.sort((a, b) => a.localeCompare(b))[0];
|
|
457
|
+
return code === undefined ? undefined : { code, response: responses[code] };
|
|
458
|
+
}
|
|
459
|
+
/** The first example the document gives: the media type's own, else its schema's. */
|
|
460
|
+
function exampleOf(document, media) {
|
|
461
|
+
if (media["example"] !== undefined)
|
|
462
|
+
return media["example"];
|
|
463
|
+
const examples = media["examples"];
|
|
464
|
+
if (isObject(examples)) {
|
|
465
|
+
for (const value of Object.values(examples)) {
|
|
466
|
+
const example = resolved(document, value);
|
|
467
|
+
if (example["value"] !== undefined)
|
|
468
|
+
return example["value"];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const schema = inlined(document, media["schema"] ?? {});
|
|
472
|
+
if (!isObject(schema))
|
|
473
|
+
return undefined;
|
|
474
|
+
const listed = schema["examples"];
|
|
475
|
+
if (Array.isArray(listed) && listed.length > 0)
|
|
476
|
+
return listed[0];
|
|
477
|
+
return schema["example"];
|
|
478
|
+
}
|
|
479
|
+
/** Whether a value matches a schema, for examples [S9]. A schema that doesn't compile matches nothing. */
|
|
480
|
+
function matches(schema, value) {
|
|
481
|
+
try {
|
|
482
|
+
return new Ajv2020({ strict: false, validateFormats: false }).validate(schema, value);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/** The API's own credentials as upstream headers read from the environment: an API key header, bearer or basic. */
|
|
489
|
+
function credentialsFor(document, operation) {
|
|
490
|
+
const none = { headers: {}, credentials: [] };
|
|
491
|
+
const requirements = Array.isArray(operation["security"]) ? operation["security"] : Array.isArray(document["security"]) ? document["security"] : [];
|
|
492
|
+
if (requirements.length === 0)
|
|
493
|
+
return none;
|
|
494
|
+
const components = isObject(document["components"]) && isObject(document["components"]["securitySchemes"]) ? document["components"]["securitySchemes"] : {};
|
|
495
|
+
const unusable = new Set();
|
|
496
|
+
// The requirements are alternatives: the first the gateway can send wins. An empty one asks for nothing.
|
|
497
|
+
for (const requirement of requirements) {
|
|
498
|
+
if (!isObject(requirement))
|
|
499
|
+
continue;
|
|
500
|
+
const names = Object.keys(requirement);
|
|
501
|
+
if (names.length === 0)
|
|
502
|
+
return none;
|
|
503
|
+
const credentials = names.map((name) => {
|
|
504
|
+
const credential = headerFor(resolved(document, components[name]));
|
|
505
|
+
if (credential === undefined)
|
|
506
|
+
unusable.add(name);
|
|
507
|
+
return credential;
|
|
508
|
+
});
|
|
509
|
+
if (credentials.every((credential) => credential !== undefined)) {
|
|
510
|
+
return { headers: Object.fromEntries(credentials.map((credential) => [credential.header, `env:${credential.name}`])), credentials };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return { ...none, warning: `the API asks for credentials (${[...unusable].join(", ")}) that the gateway can't send as a fixed header. Set upstream.headers by hand.` };
|
|
514
|
+
}
|
|
515
|
+
function headerFor(scheme) {
|
|
516
|
+
if (scheme["type"] === "apiKey" && scheme["in"] === "header" && typeof scheme["name"] === "string" && scheme["name"] !== "") {
|
|
517
|
+
const header = scheme["name"];
|
|
518
|
+
return { header, name: `UPSTREAM_${header.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`, holds: `the API's key, sent as ${header}` };
|
|
519
|
+
}
|
|
520
|
+
const kind = scheme["type"] === "http" && typeof scheme["scheme"] === "string" ? scheme["scheme"].toLowerCase() : undefined;
|
|
521
|
+
if (kind === "bearer")
|
|
522
|
+
return { header: "Authorization", name: "UPSTREAM_AUTHORIZATION", holds: `"Bearer " and the API's token` };
|
|
523
|
+
if (kind === "basic")
|
|
524
|
+
return { header: "Authorization", name: "UPSTREAM_AUTHORIZATION", holds: `"Basic " and the API's user:password in base64` };
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The address the gateway calls: --upstream, else the first server, with its variables' defaults. A relative
|
|
529
|
+
* server URL is resolved against where the document came from, and is an error for a document read from a file.
|
|
530
|
+
*/
|
|
531
|
+
function upstreamBase(servers, options) {
|
|
532
|
+
if (options.upstream !== undefined) {
|
|
533
|
+
if (!/^https?:\/\//i.test(options.upstream))
|
|
534
|
+
throw new OpenApiError(`--upstream must be the API's http(s) address, not "${options.upstream}"`);
|
|
535
|
+
return options.upstream;
|
|
536
|
+
}
|
|
537
|
+
const server = list(servers).find((candidate) => isObject(candidate) && typeof candidate["url"] === "string");
|
|
538
|
+
const variables = server !== undefined && isObject(server["variables"]) ? server["variables"] : {};
|
|
539
|
+
const url = String(server?.["url"] ?? "/").replace(/\{([^}]+)\}/g, (match, name) => {
|
|
540
|
+
const variable = variables[name];
|
|
541
|
+
return isObject(variable) && typeof variable["default"] === "string" ? variable["default"] : match;
|
|
542
|
+
});
|
|
543
|
+
if (/^https?:\/\//i.test(url))
|
|
544
|
+
return url;
|
|
545
|
+
if (options.url === undefined) {
|
|
546
|
+
throw new OpenApiError(`the document's server URL is "${url}", relative to where the API runs, so pass the API's address: --upstream <url>, such as http://127.0.0.1:8000`);
|
|
547
|
+
}
|
|
548
|
+
return new URL(url, options.url).toString();
|
|
549
|
+
}
|
|
550
|
+
/** A service name from the path, without api and version segments: /v1/text/summarize is text-summarize. */
|
|
551
|
+
function nameFor(path, operationId) {
|
|
552
|
+
const fromPath = slug(path
|
|
553
|
+
.split("/")
|
|
554
|
+
.filter((segment) => segment !== "" && !/^(api|v\d+(\.\d+)*)$/i.test(segment))
|
|
555
|
+
.join("-"));
|
|
556
|
+
return fromPath || slug(typeof operationId === "string" ? operationId : "") || "service";
|
|
557
|
+
}
|
|
558
|
+
function slug(text) {
|
|
559
|
+
return text
|
|
560
|
+
.toLowerCase()
|
|
561
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
562
|
+
.replace(/-{2,}/g, "-")
|
|
563
|
+
.replace(/^[-_]+/, "")
|
|
564
|
+
.slice(0, 64)
|
|
565
|
+
.replace(/[-_]+$/, "");
|
|
566
|
+
}
|
|
567
|
+
/** `name`, or `name-2`, `name-3`… when it is taken [M7]. */
|
|
568
|
+
function unique(name, taken) {
|
|
569
|
+
if (!taken.has(name))
|
|
570
|
+
return name;
|
|
571
|
+
for (let n = 2;; n++) {
|
|
572
|
+
const candidate = `${name.slice(0, 64 - `-${n}`.length)}-${n}`;
|
|
573
|
+
if (!taken.has(candidate))
|
|
574
|
+
return candidate;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/** The operation's tags as manifest tags: lowercase words joined by hyphens, at most 10. */
|
|
578
|
+
function tagsFor(tags) {
|
|
579
|
+
const out = new Set();
|
|
580
|
+
for (const tag of list(tags)) {
|
|
581
|
+
if (typeof tag !== "string")
|
|
582
|
+
continue;
|
|
583
|
+
const slugged = tag
|
|
584
|
+
.toLowerCase()
|
|
585
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
586
|
+
.replace(/^-+|-+$/g, "")
|
|
587
|
+
.slice(0, 32)
|
|
588
|
+
.replace(/-+$/, "");
|
|
589
|
+
if (slugged !== "")
|
|
590
|
+
out.add(slugged);
|
|
591
|
+
}
|
|
592
|
+
return [...out].slice(0, 10);
|
|
593
|
+
}
|
|
594
|
+
/** A $defs key for a component: its name, made safe for a JSON pointer in a URI, and unique. */
|
|
595
|
+
function defKey(name, defs) {
|
|
596
|
+
const base = name.replace(/[^A-Za-z0-9_.-]+/g, "_") || "Schema";
|
|
597
|
+
const taken = new Set([...defs.values()].map((def) => def.key));
|
|
598
|
+
let key = base;
|
|
599
|
+
for (let n = 2; taken.has(key); n++)
|
|
600
|
+
key = `${base}_${n}`;
|
|
601
|
+
return key;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Checks a generated service as `provider-kit serve` would: its schemas compile, and a manifest with it passes the
|
|
605
|
+
* standard's checks. The problem, in one line, or undefined.
|
|
606
|
+
*/
|
|
607
|
+
async function problemWith(service) {
|
|
608
|
+
try {
|
|
609
|
+
const { upstream: _, inputSchema, outputSchema, ...rest } = service;
|
|
610
|
+
const defined = defineService({ ...rest, inputSchema: inputSchema, outputSchema: outputSchema, handler: () => ({}) });
|
|
611
|
+
// A throwaway provider with just this service, never served or paid.
|
|
612
|
+
checkPayTo ??= Keypair.random().publicKey();
|
|
613
|
+
const provider = createProvider({
|
|
614
|
+
baseUrl: "http://localhost:8787",
|
|
615
|
+
dev: true,
|
|
616
|
+
network: "stellar:testnet",
|
|
617
|
+
payTo: checkPayTo,
|
|
618
|
+
provider: { name: "Check" },
|
|
619
|
+
services: [defined],
|
|
620
|
+
facilitator: "https://x402.org/facilitator",
|
|
621
|
+
rpc: false,
|
|
622
|
+
});
|
|
623
|
+
await provider.close();
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
const message = error.message;
|
|
628
|
+
// A manifest problem names its rule and where it is: "[S6] /services/0/inputSchema: …". The first one is enough.
|
|
629
|
+
const violation = /\n\s+(\[[A-Z]\d+\]) ([^:\n]*): ([^\n]*)/.exec(message);
|
|
630
|
+
if (violation !== null) {
|
|
631
|
+
const where = violation[2].replace(/^\/services\/0\/?/, "");
|
|
632
|
+
return `${violation[1]} ${where === "" ? "" : `${where}: `}${violation[3]}`;
|
|
633
|
+
}
|
|
634
|
+
return message.split("\n")[0].replace(/^service "[^"]*": /, "");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
let checkPayTo;
|
|
638
|
+
/** Text from the document on one line, at most `max` characters. */
|
|
639
|
+
function line(value, max) {
|
|
640
|
+
const text = prose(value, max);
|
|
641
|
+
return text?.replace(/\n+/g, " ");
|
|
642
|
+
}
|
|
643
|
+
/** Prose from the document: lines inside each paragraph joined, no control characters, at most `max` characters. */
|
|
644
|
+
function prose(value, max) {
|
|
645
|
+
if (typeof value !== "string")
|
|
646
|
+
return undefined;
|
|
647
|
+
const text = value
|
|
648
|
+
.replace(/\r\n?/g, "\n")
|
|
649
|
+
.split(/\n[ \t]*\n/)
|
|
650
|
+
.map((paragraph) => paragraph.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim())
|
|
651
|
+
.filter((paragraph) => paragraph !== "")
|
|
652
|
+
.join("\n\n");
|
|
653
|
+
if (text === "")
|
|
654
|
+
return undefined;
|
|
655
|
+
return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
|
|
656
|
+
}
|
|
657
|
+
function list(value) {
|
|
658
|
+
return Array.isArray(value) ? value : [];
|
|
659
|
+
}
|
|
660
|
+
function isObject(value) {
|
|
661
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
662
|
+
}
|
|
663
|
+
/** A JSON pointer token from a URI fragment: percent-decoded, then ~1 and ~0 unescaped. */
|
|
664
|
+
function unescapeToken(token) {
|
|
665
|
+
let decoded = token;
|
|
666
|
+
try {
|
|
667
|
+
decoded = decodeURIComponent(token);
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
// Not percent-encoded after all: used as it is.
|
|
671
|
+
}
|
|
672
|
+
return decoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
673
|
+
}
|
|
674
|
+
function escapeToken(token) {
|
|
675
|
+
return token.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
676
|
+
}
|
|
677
|
+
//# sourceMappingURL=openapi.js.map
|