@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
@@ -2,153 +2,254 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.authzHandlers = authzHandlers;
4
4
  const errors_1 = require("../errors");
5
- const store_1 = require("../store");
5
+ const access_signals_1 = require("./access-signals");
6
+ const cedar_1 = require("./cedar");
7
+ const authz_layers_1 = require("./authz-layers");
8
+ const authz_state_1 = require("./authz-state");
6
9
  const support_1 = require("./support");
7
10
  const util_1 = require("./util");
8
11
  /**
9
12
  * Authorization introspection on the engine's own state: `POST /v1/authz/explain` (why a user has or lacks a permission,
10
- * with every contributing role), `POST /v1/authz/rbac/check` (the same check, written to the decision log),
11
- * `POST /v1/authz/what_if` (replay the logged decisions under a candidate role edit and see what flips), managed FGA sync
12
- * (memberships and org roles become tuples in one store, with backfill, runs and purge) and the authorization event feed.
13
- * Layers the emulator has no data for (groups, resource roles, ABAC, access policies, FGA checks) are reported as
14
- * `skipped` with a reason and never decide, exactly as a layer that errored would not.
13
+ * asked of all seven layers: organization and app roles, groups, resource roles, ABAC, access policies and FGA tuples),
14
+ * `POST /v1/authz/rbac/check` (the role check, written to the decision log), `POST /v1/authz/what_if` (replay the logged
15
+ * role, ABAC and FGA decisions under a candidate change and see what flips), managed FGA sync (memberships, org roles and
16
+ * groups become tuples in one store, live on every change, with backfill, runs and purge) and the authorization event feed.
17
+ * The data the layers read is written by the routes in authz-data.
15
18
  */
16
- const LAYERS = ['rbac', 'org_roles', 'groups', 'resource_roles', 'abac', 'access_policies', 'fga'];
17
19
  const MAX_WAIT_MS = 25_000;
18
20
  const REPLAY_LIMIT_DEFAULT = 2000;
19
21
  const REPLAY_LIMIT_MAX = 10_000;
22
+ const SAMPLE_CAP = 50;
20
23
  const bad = (message, param) => new errors_1.EmulatorError('VALIDATION_FAILED', message, param);
