specli 0.0.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/CLAUDE.md +111 -0
- package/PLAN.md +274 -0
- package/README.md +474 -0
- package/biome.jsonc +1 -0
- package/bun.lock +98 -0
- package/cli.ts +74 -0
- package/fixtures/openapi-array-items.json +22 -0
- package/fixtures/openapi-auth.json +34 -0
- package/fixtures/openapi-body.json +41 -0
- package/fixtures/openapi-collision.json +21 -0
- package/fixtures/openapi-oauth.json +54 -0
- package/fixtures/openapi-servers.json +35 -0
- package/fixtures/openapi.json +87 -0
- package/index.ts +1 -0
- package/package.json +27 -0
- package/scripts/smoke-specs.ts +64 -0
- package/src/cli/auth-requirements.test.ts +27 -0
- package/src/cli/auth-requirements.ts +91 -0
- package/src/cli/auth-schemes.test.ts +66 -0
- package/src/cli/auth-schemes.ts +187 -0
- package/src/cli/capabilities.test.ts +94 -0
- package/src/cli/capabilities.ts +88 -0
- package/src/cli/command-id.test.ts +32 -0
- package/src/cli/command-id.ts +16 -0
- package/src/cli/command-index.ts +19 -0
- package/src/cli/command-model.test.ts +44 -0
- package/src/cli/command-model.ts +128 -0
- package/src/cli/compile.ts +119 -0
- package/src/cli/crypto.ts +9 -0
- package/src/cli/derive-name.ts +101 -0
- package/src/cli/exec.ts +72 -0
- package/src/cli/main.ts +336 -0
- package/src/cli/naming.test.ts +86 -0
- package/src/cli/naming.ts +224 -0
- package/src/cli/operations.test.ts +57 -0
- package/src/cli/operations.ts +152 -0
- package/src/cli/params.test.ts +70 -0
- package/src/cli/params.ts +71 -0
- package/src/cli/pluralize.ts +41 -0
- package/src/cli/positional.test.ts +65 -0
- package/src/cli/positional.ts +75 -0
- package/src/cli/request-body.test.ts +35 -0
- package/src/cli/request-body.ts +94 -0
- package/src/cli/runtime/argv.ts +14 -0
- package/src/cli/runtime/auth/resolve.ts +31 -0
- package/src/cli/runtime/body.ts +24 -0
- package/src/cli/runtime/collect.ts +6 -0
- package/src/cli/runtime/context.ts +62 -0
- package/src/cli/runtime/execute.ts +138 -0
- package/src/cli/runtime/generated.ts +200 -0
- package/src/cli/runtime/headers.ts +37 -0
- package/src/cli/runtime/index.ts +3 -0
- package/src/cli/runtime/profile/secrets.ts +42 -0
- package/src/cli/runtime/profile/store.ts +98 -0
- package/src/cli/runtime/request.test.ts +153 -0
- package/src/cli/runtime/request.ts +487 -0
- package/src/cli/runtime/server-url.ts +44 -0
- package/src/cli/runtime/template.ts +26 -0
- package/src/cli/runtime/validate/ajv.ts +13 -0
- package/src/cli/runtime/validate/coerce.ts +71 -0
- package/src/cli/runtime/validate/error.ts +29 -0
- package/src/cli/runtime/validate/index.ts +4 -0
- package/src/cli/runtime/validate/schema.ts +54 -0
- package/src/cli/schema-shape.ts +36 -0
- package/src/cli/schema.ts +76 -0
- package/src/cli/server.test.ts +35 -0
- package/src/cli/server.ts +88 -0
- package/src/cli/spec-id.ts +12 -0
- package/src/cli/spec-loader.ts +58 -0
- package/src/cli/stable-json.ts +35 -0
- package/src/cli/strings.ts +21 -0
- package/src/cli/types.ts +59 -0
- package/src/compiled.ts +23 -0
- package/src/macros/env.ts +25 -0
- package/src/macros/spec.ts +17 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Auth API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"servers": [{ "url": "https://api.example.com" }],
|
|
8
|
+
"components": {
|
|
9
|
+
"securitySchemes": {
|
|
10
|
+
"bearerAuth": {
|
|
11
|
+
"type": "http",
|
|
12
|
+
"scheme": "bearer",
|
|
13
|
+
"bearerFormat": "JWT"
|
|
14
|
+
},
|
|
15
|
+
"apiKeyAuth": {
|
|
16
|
+
"type": "apiKey",
|
|
17
|
+
"in": "header",
|
|
18
|
+
"name": "X-API-Key"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"paths": {
|
|
23
|
+
"/ping": {
|
|
24
|
+
"get": {
|
|
25
|
+
"operationId": "Ping.Get",
|
|
26
|
+
"responses": {
|
|
27
|
+
"200": {
|
|
28
|
+
"description": "OK"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Body API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"servers": [{ "url": "https://api.example.com" }],
|
|
8
|
+
"paths": {
|
|
9
|
+
"/contacts": {
|
|
10
|
+
"post": {
|
|
11
|
+
"operationId": "Contacts.Create",
|
|
12
|
+
"tags": ["Contacts"],
|
|
13
|
+
"requestBody": {
|
|
14
|
+
"required": true,
|
|
15
|
+
"content": {
|
|
16
|
+
"application/json": {
|
|
17
|
+
"schema": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"name": { "type": "string" }
|
|
21
|
+
},
|
|
22
|
+
"required": ["name"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"application/x-www-form-urlencoded": {
|
|
26
|
+
"schema": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"name": { "type": "string" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"responses": {
|
|
36
|
+
"200": { "description": "OK" }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Collision API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"/contacts": {
|
|
9
|
+
"get": {
|
|
10
|
+
"operationId": "Contacts.List",
|
|
11
|
+
"responses": { "200": { "description": "OK" } }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"/contacts/search": {
|
|
15
|
+
"get": {
|
|
16
|
+
"operationId": "Contacts.List",
|
|
17
|
+
"responses": { "200": { "description": "OK" } }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "OAuth API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"servers": [{ "url": "https://api.example.com" }],
|
|
8
|
+
"security": [
|
|
9
|
+
{
|
|
10
|
+
"oauth": ["read:ping"]
|
|
11
|
+
},
|
|
12
|
+
{}
|
|
13
|
+
],
|
|
14
|
+
"components": {
|
|
15
|
+
"securitySchemes": {
|
|
16
|
+
"oauth": {
|
|
17
|
+
"type": "oauth2",
|
|
18
|
+
"flows": {
|
|
19
|
+
"authorizationCode": {
|
|
20
|
+
"authorizationUrl": "https://example.com/oauth/authorize",
|
|
21
|
+
"tokenUrl": "https://example.com/oauth/token",
|
|
22
|
+
"scopes": {
|
|
23
|
+
"read:ping": "read ping"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"clientCredentials": {
|
|
27
|
+
"tokenUrl": "https://example.com/oauth/token",
|
|
28
|
+
"scopes": {
|
|
29
|
+
"read:ping": "read ping",
|
|
30
|
+
"write:ping": "write ping"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"paths": {
|
|
38
|
+
"/ping": {
|
|
39
|
+
"get": {
|
|
40
|
+
"operationId": "Ping.Get",
|
|
41
|
+
"security": [
|
|
42
|
+
{
|
|
43
|
+
"oauth": ["read:ping"]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"responses": {
|
|
47
|
+
"200": {
|
|
48
|
+
"description": "OK"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Servers API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"servers": [
|
|
8
|
+
{
|
|
9
|
+
"url": "https://{region}.api.example.com/{basePath}",
|
|
10
|
+
"description": "Regional server",
|
|
11
|
+
"variables": {
|
|
12
|
+
"region": {
|
|
13
|
+
"default": "us",
|
|
14
|
+
"enum": ["us", "eu"],
|
|
15
|
+
"description": "Region code"
|
|
16
|
+
},
|
|
17
|
+
"basePath": {
|
|
18
|
+
"default": "v1"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"paths": {
|
|
24
|
+
"/ping": {
|
|
25
|
+
"get": {
|
|
26
|
+
"operationId": "Ping.Get",
|
|
27
|
+
"responses": {
|
|
28
|
+
"200": {
|
|
29
|
+
"description": "OK"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Contacts API",
|
|
5
|
+
"version": "1.0.0"
|
|
6
|
+
},
|
|
7
|
+
"servers": [
|
|
8
|
+
{
|
|
9
|
+
"url": "https://api.example.com"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"paths": {
|
|
13
|
+
"/contacts": {
|
|
14
|
+
"get": {
|
|
15
|
+
"operationId": "Contacts.List",
|
|
16
|
+
"tags": ["Contacts"],
|
|
17
|
+
"summary": "List contacts",
|
|
18
|
+
"parameters": [
|
|
19
|
+
{
|
|
20
|
+
"in": "query",
|
|
21
|
+
"name": "limit",
|
|
22
|
+
"schema": { "type": "integer", "minimum": 1, "maximum": 100 },
|
|
23
|
+
"description": "Max items to return"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"in": "query",
|
|
27
|
+
"name": "tag",
|
|
28
|
+
"schema": { "type": "array", "items": { "type": "string" } },
|
|
29
|
+
"description": "Filter by tags"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"responses": {
|
|
33
|
+
"200": {
|
|
34
|
+
"description": "OK",
|
|
35
|
+
"content": {
|
|
36
|
+
"application/json": {
|
|
37
|
+
"schema": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"id": { "type": "string" },
|
|
43
|
+
"name": { "type": "string" }
|
|
44
|
+
},
|
|
45
|
+
"required": ["id", "name"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"/contacts/{id}": {
|
|
55
|
+
"get": {
|
|
56
|
+
"operationId": "Contacts.Get",
|
|
57
|
+
"tags": ["Contacts"],
|
|
58
|
+
"summary": "Get contact",
|
|
59
|
+
"parameters": [
|
|
60
|
+
{
|
|
61
|
+
"in": "path",
|
|
62
|
+
"name": "id",
|
|
63
|
+
"required": true,
|
|
64
|
+
"schema": { "type": "string" }
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"responses": {
|
|
68
|
+
"200": {
|
|
69
|
+
"description": "OK",
|
|
70
|
+
"content": {
|
|
71
|
+
"application/json": {
|
|
72
|
+
"schema": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"id": { "type": "string" },
|
|
76
|
+
"name": { "type": "string" }
|
|
77
|
+
},
|
|
78
|
+
"required": ["id", "name"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { main } from "./src/cli/main.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specli",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"bin": {
|
|
7
|
+
"opencli": "./cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lint": "biome ci",
|
|
11
|
+
"lint:check": "biome check --write --unsafe",
|
|
12
|
+
"lint:format": "biome format --write",
|
|
13
|
+
"typecheck": "tsgo"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@biomejs/biome": "^2.3.11",
|
|
17
|
+
"@types/bun": "^1.3.6",
|
|
18
|
+
"@typescript/native-preview": "^7.0.0-dev.20260120.1"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@apidevtools/swagger-parser": "^12.1.0",
|
|
22
|
+
"ajv": "^8.17.1",
|
|
23
|
+
"ajv-formats": "^3.0.1",
|
|
24
|
+
"commander": "^14.0.2",
|
|
25
|
+
"openapi-types": "^12.1.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const specs = [
|
|
4
|
+
{
|
|
5
|
+
name: "vercel",
|
|
6
|
+
url: "https://api.vercel.com/copper/_openapi.json",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "digitalocean",
|
|
10
|
+
url: "https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml",
|
|
11
|
+
},
|
|
12
|
+
// {
|
|
13
|
+
// name: "stripe",
|
|
14
|
+
// url: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yaml",
|
|
15
|
+
// },
|
|
16
|
+
// NOTE: OpenAI spec is not reliably fetchable via raw.githubusercontent.com
|
|
17
|
+
// in some environments (intermittent 404). Keep it out of the default smoke
|
|
18
|
+
// list to avoid flaky CI.
|
|
19
|
+
// {
|
|
20
|
+
// name: "openai",
|
|
21
|
+
// url: "https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml",
|
|
22
|
+
// },
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
type Result = {
|
|
26
|
+
spec: string;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
ms: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const results: Result[] = [];
|
|
33
|
+
|
|
34
|
+
for (const spec of specs) {
|
|
35
|
+
const start = performance.now();
|
|
36
|
+
try {
|
|
37
|
+
console.log(spec.url);
|
|
38
|
+
await Bun.$`bun ./cli.ts exec ${spec.url} __schema --json --min > /dev/null`;
|
|
39
|
+
results.push({
|
|
40
|
+
spec: spec.name,
|
|
41
|
+
ok: true,
|
|
42
|
+
ms: Math.round(performance.now() - start),
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
results.push({
|
|
47
|
+
spec: spec.name,
|
|
48
|
+
ok: false,
|
|
49
|
+
ms: Math.round(performance.now() - start),
|
|
50
|
+
error: message,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const allOk = results.every((r) => r.ok);
|
|
56
|
+
for (const r of results) {
|
|
57
|
+
if (r.ok) {
|
|
58
|
+
process.stdout.write(`ok ${r.spec} (${r.ms}ms)\n`);
|
|
59
|
+
} else {
|
|
60
|
+
process.stdout.write(`fail ${r.spec} (${r.ms}ms)\n${r.error}\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.exitCode = allOk ? 0 : 1;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { summarizeAuth } from "./auth-requirements.ts";
|
|
4
|
+
import type { AuthScheme } from "./auth-schemes.ts";
|
|
5
|
+
|
|
6
|
+
describe("summarizeAuth", () => {
|
|
7
|
+
test("uses operation-level security when present", () => {
|
|
8
|
+
const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
|
|
9
|
+
|
|
10
|
+
const summary = summarizeAuth(
|
|
11
|
+
[{ oauth: ["read:ping"] }],
|
|
12
|
+
[{ oauth: ["read:other"] }],
|
|
13
|
+
schemes,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(summary.alternatives).toEqual([
|
|
17
|
+
[{ key: "oauth", scopes: ["read:ping"] }],
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("empty operation security disables auth", () => {
|
|
22
|
+
const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
|
|
23
|
+
|
|
24
|
+
const summary = summarizeAuth([], [{ oauth: ["read:other"] }], schemes);
|
|
25
|
+
expect(summary.alternatives).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { AuthScheme } from "./auth-schemes.ts";
|
|
2
|
+
import type { SecurityRequirement } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export type AuthRequirement = {
|
|
5
|
+
key: string;
|
|
6
|
+
scopes: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type AuthSummary = {
|
|
10
|
+
// Alternatives: any one of these sets is sufficient.
|
|
11
|
+
alternatives: AuthRequirement[][];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isSecurityRequirement(value: unknown): value is SecurityRequirement {
|
|
15
|
+
if (!value || typeof value !== "object") return false;
|
|
16
|
+
if (Array.isArray(value)) return false;
|
|
17
|
+
|
|
18
|
+
for (const [k, v] of Object.entries(value)) {
|
|
19
|
+
if (typeof k !== "string") return false;
|
|
20
|
+
if (!Array.isArray(v)) return false;
|
|
21
|
+
if (!v.every((s) => typeof s === "string")) return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeSecurity(value: unknown): {
|
|
28
|
+
requirements: SecurityRequirement[];
|
|
29
|
+
source: "none" | "empty" | "non-empty";
|
|
30
|
+
} {
|
|
31
|
+
if (value == null) return { requirements: [], source: "none" };
|
|
32
|
+
if (!Array.isArray(value)) return { requirements: [], source: "none" };
|
|
33
|
+
|
|
34
|
+
const reqs = value.filter(isSecurityRequirement);
|
|
35
|
+
if (reqs.length === 0) return { requirements: [], source: "empty" };
|
|
36
|
+
return { requirements: reqs, source: "non-empty" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function summarizeAuth(
|
|
40
|
+
operationSecurity: unknown,
|
|
41
|
+
globalSecurity: unknown,
|
|
42
|
+
knownSchemes: AuthScheme[],
|
|
43
|
+
): AuthSummary {
|
|
44
|
+
// Per spec:
|
|
45
|
+
// - operation security overrides root
|
|
46
|
+
// - empty array [] means "no auth"
|
|
47
|
+
const op = normalizeSecurity(operationSecurity);
|
|
48
|
+
if (op.source === "non-empty") {
|
|
49
|
+
return { alternatives: toAlternatives(op.requirements, knownSchemes) };
|
|
50
|
+
}
|
|
51
|
+
if (op.source === "empty") {
|
|
52
|
+
return { alternatives: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const global = normalizeSecurity(globalSecurity);
|
|
56
|
+
if (global.source === "non-empty") {
|
|
57
|
+
return { alternatives: toAlternatives(global.requirements, knownSchemes) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { alternatives: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toAlternatives(
|
|
64
|
+
requirements: SecurityRequirement[],
|
|
65
|
+
knownSchemes: AuthScheme[],
|
|
66
|
+
): AuthRequirement[][] {
|
|
67
|
+
const known = new Set(knownSchemes.map((s) => s.key));
|
|
68
|
+
|
|
69
|
+
return requirements.map((req) => {
|
|
70
|
+
const out: AuthRequirement[] = [];
|
|
71
|
+
for (const [key, scopes] of Object.entries(req)) {
|
|
72
|
+
out.push({
|
|
73
|
+
key,
|
|
74
|
+
scopes: Array.isArray(scopes) ? scopes : [],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Stable order.
|
|
79
|
+
out.sort((a, b) => a.key.localeCompare(b.key));
|
|
80
|
+
|
|
81
|
+
// Prefer known schemes first.
|
|
82
|
+
out.sort((a, b) => {
|
|
83
|
+
const ak = known.has(a.key) ? 0 : 1;
|
|
84
|
+
const bk = known.has(b.key) ? 0 : 1;
|
|
85
|
+
if (ak !== bk) return ak - bk;
|
|
86
|
+
return a.key.localeCompare(b.key);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return out;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { listAuthSchemes } from "./auth-schemes.ts";
|
|
4
|
+
import type { OpenApiDoc } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
describe("listAuthSchemes", () => {
|
|
7
|
+
test("parses bearer + apiKey", () => {
|
|
8
|
+
const doc: OpenApiDoc = {
|
|
9
|
+
openapi: "3.0.3",
|
|
10
|
+
components: {
|
|
11
|
+
securitySchemes: {
|
|
12
|
+
bearerAuth: {
|
|
13
|
+
type: "http",
|
|
14
|
+
scheme: "bearer",
|
|
15
|
+
bearerFormat: "JWT",
|
|
16
|
+
},
|
|
17
|
+
apiKeyAuth: {
|
|
18
|
+
type: "apiKey",
|
|
19
|
+
in: "header",
|
|
20
|
+
name: "X-API-Key",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const schemes = listAuthSchemes(doc);
|
|
27
|
+
expect(schemes).toHaveLength(2);
|
|
28
|
+
|
|
29
|
+
const bearer = schemes.find((s) => s.key === "bearerAuth");
|
|
30
|
+
expect(bearer?.kind).toBe("http-bearer");
|
|
31
|
+
|
|
32
|
+
const apiKey = schemes.find((s) => s.key === "apiKeyAuth");
|
|
33
|
+
expect(apiKey?.kind).toBe("api-key");
|
|
34
|
+
expect(apiKey?.in).toBe("header");
|
|
35
|
+
expect(apiKey?.name).toBe("X-API-Key");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("parses oauth2 flows", () => {
|
|
39
|
+
const doc = {
|
|
40
|
+
openapi: "3.0.3",
|
|
41
|
+
components: {
|
|
42
|
+
securitySchemes: {
|
|
43
|
+
oauth: {
|
|
44
|
+
type: "oauth2",
|
|
45
|
+
flows: {
|
|
46
|
+
clientCredentials: {
|
|
47
|
+
tokenUrl: "https://example.com/oauth/token",
|
|
48
|
+
scopes: {
|
|
49
|
+
"read:ping": "read ping",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
const schemes = listAuthSchemes(doc);
|
|
59
|
+
const oauth = schemes.find((s) => s.key === "oauth");
|
|
60
|
+
expect(oauth?.kind).toBe("oauth2");
|
|
61
|
+
expect(oauth?.oauthFlows?.clientCredentials?.tokenUrl).toBe(
|
|
62
|
+
"https://example.com/oauth/token",
|
|
63
|
+
);
|
|
64
|
+
expect(oauth?.oauthFlows?.clientCredentials?.scopes).toEqual(["read:ping"]);
|
|
65
|
+
});
|
|
66
|
+
});
|