@ultimat3/action 22.11.0 → 22.13.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.
- package/README.md +3 -1
- package/package.json +7 -7
- package/src/openapi-complete.ts +35 -3
package/README.md
CHANGED
|
@@ -211,7 +211,9 @@ export const api = defineApi({
|
|
|
211
211
|
scopes: MCP_SCOPES, // scope -> [primitive names]; the map defineAppMcp takes
|
|
212
212
|
resolveToken, // the MCP resolver: token -> { actor, scopes } | null
|
|
213
213
|
rateLimit: { limit: 120, windowMs: 60_000 },
|
|
214
|
-
openapi: 'openapi.v1.json', // x manifest writes only the cut here; x verify checks it
|
|
214
|
+
openapi: 'openapi.v1.json', // x manifest writes only the cut here; x verify checks it —
|
|
215
|
+
// each operation `security: [{ bearer: ['<its scope>'] }]`,
|
|
216
|
+
// the bearer scheme lists every scope (x-ultimate.scopes)
|
|
215
217
|
}],
|
|
216
218
|
},
|
|
217
219
|
// Declaring it (any key) makes openapi.json COMPLETE: info, servers, securitySchemes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/action",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.13.0",
|
|
4
4
|
"description": "The action primitive: one declaration projected to route, OpenAPI, client, MCP tool, job handle, tests",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"test": "bun test"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@ultimat3/cache": "22.
|
|
38
|
-
"@ultimat3/core": "22.
|
|
39
|
-
"@ultimat3/entity": "22.
|
|
40
|
-
"@ultimat3/http": "22.
|
|
41
|
-
"@ultimat3/policy": "22.
|
|
42
|
-
"@ultimat3/schema": "22.
|
|
37
|
+
"@ultimat3/cache": "22.13.0",
|
|
38
|
+
"@ultimat3/core": "22.13.0",
|
|
39
|
+
"@ultimat3/entity": "22.13.0",
|
|
40
|
+
"@ultimat3/http": "22.13.0",
|
|
41
|
+
"@ultimat3/policy": "22.13.0",
|
|
42
|
+
"@ultimat3/schema": "22.13.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/openapi-complete.ts
CHANGED
|
@@ -69,6 +69,11 @@ function routeIndex(routes: readonly Route[]): ReadonlyMap<string, Route> {
|
|
|
69
69
|
|
|
70
70
|
interface Refusals {
|
|
71
71
|
readonly scheme: string;
|
|
72
|
+
/**
|
|
73
|
+
* The security requirement's list: empty on the cookie document, the ONE scope a mounted
|
|
74
|
+
* operation needs on a bearer mount's (OpenAPI 3.1 allows role names for a non-OAuth scheme).
|
|
75
|
+
*/
|
|
76
|
+
readonly scopes: readonly string[];
|
|
72
77
|
/** The mount's own allowance applies to every operation, whatever the route declares. */
|
|
73
78
|
readonly everyOpLimited: boolean;
|
|
74
79
|
readonly unauthorizedHeaders?: Record<string, unknown>;
|
|
@@ -105,7 +110,7 @@ function completeOperation(operation: Operation, route: Route | undefined, refus
|
|
|
105
110
|
return {
|
|
106
111
|
...operation,
|
|
107
112
|
responses,
|
|
108
|
-
...(authenticated ? { security: [{ [refusals.scheme]: [] }] } : {}),
|
|
113
|
+
...(authenticated ? { security: [{ [refusals.scheme]: [...refusals.scopes] }] } : {}),
|
|
109
114
|
};
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -162,6 +167,7 @@ export function completeOpenApi(
|
|
|
162
167
|
const paths = mapOperations(document.paths, (path, method, operation) =>
|
|
163
168
|
completeOperation(operation, index.get(`${method} ${path}`), {
|
|
164
169
|
scheme: COOKIE_SCHEME,
|
|
170
|
+
scopes: [],
|
|
165
171
|
everyOpLimited: false,
|
|
166
172
|
}),
|
|
167
173
|
);
|
|
@@ -187,7 +193,8 @@ export function completeOpenApi(
|
|
|
187
193
|
/**
|
|
188
194
|
* A bearer mount's own document: only the operations its `scopes` name, re-keyed to the mounted
|
|
189
195
|
* paths, each `bearer`-secured with 401, 404-when-hidden and 429, and only the schemas those
|
|
190
|
-
* operations reference. `
|
|
196
|
+
* operations reference. Each operation's `security` is `[{ bearer: ['<scope>'] }]` — the scope a
|
|
197
|
+
* token needs, also in `x-ultimate.scope` — and the scheme documents the whole scope map.
|
|
191
198
|
*/
|
|
192
199
|
export function mountOpenApi(
|
|
193
200
|
document: OpenApiDocument,
|
|
@@ -205,6 +212,9 @@ export function mountOpenApi(
|
|
|
205
212
|
// A `Map`: the key is a route path, and a plain object keyed by one would read `__proto__`.
|
|
206
213
|
const mountedItems = new Map<string, Record<string, unknown>>();
|
|
207
214
|
const tags = new Set<string>();
|
|
215
|
+
// Scope → the operations in THIS document it unlocks: what the scheme documents, so it can never
|
|
216
|
+
// name an operation the document does not carry.
|
|
217
|
+
const unlocks = new Map<string, string[]>();
|
|
208
218
|
const byRoute = [...input.routes].sort((a, b) =>
|
|
209
219
|
`${a.path} ${a.method}` < `${b.path} ${b.method}` ? -1 : 1,
|
|
210
220
|
);
|
|
@@ -222,6 +232,7 @@ export function mountOpenApi(
|
|
|
222
232
|
{ ...route, meta: { ...route.meta, auth: 'required' } },
|
|
223
233
|
{
|
|
224
234
|
scheme: BEARER_SCHEME,
|
|
235
|
+
scopes: [scope],
|
|
225
236
|
everyOpLimited: mount.rateLimit !== undefined,
|
|
226
237
|
unauthorizedHeaders: {
|
|
227
238
|
'WWW-Authenticate': { description: 'Bearer', schema: { type: 'string' } },
|
|
@@ -234,6 +245,7 @@ export function mountOpenApi(
|
|
|
234
245
|
};
|
|
235
246
|
const extensions = (completed['x-ultimate'] as Record<string, unknown> | undefined) ?? {};
|
|
236
247
|
for (const tag of (completed['tags'] as readonly string[] | undefined) ?? []) tags.add(tag);
|
|
248
|
+
unlocks.set(scope, [...(unlocks.get(scope) ?? []), route.meta.name]);
|
|
237
249
|
const mounted = mountedPath(mount.prefix, route.path);
|
|
238
250
|
mountedItems.set(mounted, {
|
|
239
251
|
...mountedItems.get(mounted),
|
|
@@ -249,12 +261,32 @@ export function mountOpenApi(
|
|
|
249
261
|
paths,
|
|
250
262
|
components: {
|
|
251
263
|
schemas,
|
|
252
|
-
securitySchemes: { [BEARER_SCHEME]:
|
|
264
|
+
securitySchemes: { [BEARER_SCHEME]: scopedBearerScheme(unlocks) },
|
|
253
265
|
},
|
|
254
266
|
tags: [...tags].sort().map((name) => ({ name })),
|
|
255
267
|
};
|
|
256
268
|
}
|
|
257
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The mount's `bearer` scheme with its scopes stated. An `http` scheme has no `scopes` field (that
|
|
272
|
+
* is OAuth's `flows`), so they are in the description for a reader and in `x-ultimate.scopes` —
|
|
273
|
+
* scope → operation names, both sorted — for a generator; each operation's requirement names one.
|
|
274
|
+
*/
|
|
275
|
+
function scopedBearerScheme(unlocks: ReadonlyMap<string, readonly string[]>) {
|
|
276
|
+
const scopes = [...unlocks.keys()].sort();
|
|
277
|
+
const table = scopes.map((scope) => [scope, [...(unlocks.get(scope) ?? [])].sort()] as const);
|
|
278
|
+
const lines = table.map(([scope, names]) => `- \`${scope}\`: ${names.join(', ')}`);
|
|
279
|
+
return {
|
|
280
|
+
...BEARER_SECURITY_SCHEME,
|
|
281
|
+
description: [
|
|
282
|
+
BEARER_SECURITY_SCHEME.description,
|
|
283
|
+
'Each operation lists the one scope a token needs in its security requirement; a token without it is answered 404.',
|
|
284
|
+
...(lines.length === 0 ? [] : ['', 'Scopes:', ...lines]),
|
|
285
|
+
].join('\n'),
|
|
286
|
+
'x-ultimate': { scopes: Object.fromEntries(table) },
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
258
290
|
/** Every component schema `root` reaches through `$ref`, transitively. */
|
|
259
291
|
function referencedSchemas(
|
|
260
292
|
root: unknown,
|