@atlasauth/emulator-core 0.2.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 (244) hide show
  1. package/dist/custom-html.d.ts +57 -0
  2. package/dist/custom-html.js +491 -0
  3. package/dist/custom-html.js.map +1 -0
  4. package/dist/engine.d.ts +1 -1
  5. package/dist/engine.js +4 -0
  6. package/dist/engine.js.map +1 -1
  7. package/dist/error-catalog.js +8 -0
  8. package/dist/error-catalog.js.map +1 -1
  9. package/dist/event-gate.js +60 -2
  10. package/dist/event-gate.js.map +1 -1
  11. package/dist/generic.js +5 -1
  12. package/dist/generic.js.map +1 -1
  13. package/dist/handlers/access-signals.d.ts +48 -0
  14. package/dist/handlers/access-signals.js +76 -0
  15. package/dist/handlers/access-signals.js.map +1 -0
  16. package/dist/handlers/approvals.js +54 -1
  17. package/dist/handlers/approvals.js.map +1 -1
  18. package/dist/handlers/authz-data.d.ts +2 -0
  19. package/dist/handlers/authz-data.js +627 -0
  20. package/dist/handlers/authz-data.js.map +1 -0
  21. package/dist/handlers/authz-layers.d.ts +229 -0
  22. package/dist/handlers/authz-layers.js +786 -0
  23. package/dist/handlers/authz-layers.js.map +1 -0
  24. package/dist/handlers/authz-state.d.ts +80 -0
  25. package/dist/handlers/authz-state.js +175 -0
  26. package/dist/handlers/authz-state.js.map +1 -0
  27. package/dist/handlers/authz.js +240 -177
  28. package/dist/handlers/authz.js.map +1 -1
  29. package/dist/handlers/billing.d.ts +99 -1
  30. package/dist/handlers/billing.js +556 -77
  31. package/dist/handlers/billing.js.map +1 -1
  32. package/dist/handlers/cedar-ext.d.ts +35 -0
  33. package/dist/handlers/cedar-ext.js +174 -0
  34. package/dist/handlers/cedar-ext.js.map +1 -0
  35. package/dist/handlers/cedar.d.ts +146 -0
  36. package/dist/handlers/cedar.js +689 -0
  37. package/dist/handlers/cedar.js.map +1 -0
  38. package/dist/handlers/connections.d.ts +4 -1
  39. package/dist/handlers/connections.js +267 -38
  40. package/dist/handlers/connections.js.map +1 -1
  41. package/dist/handlers/control.js +6 -1
  42. package/dist/handlers/control.js.map +1 -1
  43. package/dist/handlers/device.js +18 -1
  44. package/dist/handlers/device.js.map +1 -1
  45. package/dist/handlers/emu3-misc.d.ts +38 -0
  46. package/dist/handlers/emu3-misc.js +1056 -0
  47. package/dist/handlers/emu3-misc.js.map +1 -0
  48. package/dist/handlers/emu4-actions-access.d.ts +46 -0
  49. package/dist/handlers/emu4-actions-access.js +107 -0
  50. package/dist/handlers/emu4-actions-access.js.map +1 -0
  51. package/dist/handlers/emu4-actions-catalog-extra.d.ts +3 -0
  52. package/dist/handlers/emu4-actions-catalog-extra.js +204 -0
  53. package/dist/handlers/emu4-actions-catalog-extra.js.map +1 -0
  54. package/dist/handlers/emu4-actions-catalog.d.ts +3 -0
  55. package/dist/handlers/emu4-actions-catalog.js +143 -0
  56. package/dist/handlers/emu4-actions-catalog.js.map +1 -0
  57. package/dist/handlers/emu4-actions-def.d.ts +50 -0
  58. package/dist/handlers/emu4-actions-def.js +250 -0
  59. package/dist/handlers/emu4-actions-def.js.map +1 -0
  60. package/dist/handlers/emu4-actions-sandbox.d.ts +36 -0
  61. package/dist/handlers/emu4-actions-sandbox.js +318 -0
  62. package/dist/handlers/emu4-actions-sandbox.js.map +1 -0
  63. package/dist/handlers/emu4-actions-typegen.d.ts +5 -0
  64. package/dist/handlers/emu4-actions-typegen.js +73 -0
  65. package/dist/handlers/emu4-actions-typegen.js.map +1 -0
  66. package/dist/handlers/emu4-actions.d.ts +2 -0
  67. package/dist/handlers/emu4-actions.js +506 -0
  68. package/dist/handlers/emu4-actions.js.map +1 -0
  69. package/dist/handlers/emu4-authz-trust.d.ts +40 -0
  70. package/dist/handlers/emu4-authz-trust.js +488 -0
  71. package/dist/handlers/emu4-authz-trust.js.map +1 -0
  72. package/dist/handlers/emu4-authz.d.ts +2 -0
  73. package/dist/handlers/emu4-authz.js +752 -0
  74. package/dist/handlers/emu4-authz.js.map +1 -0
  75. package/dist/handlers/emu4-billing-core.d.ts +23 -0
  76. package/dist/handlers/emu4-billing-core.js +22 -0
  77. package/dist/handlers/emu4-billing-core.js.map +1 -0
  78. package/dist/handlers/emu4-billing-x.d.ts +2 -0
  79. package/dist/handlers/emu4-billing-x.js +489 -0
  80. package/dist/handlers/emu4-billing-x.js.map +1 -0
  81. package/dist/handlers/emu4-billing.d.ts +14 -0
  82. package/dist/handlers/emu4-billing.js +846 -0
  83. package/dist/handlers/emu4-billing.js.map +1 -0
  84. package/dist/handlers/emu4-clones.d.ts +2 -0
  85. package/dist/handlers/emu4-clones.js +322 -0
  86. package/dist/handlers/emu4-clones.js.map +1 -0
  87. package/dist/handlers/emu4-compliance.d.ts +8 -0
  88. package/dist/handlers/emu4-compliance.js +39 -0
  89. package/dist/handlers/emu4-compliance.js.map +1 -0
  90. package/dist/handlers/emu4-config.d.ts +14 -0
  91. package/dist/handlers/emu4-config.js +148 -0
  92. package/dist/handlers/emu4-config.js.map +1 -0
  93. package/dist/handlers/emu4-cookies.d.ts +8 -0
  94. package/dist/handlers/emu4-cookies.js +248 -0
  95. package/dist/handlers/emu4-cookies.js.map +1 -0
  96. package/dist/handlers/emu4-erasure.d.ts +2 -0
  97. package/dist/handlers/emu4-erasure.js +587 -0
  98. package/dist/handlers/emu4-erasure.js.map +1 -0
  99. package/dist/handlers/emu4-federation.d.ts +8 -0
  100. package/dist/handlers/emu4-federation.js +18 -0
  101. package/dist/handlers/emu4-federation.js.map +1 -0
  102. package/dist/handlers/emu4-journey-math.d.ts +42 -0
  103. package/dist/handlers/emu4-journey-math.js +99 -0
  104. package/dist/handlers/emu4-journey-math.js.map +1 -0
  105. package/dist/handlers/emu4-messaging.d.ts +21 -0
  106. package/dist/handlers/emu4-messaging.js +595 -0
  107. package/dist/handlers/emu4-messaging.js.map +1 -0
  108. package/dist/handlers/emu4-minors.d.ts +7 -0
  109. package/dist/handlers/emu4-minors.js +408 -0
  110. package/dist/handlers/emu4-minors.js.map +1 -0
  111. package/dist/handlers/emu4-oauth-audit.d.ts +5 -0
  112. package/dist/handlers/emu4-oauth-audit.js +17 -0
  113. package/dist/handlers/emu4-oauth-audit.js.map +1 -0
  114. package/dist/handlers/emu4-oauth-auth.d.ts +83 -0
  115. package/dist/handlers/emu4-oauth-auth.js +363 -0
  116. package/dist/handlers/emu4-oauth-auth.js.map +1 -0
  117. package/dist/handlers/emu4-oauth-delivery.d.ts +68 -0
  118. package/dist/handlers/emu4-oauth-delivery.js +207 -0
  119. package/dist/handlers/emu4-oauth-delivery.js.map +1 -0
  120. package/dist/handlers/emu4-oauth-keys.d.ts +11 -0
  121. package/dist/handlers/emu4-oauth-keys.js +268 -0
  122. package/dist/handlers/emu4-oauth-keys.js.map +1 -0
  123. package/dist/handlers/emu4-oauth-pub.d.ts +36 -0
  124. package/dist/handlers/emu4-oauth-pub.js +420 -0
  125. package/dist/handlers/emu4-oauth-pub.js.map +1 -0
  126. package/dist/handlers/emu4-oauth.d.ts +10 -0
  127. package/dist/handlers/emu4-oauth.js +23 -0
  128. package/dist/handlers/emu4-oauth.js.map +1 -0
  129. package/dist/handlers/emu4-policies.d.ts +2 -0
  130. package/dist/handlers/emu4-policies.js +408 -0
  131. package/dist/handlers/emu4-policies.js.map +1 -0
  132. package/dist/handlers/emu4-region.d.ts +2 -0
  133. package/dist/handlers/emu4-region.js +189 -0
  134. package/dist/handlers/emu4-region.js.map +1 -0
  135. package/dist/handlers/emu4-saml.d.ts +12 -0
  136. package/dist/handlers/emu4-saml.js +532 -0
  137. package/dist/handlers/emu4-saml.js.map +1 -0
  138. package/dist/handlers/emu4-shared.d.ts +44 -0
  139. package/dist/handlers/emu4-shared.js +90 -0
  140. package/dist/handlers/emu4-shared.js.map +1 -0
  141. package/dist/handlers/emu4-snapshots.d.ts +38 -0
  142. package/dist/handlers/emu4-snapshots.js +231 -0
  143. package/dist/handlers/emu4-snapshots.js.map +1 -0
  144. package/dist/handlers/emu4-sns.d.ts +9 -0
  145. package/dist/handlers/emu4-sns.js +21 -0
  146. package/dist/handlers/emu4-sns.js.map +1 -0
  147. package/dist/handlers/emu4-trust-binding.d.ts +27 -0
  148. package/dist/handlers/emu4-trust-binding.js +344 -0
  149. package/dist/handlers/emu4-trust-binding.js.map +1 -0
  150. package/dist/handlers/emu4-trust.d.ts +35 -0
  151. package/dist/handlers/emu4-trust.js +543 -0
  152. package/dist/handlers/emu4-trust.js.map +1 -0
  153. package/dist/handlers/fga-engine.d.ts +78 -0
  154. package/dist/handlers/fga-engine.js +663 -0
  155. package/dist/handlers/fga-engine.js.map +1 -0
  156. package/dist/handlers/hooks.js +13 -0
  157. package/dist/handlers/hooks.js.map +1 -1
  158. package/dist/handlers/index.js +14 -1
  159. package/dist/handlers/index.js.map +1 -1
  160. package/dist/handlers/machines.js +6 -0
  161. package/dist/handlers/machines.js.map +1 -1
  162. package/dist/handlers/messaging.d.ts +3 -1
  163. package/dist/handlers/messaging.js +455 -48
  164. package/dist/handlers/messaging.js.map +1 -1
  165. package/dist/handlers/oauth-adv.d.ts +191 -0
  166. package/dist/handlers/oauth-adv.js +1305 -0
  167. package/dist/handlers/oauth-adv.js.map +1 -0
  168. package/dist/handlers/oauth-flows.d.ts +25 -1
  169. package/dist/handlers/oauth-flows.js +111 -50
  170. package/dist/handlers/oauth-flows.js.map +1 -1
  171. package/dist/handlers/oauth.d.ts +24 -3
  172. package/dist/handlers/oauth.js +245 -60
  173. package/dist/handlers/oauth.js.map +1 -1
  174. package/dist/handlers/orgs.js +6 -0
  175. package/dist/handlers/orgs.js.map +1 -1
  176. package/dist/handlers/platform.d.ts +5 -0
  177. package/dist/handlers/platform.js +33 -15
  178. package/dist/handlers/platform.js.map +1 -1
  179. package/dist/handlers/scim.d.ts +2 -0
  180. package/dist/handlers/scim.js +198 -11
  181. package/dist/handlers/scim.js.map +1 -1
  182. package/dist/handlers/sessions.d.ts +2 -0
  183. package/dist/handlers/sessions.js +24 -16
  184. package/dist/handlers/sessions.js.map +1 -1
  185. package/dist/handlers/users.js +3 -0
  186. package/dist/handlers/users.js.map +1 -1
  187. package/dist/jwe.d.ts +18 -0
  188. package/dist/jwe.js +101 -0
  189. package/dist/jwe.js.map +1 -0
  190. package/dist/jws.js +2 -0
  191. package/dist/jws.js.map +1 -1
  192. package/dist/messaging-callbacks.d.ts +100 -0
  193. package/dist/messaging-callbacks.js +767 -0
  194. package/dist/messaging-callbacks.js.map +1 -0
  195. package/dist/openapi.js +41 -1
  196. package/dist/openapi.js.map +1 -1
  197. package/dist/parity-emu3-authz.d.ts +3 -0
  198. package/dist/parity-emu3-authz.js +61 -0
  199. package/dist/parity-emu3-authz.js.map +1 -0
  200. package/dist/parity-emu3-billing.d.ts +3 -0
  201. package/dist/parity-emu3-billing.js +25 -0
  202. package/dist/parity-emu3-billing.js.map +1 -0
  203. package/dist/parity-emu3-messaging.d.ts +3 -0
  204. package/dist/parity-emu3-messaging.js +27 -0
  205. package/dist/parity-emu3-messaging.js.map +1 -0
  206. package/dist/parity-emu3-misc.d.ts +2 -0
  207. package/dist/parity-emu3-misc.js +63 -0
  208. package/dist/parity-emu3-misc.js.map +1 -0
  209. package/dist/parity-emu3-oauth.d.ts +2 -0
  210. package/dist/parity-emu3-oauth.js +39 -0
  211. package/dist/parity-emu3-oauth.js.map +1 -0
  212. package/dist/parity-emu4-actions.d.ts +2 -0
  213. package/dist/parity-emu4-actions.js +38 -0
  214. package/dist/parity-emu4-actions.js.map +1 -0
  215. package/dist/parity-emu4-authz.d.ts +3 -0
  216. package/dist/parity-emu4-authz.js +68 -0
  217. package/dist/parity-emu4-authz.js.map +1 -0
  218. package/dist/parity-emu4-billing.d.ts +6 -0
  219. package/dist/parity-emu4-billing.js +44 -0
  220. package/dist/parity-emu4-billing.js.map +1 -0
  221. package/dist/parity-emu4-compliance.d.ts +2 -0
  222. package/dist/parity-emu4-compliance.js +92 -0
  223. package/dist/parity-emu4-compliance.js.map +1 -0
  224. package/dist/parity-emu4-federation.d.ts +2 -0
  225. package/dist/parity-emu4-federation.js +33 -0
  226. package/dist/parity-emu4-federation.js.map +1 -0
  227. package/dist/parity-emu4-messaging.d.ts +3 -0
  228. package/dist/parity-emu4-messaging.js +42 -0
  229. package/dist/parity-emu4-messaging.js.map +1 -0
  230. package/dist/parity-emu4-oauth.d.ts +2 -0
  231. package/dist/parity-emu4-oauth.js +55 -0
  232. package/dist/parity-emu4-oauth.js.map +1 -0
  233. package/dist/parity-emu4-trust.d.ts +2 -0
  234. package/dist/parity-emu4-trust.js +41 -0
  235. package/dist/parity-emu4-trust.js.map +1 -0
  236. package/dist/parity-routes.js +19 -1
  237. package/dist/parity-routes.js.map +1 -1
  238. package/dist/sns-cert.d.ts +9 -0
  239. package/dist/sns-cert.js +101 -0
  240. package/dist/sns-cert.js.map +1 -0
  241. package/dist/tokens.d.ts +69 -4
  242. package/dist/tokens.js +164 -38
  243. package/dist/tokens.js.map +1 -1
  244. package/package.json +2 -2