21
- const st = (0, support_1.engineState)((e) => ({
22
- decisions: new store_1.Table('authz_decisions', e.clock.now, 'azd'),
23
- whatIf: new store_1.Table('authz_what_if_runs', e.clock.now, 'wif'),
24
- runs: new store_1.Table('fga_sync_runs', e.clock.now, 'fsr'),
25
- events: [],
26
- seq: 0,
27
- config: { enabled: false, store_id: null, memberships: true, org_roles: true, groups: true, updated_at: null },
28
- managed: new Map(),
29
- }));
30
- const permMatches = (have, want) => have === want || have === '*' || (have.endsWith(':*') && want.startsWith(have.slice(0, -1)));
31
- /** What a user's organization roles grant for one permission, optionally under role edits (`edits`: role key -> { add, remove }). */
32
- function rbac(e, userId, permission, organizationId, edits) {
33
- const contributions = [];
34
- for (const m of e.db.memberships.filter((x) => x.user_id === userId && (!organizationId || x.organization_id === organizationId))) {
35
- const role = e.db.roles.find((r) => r.key === m.role);
36
- const edit = edits?.get(m.role);
37
- const perms = (role?.permissions ?? []).filter((p) => !edit?.remove.includes(p)).concat(edit?.add ?? []);
38
- const matched = perms.some((p) => permMatches(p, permission));
39
- contributions.push({ layer: 'rbac', effect: 'allow', matched, permission, via: { kind: 'org_role', key: m.role, organization_id: m.organization_id } });
40
- }
41
- return { allowed: contributions.some((c) => c.matched), contributions };
42
- }
43
- function explain(e, userId, permission, organizationId) {
44
- if (!e.db.users.get(userId))
45
- throw (0, errors_1.notFound)('User');
46
- const r = rbac(e, userId, permission, organizationId);
47
- const skipped = (reason) => ({ status: 'skipped', effect: 'none', reason, contributions: [] });
48
- const layers = { rbac: { status: 'evaluated', effect: r.allowed ? 'allow' : 'none', contributions: r.contributions }, org_roles: skipped('No delegated roles in the emulator.') };
49
- for (const l of LAYERS.slice(2))
50
- layers[l] = skipped(`The emulator holds no ${l.replace('_', ' ')} data.`);
51
- const deciding = r.contributions.filter((c) => c.matched);
52
- const decision = r.allowed ? 'allow' : 'deny';
53
- return { object: 'authz_explanation', subject: { id: userId }, permission, organization_id: organizationId ?? null, decision, combination: 'deny_overrides', summary: r.allowed ? `Allowed by ${deciding.map((c) => c.via.key).join(', ')}.` : 'No layer allows this: denied by default.', deciding, layers, evaluated_at: e.clock.now() };
54
- }
55
- function pushEvent(e, kind, storeId, source, data) {
56
- const s = st(e);
57
- s.events.push({ object: 'authz_event', id: e.db.newId('azev'), seq: ++s.seq, kind, store_id: storeId, store_revision: s.seq, source, data, created_at: e.clock.now() });
58
- }
59
- const parseEdits = (body) => {
60
- const roles = body.candidate?.rbac?.roles;
61
- if (roles === undefined)
62
- return undefined;
63
- if (!Array.isArray(roles) || !roles.length || roles.length > 200)
64
- throw bad('candidate.rbac.roles must list 1-200 role edits.', 'candidate.rbac.roles');
65
- const out = new Map();
66
- roles.forEach((r, i) => {
67
- if (!r || typeof r !== 'object' || (r.type !== 'org_role' && r.type !== 'app_role'))
68
- throw bad('type must be org_role or app_role.', `candidate.rbac.roles[${i}].type`);
69
- if (typeof r.key !== 'string' || !r.key)
70
- throw bad('key is required.', `candidate.rbac.roles[${i}].key`);
71
- const list = (v, f) => {
72
- if (v === undefined)
73
- return [];
74
- if (!Array.isArray(v) || v.some((x) => typeof x !== 'string'))
75
- throw bad(`${f} must be a list of permissions.`, `candidate.rbac.roles[${i}].${f}`);
76
- return v;
77
- };
78
- const prev = out.get(r.key) ?? { add: [], remove: [] };
79
- out.set(r.key, { add: [...prev.add, ...list(r.add_permissions, 'add_permissions')], remove: [...prev.remove, ...list(r.remove_permissions, 'remove_permissions')] });
24
+ const obj = (v, field) => {
25
+ if (v === undefined || v === null)
26
+ return {};
27
+ if (typeof v !== 'object' || Array.isArray(v))
28
+ throw bad(`${field} must be an object.`, field);
29
+ return v;
30
+ };
31
+ const strList = (v, field) => {
32
+ if (v === undefined)
33
+ return [];
34
+ if (!Array.isArray(v) || v.some((x) => typeof x !== 'string' || !x))
35
+ throw bad(`${field} must be a list of strings.`, field);
36
+ return v;
37
+ };
38
+ const tupleList = (v, field) => {
39
+ if (v === undefined)
40
+ return [];
41
+ if (!Array.isArray(v))
42
+ throw bad(`${field} must be a list of tuples.`, field);
43
+ return v.map((t, i) => {
44
+ const o = obj(t, `${field}[${i}]`);
45
+ if (typeof o.user !== 'string' || typeof o.relation !== 'string' || typeof (o.object ?? o.target) !== 'string')
46
+ throw bad(`${field}[${i}] requires user, relation and object.`, field);
47
+ if (!/^[^:#]+:[^#]+(#[^#]+)?$/.test(o.user))
48
+ throw bad(`${field}[${i}].user is malformed.`, field);
49
+ return { user: o.user, relation: o.relation, object: (o.object ?? o.target) };
80
50
  });
81
- return out;
82
51
  };
83
- const desiredTuples = (e) => {
84
- const cfg = st(e).config;
85
- const out = [];
86
- for (const m of e.db.memberships.all()) {
87
- const u = e.db.users.get(m.user_id);
88
- if (!u || u.deleted)
89
- continue;
90
- if (cfg.memberships)
91
- out.push({ user: `user:${m.user_id}`, relation: 'member', object: `organization:${m.organization_id}` });
92
- if (cfg.org_roles && m.role)
93
- out.push({ user: `user:${m.user_id}`, relation: String(m.role).replace(/^org:/, ''), object: `organization:${m.organization_id}` });
52
+ const policySources = (v, field) => {
53
+ if (v === undefined)
54
+ return [];
55
+ if (!Array.isArray(v))
56
+ throw bad(`${field} must be a list.`, field);
57
+ return v.map((p, i) => {
58
+ const o = obj(p, `${field}[${i}]`);
59
+ if (typeof o.name !== 'string' || !o.name || typeof o.source !== 'string' || !o.source)
60
+ throw bad(`${field}[${i}] needs name and source.`, field);
61
+ try {
62
+ (0, cedar_1.parseCedar)(o.source);
63
+ }
64
+ catch (err) {
65
+ if (err instanceof cedar_1.CedarParseError)
66
+ throw bad(`${field}[${i}].source: ${err.message} (at character ${err.position})`, field);
67
+ throw err;
68
+ }
69
+ return { name: o.name, source: o.source };
70
+ });
71
+ };
72
+ const time = (v, field) => {
73
+ if (v === undefined || v === null)
74
+ return undefined;
75
+ const t = typeof v === 'number' ? v : typeof v === 'string' ? Date.parse(v) : NaN;
76
+ if (Number.isNaN(t))
77
+ throw bad(`${field} must be a millisecond timestamp or an ISO date.`, field);
78
+ return t;
79
+ };
80
+ function parseCandidate(cand) {
81
+ const out = {};
82
+ if (cand.abac !== undefined) {
83
+ const a = obj(cand.abac, 'candidate.abac');
84
+ out.abac = { add: policySources(a.add, 'candidate.abac.add'), replace: policySources(a.replace, 'candidate.abac.replace'), remove: strList(a.remove, 'candidate.abac.remove') };
94
85
  }
86
+ if (cand.rbac !== undefined) {
87
+ const r = obj(cand.rbac, 'candidate.rbac');
88
+ if (!Array.isArray(r.roles) || r.roles.length === 0 || r.roles.length > 200)
89
+ throw bad('candidate.rbac.roles must list 1-200 role edits.', 'candidate.rbac.roles');
90
+ out.rbac = {
91
+ roles: r.roles.map((x, i) => {
92
+ const o = obj(x, `candidate.rbac.roles[${i}]`);
93
+ if (o.type !== 'org_role' && o.type !== 'app_role')
94
+ throw bad('type must be org_role or app_role.', `candidate.rbac.roles[${i}].type`);
95
+ if (typeof o.key !== 'string' || !o.key)
96
+ throw bad('key is required.', `candidate.rbac.roles[${i}].key`);
97
+ return { type: o.type, key: o.key, organizationId: typeof o.organization_id === 'string' ? o.organization_id : undefined, addPermissions: strList(o.add_permissions, 'add_permissions'), removePermissions: strList(o.remove_permissions, 'remove_permissions') };
98
+ }),
99
+ };
100
+ }
101
+ if (cand.fga !== undefined) {
102
+ const f = obj(cand.fga, 'candidate.fga');
103
+ if (typeof f.store_id !== 'string' || !f.store_id)
104
+ throw bad('candidate.fga.store_id is required.', 'candidate.fga.store_id');
105
+ out.fga = { storeId: f.store_id, writes: tupleList(f.writes, 'candidate.fga.writes'), deletes: tupleList(f.deletes, 'candidate.fga.deletes') };
106
+ }
107
+ if (cand.access_policy !== undefined)
108
+ out.accessPolicy = obj(obj(cand.access_policy, 'candidate.access_policy').draft ?? cand.access_policy, 'candidate.access_policy.draft');
109
+ if (!out.abac && !out.rbac && !out.fga && !out.accessPolicy)
110
+ throw bad('candidate needs at least one of abac, rbac, fga, access_policy.', 'candidate');
95
111
  return out;
96
- };
97
- const tupleKey = (t) => `${t.user}|${t.relation}|${t.object}`;
98
- /** One sync run: write what should exist; with `repair`, also delete managed tuples that no longer should. Only managed tuples are ever touched. */
99
- function runSync(e, kind, repair) {
100
- const s = st(e);
101
- const now = e.clock.now();
102
- const want = new Map(desiredTuples(e).map((t) => [tupleKey(t), t]));
103
- let written = 0;
104
- let deleted = 0;
105
- for (const [k, t] of want) {
106
- if (s.managed.has(k))
107
- continue;
108
- s.managed.set(k, t);
109
- written++;
110
- pushEvent(e, 'tuple_written', s.config.store_id, 'sync', { ...t });
111
- e.webhooks.emit('fga.tuple_written', { object: 'fga_tuple_batch', store_id: s.config.store_id, store_revision: null, source: 'sync', count: 1, tuples: [{ ...t }] });
112
+ }
113
+ function runWhatIf(e, body) {
114
+ const c = parseCandidate(obj(body.candidate, 'candidate'));
115
+ const until = time(body.until, 'until') ?? e.clock.now();
116
+ let since = time(body.since, 'since');
117
+ if (since === undefined) {
118
+ const days = body.days === undefined ? 7 : body.days;
119
+ if (!Number.isInteger(days) || days < 1 || days > 30)
120
+ throw bad('days must be an integer from 1 to 30.', 'days');
121
+ since = until - days * 86_400_000;
112
122
  }
113
- if (repair) {
114
- for (const [k, t] of [...s.managed]) {
115
- if (want.has(k))
116
- continue;
117
- s.managed.delete(k);
118
- deleted++;
119
- pushEvent(e, 'tuple_deleted', s.config.store_id, 'sync', { ...t });
120
- e.webhooks.emit('fga.tuple_deleted', { object: 'fga_tuple_batch', store_id: s.config.store_id, store_revision: null, source: 'sync', count: 1, tuples: [{ ...t }] });
123
+ if (since >= until)
124
+ throw bad('since must be before until.', 'since');
125
+ if (until - since > 31 * 86_400_000)
126
+ throw bad('The window can be at most 31 days.', 'since');
127
+ const limit = body.limit === undefined ? REPLAY_LIMIT_DEFAULT : body.limit;
128
+ if (!Number.isInteger(limit) || limit < 1 || limit > REPLAY_LIMIT_MAX)
129
+ throw bad(`limit must be an integer from 1 to ${REPLAY_LIMIT_MAX}.`, 'limit');
130
+ if (body.name !== undefined && (typeof body.name !== 'string' || body.name.length > 200))
131
+ throw bad('name must be a string of at most 200 characters.', 'name');
132
+ if (body.save !== undefined && typeof body.save !== 'boolean')
133
+ throw bad('save must be a boolean.', 'save');
134
+ if (c.fga && !(0, authz_state_1.storeExists)(e, c.fga.storeId))
135
+ throw bad('Unknown FGA store.', 'candidate.fga.store_id');
136
+ if (c.fga && !(0, authz_layers_1.latestModel)(e, c.fga.storeId))
137
+ throw bad('The FGA store has no authorization model.', 'candidate.fga.store_id');
138
+ const by = {};
139
+ const skippedBy = {};
140
+ const skip = (reason) => void (skippedBy[reason] = (skippedBy[reason] ?? 0) + 1);
141
+ const flips = [];
142
+ const principals = new Set();
143
+ let replayed = 0;
144
+ const note = (row, before, after, mismatch) => {
145
+ replayed++;
146
+ const s = (by[row.source] ??= { replayed: 0, unchanged: 0, allow_to_deny: 0, deny_to_allow: 0, baseline_mismatch: 0 });
147
+ s.replayed++;
148
+ if (mismatch)
149
+ s.baseline_mismatch++;
150
+ if (before === after)
151
+ return void s.unchanged++;
152
+ if (before === 'allow')
153
+ s.allow_to_deny++;
154
+ else
155
+ s.deny_to_allow++;
156
+ if (row.principal)
157
+ principals.add(row.principal);
158
+ if (flips.length < SAMPLE_CAP)
159
+ flips.push({ decision_id: row.id, source: row.source, principal: row.principal, action: row.action, resource: row.resource, before, after, at: row.at });
160
+ };
161
+ const pdp = c.abac ? (0, authz_layers_1.loadPdp)(e) : null;
162
+ const cpdp = c.abac && pdp ? (0, authz_layers_1.candidatePdp)(pdp, c.abac) : null;
163
+ const all = (0, authz_state_1.st)(e).decisions.all().filter((d) => d.at >= since && d.at <= until);
164
+ const rows = all.slice(0, limit);
165
+ for (const row of rows) {
166
+ try {
167
+ if (['abac', 'authzen', 'mcp_gateway', 'pdp'].includes(row.source) && pdp && cpdp) {
168
+ const req = row.input;
169
+ if (!req || typeof req !== 'object' || !req.principal || !req.resource) {
170
+ skip('no_input');
171
+ continue;
172
+ }
173
+ const now = (0, authz_layers_1.abacDecide)(pdp, req).decision;
174
+ note(row, now, (0, authz_layers_1.abacDecide)(cpdp, req).decision, now !== row.decision);
175
+ }
176
+ else if (row.source === 'rbac' && c.rbac) {
177
+ if (row.result?.kind !== 'role' || !row.action) {
178
+ skip('rbac_not_role_based');
179
+ continue;
180
+ }
181
+ note(row, row.decision, (0, authz_layers_1.replayRoleDecision)(row.result, c.rbac.roles, row.action) ? 'allow' : 'deny', false);
182
+ }
183
+ else if (row.source === 'fga' && c.fga) {
184
+ if (row.policyVersion?.store_id !== c.fga.storeId) {
185
+ skip('other_store');
186
+ continue;
187
+ }
188
+ const i = row.input ?? {};
189
+ if (!i.user || !i.relation || !i.object) {
190
+ skip('no_input');
191
+ continue;
192
+ }
193
+ const req = { user: i.user, relation: i.relation, object: i.object, contextual: i.contextual_tuples };
194
+ const now = (0, authz_layers_1.fgaCheck)(e, c.fga.storeId, req) ? 'allow' : 'deny';
195
+ note(row, now, (0, authz_layers_1.fgaCheck)(e, c.fga.storeId, req, { writes: c.fga.writes, deletes: c.fga.deletes }) ? 'allow' : 'deny', now !== row.decision);
196
+ }
197
+ else
198
+ skip(row.source === 'access_policy' ? 'access_policy_replayed_separately' : 'no_matching_candidate');
199
+ }
200
+ catch {
201
+ skip('replay_error');
121
202
  }
122
203
  }
204
+ // shortcut: the emulator keeps no sampled sign-ins, so an access-policy candidate has nothing to replay against.
205
+ if (c.accessPolicy)
206
+ skip('access_policy_no_sampled_sign_ins');
207
+ const totals = { unchanged: 0, allow_to_deny: 0, deny_to_allow: 0 };
208
+ for (const s of Object.values(by)) {
209
+ totals.unchanged += s.unchanged;
210
+ totals.allow_to_deny += s.allow_to_deny;
211
+ totals.deny_to_allow += s.deny_to_allow;
212
+ }
213
+ const result = { object: 'authz_what_if', name: body.name ?? null, window: { since, until }, evaluated: rows.length, replayed, truncated: all.length > rows.length, skipped: skippedBy, by_source: by, totals, affected_principals: principals.size, samples: flips };
214
+ if (body.save === false)
215
+ return { ...result, status: 200 };
216
+ const describe = {
217
+ ...(c.abac ? { abac: c.abac } : {}),
218
+ ...(c.rbac ? { rbac: { roles: c.rbac.roles.map((r) => ({ type: r.type, key: r.key, organization_id: r.organizationId ?? null, add_permissions: r.addPermissions, remove_permissions: r.removePermissions })) } } : {}),
219
+ ...(c.fga ? { fga: { store_id: c.fga.storeId, writes: c.fga.writes, deletes: c.fga.deletes } } : {}),
220
+ ...(c.accessPolicy ? { access_policy: { draft: c.accessPolicy } } : {}),
221
+ };
222
+ const saved = (0, authz_state_1.st)(e).whatIf.insert({ name: body.name ?? null, candidate: describe, window_start: since, window_end: until, summary: { evaluated: rows.length, replayed, truncated: result.truncated, totals, affected_principals: principals.size, by_source: by }, result, created_at: e.clock.now() });
223
+ return { ...result, id: saved.id, created_at: saved.created_at, status: 201 };
224
+ }
225
+ const whatIfView = (r, full) => ({ object: 'authz_what_if_run', id: r.id, name: r.name, candidate: r.candidate, window: { since: r.window_start, until: r.window_end }, summary: r.summary, ...(full ? { result: r.result } : {}), created_by: null, created_at: r.created_at });
226
+ /** One sync run: write what should exist; with `repair`, also delete managed tuples that no longer should. Only managed tuples are ever touched. */
227
+ function runSync(e, kind, repair) {
228
+ const s = (0, authz_state_1.st)(e);
229
+ const now = e.clock.now();
230
+ const { written, deleted } = (0, authz_state_1.reconcileManaged)(e, repair);
123
231
  const run = s.runs.insert({ kind, status: 'completed', store_id: s.config.store_id, repair, written, deleted, managed: s.managed.size, error: null, created_at: now, completed_at: now });
124
- pushEvent(e, 'sync_run', s.config.store_id, 'sync', { run_id: run.id, written, deleted });
232
+ (0, authz_state_1.pushEvent)(e, 'sync_run', s.config.store_id, 'sync', { run_id: run.id, written, deleted });
125
233
  e.webhooks.emit('fga_sync.completed', { object: 'fga_sync_run', id: run.id, store_id: s.config.store_id, written, deleted });
126
234
  return run;
127
235
  }
128
236
  const runView = (r) => ({ object: 'fga_sync_run', id: r.id, kind: r.kind, status: r.status, store_id: r.store_id, repair: r.repair, tuples_written: r.written, tuples_deleted: r.deleted, managed_tuples: r.managed, error: r.error, created_at: r.created_at, completed_at: r.completed_at });
129
237
  const statusView = (e) => {
130
- const s = st(e);
238
+ const s = (0, authz_state_1.st)(e);
131
239
  const last = s.runs.all()[0];
132
240
  return { object: 'fga_sync', enabled: s.config.enabled, store_id: s.config.store_id, memberships: s.config.memberships, org_roles: s.config.org_roles, groups: s.config.groups, managed_tuples: s.managed.size, last_run: last ? runView(last) : null, updated_at: s.config.updated_at };
133
241
  };
134
- const storeExists = (e, id) => !!e.generic.get('fga/stores')?.get(id);
135
242
  function authzHandlers() {
136
- const whatIfView = (r, full) => ({ object: 'authz_what_if_run', id: r.id, status: 'completed', created_at: r.created_at, window: r.window, evaluated: r.evaluated, changed: r.changed, flipped_to_allow: r.flipped_to_allow, flipped_to_deny: r.flipped_to_deny, truncated: r.truncated, not_evaluated: r.not_evaluated, ...(full ? { samples: r.samples } : {}) });
137
243
  return [
138
244
  {
139
245
  method: 'POST',
140
246
  pattern: '/v1/authz/explain',
141
247
  scope: 'authz_decisions:read',
142
248
  handler: (c) => {
143
- const id = c.body.subject?.id;
144
- if (typeof id !== 'string' || !id)
145
- throw bad('subject.id is required.', 'subject.id');
146
- const permission = c.body.permission ?? c.body.action;
147
- if (typeof permission !== 'string' || !permission)
148
- throw bad('permission (or action) is required.', 'permission');
149
- if (c.body.organization_id !== undefined && typeof c.body.organization_id !== 'string')
150
- throw bad('organization_id must be a string.', 'organization_id');
151
- return { body: explain(c.engine, id, permission, c.body.organization_id) };
249
+ const input = (0, authz_layers_1.parseExplainBody)(c.body);
250
+ // Signals a caller proxies in headers (country, risk level, device trust) apply when the body does not say.
251
+ input.access = { ...input.access, signals: (0, access_signals_1.firstOf)(input.access?.signals, (0, access_signals_1.headerSignals)(c.request.headers)) };
252
+ return { body: (0, authz_layers_1.explainAccess)(c.engine, input) };
152
253
  },
153
254
  },
154
255
  {
@@ -158,17 +259,18 @@ function authzHandlers() {
158
259
  scope: 'authz_decisions:read',
159
260
  handler: (c) => {
160
261
  const e = c.engine;
161
- const id = c.body.subject?.id;
162
- if (typeof id !== 'string' || !id)
262
+ const subject = (c.body.subject ?? {});
263
+ if (typeof subject.id !== 'string' || !subject.id)
163
264
  throw bad('subject.id is required.', 'subject.id');
164
265
  if (typeof c.body.permission !== 'string' || !c.body.permission)
165
266
  throw bad('permission is required.', 'permission');
166
- if (!e.db.users.get(id))
167
- throw (0, errors_1.notFound)('User');
168
- const r = rbac(e, id, c.body.permission, c.body.organization_id);
169
- const held = r.contributions.filter((x) => x.matched);
170
- st(e).decisions.insert({ source: 'rbac', user_id: id, permission: c.body.permission, organization_id: c.body.organization_id ?? null, allowed: r.allowed, at: e.clock.now() });
171
- return { body: { object: 'rbac_check', allowed: r.allowed, decision: r.allowed ? 'allow' : 'deny', grants: held.map((h) => h.via.key), rules: held } };
267
+ if (c.body.organization_id !== undefined && typeof c.body.organization_id !== 'string')
268
+ throw bad('organization_id must be a string.', 'organization_id');
269
+ const input = { userId: subject.id, permission: c.body.permission, organizationId: c.body.organization_id };
270
+ const r = (0, authz_layers_1.rbacCheck)(e, input);
271
+ (0, authz_layers_1.recordRbacCheck)(e, input, r);
272
+ const rules = [...r.layers.rbac.contributions, ...r.layers.org_roles.contributions, ...r.layers.groups.contributions].filter((x) => x.matched);
273
+ return { body: { object: 'rbac_check', allowed: r.allowed, decision: r.allowed ? 'allow' : 'deny', grants: r.grants, rules } };
172
274
  },
173
275
  },
174
276
  {
@@ -176,57 +278,17 @@ function authzHandlers() {
176
278
  pattern: '/v1/authz/what_if',
177
279
  scope: 'authz_decisions:read',
178
280
  handler: (c) => {
179
- const e = c.engine;
180
- const cand = c.body.candidate;
181
- if (!cand || typeof cand !== 'object' || Array.isArray(cand))
182
- throw bad('candidate is required.', 'candidate');
183
- const edits = parseEdits(c.body);
184
- const notEvaluated = [];
185
- for (const k of ['abac', 'fga', 'access_policy'])
186
- if (cand[k] !== undefined)
187
- notEvaluated.push(k);
188
- if (cand.fga !== undefined && (!cand.fga || typeof cand.fga.store_id !== 'string' || !cand.fga.store_id))
189
- throw bad('candidate.fga.store_id is required.', 'candidate.fga.store_id');
190
- if (!edits && !notEvaluated.length)
191
- throw bad('candidate needs at least one of abac, rbac, fga, access_policy.', 'candidate');
192
- const until = c.body.until === undefined ? e.clock.now() : Date.parse(String(c.body.until));
193
- const days = (0, support_1.intIn)(c.body.days, 'days', 1, 30, 7);
194
- const since = c.body.since === undefined ? until - days * 86_400_000 : Date.parse(String(c.body.since));
195
- if (Number.isNaN(until) || Number.isNaN(since))
196
- throw bad('since and until must be ISO 8601 times.', 'since');
197
- if (since >= until)
198
- throw bad('since must be before until.', 'since');
199
- if (until - since > 31 * 86_400_000)
200
- throw bad('The window can be at most 31 days.', 'since');
201
- const limit = (0, support_1.intIn)(c.body.limit, 'limit', 1, REPLAY_LIMIT_MAX, REPLAY_LIMIT_DEFAULT);
202
- const all = st(e).decisions.all().filter((d) => d.at >= since && d.at <= until);
203
- const rows = all.slice(0, limit);
204
- const flips = [];
205
- let toAllow = 0;
206
- let toDeny = 0;
207
- for (const d of rows) {
208
- if (!edits)
209
- break;
210
- const next = rbac(e, d.user_id, d.permission, d.organization_id ?? undefined, edits).allowed;
211
- if (next === d.allowed)
212
- continue;
213
- next ? toAllow++ : toDeny++;
214
- flips.push({ decision_id: d.id, user_id: d.user_id, permission: d.permission, organization_id: d.organization_id, before: d.allowed ? 'allow' : 'deny', after: next ? 'allow' : 'deny' });
215
- }
216
- const result = { evaluated: rows.length, changed: flips.length, flipped_to_allow: toAllow, flipped_to_deny: toDeny, truncated: all.length > rows.length, not_evaluated: notEvaluated, samples: flips.slice(0, 25), window: { since, until } };
217
- if (c.body.save === false)
218
- return { body: { object: 'authz_what_if_run', id: null, status: 'completed', created_at: e.clock.now(), ...result } };
219
- const row = st(e).whatIf.insert({ ...result, created_at: e.clock.now() });
220
- return { status: 201, body: whatIfView(row, true) };
281
+ const { status, ...body } = runWhatIf(c.engine, c.body);
282
+ return { status, body };
221
283
  },
222
284
  },
223
- { method: 'GET', pattern: '/v1/authz/what_if', scope: 'authz_decisions:read', handler: (c) => (0, util_1.listReply)(c, st(c.engine).whatIf.all(), (r) => whatIfView(r, false)) },
285
+ { method: 'GET', pattern: '/v1/authz/what_if', scope: 'authz_decisions:read', handler: (c) => (0, util_1.listReply)(c, (0, authz_state_1.st)(c.engine).whatIf.all(), (r) => whatIfView(r, false)) },
224
286
  {
225
287
  method: 'GET',
226
288
  pattern: '/v1/authz/what_if/:id',
227
289
  scope: 'authz_decisions:read',
228
290
  handler: (c) => {
229
- const r = st(c.engine).whatIf.get(c.params.id);
291
+ const r = (0, authz_state_1.st)(c.engine).whatIf.get(c.params.id);
230
292
  if (!r)
231
293
  throw (0, errors_1.notFound)('What-if run');
232
294
  return { body: whatIfView(r, true) };
@@ -237,7 +299,7 @@ function authzHandlers() {
237
299
  pattern: '/v1/authz/what_if/:id',
238
300
  scope: 'authz_decisions:write',
239
301
  handler: (c) => {
240
- if (!st(c.engine).whatIf.delete(c.params.id ?? ''))
302
+ if (!(0, authz_state_1.st)(c.engine).whatIf.delete(c.params.id ?? ''))
241
303
  throw (0, errors_1.notFound)('What-if run');
242
304
  return { body: { object: 'authz_what_if_run', id: c.params.id, deleted: true } };
243
305
  },
@@ -250,13 +312,13 @@ function authzHandlers() {
250
312
  scope: 'fga:write',
251
313
  handler: (c) => {
252
314
  const e = c.engine;
253
- const s = st(e);
315
+ const s = (0, authz_state_1.st)(e);
254
316
  const b = c.body;
255
317
  for (const k of ['enabled', 'memberships', 'org_roles', 'groups'])
256
318
  if (b[k] !== undefined && typeof b[k] !== 'boolean')
257
319
  throw bad(`${k} must be true or false.`, k);
258
320
  const storeId = b.store_id === undefined ? s.config.store_id : b.store_id;
259
- if (storeId !== null && (typeof storeId !== 'string' || !storeExists(e, storeId)))
321
+ if (storeId !== null && (typeof storeId !== 'string' || !(0, authz_state_1.storeExists)(e, storeId)))
260
322
  throw bad('store_id must be an existing FGA store.', 'store_id');
261
323
  // Owned tuples live in one store: it cannot change under them.
262
324
  if (storeId !== s.config.store_id && s.managed.size)
@@ -279,18 +341,18 @@ function authzHandlers() {
279
341
  const e = c.engine;
280
342
  if (c.body.repair !== undefined && typeof c.body.repair !== 'boolean')
281
343
  throw bad('repair must be a boolean.', 'repair');
282
- if (!st(e).config.enabled || !st(e).config.store_id)
344
+ if (!(0, authz_state_1.st)(e).config.enabled || !(0, authz_state_1.st)(e).config.store_id)
283
345
  throw bad('Enable sync with a store before running a backfill.', 'enabled');
284
346
  return { status: 202, body: runView(runSync(e, 'backfill', c.body.repair === true)) };
285
347
  },
286
348
  },
287
- { method: 'GET', pattern: '/v1/fga/sync/runs', scope: 'fga:read', handler: (c) => (0, util_1.listReply)(c, st(c.engine).runs.all(), runView) },
349
+ { method: 'GET', pattern: '/v1/fga/sync/runs', scope: 'fga:read', handler: (c) => (0, util_1.listReply)(c, (0, authz_state_1.st)(c.engine).runs.all(), runView) },
288
350
  {
289
351
  method: 'GET',
290
352
  pattern: '/v1/fga/sync/runs/:id',
291
353
  scope: 'fga:read',
292
354
  handler: (c) => {
293
- const r = st(c.engine).runs.get(c.params.id);
355
+ const r = (0, authz_state_1.st)(c.engine).runs.get(c.params.id);
294
356
  if (!r)
295
357
  throw (0, errors_1.notFound)('Sync run');
296
358
  return { body: runView(r) };
@@ -303,7 +365,7 @@ function authzHandlers() {
303
365
  scope: 'fga:write',
304
366
  handler: (c) => {
305
367
  const e = c.engine;
306
- const s = st(e);
368
+ const s = (0, authz_state_1.st)(e);
307
369
  if (c.body.confirm !== true)
308
370
  throw bad('Send confirm: true to remove the tuples the sync wrote.', 'confirm');
309
371
  if (!s.config.store_id)
@@ -311,9 +373,10 @@ function authzHandlers() {
311
373
  if (s.config.enabled)
312
374
  throw new errors_1.EmulatorError('CONFLICT', 'Disable sync before purging.');
313
375
  const n = s.managed.size;
314
- for (const t of s.managed.values())
315
- pushEvent(e, 'tuple_deleted', s.config.store_id, 'sync', { ...t });
376
+ const owned = [...s.managed.values()];
316
377
  s.managed.clear();
378
+ if (owned.length)
379
+ (0, authz_state_1.applyTuples)(e, s.config.store_id, [], owned, 'sync');
317
380
  return { body: { object: 'fga.sync_purge', store_id: s.config.store_id, deleted: n, released: 0 } };
318
381
  },
319
382
  },
@@ -324,7 +387,7 @@ function authzHandlers() {
324
387
  scope: 'fga:read',
325
388
  handler: async (c) => {
326
389
  const e = c.engine;
327
- const s = st(e);
390
+ const s = (0, authz_state_1.st)(e);
328
391
  const rawAfter = c.query.get('after');
329
392
  const after = rawAfter === 'latest' ? s.seq : rawAfter === null ? 0 : Number(rawAfter);
330
393
  if (!Number.isInteger(after) || after < 0)