@willyim/idp 0.1.1 → 0.3.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.
Files changed (42) hide show
  1. package/README.md +95 -12
  2. package/dist/src/api.d.ts +31 -55
  3. package/dist/src/api.d.ts.map +1 -1
  4. package/dist/src/api.js +16 -11
  5. package/dist/src/claims.d.ts +9 -34
  6. package/dist/src/claims.d.ts.map +1 -1
  7. package/dist/src/claims.js +12 -40
  8. package/dist/src/client.d.ts +30 -34
  9. package/dist/src/client.d.ts.map +1 -1
  10. package/dist/src/client.js +16 -22
  11. package/dist/src/drizzle/index.d.ts +78 -13
  12. package/dist/src/drizzle/index.d.ts.map +1 -1
  13. package/dist/src/errors.d.ts +8 -0
  14. package/dist/src/errors.d.ts.map +1 -0
  15. package/dist/src/errors.js +12 -0
  16. package/dist/src/index.d.ts +3 -1
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js +3 -1
  19. package/dist/src/schemas/index.d.ts +323 -0
  20. package/dist/src/schemas/index.d.ts.map +1 -0
  21. package/dist/src/schemas/index.js +207 -0
  22. package/dist/src/schemas/openapi.d.ts +31 -0
  23. package/dist/src/schemas/openapi.d.ts.map +1 -0
  24. package/dist/src/schemas/openapi.js +111 -0
  25. package/dist/src/schemas/operations.d.ts +502 -0
  26. package/dist/src/schemas/operations.d.ts.map +1 -0
  27. package/dist/src/schemas/operations.js +213 -0
  28. package/dist/src/session.d.ts +17 -3
  29. package/dist/src/session.d.ts.map +1 -1
  30. package/dist/src/session.js +12 -1
  31. package/dist/src/user-keys.d.ts +124 -0
  32. package/dist/src/user-keys.d.ts.map +1 -0
  33. package/dist/src/user-keys.js +174 -0
  34. package/dist/src/validate.d.ts +13 -0
  35. package/dist/src/validate.d.ts.map +1 -0
  36. package/dist/src/validate.js +28 -0
  37. package/dist/src/wire.d.ts +164 -0
  38. package/dist/src/wire.d.ts.map +1 -0
  39. package/dist/src/wire.js +100 -0
  40. package/openapi/idp-api.json +1110 -17
  41. package/package.json +16 -7
  42. package/dist/src/generated/idp-api.d.ts +0 -1022
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Every operation the management API exposes, in one table.
3
+ *
4
+ * Three things read it: `buildOpenApiDocument` (in `./openapi.ts`) turns it
5
+ * into the published document, `api.ts` uses it to type its `request()` and to
6
+ * parse what comes back, and `apps/idp` validates request bodies with the same
7
+ * `input` schemas. Adding an endpoint means adding a row here; nothing else
8
+ * needs to learn about it.
9
+ */
10
+ import { z } from "zod";
11
+ import { AdminKeyCreatedSchema, AdminKeyListSchema, ApiKeyCreatedSchema, ApiKeyListSchema, ApplicationCreatedSchema, ApplicationListSchema, ApplicationSchema, AppPermissionsSchema, AuditListSchema, ClientSecretSchema, CreateAdminKeyInput, CreateApiKeyInput, CreateApplicationInput, CreateUserApiKeyInput, CreateWorkspaceInput, InviteMemberInput, InviteMemberResult, MemberListSchema, OkSchema, SetAppPermissionsInput, UpdateApplicationInput, UpdateMemberInput, UserApiKeyCreatedSchema, UserApiKeyListSchema, UserApiKeyValidationSchema, UserListSchema, ValidateUserApiKeyInput, WorkspaceCreatedSchema, WorkspaceListSchema, } from "./index.js";
12
+ const APP_PARAM = { app: "Application key (oauth_client.metadata.app)." };
13
+ const CLIENT_PARAM = { clientId: "OAuth client id of the application." };
14
+ export const operations = {
15
+ "get /api/v1/applications": {
16
+ summary: "List registered applications",
17
+ successCode: "200",
18
+ success: ApplicationListSchema,
19
+ },
20
+ "post /api/v1/applications": {
21
+ summary: "Register an application (client secret returned once)",
22
+ description: "Requires the superadmin token. Creating an application is an IdP-level act — there is no app to scope a permission to yet.",
23
+ input: CreateApplicationInput,
24
+ successCode: "201",
25
+ success: ApplicationCreatedSchema,
26
+ },
27
+ "get /api/v1/applications/{clientId}": {
28
+ summary: "Get one application",
29
+ permission: "app:read",
30
+ params: CLIENT_PARAM,
31
+ notFound: "No application with that client id",
32
+ successCode: "200",
33
+ success: ApplicationSchema,
34
+ },
35
+ "patch /api/v1/applications/{clientId}": {
36
+ summary: "Update an application",
37
+ permission: "app:update",
38
+ params: CLIENT_PARAM,
39
+ notFound: "No application with that client id",
40
+ input: UpdateApplicationInput,
41
+ successCode: "200",
42
+ success: ApplicationSchema,
43
+ },
44
+ "delete /api/v1/applications/{clientId}": {
45
+ summary: "Deregister an application",
46
+ permission: "app:delete",
47
+ params: CLIENT_PARAM,
48
+ notFound: "No application with that client id",
49
+ successCode: "200",
50
+ success: OkSchema,
51
+ },
52
+ "post /api/v1/applications/{clientId}/rotate-secret": {
53
+ summary: "Rotate the client secret (returned once; the old one stops working)",
54
+ permission: "app:update",
55
+ params: CLIENT_PARAM,
56
+ notFound: "No application with that client id",
57
+ successCode: "200",
58
+ success: ClientSecretSchema,
59
+ },
60
+ "put /api/v1/apps/{app}/permissions": {
61
+ summary: "Replace the app's product-permission catalog",
62
+ permission: "app:update",
63
+ params: APP_PARAM,
64
+ notFound: "No application with that app key",
65
+ input: SetAppPermissionsInput,
66
+ successCode: "200",
67
+ success: AppPermissionsSchema,
68
+ },
69
+ "get /api/v1/apps/{app}/keys": {
70
+ summary: "List scoped management API keys (never the hashes)",
71
+ permission: "apikey:read",
72
+ params: APP_PARAM,
73
+ successCode: "200",
74
+ success: ApiKeyListSchema,
75
+ },
76
+ "post /api/v1/apps/{app}/keys": {
77
+ summary: "Mint a scoped management API key (plaintext returned once)",
78
+ description: "Requires `apikey:create` on the path app (or the superadmin token). The requested permissions must be a subset of the caller's own, otherwise 403 `permissions_exceed_caller` — without that rule any key holding `apikey:create` could mint itself a more powerful successor.",
79
+ permission: "apikey:create",
80
+ params: APP_PARAM,
81
+ input: CreateApiKeyInput,
82
+ successCode: "201",
83
+ success: ApiKeyCreatedSchema,
84
+ },
85
+ "delete /api/v1/apps/{app}/keys/{id}": {
86
+ summary: "Revoke a scoped management API key (idempotent)",
87
+ permission: "apikey:revoke",
88
+ params: APP_PARAM,
89
+ notFound: "No key with that id on this app",
90
+ successCode: "200",
91
+ success: OkSchema,
92
+ },
93
+ "get /api/v1/admin-keys": {
94
+ summary: "List IdP-level admin keys (never the hashes)",
95
+ description: "Requires the superadmin token or an admin key. Admin keys are `api_key` rows with no application scope, so they hold every permission on every app.",
96
+ successCode: "200",
97
+ success: AdminKeyListSchema,
98
+ },
99
+ "post /api/v1/admin-keys": {
100
+ summary: "Mint an IdP-level admin key (plaintext returned once)",
101
+ description: "Requires the superadmin token or an admin key. Mint one per agent: unlike the static ADMIN_API_TOKEN, an admin key has a name, an optional expiry, a revoke switch, and its own `adminkey:<id>` identity in the audit log.",
102
+ input: CreateAdminKeyInput,
103
+ successCode: "201",
104
+ success: AdminKeyCreatedSchema,
105
+ },
106
+ "delete /api/v1/admin-keys/{id}": {
107
+ summary: "Revoke an IdP-level admin key (idempotent)",
108
+ description: "A key may revoke itself — an agent cleaning up after itself is legitimate — after which its next request is simply unauthorized.",
109
+ params: { id: "Admin key id." },
110
+ notFound: "No admin key with that id",
111
+ successCode: "200",
112
+ success: OkSchema,
113
+ },
114
+ "get /api/v1/users": {
115
+ summary: "List users",
116
+ successCode: "200",
117
+ success: UserListSchema,
118
+ },
119
+ "get /api/v1/workspaces": {
120
+ summary: "List workspaces",
121
+ successCode: "200",
122
+ success: WorkspaceListSchema,
123
+ },
124
+ "get /api/v1/apps/{app}/members": {
125
+ summary: "List app members",
126
+ permission: "member:read",
127
+ params: APP_PARAM,
128
+ successCode: "200",
129
+ success: MemberListSchema,
130
+ },
131
+ "post /api/v1/apps/{app}/members": {
132
+ summary: "Add or invite a member",
133
+ permission: "member:invite",
134
+ params: APP_PARAM,
135
+ input: InviteMemberInput,
136
+ successCode: "201",
137
+ success: InviteMemberResult,
138
+ },
139
+ "patch /api/v1/apps/{app}/members/{userId}": {
140
+ summary: "Update a member's role + permissions",
141
+ permission: "member:manage",
142
+ params: APP_PARAM,
143
+ input: UpdateMemberInput,
144
+ successCode: "200",
145
+ success: OkSchema,
146
+ },
147
+ "delete /api/v1/apps/{app}/members/{userId}": {
148
+ summary: "Remove a member",
149
+ permission: "member:manage",
150
+ params: APP_PARAM,
151
+ successCode: "200",
152
+ success: OkSchema,
153
+ },
154
+ "get /api/v1/apps/{app}/workspaces": {
155
+ summary: "List app workspaces",
156
+ permission: "workspace:read",
157
+ params: APP_PARAM,
158
+ successCode: "200",
159
+ success: WorkspaceListSchema,
160
+ },
161
+ "post /api/v1/apps/{app}/workspaces": {
162
+ summary: "Create a workspace",
163
+ permission: "workspace:create",
164
+ params: APP_PARAM,
165
+ input: CreateWorkspaceInput,
166
+ successCode: "201",
167
+ success: WorkspaceCreatedSchema,
168
+ },
169
+ "get /api/v1/apps/{app}/user-keys": {
170
+ summary: "List end-user API keys",
171
+ permission: "userkey:read",
172
+ params: APP_PARAM,
173
+ query: [
174
+ { name: "userId", description: "Only keys owned by this user." },
175
+ { name: "workspaceId", description: "Only keys bound to this workspace." },
176
+ ],
177
+ successCode: "200",
178
+ success: UserApiKeyListSchema,
179
+ },
180
+ "post /api/v1/apps/{app}/user-keys": {
181
+ summary: "Mint an end-user API key (plaintext returned once)",
182
+ permission: "userkey:create",
183
+ params: APP_PARAM,
184
+ input: CreateUserApiKeyInput,
185
+ successCode: "201",
186
+ success: UserApiKeyCreatedSchema,
187
+ },
188
+ "post /api/v1/apps/{app}/user-keys/validate": {
189
+ summary: "Validate a presented end-user key (200 + valid discriminator)",
190
+ permission: "userkey:validate",
191
+ params: APP_PARAM,
192
+ input: ValidateUserApiKeyInput,
193
+ successCode: "200",
194
+ success: UserApiKeyValidationSchema,
195
+ },
196
+ "delete /api/v1/apps/{app}/user-keys/{id}": {
197
+ summary: "Revoke an end-user API key (idempotent)",
198
+ permission: "userkey:revoke",
199
+ params: APP_PARAM,
200
+ successCode: "200",
201
+ success: OkSchema,
202
+ },
203
+ "get /api/v1/apps/{app}/audit": {
204
+ summary: "List recent audit entries",
205
+ permission: "audit:read",
206
+ params: APP_PARAM,
207
+ successCode: "200",
208
+ success: AuditListSchema,
209
+ },
210
+ };
211
+ export function lookup(method, path) {
212
+ return operations[`${method.toLowerCase()} ${path}`];
213
+ }
@@ -68,14 +68,28 @@ export type Idp = ReturnType<typeof createIdp>;
68
68
  export declare function createIdp(options: IdpOptions): {
69
69
  /** The Layer 0 client, for anything the session layer doesn't wrap. */
70
70
  client: {
71
- discover: () => Promise<import("./client.js").Discovery>;
71
+ discover: () => Promise<import("./wire.js").Discovery>;
72
72
  authorizationUrl(input: import("./client.js").AuthorizationUrlInput): Promise<string>;
73
73
  exchangeCode(input: {
74
74
  code: string;
75
75
  redirectUri: string;
76
76
  codeVerifier: string;
77
- }): Promise<import("./client.js").Tokens>;
78
- refresh(refreshToken: string): Promise<import("./client.js").Tokens>;
77
+ }): Promise<{
78
+ accessToken: string;
79
+ tokenType: string;
80
+ expiresIn: number | null;
81
+ refreshToken: string | null;
82
+ idToken: string | null;
83
+ scope: string | null;
84
+ }>;
85
+ refresh(refreshToken: string): Promise<{
86
+ accessToken: string;
87
+ tokenType: string;
88
+ expiresIn: number | null;
89
+ refreshToken: string | null;
90
+ idToken: string | null;
91
+ scope: string | null;
92
+ }>;
79
93
  userinfo(accessToken: string): Promise<Claims>;
