@willyim/idp 0.5.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
@@ -82,7 +82,8 @@ return requestHandler(request, {
82
82
  })
83
83
  ```
84
84
 
85
- **`app/routes/auth.$.tsx`** — serves `/auth/login`, `/auth/callback`, `/auth/logout`:
85
+ **`app/routes/auth.$.tsx`** — serves `/auth/login`, `/auth/callback`, `/auth/logout`
86
+ and `/auth/me`:
86
87
 
87
88
  ```ts
88
89
  import { createAuthRoute } from "@willyim/idp/react-router"
@@ -110,6 +111,17 @@ export async function loader(args: Route.LoaderArgs) {
110
111
  }
111
112
  ```
112
113
 
114
+ For code that can't reach a loader — a client-only route, a widget mounted
115
+ outside the router, a fetch from a worker — the same route answers `/auth/me`:
116
+
117
+ ```ts
118
+ const { user } = await fetch("/auth/me").then((r) => r.json())
119
+ // 200 { user: PublicSession } · 401 { user: null }
120
+ ```
121
+
122
+ A server-rendered page should keep using `requireSession`: `/auth/me` is the
123
+ same data, one network hop later.
124
+
113
125
  ## Env
114
126
 
115
127
  | Var | Purpose |
@@ -203,7 +215,21 @@ type Session = {
203
215
  }
204
216
  ```
205
217
 
206
- `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)).
221
+
222
+ `image` is effectively always set against the willy.im IdP: it renders a
223
+ deterministic [blobatar](https://blobatar.dev) for anyone who never uploaded a
224
+ picture, so `<img src={session.image} />` needs no fallback of its own. The type
225
+ stays nullable for other issuers. Show `session.name || session.email` — a name
226
+ is a person, an email is a login.
227
+
228
+ ### `publicSession(session)`
229
+
230
+ `Session` as JSON, without the methods and without the session id — the shape
231
+ that is safe to hand a browser. This is what `/auth/me` (below) returns, and
232
+ what your own `/me` should.
207
233
 
208
234
  ### `createIdpClient(options)` — the OIDC relying party on its own
209
235
 
@@ -297,7 +323,9 @@ const keys = createUserKeys({
297
323
  const minted = await keys.create({
298
324
  userId: session.userId,
299
325
  name: "cli",
300
- 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"],
301
329
  workspaceId: session.workspaceId,
302
330
  })
303
331
 
@@ -323,6 +351,61 @@ ingest token, say — identifies a site rather than a user, cannot be kept secre
323
351
  and must not pay a round trip per hit. Keep those in the app's own table and
324
352
  gate them on `Origin` plus rate limiting.
325
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
+
326
409
  ## Linked identities
327
410
 
328
411
  A user's ids on *other* systems — their Slack member id, their WhatsApp number,
@@ -21,7 +21,7 @@ export declare const DEFAULT_SCOPES: readonly IdpScope[];
21
21
  export type IdpClientOptions = {
22
22
  /**
23
23
  * The OIDC issuer, including the basepath the IdP is mounted on:
24
- * `https://idp.willy.im/auth` (or a vanity domain, `https://idp.kasso.do/auth`).
24
+ * `https://idp.willy.im/auth` (or a vanity domain, `https://idp.app1.com/auth`).
25
25
  * Discovery is `${issuer}/.well-known/openid-configuration`.
26
26
  */
27
27
  issuer: string;
@@ -13,10 +13,11 @@
13
13
  * @willyim/idp/react-router the auth route and the loader guards
14
14
  */
15
15
  export { createIdpClient, createPkce, DEFAULT_SCOPES, IdpError, SUPPORTED_SCOPES, type AuthorizationUrlInput, type Discovery, type IdpClient, type IdpClientOptions, type IdpScope, type Tokens, } from "./client.js";
16
- export { createIdp, DEFAULT_SESSION_COOKIE, safeNext, type Idp, type IdpOptions, type Session, type SessionOptions, } from "./session.js";
16
+ export { createIdp, DEFAULT_SESSION_COOKIE, publicSession, safeNext, type Idp, type IdpOptions, type PublicSession, type Session, type SessionOptions, } from "./session.js";
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,QAAQ,EACR,KAAK,GAAG,EACR,KAAK,UAAU,EACf,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
@@ -13,10 +13,13 @@
13
13
  * @willyim/idp/react-router the auth route and the loader guards
