@willyim/idp 0.1.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/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/src/api.d.ts +77 -0
- package/dist/src/api.d.ts.map +1 -0
- package/dist/src/api.js +50 -0
- package/dist/src/claims.d.ts +46 -0
- package/dist/src/claims.d.ts.map +1 -0
- package/dist/src/claims.js +61 -0
- package/dist/src/client.d.ts +112 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +155 -0
- package/dist/src/cookie.d.ts +19 -0
- package/dist/src/cookie.d.ts.map +1 -0
- package/dist/src/cookie.js +49 -0
- package/dist/src/crypto.d.ts +32 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +76 -0
- package/dist/src/drizzle/index.d.ts +873 -0
- package/dist/src/drizzle/index.d.ts.map +1 -0
- package/dist/src/drizzle/index.js +130 -0
- package/dist/src/duration.d.ts +8 -0
- package/dist/src/duration.d.ts.map +1 -0
- package/dist/src/duration.js +24 -0
- package/dist/src/generated/idp-api.d.ts +1022 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +20 -0
- package/dist/src/react-router/index.d.ts +75 -0
- package/dist/src/react-router/index.d.ts.map +1 -0
- package/dist/src/react-router/index.js +110 -0
- package/dist/src/session.d.ts +141 -0
- package/dist/src/session.d.ts.map +1 -0
- package/dist/src/session.js +369 -0
- package/dist/src/store.d.ts +44 -0
- package/dist/src/store.d.ts.map +1 -0
- package/dist/src/store.js +41 -0
- package/openapi/idp-api.json +1344 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Willy Ovalle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# @willyim/idp
|
|
2
|
+
|
|
3
|
+
Login for apps that don't own identity.
|
|
4
|
+
|
|
5
|
+
The [willy.im IdP](https://idp.willy.im) is the single source of truth for who
|
|
6
|
+
someone is and what they may do. An app installing this package runs no auth
|
|
7
|
+
framework: it owns one session table, which is a _handle_ to IdP truth rather
|
|
8
|
+
than a record of it. No user table, no account table, no `ADMIN_EMAILS` list.
|
|
9
|
+
|
|
10
|
+
Zero runtime dependencies — `fetch`, WebCrypto and `Request`/`Response` only, so
|
|
11
|
+
the same build runs on Cloudflare Workers, Node 20+ and Bun.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
@willyim/idp core: OIDC client + server sessions
|
|
15
|
+
@willyim/idp/drizzle the session store, and the `idp_session` table
|
|
16
|
+
@willyim/idp/react-router the auth route and the loader guards
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @willyim/idp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`drizzle-orm` is an optional peer dependency, needed only by the `/drizzle`
|
|
26
|
+
subpath. The `/react-router` subpath imports nothing at all — it just produces
|
|
27
|
+
`Response`s.
|
|
28
|
+
|
|
29
|
+
## Standard installation
|
|
30
|
+
|
|
31
|
+
Four touch-points: a factory, a context wire-up, one route, one migration.
|
|
32
|
+
|
|
33
|
+
**`app/lib/idp.server.ts`**
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createIdp } from "@willyim/idp"
|
|
37
|
+
import { drizzleSessions } from "@willyim/idp/drizzle"
|
|
38
|
+
|
|
39
|
+
import * as schema from "~/db/schema"
|
|
40
|
+
|
|
41
|
+
export function getIdp(env: AppEnv, db: AppDb) {
|
|
42
|
+
return createIdp({
|
|
43
|
+
issuer: "https://idp.willy.im/auth",
|
|
44
|
+
clientId: env.IDP_CLIENT_ID,
|
|
45
|
+
clientSecret: env.IDP_CLIENT_SECRET,
|
|
46
|
+
sessions: drizzleSessions(db, schema.idpSession),
|
|
47
|
+
session: { secret: env.SESSION_SECRET, expiresIn: "30d" },
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type Idp = ReturnType<typeof getIdp>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**`app/db/schema.ts`** — re-export the table so `drizzle-kit generate` sees it:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
export { idpSession } from "@willyim/idp/drizzle"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**`workers/app.ts`** — built once per request, injected via load context:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const idp = getIdp(appEnv, db)
|
|
64
|
+
const session = await idp.getSession(request)
|
|
65
|
+
|
|
66
|
+
return requestHandler(request, {
|
|
67
|
+
env,
|
|
68
|
+
cloudflare: { env, ctx },
|
|
69
|
+
db,
|
|
70
|
+
services,
|
|
71
|
+
idp,
|
|
72
|
+
getSession: async () => {
|
|
73
|
+
if (!session) return null
|
|
74
|
+
const local = await db.query.owners.findFirst({ where: { email: session.email } })
|
|
75
|
+
return { sessionUser: session, currentUser: local ?? null, isAdmin: session.can("admin") }
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**`app/routes/auth.$.tsx`** — serves `/auth/login`, `/auth/callback`, `/auth/logout`:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { createAuthRoute } from "@willyim/idp/react-router"
|
|
84
|
+
|
|
85
|
+
export const { loader, action } = createAuthRoute(({ context }) => context.idp)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then guard whatever needs guarding:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { requirePermission, requireSession } from "@willyim/idp/react-router"
|
|
92
|
+
|
|
93
|
+
export async function loader(args: Route.LoaderArgs) {
|
|
94
|
+
const session = await requireSession(args) // redirects to login, preserves ?next=
|
|
95
|
+
const admin = await requirePermission(args, "admin") // 403 without it
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Env
|
|
100
|
+
|
|
101
|
+
| Var | Purpose |
|
|
102
|
+
| ------------------------------------- | ------------------------ |
|
|
103
|
+
| `IDP_CLIENT_ID` / `IDP_CLIENT_SECRET` | OIDC handshake |
|
|
104
|
+
| `SESSION_SECRET` | signs the session cookie |
|
|
105
|
+
|
|
106
|
+
Register `https://your-app/auth/callback` as a redirect URI in the IdP console.
|
|
107
|
+
|
|
108
|
+
## How a session works
|
|
109
|
+
|
|
110
|
+
The cookie holds an **HMAC-signed opaque session id**. The signature is checked
|
|
111
|
+
before the store is touched, so a junk cookie never costs a query. Everything
|
|
112
|
+
else is in the row.
|
|
113
|
+
|
|
114
|
+
`getSession`:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
verify cookie signature -> invalid/absent: null
|
|
118
|
+
load row -> missing/expired: null
|
|
119
|
+
now - syncedAt < freshness -> return as-is (common path, one read)
|
|
120
|
+
otherwise -> userinfo(accessToken)
|
|
121
|
+
access token expired -> refresh first
|
|
122
|
+
401 -> refresh, retry once
|
|
123
|
+
refresh fails -> delete row, return null (revoked at the IdP)
|
|
124
|
+
IdP unreachable -> serve cached claims, retry next request
|
|
125
|
+
ok -> update claims + syncedAt, return
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Permissions are a TTL-cached projection, not a login-time snapshot. A permission
|
|
129
|
+
revoked at the IdP stops granting access within the freshness window — minutes,
|
|
130
|
+
not session-lengths. The per-freshness `/userinfo` call doubles as a liveness
|
|
131
|
+
ping, which is how revocation reaches the app.
|
|
132
|
+
|
|
133
|
+
Instant revocation is a row delete: `idp.destroyAllSessions(sub)` logs a subject
|
|
134
|
+
out of every browser on their next request.
|
|
135
|
+
|
|
136
|
+
## Session lifetimes
|
|
137
|
+
|
|
138
|
+
| Setting | Default | Meaning |
|
|
139
|
+
| ------------------- | ------- | ---------------------------------------------------------------- |
|
|
140
|
+
| `expiresIn` | `7d` | idle timeout; slides on use |
|
|
141
|
+
| `updateAge` | `1d` | how far into its life a session must be before the expiry slides |
|
|
142
|
+
| `freshness` | `5m` | how long cached claims are trusted |
|
|
143
|
+
| `absoluteExpiresIn` | — | hard cap measured from login |
|
|
144
|
+
|
|
145
|
+
When the IdP advertises a per-app ceiling (`session_max_age` in its discovery
|
|
146
|
+
document), a longer `expiresIn` is clamped to it with a warning in development.
|
|
147
|
+
No ceiling advertised, no clamp.
|
|
148
|
+
|
|
149
|
+
Sliding the row past the browser cookie's own `Max-Age` needs one line: when the
|
|
150
|
+
expiry moves, `session.renewCookie()` returns the `Set-Cookie` to attach.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const session = await idp.getSession(request)
|
|
154
|
+
const renew = session?.renewCookie()
|
|
155
|
+
if (renew) response.headers.append("set-cookie", renew)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## API
|
|
159
|
+
|
|
160
|
+
### `createIdp(options)`
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
idp.getSession(request): Promise<Session | null>
|
|
164
|
+
idp.startLogin({ redirectUri, next?, prompt?, loginHint? }): Promise<{ url, headers }>
|
|
165
|
+
idp.completeLogin(request, { redirectUri }): Promise<{ session, headers, next }>
|
|
166
|
+
idp.destroySession(request): Promise<Headers>
|
|
167
|
+
idp.destroyAllSessions(sub): Promise<void>
|
|
168
|
+
idp.logout(request, { redirectTo?, idpLogout? }): Promise<{ headers, url }>
|
|
169
|
+
idp.client: IdpClient
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`Session` carries claims and never tokens, so it is safe to project into loader
|
|
173
|
+
data:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
type Session = {
|
|
177
|
+
id: string
|
|
178
|
+
sub: string
|
|
179
|
+
email: string
|
|
180
|
+
name: string | null
|
|
181
|
+
image: string | null
|
|
182
|
+
permissions: string[]
|
|
183
|
+
workspaces: { id; slug; name; domain; role }[]
|
|
184
|
+
actor: { sub; email? } | null // RFC 8693 `act` — audit only, never authorize on it
|
|
185
|
+
createdAt: Date
|
|
186
|
+
expiresAt: Date
|
|
187
|
+
can(permission: string): boolean
|
|
188
|
+
renewCookie(): string | null
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`can()` matches exactly, and honours `resource:*` and `*` grants.
|
|
193
|
+
|
|
194
|
+
### `createIdpClient(options)` — the OIDC relying party on its own
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
client.authorizationUrl({ redirectUri, state, codeChallenge, scopes?, prompt? }): Promise<string>
|
|
198
|
+
client.exchangeCode({ code, redirectUri, codeVerifier }): Promise<Tokens>
|
|
199
|
+
client.refresh(refreshToken): Promise<Tokens>
|
|
200
|
+
client.userinfo(accessToken): Promise<Claims>
|
|
201
|
+
client.logoutUrl({ idToken, redirectTo }): Promise<string | null>
|
|
202
|
+
client.discover(): Promise<Discovery>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Discovery is derived from `issuer` and memoized per instance. PKCE is not
|
|
206
|
+
optional. `Claims` arrive with the `https://willy.im/*` namespace unwrapped —
|
|
207
|
+
`claims.permissions`, not `claims["https://willy.im/permissions"]`.
|
|
208
|
+
|
|
209
|
+
### `@willyim/idp/drizzle`
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
idpSession // the SQLite/D1 table, named `idp_session`
|
|
213
|
+
idpSessionSqliteTable(name?) // …under another name
|
|
214
|
+
idpSessionPgTable(name?) // the Postgres equivalent
|
|
215
|
+
drizzleSessions(db, table) // -> SessionStore
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Session stores
|
|
219
|
+
|
|
220
|
+
`SessionStore` is five methods, so swapping drizzle out is not a project:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
type SessionStore = {
|
|
224
|
+
get(id): Promise<SessionRecord | null>
|
|
225
|
+
create(record): Promise<SessionRecord>
|
|
226
|
+
update(id, patch): Promise<SessionRecord | null>
|
|
227
|
+
delete(id): Promise<void>
|
|
228
|
+
deleteBySub(sub): Promise<void>
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`memorySessions()` ships for tests and single-process development.
|
|
233
|
+
|
|
234
|
+
## Logout
|
|
235
|
+
|
|
236
|
+
`/auth/logout` destroys the local session _first_, then redirects to the IdP's
|
|
237
|
+
`end_session_endpoint` (read from discovery, never hardcoded) so the SSO session
|
|
238
|
+
ends too. If that endpoint is unavailable the visitor simply lands back on the
|
|
239
|
+
app — logout never leaves anyone signed in locally.
|
|
240
|
+
|
|
241
|
+
RP-initiated logout needs the OAuth client registered for it at the IdP:
|
|
242
|
+
|
|
243
|
+
- `enable_end_session` must be set, or the IdP answers `401 invalid_client`.
|
|
244
|
+
- your post-logout URL must be listed in `post_logout_redirect_uris`, or the IdP
|
|
245
|
+
ends the session and leaves the visitor there.
|
|
246
|
+
|
|
247
|
+
Until both are configured, pass `idpLogout: false` to `createAuthRoute` to keep
|
|
248
|
+
logout app-local.
|
|
249
|
+
|
|
250
|
+
## Testing
|
|
251
|
+
|
|
252
|
+
Inject `fetch` and use the in-memory store; nothing needs the network.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const idp = createIdp({
|
|
256
|
+
issuer: "https://idp.test/auth",
|
|
257
|
+
clientId: "test",
|
|
258
|
+
clientSecret: "test",
|
|
259
|
+
fetch: stubbedFetch,
|
|
260
|
+
sessions: memorySessions(),
|
|
261
|
+
session: { secret: "test", secure: false },
|
|
262
|
+
now: () => clock, // the clock every session decision reads
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Management API types
|
|
267
|
+
|
|
268
|
+
The IdP's `/api/v1/*` management surface is not part of this package's public
|
|
269
|
+
API in v1. The types for it are generated from the IdP's OpenAPI document —
|
|
270
|
+
`npm run openapi` refreshes both `openapi/idp-api.json` and
|
|
271
|
+
`src/generated/idp-api.d.ts` — so that when management calls are added they
|
|
272
|
+
cannot be hand-written. The OIDC endpoints are not in that document and never
|
|
273
|
+
will be: they are standards-defined and discovered at runtime from
|
|
274
|
+
`.well-known`.
|
|
275
|
+
|
|
276
|
+
## Licence
|
|
277
|
+
|
|
278
|
+
MIT
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed door to the IdP's `/api/v1/*` management surface.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in v1 goes through it — the management key, the directory API and
|
|
5
|
+
* end-user API keys were all cut — but it exists now so that the *first*
|
|
6
|
+
* management call can't be hand-written. Paths, methods, path parameters,
|
|
7
|
+
* request bodies and success shapes all come from `src/generated/idp-api.d.ts`,
|
|
8
|
+
* which `npm run openapi` regenerates from apps/idp's OpenAPI document. A typo
|
|
9
|
+
* in a path is a compile error, not a 404 in production.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not exported from any entrypoint: it is internal until the
|
|
12
|
+
* management surface is back in scope. Types only at runtime, so core stays
|
|
13
|
+
* dependency-free.
|
|
14
|
+
*/
|
|
15
|
+
import type { paths } from "./generated/idp-api.js";
|
|
16
|
+
export type ApiPaths = paths;
|
|
17
|
+
export type Method = "get" | "put" | "post" | "delete" | "patch";
|
|
18
|
+
/** The paths that actually declare `M` — `paths` types the rest as `never`. */
|
|
19
|
+
export type PathsFor<M extends Method> = {
|
|
20
|
+
[P in keyof paths]: paths[P] extends {
|
|
21
|
+
[K in M]: object;
|
|
22
|
+
} ? P : never;
|
|
23
|
+
}[keyof paths];
|
|
24
|
+
export type Operation<P extends keyof paths, M extends Method> = M extends keyof paths[P] ? paths[P][M] : never;
|
|
25
|
+
type PathParams<O> = O extends {
|
|
26
|
+
parameters: {
|
|
27
|
+
path: infer T;
|
|
28
|
+
};
|
|
29
|
+
} ? T extends object ? T : never : never;
|
|
30
|
+
type RequestBody<O> = O extends {
|
|
31
|
+
requestBody: {
|
|
32
|
+
content: {
|
|
33
|
+
"application/json": infer B;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
} ? B : never;
|
|
37
|
+
/** The 200 or 201 JSON body, whichever this operation declares. */
|
|
38
|
+
type SuccessBody<O> = O extends {
|
|
39
|
+
responses: infer R;
|
|
40
|
+
} ? R extends {
|
|
41
|
+
200: {
|
|
42
|
+
content: {
|
|
43
|
+
"application/json": infer T;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
} ? T : R extends {
|
|
47
|
+
201: {
|
|
48
|
+
content: {
|
|
49
|
+
"application/json": infer T;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
} ? T : never : never;
|
|
53
|
+
export type RequestOptions<O> = ([PathParams<O>] extends [never] ? {
|
|
54
|
+
params?: undefined;
|
|
55
|
+
} : {
|
|
56
|
+
params: PathParams<O>;
|
|
57
|
+
}) & ([RequestBody<O>] extends [never] ? {
|
|
58
|
+
body?: undefined;
|
|
59
|
+
} : {
|
|
60
|
+
body: RequestBody<O>;
|
|
61
|
+
}) & {
|
|
62
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
};
|
|
65
|
+
export type ManagementApiOptions = {
|
|
66
|
+
/** IdP origin — the API lives at the root, not under the `/auth` basepath. */
|
|
67
|
+
baseUrl: string;
|
|
68
|
+
/** Superadmin `ADMIN_API_TOKEN` or a scoped `wim_…` key. */
|
|
69
|
+
token: string;
|
|
70
|
+
fetch?: typeof fetch;
|
|
71
|
+
};
|
|
72
|
+
export declare function createManagementApi(options: ManagementApiOptions): {
|
|
73
|
+
request<M extends Method, P extends PathsFor<M>>(method: M, path: P, init?: RequestOptions<Operation<P, M>>): Promise<SuccessBody<Operation<P, M>>>;
|
|
74
|
+
};
|
|
75
|
+
export type ManagementApi = ReturnType<typeof createManagementApi>;
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/src/api.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed door to the IdP's `/api/v1/*` management surface.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in v1 goes through it — the management key, the directory API and
|
|
5
|
+
* end-user API keys were all cut — but it exists now so that the *first*
|
|
6
|
+
* management call can't be hand-written. Paths, methods, path parameters,
|
|
7
|
+
* request bodies and success shapes all come from `src/generated/idp-api.d.ts`,
|
|
8
|
+
* which `npm run openapi` regenerates from apps/idp's OpenAPI document. A typo
|
|
9
|
+
* in a path is a compile error, not a 404 in production.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not exported from any entrypoint: it is internal until the
|
|
12
|
+
* management surface is back in scope. Types only at runtime, so core stays
|
|
13
|
+
* dependency-free.
|
|
14
|
+
*/
|
|
15
|
+
import { IdpError } from "./client.js";
|
|
16
|
+
export function createManagementApi(options) {
|
|
17
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
18
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
19
|
+
return {
|
|
20
|
+
async request(method, path, init = {}) {
|
|
21
|
+
const params = (init.params ?? {});
|
|
22
|
+
const rendered = String(path).replace(/\{([^}]+)\}/g, (_, name) => {
|
|
23
|
+
const value = params[name];
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
throw new Error(`missing path parameter "${name}" for ${path}`);
|
|
26
|
+
return encodeURIComponent(value);
|
|
27
|
+
});
|
|
28
|
+
const url = new URL(`${baseUrl}${rendered}`);
|
|
29
|
+
for (const [key, value] of Object.entries(init.query ?? {})) {
|
|
30
|
+
if (value !== undefined)
|
|
31
|
+
url.searchParams.set(key, String(value));
|
|
32
|
+
}
|
|
33
|
+
const response = await doFetch(url.toString(), {
|
|
34
|
+
method: method.toUpperCase(),
|
|
35
|
+
signal: init.signal,
|
|
36
|
+
headers: {
|
|
37
|
+
authorization: `Bearer ${options.token}`,
|
|
38
|
+
accept: "application/json",
|
|
39
|
+
...(init.body === undefined ? {} : { "content-type": "application/json" }),
|
|
40
|
+
},
|
|
41
|
+
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
|
|
42
|
+
});
|
|
43
|
+
const json = await response.json().catch(() => null);
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new IdpError(`${method.toUpperCase()} ${rendered} failed (${response.status})`, response.status, json);
|
|
46
|
+
}
|
|
47
|
+
return json;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The claim set the IdP hands back, with the `https://willy.im/*` namespace
|
|
3
|
+
* unwrapped. Namespaced URI claims are an OIDC requirement, not something app
|
|
4
|
+
* code should ever have to type — `claims.permissions`, not
|
|
5
|
+
* `claims["https://willy.im/permissions"]`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PERMISSIONS_CLAIM = "https://willy.im/permissions";
|
|
8
|
+
export declare const WORKSPACES_CLAIM = "https://willy.im/workspaces";
|
|
9
|
+
/** A tenant inside THIS app. `domain` is set for multi-domain apps, else null. */
|
|
10
|
+
export type Workspace = {
|
|
11
|
+
id: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
name: string;
|
|
14
|
+
domain: string | null;
|
|
15
|
+
role: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The RFC 8693 `act` claim: present while an IdP admin is impersonating the
|
|
19
|
+
* user. Audit-only — tag your logs with it, never branch authorization on it.
|
|
20
|
+
*/
|
|
21
|
+
export type Actor = {
|
|
22
|
+
sub: string;
|
|
23
|
+
email?: string;
|
|
24
|
+
};
|
|
25
|
+
export type Claims = {
|
|
26
|
+
sub: string;
|
|
27
|
+
email: string;
|
|
28
|
+
emailVerified: boolean;
|
|
29
|
+
name: string | null;
|
|
30
|
+
image: string | null;
|
|
31
|
+
/** Product permissions granted in this app. Unwrapped from the namespace. */
|
|
32
|
+
permissions: string[];
|
|
33
|
+
/** Workspaces the user belongs to in this app. Unwrapped from the namespace. */
|
|
34
|
+
workspaces: Workspace[];
|
|
35
|
+
actor: Actor | null;
|
|
36
|
+
};
|
|
37
|
+
/** Wire claims -> `Claims`. Tolerant: a missing claim is an empty value, not a throw. */
|
|
38
|
+
export declare function normalizeClaims(payload: Record<string, unknown>): Claims;
|
|
39
|
+
/**
|
|
40
|
+
* Does this permission set grant `permission`? Exact match, plus prefix
|
|
41
|
+
* wildcards — a grant of `invoices:*` covers `invoices:read`, and `*` covers
|
|
42
|
+
* everything. The IdP hands out concrete permissions today; the wildcard exists
|
|
43
|
+
* so an app-level catalog can group them without the SDK needing to know how.
|
|
44
|
+
*/
|
|
45
|
+
export declare function grants(permissions: readonly string[], permission: string): boolean;
|
|
46
|
+
//# sourceMappingURL=claims.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../../src/claims.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,iBAAiB,iCAAiC,CAAA;AAC/D,eAAO,MAAM,gBAAgB,gCAAgC,CAAA;AAE7D,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,KAAK,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnD,MAAM,MAAM,MAAM,GAAG;IACnB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,OAAO,CAAA;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gFAAgF;IAChF,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB,CAAA;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAqBxE;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAOlF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The claim set the IdP hands back, with the `https://willy.im/*` namespace
|
|
3
|
+
* unwrapped. Namespaced URI claims are an OIDC requirement, not something app
|
|
4
|
+
* code should ever have to type — `claims.permissions`, not
|
|
5
|
+
* `claims["https://willy.im/permissions"]`.
|
|
6
|
+
*/
|
|
7
|
+
export const PERMISSIONS_CLAIM = "https://willy.im/permissions";
|
|
8
|
+
export const WORKSPACES_CLAIM = "https://willy.im/workspaces";
|
|
9
|
+
/** Wire claims -> `Claims`. Tolerant: a missing claim is an empty value, not a throw. */
|
|
10
|
+
export function normalizeClaims(payload) {
|
|
11
|
+
return {
|
|
12
|
+
sub: str(payload.sub) ?? "",
|
|
13
|
+
email: str(payload.email) ?? "",
|
|
14
|
+
emailVerified: payload.email_verified === true,
|
|
15
|
+
name: str(payload.name),
|
|
16
|
+
image: str(payload.picture) ?? str(payload.image),
|
|
17
|
+
permissions: Array.isArray(payload[PERMISSIONS_CLAIM])
|
|
18
|
+
? payload[PERMISSIONS_CLAIM].map(String)
|
|
19
|
+
: [],
|
|
20
|
+
workspaces: Array.isArray(payload[WORKSPACES_CLAIM])
|
|
21
|
+
? payload[WORKSPACES_CLAIM].map((w) => ({
|
|
22
|
+
id: String(w.id ?? ""),
|
|
23
|
+
slug: String(w.slug ?? ""),
|
|
24
|
+
name: String(w.name ?? ""),
|
|
25
|
+
domain: str(w.domain),
|
|
26
|
+
role: String(w.role ?? "member"),
|
|
27
|
+
}))
|
|
28
|
+
: [],
|
|
29
|
+
actor: actorOf(payload.act),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Does this permission set grant `permission`? Exact match, plus prefix
|
|
34
|
+
* wildcards — a grant of `invoices:*` covers `invoices:read`, and `*` covers
|
|
35
|
+
* everything. The IdP hands out concrete permissions today; the wildcard exists
|
|
36
|
+
* so an app-level catalog can group them without the SDK needing to know how.
|
|
37
|
+
*/
|
|
38
|
+
export function grants(permissions, permission) {
|
|
39
|
+
if (permissions.includes(permission) || permissions.includes("*"))
|
|
40
|
+
return true;
|
|
41
|
+
for (const granted of permissions) {
|
|
42
|
+
if (!granted.endsWith(":*"))
|
|
43
|
+
continue;
|
|
44
|
+
if (permission.startsWith(granted.slice(0, -1)))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function str(value) {
|
|
50
|
+
return typeof value === "string" && value ? value : null;
|
|
51
|
+
}
|
|
52
|
+
function actorOf(value) {
|
|
53
|
+
if (!value || typeof value !== "object")
|
|
54
|
+
return null;
|
|
55
|
+
const act = value;
|
|
56
|
+
const sub = str(act.sub);
|
|
57
|
+
if (!sub)
|
|
58
|
+
return null;
|
|
59
|
+
const email = str(act.email);
|
|
60
|
+
return email ? { sub, email } : { sub };
|
|
61
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 0 — the OIDC relying party. Everything on the wire lives here: the
|
|
3
|
+
* discovery document, the authorization-code flow with PKCE, refresh, userinfo,
|
|
4
|
+
* and RP-initiated logout. No sessions, no cookies, no storage.
|
|
5
|
+
*
|
|
6
|
+
* `fetch` + WebCrypto only, so it runs anywhere the platform is web-standard.
|
|
7
|
+
*/
|
|
8
|
+
import { type Claims } from "./claims.js";
|
|
9
|
+
export declare const DEFAULT_SCOPES: string[];
|
|
10
|
+
export type IdpClientOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* The OIDC issuer, including the basepath the IdP is mounted on:
|
|
13
|
+
* `https://idp.willy.im/auth` (or a vanity domain, `https://idp.kasso.do/auth`).
|
|
14
|
+
* Discovery is `${issuer}/.well-known/openid-configuration`.
|
|
15
|
+
*/
|
|
16
|
+
issuer: string;
|
|
17
|
+
clientId: string;
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
/** Default scopes for `authorizationUrl`. `offline_access` buys refresh tokens. */
|
|
20
|
+
scopes?: string[];
|
|
21
|
+
/** Override `fetch` — tests, instrumentation, a Worker's bound fetcher. */
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
};
|
|
24
|
+
/** The subset of the discovery document we use, plus the fields we may. */
|
|
25
|
+
export type Discovery = {
|
|
26
|
+
issuer: string;
|
|
27
|
+
authorization_endpoint: string;
|
|
28
|
+
token_endpoint: string;
|
|
29
|
+
userinfo_endpoint: string;
|
|
30
|
+
end_session_endpoint?: string;
|
|
31
|
+
jwks_uri?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Per-app session ceiling, in seconds. Not standard OIDC — an IdP extension
|
|
34
|
+
* the SDK clamps `session.expiresIn` against when present.
|
|
35
|
+
*/
|
|
36
|
+
session_max_age?: number;
|
|
37
|
+
};
|
|
38
|
+
/** Token-endpoint output, camelCased so app code never sees the wire shape. */
|
|
39
|
+
export type Tokens = {
|
|
40
|
+
accessToken: string;
|
|
41
|
+
tokenType: string;
|
|
42
|
+
/** Seconds until the access token expires, when the IdP says. */
|
|
43
|
+
expiresIn: number | null;
|
|
44
|
+
refreshToken: string | null;
|
|
45
|
+
idToken: string | null;
|
|
46
|
+
scope: string | null;
|
|
47
|
+
};
|
|
48
|
+
export declare class IdpError extends Error {
|
|
49
|
+
readonly status: number;
|
|
50
|
+
readonly body: unknown;
|
|
51
|
+
constructor(message: string, status: number, body?: unknown);
|
|
52
|
+
}
|
|
53
|
+
export type AuthorizationUrlInput = {
|
|
54
|
+
redirectUri: string;
|
|
55
|
+
state: string;
|
|
56
|
+
/** The S256 challenge. PKCE is not optional — see `createPkce`. */
|
|
57
|
+
codeChallenge: string;
|
|
58
|
+
scopes?: string[];
|
|
59
|
+
/** `login` to force re-authentication, `none` for a silent check. */
|
|
60
|
+
prompt?: string;
|
|
61
|
+
loginHint?: string;
|
|
62
|
+
};
|
|
63
|
+
export type IdpClient = ReturnType<typeof createIdpClient>;
|
|
64
|
+
/** A fresh PKCE verifier and its S256 challenge. */
|
|
65
|
+
export declare function createPkce(): Promise<{
|
|
66
|
+
codeVerifier: string;
|
|
67
|
+
codeChallenge: string;
|
|
68
|
+
}>;
|
|
69
|
+
export declare function createIdpClient(options: IdpClientOptions): {
|
|
70
|
+
discover: () => Promise<Discovery>;
|
|
71
|
+
/** Where to send the browser to log in. */
|
|
72
|
+
authorizationUrl(input: AuthorizationUrlInput): Promise<string>;
|
|
73
|
+
/** Swap the callback `code` for tokens. The verifier proves we started the flow. */
|
|
74
|
+
exchangeCode(input: {
|
|
75
|
+
code: string;
|
|
76
|
+
redirectUri: string;
|
|
77
|
+
codeVerifier: string;
|
|
78
|
+
}): Promise<Tokens>;
|
|
79
|
+
/** Requires `offline_access` at login. Throws `IdpError` once the grant is gone. */
|
|
80
|
+
refresh(refreshToken: string): Promise<Tokens>;
|
|
81
|
+
/**
|
|
82
|
+
* Live claims for an access token. Unlike the id_token — a login-time
|
|
83
|
+
* snapshot — this reflects permissions and workspaces as they are *now*,
|
|
84
|
+
* which is how a revocation at the IdP reaches the app.
|
|
85
|
+
*/
|
|
86
|
+
userinfo(accessToken: string): Promise<Claims>;
|
|
87
|
+
/**
|
|
88
|
+
* RP-initiated logout, built from the `end_session_endpoint` in discovery —
|
|
89
|
+
* never a hardcoded path. Returns null when there is nothing usable to
|
|
90
|
+
* redirect to, which is either of:
|
|
91
|
+
*
|
|
92
|
+
* - the IdP advertises no `end_session_endpoint`, or
|
|
93
|
+
* - we have no `id_token` to hint with. The spec makes `id_token_hint`
|
|
94
|
+
* optional; this IdP does not — it identifies the client from the hint,
|
|
95
|
+
* verifies its signature, and reads `sid` out of it to pick the SSO
|
|
96
|
+
* session to kill. Without one the request is rejected.
|
|
97
|
+
*
|
|
98
|
+
* Callers must treat null as "log out locally and move on" — see
|
|
99
|
+
* `createIdp().logout`, which does exactly that.
|
|
100
|
+
*
|
|
101
|
+
* Note the IdP-side registration this depends on: the OAuth client needs
|
|
102
|
+
* `enable_end_session` set (better-auth answers 401 `invalid_client`
|
|
103
|
+
* otherwise) and `redirectTo` must be listed in its
|
|
104
|
+
* `post_logout_redirect_uris` (an unlisted URI is silently not redirected
|
|
105
|
+
* to, leaving the visitor at the IdP).
|
|
106
|
+
*/
|
|
107
|
+
logoutUrl(input: {
|
|
108
|
+
idToken?: string | null;
|
|
109
|
+
redirectTo?: string;
|
|
110
|
+
}): Promise<string | null>;
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AAG1D,eAAO,MAAM,cAAc,UAAmD,CAAA;AAE9E,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB,CAAA;AAED,2EAA2E;AAC3E,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,sBAAsB,EAAE,MAAM,CAAA;IAC9B,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,+EAA+E;AAC/E,MAAM,MAAM,MAAM,GAAG;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;gBACV,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;CAM5D;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA;AAE1D,oDAAoD;AACpD,wBAAsB,UAAU,IAAI,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,CAG3F;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB;oBAOlC,OAAO,CAAC,SAAS,CAAC;IAiDrC,2CAA2C;4BACb,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAerE,oFAAoF;wBAChE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE;IAY/E,oFAAoF;0BAC9D,MAAM;IAO5B;;;;OAIG;0BACyB,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAepD;;;;;;;;;;;;;;;;;;;OAmBG;qBACoB;QACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACvB,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;EAU7B"}
|