@willyim/idp 0.4.0 → 0.6.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 +25 -1
- package/dist/src/client.d.ts +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/react-router/index.d.ts +3 -0
- package/dist/src/react-router/index.d.ts.map +1 -1
- package/dist/src/react-router/index.js +24 -1
- package/dist/src/resource-server.d.ts +90 -0
- package/dist/src/resource-server.d.ts.map +1 -0
- package/dist/src/resource-server.js +206 -0
- package/dist/src/schemas/index.d.ts +5 -0
- package/dist/src/schemas/index.d.ts.map +1 -1
- package/dist/src/schemas/index.js +10 -0
- package/dist/src/schemas/operations.d.ts +5 -0
- package/dist/src/schemas/operations.d.ts.map +1 -1
- package/dist/src/session.d.ts +20 -0
- package/dist/src/session.d.ts.map +1 -1
- package/dist/src/session.js +13 -0
- package/openapi/idp-api.json +37 -0
- package/package.json +1 -1
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 |
|
|
@@ -205,6 +217,18 @@ type Session = {
|
|
|
205
217
|
|
|
206
218
|
`can()` matches exactly, and honours `resource:*` and `*` grants.
|
|
207
219
|
|
|
220
|
+
`image` is effectively always set against the willy.im IdP: it renders a
|
|
221
|
+
deterministic [blobatar](https://blobatar.dev) for anyone who never uploaded a
|
|
222
|
+
picture, so `<img src={session.image} />` needs no fallback of its own. The type
|
|
223
|
+
stays nullable for other issuers. Show `session.name || session.email` — a name
|
|
224
|
+
is a person, an email is a login.
|
|
225
|
+
|
|
226
|
+
### `publicSession(session)`
|
|
227
|
+
|
|
228
|
+
`Session` as JSON, without the methods and without the session id — the shape
|
|
229
|
+
that is safe to hand a browser. This is what `/auth/me` (below) returns, and
|
|
230
|
+
what your own `/me` should.
|
|
231
|
+
|
|
208
232
|
### `createIdpClient(options)` — the OIDC relying party on its own
|
|
209
233
|
|
|
210
234
|
```ts
|
package/dist/src/client.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -13,12 +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, 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
20
|
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
21
|
export { createIdentities, type Identities, type IdentitiesOptions, type IdentityCacheOptions, type ResolvedIdentity, type IdentityResolution, } from "./identities.js";
|
|
22
|
+
export { createResourceServer, type ResourceServer, type ResourceServerOptions, type VerifiedAccessToken, type AuthResult, } from "./resource-server.js";
|
|
22
23
|
export { parseDuration, type Duration } from "./duration.js";
|
|
23
24
|
export { clearCookie, parseCookies, readCookie, serializeCookie, type CookieOptions, } from "./cookie.js";
|
|
24
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -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,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;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"}
|
package/dist/src/index.js
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
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
20
|
export { createUserKeys, readApiKey, } from "./user-keys.js";
|
|
21
21
|
export { createIdentities, } from "./identities.js";
|
|
22
|
+
export { createResourceServer, } from "./resource-server.js";
|
|
22
23
|
export { parseDuration } from "./duration.js";
|
|
23
24
|
export { clearCookie, parseCookies, readCookie, serializeCookie, } from "./cookie.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
|
|
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 {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Being an OAuth 2.1 resource server against the willy.im IdP — which is what
|
|
3
|
+
* an MCP server is.
|
|
4
|
+
*
|
|
5
|
+
* The MCP authorization spec is plain OAuth with three discovery hops, and
|
|
6
|
+
* every one of them is either served by the IdP already or is a static
|
|
7
|
+
* document this module writes for you:
|
|
8
|
+
*
|
|
9
|
+
* 1. The client hits the resource without a token and gets a 401 carrying
|
|
10
|
+
* `WWW-Authenticate: Bearer resource_metadata="…"` → `challenge()`
|
|
11
|
+
* 2. It fetches that URL, the Protected Resource Metadata (RFC 9728), which
|
|
12
|
+
* names the authorization server → `metadata()`
|
|
13
|
+
* 3. It fetches the AS metadata (RFC 8414) from the IdP, registers itself
|
|
14
|
+
* (RFC 7591), runs authorization code + PKCE with `resource=` (RFC 8707),
|
|
15
|
+
* and comes back with a JWT access token whose `aud` is this resource.
|
|
16
|
+
* 4. The resource server verifies that JWT against the IdP's JWKS and reads
|
|
17
|
+
* the permissions claim → `authenticate()`
|
|
18
|
+
*
|
|
19
|
+
* Step 4 is the only one with any real code, and it is deliberately small:
|
|
20
|
+
* signature (EdDSA/Ed25519, ES256 or RS256 via WebCrypto — no jose, no deps),
|
|
21
|
+
* issuer, audience, time. The permissions on the token were computed by the
|
|
22
|
+
* IdP for the app that owns this resource (the same way a browser session's
|
|
23
|
+
* are), so the app looks nothing up — it reads a claim and matches it with
|
|
24
|
+
* `grants()` like everywhere else.
|
|
25
|
+
*
|
|
26
|
+
* The JWKS is cached and refetched once on an unknown `kid`, which is how a key
|
|
27
|
+
* rotation at the IdP is picked up without a restart.
|
|
28
|
+
*/
|
|
29
|
+
export declare const PERMISSIONS_CLAIM = "https://willy.im/permissions";
|
|
30
|
+
export declare const APP_CLAIM = "https://willy.im/app";
|
|
31
|
+
export type ResourceServerOptions = {
|
|
32
|
+
/** The IdP's OAuth issuer — `https://idp.willy.im/auth` (note the /auth). */
|
|
33
|
+
issuer: string;
|
|
34
|
+
/**
|
|
35
|
+
* This server's canonical URI, exactly as registered on the application in
|
|
36
|
+
* the IdP (`resources`) — e.g. `https://bender.romo.fyi/mcp`. It is the
|
|
37
|
+
* audience a token must carry, compared byte for byte.
|
|
38
|
+
*/
|
|
39
|
+
resource: string;
|
|
40
|
+
/** Defaults to `${issuer}/jwks`. */
|
|
41
|
+
jwksUrl?: string;
|
|
42
|
+
/** Advertised in the metadata document. Informational only. */
|
|
43
|
+
scopesSupported?: string[];
|
|
44
|
+
/** How long a fetched JWKS is reused. Default 10 minutes. */
|
|
45
|
+
jwksTtlMs?: number;
|
|
46
|
+
/** Clock skew tolerated on exp/nbf. Default 60s. */
|
|
47
|
+
leewaySeconds?: number;
|
|
48
|
+
fetch?: typeof fetch;
|
|
49
|
+
/** Clock seam, for tests. */
|
|
50
|
+
now?: () => number;
|
|
51
|
+
};
|
|
52
|
+
/** A verified access token, reduced to what an app acts on. */
|
|
53
|
+
export type VerifiedAccessToken = {
|
|
54
|
+
/** The IdP user id — the same `sub` a session or a wak_ key resolves to. */
|
|
55
|
+
sub: string;
|
|
56
|
+
/** The app the permissions are for (the owner of `resource`), when present. */
|
|
57
|
+
app: string | null;
|
|
58
|
+
/** The user's product permissions for that app. Match with `has()`/`grants()`. */
|
|
59
|
+
permissions: string[];
|
|
60
|
+
/** Space-delimited OAuth scopes on the token (openid/profile/…), if any. */
|
|
61
|
+
scopes: string[];
|
|
62
|
+
/** Everything else on the token, for a caller that needs a raw claim. */
|
|
63
|
+
claims: Record<string, unknown>;
|
|
64
|
+
};
|
|
65
|
+
export type AuthResult = {
|
|
66
|
+
ok: true;
|
|
67
|
+
token: VerifiedAccessToken;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
status: 401 | 403;
|
|
71
|
+
error: string;
|
|
72
|
+
description: string;
|
|
73
|
+
};
|
|
74
|
+
export declare function createResourceServer(options: ResourceServerOptions): {
|
|
75
|
+
verify: (token: string) => Promise<AuthResult>;
|
|
76
|
+
authenticate: (request: {
|
|
77
|
+
headers: {
|
|
78
|
+
get(name: string): string | null;
|
|
79
|
+
};
|
|
80
|
+
}, init?: {
|
|
81
|
+
permissions?: string[];
|
|
82
|
+
}) => Promise<AuthResult>;
|
|
83
|
+
challenge: (error?: {
|
|
84
|
+
error: string;
|
|
85
|
+
description: string;
|
|
86
|
+
}) => string;
|
|
87
|
+
metadata: () => Record<string, unknown>;
|
|
88
|
+
};
|
|
89
|
+
export type ResourceServer = ReturnType<typeof createResourceServer>;
|
|
90
|
+
//# sourceMappingURL=resource-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-server.d.ts","sourceRoot":"","sources":["../../src/resource-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,eAAO,MAAM,iBAAiB,iCAAiC,CAAA;AAC/D,eAAO,MAAM,SAAS,yBAAyB,CAAA;AAE/C,MAAM,MAAM,qBAAqB,GAAG;IAClC,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED,+DAA+D;AAC/D,MAAM,MAAM,mBAAmB,GAAG;IAChC,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAA;IACX,+EAA+E;IAC/E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,kFAAkF;IAClF,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAA;AAgDxE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB;oBAiDpC,MAAM,KAAG,OAAO,CAAC,UAAU,CAAC;4BA8D9C;QAAE,OAAO,EAAE;YAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;SAAE,CAAA;KAAE,SACpD;QAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,KAC/B,OAAO,CAAC,UAAU,CAAC;wBAwBK;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,KAAG,MAAM;oBAarD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;EAU7C;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAA"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Being an OAuth 2.1 resource server against the willy.im IdP — which is what
|
|
3
|
+
* an MCP server is.
|
|
4
|
+
*
|
|
5
|
+
* The MCP authorization spec is plain OAuth with three discovery hops, and
|
|
6
|
+
* every one of them is either served by the IdP already or is a static
|
|
7
|
+
* document this module writes for you:
|
|
8
|
+
*
|
|
9
|
+
* 1. The client hits the resource without a token and gets a 401 carrying
|
|
10
|
+
* `WWW-Authenticate: Bearer resource_metadata="…"` → `challenge()`
|
|
11
|
+
* 2. It fetches that URL, the Protected Resource Metadata (RFC 9728), which
|
|
12
|
+
* names the authorization server → `metadata()`
|
|
13
|
+
* 3. It fetches the AS metadata (RFC 8414) from the IdP, registers itself
|
|
14
|
+
* (RFC 7591), runs authorization code + PKCE with `resource=` (RFC 8707),
|
|
15
|
+
* and comes back with a JWT access token whose `aud` is this resource.
|
|
16
|
+
* 4. The resource server verifies that JWT against the IdP's JWKS and reads
|
|
17
|
+
* the permissions claim → `authenticate()`
|
|
18
|
+
*
|
|
19
|
+
* Step 4 is the only one with any real code, and it is deliberately small:
|
|
20
|
+
* signature (EdDSA/Ed25519, ES256 or RS256 via WebCrypto — no jose, no deps),
|
|
21
|
+
* issuer, audience, time. The permissions on the token were computed by the
|
|
22
|
+
* IdP for the app that owns this resource (the same way a browser session's
|
|
23
|
+
* are), so the app looks nothing up — it reads a claim and matches it with
|
|
24
|
+
* `grants()` like everywhere else.
|
|
25
|
+
*
|
|
26
|
+
* The JWKS is cached and refetched once on an unknown `kid`, which is how a key
|
|
27
|
+
* rotation at the IdP is picked up without a restart.
|
|
28
|
+
*/
|
|
29
|
+
import { grants } from "./claims.js";
|
|
30
|
+
export const PERMISSIONS_CLAIM = "https://willy.im/permissions";
|
|
31
|
+
export const APP_CLAIM = "https://willy.im/app";
|
|
32
|
+
const DEFAULT_JWKS_TTL_MS = 600_000;
|
|
33
|
+
const DEFAULT_LEEWAY_S = 60;
|
|
34
|
+
function b64urlToBytes(s) {
|
|
35
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
|
|
36
|
+
const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
|
|
37
|
+
const out = new Uint8Array(b.length);
|
|
38
|
+
for (let i = 0; i < b.length; i++)
|
|
39
|
+
out[i] = b.charCodeAt(i);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function decodeJson(segment) {
|
|
43
|
+
return JSON.parse(new TextDecoder().decode(b64urlToBytes(segment)));
|
|
44
|
+
}
|
|
45
|
+
/** The WebCrypto import + verify params for the algorithms the IdP may sign with. */
|
|
46
|
+
function algParams(alg, jwk) {
|
|
47
|
+
switch (alg) {
|
|
48
|
+
case "EdDSA":
|
|
49
|
+
return { importAlg: { name: "Ed25519" }, verifyAlg: { name: "Ed25519" } };
|
|
50
|
+
case "ES256":
|
|
51
|
+
return { importAlg: { name: "ECDSA", namedCurve: "P-256" }, verifyAlg: { name: "ECDSA", hash: "SHA-256" } };
|
|
52
|
+
case "RS256":
|
|
53
|
+
return {
|
|
54
|
+
importAlg: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
55
|
+
verifyAlg: { name: "RSASSA-PKCS1-v1_5" },
|
|
56
|
+
};
|
|
57
|
+
default:
|
|
58
|
+
return jwk.kty ? null : null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function createResourceServer(options) {
|
|
62
|
+
const doFetch = options.fetch ?? fetch;
|
|
63
|
+
const now = options.now ?? (() => Date.now());
|
|
64
|
+
const jwksUrl = options.jwksUrl ?? `${options.issuer.replace(/\/$/, "")}/jwks`;
|
|
65
|
+
const jwksTtlMs = options.jwksTtlMs ?? DEFAULT_JWKS_TTL_MS;
|
|
66
|
+
const leeway = (options.leewaySeconds ?? DEFAULT_LEEWAY_S) * 1000;
|
|
67
|
+
let jwks = null;
|
|
68
|
+
async function loadJwks(force) {
|
|
69
|
+
if (!force && jwks && now() - jwks.at < jwksTtlMs)
|
|
70
|
+
return jwks.keys;
|
|
71
|
+
const res = await doFetch(jwksUrl);
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
throw new Error(`jwks fetch failed: ${res.status}`);
|
|
74
|
+
const body = (await res.json());
|
|
75
|
+
jwks = { at: now(), keys: body.keys ?? [] };
|
|
76
|
+
return jwks.keys;
|
|
77
|
+
}
|
|
78
|
+
/** Find the signing key by kid, refetching ONCE on a miss to catch rotation. */
|
|
79
|
+
async function keyFor(kid) {
|
|
80
|
+
const pick = (keys) => keys.find((k) => (kid ? k.kid === kid : true)) ?? null;
|
|
81
|
+
let key = pick(await loadJwks(false));
|
|
82
|
+
if (!key)
|
|
83
|
+
key = pick(await loadJwks(true));
|
|
84
|
+
return key;
|
|
85
|
+
}
|
|
86
|
+
async function verifySignature(header, signingInput, signature) {
|
|
87
|
+
const jwk = await keyFor(header.kid);
|
|
88
|
+
if (!jwk)
|
|
89
|
+
return false;
|
|
90
|
+
const params = algParams(header.alg, jwk);
|
|
91
|
+
if (!params)
|
|
92
|
+
return false;
|
|
93
|
+
const key = await crypto.subtle.importKey("jwk", jwk, params.importAlg, false, ["verify"]);
|
|
94
|
+
return crypto.subtle.verify(params.verifyAlg, key, signature, new TextEncoder().encode(signingInput));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Verifies a raw bearer token. Returns a discriminated result rather than
|
|
98
|
+
* throwing, so the caller owns the response shape. `insufficient_scope` is a
|
|
99
|
+
* 403; everything about the token being absent or bad is a 401.
|
|
100
|
+
*/
|
|
101
|
+
async function verify(token) {
|
|
102
|
+
const parts = token.split(".");
|
|
103
|
+
if (parts.length !== 3) {
|
|
104
|
+
return { ok: false, status: 401, error: "invalid_token", description: "Not a JWT." };
|
|
105
|
+
}
|
|
106
|
+
let header;
|
|
107
|
+
let claims;
|
|
108
|
+
try {
|
|
109
|
+
header = decodeJson(parts[0]);
|
|
110
|
+
claims = decodeJson(parts[1]);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { ok: false, status: 401, error: "invalid_token", description: "Malformed JWT." };
|
|
114
|
+
}
|
|
115
|
+
let valid = false;
|
|
116
|
+
try {
|
|
117
|
+
valid = await verifySignature(header, `${parts[0]}.${parts[1]}`, b64urlToBytes(parts[2]));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
valid = false;
|
|
121
|
+
}
|
|
122
|
+
if (!valid) {
|
|
123
|
+
return { ok: false, status: 401, error: "invalid_token", description: "Bad signature." };
|
|
124
|
+
}
|
|
125
|
+
if (claims.iss !== options.issuer) {
|
|
126
|
+
return { ok: false, status: 401, error: "invalid_token", description: "Wrong issuer." };
|
|
127
|
+
}
|
|
128
|
+
const aud = claims.aud;
|
|
129
|
+
const audOk = Array.isArray(aud) ? aud.includes(options.resource) : aud === options.resource;
|
|
130
|
+
if (!audOk) {
|
|
131
|
+
return { ok: false, status: 401, error: "invalid_token", description: "Token is for a different resource." };
|
|
132
|
+
}
|
|
133
|
+
const t = now();
|
|
134
|
+
if (typeof claims.exp === "number" && t > claims.exp * 1000 + leeway) {
|
|
135
|
+
return { ok: false, status: 401, error: "invalid_token", description: "Token expired." };
|
|
136
|
+
}
|
|
137
|
+
if (typeof claims.nbf === "number" && t < claims.nbf * 1000 - leeway) {
|
|
138
|
+
return { ok: false, status: 401, error: "invalid_token", description: "Token not yet valid." };
|
|
139
|
+
}
|
|
140
|
+
const permissions = Array.isArray(claims[PERMISSIONS_CLAIM])
|
|
141
|
+
? claims[PERMISSIONS_CLAIM].filter((p) => typeof p === "string")
|
|
142
|
+
: [];
|
|
143
|
+
const scopes = typeof claims.scope === "string" ? claims.scope.split(" ").filter(Boolean) : [];
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
token: {
|
|
147
|
+
sub: String(claims.sub ?? ""),
|
|
148
|
+
app: typeof claims[APP_CLAIM] === "string" ? claims[APP_CLAIM] : null,
|
|
149
|
+
permissions,
|
|
150
|
+
scopes,
|
|
151
|
+
claims,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The whole check off a `Request`: pull the bearer, verify it, and confirm
|
|
157
|
+
* every required permission (wildcard-aware via `grants()`). Returns a
|
|
158
|
+
* result rather than throwing.
|
|
159
|
+
*/
|
|
160
|
+
async function authenticate(request, init = {}) {
|
|
161
|
+
const authz = request.headers.get("authorization");
|
|
162
|
+
const bearer = authz && /^Bearer\s+(.+)$/i.exec(authz.trim())?.[1];
|
|
163
|
+
if (!bearer) {
|
|
164
|
+
return { ok: false, status: 401, error: "invalid_token", description: "No bearer token." };
|
|
165
|
+
}
|
|
166
|
+
const verified = await verify(bearer.trim());
|
|
167
|
+
if (!verified.ok)
|
|
168
|
+
return verified;
|
|
169
|
+
const missing = (init.permissions ?? []).filter((p) => !grants(verified.token.permissions, p));
|
|
170
|
+
if (missing.length) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: 403,
|
|
174
|
+
error: "insufficient_scope",
|
|
175
|
+
description: `Missing permission(s): ${missing.join(", ")}.`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return verified;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The `WWW-Authenticate` value for a 401, pointing a client at the metadata
|
|
182
|
+
* document so it can discover how to get a token (RFC 9728 §5.1).
|
|
183
|
+
*/
|
|
184
|
+
function challenge(error) {
|
|
185
|
+
const parts = [
|
|
186
|
+
`Bearer resource_metadata="${options.resource}/.well-known/oauth-protected-resource"`,
|
|
187
|
+
];
|
|
188
|
+
if (error)
|
|
189
|
+
parts.push(`error="${error.error}"`, `error_description="${error.description}"`);
|
|
190
|
+
return parts.join(", ");
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The Protected Resource Metadata document (RFC 9728) — served at
|
|
194
|
+
* `/.well-known/oauth-protected-resource`. It names this resource and the
|
|
195
|
+
* IdP as its authorization server; the client takes it from there.
|
|
196
|
+
*/
|
|
197
|
+
function metadata() {
|
|
198
|
+
return {
|
|
199
|
+
resource: options.resource,
|
|
200
|
+
authorization_servers: [options.issuer],
|
|
201
|
+
bearer_methods_supported: ["header"],
|
|
202
|
+
...(options.scopesSupported ? { scopes_supported: options.scopesSupported } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return { verify, authenticate, challenge, metadata };
|
|
206
|
+
}
|
|
@@ -16,6 +16,7 @@ export declare const ApplicationSchema: z.ZodObject<{
|
|
|
16
16
|
app: z.ZodNullable<z.ZodString>;
|
|
17
17
|
allowSignup: z.ZodBoolean;
|
|
18
18
|
permissions: z.ZodArray<z.ZodString>;
|
|
19
|
+
resources: z.ZodArray<z.ZodString>;
|
|
19
20
|
redirectUris: z.ZodArray<z.ZodString>;
|
|
20
21
|
disabled: z.ZodBoolean;
|
|
21
22
|
createdAt: z.ZodString;
|
|
@@ -24,6 +25,7 @@ export declare const UserSchema: z.ZodObject<{
|
|
|
24
25
|
id: z.ZodString;
|
|
25
26
|
email: z.ZodString;
|
|
26
27
|
name: z.ZodNullable<z.ZodString>;
|
|
28
|
+
image: z.ZodString;
|
|
27
29
|
emailVerified: z.ZodBoolean;
|
|
28
30
|
createdAt: z.ZodString;
|
|
29
31
|
}, z.core.$strip>;
|
|
@@ -41,6 +43,7 @@ export declare const ApplicationListSchema: z.ZodObject<{
|
|
|
41
43
|
app: z.ZodNullable<z.ZodString>;
|
|
42
44
|
allowSignup: z.ZodBoolean;
|
|
43
45
|
permissions: z.ZodArray<z.ZodString>;
|
|
46
|
+
resources: z.ZodArray<z.ZodString>;
|
|
44
47
|
redirectUris: z.ZodArray<z.ZodString>;
|
|
45
48
|
disabled: z.ZodBoolean;
|
|
46
49
|
createdAt: z.ZodString;
|
|
@@ -66,6 +69,7 @@ export declare const UpdateApplicationInput: z.ZodObject<{
|
|
|
66
69
|
name: z.ZodOptional<z.ZodString>;
|
|
67
70
|
redirectUris: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
68
71
|
allowSignup: z.ZodOptional<z.ZodBoolean>;
|
|
72
|
+
resources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
69
73
|
}, z.core.$strip>;
|
|
70
74
|
export declare const ClientSecretSchema: z.ZodObject<{
|
|
71
75
|
clientSecret: z.ZodString;
|
|
@@ -82,6 +86,7 @@ export declare const UserListSchema: z.ZodObject<{
|
|
|
82
86
|
id: z.ZodString;
|
|
83
87
|
email: z.ZodString;
|
|
84
88
|
name: z.ZodNullable<z.ZodString>;
|
|
89
|
+
image: z.ZodString;
|
|
85
90
|
emailVerified: z.ZodBoolean;
|
|
86
91
|
createdAt: z.ZodString;
|
|
87
92
|
}, 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
|
|
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"}
|
|
@@ -16,6 +16,9 @@ export const ApplicationSchema = z.object({
|
|
|
16
16
|
app: z.string().nullable().describe("Application key; consumer workspace claims are filtered by this"),
|
|
17
17
|
allowSignup: z.boolean().describe("Whether unknown users may sign themselves up"),
|
|
18
18
|
permissions: z.array(z.string()).describe("The app's declared product-permission catalog"),
|
|
19
|
+
resources: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.describe("Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"),
|
|
19
22
|
redirectUris: z.array(z.string()),
|
|
20
23
|
disabled: z.boolean(),
|
|
21
24
|
createdAt: z.string().describe("ISO 8601 timestamp"),
|
|
@@ -24,6 +27,9 @@ export const UserSchema = z.object({
|
|
|
24
27
|
id: z.string(),
|
|
25
28
|
email: z.string(),
|
|
26
29
|
name: z.string().nullable(),
|
|
30
|
+
image: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("Avatar URL — the user's own picture, or the blobatar the IdP renders for them"),
|
|
27
33
|
emailVerified: z.boolean(),
|
|
28
34
|
createdAt: z.string(),
|
|
29
35
|
});
|
|
@@ -63,6 +69,10 @@ export const UpdateApplicationInput = z.object({
|
|
|
63
69
|
name: z.string().min(1).optional(),
|
|
64
70
|
redirectUris: z.array(z.string().min(1)).min(1).optional(),
|
|
65
71
|
allowSignup: z.boolean().optional(),
|
|
72
|
+
resources: z
|
|
73
|
+
.array(z.string().url())
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Replace the app's protected resource URIs — absolute https, no fragment"),
|
|
66
76
|
});
|
|
67
77
|
export const ClientSecretSchema = z.object({
|
|
68
78
|
clientSecret: z.string().describe("Plaintext client secret — shown exactly once, never stored"),
|
|
@@ -43,6 +43,7 @@ export declare const operations: {
|
|
|
43
43
|
app: z.ZodNullable<z.ZodString>;
|
|
44
44
|
allowSignup: z.ZodBoolean;
|
|
45
45
|
permissions: z.ZodArray<z.ZodString>;
|
|
46
|
+
resources: z.ZodArray<z.ZodString>;
|
|
46
47
|
redirectUris: z.ZodArray<z.ZodString>;
|
|
47
48
|
disabled: z.ZodBoolean;
|
|
48
49
|
createdAt: z.ZodString;
|
|
@@ -79,6 +80,7 @@ export declare const operations: {
|
|
|
79
80
|
app: z.ZodNullable<z.ZodString>;
|
|
80
81
|
allowSignup: z.ZodBoolean;
|
|
81
82
|
permissions: z.ZodArray<z.ZodString>;
|
|
83
|
+
resources: z.ZodArray<z.ZodString>;
|
|
82
84
|
redirectUris: z.ZodArray<z.ZodString>;
|
|
83
85
|
disabled: z.ZodBoolean;
|
|
84
86
|
createdAt: z.ZodString;
|
|
@@ -95,6 +97,7 @@ export declare const operations: {
|
|
|
95
97
|
name: z.ZodOptional<z.ZodString>;
|
|
96
98
|
redirectUris: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
97
99
|
allowSignup: z.ZodOptional<z.ZodBoolean>;
|
|
100
|
+
resources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
98
101
|
}, z.core.$strip>;
|
|
99
102
|
readonly successCode: "200";
|
|
100
103
|
readonly success: z.ZodObject<{
|
|
@@ -103,6 +106,7 @@ export declare const operations: {
|
|
|
103
106
|
app: z.ZodNullable<z.ZodString>;
|
|
104
107
|
allowSignup: z.ZodBoolean;
|
|
105
108
|
permissions: z.ZodArray<z.ZodString>;
|
|
109
|
+
resources: z.ZodArray<z.ZodString>;
|
|
106
110
|
redirectUris: z.ZodArray<z.ZodString>;
|
|
107
111
|
disabled: z.ZodBoolean;
|
|
108
112
|
createdAt: z.ZodString;
|
|
@@ -258,6 +262,7 @@ export declare const operations: {
|
|
|
258
262
|
id: z.ZodString;
|
|
259
263
|
email: z.ZodString;
|
|
260
264
|
name: z.ZodNullable<z.ZodString>;
|
|
265
|
+
image: z.ZodString;
|
|
261
266
|
emailVerified: z.ZodBoolean;
|
|
262
267
|
createdAt: z.ZodString;
|
|
263
268
|
}, z.core.$strip>>;
|
|
@@ -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
|
|
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"}
|
package/dist/src/session.d.ts
CHANGED
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"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"}
|
package/dist/src/session.js
CHANGED
|
@@ -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
|
package/openapi/idp-api.json
CHANGED
|
@@ -78,6 +78,13 @@
|
|
|
78
78
|
},
|
|
79
79
|
"description": "The app's declared product-permission catalog"
|
|
80
80
|
},
|
|
81
|
+
"resources": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"items": {
|
|
84
|
+
"type": "string"
|
|
85
|
+
},
|
|
86
|
+
"description": "Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"
|
|
87
|
+
},
|
|
81
88
|
"redirectUris": {
|
|
82
89
|
"type": "array",
|
|
83
90
|
"items": {
|
|
@@ -98,6 +105,7 @@
|
|
|
98
105
|
"app",
|
|
99
106
|
"allowSignup",
|
|
100
107
|
"permissions",
|
|
108
|
+
"resources",
|
|
101
109
|
"redirectUris",
|
|
102
110
|
"disabled",
|
|
103
111
|
"createdAt"
|
|
@@ -283,6 +291,13 @@
|
|
|
283
291
|
},
|
|
284
292
|
"description": "The app's declared product-permission catalog"
|
|
285
293
|
},
|
|
294
|
+
"resources": {
|
|
295
|
+
"type": "array",
|
|
296
|
+
"items": {
|
|
297
|
+
"type": "string"
|
|
298
|
+
},
|
|
299
|
+
"description": "Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"
|
|
300
|
+
},
|
|
286
301
|
"redirectUris": {
|
|
287
302
|
"type": "array",
|
|
288
303
|
"items": {
|
|
@@ -303,6 +318,7 @@
|
|
|
303
318
|
"app",
|
|
304
319
|
"allowSignup",
|
|
305
320
|
"permissions",
|
|
321
|
+
"resources",
|
|
306
322
|
"redirectUris",
|
|
307
323
|
"disabled",
|
|
308
324
|
"createdAt"
|
|
@@ -364,6 +380,14 @@
|
|
|
364
380
|
},
|
|
365
381
|
"allowSignup": {
|
|
366
382
|
"type": "boolean"
|
|
383
|
+
},
|
|
384
|
+
"resources": {
|
|
385
|
+
"description": "Replace the app's protected resource URIs — absolute https, no fragment",
|
|
386
|
+
"type": "array",
|
|
387
|
+
"items": {
|
|
388
|
+
"type": "string",
|
|
389
|
+
"format": "uri"
|
|
390
|
+
}
|
|
367
391
|
}
|
|
368
392
|
}
|
|
369
393
|
}
|
|
@@ -414,6 +438,13 @@
|
|
|
414
438
|
},
|
|
415
439
|
"description": "The app's declared product-permission catalog"
|
|
416
440
|
},
|
|
441
|
+
"resources": {
|
|
442
|
+
"type": "array",
|
|
443
|
+
"items": {
|
|
444
|
+
"type": "string"
|
|
445
|
+
},
|
|
446
|
+
"description": "Protected resource URIs (e.g. the app's MCP server) — valid `resource` audiences for access tokens"
|
|
447
|
+
},
|
|
417
448
|
"redirectUris": {
|
|
418
449
|
"type": "array",
|
|
419
450
|
"items": {
|
|
@@ -434,6 +465,7 @@
|
|
|
434
465
|
"app",
|
|
435
466
|
"allowSignup",
|
|
436
467
|
"permissions",
|
|
468
|
+
"resources",
|
|
437
469
|
"redirectUris",
|
|
438
470
|
"disabled",
|
|
439
471
|
"createdAt"
|
|
@@ -1242,6 +1274,10 @@
|
|
|
1242
1274
|
}
|
|
1243
1275
|
]
|
|
1244
1276
|
},
|
|
1277
|
+
"image": {
|
|
1278
|
+
"type": "string",
|
|
1279
|
+
"description": "Avatar URL — the user's own picture, or the blobatar the IdP renders for them"
|
|
1280
|
+
},
|
|
1245
1281
|
"emailVerified": {
|
|
1246
1282
|
"type": "boolean"
|
|
1247
1283
|
},
|
|
@@ -1253,6 +1289,7 @@
|
|
|
1253
1289
|
"id",
|
|
1254
1290
|
"email",
|
|
1255
1291
|
"name",
|
|
1292
|
+
"image",
|
|
1256
1293
|
"emailVerified",
|
|
1257
1294
|
"createdAt"
|
|
1258
1295
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willyim/idp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|