14
14
  */
15
15
  export { createIdpClient, createPkce, DEFAULT_SCOPES, IdpError, SUPPORTED_SCOPES, } from "./client.js";
16
- export { createIdp, DEFAULT_SESSION_COOKIE, safeNext, } from "./session.js";
16
+ export { createIdp, DEFAULT_SESSION_COOKIE, publicSession, safeNext, } from "./session.js";
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";
@@ -9,6 +9,9 @@
9
9
  * const session = await requireSession(args)
10
10
  * const session = await requirePermission(args, "admin")
11
11
  *
12
+ * // from the browser, when there's no loader to read
13
+ * await fetch("/auth/me").then((r) => r.json()) // { user: PublicSession | null }
14
+ *
12
15
  * No react-router import: the adapter only ever produces `Response`s and
13
16
  * functions, both of which react-router already understands. That keeps the
14
17
  * subpath dependency-free too, and makes it usable from any framework whose
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react-router/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAGjD,4EAA4E;AAC5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAC5C,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;AAEjE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAA;CAC3C,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,gBAAqB;mBA+ChE,SAAS;mBACT,SAAS;EAE3B;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,GAAG,CAAA;IACT,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oCAAoC;IACpC,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,kFAAkF;AAClF,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAEzB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,OAAO,CAAC,CAMlB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react-router/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAGjD,4EAA4E;AAC5E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAC5C,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;AAEjE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAA;CAC3C,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,gBAAqB;mBAmEhE,SAAS;mBACT,SAAS;EAE3B;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,GAAG,CAAA;IACT,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oCAAoC;IACpC,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,kFAAkF;AAClF,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAEzB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,EACf,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,OAAO,CAAC,CAMlB"}
@@ -9,12 +9,15 @@
9
9
  * const session = await requireSession(args)
10
10
  * const session = await requirePermission(args, "admin")
11
11
  *
12
+ * // from the browser, when there's no loader to read
13
+ * await fetch("/auth/me").then((r) => r.json()) // { user: PublicSession | null }
14
+ *
12
15
  * No react-router import: the adapter only ever produces `Response`s and
13
16
  * functions, both of which react-router already understands. That keeps the
14
17
  * subpath dependency-free too, and makes it usable from any framework whose
15
18
  * handlers take a `Request`.
16
19
  */
17
- import { safeNext } from "../session.js";
20
+ import { publicSession, safeNext } from "../session.js";
18
21
  /**
19
22
  * Serves `/auth/login`, `/auth/callback` and `/auth/logout` from a single splat
20
23
  * route. `loader` covers the GET navigations; `action` covers a POSTed logout
@@ -57,6 +60,26 @@ export function createAuthRoute(getIdp, options = {}) {
57
60
  // outlives the app's.
58
61
  return redirect(endSession ?? redirectTo, headers);
59
62
  }
63
+ // The session as JSON, for the code that can't reach a loader: a client-only
64
+ // route, a widget mounted outside the router, a fetch from a worker. A
65
+ // server-rendered page should keep reading `requireSession` — this is the
66
+ // same data one network hop later.
67
+ if (action === "me") {
68
+ const session = await idp.getSession(args.request);
69
+ const headers = new Headers({
70
+ "content-type": "application/json",
71
+ // Per-user and revocable — never let a shared cache hold it.
72
+ "cache-control": "no-store, private",
73
+ });
74
+ if (!session) {
75
+ return new Response(JSON.stringify({ user: null }), { status: 401, headers });
76
+ }
77
+ // Polling this keeps a sliding session alive, same as any other request.
78
+ const renewed = session.renewCookie();
79
+ if (renewed)
80
+ headers.append("set-cookie", renewed);
81
+ return new Response(JSON.stringify({ user: publicSession(session) }), { status: 200, headers });
82
+ }
60
83
  return new Response("not found", { status: 404 });
61
84
  }
62
85
  return {
@@ -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;
@@ -25,6 +66,7 @@ export declare const UserSchema: z.ZodObject<{
25
66
  id: z.ZodString;
26
67
  email: z.ZodString;
27
68
  name: z.ZodNullable<z.ZodString>;
69
+ image: z.ZodString;
28
70
  emailVerified: z.ZodBoolean;
29
71
  createdAt: z.ZodString;
30
72
  }, z.core.$strip>;
@@ -42,6 +84,11 @@ export declare const ApplicationListSchema: z.ZodObject<{
42
84
  app: z.ZodNullable<z.ZodString>;
43
85
  allowSignup: z.ZodBoolean;
44
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>>;
45
92
  resources: z.ZodArray<z.ZodString>;
46
93
  redirectUris: z.ZodArray<z.ZodString>;
47
94
  disabled: z.ZodBoolean;
@@ -73,18 +120,33 @@ export declare const UpdateApplicationInput: z.ZodObject<{
73
120
  export declare const ClientSecretSchema: z.ZodObject<{
74
121
  clientSecret: z.ZodString;
75
122
  }, z.core.$strip>;
76
- /** 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
+ */
77
128
  export declare const SetAppPermissionsInput: z.ZodObject<{
78
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>>>;
79
135
  }, z.core.$strip>;
