@_mustachio/openauth 0.12.0 → 0.13.1

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 (98) hide show
  1. package/dist/esm/domain/scim/discovery.js +206 -0
  2. package/dist/esm/domain/scim/filter.js +177 -0
  3. package/dist/esm/domain/scim/handle.js +338 -0
  4. package/dist/esm/domain/scim/patch.js +568 -0
  5. package/dist/esm/domain/scim/resource.js +256 -0
  6. package/dist/esm/http/handlers/scim.js +59 -0
  7. package/dist/esm/http/middleware/tenant.js +25 -0
  8. package/dist/esm/http/router.js +3 -0
  9. package/dist/esm/index.js +1 -0
  10. package/dist/esm/methods/saml-sp/acs.js +44 -20
  11. package/dist/esm/methods/saml-sp/attributes.js +2 -0
  12. package/dist/esm/methods/saml-sp/authnrequest.js +2 -2
  13. package/dist/esm/methods/saml-sp/factory.js +12 -1
  14. package/dist/esm/methods/saml-sp/metadata.js +5 -3
  15. package/dist/esm/methods/saml-sp/saml-instance.js +13 -3
  16. package/dist/esm/methods/saml-sp/slo-initiate.js +2 -2
  17. package/dist/esm/methods/saml-sp/sls.js +12 -4
  18. package/dist/esm/ports/scim-directory.js +0 -0
  19. package/dist/esm/types/error.js +5 -0
  20. package/dist/esm/types/scim.js +0 -0
  21. package/dist/types/domain/scim/discovery.d.ts +11 -0
  22. package/dist/types/domain/scim/discovery.d.ts.map +1 -0
  23. package/dist/types/domain/scim/filter.d.ts +51 -0
  24. package/dist/types/domain/scim/filter.d.ts.map +1 -0
  25. package/dist/types/domain/scim/handle.d.ts +35 -0
  26. package/dist/types/domain/scim/handle.d.ts.map +1 -0
  27. package/dist/types/domain/scim/patch.d.ts +73 -0
  28. package/dist/types/domain/scim/patch.d.ts.map +1 -0
  29. package/dist/types/domain/scim/resource.d.ts +56 -0
  30. package/dist/types/domain/scim/resource.d.ts.map +1 -0
  31. package/dist/types/http/context.d.ts +3 -0
  32. package/dist/types/http/context.d.ts.map +1 -1
  33. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  34. package/dist/types/http/handlers/scim.d.ts +3 -0
  35. package/dist/types/http/handlers/scim.d.ts.map +1 -0
  36. package/dist/types/http/middleware/tenant.d.ts.map +1 -1
  37. package/dist/types/http/router.d.ts.map +1 -1
  38. package/dist/types/index.d.ts +2 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -1
  41. package/dist/types/methods/saml-sp/attributes.d.ts +5 -0
  42. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -1
  43. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -1
  44. package/dist/types/methods/saml-sp/factory.d.ts +217 -1
  45. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -1
  46. package/dist/types/methods/saml-sp/metadata.d.ts +8 -0
  47. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -1
  48. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -1
  49. package/dist/types/methods/saml-sp/saml-instance.d.ts +12 -0
  50. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -1
  51. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -1
  52. package/dist/types/methods/saml-sp/sls.d.ts +20 -0
  53. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -1
  54. package/dist/types/methods/saml-sp/types.d.ts +95 -0
  55. package/dist/types/methods/saml-sp/types.d.ts.map +1 -1
  56. package/dist/types/ports/scim-directory.d.ts +104 -0
  57. package/dist/types/ports/scim-directory.d.ts.map +1 -0
  58. package/dist/types/ports/session-store.d.ts.map +1 -1
  59. package/dist/types/types/error.d.ts +5 -0
  60. package/dist/types/types/error.d.ts.map +1 -1
  61. package/dist/types/types/idp.d.ts +12 -0
  62. package/dist/types/types/idp.d.ts.map +1 -1
  63. package/dist/types/types/result.d.ts.map +1 -1
  64. package/dist/types/types/scim.d.ts +269 -0
  65. package/dist/types/types/scim.d.ts.map +1 -0
  66. package/dist/types/types/tenant.d.ts +10 -0
  67. package/dist/types/types/tenant.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/src/domain/scim/discovery.ts +262 -0
  70. package/src/domain/scim/filter.ts +295 -0
  71. package/src/domain/scim/handle.ts +597 -0
  72. package/src/domain/scim/patch.ts +807 -0
  73. package/src/domain/scim/resource.ts +358 -0
  74. package/src/http/context.ts +3 -0
  75. package/src/http/handlers/authorize.ts +0 -1
  76. package/src/http/handlers/method-route.ts +1 -4
  77. package/src/http/handlers/scim.ts +107 -0
  78. package/src/http/middleware/tenant.ts +47 -1
  79. package/src/http/router.ts +9 -0
  80. package/src/index.ts +20 -0
  81. package/src/methods/saml-sp/acs.ts +98 -49
  82. package/src/methods/saml-sp/attributes.ts +12 -3
  83. package/src/methods/saml-sp/authnrequest.ts +3 -2
  84. package/src/methods/saml-sp/factory.ts +24 -3
  85. package/src/methods/saml-sp/metadata.ts +19 -8
  86. package/src/methods/saml-sp/parse-idp-metadata.ts +3 -9
  87. package/src/methods/saml-sp/saml-instance.ts +42 -5
  88. package/src/methods/saml-sp/slo-initiate.ts +6 -3
  89. package/src/methods/saml-sp/sls.ts +35 -5
  90. package/src/methods/saml-sp/types.ts +97 -6
  91. package/src/ports/CONSISTENCY.md +28 -25
  92. package/src/ports/scim-directory.ts +166 -0
  93. package/src/ports/session-store.ts +1 -5
  94. package/src/types/error.ts +11 -0
  95. package/src/types/idp.ts +13 -3
  96. package/src/types/result.ts +1 -2
  97. package/src/types/scim.ts +280 -0
  98. package/src/types/tenant.ts +12 -10