@@ -0,0 +1,1056 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.erasureHooks = exports.dsrRows = exports.EXPORT_COOLDOWN_MS = exports.ACTION_TRIGGERS = void 0;
4
+ exports.runActionTrigger = runActionTrigger;
5
+ exports.machineRateLimitClaim = machineRateLimitClaim;
6
+ exports.enforceOrgSessionCap = enforceOrgSessionCap;
7
+ exports.view = view;
8
+ exports.exportBundle = exportBundle;
9
+ exports.emu3MiscHandlers = emu3MiscHandlers;
10
+ const errors_1 = require("../errors");
11
+ const store_1 = require("../store");
12
+ const support_1 = require("./support");
13
+ const util_1 = require("./util");
14
+ const round2_1 = require("./round2");
15
+ const emu4_actions_sandbox_1 = require("./emu4-actions-sandbox");
16
+ /**
17
+ * Round-three odds and ends that share state: Action bindings and the `pre_token` trigger applied to machine tokens, the
18
+ * machine-token `rl` (rate limit) claim, data-subject requests (create, poll, fulfil, reject, cancel, extend, signed deletion
19
+ * receipt) with the per-user data export, and the organization policy with its per-member session cap.
20
+ */
21
+ const bad = (message, param) => new errors_1.EmulatorError('VALIDATION_FAILED', message, param);
22
+ const DAY = 86_400_000;
23
+ // ── Actions: bindings and the pre_token trigger ──────────────────────
24
+ exports.ACTION_TRIGGERS = ['post_login', 'pre_user_registration', 'post_user_registration', 'pre_organization_create', 'pre_member_add', 'pre_invitation_create', 'pre_authentication', 'pre_token', 'post_token_exchange', 'custom_token_exchange', 'pre_user_update', 'pre_user_deletion', 'mfa_verification_attempt', 'password_verification_attempt', 'account_link_decision', 'billing_plan_selected', 'pre_message_send', 'post_change_password', 'post_password_reset', 'post_user_update', 'post_email_verified', 'post_mfa_enroll', 'post_mfa_remove', 'post_logout', 'org_membership_changed', 'custom_challenge_create', 'custom_challenge_verify', 'workflow_step'];
25
+ const MAX_ACTIONS_PER_FLOW = 20;
26
+ const MAX_ACTION_CLAIMS_BYTES = 2048;
27
+ const bindingsOf = (0, support_1.engineState)(() => new Map());
28
+ /** Claims Atlas owns: an Action can never set them (the real list, plus the grant claims a machine token carries). */
29
+ const RESERVED = new Set(['iss', 'sub', 'aud', 'exp', 'nbf', 'iat', 'jti', 'sid', 'azp', 'sv', 'mfa', 'mfa_satisfied', 'org_mfa_required', 'org_mfa_ok', 'last_active_at', 'anon', 'token_use', 'act', 'auth_time', 'amr', 'acr', 'email_verified', 'org', 'orgs', 'team_id', 'eu_orgs', 'groups', 'sso_connection_id', 'sso_organization_id', 'sso_connection_type', 'sso_tenant_id', 'sso_issuer', 'sso_domain', 'pla', 'fea', 'ent', 'referral_code', 'pol', 'risk', 'scope', 'grant_id', 'grant_type', 'grants', 'constraints', 'cnf', 'rl', '__proto__', 'prototype', 'constructor']);
30
+ const reserved = (k) => RESERVED.has(k) || k.startsWith('org_') || k.startsWith('app_');
31
+ /** The size cap the real service puts on an Action's claims: reserved names are dropped, then the largest claims until it fits. */
32
+ function capClaims(claims) {
33
+ const kept = {};
34
+ for (const [k, v] of Object.entries(claims))
35
+ if (!reserved(k))
36
+ kept[k] = v;
37
+ const size = (v) => new TextEncoder().encode(JSON.stringify(v) ?? '').length;
38
+ if (size(kept) <= MAX_ACTION_CLAIMS_BYTES)
39
+ return kept;
40
+ for (const [k] of Object.entries(claims).sort((a, b) => size(b[1]) - size(a[1]))) {
41
+ delete kept[k];
42
+ if (size(kept) <= MAX_ACTION_CLAIMS_BYTES)
43
+ break;
44
+ }
45
+ return kept;
46
+ }
47
+ /** Top-level statements of a script: text outside every bracket, split at `;` and newlines. */
48
+ function statementsOf(code) {
49
+ const src = code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
50
+ const out = [];
51
+ let cur = '';
52
+ let depth = 0;
53
+ let quote = '';
54
+ for (let i = 0; i < src.length; i++) {
55
+ const ch = src[i];
56
+ if (quote) {
57
+ cur += ch;
58
+ if (ch === '\\')
59
+ cur += src[++i] ?? '';
60
+ else if (ch === quote)
61
+ quote = '';
62
+ continue;
63
+ }
64
+ if (ch === "'" || ch === '"' || ch === '`')
65
+ quote = ch;
66
+ else if ('([{'.includes(ch))
67
+ depth++;
68
+ else if (')]}'.includes(ch))
69
+ depth--;
70
+ if ((ch === ';' || ch === '\n') && depth === 0) {
71
+ if (cur.trim())
72
+ out.push(cur.trim());
73
+ cur = '';
74
+ }
75
+ else
76
+ cur += ch;
77
+ }
78
+ if (cur.trim())
79
+ out.push(cur.trim());
80
+ return out;
81
+ }
82
+ function splitArgs(s) {
83
+ const out = [];
84
+ let cur = '';
85
+ let depth = 0;
86
+ let quote = '';
87
+ for (let i = 0; i < s.length; i++) {
88
+ const ch = s[i];
89
+ if (quote) {
90
+ cur += ch;
91
+ if (ch === '\\')
92
+ cur += s[++i] ?? '';
93
+ else if (ch === quote)
94
+ quote = '';
95
+ continue;
96
+ }
97
+ if (ch === "'" || ch === '"' || ch === '`')
98
+ quote = ch;
99
+ else if ('([{'.includes(ch))
100
+ depth++;
101
+ else if (')]}'.includes(ch))
102
+ depth--;
103
+ if (ch === ',' && depth === 0) {
104
+ out.push(cur.trim());
105
+ cur = '';
106
+ }
107
+ else
108
+ cur += ch;
109
+ }
110
+ if (cur.trim())
111
+ out.push(cur.trim());
112
+ return out;
113
+ }
114
+ /** A literal (string, number, boolean, null, JSON) or an `event.context.*` / `event.user.*` read; anything else is unsupported. */
115
+ function evalArg(src, event) {
116
+ const s = src.trim();
117
+ const str = /^(['"`])([\s\S]*)\1$/.exec(s);
118
+ if (str) {
119
+ if (str[1] === '`' && str[2].includes('${'))
120
+ throw new Error('template literal');
121
+ return str[2].replace(/\\(['"`\\])/g, '$1').replace(/\\n/g, '\n');
122
+ }
123
+ if (/^-?\d+(\.\d+)?$/.test(s))
124
+ return Number(s);
125
+ if (s === 'true' || s === 'false')
126
+ return s === 'true';
127
+ if (s === 'null' || s === 'undefined')
128
+ return null;
129
+ const ref = /^event((?:\.[A-Za-z_$][\w$]*)+)$/.exec(s);
130
+ if (ref)
131
+ return ref[1].slice(1).split('.').reduce((o, k) => (o == null ? undefined : o[k]), event) ?? null;
132
+ return JSON.parse(s.replace(/'/g, '"'));
133
+ }
134
+ /** Triggers that run after the change happened: an error is logged and the flow carries on, and a deny means nothing (the real list). */
135
+ const SIDE_EFFECT_TRIGGERS = new Set(['post_user_registration', 'post_change_password', 'post_password_reset', 'post_user_update', 'post_email_verified', 'post_mfa_enroll', 'post_mfa_remove', 'post_logout', 'org_membership_changed']);
136
+ /** Wall-clock budget of one flow (every bound Action of a trigger), as in the real service. */
137
+ const FLOW_BUDGET_MS = 10_000;
138
+ /**
139
+ * Run the Actions bound to a trigger. Code in the runtime's form runs in the sandbox (emu4-actions-sandbox: the real `api`
140
+ * object, no host globals, CPU timeout); code written as a handler module, or any code when node:vm is unavailable, is read
141
+ * declaratively: only top-level `api.accessToken.setCustomClaim`, `suppressClaim` and `api.access.deny` statements with literal
142
+ * arguments or `event.*` reads, and a statement it cannot read fails the run. Every gate trigger fails closed (a deny or a failed
143
+ * run refuses with 403 ACTION_BLOCKED and stops the rest); side-effect triggers fail open and ignore a deny. Actions of runtime
144
+ * `http` are not called.
145
+ */
146
+ function runActionTrigger(e, trigger, context = {}) {
147
+ const ids = bindingsOf(e).get(trigger) ?? [];
148
+ const event = { user: { id: context.userId ?? null, email: null, emailVerified: false, metadata: {} }, connection: { strategy: null }, request: { ip: null, userAgent: null, country: null }, context: { trigger, ...context }, secrets: {} };
149
+ const claims = {};
150
+ const out = { claims: {}, suppressed: [], idClaims: {}, suppressedId: [], appMetadata: {}, sessionData: {}, lockReason: null, pauseReason: null, userEdits: {}, httpRequests: [] };
151
+ const failClosed = !SIDE_EFFECT_TRIGGERS.has(trigger);
152
+ const blocked = (message) => new errors_1.EmulatorError('ACTION_BLOCKED', message, undefined, { meta: { trigger } });
153
+ const started = Date.now();
154
+ for (const id of ids) {
155
+ const a = e.generic.get('actions')?.get(id);
156
+ if (!a || a.enabled === false || a.trigger !== trigger || a.runtime === 'http')
157
+ continue;
158
+ if (Date.now() - started >= FLOW_BUDGET_MS) {
159
+ if (failClosed)
160
+ throw blocked('Sign-in extensions exceeded their time budget and were blocked for safety.');
161
+ break;
162
+ }
163
+ const t0 = Date.now();
164
+ const code = String(a.code ?? '');
165
+ const run = (0, emu4_actions_sandbox_1.runActionCode)(event, code);
166
+ if (run) {
167
+ const outcome = run.error ? 'error' : run.denied && failClosed ? 'denied' : 'ok';
168
+ (0, emu4_actions_sandbox_1.actionRuns)(e).insert({ action_id: id, trigger, outcome, error: run.error, deny_reason: run.denied ? run.denyReason : null, duration_ms: Date.now() - t0, logs: run.logs, http_requests: run.httpRequests, connector_calls: run.connectorCalls, ran_at: e.clock.now() });
169
+ if (run.error) {
170
+ if (failClosed)
171
+ throw blocked('A sign-in extension failed to run and was blocked for safety.');
172
+ continue;
173
+ }
174
+ Object.assign(claims, run.accessTokenClaims);
175
+ Object.assign(out.idClaims, run.idTokenClaims);
176
+ Object.assign(out.appMetadata, run.appMetadata);
177
+ Object.assign(out.sessionData, run.sessionData);
178
+ Object.assign(out.userEdits, run.userEdits);
179
+ out.lockReason = run.lockReason ?? out.lockReason;
180
+ out.pauseReason = out.pauseReason ?? run.pauseReason;
181
+ out.suppressed.push(...run.suppressedAccessClaims);
182
+ out.suppressedId.push(...run.suppressedIdClaims);
183
+ out.httpRequests.push(...run.httpRequests);
184
+ if (run.denied && failClosed)
185
+ throw blocked(run.denyReason ?? 'Access denied.');
186
+ continue;
187
+ }
188
+ let denied = null;
189
+ try {
190
+ for (const st of statementsOf(code)) {
191
+ const m = /^api\.(accessToken|access)\.(setCustomClaim|suppressClaim|deny)\(([\s\S]*)\)$/.exec(st);
192
+ if (!m)
193
+ continue;
194
+ const args = splitArgs(m[3]).map((x) => evalArg(x, event));
195
+ if (m[1] === 'accessToken' && m[2] === 'setCustomClaim')
196
+ claims[String(args[0]).slice(0, 256)] = args[1];
197
+ else if (m[1] === 'accessToken' && m[2] === 'suppressClaim' && out.suppressed.length < 50)
198
+ out.suppressed.push(String(args[0]).slice(0, 256));
199
+ else if (m[1] === 'access' && m[2] === 'deny') {
200
+ denied = (args[0] == null ? 'Access denied.' : String(args[0])).slice(0, 512);
201
+ break;
202
+ }
203
+ }
204
+ }
205
+ catch {
206
+ (0, emu4_actions_sandbox_1.actionRuns)(e).insert({ action_id: id, trigger, outcome: 'error', error: 'unreadable statement', deny_reason: null, duration_ms: Date.now() - t0, logs: [], http_requests: [], connector_calls: [], ran_at: e.clock.now() });
207
+ if (failClosed)
208
+ throw blocked('A sign-in extension failed to run and was blocked for safety.');
209
+ continue;
210
+ }
211
+ (0, emu4_actions_sandbox_1.actionRuns)(e).insert({ action_id: id, trigger, outcome: denied !== null && failClosed ? 'denied' : 'ok', error: null, deny_reason: denied, duration_ms: Date.now() - t0, logs: [], http_requests: [], connector_calls: [], ran_at: e.clock.now() });
212
+ if (denied !== null && failClosed)
213
+ throw blocked(denied);
214
+ }
215
+ out.claims = capClaims(claims);
216
+ return out;
217
+ }
218
+ // ── rate-limit policies, overrides and the machine token's rl claim ──
219
+ const RL_SUBJECTS = ['key', 'user', 'org', 'machine', 'ip'];
220
+ const RL_MAX_LIMIT = 10_000_000;
221
+ const RL_MAX_POLICIES = 100;
222
+ const RL_MAX_PLANS = 50;
223
+ const SLUG = /^[a-z0-9][a-z0-9_.-]{0,63}$/;
224
+ const PLAN_SLUG = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
225
+ const rl = (0, support_1.engineState)((e) => ({ policies: new store_1.Table('rate_limit_policies', e.clock.now, 'rlp'), overrides: new store_1.Table('rate_limit_overrides', e.clock.now, 'rlo') }));
226
+ const rlInt = (v, param, min, max) => {
227
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < min || v > max)
228
+ throw bad(`${param} must be an integer from ${min} to ${max}.`, param);
229
+ return v;
230
+ };
231
+ const rlBurst = (v) => (v === null ? null : rlInt(v, 'burst', 0, RL_MAX_LIMIT));
232
+ const rlBool = (v, param) => {
233
+ if (typeof v !== 'boolean')
234
+ throw bad(`${param} must be true or false.`, param);
235
+ return v;
236
+ };
237
+ const rlSubject = (v, param = 'subject') => {
238
+ if (typeof v !== 'string' || !RL_SUBJECTS.includes(v))
239
+ throw bad(`${param} must be one of ${RL_SUBJECTS.join(', ')}.`, param);
240
+ return v;
241
+ };
242
+ function planOverridesOf(raw) {
243
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
244
+ throw bad('plan_overrides must be an object keyed by billing plan slug.', 'plan_overrides');
245
+ const entries = Object.entries(raw);
246
+ if (entries.length > RL_MAX_PLANS)
247
+ throw bad(`plan_overrides holds at most ${RL_MAX_PLANS} plans.`, 'plan_overrides');
248
+ const out = {};
249
+ for (const [slug, v] of entries) {
250
+ const at = `plan_overrides.${slug}`;
251
+ if (!PLAN_SLUG.test(slug))
252
+ throw bad('A plan slug is 1-64 letters, digits, dot, dash, underscore or colon.', at);
253
+ if (!v || typeof v !== 'object' || Array.isArray(v))
254
+ throw bad('Each plan entry is an object with a limit.', at);
255
+ const unknown = Object.keys(v).find((k) => !['limit', 'window_seconds', 'burst'].includes(k));
256
+ if (unknown)
257
+ throw bad(`Unknown field ${unknown}; a plan entry takes limit, window_seconds and burst.`, `${at}.${unknown}`);
258
+ out[slug] = { limit: rlInt(v.limit, `${at}.limit`, 1, RL_MAX_LIMIT), ...(v.window_seconds !== undefined ? { window_seconds: rlInt(v.window_seconds, `${at}.window_seconds`, 1, 86_400) } : {}), ...(v.burst !== undefined ? { burst: rlBurst(v.burst) } : {}) };
259
+ }
260
+ return out;
261
+ }
262
+ const policyView = (p) => ({ object: 'api_rate_limit_policy', id: p.id, name: p.name, subject: p.subject, limit: p.limit, window_seconds: p.window_seconds, burst: p.burst, plan_overrides: p.plan_overrides, is_default: p.is_default, enabled: p.enabled, version: p.version, created_at: p.created_at, updated_at: p.updated_at });
263
+ const overrideView = (o) => ({ object: 'api_rate_limit_override', id: o.id, subject_type: o.subject_type, subject_id: o.subject_id, policy_id: o.policy_id, limit: o.limit, window_seconds: o.window_seconds, burst: o.burst, reason: o.reason, expires_at: o.expires_at, created_at: o.created_at, updated_at: o.updated_at });
264
+ const rlChanged = (e, policy, previous, current) => void e.webhooks.emit('rate_limit_policy.updated', { object: 'rate_limit_policy', policy, previous, current, at: e.clock.now() });
265
+ const h16 = (s) => {
266
+ let a = 0x811c9dc5;
267
+ let b = 0x9e3779b9;
268
+ for (const ch of s) {
269
+ a = Math.imul(a ^ ch.charCodeAt(0), 16777619) >>> 0;
270
+ b = Math.imul(b + ch.charCodeAt(0), 2654435761) >>> 0;
271
+ }
272
+ return a.toString(16).padStart(8, '0') + b.toString(16).padStart(8, '0');
273
+ };
274
+ /**
275
+ * The limit that applies to one subject: its live override (a policy it points at, which must be enabled, or its own numbers), else
276
+ * the enabled default policy for the subject kind. shortcut: the per-plan entries of a policy and a key's own `rate_limit` are not
277
+ * applied (no billing plan or key limit is resolved here), so `plan` is always null and a key resolves by override or default only.
278
+ */
279
+ function resolveRateLimit(e, subject, id) {
280
+ const { policies, overrides } = rl(e);
281
+ const now = e.clock.now();
282
+ const make = (source, limit, window, burst, p, extra) => ({ limit, window_seconds: window, burst, source, policy_id: p?.id ?? null, policy_name: p?.name ?? null, plan: null, version: h16(JSON.stringify([source, limit, window, burst, p?.id ?? null, p?.version ?? null, null, ...extra])) });
283
+ const o = overrides.find((x) => x.subject_type === subject && x.subject_id === id);
284
+ if (o && !(o.expires_at && o.expires_at <= now)) {
285
+ if (o.policy_id) {
286
+ const p = policies.get(o.policy_id);
287
+ if (p && p.enabled)
288
+ return make('override', p.limit, p.window_seconds, o.burst ?? p.burst, p, [o.updated_at]);
289
+ }
290
+ else if (o.limit !== null && o.window_seconds !== null)
291
+ return make('override', o.limit, o.window_seconds, o.burst, null, [o.updated_at]);
292
+ }
293
+ const d = policies.find((p) => p.subject === subject && p.is_default && p.enabled);
294
+ return d ? make('default', d.limit, d.window_seconds, d.burst, d, []) : null;
295
+ }
296
+ /** The compact `rl` claim of a machine token, `{ limit, window, burst?, v }`; null when no policy or override covers the machine. */
297
+ function machineRateLimitClaim(e, machineId) {
298
+ const r = resolveRateLimit(e, 'machine', machineId);
299
+ return r ? { limit: r.limit, window: r.window_seconds, ...(r.burst !== null ? { burst: r.burst } : {}), v: r.version } : null;
300
+ }
301
+ function rateLimitHandlers() {
302
+ const READ = 'rate_limit:read';
303
+ const WRITE = 'rate_limit:write';
304
+ const policyOf = (e, id) => {
305
+ const p = rl(e).policies.get(id);
306
+ if (!p)
307
+ throw (0, errors_1.notFound)('Rate-limit policy');
308
+ return p;
309
+ };
310
+ const refOf = (c) => {
311
+ const type = rlSubject(c.params.subject_type, 'subject_type');
312
+ const id = c.params.subject_id ?? '';
313
+ if (id.length === 0 || id.length > 200)
314
+ throw bad('subject_id must be a non-empty string of at most 200 characters.', 'subject_id');
315
+ return { type, id: type === 'ip' ? id.toLowerCase() : id };
316
+ };
317
+ const conflict = (message, param) => new errors_1.EmulatorError('CONFLICT', message, param);
318
+ const subjectExists = (e, type, id) => {
319
+ if (type === 'ip') {
320
+ if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(id) && !/^[0-9a-f:]+$/.test(id.replace(/\.\d+/g, '')) && !id.includes(':'))
321
+ throw bad('An ip subject must be one IPv4 or IPv6 address.', 'subject_id');
322
+ return;
323
+ }
324
+ const found = type === 'key' ? e.db.apiKeys.get(id) : type === 'user' ? e.db.users.get(id) : type === 'org' ? e.db.organizations.get(id) : e.db.machines.get(id);
325
+ if (!found || found.deleted)
326
+ throw (0, errors_1.notFound)(type === 'key' ? 'Key' : type === 'org' ? 'Organization' : type === 'user' ? 'User' : 'Machine');
327
+ };
328
+ return [
329
+ {
330
+ method: 'GET',
331
+ pattern: '/v1/rate_limit_policies',
332
+ scope: READ,
333
+ handler: (c) => {
334
+ const subject = c.query.get('subject');
335
+ if (subject !== null)
336
+ rlSubject(subject);
337
+ return { body: { object: 'list', data: rl(c.engine).policies.filter((p) => !subject || p.subject === subject).map(policyView), has_more: false } };
338
+ },
339
+ },
340
+ {
341
+ method: 'POST',
342
+ pattern: '/v1/rate_limit_policies',
343
+ scope: WRITE,
344
+ handler: (c) => {
345
+ const e = c.engine;
346
+ const b = c.body;
347
+ const name = typeof b.name === 'string' && SLUG.test(b.name) ? b.name : null;
348
+ if (name === null)
349
+ throw bad('name is 1-64 characters: lowercase letters, digits, dot, dash or underscore, starting with a letter or digit.', 'name');
350
+ const subject = rlSubject(b.subject);
351
+ const limit = rlInt(b.limit, 'limit', 1, RL_MAX_LIMIT);
352
+ const window = rlInt(b.window_seconds, 'window_seconds', 1, 86_400);
353
+ const burst = b.burst === undefined ? null : rlBurst(b.burst);
354
+ const planOverrides = b.plan_overrides === undefined ? {} : planOverridesOf(b.plan_overrides);
355
+ const isDefault = b.is_default === undefined ? false : rlBool(b.is_default, 'is_default');
356
+ const enabled = b.enabled === undefined ? true : rlBool(b.enabled, 'enabled');
357
+ const { policies } = rl(e);
358
+ if (policies.size >= RL_MAX_POLICIES)
359
+ throw bad(`An instance holds at most ${RL_MAX_POLICIES} rate-limit policies.`, 'name');
360
+ if (policies.find((p) => p.name === name))
361
+ throw conflict(`A rate-limit policy named ${name} already exists.`, 'name');
362
+ if (isDefault && policies.find((p) => p.subject === subject && p.is_default))
363
+ throw conflict(`A default policy for ${subject} already exists. Unset it first, or create this one without is_default.`, 'is_default');
364
+ const now = e.clock.now();
365
+ const row = policies.insert({ name, subject, limit, window_seconds: window, burst, plan_overrides: planOverrides, is_default: isDefault, enabled, version: 1, created_at: now, updated_at: now });
366
+ rlChanged(e, row.id, null, policyView(row));
367
+ return { status: 201, body: policyView(row) };
368
+ },
369
+ },
370
+ {
371
+ method: 'GET',
372
+ pattern: '/v1/rate_limit_policies/resolve',
373
+ scope: READ,
374
+ handler: (c) => {
375
+ const e = c.engine;
376
+ const given = ['key_id', 'user_id', 'org_id', 'machine_id', 'ip'].filter((k) => c.query.get(k) !== null && c.query.get(k) !== '');
377
+ if (given.length !== 1)
378
+ throw bad('Send exactly one of key_id, user_id, org_id, machine_id or ip.', 'key_id');
379
+ const which = given[0];
380
+ const subject = which === 'key_id' ? 'key' : which === 'user_id' ? 'user' : which === 'org_id' ? 'org' : which === 'machine_id' ? 'machine' : 'ip';
381
+ const id = c.query.get(which);
382
+ if (subject === 'key' && !e.db.apiKeys.get(id))
383
+ throw (0, errors_1.notFound)('Key');
384
+ return { body: { object: 'api_rate_limit_resolution', subject_type: subject, subject_id: id, rate_limit: resolveRateLimit(e, subject, subject === 'ip' ? id.toLowerCase() : id) } };
385
+ },
386
+ },
387
+ { method: 'GET', pattern: '/v1/rate_limit_policies/:id', scope: READ, handler: (c) => ({ body: policyView(policyOf(c.engine, c.params.id)) }) },
388
+ {
389
+ method: 'PATCH',
390
+ pattern: '/v1/rate_limit_policies/:id',
391
+ scope: WRITE,
392
+ handler: (c) => {
393
+ const e = c.engine;
394
+ const p = policyOf(e, c.params.id);
395
+ const b = c.body;
396
+ if (b.subject !== undefined && rlSubject(b.subject) !== p.subject)
397
+ throw bad('subject cannot change. Create a new policy for the other subject kind.', 'subject');
398
+ const patch = {};
399
+ if (b.name !== undefined) {
400
+ if (typeof b.name !== 'string' || !SLUG.test(b.name))
401
+ throw bad('name is 1-64 characters: lowercase letters, digits, dot, dash or underscore, starting with a letter or digit.', 'name');
402
+ patch.name = b.name;
403
+ }
404
+ if (b.limit !== undefined)
405
+ patch.limit = rlInt(b.limit, 'limit', 1, RL_MAX_LIMIT);
406
+ if (b.window_seconds !== undefined)
407
+ patch.window_seconds = rlInt(b.window_seconds, 'window_seconds', 1, 86_400);
408
+ if (b.burst !== undefined)
409
+ patch.burst = rlBurst(b.burst);
410
+ if (b.plan_overrides !== undefined)
411
+ patch.plan_overrides = planOverridesOf(b.plan_overrides);
412
+ if (b.is_default !== undefined)
413
+ patch.is_default = rlBool(b.is_default, 'is_default');
414
+ if (b.enabled !== undefined)
415
+ patch.enabled = rlBool(b.enabled, 'enabled');
416
+ const before = policyView(p);
417
+ // The same content again is a no-op, so a retried PATCH never bumps the version.
418
+ if (Object.entries(patch).every(([k, v]) => JSON.stringify(v) === JSON.stringify(p[k])))
419
+ return { body: before };
420
+ const { policies } = rl(e);
421
+ if (patch.name !== undefined && patch.name !== p.name && policies.find((x) => x.name === patch.name))
422
+ throw conflict(`A rate-limit policy named ${patch.name} already exists.`, 'name');
423
+ if (patch.is_default === true && !p.is_default && policies.find((x) => x.subject === p.subject && x.is_default))
424
+ throw conflict('That name, or the default slot for this subject, is already taken.');
425
+ Object.assign(p, patch, { version: p.version + 1, updated_at: e.clock.now() });
426
+ rlChanged(e, p.id, before, policyView(p));
427
+ return { body: policyView(p) };
428
+ },
429
+ },
430
+ {
431
+ method: 'DELETE',
432
+ pattern: '/v1/rate_limit_policies/:id',
433
+ scope: WRITE,
434
+ handler: (c) => {
435
+ const e = c.engine;
436
+ const p = policyOf(e, c.params.id);
437
+ const using = rl(e).overrides.filter((o) => o.policy_id === p.id).length;
438
+ if (using > 0)
439
+ throw conflict(`${using} per-subject override(s) use this policy. Remove or repoint them first.`);
440
+ const before = policyView(p);
441
+ rl(e).policies.delete(p.id);
442
+ rlChanged(e, p.id, before, null);
443
+ return { body: { object: 'api_rate_limit_policy', id: p.id, deleted: true } };
444
+ },
445
+ },
446
+ {
447
+ method: 'GET',
448
+ pattern: '/v1/rate_limit_overrides',
449
+ scope: READ,
450
+ handler: (c) => {
451
+ const type = c.query.get('subject_type');
452
+ if (type !== null)
453
+ rlSubject(type, 'subject_type');
454
+ const limit = c.query.get('limit') === null ? 50 : Number(c.query.get('limit'));
455
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200)
456
+ throw bad('limit must be an integer from 1 to 200.', 'limit');
457
+ const page = (0, store_1.paginate)(rl(c.engine).overrides.filter((o) => !type || o.subject_type === type), { limit, startingAfter: c.query.get('starting_after') ?? undefined });
458
+ return { body: { object: 'list', data: page.data.map(overrideView), has_more: page.has_more, next_cursor: page.next_cursor } };
459
+ },
460
+ },
461
+ {
462
+ method: 'GET',
463
+ pattern: '/v1/rate_limit_overrides/:subject_type/:subject_id',
464
+ scope: READ,
465
+ handler: (c) => {
466
+ const ref = refOf(c);
467
+ const o = rl(c.engine).overrides.find((x) => x.subject_type === ref.type && x.subject_id === ref.id);
468
+ if (!o)
469
+ throw (0, errors_1.notFound)('Override for that subject');
470
+ return { body: overrideView(o) };
471
+ },
472
+ },
473
+ {
474
+ method: 'PUT',
475
+ pattern: '/v1/rate_limit_overrides/:subject_type/:subject_id',
476
+ scope: WRITE,
477
+ handler: (c) => {
478
+ const e = c.engine;
479
+ const ref = refOf(c);
480
+ const b = c.body;
481
+ const known = ['policy_id', 'limit', 'window_seconds', 'burst', 'reason', 'expires_at'];
482
+ const stray = Object.keys(b).find((k) => !known.includes(k));
483
+ if (stray)
484
+ throw bad(`Unknown field ${stray}.`, stray);
485
+ subjectExists(e, ref.type, ref.id);
486
+ const hasPolicy = b.policy_id !== undefined && b.policy_id !== null;
487
+ const hasInline = b.limit !== undefined || b.window_seconds !== undefined || b.burst !== undefined;
488
+ if (hasPolicy === hasInline)
489
+ throw bad('Send policy_id, or limit with window_seconds (and optionally burst), not both and not neither.', 'policy_id');
490
+ let policyId = null;
491
+ if (hasPolicy) {
492
+ if (typeof b.policy_id !== 'string')
493
+ throw bad('policy_id must be a policy id.', 'policy_id');
494
+ const p = rl(e).policies.get(b.policy_id);
495
+ if (!p)
496
+ throw bad('Unknown policy_id.', 'policy_id');
497
+ if (p.subject !== ref.type)
498
+ throw bad(`That policy is for ${p.subject} subjects, not ${ref.type}.`, 'policy_id');
499
+ policyId = p.id;
500
+ }
501
+ let expiresAt = null;
502
+ if (b.expires_at !== undefined && b.expires_at !== null) {
503
+ const t = typeof b.expires_at === 'number' || typeof b.expires_at === 'string' ? new Date(b.expires_at).getTime() : NaN;
504
+ if (Number.isNaN(t) || t <= e.clock.now())
505
+ throw bad('expires_at must be a future time (epoch milliseconds or ISO 8601).', 'expires_at');
506
+ expiresAt = t;
507
+ }
508
+ if (b.reason !== undefined && b.reason !== null && (typeof b.reason !== 'string' || b.reason.length > 500))
509
+ throw bad('reason must be a string of at most 500 characters.', 'reason');
510
+ const next = { policy_id: policyId, limit: hasInline ? rlInt(b.limit, 'limit', 1, RL_MAX_LIMIT) : null, window_seconds: hasInline ? rlInt(b.window_seconds, 'window_seconds', 1, 86_400) : null, burst: hasInline && b.burst !== undefined ? rlBurst(b.burst) : null, reason: b.reason ?? null, expires_at: expiresAt };
511
+ const prior = rl(e).overrides.find((x) => x.subject_type === ref.type && x.subject_id === ref.id);
512
+ const before = prior ? overrideView(prior) : null;
513
+ // The same content again is a no-op, so a retried PUT leaves the resolved version alone.
514
+ if (prior && Object.entries(next).every(([k, v]) => prior[k] === v))
515
+ return { body: before };
516
+ const now = e.clock.now();
517
+ const row = prior ? Object.assign(prior, next, { updated_at: now }) : rl(e).overrides.insert({ subject_type: ref.type, subject_id: ref.id, ...next, created_at: now, updated_at: now });
518
+ rlChanged(e, `override:${ref.type}:${ref.id}`, before, overrideView(row));
519
+ return { body: overrideView(row) };
520
+ },
521
+ },
522
+ {
523
+ method: 'DELETE',
524
+ pattern: '/v1/rate_limit_overrides/:subject_type/:subject_id',
525
+ scope: WRITE,
526
+ handler: (c) => {
527
+ const e = c.engine;
528
+ const ref = refOf(c);
529
+ const o = rl(e).overrides.find((x) => x.subject_type === ref.type && x.subject_id === ref.id);
530
+ if (!o)
531
+ throw (0, errors_1.notFound)('Override for that subject');
532
+ rl(e).overrides.delete(o.id);
533
+ rlChanged(e, `override:${ref.type}:${ref.id}`, overrideView(o), null);
534
+ return { body: { object: 'api_rate_limit_override', id: o.id, deleted: true } };
535
+ },
536
+ },
537
+ ];
538
+ }
539
+ // ── organization policy and the per-member session cap ───────────────
540
+ const MIN_AGE = 300;
541
+ const MAX_AGE = 7_776_000;
542
+ /** After a session is created: past an organization's `maxSessions`, the member's least recently active sessions end. */
543
+ function enforceOrgSessionCap(e, session) {
544
+ let cap;
545
+ for (const m of e.db.memberships.filter((x) => x.user_id === session.user_id)) {
546
+ const n = e.db.organizations.get(m.organization_id)?.policy?.maxSessions;
547
+ if (Number.isInteger(n))
548
+ cap = Math.min(cap ?? Infinity, n);
549
+ }
550
+ if (cap === undefined)
551
+ return;
552
+ const now = e.clock.now();
553
+ const others = e.db.sessions.filter((s) => s.user_id === session.user_id && s.id !== session.id && s.status === 'active' && s.expires_at > now).sort((a, b) => b.last_active_at - a.last_active_at);
554
+ // The new session is never evicted: keep cap - 1 others, end the rest.
555
+ for (const stale of others.slice(cap - 1))
556
+ (0, round2_1.revokeSession)(e, stale, 'org_max_sessions_exceeded');
557
+ }
558
+ function policyProblems(p) {
559
+ const out = [];
560
+ const intOrNull = (k) => p[k] !== undefined && p[k] !== null && (typeof p[k] !== 'number' || !Number.isInteger(p[k]));
561
+ if (intOrNull('maxSessions'))
562
+ out.push({ param: 'maxSessions', message: 'Expected a whole number of sessions.' });
563
+ else if (typeof p.maxSessions === 'number' && (p.maxSessions < 1 || p.maxSessions > 100))
564
+ out.push({ param: 'maxSessions', message: 'maxSessions must be from 1 to 100.' });
565
+ if (intOrNull('maxSessionAgeSeconds'))
566
+ out.push({ param: 'maxSessionAgeSeconds', message: 'Expected a whole number of seconds.' });
567
+ else if (typeof p.maxSessionAgeSeconds === 'number' && p.maxSessionAgeSeconds < MIN_AGE)
568
+ out.push({ param: 'maxSessionAgeSeconds', message: 'A maximum session age under five minutes signs members out almost immediately.' });
569
+ else if (typeof p.maxSessionAgeSeconds === 'number' && p.maxSessionAgeSeconds > MAX_AGE)
570
+ out.push({ param: 'maxSessionAgeSeconds', message: 'A session may not outlive 90 days.' });
571
+ for (const k of ['ssoMultiConnection', 'ssoRequired', 'requireMfa'])
572
+ if (p[k] !== undefined && p[k] !== null && typeof p[k] !== 'boolean')
573
+ out.push({ param: k, message: 'Expected true or false.' });
574
+ for (const k of ['allowedSignInMethods', 'ipAllowlist'])
575
+ if (p[k] !== undefined && p[k] !== null && (!Array.isArray(p[k]) || !p[k].every((x) => typeof x === 'string')))
576
+ out.push({ param: k, message: 'Expected an array of strings.' });
577
+ for (const k of ['entitlementPlan', 'entitlementOverrides'])
578
+ if (p[k] !== undefined && p[k] !== null)
579
+ out.push({ param: k, message: `${k} is not supported on the organization policy. Send null to clear a stored value.` });
580
+ return out;
581
+ }
582
+ // ── data-subject requests and the per-user export ────────────────────
583
+ const TYPES = {
584
+ access: { kind: 'export', canonical: 'access' },
585
+ export: { kind: 'export', canonical: 'access' },
586
+ deletion: { kind: 'erasure', canonical: 'deletion' },
587
+ erasure: { kind: 'erasure', canonical: 'deletion' },
588
+ erase: { kind: 'erasure', canonical: 'deletion' },
589
+ closure: { kind: 'erasure', canonical: 'closure' },
590
+ restriction: { kind: 'restriction', canonical: 'restriction' },
591
+ rectification: { kind: 'rectification', canonical: 'rectification' },
592
+ };
593
+ const TYPE_ERROR = `type must be one of ${[...Object.keys(TYPES)].map((t) => `'${t}'`).join(', ')}.`;
594
+ const ERASURE_GRACE_MS = 7 * DAY;
595
+ const DEFAULT_DUE_MS = 30 * DAY;
596
+ const SCRUB_DAYS = 30;
597
+ exports.EXPORT_COOLDOWN_MS = 60 * 60_000;
598
+ const terminal = (s) => s === 'completed' || s === 'rejected' || s === 'cancelled';
599
+ const st = (0, support_1.engineState)((e) => ({ dsr: new store_1.Table('data_subject_requests', e.clock.now, 'dsr'), receipts: new Map(), audit: [] }));
600
+ /** Round-four compliance: the request table and presenter, and hooks that run before an erasure removes the user. */
601
+ const dsrRows = (e) => st(e).dsr;
602
+ exports.dsrRows = dsrRows;
603
+ exports.erasureHooks = [];
604
+ const note = (e, action, targetType, targetId, actor) => {
605
+ st(e).audit.push({ id: e.db.newId('audit'), action, actor_type: actor[0], actor_id: actor[1], target_type: targetType, target_id: targetId, ip: null, created_at: e.clock.now() });
606
+ };
607
+ const dueAt = (r) => r.due_at ?? (r.canonical_type === 'closure' ? null : r.requested_at + DEFAULT_DUE_MS);
608
+ function view(e, r, includeResult = false) {
609
+ const due = dueAt(r);
610
+ const now = e.clock.now();
611
+ const waiting = r.type === 'erasure' && !!r.scheduled_for && r.scheduled_for > now;
612
+ return {
613
+ object: 'data_subject_request',
614
+ id: r.id,
615
+ type: r.type,
616
+ canonical_type: r.canonical_type,
617
+ canonicalType: r.canonical_type,
618
+ status: r.status,
619
+ user_id: r.user_id,
620
+ source: r.source,
621
+ requested_by_type: r.requested_by_type,
622
+ requested_by_id: r.requested_by_id ?? null,
623
+ reason: r.reason ?? null,
624
+ detail: r.detail ?? null,
625
+ case_id: null,
626
+ external_case: null,
627
+ trace_id: null,
628
+ steps: r.steps ?? [],
629
+ retention: [],
630
+ scheduled_for: r.scheduled_for ?? null,
631
+ eta: r.scheduled_for ?? null,
632
+ deletion_effective_at: r.type === 'erasure' ? (r.scheduled_for ?? null) : null,
633
+ requested_at: r.requested_at,
634
+ updated_at: r.updated_at,
635
+ completed_at: r.completed_at ?? null,
636
+ due_at: due,
637
+ overdue: due !== null && !terminal(r.status) && !waiting && due < now,
638
+ due_extended_reason: r.due_extended_reason ?? null,
639
+ assignee: r.assignee ?? null,
640
+ priority: r.priority ?? 'normal',
641
+ labels: r.labels ?? [],
642
+ ...(includeResult ? { result: r.result ?? null } : {}),
643
+ };
644
+ }
645
+ const clientView = (r, includeResult = false) => ({ object: 'data_subject_request', id: r.id, type: r.type, canonical_type: r.canonical_type, status: r.status, reason: r.reason ?? null, requested_at: r.requested_at, scheduled_for: r.scheduled_for ?? null, completed_at: r.completed_at ?? null, ...(includeResult ? { result: r.result ?? null } : {}), detail: r.detail ?? null });
646
+ /** The Article 15 / 20 package: written field by field, so a secret or private metadata cannot ride along by accident. */
647
+ function exportBundle(e, userId) {
648
+ const u = e.db.users.get(userId);
649
+ if (!u)
650
+ throw (0, errors_1.notFound)('User');
651
+ const mail = u.emails ?? [];
652
+ return {
653
+ object: 'data_export',
654
+ format_version: 1,
655
+ generated_at: e.clock.now(),
656
+ user: { object: 'user', id: u.id, username: u.username ?? null, first_name: u.first_name ?? null, last_name: u.last_name ?? null, image_url: u.image_url ?? null, locale: u.locale ?? null, public_metadata: u.public_metadata ?? {}, unsafe_metadata: u.unsafe_metadata ?? {}, has_password: u.has_password === true, mfa_enabled: false, external_id: u.external_id ?? null, created_at: u.created_at, updated_at: u.updated_at },
657
+ email_addresses: mail.map((m) => ({ object: 'email_address', id: m.id, email_address: m.email_address, verified: m.verified === true, primary: m.id === u.primary_email_address_id, created_at: u.created_at })),
658
+ phone_numbers: [],
659
+ external_accounts: [],
660
+ passkeys: e.db.passkeys.filter((p) => p.user_id === userId).map((p) => ({ object: 'passkey', id: p.id, name: p.name ?? null })),
661
+ mfa_factors: [],
662
+ sessions: e.db.sessions.filter((s) => s.user_id === userId).map((s) => ({ object: 'session', id: s.id, status: s.status, device: null, ip_first: null, ip_last: null, created_at: s.created_at, last_active_at: s.last_active_at, expires_at: s.expires_at, custom_data: null })),
663
+ ens_names: [],
664
+ organization_memberships: e.db.memberships.filter((m) => m.user_id === userId).flatMap((m) => {
665
+ const o = e.db.organizations.get(m.organization_id);
666
+ return o ? [{ object: 'organization_membership', role: m.role, organization: { object: 'organization', id: o.id, name: o.name, slug: o.slug } }] : [];
667
+ }),
668
+ organization_invitations: [],
669
+ audit_log: st(e).audit.filter((a) => a.actor_id === userId || a.target_id === userId).slice(-1000).map((a) => ({ object: 'audit_log_entry', id: a.id, action: a.action, actor_type: a.actor_type, target_type: a.target_type, ip: a.ip, created_at: a.created_at })),
670
+ consents: [],
671
+ // Provider connections the user owns: never a token.
672
+ connections: (e.generic.get('connections')?.all() ?? []).filter((c) => c.owner?.type === 'user' && c.owner?.id === userId).map((c) => ({ object: 'connection', id: c.id, provider: c.provider, external_account_id: c.external_id ?? null, environment: c.environment ?? 'production', status: c.status, scopes: c.scopes ?? [], connected_at: c.connected_at ?? null, last_refreshed_at: c.last_refreshed_at ?? null, created_at: c.created_at })),
673
+ app_roles: [],
674
+ groups: [],
675
+ entitlements: [],
676
+ identity_verifications: [],
677
+ api_keys: [],
678
+ sign_in_history: [],
679
+ billing: [],
680
+ };
681
+ }
682
+ function fulfil(e, r, actor) {
683
+ const now = e.clock.now();
684
+ if (r.type === 'export')
685
+ r.result = exportBundle(e, r.user_id);
686
+ else if (r.type === 'erasure') {
687
+ (0, support_1.assertNotOnHold)(e, r.user_id);
688
+ for (const hook of exports.erasureHooks)
689
+ hook(e, r);
690
+ const u = e.db.users.get(r.user_id);
691
+ if (u && !u.deleted) {
692
+ u.deleted = true;
693
+ e.db.sessions.filter((s) => s.user_id === u.id).forEach((s) => (0, round2_1.revokeSession)(e, s, 'user_deleted'));
694
+ e.changed({ type: 'user.deleted', data: { id: u.id, object: 'user', deleted: true }, resource: 'user', id: u.id, op: 'delete' });
695
+ }
696
+ r.steps = r.steps ?? [];
697
+ r.result = { erased_at: new Date(now).toISOString(), personal_data_scrubbed_at: new Date(now + SCRUB_DAYS * DAY).toISOString() };
698
+ }
699
+ else {
700
+ r.detail = `An operator recorded the ${r.type} as done.`;
701
+ r.result = { completed_by: actor[0], completed_at: new Date(now).toISOString() };
702
+ }
703
+ Object.assign(r, { status: 'completed', completed_at: now, updated_at: now });
704
+ e.webhooks.emit('data_subject_request.fulfilled', { object: 'data_subject_request', id: r.id, user_id: r.user_id, type: r.type, status: r.status, reason: r.reason ?? null });
705
+ note(e, `data_subject_request.${r.type}.fulfilled`, 'data_subject_request', r.id, actor);
706
+ }
707
+ /** The worker pass: an export builds on the next pass, an erasure runs when its cancellation window closes (a held user stays scheduled). */
708
+ function settle(e) {
709
+ const now = e.clock.now();
710
+ for (const r of st(e).dsr.all()) {
711
+ try {
712
+ if (r.type === 'export' && r.status === 'pending')
713
+ fulfil(e, r, ['system', null]);
714
+ else if (r.type === 'erasure' && r.status === 'scheduled' && r.scheduled_for <= now)
715
+ fulfil(e, r, ['system', null]);
716
+ }
717
+ catch {
718
+ /* legal hold: the request waits */
719
+ }
720
+ }
721
+ }
722
+ const forbidden409 = (message) => ({ status: 409, body: (0, errors_1.errorBody)([{ code: 'FORBIDDEN', message }]) });
723
+ const dsrOf = (e, id) => {
724
+ const r = st(e).dsr.get(id);
725
+ if (!r)
726
+ throw (0, errors_1.notFound)('Data subject request');
727
+ return r;
728
+ };
729
+ const subjectOf = (e, body) => {
730
+ const id = typeof body.user_id === 'string' ? body.user_id.trim() : '';
731
+ const email = typeof body.subject_email === 'string' ? body.subject_email.trim().toLowerCase() : '';
732
+ if (!id && !email)
733
+ throw bad('Provide user_id or subject_email.', 'user_id');
734
+ if (id) {
735
+ const u = e.db.users.get(id);
736
+ if (!u || u.deleted)
737
+ throw (0, errors_1.notFound)('User');
738
+ return u;
739
+ }
740
+ const has = (u, verified) => !u.deleted && u.emails.some((m) => m.email_address.toLowerCase() === email && (m.verified === true) === verified);
741
+ const u = e.db.users.all().find((x) => has(x, true)) ?? e.db.users.all().find((x) => has(x, false));
742
+ if (!u)
743
+ throw (0, errors_1.notFound)('User');
744
+ return u;
745
+ };
746
+ const jwtKid = (jws) => {
747
+ try {
748
+ return JSON.parse(atob(jws.split('.')[0].replace(/-/g, '+').replace(/_/g, '/'))).kid ?? null;
749
+ }
750
+ catch {
751
+ return null;
752
+ }
753
+ };
754
+ function privacyHandlers() {
755
+ const READ = 'data_subject_requests:read';
756
+ const WRITE = 'data_subject_requests:write';
757
+ const actor = ['api_key', null];
758
+ const decide = (kind) => (c) => {
759
+ const e = c.engine;
760
+ const r = dsrOf(e, c.params.id);
761
+ if (kind === 'cancel' && r.type === 'export')
762
+ return forbidden409('Only a scheduled erasure, or a restriction or rectification still waiting, can be cancelled.');
763
+ const allowed = kind === 'reject' ? ['scheduled', 'pending', 'processing'] : ['scheduled', 'pending'];
764
+ if (!allowed.includes(r.status))
765
+ return forbidden409(kind === 'reject' ? `This request is already ${r.status} and cannot be rejected.` : `This erasure is already ${r.status} and can no longer be cancelled.`);
766
+ const now = e.clock.now();
767
+ Object.assign(r, { status: kind === 'reject' ? 'rejected' : 'cancelled', updated_at: now, completed_at: now, ...(kind === 'reject' && typeof c.body.reason === 'string' ? { reason: c.body.reason } : {}) });
768
+ note(e, `data_subject_request.${r.type}.${kind === 'reject' ? 'rejected' : 'cancelled'}`, 'data_subject_request', r.id, actor);
769
+ if (kind === 'reject')
770
+ e.webhooks.emit('data_subject_request.rejected', { object: 'data_subject_request', id: r.id, user_id: r.user_id, type: r.type, status: r.status });
771
+ else if (r.type === 'erasure')
772
+ e.webhooks.emit('user.deletion_canceled', { object: 'user', id: r.user_id, primary_email_address_id: null, deletion_scheduled_at: null, deletion_effective_at: null, deletion_canceled_at: now, data_subject_request_id: r.id });
773
+ return { body: view(e, r) };
774
+ };
775
+ return [
776
+ {
777
+ method: 'POST',
778
+ pattern: '/v1/data_subject_requests',
779
+ scope: WRITE,
780
+ handler: (c) => {
781
+ const e = c.engine;
782
+ const resolved = typeof c.body.type === 'string' ? TYPES[c.body.type.trim().toLowerCase()] : undefined;
783
+ if (!resolved)
784
+ throw bad(TYPE_ERROR, 'type');
785
+ const reason = typeof c.body.reason === 'string' ? c.body.reason : undefined;
786
+ const user = subjectOf(e, c.body);
787
+ const latest = st(e).dsr.filter((r) => r.user_id === user.id && r.type === resolved.kind)[0];
788
+ if (latest && (latest.status === 'pending' || (resolved.kind === 'erasure' && latest.status === 'scheduled') || (resolved.kind !== 'erasure' && latest.status === 'processing')))
789
+ return { status: 202, body: view(e, latest) };
790
+ // A pre_user_deletion Action may refuse (403 ACTION_BLOCKED, nothing created).
791
+ if (resolved.kind === 'erasure')
792
+ runActionTrigger(e, 'pre_user_deletion', { userId: user.id, source: 'data_subject_request' });
793
+ const now = e.clock.now();
794
+ const erasure = resolved.kind === 'erasure';
795
+ const row = st(e).dsr.insert({
796
+ user_id: user.id,
797
+ type: resolved.kind,
798
+ canonical_type: resolved.canonical,
799
+ status: erasure ? 'scheduled' : 'pending',
800
+ source: 'api',
801
+ requested_by_type: 'api_key',
802
+ requested_by_id: null,
803
+ reason: reason ?? null,
804
+ detail: resolved.kind === 'restriction' || resolved.kind === 'rectification' ? `Waiting for an operator: carry out the ${resolved.canonical}, then fulfil this request.` : null,
805
+ steps: [],
806
+ scheduled_for: erasure ? now + ERASURE_GRACE_MS : null,
807
+ requested_at: now,
808
+ updated_at: now,
809
+ });
810
+ note(e, `data_subject_request.${resolved.kind}.requested`, 'user', user.id, actor);
811
+ const requested = { object: 'data_subject_request', id: row.id, user_id: user.id, type: resolved.kind, status: row.status, reason: reason ?? null, ...(erasure ? { scheduled_for: row.scheduled_for } : {}) };
812
+ if (resolved.kind === 'export')
813
+ e.webhooks.emit('user.data_export_requested', requested);
814
+ if (erasure) {
815
+ e.webhooks.emit('user.deletion_requested', requested);
816
+ e.webhooks.emit('data_subject_request.scheduled', { ...requested, canonical_type: resolved.canonical, deletion_effective_at: row.scheduled_for });
817
+ e.webhooks.emit('user.deletion_scheduled', { object: 'user', id: user.id, primary_email_address_id: user.primary_email_address_id ?? null, deletion_scheduled_at: now, deletion_effective_at: row.scheduled_for, data_subject_request_id: row.id });
818
+ }
819
+ return { status: 202, body: view(e, row) };
820
+ },
821
+ },
822
+ {
823
+ method: 'GET',
824
+ pattern: '/v1/data_subject_requests',
825
+ scope: READ,
826
+ handler: (c) => {
827
+ const e = c.engine;
828
+ settle(e);
829
+ const q = (k) => c.query.get(k);
830
+ const type = q('type') ? (TYPES[q('type').toLowerCase()]?.kind ?? q('type')) : null;
831
+ const rows = st(e).dsr.filter((r) => (!type || r.type === type) && (!q('status') || r.status === q('status')) && (!q('user_id') || r.user_id === q('user_id')) && (q('overdue') !== 'true' || view(e, r).overdue === true));
832
+ const page = (0, store_1.paginate)(rows, (0, util_1.pageOf)(c));
833
+ return { body: { data: page.data.map((r) => view(e, r)), has_more: page.has_more, next_cursor: page.next_cursor } };
834
+ },
835
+ },
836
+ {
837
+ method: 'GET',
838
+ pattern: '/v1/data_subject_requests/:id',
839
+ scope: READ,
840
+ handler: (c) => {
841
+ settle(c.engine);
842
+ return { body: view(c.engine, dsrOf(c.engine, c.params.id), true) };
843
+ },
844
+ },
845
+ {
846
+ method: 'POST',
847
+ pattern: '/v1/data_subject_requests/:id/fulfill',
848
+ scope: WRITE,
849
+ handler: (c) => {
850
+ const e = c.engine;
851
+ const r = dsrOf(e, c.params.id);
852
+ if (terminal(r.status))
853
+ return forbidden409(`This request is already ${r.status} and cannot be actioned again.`);
854
+ fulfil(e, r, actor);
855
+ return { body: view(e, r, true) };
856
+ },
857
+ },
858
+ { method: 'POST', pattern: '/v1/data_subject_requests/:id/reject', scope: WRITE, handler: decide('reject') },
859
+ { method: 'POST', pattern: '/v1/data_subject_requests/:id/cancel', scope: WRITE, handler: decide('cancel') },
860
+ {
861
+ method: 'POST',
862
+ pattern: '/v1/data_subject_requests/:id/extend',
863
+ scope: WRITE,
864
+ handler: (c) => {
865
+ const e = c.engine;
866
+ const reason = typeof c.body.reason === 'string' ? c.body.reason.trim() : '';
867
+ if (!reason || reason.length > 1000)
868
+ throw bad('reason is required (max 1000 characters): the deadline moves for a stated reason.', 'reason');
869
+ const raw = c.body.due_at;
870
+ const ms = typeof raw === 'number' ? raw : typeof raw === 'string' ? Date.parse(raw) : NaN;
871
+ if (!Number.isFinite(ms))
872
+ throw bad('due_at is a time (Unix milliseconds or ISO 8601).', 'due_at');
873
+ const r = dsrOf(e, c.params.id);
874
+ if (terminal(r.status))
875
+ throw new errors_1.EmulatorError('CONFLICT', `This request is already ${r.status}.`);
876
+ const current = dueAt(r);
877
+ if (current !== null && ms <= current)
878
+ throw bad('due_at must be later than the current deadline.', 'due_at');
879
+ // The cap keeps a typo from parking a request for years.
880
+ if (ms > r.requested_at + 3 * 365 * DAY)
881
+ throw bad('due_at is more than three years after the request.', 'due_at');
882
+ Object.assign(r, { due_at: ms, due_extended_reason: reason, updated_at: e.clock.now() });
883
+ note(e, 'data_subject_request.due_extended', 'data_subject_request', r.id, actor);
884
+ return { body: view(e, r) };
885
+ },
886
+ },
887
+ {
888
+ method: 'GET',
889
+ pattern: '/v1/data_subject_requests/:id/receipt',
890
+ scope: READ,
891
+ handler: async (c) => {
892
+ const e = c.engine;
893
+ settle(e);
894
+ const r = dsrOf(e, c.params.id);
895
+ if (r.type !== 'erasure' || r.status !== 'completed' || !r.completed_at)
896
+ throw new errors_1.EmulatorError('CONFLICT', 'A deletion receipt exists only for an erasure request that has completed.');
897
+ let rec = st(e).receipts.get(r.id);
898
+ if (!rec) {
899
+ const result = r.result ?? {};
900
+ const issued = e.clock.now();
901
+ const payload = {
902
+ type: 'deletion_receipt',
903
+ version: 1,
904
+ instance_id: e.instanceId,
905
+ request_id: r.id,
906
+ request_type: r.canonical_type,
907
+ subject_id: r.user_id,
908
+ requested_at: new Date(r.requested_at).toISOString(),
909
+ erased_at: result.erased_at,
910
+ personal_data_scrubbed_at: result.personal_data_scrubbed_at,
911
+ steps: (r.steps ?? []).map((s) => ({ name: s.name, status: s.status })),
912
+ retention: [],
913
+ issued_at: new Date(issued).toISOString(),
914
+ };
915
+ // shortcut: signed as a JWT with the engine's key (not a typ "atlas-deletion-receipt+jwt" JWS); it verifies against the engine JWKS, and never expires.
916
+ const jws = await e.tokens.signClaims(payload, 10 * 365 * 86_400);
917
+ rec = { id: e.db.newId('delrec'), request_id: r.id, subject_id: r.user_id, issued_at: issued, jws, kid: jwtKid(jws), payload };
918
+ st(e).receipts.set(r.id, rec);
919
+ note(e, 'data_subject_request.receipt_issued', 'data_subject_request', r.id, actor);
920
+ }
921
+ return { body: { object: 'deletion_receipt', id: rec.id, request_id: rec.request_id, subject_id: rec.subject_id, issued_at: rec.issued_at, receipt: rec.jws, claims: rec.payload, kid: rec.kid, jwks_uri: `${e.issuer}/.well-known/jwks.json` } };
922
+ },
923
+ },
924
+ // ── the user's own export (session bearer): one in flight, one per hour ──
925
+ {
926
+ method: 'POST',
927
+ pattern: '/v1/client/me/data_export',
928
+ public: true,
929
+ handler: async (c) => {
930
+ const e = c.engine;
931
+ const { userId } = await (0, support_1.sessionFromBearer)(c);
932
+ const latest = st(e).dsr.filter((r) => r.user_id === userId && r.type === 'export')[0];
933
+ if (latest && (latest.status === 'pending' || latest.status === 'processing'))
934
+ throw new errors_1.EmulatorError('RATE_LIMITED', 'A data export is already being prepared. Wait for it to finish.');
935
+ if (latest && e.clock.now() - latest.requested_at < exports.EXPORT_COOLDOWN_MS)
936
+ throw new errors_1.EmulatorError('RATE_LIMITED', 'You recently requested a data export. Try again later.');
937
+ const now = e.clock.now();
938
+ const row = st(e).dsr.insert({ user_id: userId, type: 'export', canonical_type: 'access', status: 'pending', source: 'user', requested_by_type: 'user', requested_by_id: userId, reason: null, detail: null, steps: [], scheduled_for: null, requested_at: now, updated_at: now });
939
+ note(e, 'data_subject_request.export.requested', 'user', userId, ['user', userId]);
940
+ e.webhooks.emit('user.data_export_requested', { object: 'data_subject_request', id: row.id, user_id: userId, type: 'export', status: row.status });
941
+ return { status: 202, body: clientView(row) };
942
+ },
943
+ },
944
+ {
945
+ method: 'GET',
946
+ pattern: '/v1/client/me/data_export',
947
+ public: true,
948
+ handler: async (c) => {
949
+ const { userId } = await (0, support_1.sessionFromBearer)(c);
950
+ settle(c.engine);
951
+ return { body: { object: 'list', data: st(c.engine).dsr.filter((r) => r.user_id === userId && r.type === 'export').map((r) => clientView(r)) } };
952
+ },
953
+ },
954
+ {
955
+ method: 'GET',
956
+ pattern: '/v1/client/me/data_export/:id',
957
+ public: true,
958
+ handler: async (c) => {
959
+ const { userId } = await (0, support_1.sessionFromBearer)(c);
960
+ settle(c.engine);
961
+ // Another user's request id is a 404, never a 403.
962
+ const r = st(c.engine).dsr.get(c.params.id);
963
+ if (!r || r.user_id !== userId || r.type !== 'export')
964
+ throw (0, errors_1.notFound)('Data export');
965
+ return { body: clientView(r, true) };
966
+ },
967
+ },
968
+ ];
969
+ }
970
+ // ── routes ───────────────────────────────────────────────────────────
971
+ function emu3MiscHandlers() {
972
+ const triggerOf = (raw) => {
973
+ if (!raw || !exports.ACTION_TRIGGERS.includes(raw))
974
+ throw bad(`A trigger of ${exports.ACTION_TRIGGERS.join(', ')} is required.`, 'trigger');
975
+ return raw;
976
+ };
977
+ const orgOf = (e, id) => {
978
+ const o = e.db.organizations.get(id);
979
+ if (!o)
980
+ throw (0, errors_1.notFound)('Organization');
981
+ return o;
982
+ };
983
+ return [
984
+ {
985
+ method: 'GET',
986
+ pattern: '/v1/actions/bindings/:trigger',
987
+ scope: 'actions:read',
988
+ handler: (c) => {
989
+ const trigger = triggerOf(c.params.trigger);
990
+ const live = (bindingsOf(c.engine).get(trigger) ?? []).filter((id) => c.engine.generic.get('actions')?.get(id));
991
+ return { body: { object: 'action_binding_list', trigger, action_ids: live } };
992
+ },
993
+ },
994
+ {
995
+ method: 'PUT',
996
+ pattern: '/v1/actions/bindings/:trigger',
997
+ scope: 'actions:write',
998
+ handler: (c) => {
999
+ const e = c.engine;
1000
+ const trigger = triggerOf(c.params.trigger);
1001
+ const ids = c.body.action_ids;
1002
+ if (!Array.isArray(ids) || ids.some((x) => typeof x !== 'string'))
1003
+ throw bad('action_ids must be an array of action ids.', 'action_ids');
1004
+ if (new Set(ids).size !== ids.length)
1005
+ throw bad('action_ids must not repeat an action.', 'action_ids');
1006
+ if (ids.length > MAX_ACTIONS_PER_FLOW)
1007
+ throw bad(`A flow may bind at most ${MAX_ACTIONS_PER_FLOW} actions.`, 'action_ids');
1008
+ // Every id must be an action of this instance that declares this trigger.
1009
+ for (const id of ids) {
1010
+ const a = e.generic.get('actions')?.get(id);
1011
+ if (!a)
1012
+ throw bad(`Unknown action: ${id}.`, 'action_ids');
1013
+ if (a.trigger !== trigger)
1014
+ throw new errors_1.EmulatorError('CONFLICT', `Action ${id} is a ${a.trigger} action and cannot bind to ${trigger}.`);
1015
+ }
1016
+ bindingsOf(e).set(trigger, [...ids]);
1017
+ return { body: { object: 'action_binding_list', trigger, action_ids: ids } };
1018
+ },
1019
+ },
1020
+ {
1021
+ method: 'GET',
1022
+ pattern: '/v1/organizations/:id/policy',
1023
+ scope: 'organizations:read',
1024
+ handler: (c) => {
1025
+ const o = orgOf(c.engine, c.params.id);
1026
+ return { body: { object: 'organization_policy', organization_id: o.id, policy: o.policy ?? {} } };
1027
+ },
1028
+ },
1029
+ {
1030
+ method: 'PATCH',
1031
+ pattern: '/v1/organizations/:id/policy',
1032
+ scope: 'organizations:write',
1033
+ handler: (c) => {
1034
+ const e = c.engine;
1035
+ const o = orgOf(e, c.params.id);
1036
+ const problems = policyProblems(c.body);
1037
+ if (problems.length)
1038
+ return { status: 422, body: (0, errors_1.errorBody)(problems.map((p) => ({ code: 'VALIDATION_FAILED', message: p.message, param: p.param }))) };
1039
+ const next = { ...(o.policy ?? {}) };
1040
+ // null clears an override; the retired keys only ever accept null.
1041
+ for (const [k, v] of Object.entries(c.body)) {
1042
+ if (v === null)
1043
+ delete next[k];
1044
+ else
1045
+ next[k] = v;
1046
+ }
1047
+ o.policy = next;
1048
+ o.updated_at = e.clock.now();
1049
+ return { body: { object: 'organization_policy', organization_id: o.id, policy: o.policy } };
1050
+ },
1051
+ },
1052
+ ...rateLimitHandlers(),
1053
+ ...privacyHandlers(),
1054
+ ];
1055
+ }
1056
+ //# sourceMappingURL=emu3-misc.js.map