80
136
  export declare const AppPermissionsSchema: z.ZodObject<{
81
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>>;
82
143
  }, z.core.$strip>;
83
144
  export declare const UserListSchema: z.ZodObject<{
84
145
  users: z.ZodArray<z.ZodObject<{
85
146
  id: z.ZodString;
86
147
  email: z.ZodString;
87
148
  name: z.ZodNullable<z.ZodString>;
149
+ image: z.ZodString;
88
150
  emailVerified: z.ZodBoolean;
89
151
  createdAt: z.ZodString;
90
152
  }, z.core.$strip>>;
@@ -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;;;;;;iBAMrB,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"),
@@ -27,6 +75,9 @@ export const UserSchema = z.object({
27
75
  id: z.string(),
28
76
  email: z.string(),
29
77
  name: z.string().nullable(),
78
+ image: z
79
+ .string()
80
+ .describe("Avatar URL — the user's own picture, or the blobatar the IdP renders for them"),
30
81
  emailVerified: z.boolean(),
31
82
  createdAt: z.string(),
32
83
  });
@@ -74,11 +125,19 @@ export const UpdateApplicationInput = z.object({
74
125
  export const ClientSecretSchema = z.object({
75
126
  clientSecret: z.string().describe("Plaintext client secret — shown exactly once, never stored"),
76
127
  });
77
- /** 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
+ */
78
133
  export const SetAppPermissionsInput = z.object({
79
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),
80
140
  });
81
- export const AppPermissionsSchema = z.object({ permissions: z.array(z.string()) });
82
141
  export const UserListSchema = z.object({ users: z.array(UserSchema) });
83
142
  export const WorkspaceListSchema = z.object({ workspaces: z.array(WorkspaceSchema) });
84
143
  // --- Write management API (scoped-key authenticated, per-app) ---
@@ -135,7 +194,10 @@ export const UserApiKeyListSchema = z.object({ keys: z.array(UserApiKeySchema) }
135
194
  export const CreateUserApiKeyInput = z.object({
136
195
  userId: z.string().min(1),
137
196
  name: z.string().min(1),
138
- 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"),
139
201
  workspaceId: z.string().optional(),
140
202
  expiresAt: z.iso.datetime().optional().describe("ISO 8601; omit for non-expiring"),
141
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": {
@@ -262,6 +288,7 @@ export declare const operations: {
262
288
  id: z.ZodString;
263
289
  email: z.ZodString;
264
290
  name: z.ZodNullable<z.ZodString>;
291
+ image: z.ZodString;
265
292
  emailVerified: z.ZodBoolean;
266
293
  createdAt: z.ZodString;
267
294
  }, z.core.$strip>>;
@@ -420,6 +447,7 @@ export declare const operations: {
420
447
  };
421
448
  readonly "post /api/v1/apps/{app}/user-keys": {
422
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.";
423
451
  readonly permission: "userkey:create";
424
452
  readonly params: {
425
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,
@@ -64,6 +64,26 @@ export type Session = {
64
64
  */
65
65
  renewCookie(): string | null;
66
66
  };
67
+ /**
68
+ * A `Session` as JSON — the shape that is safe to hand a browser.
69
+ *
70
+ * Two deliberate omissions. The methods are gone because they don't serialize.
71
+ * The session *id* is gone because it is the thing the signed cookie resolves
72
+ * to: it never needs to be in reach of script, and a projection that leaves it
73
+ * out can't leak it by accident later.
74
+ */
75
+ export type PublicSession = {
76
+ sub: string;
77
+ email: string;
78
+ name: string | null;
79
+ image: string | null;
80
+ permissions: string[];
81
+ workspaces: Workspace[];
82
+ actor: Actor | null;
83
+ expiresAt: string;
84
+ };
85
+ /** `Session` -> `PublicSession`. What `/auth/me` returns, and what an app's own `/me` should. */
86
+ export declare function publicSession(session: Session): PublicSession;
67
87
  export type Idp = ReturnType<typeof createIdp>;
68
88
  export declare function createIdp(options: IdpOptions): {
69
89
  /** The Layer 0 client, for anything the session layer doesn't wrap. */
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE3D,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAA;AAQpB,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,YAAY,CAAA;AAE7D,eAAO,MAAM,sBAAsB,gBAAgB,CAAA;AAKnD,MAAM,MAAM,cAAc,GAAG;IAC3B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,SAAS,CAAC,EAAE,QAAQ,CAAA;IACpB,wFAAwF;IACxF,SAAS,CAAC,EAAE,QAAQ,CAAA;IACpB,wFAAwF;IACxF,SAAS,CAAC,EAAE,QAAQ,CAAA;IACpB,wEAAwE;IACxE,iBAAiB,CAAC,EAAE,QAAQ,CAAA;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,gBAAgB,GAAG;IAC1C,QAAQ,EAAE,YAAY,CAAA;IACtB,OAAO,EAAE,cAAc,CAAA;IACvB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,sFAAsF;IACtF,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;IACf,iEAAiE;IACjE,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC;;;;OAIG;IACH,WAAW,IAAI,MAAM,GAAG,IAAI,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAA;AAe9C,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU;IAqNzC,uEAAuE;;;;;;;;;;;;;;;;;;;;;;;;;;mBA7GhD,CAAC;sBAEZ,CAAC;;;IA8Gb;;;OAGG;sBACqB;QACtB,WAAW,EAAE,MAAM,CAAA;QACnB,+EAA+E;QAC/E,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAuB9C;;;OAGG;2BAEQ,OAAO,SACT;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,GAC7B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAmEhE;;;OAGG;wBACuB,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAuB3D;;;OAGG;8BAtJkC,OAAO,KAAG,OAAO,CAAC,OAAO,CAAC;IAyJ/D,gFAAgF;4BAClD,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpD;;;;;;;;OAQG;oBAEQ,OAAO,UACT;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAClD,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;EAWvD;AAMD,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAI5E"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,19 @@ import { parseDuration } from "./duration.js";
17
17
  export const DEFAULT_SESSION_COOKIE = "idp_session";
18
18
  /** How long the login handshake may take before its state cookie is stale. */
19
19
  const STATE_COOKIE_MAX_AGE_MS = 10 * 60_000;
20
+ /** `Session` -> `PublicSession`. What `/auth/me` returns, and what an app's own `/me` should. */
21
+ export function publicSession(session) {
22
+ return {
23
+ sub: session.sub,
24
+ email: session.email,
25
+ name: session.name,
26
+ image: session.image,
27
+ permissions: session.permissions,
28
+ workspaces: session.workspaces,
29
+ actor: session.actor,
30
+ expiresAt: session.expiresAt.toISOString(),
31
+ };
32
+ }
20
33
  /**
21
34
  * What rides in the login state cookie. HMAC-signed, so tampering is caught
22
35
  * before this ever parses — the schema is here to catch a cookie left over
@@ -78,6 +78,37 @@
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
+ },
105
+ "resources": {
106
+ "type": "array",
107
+ "items": {
108
+ "type": "string"
109
+ },
110
+ "description": "Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"
111
+ },
81
112
  "redirectUris": {
82
113
  "type": "array",
83
114
  "items": {
@@ -98,6 +129,8 @@
98
129
  "app",
99
130
  "allowSignup",
100
131
  "permissions",
132
+ "resourceTypes",
133
+ "resources",
101
134
  "redirectUris",
102
135
  "disabled",
103
136
  "createdAt"
@@ -283,6 +316,37 @@
283
316
  },
284
317
  "description": "The app's declared product-permission catalog"
285
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
+ },
343
+ "resources": {
344
+ "type": "array",
345
+ "items": {
346
+ "type": "string"
347
+ },
348
+ "description": "Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"
349
+ },
286
350
  "redirectUris": {
287
351
  "type": "array",
288
352
  "items": {
@@ -303,6 +367,8 @@
303
367
  "app",
304
368
  "allowSignup",
305
369
  "permissions",
370
+ "resourceTypes",
371
+ "resources",
306
372
  "redirectUris",
307
373
  "disabled",
308
374
  "createdAt"
@@ -364,6 +430,14 @@
364
430
  },
365
431
  "allowSignup": {
366
432
  "type": "boolean"
433
+ },
434
+ "resources": {
435
+ "description": "Replace the app's protected resource URIs — absolute https, no fragment",
436
+ "type": "array",
437
+ "items": {
438
+ "type": "string",
439
+ "format": "uri"
440
+ }
367
441
  }
368
442
  }
369
443
  }
@@ -414,6 +488,37 @@
414
488
  },
415
489
  "description": "The app's declared product-permission catalog"
416
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
+ },
515
+ "resources": {
516
+ "type": "array",
517
+ "items": {
518
+ "type": "string"
519
+ },
520
+ "description": "Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"
521
+ },
417
522
  "redirectUris": {
418
523
  "type": "array",
419
524
  "items": {
@@ -434,6 +539,8 @@
434
539
  "app",
435
540
  "allowSignup",
436
541
  "permissions",
542
+ "resourceTypes",
543
+ "resources",
437
544
  "redirectUris",
438
545
  "disabled",
439
546
  "createdAt"
@@ -585,7 +692,7 @@
585
692
  "/api/v1/apps/{app}/permissions": {
586
693
  "put": {
587
694
  "summary": "Replace the app's product-permission catalog",
588
- "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).",
589
696
  "security": [
590
697
  {
591
698
  "bearerAuth": []
@@ -616,6 +723,34 @@
616
723
  "type": "string",
617
724
  "minLength": 1
618
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
+ }
619
754
  }
620
755
  },
621
756
  "required": [
@@ -639,10 +774,34 @@
639
774
  "items": {
640
775
  "type": "string"
641
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
+ }
642
800
  }
643
801
  },
644
802
  "required": [
645
- "permissions"
803
+ "permissions",
804
+ "resourceTypes"
646
805
  ],
647
806
  "additionalProperties": false
648
807
  }
@@ -1242,6 +1401,10 @@
1242
1401
  }
1243
1402
  ]
1244
1403
  },
1404
+ "image": {
1405
+ "type": "string",
1406
+ "description": "Avatar URL — the user's own picture, or the blobatar the IdP renders for them"
1407
+ },
1245
1408
  "emailVerified": {
1246
1409
  "type": "boolean"
1247
1410
  },
@@ -1253,6 +1416,7 @@
1253
1416
  "id",
1254
1417
  "email",
1255
1418
  "name",
1419
+ "image",
1256
1420
  "emailVerified",
1257
1421
  "createdAt"
1258
1422
  ],
@@ -2018,7 +2182,7 @@
2018
2182
  },
2019
2183
  "post": {
2020
2184
  "summary": "Mint an end-user API key (plaintext returned once)",
2021
- "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.",
2022
2186
  "security": [
2023
2187
  {
2024
2188
  "bearerAuth": []
@@ -2053,7 +2217,7 @@
2053
2217
  },
2054
2218
  "scopes": {
2055
2219
  "default": [],
2056
- "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",
2057
2221
  "type": "array",
2058
2222
  "items": {
2059
2223
  "type": "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willyim/idp",
3
- "version": "0.5.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",