@willyim/idp 0.6.0 → 0.7.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 CHANGED
@@ -215,7 +215,9 @@ type Session = {
215
215
  }
216
216
  ```
217
217
 
218
- `can()` matches exactly, and honours `resource:*` and `*` grants.
218
+ `can()` matches exactly, and honours `resource:*` and `*` grants — so a grant of
219
+ `kirby:*` covers `kirby:read` and a per-instance `kirby:thread:t_7f3a` alike
220
+ (see [Resource-scoped grants](#resource-scoped-grants)).
219
221
 
220
222
  `image` is effectively always set against the willy.im IdP: it renders a
221
223
  deterministic [blobatar](https://blobatar.dev) for anyone who never uploaded a
@@ -321,7 +323,9 @@ const keys = createUserKeys({
321
323
  const minted = await keys.create({
322
324
  userId: session.userId,
323
325
  name: "cli",
324
- scopes: ["analytics:read"], // must be in the app's product permission catalog
326
+ // A declared permission, or `<type>:<id>` for an instance the app lists
327
+ // anything else is a 422 (`unknown_scopes` / `unknown_resource`).
328
+ scopes: ["analytics:read", "kirby:thread:t_7f3a"],
325
329
  workspaceId: session.workspaceId,
326
330
  })
327
331
 
@@ -347,6 +351,61 @@ ingest token, say — identifies a site rather than a user, cannot be kept secre
347
351
  and must not pay a round trip per hit. Keep those in the app's own table and
348
352
  gate them on `Origin` plus rate limiting.
349
353
 
354
+ ## Resource-scoped grants
355
+
356
+ A permission names a surface (`kirby:read`). When the honest grant is one thing
357
+ inside it — one conversation, one document, one workspace — the app declares a
358
+ resource **type** in its catalog and the grant becomes `<type>:<id>`:
359
+
360
+ ```ts
361
+ await api.request("put", "/api/v1/apps/{app}/permissions", {
362
+ params: { app: "bender" },
363
+ body: {
364
+ permissions: ["kirby:read", "kirby:write"],
365
+ resourceTypes: [
366
+ {
367
+ type: "kirby:thread", // grants compose as kirby:thread:<id>
368
+ label: "WhatsApp conversation", // what the console calls one
369
+ list: "https://bender.romo.fyi/idp/resources/kirby-thread",
370
+ },
371
+ ],
372
+ },
373
+ })
374
+ ```
375
+
376
+ The IdP never stores the instances. When someone picks one in the console, or a
377
+ key is minted for one, it GETs the type's `list` URL and reads
378
+ `{ resources: [{ id, label, description? }] }` (`ResourceListSchema` in
379
+ `@willyim/idp/schemas`). The call carries a one-minute JWT the IdP signs with
380
+ its OIDC key — `aud` is the list URL, the permissions claim is
381
+ `idp:resources:list` — which the app verifies exactly as it verifies an MCP
382
+ access token:
383
+
384
+ ```ts
385
+ import { createResourceServer, RESOURCE_LIST_PERMISSION } from "@willyim/idp"
386
+
387
+ const listing = createResourceServer({
388
+ issuer: "https://idp.willy.im/auth",
389
+ resource: "https://bender.romo.fyi/idp/resources/kirby-thread",
390
+ })
391
+
392
+ const auth = await listing.authenticate(request, { permissions: [RESOURCE_LIST_PERMISSION] })
393
+ if (!auth.ok) return new Response(auth.error, { status: auth.status })
394
+ return Response.json({ resources: threads.map((t) => ({ id: t.handle, label: t.title })) })
395
+ ```
396
+
397
+ `id` is the stable, opaque handle the grant will name — never an alias — and
398
+ is one segment (no colon, no `*`, no whitespace). On the consuming side nothing
399
+ changes: the composed string arrives in `permissions` / `scopes`, and
400
+ `grants(permissions, "kirby:thread:t_7f3a")` is true for that grant, for
401
+ `kirby:*`, and for `*`. App admins hold `<type>:*` for every declared type. A
402
+ grant whose instance later disappears stays on the key and simply matches
403
+ nothing; removing it is a console act, not the app's.
404
+
405
+ The full contract — token claims, every error shape, what happens when the
406
+ list is unreachable — is in
407
+ [`apps/idp/docs/resource-scopes.md`](../../apps/idp/docs/resource-scopes.md).
408
+
350
409
  ## Linked identities
351
410
 
352
411
  A user's ids on *other* systems — their Slack member id, their WhatsApp number,
@@ -17,6 +17,7 @@ export { createIdp, DEFAULT_SESSION_COOKIE, publicSession, safeNext, type Idp, t
17
17
  export { memorySessions, type MemorySessionStore, type SessionRecord, type SessionStore, } from "./store.js";
18
18
  export { grants, normalizeClaims, PERMISSIONS_CLAIM, WORKSPACES_CLAIM, type Actor, type Claims, type Workspace, } from "./claims.js";
19
19
  export { createManagementApi, type ManagementApi, type ManagementApiOptions, } from "./api.js";
20
+ export { RESOURCE_ID_RE, RESOURCE_LIST_PERMISSION, RESOURCE_LIST_TOKEN_TTL_S, RESOURCE_TYPE_RE, ResourceInstanceSchema, ResourceListSchema, ResourceTypeInput, ResourceTypeSchema, } from "./schemas/index.js";
20
21
  export { createUserKeys, readApiKey, type AuthenticatedKey, type AuthenticateOptions, type AuthenticateResult, type CreateUserApiKeyInput, type ListFilter, type MintedUserApiKey, type UserApiKey, type UserKeyCacheOptions, type UserKeys, type UserKeysOptions, type UserKeyValidation, } from "./user-keys.js";
21
22
  export { createIdentities, type Identities, type IdentitiesOptions, type IdentityCacheOptions, type ResolvedIdentity, type IdentityResolution, } from "./identities.js";
22
23
  export { createResourceServer, type ResourceServer, type ResourceServerOptions, type VerifiedAccessToken, type AuthResult, } from "./resource-server.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,UAAU,EACV,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,QAAQ,EACb,KAAK,MAAM,GACZ,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,aAAa,EACb,QAAQ,EACR,KAAK,GAAG,EACR,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,OAAO,EACZ,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,MAAM,EACN,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,KAAK,EACV,KAAK,MAAM,EACX,KAAK,SAAS,GACf,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,oBAAoB,GAC1B,MAAM,UAAU,CAAA;AAEjB,OAAO,EACL,cAAc,EACd,UAAU,EACV,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,UAAU,GAChB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EACf,UAAU,EACV,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,QAAQ,EACb,KAAK,MAAM,GACZ,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,aAAa,EACb,QAAQ,EACR,KAAK,GAAG,EACR,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,OAAO,EACZ,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,MAAM,EACN,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,KAAK,EACV,KAAK,MAAM,EACX,KAAK,SAAS,GACf,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,oBAAoB,GAC1B,MAAM,UAAU,CAAA;AAIjB,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACL,cAAc,EACd,UAAU,EACV,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,UAAU,GAChB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA"}
package/dist/src/index.js CHANGED
@@ -17,6 +17,9 @@ export { createIdp, DEFAULT_SESSION_COOKIE, publicSession, safeNext, } from "./s
17
17
  export { memorySessions, } from "./store.js";
18
18
  export { grants, normalizeClaims, PERMISSIONS_CLAIM, WORKSPACES_CLAIM, } from "./claims.js";
19
19
  export { createManagementApi, } from "./api.js";
20
+ // Resource-scoped grants: the permission the IdP's listing token carries, and
21
+ // the shapes of a type and an instance. The full set lives in ./schemas.
22
+ export { RESOURCE_ID_RE, RESOURCE_LIST_PERMISSION, RESOURCE_LIST_TOKEN_TTL_S, RESOURCE_TYPE_RE, ResourceInstanceSchema, ResourceListSchema, ResourceTypeInput, ResourceTypeSchema, } from "./schemas/index.js";
20
23
  export { createUserKeys, readApiKey, } from "./user-keys.js";
21
24
  export { createIdentities, } from "./identities.js";
22
25
  export { createResourceServer, } from "./resource-server.js";
@@ -10,12 +10,53 @@
10
10
  * `../wire.ts`, next to the code that talks to them.
11
11
  */
12
12
  import { z } from "zod";
13
+ /** A resource type name: colon-separated lowercase segments, no wildcard. */
14
+ export declare const RESOURCE_TYPE_RE: RegExp;
15
+ /** An instance id: the final segment of a grant, so no colon, no `*`, no whitespace. */
16
+ export declare const RESOURCE_ID_RE: RegExp;
17
+ /**
18
+ * The permission the IdP's listing token carries, so an app can gate its `list`
19
+ * endpoint with `resourceServer.authenticate(request, { permissions: [...] })`.
20
+ * Namespaced under `idp:` so it can never collide with an app's own catalog.
21
+ */
22
+ export declare const RESOURCE_LIST_PERMISSION = "idp:resources:list";
23
+ /** Lifetime of the IdP-signed token that authenticates a listing call. */
24
+ export declare const RESOURCE_LIST_TOKEN_TTL_S = 60;
25
+ export declare const ResourceTypeInput: z.ZodObject<{
26
+ type: z.ZodString;
27
+ label: z.ZodOptional<z.ZodString>;
28
+ list: z.ZodString;
29
+ }, z.core.$strip>;
30
+ export declare const ResourceTypeSchema: z.ZodObject<{
31
+ type: z.ZodString;
32
+ label: z.ZodString;
33
+ list: z.ZodString;
34
+ }, z.core.$strip>;
35
+ /** One instance of a resource type, as the app's `list` endpoint reports it. */
36
+ export declare const ResourceInstanceSchema: z.ZodObject<{
37
+ id: z.ZodString;
38
+ label: z.ZodString;
39
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
+ }, z.core.$strip>;
41
+ /** The body an app's resource `list` endpoint must answer with. */
42
+ export declare const ResourceListSchema: z.ZodObject<{
43
+ resources: z.ZodArray<z.ZodObject<{
44
+ id: z.ZodString;
45
+ label: z.ZodString;
46
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ }, z.core.$strip>>;
48
+ }, z.core.$strip>;
13
49
  export declare const ApplicationSchema: z.ZodObject<{
14
50
  clientId: z.ZodString;
15
51
  name: z.ZodNullable<z.ZodString>;
16
52
  app: z.ZodNullable<z.ZodString>;
17
53
  allowSignup: z.ZodBoolean;
18
54
  permissions: z.ZodArray<z.ZodString>;
55
+ resourceTypes: z.ZodArray<z.ZodObject<{
56
+ type: z.ZodString;
57
+ label: z.ZodString;
58
+ list: z.ZodString;
59
+ }, z.core.$strip>>;
19
60
  resources: z.ZodArray<z.ZodString>;
20
61
  redirectUris: z.ZodArray<z.ZodString>;
21
62
  disabled: z.ZodBoolean;
@@ -43,6 +84,11 @@ export declare const ApplicationListSchema: z.ZodObject<{
43
84
  app: z.ZodNullable<z.ZodString>;
44
85
  allowSignup: z.ZodBoolean;
45
86
  permissions: z.ZodArray<z.ZodString>;
87
+ resourceTypes: z.ZodArray<z.ZodObject<{
88
+ type: z.ZodString;
89
+ label: z.ZodString;
90
+ list: z.ZodString;
91
+ }, z.core.$strip>>;
46
92
  resources: z.ZodArray<z.ZodString>;
47
93
  redirectUris: z.ZodArray<z.ZodString>;
48
94
  disabled: z.ZodBoolean;
@@ -74,12 +120,26 @@ export declare const UpdateApplicationInput: z.ZodObject<{
74
120
  export declare const ClientSecretSchema: z.ZodObject<{
75
121
  clientSecret: z.ZodString;
76
122
  }, z.core.$strip>;
77
- /** Replaces the app's product-permission catalog wholesale. */
123
+ /**
124
+ * Replaces the app's product-permission catalog wholesale — both the flat
125
+ * permissions and the resource types. Omitting `resourceTypes` clears them,
126
+ * the same way omitting a permission removes it.
127
+ */
78
128
  export declare const SetAppPermissionsInput: z.ZodObject<{
79
129
  permissions: z.ZodArray<z.ZodString>;
130
+ resourceTypes: z.ZodDefault<z.ZodArray<z.ZodObject<{
131
+ type: z.ZodString;
132
+ label: z.ZodOptional<z.ZodString>;
133
+ list: z.ZodString;
134
+ }, z.core.$strip>>>;
80
135
  }, z.core.$strip>;
81
136
  export declare const AppPermissionsSchema: z.ZodObject<{
82
137
  permissions: z.ZodArray<z.ZodString>;
138
+ resourceTypes: z.ZodArray<z.ZodObject<{
139
+ type: z.ZodString;
140
+ label: z.ZodString;
141
+ list: z.ZodString;
142
+ }, z.core.$strip>>;
83
143
  }, z.core.$strip>;
84
144
  export declare const UserListSchema: z.ZodObject<{
85
145
  users: z.ZodArray<z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/schemas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,iBAAiB;;;;;;;;;;iBAY5B,CAAA;AAEF,eAAO,MAAM,UAAU;;;;;;;iBASrB,CAAA;AAEF,eAAO,MAAM,eAAe;;;;;;iBAM1B,CAAA;AAEF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;iBAAyD,CAAA;AAE3F;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;;;iBAejC,CAAA;AACF,eAAO,MAAM,wBAAwB;;;;iBAInC,CAAA;AAEF,uEAAuE;AACvE,eAAO,MAAM,sBAAsB;;;;;iBAQjC,CAAA;AAEF,eAAO,MAAM,kBAAkB;;iBAE7B,CAAA;AAEF,+DAA+D;AAC/D,eAAO,MAAM,sBAAsB;;iBAEjC,CAAA;AACF,eAAO,MAAM,oBAAoB;;iBAAiD,CAAA;AAClF,eAAO,MAAM,cAAc;;;;;;;;;iBAA2C,CAAA;AACtE,eAAO,MAAM,mBAAmB;;;;;;;;iBAAqD,CAAA;AAIrF,eAAO,MAAM,UAAU;;;EAA8B,CAAA;AAErD,eAAO,MAAM,YAAY;;;;;;;;;iBAMvB,CAAA;AACF,eAAO,MAAM,gBAAgB;;;;;;;;;;;iBAA+C,CAAA;AAE5E,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB;;;;;;;iBAI5B,CAAA;AACF,eAAO,MAAM,kBAAkB;;;;;iBAG7B,CAAA;AAEF,eAAO,MAAM,iBAAiB;;;;;;iBAG5B,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;iBAM/B,CAAA;AACF,eAAO,MAAM,sBAAsB;;;;iBAIjC,CAAA;AAEF,eAAO,MAAM,QAAQ;;iBAAoC,CAAA;AAIzD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;iBAW3B,CAAA;AACF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;iBAAgD,CAAA;AAEjF,eAAO,MAAM,qBAAqB;;;;;;iBAMhC,CAAA;AACF,eAAO,MAAM,uBAAuB;;;;iBAIlC,CAAA;AAEF,eAAO,MAAM,uBAAuB;;iBAAyC,CAAA;AAC7E,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;mBAUrC,CAAA;AAIF,eAAO,MAAM,oBAAoB;;;;;;;iBAO/B,CAAA;AACF,eAAO,MAAM,wBAAwB;;;;;;;;;iBAA0D,CAAA;AAE/F,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAA;AACF,eAAO,MAAM,2BAA2B;;;iBAGtC,CAAA;AAEF,eAAO,MAAM,wBAAwB;;;;;;;;mBAWnC,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;iBAQ3B,CAAA;AACF,eAAO,MAAM,eAAe;;;;;;;;;;iBAAmD,CAAA;AAI/E,eAAO,MAAM,YAAY;;;;;;;;;;;;;;iBAUvB,CAAA;AACF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;iBAA4C,CAAA;AAEzE,eAAO,MAAM,iBAAiB;;;;iBAM5B,CAAA;AACF,eAAO,MAAM,mBAAmB;;;;iBAI9B,CAAA;AAIF,eAAO,MAAM,cAAc;;;;;;;;;;;;;iBASzB,CAAA;AACF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;iBAA8C,CAAA;AAE7E,eAAO,MAAM,mBAAmB;;;iBAG9B,CAAA;AACF,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/schemas/index.ts"],"names":[],"mappings":"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"}
@@ -10,12 +10,60 @@
10
10
  * `../wire.ts`, next to the code that talks to them.
11
11
  */
12
12
  import { z } from "zod";
13
+ // --- Resource-scoped permissions ---
14
+ //
15
+ // A permission names a surface (`kirby:read`). A resource TYPE names a family
16
+ // of permissions over instances the app holds (`kirby:thread` over every
17
+ // conversation), and a grant is the composed string `<type>:<id>` —
18
+ // `kirby:thread:t_7f3a`. The IdP never stores the instances: it asks the app
19
+ // for them, over the `list` URL the type declares, whenever a human picks one
20
+ // or a key is minted. `grants()` is untouched — `kirby:*` and `*` cover every
21
+ // instance the same way they cover `kirby:read`.
22
+ /** A resource type name: colon-separated lowercase segments, no wildcard. */
23
+ export const RESOURCE_TYPE_RE = /^[a-z0-9_-]+(:[a-z0-9_-]+)*$/;
24
+ /** An instance id: the final segment of a grant, so no colon, no `*`, no whitespace. */
25
+ export const RESOURCE_ID_RE = /^[^\s:*]+$/;
26
+ /**
27
+ * The permission the IdP's listing token carries, so an app can gate its `list`
28
+ * endpoint with `resourceServer.authenticate(request, { permissions: [...] })`.
29
+ * Namespaced under `idp:` so it can never collide with an app's own catalog.
30
+ */
31
+ export const RESOURCE_LIST_PERMISSION = "idp:resources:list";
32
+ /** Lifetime of the IdP-signed token that authenticates a listing call. */
33
+ export const RESOURCE_LIST_TOKEN_TTL_S = 60;
34
+ export const ResourceTypeInput = z.object({
35
+ type: z
36
+ .string()
37
+ .regex(RESOURCE_TYPE_RE, "lowercase segments separated by colons, e.g. kirby:thread")
38
+ .describe("Permission prefix a grant composes under: `kirby:thread` → `kirby:thread:<id>`"),
39
+ label: z.string().min(1).optional().describe("Human name for one instance, shown in the console — “WhatsApp conversation”"),
40
+ list: z
41
+ .string()
42
+ .url()
43
+ .describe("Absolute URL the IdP GETs to enumerate instances. Called with an IdP-signed bearer JWT (aud = this URL); must answer `ResourceListSchema`"),
44
+ });
45
+ export const ResourceTypeSchema = z.object({
46
+ type: z.string(),
47
+ label: z.string(),
48
+ list: z.string(),
49
+ });
50
+ /** One instance of a resource type, as the app's `list` endpoint reports it. */
51
+ export const ResourceInstanceSchema = z.object({
52
+ id: z.string().regex(RESOURCE_ID_RE).describe("The stable, opaque id the grant will name — a handle, never an alias"),
53
+ label: z.string().min(1).describe("What a human sees when picking it"),
54
+ description: z.string().nullable().optional().describe("Secondary line in the picker — kind, size, last activity"),
55
+ });
56
+ /** The body an app's resource `list` endpoint must answer with. */
57
+ export const ResourceListSchema = z.object({ resources: z.array(ResourceInstanceSchema) });
13
58
  export const ApplicationSchema = z.object({
14
59
  clientId: z.string(),
15
60
  name: z.string().nullable(),
16
61
  app: z.string().nullable().describe("Application key; consumer workspace claims are filtered by this"),
17
62
  allowSignup: z.boolean().describe("Whether unknown users may sign themselves up"),
18
63
  permissions: z.array(z.string()).describe("The app's declared product-permission catalog"),
64
+ resourceTypes: z
65
+ .array(ResourceTypeSchema)
66
+ .describe("Declared resource types — permission families over instances the app holds"),
19
67
  resources: z
20
68
  .array(z.string())
21
69
  .describe("Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"),
@@ -77,11 +125,19 @@ export const UpdateApplicationInput = z.object({
77
125
  export const ClientSecretSchema = z.object({
78
126
  clientSecret: z.string().describe("Plaintext client secret — shown exactly once, never stored"),
79
127
  });
80
- /** Replaces the app's product-permission catalog wholesale. */
128
+ /**
129
+ * Replaces the app's product-permission catalog wholesale — both the flat
130
+ * permissions and the resource types. Omitting `resourceTypes` clears them,
131
+ * the same way omitting a permission removes it.
132
+ */
81
133
  export const SetAppPermissionsInput = z.object({
82
134
  permissions: z.array(z.string().min(1)),
135
+ resourceTypes: z.array(ResourceTypeInput).default([]),
136
+ });
137
+ export const AppPermissionsSchema = z.object({
138
+ permissions: z.array(z.string()),
139
+ resourceTypes: z.array(ResourceTypeSchema),
83
140
  });
84
- export const AppPermissionsSchema = z.object({ permissions: z.array(z.string()) });
85
141
  export const UserListSchema = z.object({ users: z.array(UserSchema) });
86
142
  export const WorkspaceListSchema = z.object({ workspaces: z.array(WorkspaceSchema) });
87
143
  // --- Write management API (scoped-key authenticated, per-app) ---
@@ -138,7 +194,10 @@ export const UserApiKeyListSchema = z.object({ keys: z.array(UserApiKeySchema) }
138
194
  export const CreateUserApiKeyInput = z.object({
139
195
  userId: z.string().min(1),
140
196
  name: z.string().min(1),
141
- scopes: z.array(z.string()).default([]).describe("Subset of the app's product permission catalog"),
197
+ scopes: z
198
+ .array(z.string())
199
+ .default([])
200
+ .describe("Declared permissions, or `<type>:<id>` grants over a declared resource type whose instance the app currently lists"),
142
201
  workspaceId: z.string().optional(),
143
202
  expiresAt: z.iso.datetime().optional().describe("ISO 8601; omit for non-expiring"),
144
203
  });
@@ -43,6 +43,11 @@ export declare const operations: {
43
43
  app: z.ZodNullable<z.ZodString>;
44
44
  allowSignup: z.ZodBoolean;
45
45
  permissions: z.ZodArray<z.ZodString>;
46
+ resourceTypes: z.ZodArray<z.ZodObject<{
47
+ type: z.ZodString;
48
+ label: z.ZodString;
49
+ list: z.ZodString;
50
+ }, z.core.$strip>>;
46
51
  resources: z.ZodArray<z.ZodString>;
47
52
  redirectUris: z.ZodArray<z.ZodString>;
48
53
  disabled: z.ZodBoolean;
@@ -80,6 +85,11 @@ export declare const operations: {
80
85
  app: z.ZodNullable<z.ZodString>;
81
86
  allowSignup: z.ZodBoolean;
82
87
  permissions: z.ZodArray<z.ZodString>;
88
+ resourceTypes: z.ZodArray<z.ZodObject<{
89
+ type: z.ZodString;
90
+ label: z.ZodString;
91
+ list: z.ZodString;
92
+ }, z.core.$strip>>;
83
93
  resources: z.ZodArray<z.ZodString>;
84
94
  redirectUris: z.ZodArray<z.ZodString>;
85
95
  disabled: z.ZodBoolean;
@@ -106,6 +116,11 @@ export declare const operations: {
106
116
  app: z.ZodNullable<z.ZodString>;
107
117
  allowSignup: z.ZodBoolean;
108
118
  permissions: z.ZodArray<z.ZodString>;
119
+ resourceTypes: z.ZodArray<z.ZodObject<{
120
+ type: z.ZodString;
121
+ label: z.ZodString;
122
+ list: z.ZodString;
123
+ }, z.core.$strip>>;
109
124
  resources: z.ZodArray<z.ZodString>;
110
125
  redirectUris: z.ZodArray<z.ZodString>;
111
126
  disabled: z.ZodBoolean;
@@ -138,6 +153,7 @@ export declare const operations: {
138
153
  };
139
154
  readonly "put /api/v1/apps/{app}/permissions": {
140
155
  readonly summary: "Replace the app's product-permission catalog";
156
+ readonly description: "Wholesale replace of both halves of the catalog: the flat permissions, and the resource types (`{ type, label, list }`) that per-instance grants such as `kirby:thread:<id>` compose under. The IdP GETs each type's `list` URL — with a 60s IdP-signed JWT whose `aud` is that URL — whenever it needs the instances; it never stores them. Omitting `resourceTypes` clears them. 422 `invalid_resource_type` names a `list` URL that is not absolute https (http is allowed for loopback hosts only).";
141
157
  readonly permission: "app:update";
142
158
  readonly params: {
143
159
  readonly app: "Application key (oauth_client.metadata.app).";
@@ -145,10 +161,20 @@ export declare const operations: {
145
161
  readonly notFound: "No application with that app key";
146
162
  readonly input: z.ZodObject<{
147
163
  permissions: z.ZodArray<z.ZodString>;
164
+ resourceTypes: z.ZodDefault<z.ZodArray<z.ZodObject<{
165
+ type: z.ZodString;
166
+ label: z.ZodOptional<z.ZodString>;
167
+ list: z.ZodString;
168
+ }, z.core.$strip>>>;
148
169
  }, z.core.$strip>;
149
170
  readonly successCode: "200";
150
171
  readonly success: z.ZodObject<{
151
172
  permissions: z.ZodArray<z.ZodString>;
173
+ resourceTypes: z.ZodArray<z.ZodObject<{
174
+ type: z.ZodString;
175
+ label: z.ZodString;
176
+ list: z.ZodString;
177
+ }, z.core.$strip>>;
152
178
  }, z.core.$strip>;
153
179
  };
154
180
  readonly "get /api/v1/apps/{app}/keys": {
@@ -421,6 +447,7 @@ export declare const operations: {
421
447
  };
422
448
  readonly "post /api/v1/apps/{app}/user-keys": {
423
449
  readonly summary: "Mint an end-user API key (plaintext returned once)";
450
+ readonly description: "Every scope must be a declared permission or `<declared type>:<id>` for an instance the app currently lists. 422 `unknown_scopes` names scopes the catalog does not declare; 422 `unknown_resource` names `<type>:<id>` grants whose instance the app did not list; 502 `resource_lookup_failed` names the types whose `list` URL could not be read.";
424
451
  readonly permission: "userkey:create";
425
452
  readonly params: {
426
453
  readonly app: "Application key (oauth_client.metadata.app).";
@@ -1 +1 @@
1
- {"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../src/schemas/operations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAsCvB,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEpE,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAEnF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;IAC7B,wEAAwE;IACxE,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAA;IACjB,WAAW,EAAE,KAAK,GAAG,KAAK,CAAA;IAC1B,OAAO,EAAE,CAAC,CAAC,OAAO,CAAA;CACnB,CAAA;AAKD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+O0B,CAAA;AAEjD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAA;AAC1C,MAAM,MAAM,YAAY,GAAG,MAAM,UAAU,CAAA;AAE3C,mEAAmE;AACnE,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,UAAU,IAAI,YAAY,SAAS,MAAM,CAAC,GACrE,CAAC,SAAS,GAAG,CAAC,IAAI,MAAM,CAAC,EAAE,GACzB,CAAC,GACD,KAAK,GACP,KAAK,CAAA;AAET,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,SAAS,YAAY,GAC/F,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GACvB,KAAK,CAAA;AAET,iFAAiF;AACjF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,GAAG,MAAM,IAAI,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GAC5F,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,GAC3B,KAAK,CAAA;AAET,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAE7E"}
1
+ {"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../src/schemas/operations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAsCvB,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEpE,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAEnF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;IAC7B,wEAAwE;IACxE,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAA;IACjB,WAAW,EAAE,KAAK,GAAG,KAAK,CAAA;IAC1B,OAAO,EAAE,CAAC,CAAC,OAAO,CAAA;CACnB,CAAA;AAKD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmP0B,CAAA;AAEjD,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAA;AAC1C,MAAM,MAAM,YAAY,GAAG,MAAM,UAAU,CAAA;AAE3C,mEAAmE;AACnE,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,UAAU,IAAI,YAAY,SAAS,MAAM,CAAC,GACrE,CAAC,SAAS,GAAG,CAAC,IAAI,MAAM,CAAC,EAAE,GACzB,CAAC,GACD,KAAK,GACP,KAAK,CAAA;AAET,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,SAAS,YAAY,GAC/F,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GACvB,KAAK,CAAA;AAET,iFAAiF;AACjF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,GAAG,MAAM,IAAI,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GAC5F,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,GAC3B,KAAK,CAAA;AAET,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAE7E"}
@@ -59,6 +59,7 @@ export const operations = {
59
59
  },
60
60
  "put /api/v1/apps/{app}/permissions": {
61
61
  summary: "Replace the app's product-permission catalog",
62
+ description: "Wholesale replace of both halves of the catalog: the flat permissions, and the resource types (`{ type, label, list }`) that per-instance grants such as `kirby:thread:<id>` compose under. The IdP GETs each type's `list` URL — with a 60s IdP-signed JWT whose `aud` is that URL — whenever it needs the instances; it never stores them. Omitting `resourceTypes` clears them. 422 `invalid_resource_type` names a `list` URL that is not absolute https (http is allowed for loopback hosts only).",
62
63
  permission: "app:update",
63
64
  params: APP_PARAM,
64
65
  notFound: "No application with that app key",
@@ -179,6 +180,7 @@ export const operations = {
179
180
  },
180
181
  "post /api/v1/apps/{app}/user-keys": {
181
182
  summary: "Mint an end-user API key (plaintext returned once)",
183
+ description: "Every scope must be a declared permission or `<declared type>:<id>` for an instance the app currently lists. 422 `unknown_scopes` names scopes the catalog does not declare; 422 `unknown_resource` names `<type>:<id>` grants whose instance the app did not list; 502 `resource_lookup_failed` names the types whose `list` URL could not be read.",
182
184
  permission: "userkey:create",
183
185
  params: APP_PARAM,
184
186
  input: CreateUserApiKeyInput,
@@ -78,6 +78,30 @@
78
78
  },
79
79
  "description": "The app's declared product-permission catalog"
80
80
  },
81
+ "resourceTypes": {
82
+ "type": "array",
83
+ "items": {
84
+ "type": "object",
85
+ "properties": {
86
+ "type": {
87
+ "type": "string"
88
+ },
89
+ "label": {
90
+ "type": "string"
91
+ },
92
+ "list": {
93
+ "type": "string"
94
+ }
95
+ },
96
+ "required": [
97
+ "type",
98
+ "label",
99
+ "list"
100
+ ],
101
+ "additionalProperties": false
102
+ },
103
+ "description": "Declared resource types — permission families over instances the app holds"
104
+ },
81
105
  "resources": {
82
106
  "type": "array",
83
107
  "items": {
@@ -105,6 +129,7 @@
105
129
  "app",
106
130
  "allowSignup",
107
131
  "permissions",
132
+ "resourceTypes",
108
133
  "resources",
109
134
  "redirectUris",
110
135
  "disabled",
@@ -291,6 +316,30 @@
291
316
  },
292
317
  "description": "The app's declared product-permission catalog"
293
318
  },
319
+ "resourceTypes": {
320
+ "type": "array",
321
+ "items": {
322
+ "type": "object",
323
+ "properties": {
324
+ "type": {
325
+ "type": "string"
326
+ },
327
+ "label": {
328
+ "type": "string"
329
+ },
330
+ "list": {
331
+ "type": "string"
332
+ }
333
+ },
334
+ "required": [
335
+ "type",
336
+ "label",
337
+ "list"
338
+ ],
339
+ "additionalProperties": false
340
+ },
341
+ "description": "Declared resource types — permission families over instances the app holds"
342
+ },
294
343
  "resources": {
295
344
  "type": "array",
296
345
  "items": {
@@ -318,6 +367,7 @@
318
367
  "app",
319
368
  "allowSignup",
320
369
  "permissions",
370
+ "resourceTypes",
321
371
  "resources",
322
372
  "redirectUris",
323
373
  "disabled",
@@ -438,6 +488,30 @@
438
488
  },
439
489
  "description": "The app's declared product-permission catalog"
440
490
  },
491
+ "resourceTypes": {
492
+ "type": "array",
493
+ "items": {
494
+ "type": "object",
495
+ "properties": {
496
+ "type": {
497
+ "type": "string"
498
+ },
499
+ "label": {
500
+ "type": "string"
501
+ },
502
+ "list": {
503
+ "type": "string"
504
+ }
505
+ },
506
+ "required": [
507
+ "type",
508
+ "label",
509
+ "list"
510
+ ],
511
+ "additionalProperties": false
512
+ },
513
+ "description": "Declared resource types — permission families over instances the app holds"
514
+ },
441
515
  "resources": {
442
516
  "type": "array",
443
517
  "items": {
@@ -465,6 +539,7 @@
465
539
  "app",
466
540
  "allowSignup",
467
541
  "permissions",
542
+ "resourceTypes",
468
543
  "resources",
469
544
  "redirectUris",
470
545
  "disabled",
@@ -617,7 +692,7 @@
617
692
  "/api/v1/apps/{app}/permissions": {
618
693
  "put": {
619
694
  "summary": "Replace the app's product-permission catalog",
620
- "description": "Requires `app:update` on the path app (or an admin key).",
695
+ "description": "Wholesale replace of both halves of the catalog: the flat permissions, and the resource types (`{ type, label, list }`) that per-instance grants such as `kirby:thread:<id>` compose under. The IdP GETs each type's `list` URL — with a 60s IdP-signed JWT whose `aud` is that URL — whenever it needs the instances; it never stores them. Omitting `resourceTypes` clears them. 422 `invalid_resource_type` names a `list` URL that is not absolute https (http is allowed for loopback hosts only).",
621
696
  "security": [
622
697
  {
623
698
  "bearerAuth": []
@@ -648,6 +723,34 @@
648
723
  "type": "string",
649
724
  "minLength": 1
650
725
  }
726
+ },
727
+ "resourceTypes": {
728
+ "default": [],
729
+ "type": "array",
730
+ "items": {
731
+ "type": "object",
732
+ "properties": {
733
+ "type": {
734
+ "type": "string",
735
+ "pattern": "^[a-z0-9_-]+(:[a-z0-9_-]+)*$",
736
+ "description": "Permission prefix a grant composes under: `kirby:thread` → `kirby:thread:<id>`"
737
+ },
738
+ "label": {
739
+ "description": "Human name for one instance, shown in the console — “WhatsApp conversation”",
740
+ "type": "string",
741
+ "minLength": 1
742
+ },
743
+ "list": {
744
+ "type": "string",
745
+ "format": "uri",
746
+ "description": "Absolute URL the IdP GETs to enumerate instances. Called with an IdP-signed bearer JWT (aud = this URL); must answer `ResourceListSchema`"
747
+ }
748
+ },
749
+ "required": [
750
+ "type",
751
+ "list"
752
+ ]
753
+ }
651
754
  }
652
755
  },
653
756
  "required": [
@@ -671,10 +774,34 @@
671
774
  "items": {
672
775
  "type": "string"
673
776
  }
777
+ },
778
+ "resourceTypes": {
779
+ "type": "array",
780
+ "items": {
781
+ "type": "object",
782
+ "properties": {
783
+ "type": {
784
+ "type": "string"
785
+ },
786
+ "label": {
787
+ "type": "string"
788
+ },
789
+ "list": {
790
+ "type": "string"
791
+ }
792
+ },
793
+ "required": [
794
+ "type",
795
+ "label",
796
+ "list"
797
+ ],
798
+ "additionalProperties": false
799
+ }
674
800
  }
675
801
  },
676
802
  "required": [
677
- "permissions"
803
+ "permissions",
804
+ "resourceTypes"
678
805
  ],
679
806
  "additionalProperties": false
680
807
  }
@@ -2055,7 +2182,7 @@
2055
2182
  },
2056
2183
  "post": {
2057
2184
  "summary": "Mint an end-user API key (plaintext returned once)",
2058
- "description": "Requires `userkey:create` on the path app (or an admin key).",
2185
+ "description": "Every scope must be a declared permission or `<declared type>:<id>` for an instance the app currently lists. 422 `unknown_scopes` names scopes the catalog does not declare; 422 `unknown_resource` names `<type>:<id>` grants whose instance the app did not list; 502 `resource_lookup_failed` names the types whose `list` URL could not be read.",
2059
2186
  "security": [
2060
2187
  {
2061
2188
  "bearerAuth": []
@@ -2090,7 +2217,7 @@
2090
2217
  },
2091
2218
  "scopes": {
2092
2219
  "default": [],
2093
- "description": "Subset of the app's product permission catalog",
2220
+ "description": "Declared permissions, or `<type>:<id>` grants over a declared resource type whose instance the app currently lists",
2094
2221
  "type": "array",
2095
2222
  "items": {
2096
2223
  "type": "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willyim/idp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Login for apps that don't own identity \u2014 OIDC client, server sessions, and react-router guards against the willy.im IdP",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",