80
94
  logoutUrl(input: {
81
95
  idToken?: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@
8
8
  * call doubles as a liveness ping — it is how a revocation at the IdP reaches
9
9
  * the app, in minutes rather than in session-lengths.
10
10
  */
11
+ import { z } from "zod";
11
12
  import { grants } from "./claims.js";
12
13
  import { createIdpClient, createPkce, IdpError, } from "./client.js";
13
14
  import { clearCookie, readCookie, serializeCookie } from "./cookie.js";
@@ -16,6 +17,16 @@ import { parseDuration } from "./duration.js";
16
17
  export const DEFAULT_SESSION_COOKIE = "idp_session";
17
18
  /** How long the login handshake may take before its state cookie is stale. */
18
19
  const STATE_COOKIE_MAX_AGE_MS = 10 * 60_000;
20
+ /**
21
+ * What rides in the login state cookie. HMAC-signed, so tampering is caught
22
+ * before this ever parses — the schema is here to catch a cookie left over
23
+ * from an older version of the SDK, not an attacker.
24
+ */
25
+ const StatePayloadSchema = z.object({
26
+ state: z.string().min(1),
27
+ codeVerifier: z.string().min(1),
28
+ next: z.string().optional(),
29
+ });
19
30
  export function createIdp(options) {
20
31
  const client = createIdpClient(options);
21
32
  const store = options.sessions;
@@ -255,7 +266,7 @@ export function createIdp(options) {
255
266
  throw new IdpError("login state cookie is missing or invalid", 400);
256
267
  let payload;
257
268
  try {
258
- payload = JSON.parse(base64urlDecodeString(raw));
269
+ payload = StatePayloadSchema.parse(JSON.parse(base64urlDecodeString(raw)));
259
270
  }
260
271
  catch {
261
272
  throw new IdpError("login state cookie is malformed", 400);
@@ -0,0 +1,124 @@
1
+ /**
2
+ * End-user API keys, from the consuming app's side.
3
+ *
4
+ * The IdP is the key store: an app mints, lists, revokes and validates `wak_…`
5
+ * keys through the management API, authenticated with its own scoped `wim_…`
6
+ * key, and never persists a plaintext token or a hash of one. This module is
7
+ * the sugar over those four calls, plus the two things every consumer would
8
+ * otherwise write badly by hand:
9
+ *
10
+ * - a validation cache. `validate` is a network round trip, and API keys
11
+ * arrive on the request-per-request hot path. Results are cached by digest
12
+ * of the token, with a short TTL, and concurrent validations of the same
13
+ * token share one in-flight request. The cost is revocation lag bounded by
14
+ * `cache.ttlMs` — pick it deliberately, and call `forget` after a revoke you
15
+ * performed yourself.
16
+ * - `authenticate`, which reads the bearer token off a `Request`, validates it
17
+ * and checks required scopes, returning a discriminated result rather than
18
+ * throwing, so the caller decides what a 401 looks like.
19
+ *
20
+ * Only for *secret* credentials. A public write key embedded in a page (an
21
+ * analytics ingest token, say) identifies a site rather than a user, cannot be
22
+ * kept secret, and must not pay a round trip per hit — keep those in the app's
23
+ * own table.
24
+ */
25
+ import type { z } from "zod";
26
+ import { type ManagementApiOptions } from "./api.js";
27
+ import type { CreateUserApiKeyInput, UserApiKeyCreatedSchema, UserApiKeySchema, UserApiKeyValidationSchema } from "./schemas/index.js";
28
+ type CreateBody = z.input<typeof CreateUserApiKeyInput>;
29
+ /** One key as the IdP reports it. Never includes the token or its hash. */
30
+ export type UserApiKey = z.output<typeof UserApiKeySchema>;
31
+ /** What `create` hands back. `token` is the only time the plaintext exists. */
32
+ export type MintedUserApiKey = z.output<typeof UserApiKeyCreatedSchema>;
33
+ /** A validation verdict. A miss is data, not an error — hence `valid: false`. */
34
+ export type UserKeyValidation = z.output<typeof UserApiKeyValidationSchema>;
35
+ /** The `valid: true` half, i.e. an authenticated key. */
36
+ export type AuthenticatedKey = Extract<UserKeyValidation, {
37
+ valid: true;
38
+ }>;
39
+ export type UserKeyCacheOptions = {
40
+ /** How long a `valid: true` verdict is reused. Default 60s. */
41
+ ttlMs?: number;
42
+ /** How long a `valid: false` verdict is reused. Default 10s. */
43
+ missTtlMs?: number;
44
+ /** Entry ceiling before the oldest are dropped. Default 1000. */
45
+ max?: number;
46
+ };
47
+ export type UserKeysOptions = ManagementApiOptions & {
48
+ /** The app key these keys belong to — `oauth_client.metadata.app`. */
49
+ app: string;
50
+ /** `false` disables caching entirely (every validate is a round trip). */
51
+ cache?: UserKeyCacheOptions | false;
52
+ /** Clock seam, for tests. */
53
+ now?: () => number;
54
+ };
55
+ export type ListFilter = {
56
+ userId?: string;
57
+ workspaceId?: string;
58
+ signal?: AbortSignal;
59
+ };
60
+ export type CreateUserApiKeyInput = CreateBody & {
61
+ signal?: AbortSignal;
62
+ };
63
+ export type AuthenticateOptions = {
64
+ /** Every scope listed must be present on the key. */
65
+ scopes?: string[];
66
+ signal?: AbortSignal;
67
+ };
68
+ export type AuthenticateResult = {
69
+ ok: true;
70
+ key: AuthenticatedKey;
71
+ } | {
72
+ ok: false;
73
+ status: 401 | 403;
74
+ reason: "missing" | "not_found" | "revoked" | "expired" | "insufficient_scope";
75
+ /** The scopes that were required but absent, when `insufficient_scope`. */
76
+ missing?: string[];
77
+ };
78
+ /**
79
+ * Reads a presented key off a request: `Authorization: Bearer …` first, then
80
+ * `X-API-Key`. Returns null when neither is present, so "no credential" stays
81
+ * distinguishable from "bad credential".
82
+ */
83
+ export declare function readApiKey(request: {
84
+ headers: Headers;
85
+ }): string | null;
86
+ export declare function createUserKeys(options: UserKeysOptions): {
87
+ validate: (token: string, init?: {
88
+ signal?: AbortSignal;
89
+ fresh?: boolean;
90
+ }) => Promise<UserKeyValidation>;
91
+ /** The keys this app has minted, newest first. Optionally filtered. */
92
+ list(filter?: ListFilter): Promise<UserApiKey[]>;
93
+ /**
94
+ * Mints a key for one of the app's users. The returned `token` is the only
95
+ * copy — show it once and forget it. Scopes must come from the app's
96
+ * declared product permission catalog; unknown ones are a 422, not a
97
+ * silent drop.
98
+ */
99
+ create(input: CreateUserApiKeyInput): Promise<MintedUserApiKey>;
100
+ /**
101
+ * Revokes a key by id (idempotent). Cached verdicts for *other* tokens are
102
+ * untouched; this app never saw the revoked plaintext, so the entry for it
103
+ * can only expire on its own TTL. Call `forget(token)` instead when the
104
+ * plaintext is in hand.
105
+ */
106
+ revoke(id: string, init?: {
107
+ signal?: AbortSignal;
108
+ }): Promise<{
109
+ ok: true;
110
+ }>;
111
+ /**
112
+ * The whole check in one call: read the credential off the request,
113
+ * validate it, and confirm every required scope. Returns a result rather
114
+ * than throwing, so the caller owns the response shape.
115
+ */
116
+ authenticate(request: {
117
+ headers: Headers;
118
+ }, init?: AuthenticateOptions): Promise<AuthenticateResult>;
119
+ /** Drops one token's cached verdict, or the whole cache when called bare. */
120
+ forget(token?: string): Promise<void>;
121
+ };
122
+ export type UserKeys = ReturnType<typeof createUserKeys>;
123
+ export {};
124
+ //# sourceMappingURL=user-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-keys.d.ts","sourceRoot":"","sources":["../../src/user-keys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * End-user API keys, from the consuming app's side.
3
+ *
4
+ * The IdP is the key store: an app mints, lists, revokes and validates `wak_…`
5
+ * keys through the management API, authenticated with its own scoped `wim_…`
6
+ * key, and never persists a plaintext token or a hash of one. This module is
7
+ * the sugar over those four calls, plus the two things every consumer would
8
+ * otherwise write badly by hand:
9
+ *
10
+ * - a validation cache. `validate` is a network round trip, and API keys
11
+ * arrive on the request-per-request hot path. Results are cached by digest
12
+ * of the token, with a short TTL, and concurrent validations of the same
13
+ * token share one in-flight request. The cost is revocation lag bounded by
14
+ * `cache.ttlMs` — pick it deliberately, and call `forget` after a revoke you
15
+ * performed yourself.
16
+ * - `authenticate`, which reads the bearer token off a `Request`, validates it
17
+ * and checks required scopes, returning a discriminated result rather than
18
+ * throwing, so the caller decides what a 401 looks like.
19
+ *
20
+ * Only for *secret* credentials. A public write key embedded in a page (an
21
+ * analytics ingest token, say) identifies a site rather than a user, cannot be
22
+ * kept secret, and must not pay a round trip per hit — keep those in the app's
23
+ * own table.
24
+ */
25
+ import { createManagementApi } from "./api.js";
26
+ import { sha256Base64url } from "./crypto.js";
27
+ const DEFAULT_TTL_MS = 60_000;
28
+ const DEFAULT_MISS_TTL_MS = 10_000;
29
+ const DEFAULT_MAX = 1000;
30
+ /**
31
+ * Reads a presented key off a request: `Authorization: Bearer …` first, then
32
+ * `X-API-Key`. Returns null when neither is present, so "no credential" stays
33
+ * distinguishable from "bad credential".
34
+ */
35
+ export function readApiKey(request) {
36
+ const authorization = request.headers.get("authorization");
37
+ if (authorization) {
38
+ const [scheme, ...rest] = authorization.split(" ");
39
+ const value = rest.join(" ").trim();
40
+ if (scheme?.toLowerCase() === "bearer" && value)
41
+ return value;
42
+ }
43
+ return request.headers.get("x-api-key")?.trim() || null;
44
+ }
45
+ export function createUserKeys(options) {
46
+ const api = createManagementApi(options);
47
+ const app = options.app;
48
+ const now = options.now ?? (() => Date.now());
49
+ const caching = options.cache !== false;
50
+ const ttlMs = (options.cache || {}).ttlMs ?? DEFAULT_TTL_MS;
51
+ const missTtlMs = (options.cache || {}).missTtlMs ?? DEFAULT_MISS_TTL_MS;
52
+ const max = (options.cache || {}).max ?? DEFAULT_MAX;
53
+ // Keyed by digest, never by the token itself: a heap dump or a logged Map
54
+ // then leaks nothing usable. Insertion-ordered, so the oldest entry is the
55
+ // first key — good enough eviction for a cache this size.
56
+ const cache = new Map();
57
+ const inFlight = new Map();
58
+ const digest = (token) => sha256Base64url(`user-key:${app}:${token}`);
59
+ function remember(key, verdict) {
60
+ if (!caching)
61
+ return;
62
+ if (cache.size >= max) {
63
+ const oldest = cache.keys().next();
64
+ if (!oldest.done)
65
+ cache.delete(oldest.value);
66
+ }
67
+ cache.set(key, { verdict, expiresAt: now() + (verdict.valid ? ttlMs : missTtlMs) });
68
+ }
69
+ async function fetchVerdict(token, signal) {
70
+ return api.request("post", "/api/v1/apps/{app}/user-keys/validate", {
71
+ params: { app },
72
+ body: { token },
73
+ signal,
74
+ });
75
+ }
76
+ /**
77
+ * Validates a presented token. Served from cache when fresh; concurrent
78
+ * callers presenting the same token share one round trip. `fresh: true`
79
+ * bypasses the cache for that call and reseeds it.
80
+ */
81
+ async function validate(token, init = {}) {
82
+ if (!token)
83
+ return { valid: false, reason: "not_found" };
84
+ const key = await digest(token);
85
+ if (!init.fresh && caching) {
86
+ const hit = cache.get(key);
87
+ if (hit && hit.expiresAt > now())
88
+ return hit.verdict;
89
+ if (hit)
90
+ cache.delete(key);
91
+ const pending = inFlight.get(key);
92
+ if (pending)
93
+ return pending;
94
+ }
95
+ const request = fetchVerdict(token, init.signal)
96
+ .then((verdict) => {
97
+ remember(key, verdict);
98
+ return verdict;
99
+ })
100
+ .finally(() => {
101
+ inFlight.delete(key);
102
+ });
103
+ // A failed round trip must not be cached — an IdP blip would otherwise
104
+ // lock every caller out for the whole TTL.
105
+ if (caching)
106
+ inFlight.set(key, request);
107
+ return request;
108
+ }
109
+ return {
110
+ validate,
111
+ /** The keys this app has minted, newest first. Optionally filtered. */
112
+ async list(filter = {}) {
113
+ const { keys } = await api.request("get", "/api/v1/apps/{app}/user-keys", {
114
+ params: { app },
115
+ query: { userId: filter.userId, workspaceId: filter.workspaceId },
116
+ signal: filter.signal,
117
+ });
118
+ return keys;
119
+ },
120
+ /**
121
+ * Mints a key for one of the app's users. The returned `token` is the only
122
+ * copy — show it once and forget it. Scopes must come from the app's
123
+ * declared product permission catalog; unknown ones are a 422, not a
124
+ * silent drop.
125
+ */
126
+ async create(input) {
127
+ const { signal, ...body } = input;
128
+ return api.request("post", "/api/v1/apps/{app}/user-keys", {
129
+ params: { app },
130
+ body,
131
+ signal,
132
+ });
133
+ },
134
+ /**
135
+ * Revokes a key by id (idempotent). Cached verdicts for *other* tokens are
136
+ * untouched; this app never saw the revoked plaintext, so the entry for it
137
+ * can only expire on its own TTL. Call `forget(token)` instead when the
138
+ * plaintext is in hand.
139
+ */
140
+ async revoke(id, init = {}) {
141
+ return api.request("delete", "/api/v1/apps/{app}/user-keys/{id}", {
142
+ params: { app, id },
143
+ signal: init.signal,
144
+ });
145
+ },
146
+ /**
147
+ * The whole check in one call: read the credential off the request,
148
+ * validate it, and confirm every required scope. Returns a result rather
149
+ * than throwing, so the caller owns the response shape.
150
+ */
151
+ async authenticate(request, init = {}) {
152
+ const token = readApiKey(request);
153
+ if (!token)
154
+ return { ok: false, status: 401, reason: "missing" };
155
+ const verdict = await validate(token, { signal: init.signal });
156
+ if (!verdict.valid)
157
+ return { ok: false, status: 401, reason: verdict.reason };
158
+ const required = init.scopes ?? [];
159
+ const missing = required.filter((scope) => !verdict.scopes.includes(scope));
160
+ if (missing.length) {
161
+ return { ok: false, status: 403, reason: "insufficient_scope", missing };
162
+ }
163
+ return { ok: true, key: verdict };
164
+ },
165
+ /** Drops one token's cached verdict, or the whole cache when called bare. */
166
+ async forget(token) {
167
+ if (token === undefined) {
168
+ cache.clear();
169
+ return;
170
+ }
171
+ cache.delete(await digest(token));
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One place where a wire payload becomes a typed value.
3
+ *
4
+ * Everything the IdP hands us — discovery, tokens, userinfo, management API
5
+ * responses — goes through here. A malformed payload raises an `IdpError` that
6
+ * names the offending field, rather than a `TypeError` three frames later on a
7
+ * property that was never there.
8
+ *
9
+ * `502` is the status: the failure is upstream, not in the caller's request.
10
+ */
11
+ import type { z } from "zod";
12
+ export declare function parseWire<T extends z.ZodType>(schema: T, value: unknown, what: string): z.output<T>;
13
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAS5B,wBAAgB,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAWnG"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * One place where a wire payload becomes a typed value.
3
+ *
4
+ * Everything the IdP hands us — discovery, tokens, userinfo, management API
5
+ * responses — goes through here. A malformed payload raises an `IdpError` that
6
+ * names the offending field, rather than a `TypeError` three frames later on a
7
+ * property that was never there.
8
+ *
9
+ * `502` is the status: the failure is upstream, not in the caller's request.
10
+ */
11
+ import { IdpError } from "./errors.js";
12
+ /** `["workspaces", 0, "id"]` -> `"workspaces.0.id"`. */
13
+ function issuePath(path) {
14
+ return path.length ? path.map(String).join(".") : "(root)";
15
+ }
16
+ export function parseWire(schema, value, what) {
17
+ const result = schema.safeParse(value);
18
+ if (result.success)
19
+ return result.data;
20
+ const detail = result.error.issues
21
+ .slice(0, 3)
22
+ .map((issue) => `${issuePath(issue.path)}: ${issue.message}`)
23
+ .join("; ");
24
+ throw new IdpError(`${what} returned a malformed payload (${detail})`, 502, {
25
+ issues: result.error.issues,
26
+ received: value,
27
+ });
28
+ }