@atlasauth/emulator-core 0.1.0 → 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 (300) hide show
  1. package/dist/adopt.d.ts +50 -0
  2. package/dist/adopt.js +57 -0
  3. package/dist/adopt.js.map +1 -0
  4. package/dist/coverage.d.ts +15 -0
  5. package/dist/coverage.js +31 -0
  6. package/dist/coverage.js.map +1 -0
  7. package/dist/crypto-util.d.ts +10 -0
  8. package/dist/crypto-util.js +76 -0
  9. package/dist/crypto-util.js.map +1 -0
  10. package/dist/custom-html.d.ts +57 -0
  11. package/dist/custom-html.js +491 -0
  12. package/dist/custom-html.js.map +1 -0
  13. package/dist/engine.d.ts +34 -3
  14. package/dist/engine.js +57 -8
  15. package/dist/engine.js.map +1 -1
  16. package/dist/error-catalog.js +16 -0
  17. package/dist/error-catalog.js.map +1 -1
  18. package/dist/event-gate.d.ts +3 -0
  19. package/dist/event-gate.js +430 -0
  20. package/dist/event-gate.js.map +1 -0
  21. package/dist/generic.js +14 -2
  22. package/dist/generic.js.map +1 -1
  23. package/dist/handlers/access-signals.d.ts +48 -0
  24. package/dist/handlers/access-signals.js +76 -0
  25. package/dist/handlers/access-signals.js.map +1 -0
  26. package/dist/handlers/approvals.d.ts +2 -0
  27. package/dist/handlers/approvals.js +418 -0
  28. package/dist/handlers/approvals.js.map +1 -0
  29. package/dist/handlers/authz-data.d.ts +2 -0
  30. package/dist/handlers/authz-data.js +627 -0
  31. package/dist/handlers/authz-data.js.map +1 -0
  32. package/dist/handlers/authz-layers.d.ts +229 -0
  33. package/dist/handlers/authz-layers.js +786 -0
  34. package/dist/handlers/authz-layers.js.map +1 -0
  35. package/dist/handlers/authz-state.d.ts +80 -0
  36. package/dist/handlers/authz-state.js +175 -0
  37. package/dist/handlers/authz-state.js.map +1 -0
  38. package/dist/handlers/authz.d.ts +2 -0
  39. package/dist/handlers/authz.js +410 -0
  40. package/dist/handlers/authz.js.map +1 -0
  41. package/dist/handlers/billing.d.ts +100 -0
  42. package/dist/handlers/billing.js +877 -0
  43. package/dist/handlers/billing.js.map +1 -0
  44. package/dist/handlers/cedar-ext.d.ts +35 -0
  45. package/dist/handlers/cedar-ext.js +174 -0
  46. package/dist/handlers/cedar-ext.js.map +1 -0
  47. package/dist/handlers/cedar.d.ts +146 -0
  48. package/dist/handlers/cedar.js +689 -0
  49. package/dist/handlers/cedar.js.map +1 -0
  50. package/dist/handlers/connections.d.ts +5 -0
  51. package/dist/handlers/connections.js +561 -0
  52. package/dist/handlers/connections.js.map +1 -0
  53. package/dist/handlers/control.js +6 -1
  54. package/dist/handlers/control.js.map +1 -1
  55. package/dist/handlers/device.d.ts +47 -0
  56. package/dist/handlers/device.js +665 -0
  57. package/dist/handlers/device.js.map +1 -0
  58. package/dist/handlers/emu3-misc.d.ts +38 -0
  59. package/dist/handlers/emu3-misc.js +1056 -0
  60. package/dist/handlers/emu3-misc.js.map +1 -0
  61. package/dist/handlers/emu4-actions-access.d.ts +46 -0
  62. package/dist/handlers/emu4-actions-access.js +107 -0
  63. package/dist/handlers/emu4-actions-access.js.map +1 -0
  64. package/dist/handlers/emu4-actions-catalog-extra.d.ts +3 -0
  65. package/dist/handlers/emu4-actions-catalog-extra.js +204 -0
  66. package/dist/handlers/emu4-actions-catalog-extra.js.map +1 -0
  67. package/dist/handlers/emu4-actions-catalog.d.ts +3 -0
  68. package/dist/handlers/emu4-actions-catalog.js +143 -0
  69. package/dist/handlers/emu4-actions-catalog.js.map +1 -0
  70. package/dist/handlers/emu4-actions-def.d.ts +50 -0
  71. package/dist/handlers/emu4-actions-def.js +250 -0
  72. package/dist/handlers/emu4-actions-def.js.map +1 -0
  73. package/dist/handlers/emu4-actions-sandbox.d.ts +36 -0
  74. package/dist/handlers/emu4-actions-sandbox.js +318 -0
  75. package/dist/handlers/emu4-actions-sandbox.js.map +1 -0
  76. package/dist/handlers/emu4-actions-typegen.d.ts +5 -0
  77. package/dist/handlers/emu4-actions-typegen.js +73 -0
  78. package/dist/handlers/emu4-actions-typegen.js.map +1 -0
  79. package/dist/handlers/emu4-actions.d.ts +2 -0
  80. package/dist/handlers/emu4-actions.js +506 -0
  81. package/dist/handlers/emu4-actions.js.map +1 -0
  82. package/dist/handlers/emu4-authz-trust.d.ts +40 -0
  83. package/dist/handlers/emu4-authz-trust.js +488 -0
  84. package/dist/handlers/emu4-authz-trust.js.map +1 -0
  85. package/dist/handlers/emu4-authz.d.ts +2 -0
  86. package/dist/handlers/emu4-authz.js +752 -0
  87. package/dist/handlers/emu4-authz.js.map +1 -0
  88. package/dist/handlers/emu4-billing-core.d.ts +23 -0
  89. package/dist/handlers/emu4-billing-core.js +22 -0
  90. package/dist/handlers/emu4-billing-core.js.map +1 -0
  91. package/dist/handlers/emu4-billing-x.d.ts +2 -0
  92. package/dist/handlers/emu4-billing-x.js +489 -0
  93. package/dist/handlers/emu4-billing-x.js.map +1 -0
  94. package/dist/handlers/emu4-billing.d.ts +14 -0
  95. package/dist/handlers/emu4-billing.js +846 -0
  96. package/dist/handlers/emu4-billing.js.map +1 -0
  97. package/dist/handlers/emu4-clones.d.ts +2 -0
  98. package/dist/handlers/emu4-clones.js +322 -0
  99. package/dist/handlers/emu4-clones.js.map +1 -0
  100. package/dist/handlers/emu4-compliance.d.ts +8 -0
  101. package/dist/handlers/emu4-compliance.js +39 -0
  102. package/dist/handlers/emu4-compliance.js.map +1 -0
  103. package/dist/handlers/emu4-config.d.ts +14 -0
  104. package/dist/handlers/emu4-config.js +148 -0
  105. package/dist/handlers/emu4-config.js.map +1 -0
  106. package/dist/handlers/emu4-cookies.d.ts +8 -0
  107. package/dist/handlers/emu4-cookies.js +248 -0
  108. package/dist/handlers/emu4-cookies.js.map +1 -0
  109. package/dist/handlers/emu4-erasure.d.ts +2 -0
  110. package/dist/handlers/emu4-erasure.js +587 -0
  111. package/dist/handlers/emu4-erasure.js.map +1 -0
  112. package/dist/handlers/emu4-federation.d.ts +8 -0
  113. package/dist/handlers/emu4-federation.js +18 -0
  114. package/dist/handlers/emu4-federation.js.map +1 -0
  115. package/dist/handlers/emu4-journey-math.d.ts +42 -0
  116. package/dist/handlers/emu4-journey-math.js +99 -0
  117. package/dist/handlers/emu4-journey-math.js.map +1 -0
  118. package/dist/handlers/emu4-messaging.d.ts +21 -0
  119. package/dist/handlers/emu4-messaging.js +595 -0
  120. package/dist/handlers/emu4-messaging.js.map +1 -0
  121. package/dist/handlers/emu4-minors.d.ts +7 -0
  122. package/dist/handlers/emu4-minors.js +408 -0
  123. package/dist/handlers/emu4-minors.js.map +1 -0
  124. package/dist/handlers/emu4-oauth-audit.d.ts +5 -0
  125. package/dist/handlers/emu4-oauth-audit.js +17 -0
  126. package/dist/handlers/emu4-oauth-audit.js.map +1 -0
  127. package/dist/handlers/emu4-oauth-auth.d.ts +83 -0
  128. package/dist/handlers/emu4-oauth-auth.js +363 -0
  129. package/dist/handlers/emu4-oauth-auth.js.map +1 -0
  130. package/dist/handlers/emu4-oauth-delivery.d.ts +68 -0
  131. package/dist/handlers/emu4-oauth-delivery.js +207 -0
  132. package/dist/handlers/emu4-oauth-delivery.js.map +1 -0
  133. package/dist/handlers/emu4-oauth-keys.d.ts +11 -0
  134. package/dist/handlers/emu4-oauth-keys.js +268 -0
  135. package/dist/handlers/emu4-oauth-keys.js.map +1 -0
  136. package/dist/handlers/emu4-oauth-pub.d.ts +36 -0
  137. package/dist/handlers/emu4-oauth-pub.js +420 -0
  138. package/dist/handlers/emu4-oauth-pub.js.map +1 -0
  139. package/dist/handlers/emu4-oauth.d.ts +10 -0
  140. package/dist/handlers/emu4-oauth.js +23 -0
  141. package/dist/handlers/emu4-oauth.js.map +1 -0
  142. package/dist/handlers/emu4-policies.d.ts +2 -0
  143. package/dist/handlers/emu4-policies.js +408 -0
  144. package/dist/handlers/emu4-policies.js.map +1 -0
  145. package/dist/handlers/emu4-region.d.ts +2 -0
  146. package/dist/handlers/emu4-region.js +189 -0
  147. package/dist/handlers/emu4-region.js.map +1 -0
  148. package/dist/handlers/emu4-saml.d.ts +12 -0
  149. package/dist/handlers/emu4-saml.js +532 -0
  150. package/dist/handlers/emu4-saml.js.map +1 -0
  151. package/dist/handlers/emu4-shared.d.ts +44 -0
  152. package/dist/handlers/emu4-shared.js +90 -0
  153. package/dist/handlers/emu4-shared.js.map +1 -0
  154. package/dist/handlers/emu4-snapshots.d.ts +38 -0
  155. package/dist/handlers/emu4-snapshots.js +231 -0
  156. package/dist/handlers/emu4-snapshots.js.map +1 -0
  157. package/dist/handlers/emu4-sns.d.ts +9 -0
  158. package/dist/handlers/emu4-sns.js +21 -0
  159. package/dist/handlers/emu4-sns.js.map +1 -0
  160. package/dist/handlers/emu4-trust-binding.d.ts +27 -0
  161. package/dist/handlers/emu4-trust-binding.js +344 -0
  162. package/dist/handlers/emu4-trust-binding.js.map +1 -0
  163. package/dist/handlers/emu4-trust.d.ts +35 -0
  164. package/dist/handlers/emu4-trust.js +543 -0
  165. package/dist/handlers/emu4-trust.js.map +1 -0
  166. package/dist/handlers/federation.d.ts +2 -0
  167. package/dist/handlers/federation.js +481 -0
  168. package/dist/handlers/federation.js.map +1 -0
  169. package/dist/handlers/fga-engine.d.ts +78 -0
  170. package/dist/handlers/fga-engine.js +663 -0
  171. package/dist/handlers/fga-engine.js.map +1 -0
  172. package/dist/handlers/governance.d.ts +2 -0
  173. package/dist/handlers/governance.js +460 -0
  174. package/dist/handlers/governance.js.map +1 -0
  175. package/dist/handlers/hooks.js +17 -1
  176. package/dist/handlers/hooks.js.map +1 -1
  177. package/dist/handlers/index.d.ts +5 -1
  178. package/dist/handlers/index.js +45 -3
  179. package/dist/handlers/index.js.map +1 -1
  180. package/dist/handlers/keys.js +6 -1
  181. package/dist/handlers/keys.js.map +1 -1
  182. package/dist/handlers/machines.js +61 -8
  183. package/dist/handlers/machines.js.map +1 -1
  184. package/dist/handlers/messaging.d.ts +6 -0
  185. package/dist/handlers/messaging.js +893 -0
  186. package/dist/handlers/messaging.js.map +1 -0
  187. package/dist/handlers/oauth-adv.d.ts +191 -0
  188. package/dist/handlers/oauth-adv.js +1305 -0
  189. package/dist/handlers/oauth-adv.js.map +1 -0
  190. package/dist/handlers/oauth-flows.d.ts +52 -0
  191. package/dist/handlers/oauth-flows.js +480 -0
  192. package/dist/handlers/oauth-flows.js.map +1 -0
  193. package/dist/handlers/oauth.d.ts +96 -0
  194. package/dist/handlers/oauth.js +793 -0
  195. package/dist/handlers/oauth.js.map +1 -0
  196. package/dist/handlers/orgs.js +6 -0
  197. package/dist/handlers/orgs.js.map +1 -1
  198. package/dist/handlers/passkeys.d.ts +3 -0
  199. package/dist/handlers/passkeys.js +293 -0
  200. package/dist/handlers/passkeys.js.map +1 -0
  201. package/dist/handlers/platform.d.ts +20 -0
  202. package/dist/handlers/platform.js +472 -0
  203. package/dist/handlers/platform.js.map +1 -0
  204. package/dist/handlers/round2.d.ts +8 -0
  205. package/dist/handlers/round2.js +372 -0
  206. package/dist/handlers/round2.js.map +1 -0
  207. package/dist/handlers/scim.d.ts +4 -0
  208. package/dist/handlers/scim.js +595 -0
  209. package/dist/handlers/scim.js.map +1 -0
  210. package/dist/handlers/sessions.d.ts +9 -3
  211. package/dist/handlers/sessions.js +76 -38
  212. package/dist/handlers/sessions.js.map +1 -1
  213. package/dist/handlers/support.d.ts +20 -0
  214. package/dist/handlers/support.js +62 -0
  215. package/dist/handlers/support.js.map +1 -0
  216. package/dist/handlers/users.js +7 -0
  217. package/dist/handlers/users.js.map +1 -1
  218. package/dist/handlers/workflows.d.ts +2 -0
  219. package/dist/handlers/workflows.js +428 -0
  220. package/dist/handlers/workflows.js.map +1 -0
  221. package/dist/index.d.ts +8 -0
  222. package/dist/index.js +23 -1
  223. package/dist/index.js.map +1 -1
  224. package/dist/jwe.d.ts +18 -0
  225. package/dist/jwe.js +101 -0
  226. package/dist/jwe.js.map +1 -0
  227. package/dist/jws.d.ts +41 -0
  228. package/dist/jws.js +135 -0
  229. package/dist/jws.js.map +1 -0
  230. package/dist/messages.d.ts +7 -0
  231. package/dist/messages.js +8 -4
  232. package/dist/messages.js.map +1 -1
  233. package/dist/messaging-callbacks.d.ts +100 -0
  234. package/dist/messaging-callbacks.js +767 -0
  235. package/dist/messaging-callbacks.js.map +1 -0
  236. package/dist/openapi.js +41 -1
  237. package/dist/openapi.js.map +1 -1
  238. package/dist/parity-bin.js +3 -1
  239. package/dist/parity-bin.js.map +1 -1
  240. package/dist/parity-emu3-authz.d.ts +3 -0
  241. package/dist/parity-emu3-authz.js +61 -0
  242. package/dist/parity-emu3-authz.js.map +1 -0
  243. package/dist/parity-emu3-billing.d.ts +3 -0
  244. package/dist/parity-emu3-billing.js +25 -0
  245. package/dist/parity-emu3-billing.js.map +1 -0
  246. package/dist/parity-emu3-messaging.d.ts +3 -0
  247. package/dist/parity-emu3-messaging.js +27 -0
  248. package/dist/parity-emu3-messaging.js.map +1 -0
  249. package/dist/parity-emu3-misc.d.ts +2 -0
  250. package/dist/parity-emu3-misc.js +63 -0
  251. package/dist/parity-emu3-misc.js.map +1 -0
  252. package/dist/parity-emu3-oauth.d.ts +2 -0
  253. package/dist/parity-emu3-oauth.js +39 -0
  254. package/dist/parity-emu3-oauth.js.map +1 -0
  255. package/dist/parity-emu4-actions.d.ts +2 -0
  256. package/dist/parity-emu4-actions.js +38 -0
  257. package/dist/parity-emu4-actions.js.map +1 -0
  258. package/dist/parity-emu4-authz.d.ts +3 -0
  259. package/dist/parity-emu4-authz.js +68 -0
  260. package/dist/parity-emu4-authz.js.map +1 -0
  261. package/dist/parity-emu4-billing.d.ts +6 -0
  262. package/dist/parity-emu4-billing.js +44 -0
  263. package/dist/parity-emu4-billing.js.map +1 -0
  264. package/dist/parity-emu4-compliance.d.ts +2 -0
  265. package/dist/parity-emu4-compliance.js +92 -0
  266. package/dist/parity-emu4-compliance.js.map +1 -0
  267. package/dist/parity-emu4-federation.d.ts +2 -0
  268. package/dist/parity-emu4-federation.js +33 -0
  269. package/dist/parity-emu4-federation.js.map +1 -0
  270. package/dist/parity-emu4-messaging.d.ts +3 -0
  271. package/dist/parity-emu4-messaging.js +42 -0
  272. package/dist/parity-emu4-messaging.js.map +1 -0
  273. package/dist/parity-emu4-oauth.d.ts +2 -0
  274. package/dist/parity-emu4-oauth.js +55 -0
  275. package/dist/parity-emu4-oauth.js.map +1 -0
  276. package/dist/parity-emu4-trust.d.ts +2 -0
  277. package/dist/parity-emu4-trust.js +41 -0
  278. package/dist/parity-emu4-trust.js.map +1 -0
  279. package/dist/parity-routes.d.ts +14 -0
  280. package/dist/parity-routes.js +439 -0
  281. package/dist/parity-routes.js.map +1 -0
  282. package/dist/parity.d.ts +10 -3
  283. package/dist/parity.js +14 -5
  284. package/dist/parity.js.map +1 -1
  285. package/dist/sns-cert.d.ts +9 -0
  286. package/dist/sns-cert.js +101 -0
  287. package/dist/sns-cert.js.map +1 -0
  288. package/dist/tokens.d.ts +85 -3
  289. package/dist/tokens.js +196 -24
  290. package/dist/tokens.js.map +1 -1
  291. package/dist/webauthn-virtual.d.ts +70 -0
  292. package/dist/webauthn-virtual.js +186 -0
  293. package/dist/webauthn-virtual.js.map +1 -0
  294. package/dist/webhook-ext.d.ts +16 -0
  295. package/dist/webhook-ext.js +218 -0
  296. package/dist/webhook-ext.js.map +1 -0
  297. package/dist/webhooks.d.ts +3 -0
  298. package/dist/webhooks.js +37 -3
  299. package/dist/webhooks.js.map +1 -1
  300. package/package.json +2 -2
