@sezzlee/openapi 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,212 @@
1
+ import { snakeCase } from "@sezzlee/core";
2
+ import { OperationDropped } from "../diagnostics.js";
3
+ import { childPointer, operationKey, rootPointer, } from "../ir/brand.js";
4
+ import { arrayOf, entriesOf, objectOf, stringOf, } from "../ir/json.js";
5
+ import { resolveComponent } from "../normalize/refs.js";
6
+ import { lowerRequestBody } from "./body.js";
7
+ import { lowerParameters } from "./parameters.js";
8
+ import { lowerResponses } from "./responses.js";
9
+ import { authOf, carriersOf, effectiveSecurity, } from "./security.js";
10
+ import { serverOf } from "./servers.js";
11
+ const methodKeys = {
12
+ get: "GET",
13
+ put: "PUT",
14
+ post: "POST",
15
+ delete: "DELETE",
16
+ options: "OPTIONS",
17
+ head: "HEAD",
18
+ patch: "PATCH",
19
+ trace: "TRACE",
20
+ query: "QUERY",
21
+ };
22
+ const describable = new Set([
23
+ "GET",
24
+ "HEAD",
25
+ "POST",
26
+ "PUT",
27
+ "PATCH",
28
+ "DELETE",
29
+ "OPTIONS",
30
+ "QUERY",
31
+ ]);
32
+ const toolNamePattern = /^[a-z][a-z0-9_]{0,255}$/;
33
+ function descriptionOf(operation) {
34
+ const summary = stringOf(operation["summary"])?.trim();
35
+ const description = stringOf(operation["description"])?.trim();
36
+ if (summary === undefined || summary === "") {
37
+ return description === "" ? undefined : description;
38
+ }
39
+ if (description === undefined ||
40
+ description === "" ||
41
+ description.startsWith(summary)) {
42
+ return description === undefined || description === ""
43
+ ? summary
44
+ : description;
45
+ }
46
+ return `${summary}\n\n${description}`;
47
+ }
48
+ function hoisted(path, prefix) {
49
+ if (prefix === undefined || prefix === "" || prefix === "/") {
50
+ return { route: path, prefix: "" };
51
+ }
52
+ const clean = prefix.replace(/\/+$/, "");
53
+ if (path === clean) {
54
+ return { route: "/", prefix: clean };
55
+ }
56
+ return path.startsWith(`${clean}/`)
57
+ ? { route: path.slice(clean.length), prefix: clean }
58
+ : { route: path, prefix: "" };
59
+ }
60
+ function sitesOf(document, diagnostics) {
61
+ const sites = [];
62
+ for (const [path, raw] of entriesOf(document["paths"])) {
63
+ const itemAt = childPointer(rootPointer, "paths", path);
64
+ let resolved;
65
+ try {
66
+ resolved = resolveComponent(document, raw, itemAt);
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof OperationDropped)) {
70
+ throw error;
71
+ }
72
+ diagnostics.report(error.code, error.at, error.message);
73
+ continue;
74
+ }
75
+ if (resolved === undefined) {
76
+ continue;
77
+ }
78
+ const item = resolved.node;
79
+ for (const [key, method] of Object.entries(methodKeys)) {
80
+ const operation = objectOf(item[key]);
81
+ if (operation !== undefined) {
82
+ sites.push({
83
+ path,
84
+ method,
85
+ operation,
86
+ at: childPointer(itemAt, key),
87
+ item,
88
+ itemAt,
89
+ });
90
+ }
91
+ }
92
+ for (const [name, raw2] of entriesOf(item["additionalOperations"])) {
93
+ const operation = objectOf(raw2);
94
+ if (operation !== undefined) {
95
+ sites.push({
96
+ path,
97
+ method: name.toUpperCase(),
98
+ operation,
99
+ at: childPointer(itemAt, "additionalOperations", name),
100
+ item,
101
+ itemAt,
102
+ });
103
+ }
104
+ }
105
+ }
106
+ return sites;
107
+ }
108
+ function searchTermsOf(context, operation, at) {
109
+ const declared = operation["x-sezzlee-search-terms"];
110
+ if (declared === undefined) {
111
+ return undefined;
112
+ }
113
+ if (!Array.isArray(declared) ||
114
+ !declared.every((term) => typeof term === "string")) {
115
+ context.diagnostics.report("search_terms_invalid", childPointer(at, "x-sezzlee-search-terms"), "x-sezzlee-search-terms must be an array of strings; the operation carries no search terms.");
116
+ return undefined;
117
+ }
118
+ const terms = declared.filter((term) => term.trim() !== "");
119
+ return terms.length === 0 ? undefined : terms;
120
+ }
121
+ function lowerOperation(context, site, schemes, options) {
122
+ const { operation, at, method } = site;
123
+ if (operation["x-sezzlee-dropped"] === true) {
124
+ return undefined;
125
+ }
126
+ if (!describable.has(method)) {
127
+ throw new OperationDropped("unsupported_method", at, `Method ${method} has no descriptor form.`);
128
+ }
129
+ if (objectOf(operation["callbacks"]) !== undefined) {
130
+ context.diagnostics.report("callbacks_ignored", childPointer(at, "callbacks"), "Callbacks are requests the backend makes, not operations the agent calls; they are not carried.");
131
+ }
132
+ const security = effectiveSecurity(operation, context.document);
133
+ const credentialSlots = carriersOf(security, schemes);
134
+ const identityCookies = new Set([...schemes.values()].flatMap((scheme) => scheme.type === "apiKey" && scheme.in === "cookie" ? [scheme.name] : []));
135
+ const lowered = lowerParameters({ ...context, cookieDenyList: options.cookieDenyList }, {
136
+ values: site.item["parameters"],
137
+ at: childPointer(site.itemAt, "parameters"),
138
+ }, { values: operation["parameters"], at: childPointer(at, "parameters") }, credentialSlots, identityCookies);
139
+ const requestBody = lowerRequestBody(context, operation["requestBody"], childPointer(at, "requestBody"), options.requestBodyRequired);
140
+ const responses = lowerResponses(context, operation["responses"], childPointer(at, "responses"), options.outputSchema === "document");
141
+ const operationId = stringOf(operation["operationId"])?.trim();
142
+ let usableId;
143
+ if (operationId !== undefined && operationId !== "") {
144
+ if (toolNamePattern.test(snakeCase(operationId))) {
145
+ usableId = operationId;
146
+ }
147
+ else {
148
+ context.diagnostics.report("operation_id_unusable", childPointer(at, "operationId"), `operationId '${operationId}' does not produce a valid tool name; the name is derived from the route instead.`);
149
+ }
150
+ }
151
+ const tags = arrayOf(operation["tags"])
152
+ .map(stringOf)
153
+ .filter((tag) => tag !== undefined && tag.trim() !== "");
154
+ const searchTerms = searchTermsOf(context, operation, at);
155
+ const { route, prefix } = hoisted(site.path, options.hoistPathPrefix);
156
+ const description = descriptionOf(operation);
157
+ const carriers = [...credentialSlots, ...lowered.carriers];
158
+ const descriptor = {
159
+ ...(usableId === undefined ? {} : { operationId: usableId }),
160
+ ...(tags[0] === undefined ? {} : { container: tags[0] }),
161
+ method: method,
162
+ route,
163
+ ...(description === undefined ? {} : { description }),
164
+ ...(operation["deprecated"] === true ? { deprecated: true } : {}),
165
+ ...(lowered.parameters.length === 0
166
+ ? {}
167
+ : { parameters: lowered.parameters }),
168
+ ...(requestBody === undefined ? {} : { requestBody }),
169
+ ...(responses === undefined ? {} : { responses }),
170
+ auth: authOf(security, carriers),
171
+ ...(tags.length === 0 ? {} : { tags }),
172
+ ...(searchTerms === undefined ? {} : { searchTerms }),
173
+ };
174
+ const server = serverOf([
175
+ { servers: operation["servers"], at: childPointer(at, "servers") },
176
+ {
177
+ servers: site.item["servers"],
178
+ at: childPointer(site.itemAt, "servers"),
179
+ },
180
+ ], {
181
+ servers: context.document["servers"],
182
+ at: childPointer(rootPointer, "servers"),
183
+ }, options, context.diagnostics);
184
+ return {
185
+ key: operationKey(method, site.path),
186
+ at,
187
+ descriptor,
188
+ baseUrl: server === undefined ? undefined : `${server}${prefix}`,
189
+ security,
190
+ };
191
+ }
192
+ export function lowerOperations(context, schemes, options) {
193
+ if (objectOf(context.document["webhooks"]) !== undefined) {
194
+ context.diagnostics.report("webhooks_ignored", childPointer(rootPointer, "webhooks"), "Webhooks are requests the backend makes, not operations the agent calls; they are not carried.");
195
+ }
196
+ const endpoints = [];
197
+ for (const site of sitesOf(context.document, context.diagnostics)) {
198
+ try {
199
+ const endpoint = lowerOperation(context, site, schemes, options);
200
+ if (endpoint !== undefined) {
201
+ endpoints.push(endpoint);
202
+ }
203
+ }
204
+ catch (error) {
205
+ if (!(error instanceof OperationDropped)) {
206
+ throw error;
207
+ }
208
+ context.diagnostics.report(error.code, error.at, error.message);
209
+ }
210
+ }
211
+ return endpoints;
212
+ }
@@ -0,0 +1,30 @@
1
+ import type { EndpointDescriptor, IdentityCarrier } from "@sezzlee/core";
2
+ import { type DiagnosticSink } from "../diagnostics.js";
3
+ import { type JsonPointer } from "../ir/brand.js";
4
+ import { type JsonValue } from "../ir/json.js";
5
+ import { type SchemaContext } from "../normalize/schema.js";
6
+ type Parameter = NonNullable<EndpointDescriptor["parameters"]>[number];
7
+ export declare const defaultCookieDenyList: RegExp;
8
+ export interface ParameterContext extends SchemaContext {
9
+ readonly diagnostics: DiagnosticSink;
10
+ readonly cookieDenyList: RegExp;
11
+ }
12
+ export interface LoweredParameters {
13
+ readonly parameters: Parameter[];
14
+ readonly carriers: IdentityCarrier[];
15
+ }
16
+ /**
17
+ * Lowers the path item's and the operation's parameters. The operation's list wins over the path
18
+ * item's for the same `(name, in)` pair; merging by name alone would collapse a query `id` into a
19
+ * path `id`.
20
+ *
21
+ * @param credentialSlots the slots the operation's security schemes write a credential into
22
+ */
23
+ export declare function lowerParameters(context: ParameterContext, pathItem: {
24
+ readonly values: JsonValue | undefined;
25
+ readonly at: JsonPointer;
26
+ }, operation: {
27
+ readonly values: JsonValue | undefined;
28
+ readonly at: JsonPointer;
29
+ }, credentialSlots: readonly IdentityCarrier[], identityCookies: ReadonlySet<string>): LoweredParameters;
30
+ export {};
@@ -0,0 +1,143 @@
1
+ import { OperationDropped } from "../diagnostics.js";
2
+ import { childPointer } from "../ir/brand.js";
3
+ import { arrayOf, booleanOf, entriesOf, objectOf, stringOf, } from "../ir/json.js";
4
+ import { resolveComponent } from "../normalize/refs.js";
5
+ import { normalizeSlot } from "../normalize/schema.js";
6
+ export const defaultCookieDenyList = /^(session|sid|token|auth|jwt)$|sess/i;
7
+ const reservedHeaders = new Set(["accept", "content-type", "authorization"]);
8
+ const locations = new Set([
9
+ "path",
10
+ "query",
11
+ "header",
12
+ "cookie",
13
+ "querystring",
14
+ ]);
15
+ const defaultStyle = {
16
+ path: "simple",
17
+ query: "form",
18
+ header: "simple",
19
+ cookie: "form",
20
+ };
21
+ const parameterContentTypes = new Set([
22
+ "application/json",
23
+ "text/plain",
24
+ "application/x-www-form-urlencoded",
25
+ ]);
26
+ /**
27
+ * The media type a `content` parameter is serialized as, or `undefined` when the composer writes
28
+ * none; a JSON-family type is written as JSON.
29
+ */
30
+ function contentTypeOf(declared) {
31
+ const bare = (declared.split(";")[0] ?? "").trim().toLowerCase();
32
+ if (parameterContentTypes.has(bare)) {
33
+ return bare;
34
+ }
35
+ return /\+json$/.test(bare) || bare === "text/json"
36
+ ? "application/json"
37
+ : undefined;
38
+ }
39
+ function collect(context, lists) {
40
+ const byKey = new Map();
41
+ for (const list of lists) {
42
+ arrayOf(list.values).forEach((value, index) => {
43
+ const resolved = resolveComponent(context.document, value, childPointer(list.at, index));
44
+ if (resolved === undefined) {
45
+ return;
46
+ }
47
+ const key = `${stringOf(resolved.node["in"])}|${stringOf(resolved.node["name"])}`;
48
+ byKey.set(key, resolved);
49
+ });
50
+ }
51
+ return [...byKey.values()];
52
+ }
53
+ /**
54
+ * Lowers the path item's and the operation's parameters. The operation's list wins over the path
55
+ * item's for the same `(name, in)` pair; merging by name alone would collapse a query `id` into a
56
+ * path `id`.
57
+ *
58
+ * @param credentialSlots the slots the operation's security schemes write a credential into
59
+ */
60
+ export function lowerParameters(context, pathItem, operation, credentialSlots, identityCookies) {
61
+ const parameters = [];
62
+ const carriers = [];
63
+ for (const { node, at } of collect(context, [pathItem, operation])) {
64
+ const name = stringOf(node["name"]);
65
+ const location = stringOf(node["in"]);
66
+ if (name === undefined || location === undefined) {
67
+ context.diagnostics.report("openapi_document_invalid", at, "A parameter needs 'name' and 'in'.");
68
+ continue;
69
+ }
70
+ if (!locations.has(location)) {
71
+ context.diagnostics.report("openapi_document_invalid", childPointer(at, "in"), `Parameter location '${location}' is not defined by OpenAPI; the parameter is ignored.`);
72
+ continue;
73
+ }
74
+ const where = location;
75
+ if (where === "header" && reservedHeaders.has(name.toLowerCase())) {
76
+ context.diagnostics.report("reserved_header_parameter_ignored", at, `Header parameter '${name}' is ignored, as OpenAPI requires; ${name.toLowerCase() === "authorization" ? "the credential arrives through a security scheme" : "the composer writes it"}.`);
77
+ continue;
78
+ }
79
+ const occupies = credentialSlots.find((slot) => slot.in === where &&
80
+ (where === "header"
81
+ ? slot.name.toLowerCase() === name.toLowerCase()
82
+ : slot.name === name));
83
+ if (occupies !== undefined) {
84
+ context.diagnostics.report("credential_parameter_ignored", at, `${where} parameter '${name}' is the slot a security scheme writes its credential into; it is not an argument.`);
85
+ continue;
86
+ }
87
+ if (where === "cookie" &&
88
+ (identityCookies.has(name) || context.cookieDenyList.test(name))) {
89
+ carriers.push({ in: "cookie", name });
90
+ context.diagnostics.report("identity_cookie_parameter", at, `Cookie parameter '${name}' carries identity and is never an argument.`);
91
+ if (!identityCookies.has(name)) {
92
+ context.diagnostics.report("identity_cookie_uncovered", at, `Cookie '${name}' looks like a session cookie but no security scheme declares it; no credential is configured for it.`);
93
+ }
94
+ continue;
95
+ }
96
+ if (node["allowEmptyValue"] !== undefined) {
97
+ context.diagnostics.report("allow_empty_value_ignored", childPointer(at, "allowEmptyValue"), "allowEmptyValue is ignored; an empty string already writes 'key='.");
98
+ }
99
+ const description = stringOf(node["description"]);
100
+ const content = entriesOf(node["content"]);
101
+ if (content.length > 0 || where === "querystring") {
102
+ const [declared, media] = content[0] ?? ["", undefined];
103
+ const contentType = contentTypeOf(declared);
104
+ if (contentType === undefined || content.length !== 1) {
105
+ throw new OperationDropped("unsupported_parameter_content", childPointer(at, "content"), `Parameter '${name}' is serialized as ${declared === "" ? "no media type" : declared}, which the composer does not write; only JSON, text/plain and — for a querystring — urlencoded content are.`);
106
+ }
107
+ parameters.push({
108
+ name,
109
+ in: where,
110
+ required: where === "path" ? true : booleanOf(node["required"]) === true,
111
+ schema: normalizeSlot(context, objectOf(media)?.["schema"], childPointer(at, "content", declared, "schema"), { direction: "request", root: "preferred" }),
112
+ contentType,
113
+ ...(description === undefined ? {} : { description }),
114
+ });
115
+ continue;
116
+ }
117
+ const allowReserved = where === "query" && booleanOf(node["allowReserved"]) === true;
118
+ const schema = normalizeSlot(context, node["schema"], childPointer(at, "schema"), {
119
+ direction: "request",
120
+ root: "required",
121
+ });
122
+ const isArray = schema.type === "array" ||
123
+ (Array.isArray(schema.type) && schema.type.includes("array"));
124
+ const declaredStyle = stringOf(node["style"]);
125
+ const style = declaredStyle ?? defaultStyle[where];
126
+ const declaredExplode = booleanOf(node["explode"]);
127
+ parameters.push({
128
+ name,
129
+ in: where,
130
+ required: where === "path" ? true : booleanOf(node["required"]) === true,
131
+ schema,
132
+ ...(style === "deepObject" || isArray || declaredStyle !== undefined
133
+ ? { style }
134
+ : {}),
135
+ ...(isArray || declaredExplode !== undefined
136
+ ? { explode: declaredExplode ?? style === "form" }
137
+ : {}),
138
+ ...(allowReserved ? { allowReserved: true } : {}),
139
+ ...(description === undefined ? {} : { description }),
140
+ });
141
+ }
142
+ return { parameters, carriers };
143
+ }
@@ -0,0 +1,6 @@
1
+ import { type EndpointDescriptor } from "@sezzlee/core";
2
+ import { type JsonPointer } from "../ir/brand.js";
3
+ import { type SchemaContext } from "../normalize/schema.js";
4
+ type Responses = NonNullable<EndpointDescriptor["responses"]>;
5
+ export declare function lowerResponses(context: SchemaContext, value: unknown, at: JsonPointer, withSchemas: boolean): Responses | undefined;
6
+ export {};
@@ -0,0 +1,63 @@
1
+ import { isJsonMediaType } from "@sezzlee/core";
2
+ import { OperationDropped } from "../diagnostics.js";
3
+ import { childPointer } from "../ir/brand.js";
4
+ import { entriesOf, isObject, objectOf, stringOf, } from "../ir/json.js";
5
+ import { resolveComponent } from "../normalize/refs.js";
6
+ import { normalizeSlot } from "../normalize/schema.js";
7
+ const streamingMediaTypes = new Set([
8
+ "text/event-stream",
9
+ "application/jsonl",
10
+ "application/x-ndjson",
11
+ "application/json-seq",
12
+ "multipart/mixed",
13
+ ]);
14
+ const bare = (declared) => (declared.split(";")[0] ?? "").trim().toLowerCase();
15
+ function jsonMedia(content) {
16
+ for (const [declared, media] of entriesOf(content)) {
17
+ const mediaType = bare(declared);
18
+ if (isObject(media) &&
19
+ media["itemSchema"] === undefined &&
20
+ (isJsonMediaType(mediaType) || mediaType === "*/*")) {
21
+ return { declared, media };
22
+ }
23
+ }
24
+ return undefined;
25
+ }
26
+ function isStreamingOnly(content) {
27
+ const entries = entriesOf(content);
28
+ return (entries.length > 0 &&
29
+ entries.every(([declared, media]) => streamingMediaTypes.has(bare(declared)) ||
30
+ (isObject(media) && media["itemSchema"] !== undefined)));
31
+ }
32
+ export function lowerResponses(context, value, at, withSchemas) {
33
+ const out = {};
34
+ for (const [code, raw] of entriesOf(value)) {
35
+ if (!/^([1-5][0-9]{2}|[1-5]XX|default)$/.test(code)) {
36
+ continue;
37
+ }
38
+ const codeAt = childPointer(at, code);
39
+ const resolved = resolveComponent(context.document, isObject(raw) ? raw : {}, codeAt);
40
+ if (resolved === undefined) {
41
+ continue;
42
+ }
43
+ const { node, at: responseAt } = resolved;
44
+ if (objectOf(node["links"]) !== undefined) {
45
+ context.diagnostics.report("links_ignored", childPointer(responseAt, "links"), "Response links describe follow-up calls the agent finds by search; they are not carried.");
46
+ }
47
+ if (code.startsWith("2") && isStreamingOnly(node["content"])) {
48
+ throw new OperationDropped("streaming_response_unsupported", childPointer(responseAt, "content"), `The ${code} response is only a streaming sequence, which an invocation cannot return as one result.`);
49
+ }
50
+ const description = stringOf(node["description"]);
51
+ const json = withSchemas ? jsonMedia(node["content"]) : undefined;
52
+ const schema = json?.media["schema"] === undefined
53
+ ? undefined
54
+ : normalizeSlot(context, json.media["schema"], childPointer(responseAt, "content", json.declared, "schema"), { direction: "response", root: "preferred" });
55
+ out[code] = {
56
+ ...(schema === undefined ? {} : { schema }),
57
+ ...(description === undefined || description === ""
58
+ ? {}
59
+ : { description }),
60
+ };
61
+ }
62
+ return Object.keys(out).length === 0 ? undefined : out;
63
+ }
@@ -0,0 +1,41 @@
1
+ import type { EndpointDescriptor, IdentityCarrier } from "@sezzlee/core";
2
+ import type { DiagnosticSink } from "../diagnostics.js";
3
+ import { type Brand } from "../ir/brand.js";
4
+ import { type JsonObject } from "../ir/json.js";
5
+ export type CredentialRef = Brand<string, "CredentialRef">;
6
+ export type SecurityScheme = {
7
+ readonly type: "apiKey";
8
+ readonly in: "header" | "query" | "cookie";
9
+ readonly name: string;
10
+ } | {
11
+ readonly type: "http";
12
+ readonly scheme: "basic";
13
+ } | {
14
+ readonly type: "http";
15
+ readonly scheme: "bearer";
16
+ readonly bearerFormat?: string;
17
+ } | {
18
+ readonly type: "oauth2";
19
+ readonly flows: JsonObject;
20
+ } | {
21
+ readonly type: "openIdConnect";
22
+ readonly url: string;
23
+ } | {
24
+ readonly type: "mutualTLS";
25
+ };
26
+ /** One alternative of a `security` list: every scheme in it applies together. */
27
+ export type SecurityRequirement = ReadonlyMap<CredentialRef, readonly string[]>;
28
+ export type SecurityModel = ReadonlyMap<CredentialRef, SecurityScheme>;
29
+ export declare function securitySchemesOf(document: JsonObject, diagnostics: DiagnosticSink): SecurityModel;
30
+ /**
31
+ * The operation's own `security`, else the document's. `undefined` means no list was declared
32
+ * anywhere, which is not the same as an explicit empty list.
33
+ */
34
+ export declare function effectiveSecurity(operation: JsonObject, document: JsonObject): readonly SecurityRequirement[] | undefined;
35
+ export declare function isAnonymousAllowed(requirements: readonly SecurityRequirement[] | undefined): boolean;
36
+ export declare function carriersOf(requirements: readonly SecurityRequirement[] | undefined, schemes: SecurityModel): IdentityCarrier[];
37
+ /**
38
+ * A document says which credential a call needs, never which caller may make it, so nothing but an
39
+ * explicit anonymous alternative is turned into a visibility fact.
40
+ */
41
+ export declare function authOf(requirements: readonly SecurityRequirement[] | undefined, carriers: readonly IdentityCarrier[]): EndpointDescriptor["auth"];
@@ -0,0 +1,113 @@
1
+ import { childPointer, rootPointer } from "../ir/brand.js";
2
+ import { arrayOf, entriesOf, objectOf, stringOf, } from "../ir/json.js";
3
+ const apiKeyLocations = new Set(["header", "query", "cookie"]);
4
+ export function securitySchemesOf(document, diagnostics) {
5
+ const schemes = new Map();
6
+ const components = objectOf(document["components"]);
7
+ for (const [name, raw] of entriesOf(components?.["securitySchemes"])) {
8
+ const at = childPointer(rootPointer, "components", "securitySchemes", name);
9
+ const scheme = objectOf(raw);
10
+ const type = stringOf(scheme?.["type"]);
11
+ const ref = name;
12
+ switch (type) {
13
+ case "apiKey": {
14
+ const location = stringOf(scheme?.["in"]) ?? "";
15
+ const key = stringOf(scheme?.["name"]);
16
+ if (!apiKeyLocations.has(location) || key === undefined) {
17
+ diagnostics.report("openapi_document_invalid", at, "An apiKey scheme needs 'in' (header, query or cookie) and 'name'.");
18
+ continue;
19
+ }
20
+ schemes.set(ref, {
21
+ type: "apiKey",
22
+ in: location,
23
+ name: key,
24
+ });
25
+ continue;
26
+ }
27
+ case "http": {
28
+ const httpScheme = stringOf(scheme?.["scheme"])?.toLowerCase();
29
+ if (httpScheme === "basic") {
30
+ schemes.set(ref, { type: "http", scheme: "basic" });
31
+ }
32
+ else if (httpScheme === "bearer") {
33
+ const bearerFormat = stringOf(scheme?.["bearerFormat"]);
34
+ schemes.set(ref, {
35
+ type: "http",
36
+ scheme: "bearer",
37
+ ...(bearerFormat === undefined ? {} : { bearerFormat }),
38
+ });
39
+ }
40
+ else {
41
+ diagnostics.report("security_scheme_unsupported", at, `The http scheme '${httpScheme ?? ""}' has no credential placement; only basic and bearer do.`);
42
+ }
43
+ continue;
44
+ }
45
+ case "oauth2":
46
+ schemes.set(ref, {
47
+ type: "oauth2",
48
+ flows: objectOf(scheme?.["flows"]) ?? {},
49
+ });
50
+ continue;
51
+ case "openIdConnect":
52
+ schemes.set(ref, {
53
+ type: "openIdConnect",
54
+ url: stringOf(scheme?.["openIdConnectUrl"]) ?? "",
55
+ });
56
+ continue;
57
+ case "mutualTLS":
58
+ schemes.set(ref, { type: "mutualTLS" });
59
+ continue;
60
+ default:
61
+ diagnostics.report("openapi_document_invalid", at, `Security scheme type '${type ?? "absent"}' is not defined by OpenAPI.`);
62
+ }
63
+ }
64
+ return schemes;
65
+ }
66
+ /**
67
+ * The operation's own `security`, else the document's. `undefined` means no list was declared
68
+ * anywhere, which is not the same as an explicit empty list.
69
+ */
70
+ export function effectiveSecurity(operation, document) {
71
+ const declared = operation["security"] ?? document["security"];
72
+ if (declared === undefined) {
73
+ return undefined;
74
+ }
75
+ return arrayOf(declared).map((alternative) => new Map(entriesOf(alternative).map(([name, scopes]) => [
76
+ name,
77
+ arrayOf(scopes)
78
+ .map(stringOf)
79
+ .filter((scope) => scope !== undefined),
80
+ ])));
81
+ }
82
+ export function isAnonymousAllowed(requirements) {
83
+ return (requirements !== undefined &&
84
+ (requirements.length === 0 ||
85
+ requirements.some((alternative) => alternative.size === 0)));
86
+ }
87
+ export function carriersOf(requirements, schemes) {
88
+ const carriers = new Map();
89
+ for (const alternative of requirements ?? []) {
90
+ for (const ref of alternative.keys()) {
91
+ const scheme = schemes.get(ref);
92
+ if (scheme?.type === "apiKey") {
93
+ carriers.set(`${scheme.in}|${scheme.name}`, {
94
+ in: scheme.in,
95
+ name: scheme.name,
96
+ });
97
+ }
98
+ }
99
+ }
100
+ return [...carriers.values()];
101
+ }
102
+ /**
103
+ * A document says which credential a call needs, never which caller may make it, so nothing but an
104
+ * explicit anonymous alternative is turned into a visibility fact.
105
+ */
106
+ export function authOf(requirements, carriers) {
107
+ return {
108
+ anonymous: isAnonymousAllowed(requirements) ? "yes" : "unknown",
109
+ policies: [],
110
+ imperative: false,
111
+ ...(carriers.length === 0 ? {} : { carriers: [...carriers] }),
112
+ };
113
+ }
@@ -0,0 +1,22 @@
1
+ import type { DiagnosticSink } from "../diagnostics.js";
2
+ import { type JsonPointer } from "../ir/brand.js";
3
+ import { type JsonValue } from "../ir/json.js";
4
+ export interface ServerOptions {
5
+ readonly documentUrl?: string;
6
+ readonly baseUrl?: string;
7
+ readonly variables?: Readonly<Record<string, string>>;
8
+ }
9
+ /**
10
+ * Resolves the first server of the nearest non-empty list among operation and path item, else the
11
+ * root's. A configured `baseUrl` replaces the root list; it does not override a server an
12
+ * operation or path item declares for itself.
13
+ *
14
+ * @param lists the operation's and the path item's lists, nearest first
15
+ */
16
+ export declare function serverOf(lists: ReadonlyArray<{
17
+ readonly servers: JsonValue | undefined;
18
+ readonly at: JsonPointer;
19
+ }>, root: {
20
+ readonly servers: JsonValue | undefined;
21
+ readonly at: JsonPointer;
22
+ }, options: ServerOptions, diagnostics: DiagnosticSink): string | undefined;