@_mustachio/openauth 0.11.0 → 0.13.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/dist/esm/adapters/d1/index.js +18 -9
- package/dist/esm/adapters/d1/migrations.js +7 -2
- package/dist/esm/adapters/d1/session-store.js +39 -0
- package/dist/esm/adapters/d1/session.js +2 -2
- package/dist/esm/adapters/durable-object/index.js +4 -0
- package/dist/esm/adapters/durable-object/session-store.js +42 -0
- package/dist/esm/adapters/dynamo/index.js +24 -7
- package/dist/esm/adapters/dynamo/session-store.js +49 -0
- package/dist/esm/adapters/kms/index.js +6 -2
- package/dist/esm/adapters/kv/index.js +6 -2
- package/dist/esm/adapters/memory/index.js +24 -7
- package/dist/esm/adapters/memory/session-store.js +23 -0
- package/dist/esm/adapters/postgres/executor.js +2 -2
- package/dist/esm/adapters/postgres/index.js +33 -10
- package/dist/esm/adapters/postgres/migrations.js +8 -2
- package/dist/esm/adapters/postgres/session-store.js +42 -0
- package/dist/esm/client.js +27 -27
- package/dist/esm/domain/authorize.js +2 -2
- package/dist/esm/domain/callback.js +121 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +2 -2
- package/dist/esm/domain/dpop.js +4 -4
- package/dist/esm/domain/id-token.js +5 -5
- package/dist/esm/domain/jwt.js +4 -4
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +80 -1
- package/dist/esm/domain/par.js +2 -2
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/scim/discovery.js +206 -0
- package/dist/esm/domain/scim/filter.js +177 -0
- package/dist/esm/domain/scim/handle.js +338 -0
- package/dist/esm/domain/scim/patch.js +568 -0
- package/dist/esm/domain/scim/resource.js +256 -0
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token.js +5 -5
- package/dist/esm/error.js +8 -8
- package/dist/esm/http/cookies.js +19 -2
- package/dist/esm/http/errors.js +4 -4
- package/dist/esm/http/handlers/authorize.js +3 -19
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/metadata.js +2 -2
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/handlers/scim.js +59 -0
- package/dist/esm/http/middleware/tenant.js +31 -6
- package/dist/esm/http/router.js +3 -0
- package/dist/esm/http/schemas/authorize.js +2 -2
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +34 -32
- package/dist/esm/methods/oauth2-factory.js +2 -2
- package/dist/esm/methods/password-hash.js +2 -2
- package/dist/esm/methods/providers/index.js +30 -14
- package/dist/esm/methods/saml-sp/acs.js +232 -0
- package/dist/esm/methods/saml-sp/attributes.js +77 -0
- package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
- package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
- package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
- package/dist/esm/methods/saml-sp/factory.js +81 -0
- package/dist/esm/methods/saml-sp/index.js +7 -0
- package/dist/esm/methods/saml-sp/metadata.js +71 -0
- package/dist/esm/methods/saml-sp/method.js +29 -0
- package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
- package/dist/esm/methods/saml-sp/saml-instance.js +59 -0
- package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
- package/dist/esm/methods/saml-sp/sls.js +154 -0
- package/dist/esm/methods/saml-sp/types.js +0 -0
- package/dist/esm/ports/scim-directory.js +0 -0
- package/dist/esm/types/error.js +5 -0
- package/dist/esm/types/result.js +3 -3
- package/dist/esm/types/scim.js +0 -0
- package/dist/esm/ui/forms.js +3 -3
- package/dist/esm/ui/index.js +8 -5
- package/dist/esm/ui/picker.js +2 -2
- package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
- package/dist/types/adapters/d1/session-store.d.ts +3 -0
- package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
- package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
- package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
- package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
- package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/session-store.d.ts +3 -0
- package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
- package/dist/types/adapters/postgres/session-store.d.ts +3 -0
- package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
- package/dist/types/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +57 -1
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/domain/scim/discovery.d.ts +11 -0
- package/dist/types/domain/scim/discovery.d.ts.map +1 -0
- package/dist/types/domain/scim/filter.d.ts +51 -0
- package/dist/types/domain/scim/filter.d.ts.map +1 -0
- package/dist/types/domain/scim/handle.d.ts +35 -0
- package/dist/types/domain/scim/handle.d.ts.map +1 -0
- package/dist/types/domain/scim/patch.d.ts +73 -0
- package/dist/types/domain/scim/patch.d.ts.map +1 -0
- package/dist/types/domain/scim/resource.d.ts +56 -0
- package/dist/types/domain/scim/resource.d.ts.map +1 -0
- package/dist/types/http/context.d.ts +6 -1
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/cookies.d.ts +9 -1
- package/dist/types/http/cookies.d.ts.map +1 -1
- package/dist/types/http/handlers/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/scim.d.ts +3 -0
- package/dist/types/http/handlers/scim.d.ts.map +1 -0
- package/dist/types/http/middleware/tenant.d.ts.map +1 -1
- package/dist/types/http/router.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/acs.d.ts +4 -0
- package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/attributes.d.ts +45 -0
- package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/factory.d.ts +920 -0
- package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/index.d.ts +24 -0
- package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/metadata.d.ts +50 -0
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/method.d.ts +17 -0
- package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts +93 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/sls.d.ts +4 -0
- package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/types.d.ts +299 -0
- package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
- package/dist/types/ports/audit-log.d.ts +18 -5
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/scim-directory.d.ts +104 -0
- package/dist/types/ports/scim-directory.d.ts.map +1 -0
- package/dist/types/ports/session-store.d.ts +23 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/types/error.d.ts +5 -0
- package/dist/types/types/error.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +83 -0
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +109 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/dist/types/types/scim.d.ts +269 -0
- package/dist/types/types/scim.d.ts.map +1 -0
- package/dist/types/types/tenant.d.ts +10 -0
- package/dist/types/types/tenant.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/adapters/d1/migrations.ts +5 -0
- package/src/adapters/d1/session-store.ts +68 -0
- package/src/adapters/durable-object/session-store.ts +61 -0
- package/src/adapters/dynamo/session-store.ts +63 -0
- package/src/adapters/memory/session-store.ts +39 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/domain/callback.ts +215 -7
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +190 -1
- package/src/domain/scim/discovery.ts +262 -0
- package/src/domain/scim/filter.ts +295 -0
- package/src/domain/scim/handle.ts +597 -0
- package/src/domain/scim/patch.ts +807 -0
- package/src/domain/scim/resource.ts +358 -0
- package/src/http/context.ts +9 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +2 -22
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/method-route.ts +72 -2
- package/src/http/handlers/scim.ts +107 -0
- package/src/http/middleware/tenant.ts +47 -1
- package/src/http/router.ts +9 -0
- package/src/index.ts +23 -0
- package/src/methods/saml-sp/acs.ts +449 -0
- package/src/methods/saml-sp/attributes.ts +146 -0
- package/src/methods/saml-sp/authnrequest.ts +129 -0
- package/src/methods/saml-sp/cache-provider.ts +76 -0
- package/src/methods/saml-sp/cert-rotation.ts +34 -0
- package/src/methods/saml-sp/factory.ts +139 -0
- package/src/methods/saml-sp/index.ts +34 -0
- package/src/methods/saml-sp/metadata.ts +215 -0
- package/src/methods/saml-sp/method.ts +65 -0
- package/src/methods/saml-sp/parse-idp-metadata.ts +158 -0
- package/src/methods/saml-sp/saml-instance.ts +210 -0
- package/src/methods/saml-sp/slo-initiate.ts +180 -0
- package/src/methods/saml-sp/sls.ts +307 -0
- package/src/methods/saml-sp/types.ts +298 -0
- package/src/ports/CONSISTENCY.md +52 -24
- package/src/ports/audit-log.ts +18 -5
- package/src/ports/scim-directory.ts +166 -0
- package/src/ports/session-store.ts +24 -0
- package/src/types/error.ts +11 -0
- package/src/types/idp.ts +86 -0
- package/src/types/method.ts +110 -0
- package/src/types/scim.ts +280 -0
- package/src/types/tenant.ts +10 -0
|
@@ -101,6 +101,30 @@ export type SessionStore = {
|
|
|
101
101
|
ttl: number,
|
|
102
102
|
): Promise<Result<void>>
|
|
103
103
|
consumePar?(requestUri: string): Promise<Result<ParRecord>>
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Optional: per-method-instance scratch storage. Survives across flows
|
|
107
|
+
* (unlike `methodState`, which is per-flow). Use cases include cross-flow
|
|
108
|
+
* deduplication state — e.g., a SAML SP method remembering recently-seen
|
|
109
|
+
* assertion IDs for replay protection.
|
|
110
|
+
*
|
|
111
|
+
* The framework scopes keys per `(tenantId, methodId)` before calling
|
|
112
|
+
* these — adapters see opaque, already-namespaced keys and store the
|
|
113
|
+
* UTF-8 string value verbatim. Methods JSON-encode if they want object
|
|
114
|
+
* state.
|
|
115
|
+
*
|
|
116
|
+
* Strong consistency + TTL respect (same semantics as flow records).
|
|
117
|
+
* `readScratch` returns `unknown_state` if the key is missing or
|
|
118
|
+
* expired. `deleteScratch` is idempotent.
|
|
119
|
+
*
|
|
120
|
+
* Adapters without these methods cannot host methods that depend on
|
|
121
|
+
* scratch; the framework surfaces a clear `unsupported` error through
|
|
122
|
+
* `MethodContext.methodScratch` at call time. Implement all three
|
|
123
|
+
* methods together — partial implementations are not supported.
|
|
124
|
+
*/
|
|
125
|
+
saveScratch?(key: string, value: string, ttlMs: number): Promise<Result<void>>
|
|
126
|
+
readScratch?(key: string): Promise<Result<string>>
|
|
127
|
+
deleteScratch?(key: string): Promise<Result<void>>
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
/**
|
package/src/types/error.ts
CHANGED
|
@@ -50,6 +50,12 @@ export type AuthError =
|
|
|
50
50
|
// endpoints. Use for control-flow signalling that should never escape to a
|
|
51
51
|
// standards-compliant client.
|
|
52
52
|
| { code: "internal_error"; description: string; cause?: unknown }
|
|
53
|
+
// Uniqueness / state conflict. Raised by hosts from `ScimDirectory`
|
|
54
|
+
// when a create or update collides with an existing record — only the
|
|
55
|
+
// host can know, since only the host stores the rows. The SCIM layer
|
|
56
|
+
// renders it as `409` with `scimType: "uniqueness"`. It has no OAuth
|
|
57
|
+
// endpoint mapping and should never reach one.
|
|
58
|
+
| { code: "conflict"; description: string; attribute?: string }
|
|
53
59
|
// RFC 9449 §5.2 — DPoP proof verification failed (bad signature, htm/htu
|
|
54
60
|
// mismatch, iat outside window, replayed jti, missing/mismatched cnf.jkt).
|
|
55
61
|
// Returned as a 400 with `error="invalid_dpop_proof"` on form-body
|
|
@@ -130,6 +136,11 @@ export const authError = {
|
|
|
130
136
|
code: "invalid_target",
|
|
131
137
|
description,
|
|
132
138
|
}),
|
|
139
|
+
conflict: (description: string, attribute?: string): AuthError => ({
|
|
140
|
+
code: "conflict",
|
|
141
|
+
description,
|
|
142
|
+
...(attribute !== undefined ? { attribute } : {}),
|
|
143
|
+
}),
|
|
133
144
|
internalError: (description: string, cause?: unknown): AuthError => ({
|
|
134
145
|
code: "internal_error",
|
|
135
146
|
description,
|
package/src/types/idp.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { AuditLog } from "../ports/audit-log"
|
|
|
7
7
|
import type { ConfigStore } from "../ports/config-store"
|
|
8
8
|
import type { KeyStore } from "../ports/key-store"
|
|
9
9
|
import type { MethodStore } from "../ports/method-store"
|
|
10
|
+
import type { ScimDirectory } from "../ports/scim-directory"
|
|
10
11
|
import type { SessionStore } from "../ports/session-store"
|
|
11
12
|
import type { TokenStore } from "../ports/token-store"
|
|
12
13
|
|
|
@@ -69,6 +70,56 @@ export type FailureEvent = {
|
|
|
69
70
|
error: AuthError
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Input to the optional `IdPOptions.onLogout` hook.
|
|
75
|
+
*
|
|
76
|
+
* Fires when an upstream provider notifies this IdP that a federated
|
|
77
|
+
* session has ended — today, a SAML front-channel `LogoutRequest`
|
|
78
|
+
* delivered to the SP's SLS endpoint. By the time this runs the library
|
|
79
|
+
* has already cryptographically verified the upstream logout message
|
|
80
|
+
* (XML-DSig via the SAML method), so the hook is purely the host's
|
|
81
|
+
* teardown point.
|
|
82
|
+
*
|
|
83
|
+
* The library deliberately does **not** know which OIDC `subject` an
|
|
84
|
+
* upstream identifier (`nameId`) maps to — that mapping lives in the
|
|
85
|
+
* host's `success` callback, not the library. So the host clears its
|
|
86
|
+
* own session and returns the subject (if any) whose library-issued
|
|
87
|
+
* tokens should be revoked; the library then runs the same
|
|
88
|
+
* `revokeAllForSubject` primitive `/end_session` uses. Returning
|
|
89
|
+
* nothing skips library-side revocation (the host handled everything,
|
|
90
|
+
* or there is nothing to revoke).
|
|
91
|
+
*
|
|
92
|
+
* Method-agnostic on purpose: any federation method that can verify an
|
|
93
|
+
* upstream logout signal reuses this. SAML SLO is the first caller;
|
|
94
|
+
* OIDC back-channel logout would be the next.
|
|
95
|
+
*/
|
|
96
|
+
export type LogoutEventInput = {
|
|
97
|
+
tenant: TenantContext
|
|
98
|
+
methodId: string
|
|
99
|
+
methodKind: string
|
|
100
|
+
/** What kind of upstream logout this is. Extensible discriminant. */
|
|
101
|
+
reason: "upstream_slo"
|
|
102
|
+
/**
|
|
103
|
+
* Upstream subject identifier from the verified logout message
|
|
104
|
+
* (SAML `LogoutRequest/NameID`). Absent if the message omitted it.
|
|
105
|
+
*/
|
|
106
|
+
nameId?: string
|
|
107
|
+
/**
|
|
108
|
+
* Upstream session index from the verified logout message
|
|
109
|
+
* (SAML `SessionIndex`), when present. Lets a host that tracks
|
|
110
|
+
* per-session state scope its teardown.
|
|
111
|
+
*/
|
|
112
|
+
sessionIndex?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Return of `IdPOptions.onLogout`. `revokeSubject` names the OIDC
|
|
117
|
+
* subject whose library-issued refresh tokens the library should
|
|
118
|
+
* revoke (the host resolves it from `nameId` — only the host has that
|
|
119
|
+
* map). Omit / return nothing to skip library-side revocation.
|
|
120
|
+
*/
|
|
121
|
+
export type LogoutHookResult = { revokeSubject?: string } | void
|
|
122
|
+
|
|
72
123
|
/**
|
|
73
124
|
* Optional hook called at `/token` time, after PKCE has succeeded and
|
|
74
125
|
* after the `success` callback has produced a `SubjectClaim`, but
|
|
@@ -230,6 +281,17 @@ export type IdPOptions = {
|
|
|
230
281
|
auditLog?: AuditLog
|
|
231
282
|
/** Optional — falls back to `ConfigStore` for `MethodConfig` lookups. */
|
|
232
283
|
methodStore?: MethodStore
|
|
284
|
+
/**
|
|
285
|
+
* The host's user directory, as SCIM needs to see it. Supply it to
|
|
286
|
+
* serve `/scim/v2/*`; omit it and those routes answer 501 regardless
|
|
287
|
+
* of per-tenant config.
|
|
288
|
+
*
|
|
289
|
+
* The library owns the SCIM protocol and stores no user data — every
|
|
290
|
+
* read and write goes through this port to the host's own tables. See
|
|
291
|
+
* `SCIM-AD2` in `docs/plans/claude/scim-plan.md` and
|
|
292
|
+
* `INTEGRATION.md` § SCIM.
|
|
293
|
+
*/
|
|
294
|
+
scimDirectory?: ScimDirectory
|
|
233
295
|
|
|
234
296
|
/** Issuer URL. Function form lets multi-tenant deployments derive it per request. */
|
|
235
297
|
issuerUrl: string | ((req: Request) => string)
|
|
@@ -259,6 +321,30 @@ export type IdPOptions = {
|
|
|
259
321
|
onFailure?: (event: FailureEvent) => Promise<void>
|
|
260
322
|
}
|
|
261
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Optional hook fired when an upstream provider signals that a
|
|
326
|
+
* federated session ended — SAML front-channel Single Logout today.
|
|
327
|
+
* The library has already verified the signed logout message; this
|
|
328
|
+
* hook is where the host tears down its own session and names the
|
|
329
|
+
* OIDC subject (if any) whose library-issued tokens to revoke. See
|
|
330
|
+
* the `LogoutEventInput` / `LogoutHookResult` type docs.
|
|
331
|
+
*
|
|
332
|
+
* Unlike `hooks.onSuccess`/`onFailure` (observation only) this hook
|
|
333
|
+
* **influences** library behaviour — its return drives token
|
|
334
|
+
* revocation — so it sits at the top level alongside `success`.
|
|
335
|
+
*
|
|
336
|
+
* Absent ⇒ the library still verifies the logout, emits a
|
|
337
|
+
* `session_logout` audit event, and returns the protocol
|
|
338
|
+
* `LogoutResponse`, but performs no token revocation (it cannot map
|
|
339
|
+
* the upstream id to a subject without the host).
|
|
340
|
+
*
|
|
341
|
+
* If it throws, the SLS endpoint fails closed with an internal error
|
|
342
|
+
* rather than acknowledging a logout it could not fully process.
|
|
343
|
+
*/
|
|
344
|
+
onLogout?: (
|
|
345
|
+
input: LogoutEventInput,
|
|
346
|
+
) => Promise<LogoutHookResult> | LogoutHookResult
|
|
347
|
+
|
|
262
348
|
/**
|
|
263
349
|
* Optional escape hatch for high-sensitivity deployments — see the
|
|
264
350
|
* `PersistUpstreamTokens` type doc.
|
package/src/types/method.ts
CHANGED
|
@@ -56,6 +56,37 @@ export type AuthMethod<P = unknown, S = unknown> = {
|
|
|
56
56
|
* same factory get distinct URL spaces).
|
|
57
57
|
*/
|
|
58
58
|
routes: Record<string, MethodHandler<P, S>>
|
|
59
|
+
/**
|
|
60
|
+
* Opt-in allowlist of route keys (same `"GET /metadata"` string form
|
|
61
|
+
* as `routes` keys) that the framework dispatches **without** a flow
|
|
62
|
+
* cookie or flow record — anonymous, unauthenticated GETs. The sole
|
|
63
|
+
* intended use is publishing static, per-instance descriptive
|
|
64
|
+
* documents (SAML SP metadata XML). The handler receives
|
|
65
|
+
* `ctx.flow === null` and `ctx.methodState === null` and MUST be a
|
|
66
|
+
* pure function of `ctx.tenant` + `ctx.dispatch` + its captured
|
|
67
|
+
* config — it must not assume an authenticated principal.
|
|
68
|
+
*
|
|
69
|
+
* Absent (the default for every method) ⇒ behaviour is unchanged:
|
|
70
|
+
* every `/m/<id>/*` request requires the `idp.flow` cookie. The
|
|
71
|
+
* cookie gate is skipped *only* for a route key a method explicitly
|
|
72
|
+
* lists here — fail-closed by construction.
|
|
73
|
+
*/
|
|
74
|
+
publicRoutes?: ReadonlyArray<string>
|
|
75
|
+
/**
|
|
76
|
+
* Opt-in: this method instance handles **unsolicited** upstream
|
|
77
|
+
* callbacks — a `POST /cb/<methodId>` carrying a provider assertion
|
|
78
|
+
* with no framework state envelope and no prior flow (SAML
|
|
79
|
+
* IdP-initiated SSO). When `true`, the framework, instead of
|
|
80
|
+
* rejecting a stateless callback, dispatches `GET /callback` with
|
|
81
|
+
* `flow === null` and a derived `dispatch` (issuer/ACS); the handler
|
|
82
|
+
* verifies the assertion and returns `success` **with
|
|
83
|
+
* `unsolicitedBinding`**. Absent (every method's default) ⇒ a
|
|
84
|
+
* stateless callback stays an `invalid_request` (the conservative
|
|
85
|
+
* default — many deployments do not want IdP-initiated). General
|
|
86
|
+
* capability, not SAML-specific; set per instance (a SAML instance
|
|
87
|
+
* sets it only when its config enables IdP-initiated).
|
|
88
|
+
*/
|
|
89
|
+
unsolicitedCallback?: boolean
|
|
59
90
|
/**
|
|
60
91
|
* Token-exchange function for the `/token` endpoint when the method
|
|
61
92
|
* participates in client-credentials-style flows (e.g. `m2m`). Most
|
|
@@ -102,6 +133,47 @@ export type MethodContext<S = unknown> = {
|
|
|
102
133
|
* and the relevant data is on `flow`).
|
|
103
134
|
*/
|
|
104
135
|
dispatch: MethodDispatchData | null
|
|
136
|
+
/**
|
|
137
|
+
* Per-method-instance scratch storage scoped to
|
|
138
|
+
* `(tenant.id, method.id)`. Survives across flows — distinct from
|
|
139
|
+
* `methodState`, which is per-flow.
|
|
140
|
+
*
|
|
141
|
+
* Most methods do NOT need this. It exists for cross-flow
|
|
142
|
+
* deduplication patterns such as SAML SP assertion-ID replay
|
|
143
|
+
* protection.
|
|
144
|
+
*
|
|
145
|
+
* Backed by `SessionStore.{saveScratch,readScratch,deleteScratch}`
|
|
146
|
+
* when those optional methods are implemented. Against adapters that
|
|
147
|
+
* don't implement them, every call returns
|
|
148
|
+
* `{ ok: false, error: unsupported }` — the method should surface a
|
|
149
|
+
* `MethodResult.error` with a clear message, not silently degrade.
|
|
150
|
+
*/
|
|
151
|
+
methodScratch: MethodScratch
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Caller-facing API for per-method-instance scratch. The framework
|
|
156
|
+
* scopes user-supplied keys with a `(tenantId, methodId)` prefix before
|
|
157
|
+
* delegating to `SessionStore` — adapters never see raw method keys.
|
|
158
|
+
*
|
|
159
|
+
* Values are UTF-8 strings; methods JSON-encode if they want to stash
|
|
160
|
+
* objects. Keeping the port-level type narrow simplifies adapter
|
|
161
|
+
* implementations (one TEXT column, one Dynamo `S` attribute, etc.).
|
|
162
|
+
*/
|
|
163
|
+
export type MethodScratch = {
|
|
164
|
+
/**
|
|
165
|
+
* Persist `value` under `key` with the given TTL. Overwrites prior
|
|
166
|
+
* value for the same key. `ttlMs` must be positive.
|
|
167
|
+
*/
|
|
168
|
+
put(key: string, value: string, ttlMs: number): Promise<Result<void>>
|
|
169
|
+
/**
|
|
170
|
+
* Read the value previously stored at `key`. Returns `unknown_state`
|
|
171
|
+
* if the key is missing or expired (the underlying adapter MAY
|
|
172
|
+
* lazily evict expired entries on read).
|
|
173
|
+
*/
|
|
174
|
+
get(key: string): Promise<Result<string>>
|
|
175
|
+
/** Idempotent. Resolves `ok` whether the key existed or not. */
|
|
176
|
+
delete(key: string): Promise<Result<void>>
|
|
105
177
|
}
|
|
106
178
|
|
|
107
179
|
/** Framework-supplied data available to the method at `/authorize` time. */
|
|
@@ -143,6 +215,27 @@ export type MethodResult<P = unknown, S = unknown> =
|
|
|
143
215
|
saveMethodState?: S
|
|
144
216
|
/** Serialized into a `Cache-Control` header by the framework. */
|
|
145
217
|
cache?: CachePolicy
|
|
218
|
+
/**
|
|
219
|
+
* Verified upstream-logout notification. Set **only** by a
|
|
220
|
+
* flowless **public** logout route (e.g. SAML front-channel SLS)
|
|
221
|
+
* once it has cryptographically verified the inbound logout
|
|
222
|
+
* message. The method stays port-free — it proves authenticity
|
|
223
|
+
* and builds the protocol response (`response` = the signed
|
|
224
|
+
* `LogoutResponse` redirect / ack); the privileged side effect
|
|
225
|
+
* runs in the framework.
|
|
226
|
+
*
|
|
227
|
+
* When present on a public route the framework fires
|
|
228
|
+
* `IdPOptions.onLogout` and, if that returns `{ revokeSubject }`,
|
|
229
|
+
* runs `revokeAllForSubject` — then returns `response`. Ignored
|
|
230
|
+
* on non-public (flow-bearing) routes: an authenticated method
|
|
231
|
+
* route never logs anyone out. Mirrors the Phase 2 V′ pattern
|
|
232
|
+
* (`success.unsolicitedBinding`): an optional field on an
|
|
233
|
+
* existing variant, not a new `MethodResult` kind.
|
|
234
|
+
*/
|
|
235
|
+
logout?: {
|
|
236
|
+
nameId?: string
|
|
237
|
+
sessionIndex?: string
|
|
238
|
+
}
|
|
146
239
|
}
|
|
147
240
|
/**
|
|
148
241
|
* Authentication succeeded. The HTTP layer hands `providerSubject` +
|
|
@@ -155,6 +248,23 @@ export type MethodResult<P = unknown, S = unknown> =
|
|
|
155
248
|
providerSubject: string
|
|
156
249
|
properties: P
|
|
157
250
|
setCookies?: SetCookie[]
|
|
251
|
+
/**
|
|
252
|
+
* RP binding for a **flowless** (unsolicited / IdP-initiated)
|
|
253
|
+
* authentication. Consulted **only** when the method ran with no
|
|
254
|
+
* `flow` (e.g. an unsolicited SAML Response — see
|
|
255
|
+
* `AuthMethod.unsolicitedCallback`). On the normal SP-initiated
|
|
256
|
+
* path a `flow` exists and this is ignored; on the flowless path
|
|
257
|
+
* it is **required** (the framework has no pending RP request to
|
|
258
|
+
* read `client_id` / `redirect_uri` / `scope` from, so the method
|
|
259
|
+
* supplies the operator-configured defaults). The framework still
|
|
260
|
+
* validates `clientId` / `redirectUri` against the tenant's
|
|
261
|
+
* registered client before issuing a code.
|
|
262
|
+
*/
|
|
263
|
+
unsolicitedBinding?: {
|
|
264
|
+
clientId: string
|
|
265
|
+
redirectUri: string
|
|
266
|
+
scopes: string[]
|
|
267
|
+
}
|
|
158
268
|
}
|
|
159
269
|
/** User refused / failed auth in a non-error way (e.g. consent declined). */
|
|
160
270
|
| { kind: "denied"; reason: string; setCookies?: SetCookie[] }
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCIM 2.0 — public types.
|
|
3
|
+
*
|
|
4
|
+
* The library is the SCIM **Service Provider** (the system being
|
|
5
|
+
* provisioned into); corporate IdPs are the SCIM clients. Inbound only —
|
|
6
|
+
* we never originate provisioning traffic. See `SCIM-AD1` in
|
|
7
|
+
* `docs/plans/claude/scim-plan.md`.
|
|
8
|
+
*
|
|
9
|
+
* These types are the whole contract between the library's protocol
|
|
10
|
+
* layer and the host's data layer. No SCIM JSON shape, path expression,
|
|
11
|
+
* or filter string reaches the host: it receives validated, normalized
|
|
12
|
+
* values and returns records. See `SCIM-AD2`.
|
|
13
|
+
*
|
|
14
|
+
* Plain TypeScript, no third-party types, no Node APIs — SCIM is JSON
|
|
15
|
+
* over HTTP, so unlike SAML this surface lives on the root entry and
|
|
16
|
+
* stays edge-clean.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** RFC 7643 §4.1.2 — a multi-valued attribute entry. */
|
|
20
|
+
export type ScimMultiValue = {
|
|
21
|
+
value: string
|
|
22
|
+
/** e.g. `"work"` / `"home"` / `"mobile"`. */
|
|
23
|
+
type?: string
|
|
24
|
+
/** At most one entry per attribute should be primary. */
|
|
25
|
+
primary?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** RFC 7643 §4.1.1 — the `name` complex attribute. */
|
|
29
|
+
export type ScimName = {
|
|
30
|
+
formatted?: string
|
|
31
|
+
familyName?: string
|
|
32
|
+
givenName?: string
|
|
33
|
+
middleName?: string
|
|
34
|
+
honorificPrefix?: string
|
|
35
|
+
honorificSuffix?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* RFC 7643 §4.3 — the enterprise user extension
|
|
40
|
+
* (`urn:ietf:params:scim:schemas:extension:enterprise:2.0:User`).
|
|
41
|
+
* Okta and Entra both populate parts of this; `department` and
|
|
42
|
+
* `manager` are the ones hosts usually care about.
|
|
43
|
+
*/
|
|
44
|
+
export type ScimEnterpriseUser = {
|
|
45
|
+
employeeNumber?: string
|
|
46
|
+
costCenter?: string
|
|
47
|
+
organization?: string
|
|
48
|
+
division?: string
|
|
49
|
+
department?: string
|
|
50
|
+
/** `manager.value` is the manager's SCIM `id`. */
|
|
51
|
+
manager?: { value?: string; displayName?: string }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A user as the **host** stores it. Returned from every `ScimDirectory`
|
|
56
|
+
* read and write.
|
|
57
|
+
*
|
|
58
|
+
* `id` is the host's stable identifier and becomes the SCIM resource id
|
|
59
|
+
* in URLs — it must be opaque, stable, and URL-safe.
|
|
60
|
+
*
|
|
61
|
+
* `externalId` is the **IdP's** identifier for the same person. It is
|
|
62
|
+
* what reconciliation depends on in practice (a user can change
|
|
63
|
+
* `userName`; `externalId` survives), so hosts should persist and index
|
|
64
|
+
* it even though SCIM marks it optional.
|
|
65
|
+
*/
|
|
66
|
+
export type ScimUserRecord = {
|
|
67
|
+
id: string
|
|
68
|
+
externalId?: string
|
|
69
|
+
userName: string
|
|
70
|
+
/** `false` is the normal deprovisioning signal — see `SCIM-AD8`. */
|
|
71
|
+
active: boolean
|
|
72
|
+
name?: ScimName
|
|
73
|
+
displayName?: string
|
|
74
|
+
emails?: ScimMultiValue[]
|
|
75
|
+
phoneNumbers?: ScimMultiValue[]
|
|
76
|
+
enterprise?: ScimEnterpriseUser
|
|
77
|
+
/** Unix ms. Surfaced as `meta.created` when present. */
|
|
78
|
+
createdAt?: number
|
|
79
|
+
/** Unix ms. Surfaced as `meta.lastModified` when present. */
|
|
80
|
+
updatedAt?: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A validated create / replace payload. Same shape as a record minus
|
|
85
|
+
* the host-assigned `id` and timestamps.
|
|
86
|
+
*
|
|
87
|
+
* `active` is always present: SCIM defaults it to `true` on create, and
|
|
88
|
+
* resolving that default in the library rather than the host keeps the
|
|
89
|
+
* semantics in one place.
|
|
90
|
+
*
|
|
91
|
+
* Note there is no `password` field, deliberately — see `SCIM-AD1`
|
|
92
|
+
* non-goals. A `password` in the payload is refused, not silently
|
|
93
|
+
* dropped.
|
|
94
|
+
*/
|
|
95
|
+
export type ScimUserWrite = {
|
|
96
|
+
externalId?: string
|
|
97
|
+
userName: string
|
|
98
|
+
active: boolean
|
|
99
|
+
name?: ScimName
|
|
100
|
+
displayName?: string
|
|
101
|
+
emails?: ScimMultiValue[]
|
|
102
|
+
phoneNumbers?: ScimMultiValue[]
|
|
103
|
+
enterprise?: ScimEnterpriseUser
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A **normalized** PATCH delta (`SCIM-AD6`).
|
|
108
|
+
*
|
|
109
|
+
* The library resolves `urn:ietf:params:scim:api:messages:2.0:PatchOp`
|
|
110
|
+
* operations — including path expressions like
|
|
111
|
+
* `emails[type eq "work"].value` and the different shapes Okta and Entra
|
|
112
|
+
* emit for the same intent — into this flat delta. The host never parses
|
|
113
|
+
* a SCIM path.
|
|
114
|
+
*
|
|
115
|
+
* Present key ⇒ set to that value. Absent key ⇒ leave untouched.
|
|
116
|
+
* `null` ⇒ the attribute was removed and should be cleared.
|
|
117
|
+
*/
|
|
118
|
+
export type ScimUserPatch = {
|
|
119
|
+
externalId?: string | null
|
|
120
|
+
userName?: string
|
|
121
|
+
active?: boolean
|
|
122
|
+
name?: ScimName | null
|
|
123
|
+
displayName?: string | null
|
|
124
|
+
emails?: ScimMultiValue[] | null
|
|
125
|
+
phoneNumbers?: ScimMultiValue[] | null
|
|
126
|
+
enterprise?: ScimEnterpriseUser | null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Attributes the filter subset may reference (`SCIM-AD3`).
|
|
131
|
+
*
|
|
132
|
+
* `emails.value` is the normalized form of the complex path Entra emits
|
|
133
|
+
* (`emails[type eq "work"].value`) — the host matches on any email
|
|
134
|
+
* value and need not model the `type` qualifier.
|
|
135
|
+
*/
|
|
136
|
+
export type ScimFilterAttribute =
|
|
137
|
+
| "id"
|
|
138
|
+
| "userName"
|
|
139
|
+
| "externalId"
|
|
140
|
+
| "active"
|
|
141
|
+
| "emails.value"
|
|
142
|
+
/** Groups only. */
|
|
143
|
+
| "displayName"
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The parsed filter, as a small typed tree. The host never receives the
|
|
147
|
+
* raw filter string — parsing is the library's job, and an expression
|
|
148
|
+
* outside the supported subset is rejected with `400 invalidFilter`
|
|
149
|
+
* before the port is called.
|
|
150
|
+
*/
|
|
151
|
+
export type ScimFilter =
|
|
152
|
+
| { op: "eq"; attribute: ScimFilterAttribute; value: string | boolean }
|
|
153
|
+
| { op: "and"; left: ScimFilter; right: ScimFilter }
|
|
154
|
+
|
|
155
|
+
/** Input to `ScimDirectory.findUsers`. */
|
|
156
|
+
export type ScimUserQuery = {
|
|
157
|
+
/** Absent ⇒ unfiltered list. */
|
|
158
|
+
filter?: ScimFilter
|
|
159
|
+
/**
|
|
160
|
+
* **1-based**, per RFC 7644 §3.4.2.4 — not zero. Already clamped to
|
|
161
|
+
* `>= 1` by the library.
|
|
162
|
+
*/
|
|
163
|
+
startIndex: number
|
|
164
|
+
/** Page size, already clamped to the connection's configured maximum. */
|
|
165
|
+
count: number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* One page of results. `totalResults` is the count of everything
|
|
170
|
+
* matching the filter, not the size of `resources` — SCIM clients use it
|
|
171
|
+
* to drive pagination, so a host that returns the page size here will
|
|
172
|
+
* make Okta loop or stop early.
|
|
173
|
+
*/
|
|
174
|
+
export type ScimPage<T> = {
|
|
175
|
+
resources: T[]
|
|
176
|
+
totalResults: number
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One group membership entry. `value` is the member's SCIM `id` — for
|
|
181
|
+
* us always a User id, since we do not support nested groups.
|
|
182
|
+
*/
|
|
183
|
+
export type ScimGroupMember = {
|
|
184
|
+
value: string
|
|
185
|
+
/** Human label the IdP supplied. Advisory; not authoritative. */
|
|
186
|
+
display?: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A group as the **host** stores it. */
|
|
190
|
+
export type ScimGroupRecord = {
|
|
191
|
+
id: string
|
|
192
|
+
externalId?: string
|
|
193
|
+
displayName: string
|
|
194
|
+
/**
|
|
195
|
+
* Omit when the caller asked for `excludedAttributes=members`, which
|
|
196
|
+
* Okta does while enumerating groups. Distinguish "not requested"
|
|
197
|
+
* (omit) from "no members" (`[]`) — returning `[]` for the former
|
|
198
|
+
* tells the client the group was emptied.
|
|
199
|
+
*/
|
|
200
|
+
members?: ScimGroupMember[]
|
|
201
|
+
/** Unix ms → `meta.created`. */
|
|
202
|
+
createdAt?: number
|
|
203
|
+
/** Unix ms → `meta.lastModified`. */
|
|
204
|
+
updatedAt?: number
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A validated group create / replace payload. */
|
|
208
|
+
export type ScimGroupWrite = {
|
|
209
|
+
externalId?: string
|
|
210
|
+
displayName: string
|
|
211
|
+
/** Full membership for a create or replace. */
|
|
212
|
+
members?: ScimGroupMember[]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A normalized group PATCH.
|
|
217
|
+
*
|
|
218
|
+
* Membership is handled differently from every user attribute, and
|
|
219
|
+
* deliberately so (`SCIM-AD9`). For a user, the library resolves a
|
|
220
|
+
* targeted patch into the complete new value because the lists involved
|
|
221
|
+
* are small and bounded. Group membership is neither: resolving
|
|
222
|
+
* "add one member" against a 20,000-member group would mean reading all
|
|
223
|
+
* 20,000 rows and handing them back on every single change.
|
|
224
|
+
*
|
|
225
|
+
* So the client's *intent* is preserved instead. `addMembers` /
|
|
226
|
+
* `removeMembers` are incremental and let the host issue one insert or
|
|
227
|
+
* delete; `members` is a full replacement. They are mutually exclusive
|
|
228
|
+
* — the library never emits `members` alongside either incremental
|
|
229
|
+
* field, so a host can branch on which is present without ordering
|
|
230
|
+
* concerns.
|
|
231
|
+
*/
|
|
232
|
+
export type ScimGroupPatch = {
|
|
233
|
+
displayName?: string
|
|
234
|
+
externalId?: string | null
|
|
235
|
+
/** Replace the entire membership with exactly these members. */
|
|
236
|
+
members?: ScimGroupMember[]
|
|
237
|
+
/** Add these members, leaving existing ones alone. Idempotent. */
|
|
238
|
+
addMembers?: ScimGroupMember[]
|
|
239
|
+
/** Remove these member ids. Removing a non-member is not an error. */
|
|
240
|
+
removeMembers?: string[]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Input to `ScimDirectory.findGroups`. */
|
|
244
|
+
export type ScimGroupQuery = {
|
|
245
|
+
filter?: ScimFilter
|
|
246
|
+
/** 1-based. */
|
|
247
|
+
startIndex: number
|
|
248
|
+
count: number
|
|
249
|
+
/**
|
|
250
|
+
* `true` when the client sent `excludedAttributes=members`. The host
|
|
251
|
+
* may skip loading membership entirely — Okta sets this while
|
|
252
|
+
* enumerating groups, and honouring it is the difference between a
|
|
253
|
+
* cheap listing and a fan-out read per group.
|
|
254
|
+
*/
|
|
255
|
+
excludeMembers: boolean
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Per-tenant SCIM connection config, carried on `TenantConfig.scim`.
|
|
260
|
+
*
|
|
261
|
+
* SCIM is not an `AuthMethod` — no `/authorize`, no flow, no user agent
|
|
262
|
+
* — so it is tenant-level config rather than a `MethodConfig`
|
|
263
|
+
* (`SCIM-AD5`).
|
|
264
|
+
*/
|
|
265
|
+
export type ScimConfig = {
|
|
266
|
+
/** `false` (or absent config) ⇒ `/scim/v2/*` responds 403 for this tenant. */
|
|
267
|
+
enabled: boolean
|
|
268
|
+
/**
|
|
269
|
+
* Hash of the bearer token issued to the IdP, produced with
|
|
270
|
+
* `hashClientSecret` — the same treatment as
|
|
271
|
+
* `ClientConfig.secretHash`. Never store the raw token.
|
|
272
|
+
*/
|
|
273
|
+
tokenHash: string
|
|
274
|
+
/**
|
|
275
|
+
* Largest page this connection will return, whatever the client asks
|
|
276
|
+
* for. Defaults to 100; hard-capped at 500 so a hostile or
|
|
277
|
+
* misconfigured `count` cannot ask the host for an unbounded read.
|
|
278
|
+
*/
|
|
279
|
+
maxPageSize?: number
|
|
280
|
+
}
|
package/src/types/tenant.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* providers carries only `{ tenantId, flowId, nonce, kid }`. Everything else
|
|
12
12
|
* lives in the server-side `FlowRecord` (see `./flow.ts`).
|
|
13
13
|
*/
|
|
14
|
+
import type { ScimConfig } from "./scim"
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Branded tenant id — **opaque to the framework**.
|
|
@@ -199,6 +200,15 @@ export type TenantConfig = {
|
|
|
199
200
|
refreshTtl?: number
|
|
200
201
|
/** Override default access-token TTL (seconds). */
|
|
201
202
|
accessTtl?: number
|
|
203
|
+
/**
|
|
204
|
+
* SCIM 2.0 provisioning for this tenant. Absent or `enabled: false` ⇒
|
|
205
|
+
* `/scim/v2/*` answers 403 for this tenant.
|
|
206
|
+
*
|
|
207
|
+
* Tenant-level rather than a `MethodConfig` because SCIM is not an
|
|
208
|
+
* auth method — no `/authorize`, no flow, no user agent. See
|
|
209
|
+
* `SCIM-AD5` in `docs/plans/claude/scim-plan.md`.
|
|
210
|
+
*/
|
|
211
|
+
scim?: ScimConfig
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
/**
|