@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,587 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.erasureHandlers = erasureHandlers;
4
+ const errors_1 = require("../errors");
5
+ const store_1 = require("../store");
6
+ const emu3_misc_1 = require("./emu3-misc");
7
+ const support_1 = require("./support");
8
+ const emu4_shared_1 = require("./emu4-shared");
9
+ const emu4_cookies_1 = require("./emu4-cookies");
10
+ /**
11
+ * Data-rights operations: the request board (assignee, priority, labels, notes), downstream erasure connectors (a catalog of
12
+ * known systems; modes ack, manual and api), per-request connector results with a probe, a dry run and a retry, and the signed
13
+ * compliance posture and record-of-processing reports. API connectors call stub vendors that the emulator owns
14
+ * (`/__emulator/compliance/erasure_vendors/...`); no network is touched and a saved key is never echoed anywhere.
15
+ */
16
+ const PRIORITIES = ['low', 'normal', 'high', 'urgent'];
17
+ const KEY_RE = /^[a-z][a-z0-9_-]{0,47}$/;
18
+ const LABEL_RE = /^[a-z0-9][a-z0-9_.:-]{0,31}$/;
19
+ const cat = (id, name, category, method, identifier, instructions, holds) => ({ id, name, category, method, identifier, instructions, holds });
20
+ const CATALOG = [
21
+ cat('stripe', 'Stripe', 'payments', 'api', 'email', 'Delete the customer object through the API; payment records the law requires you to keep stay as retained records.', ['name', 'email', 'billing address', 'payment method tokens']),
22
+ cat('paddle', 'Paddle', 'payments', 'support_request', 'email', 'Ask the vendor to erase the customer record; it keeps transaction records it must keep as the merchant of record.', ['name', 'email', 'billing address']),
23
+ cat('brevo', 'Brevo', 'email', 'api', 'email', 'Delete the contact; also remove it from lists and the transactional log.', ['email', 'name', 'message log']),
24
+ cat('mailchimp', 'Mailchimp', 'email', 'api', 'email', 'Archive then permanently delete the audience member.', ['email', 'name', 'tags']),
25
+ cat('sendgrid', 'SendGrid', 'email', 'dashboard', 'email', 'Delete the contact and clear the activity feed entries for the address.', ['email', 'message log']),
26
+ cat('postmark', 'Postmark', 'email', 'dashboard', 'email', 'Delete message content for the recipient and remove suppressions you no longer need.', ['email', 'message log']),
27
+ cat('resend', 'Resend', 'email', 'dashboard', 'email', 'Delete the contact and the sent-email records for the address.', ['email', 'message log']),
28
+ cat('customerio', 'Customer.io', 'marketing', 'api', 'user_id', 'Suppress then delete the person by id.', ['email', 'attributes', 'event history']),
29
+ cat('twilio', 'Twilio', 'sms', 'dashboard', 'email', 'Delete message logs that carry the number; keep only what telecom rules require.', ['phone number', 'message log']),
30
+ cat('vonage', 'Vonage', 'sms', 'support_request', 'email', 'Ask the vendor to purge message logs for the number.', ['phone number', 'message log']),
31
+ cat('segment', 'Segment', 'analytics', 'api', 'user_id', 'Submit a regulation request for the user id; it forwards to connected destinations.', ['user id', 'traits', 'event history']),
32
+ cat('mixpanel', 'Mixpanel', 'analytics', 'api', 'user_id', 'Submit a data deletion task for the distinct id.', ['user id', 'event history']),
33
+ cat('amplitude', 'Amplitude', 'analytics', 'api', 'user_id', 'Submit a deletion job for the user id.', ['user id', 'event history']),
34
+ cat('posthog', 'PostHog', 'analytics', 'api', 'user_id', 'Delete the person and their events by distinct id.', ['user id', 'event history', 'session recordings']),
35
+ cat('googleanalytics', 'Google Analytics', 'analytics', 'dashboard', 'user_id', 'Use the user deletion request for the client or user id.', ['client id', 'event history']),
36
+ cat('hubspot', 'HubSpot', 'crm', 'api', 'email', 'Permanently delete the contact (the GDPR delete, not an archive).', ['email', 'name', 'activity timeline']),
37
+ cat('salesforce', 'Salesforce', 'crm', 'api', 'email', 'Delete the lead or contact and clear it from the recycle bin.', ['email', 'name', 'activity history']),
38
+ cat('intercom', 'Intercom', 'support', 'api', 'email', 'Permanently delete the contact.', ['email', 'name', 'conversations']),
39
+ cat('zendesk', 'Zendesk', 'support', 'api', 'email', 'Permanently delete the user and their tickets or redact them.', ['email', 'name', 'tickets']),
40
+ cat('helpscout', 'Help Scout', 'support', 'dashboard', 'email', 'Delete the customer and their conversations.', ['email', 'name', 'conversations']),
41
+ cat('sentry', 'Sentry', 'monitoring', 'api', 'user_id', 'Delete events for the user id and scrub the user from issues.', ['user id', 'ip address', 'event payloads']),
42
+ cat('datadog', 'Datadog', 'monitoring', 'support_request', 'user_id', 'Ask support to purge logs carrying the user id; set retention short to limit exposure.', ['user id', 'logs']),
43
+ cat('s3', 'Object storage (S3-compatible)', 'storage', 'api', 'user_id', 'Delete objects under the user prefix, including versions and backups on the lifecycle.', ['uploaded files']),
44
+ cat('warehouse', 'Data warehouse', 'data_warehouse', 'api', 'user_id', 'Run the deletion for the user id across tables, snapshots and exports.', ['user id', 'event history', 'derived tables']),
45
+ ];
46
+ const VENDORS = [
47
+ { id: 'stripe', name: 'Stripe', identifier: 'email', credentialHelp: 'A restricted key that can read and delete customers.', settings: [] },
48
+ { id: 'hubspot', name: 'HubSpot', identifier: 'email', credentialHelp: 'A private app access token with the contacts scopes (it can permanently delete contacts).', settings: [] },
49
+ { id: 'mailchimp', name: 'Mailchimp', identifier: 'email', credentialHelp: 'A Mailchimp API key (it ends with the data center, like -us21).', settings: [{ key: 'list_id', required: true, pattern: /^[A-Za-z0-9]{6,20}$/, description: 'The audience (list) id the members live in.' }] },
50
+ { id: 'segment', name: 'Segment', identifier: 'user_id', credentialHelp: 'A Segment Public API token that can create regulations.', settings: [{ key: 'region', required: false, pattern: /^(us|eu)$/, description: 'us (default) or eu.' }] },
51
+ { id: 'intercom', name: 'Intercom', identifier: 'email', credentialHelp: 'An Intercom access token that can read and delete contacts.', settings: [{ key: 'region', required: false, pattern: /^(us|eu|au)$/, description: 'us (default), eu or au.' }] },
52
+ { id: 'zendesk', name: 'Zendesk', identifier: 'email', credentialHelp: 'A Zendesk API token for that agent (it needs permission to delete users permanently).', settings: [{ key: 'subdomain', required: true, pattern: /^[a-z0-9][a-z0-9-]{0,62}$/, description: 'Your Zendesk subdomain (the part before .zendesk.com).' }, { key: 'agent_email', required: true, pattern: /^[^\s@]{1,64}@[^\s@]{1,255}$/, description: 'The agent email the API token belongs to.' }] },
53
+ { id: 'brevo', name: 'Brevo', identifier: 'email', credentialHelp: 'A Brevo API key (v3).', settings: [] },
54
+ { id: 'customerio', name: 'Customer.io', identifier: 'user_id', credentialHelp: 'The Track API key for that site id.', settings: [{ key: 'site_id', required: true, pattern: /^[A-Za-z0-9]{1,64}$/, description: 'The workspace site id.' }, { key: 'region', required: false, pattern: /^(us|eu)$/, description: 'us (default) or eu.' }] },
55
+ ];
56
+ const vendorOf = (id) => VENDORS.find((v) => v.id === id);
57
+ function readVendorConfig(v, raw) {
58
+ const src = (0, emu4_shared_1.isObj)(raw) ? raw : {};
59
+ const out = {};
60
+ for (const k of Object.keys(src))
61
+ if (!v.settings.some((s) => s.key === k))
62
+ throw new Error(`${v.name} has no setting "${k}".`);
63
+ for (const s of v.settings) {
64
+ const x = src[s.key];
65
+ if (x === undefined || x === null || x === '') {
66
+ if (s.required)
67
+ throw new Error(`${v.name} needs config.${s.key}: ${s.description}`);
68
+ continue;
69
+ }
70
+ if (typeof x !== 'string' || !s.pattern.test(x))
71
+ throw new Error(`config.${s.key} is not valid: ${s.description}`);
72
+ out[s.key] = x;
73
+ }
74
+ return out;
75
+ }
76
+ const st = (0, support_1.engineState)((e) => ({ connectors: new store_1.Table('erasure_connectors', e.clock.now, 'ec'), credentials: new Map(), tasks: new Map(), notes: new store_1.Table('dsr_notes', e.clock.now, 'dsrn'), stubs: new Map(), acks: new Set(), reports: new store_1.Table('compliance_reports', e.clock.now, 'crep') }));
77
+ const stub = (e, vendor) => {
78
+ let s = st(e).stubs.get(vendor);
79
+ if (!s)
80
+ st(e).stubs.set(vendor, (s = { valid_keys: null, records: {}, fail_status: null, calls: [] }));
81
+ return s;
82
+ };
83
+ const presentConnector = (e, c) => ({
84
+ object: 'erasure_connector', id: c.id, key: c.key, catalog_id: c.catalog_id ?? null, name: c.name, mode: c.mode, ack_source: c.ack_source ?? null, enabled: c.enabled, notes: c.notes ?? null,
85
+ vendor: c.vendor ?? null, config: c.config ?? {}, has_credential: st(e).credentials.has(c.id), created_at: c.created_at, updated_at: c.updated_at,
86
+ });
87
+ function readConnector(e, b, existing) {
88
+ const out = { key: existing?.key ?? '', catalog_id: existing?.catalog_id ?? null, name: existing?.name ?? '', mode: existing?.mode ?? 'ack', ack_source: existing?.ack_source ?? null, enabled: existing?.enabled ?? true, notes: existing?.notes ?? null, vendor: existing?.vendor ?? null, config: existing?.config ?? {} };
89
+ if (b.catalog_id !== undefined && b.catalog_id !== null) {
90
+ const entry = CATALOG.find((x) => x.id === b.catalog_id);
91
+ if (!entry)
92
+ throw (0, emu4_shared_1.bad)('catalog_id is not in the erasure connector catalog (GET /v1/erasure_connector_catalog).', 'catalog_id');
93
+ out.catalog_id = entry.id;
94
+ if (!existing) {
95
+ out.key = entry.id;
96
+ out.name = entry.name;
97
+ }
98
+ }
99
+ if (b.key !== undefined) {
100
+ if (existing && b.key !== existing.key)
101
+ throw (0, emu4_shared_1.bad)('key cannot change.', 'key');
102
+ if (typeof b.key !== 'string' || !KEY_RE.test(b.key))
103
+ throw (0, emu4_shared_1.bad)('key is a slug: lowercase letters, digits, - and _, starting with a letter (at most 48).', 'key');
104
+ out.key = b.key;
105
+ }
106
+ if (b.name !== undefined) {
107
+ if (typeof b.name !== 'string' || !b.name.trim() || b.name.length > 120)
108
+ throw (0, emu4_shared_1.bad)('name is required text of at most 120 characters.', 'name');
109
+ out.name = b.name.trim();
110
+ }
111
+ if (b.mode !== undefined) {
112
+ if (b.mode !== 'ack' && b.mode !== 'manual' && b.mode !== 'api')
113
+ throw (0, emu4_shared_1.bad)('mode is ack, manual or api.', 'mode');
114
+ out.mode = b.mode;
115
+ }
116
+ if (b.ack_source !== undefined) {
117
+ if (b.ack_source !== null && (typeof b.ack_source !== 'string' || !b.ack_source.trim() || b.ack_source.length > 200))
118
+ throw (0, emu4_shared_1.bad)('ack_source is text of at most 200 characters, or null.', 'ack_source');
119
+ out.ack_source = b.ack_source === null ? null : b.ack_source.trim();
120
+ }
121
+ if (b.enabled !== undefined) {
122
+ if (typeof b.enabled !== 'boolean')
123
+ throw (0, emu4_shared_1.bad)('enabled is a boolean.', 'enabled');
124
+ out.enabled = b.enabled;
125
+ }
126
+ if (b.notes !== undefined) {
127
+ if (b.notes !== null && (typeof b.notes !== 'string' || b.notes.length > 1000))
128
+ throw (0, emu4_shared_1.bad)('notes is text of at most 1000 characters, or null.', 'notes');
129
+ out.notes = b.notes;
130
+ }
131
+ if (b.vendor !== undefined && b.vendor !== null)
132
+ out.vendor = b.vendor;
133
+ if (b.vendor === undefined && out.mode === 'api' && !out.vendor && out.catalog_id && vendorOf(out.catalog_id))
134
+ out.vendor = out.catalog_id;
135
+ let credential;
136
+ if (out.mode === 'api') {
137
+ const vendor = vendorOf(out.vendor);
138
+ if (!vendor)
139
+ throw (0, emu4_shared_1.bad)('mode api needs a vendor: stripe, hubspot, mailchimp, segment, intercom, zendesk, brevo or customerio.', 'vendor');
140
+ try {
141
+ out.config = readVendorConfig(vendor, b.config === undefined ? out.config : (b.config ?? {}));
142
+ }
143
+ catch (err) {
144
+ throw (0, emu4_shared_1.bad)(err instanceof Error ? err.message : 'config is not valid.', 'config');
145
+ }
146
+ if (b.credential !== undefined) {
147
+ if (typeof b.credential !== 'string' || !b.credential.trim() || b.credential.length > 2000)
148
+ throw (0, emu4_shared_1.bad)('credential is your key for the vendor (at most 2000 characters).', 'credential');
149
+ credential = b.credential.trim();
150
+ }
151
+ if (credential === undefined && !(existing && st(e).credentials.has(existing.id)))
152
+ throw (0, emu4_shared_1.bad)(`mode api needs credential: ${vendor.credentialHelp}`, 'credential');
153
+ }
154
+ else {
155
+ if (b.credential !== undefined && b.credential !== null)
156
+ throw (0, emu4_shared_1.bad)('credential is only for mode api.', 'credential');
157
+ out.vendor = null;
158
+ out.config = {};
159
+ credential = null;
160
+ }
161
+ if (!out.key)
162
+ throw (0, emu4_shared_1.bad)('Send key (or catalog_id to take the catalog entry).', 'key');
163
+ if (!out.name)
164
+ throw (0, emu4_shared_1.bad)('name is required.', 'name');
165
+ if (out.mode === 'ack' && !out.ack_source)
166
+ out.ack_source = out.key;
167
+ return { row: out, credential };
168
+ }
169
+ const CONNECTOR_FIELDS = ['key', 'catalog_id', 'name', 'mode', 'ack_source', 'enabled', 'notes', 'vendor', 'config', 'credential'];
170
+ const knownFields = (b, allowed) => {
171
+ const k = Object.keys(b).find((x) => !allowed.includes(x));
172
+ if (k)
173
+ throw (0, emu4_shared_1.bad)(`Unrecognised field ${k}. Expected any of ${allowed.join(', ')}.`, k);
174
+ };
175
+ // ── running a vendor ──
176
+ const emailsOf = (e, userId) => [...new Set((e.db.users.get(userId)?.emails ?? []).map((m) => String(m.email_address).toLowerCase()))];
177
+ function vendorCall(e, connector, action, identifier) {
178
+ const vendor = vendorOf(connector.vendor);
179
+ const s = stub(e, vendor.id);
180
+ const key = st(e).credentials.get(connector.id);
181
+ const log = (status) => s.calls.push({ at: e.clock.now(), action, connector_id: connector.id, identifier_hash: identifier ? identifier.length : null, status, key_accepted: status !== 401 });
182
+ if (!key)
183
+ throw new Error('This connector has no key saved.');
184
+ if (s.valid_keys && !s.valid_keys.includes(key)) {
185
+ log(401);
186
+ throw new Error(`${vendor.name} refused the key (HTTP 401).`);
187
+ }
188
+ if (s.fail_status) {
189
+ log(s.fail_status);
190
+ throw new Error(`${vendor.name} returned HTTP ${s.fail_status}.`);
191
+ }
192
+ log(200);
193
+ if (action === 'probe' || !identifier)
194
+ return { erased: 0 };
195
+ const n = s.records[identifier] ?? 0;
196
+ delete s.records[identifier];
197
+ return { erased: n };
198
+ }
199
+ function runApiConnectors(e, c, input) {
200
+ const s = st(e);
201
+ const out = [];
202
+ const subject = { userId: input.userId, emails: emailsOf(e, input.userId) };
203
+ for (const conn of s.connectors.all().reverse()) {
204
+ if (conn.mode !== 'api' || !conn.enabled || (input.connectorId && conn.id !== input.connectorId))
205
+ continue;
206
+ const vendor = vendorOf(conn.vendor);
207
+ if (!vendor)
208
+ continue;
209
+ const taskKey = `${conn.id}:${input.dsrId}`;
210
+ if (!input.dryRun && s.tasks.get(taskKey)?.status === 'done')
211
+ continue;
212
+ const row = { connector_id: conn.id, key: conn.key, vendor: vendor.id };
213
+ try {
214
+ if (input.dryRun) {
215
+ vendorCall(e, conn, 'probe');
216
+ const what = vendor.identifier === 'email' ? `${subject.emails.length} email address(es)` : 'the user id';
217
+ out.push({ ...row, status: 'dry_run', note: `The key works. Would look up ${what} and erase what is found. Nothing was deleted.`, error: null });
218
+ continue;
219
+ }
220
+ let erased = 0;
221
+ for (const id of vendor.identifier === 'email' ? subject.emails : [subject.userId])
222
+ erased += vendorCall(e, conn, 'erase', id).erased;
223
+ const note = erased === 0 ? `${vendor.name}: nothing found for this person.` : `${vendor.name}: ${erased} record${erased === 1 ? '' : 's'} erased.`;
224
+ const prev = s.tasks.get(taskKey);
225
+ s.tasks.set(taskKey, { status: 'done', note, error: null, attempts: (prev?.attempts ?? 0) + 1, completed_by: 'automatic', at: e.clock.now() });
226
+ out.push({ ...row, status: 'done', note, error: null });
227
+ }
228
+ catch (err) {
229
+ const error = (err instanceof Error ? err.message : 'The call failed.').slice(0, 300);
230
+ if (!input.dryRun) {
231
+ const prev = s.tasks.get(taskKey);
232
+ s.tasks.set(taskKey, { status: 'failed', note: null, error, attempts: (prev?.attempts ?? 0) + 1, completed_by: 'automatic', at: e.clock.now() });
233
+ }
234
+ out.push({ ...row, status: input.dryRun ? 'dry_run' : 'failed', note: null, error });
235
+ }
236
+ if (!input.dryRun)
237
+ (0, emu4_shared_1.audit)(e, c, 'erasure_connector.run', 'data_subject_request', input.dsrId, { connector_id: conn.id, vendor: vendor.id, status: out[out.length - 1].status });
238
+ }
239
+ return out;
240
+ }
241
+ function downstream(e, dsrId) {
242
+ const dsr = (0, emu3_misc_1.dsrRows)(e).get(dsrId);
243
+ if (!dsr)
244
+ throw (0, errors_1.notFound)('Data subject request');
245
+ const s = st(e);
246
+ const acked = new Set(s.acks);
247
+ const g = e.generic.get('users/erasure_acks');
248
+ if (g)
249
+ for (const r of g.all())
250
+ if (r._parent === dsr.user_id)
251
+ acked.add(String(r.source ?? r.app_id));
252
+ const rows = s.connectors.all().reverse().filter((x) => x.enabled).map((x) => {
253
+ const task = s.tasks.get(`${x.id}:${dsrId}`);
254
+ const ack = x.mode === 'ack' && !!x.ack_source && (acked.has(`${dsr.user_id}:${x.ack_source}`) || acked.has(x.ack_source));
255
+ const status = task ? (task.status === 'not_applicable' ? 'not_applicable' : task.status === 'failed' ? 'failed' : 'done') : ack ? 'done' : 'pending';
256
+ return { connector_id: x.id, key: x.key, name: x.name, mode: x.mode, status, by: task ? (task.completed_by === 'automatic' ? 'automatic' : 'operator') : ack ? 'acknowledgement' : null, note: task?.note ?? null, error: task?.error ?? null, attempts: task?.attempts ?? 0, completed_at: task?.at ?? null };
257
+ });
258
+ return { object: 'downstream_erasure', request_id: dsrId, complete: rows.every((r) => r.status === 'done' || r.status === 'not_applicable'), pending: rows.filter((r) => r.status === 'pending').length, failed: rows.filter((r) => r.status === 'failed').length, connectors: rows };
259
+ }
260
+ const dsrOf = (e, id) => {
261
+ const r = (0, emu3_misc_1.dsrRows)(e).get(id);
262
+ if (!r)
263
+ throw (0, errors_1.notFound)('Data subject request');
264
+ return r;
265
+ };
266
+ // ── reports ──
267
+ const jwtKid = (jws) => {
268
+ try {
269
+ return JSON.parse(atob(jws.split('.')[0].replace(/-/g, '+').replace(/_/g, '/'))).kid ?? null;
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ };
275
+ function summary(e) {
276
+ const rows = (0, emu3_misc_1.dsrRows)(e).all().map((r) => ({ r, v: (0, emu3_misc_1.view)(e, r) }));
277
+ const open = rows.filter((x) => !['completed', 'rejected', 'cancelled'].includes(x.v.status));
278
+ const by = {};
279
+ for (const x of rows)
280
+ by[x.v.status] = (by[x.v.status] ?? 0) + 1;
281
+ return {
282
+ open: open.length,
283
+ overdue: open.filter((x) => x.v.overdue === true).length,
284
+ due_soon: open.filter((x) => x.v.due_at !== null && x.v.due_at >= e.clock.now() && x.v.due_at <= e.clock.now() + 7 * emu4_shared_1.DAY).length,
285
+ unassigned: open.filter((x) => !x.r.assignee).length,
286
+ by_status: by,
287
+ oldest_open_at: open.length ? Math.min(...open.map((x) => x.r.requested_at)) : null,
288
+ };
289
+ }
290
+ function posture(e) {
291
+ const c = (0, emu4_shared_1.instanceConfig)(e).compliance;
292
+ const policies = (e.generic.get('policies')?.all() ?? []).filter((p) => p.status !== 'retired');
293
+ const connectors = st(e).connectors.all().filter((x) => x.enabled);
294
+ const sm = summary(e);
295
+ const holds = [...(e.generic.get('legal_holds')?.all() ?? [])].length;
296
+ const on = (b) => (b ? 'configured' : 'not_configured');
297
+ const controls = [
298
+ { id: 'dsr_deadline', title: 'Data rights response deadline', status: 'configured', detail: `${c.retention.dsrDueDays} days` },
299
+ { id: 'erasure_grace', title: 'Erasure cancellation window', status: 'configured', detail: `${c.retention.erasureGraceDays} days, personal data scrubbed ${c.retention.deletedUserGraceDays} days after deletion` },
300
+ { id: 'ownership_transfer', title: 'Ownership hand-over before deleting a sole admin', status: on(c.retention.ownershipTransfer !== 'off'), detail: c.retention.ownershipTransfer },
301
+ { id: 'policy_acceptance', title: 'Policy acceptance', status: on(policies.some((p) => p.required !== false)), detail: `${policies.filter((p) => p.required !== false).length} required, ${policies.length} active policies` },
302
+ { id: 'consent_versions', title: 'Declared consent versions', status: on(c.consents.length > 0), detail: `${c.consents.length} declared` },
303
+ { id: 'cookie_consent_store', title: 'Cookie consent store', status: on(c.cookieConsent.enabled), detail: c.cookieConsent.enabled ? `GPC ${c.cookieConsent.honorGpc ? 'honored' : 'ignored'}, re-prompt on policy change ${c.cookieConsent.policyVersion ? 'on' : 'off'}` : 'off' },
304
+ { id: 'age_gating', title: 'Age gating and parental consent', status: on(c.minors.enabled), detail: c.minors.enabled ? `consent methods ${c.minors.consentMethods.join(', ')}, floor ${c.minors.minAssuranceLevel}` : 'off' },
305
+ { id: 'inactive_users', title: 'Inactive-user policy', status: on(c.inactiveUsers.enabled), detail: c.inactiveUsers.enabled ? `${c.inactiveUsers.action} after ${c.inactiveUsers.afterDays} days` : 'off' },
306
+ { id: 'record_of_processing', title: 'Record of processing', status: on(c.dataPolicy.activities.length > 0 && !!c.dataPolicy.controller.name), detail: `${c.dataPolicy.activities.length} activities` },
307
+ { id: 'downstream_erasure', title: 'Downstream erasure tracking', status: on(connectors.length > 0), detail: `${connectors.length} systems tracked` },
308
+ { id: 'data_region', title: 'Declared data region', status: on(!!c.dataRegion), detail: c.dataRegion ?? 'not declared' },
309
+ ];
310
+ const completed90 = (0, emu3_misc_1.dsrRows)(e).all().filter((r) => r.status === 'completed' && r.completed_at && e.clock.now() - r.completed_at <= 90 * emu4_shared_1.DAY).length;
311
+ return {
312
+ object: 'compliance_posture', instance_id: e.instanceId, generated_at: new Date(e.clock.now()).toISOString(), controls,
313
+ summary: { configured: controls.filter((x) => x.status === 'configured').length, total: controls.length },
314
+ data_rights: { open: sm.open, overdue: sm.overdue, due_in_7_days: sm.due_soon, unassigned: sm.unassigned, completed_last_90_days: completed90, active_legal_holds: Math.min(holds, 100) },
315
+ downstream_erasure: connectors.map((x) => ({ key: x.key, name: x.name, mode: x.mode })),
316
+ };
317
+ }
318
+ function ropa(e) {
319
+ const c = (0, emu4_shared_1.instanceConfig)(e).compliance;
320
+ const dp = c.dataPolicy;
321
+ const findings = [];
322
+ if (!dp.controller.name)
323
+ findings.push({ code: 'controller_missing', message: 'Name the controller in compliance.dataPolicy.controller.name.' });
324
+ if (dp.activities.length === 0)
325
+ findings.push({ code: 'no_activities', message: 'No processing activities are declared.' });
326
+ const consentBacked = c.consents.length > 0 || c.cookieConsent.enabled;
327
+ for (const a of dp.activities) {
328
+ if (a.legalBasis === 'consent' && !consentBacked)
329
+ findings.push({ code: 'consent_without_record', message: 'The legal basis is consent but no consent versions or cookie consent store are configured to record it.', activity: a.key });
330
+ if (a.retentionDays === null || a.retentionDays === undefined)
331
+ findings.push({ code: 'no_retention', message: 'No retention period: data is kept until the account is deleted.', activity: a.key });
332
+ if ((a.transfersTo?.length ?? 0) > 0 && (a.recipients?.length ?? 0) === 0)
333
+ findings.push({ code: 'transfer_without_recipient', message: 'Transfers are listed with no recipient named.', activity: a.key });
334
+ }
335
+ return {
336
+ object: 'record_of_processing', instance_id: e.instanceId, generated_at: new Date(e.clock.now()).toISOString(), controller: dp.controller, processor: { name: 'Atlas', role: 'processor' }, data_region: c.dataRegion, activities: dp.activities,
337
+ retention: { deleted_user_scrub_days: c.retention.deletedUserGraceDays, erasure_grace_days: c.retention.erasureGraceDays, anonymous_retention_days: c.retention.anonymousRetentionDays, inactive_users: c.inactiveUsers.enabled ? { action: c.inactiveUsers.action, after_days: c.inactiveUsers.afterDays } : null },
338
+ atlas_sub_processors: [], findings,
339
+ };
340
+ }
341
+ const presentReport = (e, r, signed = true) => ({ object: 'compliance_report', id: r.id, kind: r.kind, generated_at: r.created_at, generated_by: r.generated_by, claims: r.payload, ...(signed ? { report: r.jws, kid: jwtKid(r.jws), jwks_uri: `${e.issuer}/.well-known/jwks.json` } : {}) });
342
+ let hooked = false;
343
+ function erasureHandlers() {
344
+ // Fulfilling an erasure runs the api connectors (a failure is recorded and never blocks it) and drops the user's cookie choices.
345
+ if (!hooked) {
346
+ hooked = true;
347
+ emu3_misc_1.erasureHooks.push((e, r) => {
348
+ runApiConnectors(e, null, { dsrId: r.id, userId: r.user_id });
349
+ (0, emu4_cookies_1.eraseCookieRecordsOfUser)(e, r.user_id);
350
+ });
351
+ }
352
+ const READ = 'data_subject_requests:read';
353
+ const WRITE = 'data_subject_requests:write';
354
+ const connectorOf = (e, id) => {
355
+ const x = st(e).connectors.get(id);
356
+ if (!x)
357
+ throw (0, errors_1.notFound)('Erasure connector');
358
+ return x;
359
+ };
360
+ return [
361
+ // ── reports ──
362
+ { method: 'GET', pattern: '/v1/compliance/posture', scope: 'instance:read', handler: (c) => ({ body: posture(c.engine) }) },
363
+ { method: 'GET', pattern: '/v1/compliance/ropa', scope: 'instance:read', handler: (c) => ({ body: ropa(c.engine) }) },
364
+ { method: 'POST', pattern: '/v1/compliance/reports', scope: 'instance:write', handler: async (c) => {
365
+ const e = c.engine;
366
+ knownFields(c.body, ['kind']);
367
+ const kind = c.body.kind;
368
+ if (kind !== 'posture' && kind !== 'ropa')
369
+ throw (0, emu4_shared_1.bad)('kind is posture or ropa.', 'kind');
370
+ const payload = kind === 'posture' ? posture(e) : ropa(e);
371
+ // shortcut: signed as a JWT with the engine key (verifies against the engine JWKS), not as a typ "atlas-compliance-report+jwt" JWS.
372
+ const jws = await e.tokens.signClaims(payload, 10 * 365 * 86_400);
373
+ const row = st(e).reports.insert({ kind, payload, jws, generated_by: (0, emu4_shared_1.actorOf)(c).id, created_at: e.clock.now() });
374
+ (0, emu4_shared_1.audit)(e, c, 'compliance.report_generated', 'compliance_report', row.id, { kind });
375
+ e.webhooks.emit('compliance.report_generated', { object: 'compliance_report', id: row.id, kind, generated_at: row.created_at });
376
+ return { status: 201, body: presentReport(e, row) };
377
+ } },
378
+ { method: 'GET', pattern: '/v1/compliance/reports', scope: 'instance:read', handler: (c) => ({ body: (0, emu4_shared_1.pageReply)(c, st(c.engine).reports.filter((r) => !c.query.get('kind') || r.kind === c.query.get('kind')).map((r) => presentReport(c.engine, r, false))) }) },
379
+ { method: 'GET', pattern: '/v1/compliance/reports/:id', scope: 'instance:read', handler: (c) => {
380
+ const r = st(c.engine).reports.get(c.params.id);
381
+ if (!r)
382
+ throw (0, errors_1.notFound)('Report');
383
+ return { body: presentReport(c.engine, r) };
384
+ } },
385
+ { method: 'GET', pattern: '/v1/compliance/inactive_users/preview', scope: 'users:read', handler: (c) => {
386
+ const e = c.engine;
387
+ const cfg = (0, emu4_shared_1.instanceConfig)(e).compliance.inactiveUsers;
388
+ const now = e.clock.now();
389
+ const limit = Math.min(Math.max(Math.floor(Number(c.query.get('limit') ?? 20)) || 20, 1), 100);
390
+ const last = (u) => Math.max(u.created_at ?? 0, ...e.db.sessions.filter((s) => s.user_id === u.id).map((s) => s.last_active_at ?? 0));
391
+ const live = e.db.users.all().filter((u) => !u.deleted && !u.banned && !(0, support_1.activeHold)(e, u.id));
392
+ const due = live.filter((u) => last(u) <= now - cfg.afterDays * emu4_shared_1.DAY).slice(0, limit);
393
+ const dueIds = new Set(due.map((u) => u.id));
394
+ const warn = cfg.warnDays > 0 ? live.filter((u) => last(u) <= now - (cfg.afterDays - cfg.warnDays) * emu4_shared_1.DAY && !dueIds.has(u.id)).slice(0, limit) : [];
395
+ return { body: { object: 'inactive_users_preview', enabled: cfg.enabled, action: cfg.action, after_days: cfg.afterDays, warn_days: cfg.warnDays, would_act: due.map((u) => ({ user_id: u.id, last_active_at: last(u) })), would_warn: warn.map((u) => ({ user_id: u.id, last_active_at: last(u) })), truncated: due.length >= limit || warn.length >= limit } };
396
+ } },
397
+ // ── the request board ──
398
+ { method: 'GET', pattern: '/v1/dsr_console/summary', scope: READ, handler: (c) => ({ body: { object: 'dsr_console_summary', ...summary(c.engine) } }) },
399
+ { method: 'GET', pattern: '/v1/dsr_console/requests', scope: READ, handler: (c) => {
400
+ const e = c.engine;
401
+ const q = (k) => c.query.get(k);
402
+ let within = null;
403
+ if (q('due_within_days') !== null) {
404
+ within = Number(q('due_within_days'));
405
+ if (!Number.isInteger(within) || within < 0 || within > 365)
406
+ throw (0, emu4_shared_1.bad)('due_within_days is a whole number from 0 to 365.', 'due_within_days');
407
+ }
408
+ const rows = (0, emu3_misc_1.dsrRows)(e).all().filter((r) => {
409
+ const v = (0, emu3_misc_1.view)(e, r);
410
+ return (!q('status') || r.status === q('status')) && (!q('type') || r.type === q('type')) && (!q('assignee') || r.assignee === q('assignee')) && (q('unassigned') !== 'true' || !r.assignee) && (!q('priority') || (r.priority ?? 'normal') === q('priority')) && (!q('label') || (r.labels ?? []).includes(q('label'))) && (q('overdue') !== 'true' || v.overdue === true) && (within === null || (v.due_at !== null && v.due_at <= e.clock.now() + within * emu4_shared_1.DAY));
411
+ });
412
+ return { body: (0, emu4_shared_1.pageReply)(c, rows.map((r) => (0, emu3_misc_1.view)(e, r))) };
413
+ } },
414
+ { method: 'PATCH', pattern: '/v1/data_subject_requests/:id/triage', scope: WRITE, handler: (c) => {
415
+ const e = c.engine;
416
+ knownFields(c.body, ['assignee', 'priority', 'labels']);
417
+ const r = dsrOf(e, c.params.id);
418
+ const b = c.body;
419
+ const patch = {};
420
+ if (b.assignee !== undefined) {
421
+ if (b.assignee !== null && (typeof b.assignee !== 'string' || !b.assignee.trim() || b.assignee.length > 200))
422
+ throw (0, emu4_shared_1.bad)('assignee is a name or id of at most 200 characters, or null.', 'assignee');
423
+ patch.assignee = b.assignee === null ? null : b.assignee.trim();
424
+ }
425
+ if (b.priority !== undefined) {
426
+ if (!PRIORITIES.includes(b.priority))
427
+ throw (0, emu4_shared_1.bad)(`priority must be one of ${PRIORITIES.join(', ')}.`, 'priority');
428
+ patch.priority = b.priority;
429
+ }
430
+ if (b.labels !== undefined) {
431
+ if (!Array.isArray(b.labels) || b.labels.length > 10 || b.labels.some((l) => typeof l !== 'string' || !LABEL_RE.test(l)))
432
+ throw (0, emu4_shared_1.bad)('labels is up to 10 lowercase labels (letters, digits, _ . : -).', 'labels');
433
+ patch.labels = [...new Set(b.labels)];
434
+ }
435
+ if (Object.keys(patch).length === 0)
436
+ throw (0, emu4_shared_1.bad)('Send assignee, priority or labels.', 'assignee');
437
+ Object.assign(r, patch, { updated_at: e.clock.now() });
438
+ (0, emu4_shared_1.audit)(e, c, 'data_subject_request.triaged', 'data_subject_request', r.id, patch);
439
+ return { body: (0, emu3_misc_1.view)(e, r) };
440
+ } },
441
+ { method: 'GET', pattern: '/v1/data_subject_requests/:id/notes', scope: READ, handler: (c) => {
442
+ const e = c.engine;
443
+ dsrOf(e, c.params.id);
444
+ return { body: (0, emu4_shared_1.listOf)(st(e).notes.filter((n) => n.request_id === c.params.id).reverse().map((n) => ({ object: 'dsr_note', id: n.id, request_id: n.request_id, author: n.author, body: n.body, created_at: n.created_at }))) };
445
+ } },
446
+ { method: 'POST', pattern: '/v1/data_subject_requests/:id/notes', scope: WRITE, handler: (c) => {
447
+ const e = c.engine;
448
+ knownFields(c.body, ['body']);
449
+ dsrOf(e, c.params.id);
450
+ const text = typeof c.body.body === 'string' ? c.body.body.trim() : '';
451
+ if (!text || text.length > 4000)
452
+ throw (0, emu4_shared_1.bad)('body is required (at most 4000 characters).', 'body');
453
+ const n = st(e).notes.insert({ request_id: c.params.id, author: (0, emu4_shared_1.actorOf)(c).id, body: text, created_at: e.clock.now() });
454
+ (0, emu4_shared_1.audit)(e, c, 'data_subject_request.note_added', 'data_subject_request', c.params.id, { note_id: n.id });
455
+ return { status: 201, body: { object: 'dsr_note', id: n.id, request_id: n.request_id, author: n.author, body: n.body, created_at: n.created_at } };
456
+ } },
457
+ // ── downstream erasure ──
458
+ { method: 'GET', pattern: '/v1/erasure_connector_catalog', scope: READ, handler: () => ({ body: (0, emu4_shared_1.listOf)(CATALOG.map((x) => {
459
+ const v = vendorOf(x.id);
460
+ return { object: 'erasure_catalog_entry', ...x, holds: [...x.holds], api_vendor: v ? { settings: v.settings.map((s) => ({ key: s.key, required: s.required, description: s.description })), credential_help: v.credentialHelp } : null };
461
+ })) }) },
462
+ { method: 'GET', pattern: '/v1/erasure_connectors', scope: READ, handler: (c) => ({ body: (0, emu4_shared_1.listOf)(st(c.engine).connectors.all().reverse().map((x) => presentConnector(c.engine, x))) }) },
463
+ { method: 'POST', pattern: '/v1/erasure_connectors', scope: WRITE, handler: (c) => {
464
+ const e = c.engine;
465
+ knownFields(c.body, CONNECTOR_FIELDS);
466
+ const { row, credential } = readConnector(e, c.body);
467
+ if (st(e).connectors.find((x) => x.key === row.key))
468
+ throw (0, emu4_shared_1.conflict)(`An erasure connector with key "${row.key}" already exists.`);
469
+ const now = e.clock.now();
470
+ const made = st(e).connectors.insert({ ...row, created_at: now, updated_at: now });
471
+ if (credential)
472
+ st(e).credentials.set(made.id, credential);
473
+ (0, emu4_shared_1.audit)(e, c, 'erasure_connector.created', 'erasure_connector', made.id, { key: made.key, mode: made.mode, vendor: made.vendor });
474
+ return { status: 201, body: presentConnector(e, made) };
475
+ } },
476
+ { method: 'GET', pattern: '/v1/erasure_connectors/:id', scope: READ, handler: (c) => ({ body: presentConnector(c.engine, connectorOf(c.engine, c.params.id)) }) },
477
+ { method: 'PATCH', pattern: '/v1/erasure_connectors/:id', scope: WRITE, handler: (c) => {
478
+ const e = c.engine;
479
+ knownFields(c.body, CONNECTOR_FIELDS);
480
+ const x = connectorOf(e, c.params.id);
481
+ const { row, credential } = readConnector(e, c.body, x);
482
+ Object.assign(x, { name: row.name, mode: row.mode, ack_source: row.ack_source, enabled: row.enabled, notes: row.notes, vendor: row.vendor, config: row.config, updated_at: e.clock.now() });
483
+ if (credential)
484
+ st(e).credentials.set(x.id, credential);
485
+ else if (credential === null)
486
+ st(e).credentials.delete(x.id);
487
+ (0, emu4_shared_1.audit)(e, c, 'erasure_connector.updated', 'erasure_connector', x.id, { key: x.key, credential_changed: c.body.credential !== undefined });
488
+ return { body: presentConnector(e, x) };
489
+ } },
490
+ { method: 'POST', pattern: '/v1/erasure_connectors/:id/probe', scope: WRITE, handler: (c) => {
491
+ const e = c.engine;
492
+ const x = connectorOf(e, c.params.id);
493
+ const vendor = x.mode === 'api' ? vendorOf(x.vendor) : undefined;
494
+ if (!vendor)
495
+ throw (0, emu4_shared_1.bad)('Only an api connector can be probed.', 'mode');
496
+ let ok = true;
497
+ let error = null;
498
+ try {
499
+ vendorCall(e, x, 'probe');
500
+ }
501
+ catch (err) {
502
+ ok = false;
503
+ error = err instanceof Error ? err.message : 'The probe failed.';
504
+ }
505
+ (0, emu4_shared_1.audit)(e, c, 'erasure_connector.probed', 'erasure_connector', x.id, { ok });
506
+ return { body: { object: 'erasure_connector_probe', connector_id: x.id, vendor: vendor.id, ok, error } };
507
+ } },
508
+ { method: 'DELETE', pattern: '/v1/erasure_connectors/:id', scope: WRITE, handler: (c) => {
509
+ const e = c.engine;
510
+ const x = connectorOf(e, c.params.id);
511
+ st(e).connectors.delete(x.id);
512
+ st(e).credentials.delete(x.id);
513
+ (0, emu4_shared_1.audit)(e, c, 'erasure_connector.deleted', 'erasure_connector', x.id, {});
514
+ return { status: 204 };
515
+ } },
516
+ { method: 'GET', pattern: '/v1/data_subject_requests/:id/downstream', scope: READ, handler: (c) => ({ body: downstream(c.engine, c.params.id) }) },
517
+ { method: 'POST', pattern: '/v1/data_subject_requests/:id/downstream/:connectorId', scope: WRITE, handler: (c) => {
518
+ const e = c.engine;
519
+ knownFields(c.body, ['status', 'note']);
520
+ dsrOf(e, c.params.id);
521
+ const x = connectorOf(e, c.params.connectorId);
522
+ if (x.mode !== 'manual')
523
+ throw (0, emu4_shared_1.bad)('Only a manual connector is marked by an operator; an ack connector confirms itself.', 'connector_id');
524
+ const status = c.body.status === undefined ? 'done' : c.body.status;
525
+ if (status !== 'done' && status !== 'not_applicable')
526
+ throw (0, emu4_shared_1.bad)('status is done or not_applicable.', 'status');
527
+ if (c.body.note !== undefined && (typeof c.body.note !== 'string' || c.body.note.length > 1000))
528
+ throw (0, emu4_shared_1.bad)('note is text of at most 1000 characters.', 'note');
529
+ st(e).tasks.set(`${x.id}:${c.params.id}`, { status, note: c.body.note ?? null, error: null, attempts: 1, completed_by: (0, emu4_shared_1.actorOf)(c).id ?? 'operator', at: e.clock.now() });
530
+ (0, emu4_shared_1.audit)(e, c, 'erasure_connector.task_marked', 'data_subject_request', c.params.id, { connector_id: x.id, status });
531
+ return { body: downstream(e, c.params.id) };
532
+ } },
533
+ { method: 'POST', pattern: '/v1/data_subject_requests/:id/downstream/:connectorId/run', scope: WRITE, handler: (c) => {
534
+ const e = c.engine;
535
+ knownFields(c.body, ['dry_run']);
536
+ if (c.body.dry_run !== undefined && typeof c.body.dry_run !== 'boolean')
537
+ throw (0, emu4_shared_1.bad)('dry_run is a boolean.', 'dry_run');
538
+ const dsr = dsrOf(e, c.params.id);
539
+ const x = connectorOf(e, c.params.connectorId);
540
+ if (x.mode !== 'api')
541
+ throw (0, emu4_shared_1.bad)('Only an api connector runs automatically.', 'connector_id');
542
+ if (dsr.type !== 'erasure')
543
+ throw (0, emu4_shared_1.bad)('Erasure connectors run for erasure requests.', 'id');
544
+ const results = runApiConnectors(e, c, { dsrId: dsr.id, userId: dsr.user_id, connectorId: x.id, dryRun: c.body.dry_run === true });
545
+ return { body: { object: 'erasure_connector_run', dry_run: c.body.dry_run === true, results, downstream: downstream(e, dsr.id) } };
546
+ } },
547
+ // ── emulator helpers ──
548
+ // PUT /__emulator/compliance/erasure_vendors/:vendor { valid_keys?: string[] | null, records?: { <email or user id>: <count> }, fail_status?: number | null }
549
+ { method: 'PUT', pattern: '/__emulator/compliance/erasure_vendors/:vendor', public: true, handler: (c) => {
550
+ const e = c.engine;
551
+ if (!vendorOf(c.params.vendor))
552
+ throw (0, errors_1.notFound)('Vendor');
553
+ const s = stub(e, c.params.vendor);
554
+ const b = c.body;
555
+ if (b.valid_keys !== undefined) {
556
+ if (b.valid_keys !== null && !(Array.isArray(b.valid_keys) && b.valid_keys.every((k) => typeof k === 'string')))
557
+ throw (0, emu4_shared_1.bad)('valid_keys is a list of keys the vendor accepts, or null for any key.', 'valid_keys');
558
+ s.valid_keys = b.valid_keys;
559
+ }
560
+ if (b.records !== undefined) {
561
+ if (!(0, emu4_shared_1.isObj)(b.records) || Object.values(b.records).some((n) => !Number.isInteger(n) || n < 0))
562
+ throw (0, emu4_shared_1.bad)('records maps an email address or user id to a count.', 'records');
563
+ s.records = Object.fromEntries(Object.entries(b.records).map(([k, n]) => [k.toLowerCase(), n]));
564
+ }
565
+ if (b.fail_status !== undefined) {
566
+ if (b.fail_status !== null && !(Number.isInteger(b.fail_status) && b.fail_status >= 400 && b.fail_status <= 599))
567
+ throw (0, emu4_shared_1.bad)('fail_status is an HTTP status from 400 to 599, or null.', 'fail_status');
568
+ s.fail_status = b.fail_status;
569
+ }
570
+ return { body: { object: 'erasure_vendor_stub', vendor: c.params.vendor, accepts_any_key: s.valid_keys === null, records: s.records, fail_status: s.fail_status } };
571
+ } },
572
+ { method: 'GET', pattern: '/__emulator/compliance/erasure_vendors/:vendor/calls', public: true, handler: (c) => {
573
+ if (!vendorOf(c.params.vendor))
574
+ throw (0, errors_1.notFound)('Vendor');
575
+ return { body: (0, emu4_shared_1.listOf)([...stub(c.engine, c.params.vendor).calls]) };
576
+ } },
577
+ // POST /__emulator/compliance/erasure_acks { user_id, source }: a downstream system confirming it erased the user.
578
+ { method: 'POST', pattern: '/__emulator/compliance/erasure_acks', public: true, handler: (c) => {
579
+ const e = c.engine;
580
+ if (typeof c.body.user_id !== 'string' || typeof c.body.source !== 'string' || !c.body.source)
581
+ throw (0, emu4_shared_1.bad)('user_id and source are required.', 'source');
582
+ st(e).acks.add(`${c.body.user_id}:${c.body.source}`);
583
+ return { status: 201, body: { object: 'erasure_ack', user_id: c.body.user_id, source: c.body.source } };
584
+ } },
585
+ ];
586
+ }
587
+ //# sourceMappingURL=emu4-erasure.js.map