@@ -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
+ }
@@ -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**.
@@ -49,9 +50,7 @@ export const asTenantId = (value: string): TenantId => value as TenantId
49
50
 
50
51
  /** OAuth 2.1 grant types in scope for this IdP. Implicit is intentionally absent. */
51
52
  export type GrantType =
52
- | "authorization_code"
53
- | "refresh_token"
54
- | "client_credentials"
53
+ "authorization_code" | "refresh_token" | "client_credentials"
55
54
 
56
55
  /**
57
56
  * `MethodType` is the canonical discriminator for an `AuthMethod`. It mirrors
@@ -59,13 +58,7 @@ export type GrantType =
59
58
  * form to render; the framework uses it as a routing / dispatch hint.
60
59
  */
61
60
  export type MethodType =
62
- | "oauth2"
63
- | "oidc"
64
- | "password"
65
- | "code"
66
- | "m2m"
67
- | "passkey"
68
- | "custom"
61
+ "oauth2" | "oidc" | "password" | "code" | "m2m" | "passkey" | "custom"
69
62
 
70
63
  /**
71
64
  * Per-tenant configuration for a single auth method **instance**.
@@ -199,6 +192,15 @@ export type TenantConfig = {
199
192
  refreshTtl?: number
200
193
  /** Override default access-token TTL (seconds). */
201
194
  accessTtl?: number
195
+ /**
196
+ * SCIM 2.0 provisioning for this tenant. Absent or `enabled: false` ⇒
197
+ * `/scim/v2/*` answers 403 for this tenant.
198
+ *
199
+ * Tenant-level rather than a `MethodConfig` because SCIM is not an
200
+ * auth method — no `/authorize`, no flow, no user agent. See
201
+ * `SCIM-AD5` in `docs/plans/claude/scim-plan.md`.
202
+ */
203
+ scim?: ScimConfig
202
204
  }
203
205
 
204
206
  /**