@@ -0,0 +1,595 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BULK_MAX_PAYLOAD = exports.BULK_MAX_OPERATIONS = void 0;
4
+ exports.scimHandlers = scimHandlers;
5
+ const crypto_util_1 = require("../crypto-util");
6
+ const errors_1 = require("../errors");
7
+ const store_1 = require("../store");
8
+ const util_1 = require("./util");
9
+ const users_1 = require("./users");
10
+ /**
11
+ * SCIM 2.0 server (RFC 7643/7644) over the engine's users: `/scim/v2/{ServiceProviderConfig,ResourceTypes,Schemas,Users,Groups}`
12
+ * (and the `/scim/v2/d/:directoryId` alias), authenticated with a SCIM token minted at `POST /v1/scim_tokens`, which names the
13
+ * organization it provisions into. A read-only token (`scopes: ["read"]`) refuses every write with 403. Errors use the SCIM
14
+ * error schema (`urn:ietf:params:scim:api:messages:2.0:Error`). Bulk (RFC 7644 3.7), `sortBy`/`sortOrder` on the Users and Groups lists and weak ETags with
15
+ * `If-Match` / `If-None-Match` preconditions (3.14) are served too, with the limits ServiceProviderConfig states.
16
+ */
17
+ const USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User';
18
+ const GROUP_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group';
19
+ const LIST = 'urn:ietf:params:scim:api:messages:2.0:ListResponse';
20
+ const PATCH_OP = 'urn:ietf:params:scim:api:messages:2.0:PatchOp';
21
+ const ERR = 'urn:ietf:params:scim:api:messages:2.0:Error';
22
+ const JSON_SCIM = { 'content-type': 'application/scim+json' };
23
+ const BULK_REQUEST = 'urn:ietf:params:scim:api:messages:2.0:BulkRequest';
24
+ const BULK_RESPONSE = 'urn:ietf:params:scim:api:messages:2.0:BulkResponse';
25
+ exports.BULK_MAX_OPERATIONS = 100;
26
+ exports.BULK_MAX_PAYLOAD = 1_048_576;
27
+ /** The weak ETag of a resource: a hash of its body without `meta.version`, so the tag changes exactly when the resource does. */
28
+ const etagOf = async (resource) => {
29
+ const copy = JSON.parse(JSON.stringify(resource));
30
+ if (copy.meta)
31
+ delete copy.meta.version;
32
+ return `W/"${(0, crypto_util_1.bytesToB64url)(await (0, crypto_util_1.sha256)(JSON.stringify(copy))).slice(0, 27)}"`;
33
+ };
34
+ const weakEquals = (a, b) => a.replace(/^W\//, '') === b.replace(/^W\//, '');
35
+ /** Attributes a list may be sorted by, per resource (lower-case paths). A complex attribute sorts by a sub-attribute; `emails` sorts by its primary value. */
36
+ const SORTABLE = {
37
+ User: ['id', 'externalid', 'username', 'displayname', 'active', 'name.givenname', 'name.familyname', 'name.formatted', 'emails', 'emails.value', 'meta.created', 'meta.lastmodified'],
38
+ Group: ['id', 'externalid', 'displayname', 'meta.created', 'meta.lastmodified'],
39
+ };
40
+ function sortValue(resource, path) {
41
+ let cur = resource;
42
+ for (const seg of path.split('.')) {
43
+ if (Array.isArray(cur))
44
+ cur = (cur.find((x) => x.primary === true) ?? cur[0]) ?? undefined;
45
+ if (cur == null || typeof cur !== 'object')
46
+ return null;
47
+ const key = Object.keys(cur).find((k) => k.toLowerCase() === seg.toLowerCase());
48
+ cur = key === undefined ? null : cur[key];
49
+ }
50
+ if (Array.isArray(cur))
51
+ cur = (cur.find((x) => x.primary === true) ?? cur[0])?.value ?? null;
52
+ return cur ?? null;
53
+ }
54
+ /** Sort presented resources like the real list: case-insensitive text, nulls last ascending and first descending, ties by id in the same direction. */
55
+ function sortResources(by, order, rows, kind) {
56
+ const dir = order === null || order === '' ? 'ascending' : order.toLowerCase();
57
+ if (dir !== 'ascending' && dir !== 'descending')
58
+ return scimError(400, 'sortOrder must be "ascending" or "descending".', 'invalidValue');
59
+ if (by === null || by === '')
60
+ return rows;
61
+ const path = by.trim();
62
+ if (!path || path.length > 512 || /[\s()[\]"]/.test(path))
63
+ return scimError(400, `Invalid sortBy "${by}".`, 'invalidValue');
64
+ const norm = path.toLowerCase();
65
+ if (norm === 'name' || norm === 'meta')
66
+ return scimError(400, `Sort by a sub-attribute of ${path}.`, 'invalidValue');
67
+ if (!SORTABLE[kind].includes(norm))
68
+ return scimError(400, `Sorting by ${path} is not supported.`, 'invalidValue');
69
+ const key = (r) => {
70
+ const v = sortValue(r, path);
71
+ return v === null ? null : typeof v === 'string' ? v.toLowerCase() : typeof v === 'boolean' ? Number(v) : v;
72
+ };
73
+ const sign = dir === 'descending' ? -1 : 1;
74
+ return [...rows].sort((a, b) => {
75
+ const ka = key(a);
76
+ const kb = key(b);
77
+ if (ka !== kb) {
78
+ if (ka === null)
79
+ return sign;
80
+ if (kb === null)
81
+ return -sign;
82
+ return (ka < kb ? -1 : 1) * sign;
83
+ }
84
+ return (String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0) * sign;
85
+ });
86
+ }
87
+ const scimError = (status, detail, scimType) => ({ status, headers: JSON_SCIM, body: { schemas: [ERR], status: String(status), ...(scimType ? { scimType } : {}), detail } });
88
+ const presentToken = (t) => ({
89
+ object: 'scim_token',
90
+ id: t.id,
91
+ name: t.name ?? null,
92
+ organization_id: t.organization_id,
93
+ connection_id: t.connection_id ?? null,
94
+ prefix: t.prefix,
95
+ scopes: t.scopes ?? null,
96
+ role_mappings: t.role_mappings ?? null,
97
+ revoked: t.revoked === true,
98
+ created_at: t.created_at,
99
+ });
100
+ function authenticate(e, c) {
101
+ const bearer = /^Bearer\s+(.+)$/i.exec(c.request.headers.get('authorization') ?? '')?.[1];
102
+ const token = bearer ? e.db.scimTokens.find((t) => t.secret === bearer && !t.revoked) : undefined;
103
+ if (!token)
104
+ return scimError(401, 'A valid SCIM token is required.');
105
+ return { token, orgId: token.organization_id, canWrite: !Array.isArray(token.scopes) || token.scopes.includes('write') };
106
+ }
107
+ const primaryEmail = (u) => u.emails.find((m) => m.id === u.primary_email_address_id) ?? u.emails[0];
108
+ function scimUser(e, u, base) {
109
+ const mail = primaryEmail(u);
110
+ return {
111
+ schemas: [USER_SCHEMA],
112
+ id: u.id,
113
+ externalId: u.external_id ?? undefined,
114
+ userName: mail?.email_address ?? u.username ?? u.id,
115
+ name: { givenName: u.first_name ?? undefined, familyName: u.last_name ?? undefined, formatted: [u.first_name, u.last_name].filter(Boolean).join(' ') || undefined },
116
+ displayName: [u.first_name, u.last_name].filter(Boolean).join(' ') || undefined,
117
+ emails: u.emails.map((m) => ({ value: m.email_address, primary: m.id === u.primary_email_address_id })),
118
+ active: u.scim_active !== false && u.banned !== true,
119
+ meta: { resourceType: 'User', created: new Date(u.created_at).toISOString(), lastModified: new Date(u.updated_at).toISOString(), location: `${base}/Users/${u.id}` },
120
+ };
121
+ }
122
+ const scimGroup = (e, g, base) => ({
123
+ schemas: [GROUP_SCHEMA],
124
+ id: g.id,
125
+ externalId: g.external_id ?? undefined,
126
+ displayName: g.display_name,
127
+ members: g.members.map((id) => ({ value: id, $ref: `${base}/Users/${id}` })),
128
+ meta: { resourceType: 'Group', created: new Date(g.created_at).toISOString(), lastModified: new Date(g.updated_at).toISOString(), location: `${base}/Groups/${g.id}` },
129
+ });
130
+ /** The small filter subset IdPs use: `attr eq "value"`, joined by `and`; `attr co/sw` too. */
131
+ function matcher(filter) {
132
+ if (!filter)
133
+ return () => true;
134
+ const parts = filter.split(/\s+and\s+/i).map((p) => /^([\w.]+)\s+(eq|ne|co|sw|ew|pr)(?:\s+"((?:[^"\\]|\\.)*)")?$/i.exec(p.trim()));
135
+ if (parts.some((p) => !p))
136
+ return scimError(400, `Unsupported filter: ${filter}`, 'invalidFilter');
137
+ return (o) => parts.every((p) => {
138
+ const [, attr, op, val] = p;
139
+ const have = String(o[attr] ?? '').toLowerCase();
140
+ const want = (val ?? '').toLowerCase();
141
+ return { eq: have === want, ne: have !== want, co: have.includes(want), sw: have.startsWith(want), ew: have.endsWith(want), pr: have !== '' }[op.toLowerCase()];
142
+ });
143
+ }
144
+ function scimHandlers() {
145
+ const base = (c) => `${c.engine.issuer}${new URL(c.request.url).pathname.startsWith('/scim/v2/d/') ? `/scim/v2/d/${c.params.directoryId}` : '/scim/v2'}`;
146
+ /** Run a handler with the token resolved; `write` demands a write-capable token. */
147
+ const guard = (write, fn) => (c) => {
148
+ const a = authenticate(c.engine, c);
149
+ if (!('orgId' in a))
150
+ return a;
151
+ if (write && !a.canWrite)
152
+ return scimError(403, 'This SCIM token is read-only.');
153
+ return fn(c, a, base(c));
154
+ };
155
+ const page = (c, items, present, kind) => {
156
+ const sorted = sortResources(c.query.get('sortBy'), c.query.get('sortOrder'), items.map(present), kind);
157
+ if (!Array.isArray(sorted))
158
+ return sorted;
159
+ const start = Math.max(Number(c.query.get('startIndex') ?? 1) || 1, 1);
160
+ const count = Math.min(Math.max(Number(c.query.get('count') ?? 100) || 0, 0), 200);
161
+ const slice = sorted.slice(start - 1, start - 1 + count);
162
+ return { headers: JSON_SCIM, body: { schemas: [LIST], totalResults: sorted.length, startIndex: start, itemsPerPage: slice.length, Resources: slice } };
163
+ };
164
+ const memberUsers = (e, orgId) => e.db.memberships
165
+ .filter((m) => m.organization_id === orgId)
166
+ .flatMap((m) => {
167
+ const u = e.db.users.get(m.user_id);
168
+ return u && !u.deleted ? [u] : [];
169
+ });
170
+ const userRoutes = (prefix) => [
171
+ {
172
+ method: 'GET',
173
+ pattern: `${prefix}/Users`,
174
+ public: true,
175
+ handler: guard(false, (c, a, b) => {
176
+ const m = matcher(c.query.get('filter'));
177
+ if (typeof m !== 'function')
178
+ return m;
179
+ const flat = (u) => ({ ...u, userName: primaryEmail(u)?.email_address, externalId: u.external_id, id: u.id, active: u.scim_active !== false });
180
+ return page(c, memberUsers(c.engine, a.orgId).filter((u) => m(flat(u))), (u) => scimUser(c.engine, u, b), 'User');
181
+ }),
182
+ },
183
+ {
184
+ method: 'POST',
185
+ pattern: `${prefix}/Users`,
186
+ public: true,
187
+ handler: guard(true, (c, a, b) => {
188
+ const e = c.engine;
189
+ const userName = String(c.body.userName ?? '').trim();
190
+ const addr = c.body.emails?.find((m) => m.primary)?.value ?? c.body.emails?.[0]?.value ?? userName;
191
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(addr)))
192
+ return scimError(400, 'userName or emails[].value must be an email address.', 'invalidValue');
193
+ const taken = e.db.users.all().find((u) => !u.deleted && u.emails.some((m) => m.email_address.toLowerCase() === String(addr).toLowerCase()));
194
+ if (taken)
195
+ return scimError(409, `${addr} is already in use.`, 'uniqueness');
196
+ const now = e.clock.now();
197
+ const mail = { id: e.db.newId('idn'), email_address: String(addr), verified: true };
198
+ const u = e.db.users.insert({
199
+ username: null,
200
+ first_name: c.body.name?.givenName ?? null,
201
+ last_name: c.body.name?.familyName ?? null,
202
+ image_url: null,
203
+ external_id: c.body.externalId ?? null,
204
+ emails: [mail],
205
+ primary_email_address_id: mail.id,
206
+ has_password: false,
207
+ public_metadata: {},
208
+ unsafe_metadata: {},
209
+ private_metadata: {},
210
+ banned: false,
211
+ locked: false,
212
+ scim_active: c.body.active !== false,
213
+ created_at: now,
214
+ updated_at: now,
215
+ version: 1,
216
+ });
217
+ const role = a.token.role_mappings?.find((r) => c.body.roles?.some((x) => x.value === r.value))?.role ?? 'org:member';
218
+ const mem = e.db.memberships.insert({ organization_id: a.orgId, user_id: u.id, role, role_source: 'scim', created_at: now });
219
+ e.changed({ type: 'user.created', data: (0, users_1.presentUser)(e, u), resource: 'user', id: u.id, op: 'create' });
220
+ e.changed({ type: 'organization_membership.created', data: { id: mem.id, organization_id: a.orgId, user_id: u.id, role }, resource: 'membership', id: mem.id, op: 'create' });
221
+ e.webhooks.emit('scim_user.created', { object: 'scim_user', id: u.id, organization_id: a.orgId, scim_token_id: a.token.id });
222
+ return { status: 201, headers: { ...JSON_SCIM, location: `${b}/Users/${u.id}` }, body: scimUser(e, u, b) };
223
+ }),
224
+ },
225
+ {
226
+ method: 'GET',
227
+ pattern: `${prefix}/Users/:uid`,
228
+ public: true,
229
+ handler: guard(false, (c, a, b) => {
230
+ const u = memberUsers(c.engine, a.orgId).find((x) => x.id === c.params.uid);
231
+ return u ? { headers: JSON_SCIM, body: scimUser(c.engine, u, b) } : scimError(404, 'User not found.');
232
+ }),
233
+ },
234
+ ...['PUT', 'PATCH'].map((method) => ({
235
+ method,
236
+ pattern: `${prefix}/Users/:uid`,
237
+ public: true,
238
+ handler: guard(true, (c, a, b) => {
239
+ const e = c.engine;
240
+ const u = memberUsers(e, a.orgId).find((x) => x.id === c.params.uid);
241
+ if (!u)
242
+ return scimError(404, 'User not found.');
243
+ const apply = (path, value) => {
244
+ if (path === 'active')
245
+ u.scim_active = value === true || value === 'true';
246
+ else if (path === 'name.givenName')
247
+ u.first_name = value;
248
+ else if (path === 'name.familyName')
249
+ u.last_name = value;
250
+ else if (path === 'externalId')
251
+ u.external_id = value;
252
+ };
253
+ if (method === 'PUT') {
254
+ apply('active', c.body.active ?? true);
255
+ apply('name.givenName', c.body.name?.givenName ?? null);
256
+ apply('name.familyName', c.body.name?.familyName ?? null);
257
+ apply('externalId', c.body.externalId ?? null);
258
+ }
259
+ else {
260
+ if (!Array.isArray(c.body.Operations))
261
+ return scimError(400, 'Operations is required.', 'invalidSyntax');
262
+ for (const op of c.body.Operations) {
263
+ const verb = String(op.op ?? '').toLowerCase();
264
+ if (verb !== 'replace' && verb !== 'add' && verb !== 'remove')
265
+ return scimError(400, `Unsupported op ${op.op}.`, 'invalidSyntax');
266
+ if (op.path)
267
+ apply(String(op.path), verb === 'remove' ? null : op.value);
268
+ else
269
+ for (const [k, v] of Object.entries((op.value ?? {})))
270
+ apply(k, v);
271
+ }
272
+ }
273
+ u.updated_at = e.clock.now();
274
+ u.version = (u.version ?? 0) + 1;
275
+ const inactive = u.scim_active === false;
276
+ e.changed({ type: 'user.updated', data: (0, users_1.presentUser)(e, u), resource: 'user', id: u.id, op: 'update' });
277
+ e.webhooks.emit(inactive ? 'scim_user.deactivated' : 'scim_user.updated', { object: 'scim_user', id: u.id, organization_id: a.orgId, scim_token_id: a.token.id });
278
+ return { headers: JSON_SCIM, body: scimUser(e, u, b) };
279
+ }),
280
+ })),
281
+ {
282
+ method: 'DELETE',
283
+ pattern: `${prefix}/Users/:uid`,
284
+ public: true,
285
+ handler: guard(true, (c, a) => {
286
+ const e = c.engine;
287
+ const mem = e.db.memberships.find((m) => m.organization_id === a.orgId && m.user_id === c.params.uid);
288
+ if (!mem)
289
+ return scimError(404, 'User not found.');
290
+ // De-provisioning removes the membership; the user account itself stays (it may belong elsewhere).
291
+ e.db.memberships.delete(mem.id);
292
+ e.changed({ type: 'organization_membership.deleted', data: { id: mem.id, organization_id: a.orgId, user_id: mem.user_id }, resource: 'membership', id: mem.id, op: 'delete' });
293
+ e.webhooks.emit('scim_user.deactivated', { object: 'scim_user', id: mem.user_id, organization_id: a.orgId, scim_token_id: a.token.id });
294
+ return { status: 204 };
295
+ }),
296
+ },
297
+ // ── groups ──
298
+ {
299
+ method: 'GET',
300
+ pattern: `${prefix}/Groups`,
301
+ public: true,
302
+ handler: guard(false, (c, a, b) => {
303
+ const m = matcher(c.query.get('filter'));
304
+ if (typeof m !== 'function')
305
+ return m;
306
+ return page(c, c.engine.db.scimGroups.filter((g) => g.organization_id === a.orgId && m({ displayName: g.display_name, externalId: g.external_id, id: g.id })), (g) => scimGroup(c.engine, g, b), 'Group');
307
+ }),
308
+ },
309
+ {
310
+ method: 'POST',
311
+ pattern: `${prefix}/Groups`,
312
+ public: true,
313
+ handler: guard(true, (c, a, b) => {
314
+ const e = c.engine;
315
+ const name = String(c.body.displayName ?? '').trim();
316
+ if (!name)
317
+ return scimError(400, 'displayName is required.', 'invalidValue');
318
+ if (e.db.scimGroups.find((g) => g.organization_id === a.orgId && g.display_name === name))
319
+ return scimError(409, 'A group with that displayName exists.', 'uniqueness');
320
+ const now = e.clock.now();
321
+ const g = e.db.scimGroups.insert({ organization_id: a.orgId, display_name: name, external_id: c.body.externalId ?? null, members: (c.body.members ?? []).map((m) => String(m.value)), created_at: now, updated_at: now });
322
+ e.webhooks.emit('scim_group.created', { object: 'scim_group', id: g.id, organization_id: a.orgId, display_name: name });
323
+ return { status: 201, headers: { ...JSON_SCIM, location: `${b}/Groups/${g.id}` }, body: scimGroup(e, g, b) };
324
+ }),
325
+ },
326
+ {
327
+ method: 'GET',
328
+ pattern: `${prefix}/Groups/:gid`,
329
+ public: true,
330
+ handler: guard(false, (c, a, b) => {
331
+ const g = c.engine.db.scimGroups.get(c.params.gid);
332
+ return g && g.organization_id === a.orgId ? { headers: JSON_SCIM, body: scimGroup(c.engine, g, b) } : scimError(404, 'Group not found.');
333
+ }),
334
+ },
335
+ {
336
+ method: 'PATCH',
337
+ pattern: `${prefix}/Groups/:gid`,
338
+ public: true,
339
+ handler: guard(true, (c, a, b) => {
340
+ const e = c.engine;
341
+ const g = e.db.scimGroups.get(c.params.gid);
342
+ if (!g || g.organization_id !== a.orgId)
343
+ return scimError(404, 'Group not found.');
344
+ if (!Array.isArray(c.body.Operations))
345
+ return scimError(400, 'Operations is required.', 'invalidSyntax');
346
+ for (const op of c.body.Operations) {
347
+ const verb = String(op.op ?? '').toLowerCase();
348
+ const path = String(op.path ?? 'members');
349
+ if (path === 'displayName')
350
+ g.display_name = String(op.value);
351
+ else if (path.startsWith('members')) {
352
+ const given = (Array.isArray(op.value) ? op.value : []).map((m) => String(m.value));
353
+ const quoted = /members\[value eq "([^"]+)"\]/.exec(path)?.[1];
354
+ if (verb === 'add') {
355
+ for (const id of given) {
356
+ if (g.members.includes(id))
357
+ continue;
358
+ g.members.push(id);
359
+ e.webhooks.emit('scim_group.member_added', { object: 'scim_group', id: g.id, user_id: id });
360
+ }
361
+ }
362
+ else if (verb === 'remove') {
363
+ const drop = quoted ? [quoted] : given.length ? given : [...g.members];
364
+ g.members = g.members.filter((id) => !drop.includes(id));
365
+ for (const id of drop)
366
+ e.webhooks.emit('scim_group.member_removed', { object: 'scim_group', id: g.id, user_id: id });
367
+ }
368
+ else if (verb === 'replace')
369
+ g.members = given;
370
+ }
371
+ }
372
+ g.updated_at = e.clock.now();
373
+ e.webhooks.emit('scim_group.updated', { object: 'scim_group', id: g.id, organization_id: a.orgId, display_name: g.display_name });
374
+ return { headers: JSON_SCIM, body: scimGroup(e, g, b) };
375
+ }),
376
+ },
377
+ {
378
+ method: 'DELETE',
379
+ pattern: `${prefix}/Groups/:gid`,
380
+ public: true,
381
+ handler: guard(true, (c, a) => {
382
+ const g = c.engine.db.scimGroups.get(c.params.gid);
383
+ if (!g || g.organization_id !== a.orgId)
384
+ return scimError(404, 'Group not found.');
385
+ c.engine.db.scimGroups.delete(g.id);
386
+ c.engine.webhooks.emit('scim_group.deleted', { object: 'scim_group', id: g.id, organization_id: a.orgId });
387
+ return { status: 204 };
388
+ }),
389
+ },
390
+ // ── Bulk (RFC 7644 3.7): each operation runs through the server's own routes, so auth, read-only and validation rules are the same as a direct call ──
391
+ {
392
+ method: 'POST',
393
+ pattern: `${prefix}/Bulk`,
394
+ public: true,
395
+ handler: guard(true, async (c) => {
396
+ const e = c.engine;
397
+ const body = c.body;
398
+ if (JSON.stringify(body).length > exports.BULK_MAX_PAYLOAD)
399
+ return scimError(413, `A bulk request is at most ${exports.BULK_MAX_PAYLOAD} bytes.`, 'tooLarge');
400
+ if (!Array.isArray(body.schemas) || !body.schemas.includes(BULK_REQUEST))
401
+ return scimError(400, `A bulk request must declare the ${BULK_REQUEST} schema.`, 'invalidSyntax');
402
+ const ops = body.Operations;
403
+ if (!Array.isArray(ops) || ops.length === 0)
404
+ return scimError(400, 'Operations is required.', 'invalidSyntax');
405
+ if (ops.length > exports.BULK_MAX_OPERATIONS)
406
+ return scimError(413, `At most ${exports.BULK_MAX_OPERATIONS} operations per request.`, 'tooMany');
407
+ const failOn = Number.isInteger(body.failOnErrors) && body.failOnErrors > 0 ? body.failOnErrors : Infinity;
408
+ const root = new URL(c.request.url).pathname.replace(/\/Bulk\/?$/, '');
409
+ const created = new Map(); // bulkId -> location
410
+ const bulkIds = new Set();
411
+ const results = [];
412
+ let errors = 0;
413
+ const refs = (text) => text.replace(/bulkId:([A-Za-z0-9_-]+)/g, (m, id) => created.get(id)?.split('/').pop() ?? m);
414
+ for (const op of ops) {
415
+ if (errors >= failOn)
416
+ break;
417
+ const method = String(op.method ?? '').toUpperCase();
418
+ const result = { method, ...(op.bulkId ? { bulkId: op.bulkId } : {}) };
419
+ const fail = (status, detail, scimType) => {
420
+ errors += 1;
421
+ results.push({ ...result, status: String(status), response: { schemas: [ERR], ...(scimType ? { scimType } : {}), detail, status: String(status) } });
422
+ };
423
+ if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
424
+ fail(400, 'method must be POST, PUT, PATCH or DELETE.', 'invalidValue');
425
+ continue;
426
+ }
427
+ if (method === 'POST') {
428
+ if (!op.bulkId || !/^[A-Za-z0-9_-]{1,64}$/.test(op.bulkId) || bulkIds.has(op.bulkId)) {
429
+ fail(400, 'A POST needs a unique bulkId.', 'invalidValue');
430
+ continue;
431
+ }
432
+ bulkIds.add(op.bulkId);
433
+ }
434
+ if (typeof op.path !== 'string' || !op.path.startsWith('/') || op.path.includes('..') || /^\/(Bulk|ServiceProviderConfig|Schemas|ResourceTypes)\b/.test(op.path)) {
435
+ fail(400, 'path must name a resource endpoint.', 'invalidPath');
436
+ continue;
437
+ }
438
+ const path = refs(op.path);
439
+ if (/bulkId:/.test(path)) {
440
+ fail(409, 'A bulkId reference points at an operation that did not create a resource.', 'invalidValue');
441
+ continue;
442
+ }
443
+ const data = op.data === undefined ? undefined : refs(JSON.stringify(op.data));
444
+ const res = await e.handle(new Request(`${e.issuer}${root}${path}`, { method, headers: { authorization: c.request.headers.get('authorization') ?? '', 'content-type': 'application/scim+json', ...(op.version ? { 'if-match': op.version } : {}) }, ...(data !== undefined && method !== 'DELETE' ? { body: data } : {}) }));
445
+ const text = await res.text();
446
+ let json = null;
447
+ try {
448
+ json = text ? JSON.parse(text) : null;
449
+ }
450
+ catch {
451
+ json = null;
452
+ }
453
+ const location = res.headers.get('location') ?? json?.meta?.location;
454
+ if (res.status >= 400) {
455
+ errors += 1;
456
+ results.push({ ...result, status: String(res.status), response: json ?? {} });
457
+ continue;
458
+ }
459
+ if (method === 'POST' && op.bulkId && location)
460
+ created.set(op.bulkId, location);
461
+ results.push({ ...result, ...(location ? { location } : {}), ...(res.headers.get('etag') ? { version: res.headers.get('etag') } : {}), status: String(res.status) });
462
+ }
463
+ return { headers: JSON_SCIM, body: { schemas: [BULK_RESPONSE], Operations: results } };
464
+ }),
465
+ },
466
+ // ── discovery ──
467
+ {
468
+ method: 'GET',
469
+ pattern: `${prefix}/ServiceProviderConfig`,
470
+ public: true,
471
+ handler: guard(false, () => ({
472
+ headers: JSON_SCIM,
473
+ body: {
474
+ schemas: ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
475
+ patch: { supported: true },
476
+ bulk: { supported: true, maxOperations: exports.BULK_MAX_OPERATIONS, maxPayloadSize: exports.BULK_MAX_PAYLOAD },
477
+ filter: { supported: true, maxResults: 200 },
478
+ changePassword: { supported: false },
479
+ sort: { supported: true },
480
+ etag: { supported: true },
481
+ authenticationSchemes: [{ type: 'oauthbearertoken', name: 'Bearer token', description: 'A SCIM token from the Backend API.' }],
482
+ },
483
+ })),
484
+ },
485
+ {
486
+ method: 'GET',
487
+ pattern: `${prefix}/ResourceTypes`,
488
+ public: true,
489
+ handler: guard(false, () => ({
490
+ headers: JSON_SCIM,
491
+ body: { schemas: [LIST], totalResults: 2, Resources: [{ schemas: ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'], id: 'User', name: 'User', endpoint: '/Users', schema: USER_SCHEMA }, { schemas: ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'], id: 'Group', name: 'Group', endpoint: '/Groups', schema: GROUP_SCHEMA }] },
492
+ })),
493
+ },
494
+ {
495
+ method: 'GET',
496
+ pattern: `${prefix}/Schemas`,
497
+ public: true,
498
+ handler: guard(false, () => ({ headers: JSON_SCIM, body: { schemas: [LIST], totalResults: 2, Resources: [{ id: USER_SCHEMA, name: 'User' }, { id: GROUP_SCHEMA, name: 'Group' }] } })),
499
+ },
500
+ ];
501
+ /**
502
+ * ETag on every single-resource answer (`meta.version` and the `ETag` header), 304 for a matching `If-None-Match` on a GET, and 412
503
+ * `invalidVers` for an `If-Match` that no longer names the resource on a PUT, PATCH or DELETE (checked before the write, `*` always passes).
504
+ */
505
+ const withEtag = (routes) => routes.map((r) => {
506
+ if (!/\/(Users|Groups)(\/:\w+)?$/.test(r.pattern))
507
+ return r;
508
+ const isItem = /\/:\w+$/.test(r.pattern);
509
+ const kind = /\/Users/.test(r.pattern) ? 'User' : 'Group';
510
+ return {
511
+ ...r,
512
+ handler: async (c) => {
513
+ const ifMatch = c.request.headers.get('if-match');
514
+ if (isItem && ifMatch !== null && (r.method === 'PUT' || r.method === 'PATCH' || r.method === 'DELETE')) {
515
+ const a = authenticate(c.engine, c);
516
+ if ('orgId' in a) {
517
+ const id = c.params.uid ?? c.params.gid;
518
+ const b = base(c);
519
+ const current = kind === 'User' ? memberUsers(c.engine, a.orgId).filter((u) => u.id === id).map((u) => scimUser(c.engine, u, b))[0] : c.engine.db.scimGroups.filter((g) => g.id === id && g.organization_id === a.orgId).map((g) => scimGroup(c.engine, g, b))[0];
520
+ if (current) {
521
+ const tag = await etagOf(current);
522
+ if (ifMatch.trim() !== '*' && !ifMatch.split(',').some((t) => weakEquals(t.trim(), tag)))
523
+ return scimError(412, 'The resource changed since the version in If-Match.', 'invalidVers');
524
+ }
525
+ }
526
+ }
527
+ const out = await r.handler(c);
528
+ const status = out.status ?? 200;
529
+ const body = out.body;
530
+ if (status < 200 || status > 201 || !body || typeof body !== 'object' || !body.meta || typeof body.id !== 'string')
531
+ return out;
532
+ const tag = await etagOf(body);
533
+ body.meta.version = tag;
534
+ const headers = { ...(out.headers ?? {}), etag: tag };
535
+ const inm = c.request.headers.get('if-none-match');
536
+ if (r.method === 'GET' && inm !== null && inm.split(',').some((t) => t.trim() === '*' || weakEquals(t.trim(), tag)))
537
+ return { status: 304, headers };
538
+ return { ...out, headers };
539
+ },
540
+ };
541
+ });
542
+ return [
543
+ // ── token management (Backend API) ──
544
+ {
545
+ method: 'POST',
546
+ pattern: '/v1/scim_tokens',
547
+ handler: (c) => {
548
+ const e = c.engine;
549
+ const orgId = String((0, util_1.need)(c.body, 'organization_id'));
550
+ if (!e.db.organizations.get(orgId))
551
+ throw new errors_1.EmulatorError('NOT_FOUND', 'organization_id is not an organization.', 'organization_id');
552
+ const secret = `scim_${(0, store_1.randomToken)(32)}`;
553
+ const t = e.db.scimTokens.insert({ name: c.body.name ?? null, organization_id: orgId, connection_id: c.body.connection_id ?? null, scopes: c.body.scopes ?? null, role_mappings: c.body.role_mappings ?? null, secret, prefix: secret.slice(0, 10), revoked: false, created_at: e.clock.now() });
554
+ e.webhooks.emit('scim_token.created', presentToken(t));
555
+ return { status: 201, body: { ...presentToken(t), token: secret } };
556
+ },
557
+ },
558
+ {
559
+ method: 'GET',
560
+ pattern: '/v1/scim_tokens',
561
+ handler: (c) => {
562
+ const org = c.query.get('organization_id');
563
+ return (0, util_1.listReply)(c, c.engine.db.scimTokens.filter((t) => !org || t.organization_id === org), presentToken);
564
+ },
565
+ },
566
+ {
567
+ method: 'PATCH',
568
+ pattern: '/v1/scim_tokens/:id',
569
+ handler: (c) => {
570
+ const t = c.engine.db.scimTokens.get(c.params.id);
571
+ if (!t)
572
+ throw (0, errors_1.notFound)('SCIM token');
573
+ for (const k of ['name', 'scopes', 'role_mappings'])
574
+ if (k in c.body)
575
+ t[k] = c.body[k];
576
+ return { body: presentToken(t) };
577
+ },
578
+ },
579
+ {
580
+ method: 'POST',
581
+ pattern: '/v1/scim_tokens/:id/revoke',
582
+ handler: (c) => {
583
+ const t = c.engine.db.scimTokens.get(c.params.id);
584
+ if (!t)
585
+ throw (0, errors_1.notFound)('SCIM token');
586
+ t.revoked = true;
587
+ c.engine.webhooks.emit('scim_token.revoked', presentToken(t));
588
+ return { body: { object: 'scim_token', id: t.id, revoked: true, note: 'Requests with this token now answer 401.' } };
589
+ },
590
+ },
591
+ ...withEtag(userRoutes('/scim/v2')),
592
+ ...withEtag(userRoutes('/scim/v2/d/:directoryId')),
593
+ ];
594
+ }
595
+ //# sourceMappingURL=scim.js.map