@substrat-run/contract-tests 0.138.0 → 0.140.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 (113) hide show
  1. package/dist/attachment-text-fixtures.d.ts +17 -0
  2. package/dist/attachment-text-fixtures.d.ts.map +1 -1
  3. package/dist/attachment-text-fixtures.js +344 -4
  4. package/dist/attachment-text-fixtures.js.map +1 -1
  5. package/dist/attachment-text-suite.d.ts +16 -1
  6. package/dist/attachment-text-suite.d.ts.map +1 -1
  7. package/dist/attachment-text-suite.js +212 -28
  8. package/dist/attachment-text-suite.js.map +1 -1
  9. package/dist/capability-suite.d.ts.map +1 -1
  10. package/dist/capability-suite.js +5 -0
  11. package/dist/capability-suite.js.map +1 -1
  12. package/dist/caused-by-module.d.ts +23 -0
  13. package/dist/caused-by-module.d.ts.map +1 -0
  14. package/dist/caused-by-module.js +56 -0
  15. package/dist/caused-by-module.js.map +1 -0
  16. package/dist/caused-by-suite.d.ts +28 -0
  17. package/dist/caused-by-suite.d.ts.map +1 -0
  18. package/dist/caused-by-suite.js +223 -0
  19. package/dist/caused-by-suite.js.map +1 -0
  20. package/dist/connect-link-suite.d.ts +19 -0
  21. package/dist/connect-link-suite.d.ts.map +1 -0
  22. package/dist/connect-link-suite.js +215 -0
  23. package/dist/connect-link-suite.js.map +1 -0
  24. package/dist/entity-state-migration-suite.d.ts +16 -0
  25. package/dist/entity-state-migration-suite.d.ts.map +1 -0
  26. package/dist/entity-state-migration-suite.js +305 -0
  27. package/dist/entity-state-migration-suite.js.map +1 -0
  28. package/dist/entity-state-module.d.ts +317 -0
  29. package/dist/entity-state-module.d.ts.map +1 -0
  30. package/dist/entity-state-module.js +278 -0
  31. package/dist/entity-state-module.js.map +1 -0
  32. package/dist/entity-state-suite.d.ts +9 -0
  33. package/dist/entity-state-suite.d.ts.map +1 -0
  34. package/dist/entity-state-suite.js +557 -0
  35. package/dist/entity-state-suite.js.map +1 -0
  36. package/dist/entity-trash-module.d.ts +439 -0
  37. package/dist/entity-trash-module.d.ts.map +1 -0
  38. package/dist/entity-trash-module.js +287 -0
  39. package/dist/entity-trash-module.js.map +1 -0
  40. package/dist/entity-trash-suite.d.ts +59 -0
  41. package/dist/entity-trash-suite.d.ts.map +1 -0
  42. package/dist/entity-trash-suite.js +568 -0
  43. package/dist/entity-trash-suite.js.map +1 -0
  44. package/dist/erasure-module.d.ts +259 -0
  45. package/dist/erasure-module.d.ts.map +1 -0
  46. package/dist/erasure-module.js +251 -0
  47. package/dist/erasure-module.js.map +1 -0
  48. package/dist/findings-atomic-suite.d.ts +21 -0
  49. package/dist/findings-atomic-suite.d.ts.map +1 -0
  50. package/dist/findings-atomic-suite.js +131 -0
  51. package/dist/findings-atomic-suite.js.map +1 -0
  52. package/dist/findings-suite.d.ts +11 -0
  53. package/dist/findings-suite.d.ts.map +1 -0
  54. package/dist/findings-suite.js +391 -0
  55. package/dist/findings-suite.js.map +1 -0
  56. package/dist/index.d.ts +22 -3
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +20 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/list-suite.d.ts.map +1 -1
  61. package/dist/list-suite.js +101 -1
  62. package/dist/list-suite.js.map +1 -1
  63. package/dist/membership-executor-suite.d.ts +6 -2
  64. package/dist/membership-executor-suite.d.ts.map +1 -1
  65. package/dist/membership-executor-suite.js +318 -21
  66. package/dist/membership-executor-suite.js.map +1 -1
  67. package/dist/migration-comments-suite.d.ts +3 -0
  68. package/dist/migration-comments-suite.d.ts.map +1 -0
  69. package/dist/migration-comments-suite.js +92 -0
  70. package/dist/migration-comments-suite.js.map +1 -0
  71. package/dist/migration-comments.d.ts +3 -0
  72. package/dist/migration-comments.d.ts.map +1 -0
  73. package/dist/migration-comments.js +50 -0
  74. package/dist/migration-comments.js.map +1 -0
  75. package/dist/migration-digest-suite.d.ts +3 -0
  76. package/dist/migration-digest-suite.d.ts.map +1 -0
  77. package/dist/migration-digest-suite.js +175 -0
  78. package/dist/migration-digest-suite.js.map +1 -0
  79. package/dist/modules.d.ts +759 -0
  80. package/dist/modules.d.ts.map +1 -1
  81. package/dist/modules.js +158 -6
  82. package/dist/modules.js.map +1 -1
  83. package/dist/peer-suite.d.ts +2 -1
  84. package/dist/peer-suite.d.ts.map +1 -1
  85. package/dist/peer-suite.js +103 -1
  86. package/dist/peer-suite.js.map +1 -1
  87. package/dist/permission-suite.d.ts.map +1 -1
  88. package/dist/permission-suite.js +590 -2
  89. package/dist/permission-suite.js.map +1 -1
  90. package/dist/scope-host-suite.d.ts.map +1 -1
  91. package/dist/scope-host-suite.js +102 -1
  92. package/dist/scope-host-suite.js.map +1 -1
  93. package/dist/split-cases.d.ts +12 -0
  94. package/dist/split-cases.d.ts.map +1 -0
  95. package/dist/split-cases.js +59 -0
  96. package/dist/split-cases.js.map +1 -0
  97. package/dist/subject-erasure-suite.d.ts +6 -0
  98. package/dist/subject-erasure-suite.d.ts.map +1 -0
  99. package/dist/subject-erasure-suite.js +287 -0
  100. package/dist/subject-erasure-suite.js.map +1 -0
  101. package/dist/switch-audit-fault.d.ts +48 -0
  102. package/dist/switch-audit-fault.d.ts.map +1 -0
  103. package/dist/switch-audit-fault.js +80 -0
  104. package/dist/switch-audit-fault.js.map +1 -0
  105. package/dist/system-switch-suite.d.ts +2 -1
  106. package/dist/system-switch-suite.d.ts.map +1 -1
  107. package/dist/system-switch-suite.js +90 -0
  108. package/dist/system-switch-suite.js.map +1 -1
  109. package/dist/vertical-events-suite.d.ts +55 -0
  110. package/dist/vertical-events-suite.d.ts.map +1 -1
  111. package/dist/vertical-events-suite.js +55 -0
  112. package/dist/vertical-events-suite.js.map +1 -1
  113. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { coverage, moduleId, permissionKey, platformActorId, orgId, principalId, scopeId, tenantId, delegatedReadParams, moduleManifest, PERMISSION_KEY_MAX_LENGTH, } from '@substrat-run/contracts';
2
+ import { coverage, moduleId, permissionKey, platformActorId, orgId, principalId, scopeId, tenantId, delegatedReadParams, errorCodeOf, moduleManifest, PERMISSION_KEY_MAX_LENGTH, } from '@substrat-run/contracts';
3
3
  import { ulid } from '@substrat-run/kernel';
4
4
  import { expectRefusal } from './scope-host-suite.js';
5
5
  import { permMod, permModManifest } from './modules.js';
@@ -109,6 +109,111 @@ export function permissionContractSuite(adapterName, makeFixture) {
109
109
  afterAll(async () => {
110
110
  await fixture.cleanup();
111
111
  });
112
+ /** A complete page walk is compared with the real checker, never another grant query. */
113
+ const grantedIds = async (who, entityType) => {
114
+ const stub = await host.getScope(who, t1, s1);
115
+ const ids = [];
116
+ let cursor;
117
+ for (let pageNo = 0; pageNo < 100; pageNo++) {
118
+ const page = await stub.invoke('perm/granted-entities', {
119
+ permission: PERM_READ, entityType, limit: 2, ...(cursor ? { cursor } : {}),
120
+ });
121
+ if (page.kind !== 'ids')
122
+ return page.kind;
123
+ ids.push(...page.ids);
124
+ if (page.nextCursor === null)
125
+ return ids;
126
+ cursor = page.nextCursor;
127
+ }
128
+ throw new Error('grant walk did not terminate');
129
+ };
130
+ it('grant-scoped pages equal ctx.check over generated parent graphs', async () => {
131
+ const owner = await host.getScope(alice, t1, s1);
132
+ for (let graph = 0; graph < 4; graph++) {
133
+ const who = principalId.parse(ulid());
134
+ const folder = (n) => ({ entityType: 'folder', entityId: `gr-${graph}-f${n}` });
135
+ const item = (n) => ({ entityType: 'item', entityId: `gr-${graph}-i${n}` });
136
+ let seed = graph + 17;
137
+ const pick = (n) => {
138
+ seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
139
+ return seed % n;
140
+ };
141
+ const edges = new Set();
142
+ for (let i = 1; i < 6; i++) {
143
+ for (let j = 0; j < 2; j++) {
144
+ const parent = pick(i);
145
+ const key = `${i}:${parent}`;
146
+ if (edges.has(key))
147
+ continue;
148
+ edges.add(key);
149
+ await owner.invoke('perm/link', { child: folder(i), parent: folder(parent) });
150
+ }
151
+ }
152
+ for (let i = 0; i < 8; i++) {
153
+ await owner.invoke('perm/link', { child: item(i), parent: folder(pick(6)) });
154
+ }
155
+ for (const entity of [folder(pick(6)), folder(pick(6)), item(pick(8))]) {
156
+ await host.admin.grant(staff, {
157
+ principalId: who, permission: PERM_READ, node: { tenantId: t1, scopeId: s1 },
158
+ entity, grantedBy: alice,
159
+ });
160
+ }
161
+ await host.admin.grant(staff, {
162
+ principalId: who, permission: PERM_READ, node: { tenantId: t1, scopeId: s1 },
163
+ entity: folder(pick(6)), grantedBy: alice,
164
+ expiresAt: (await import('@substrat-run/contracts')).instant.parse('2000-01-01T00:00:00Z'),
165
+ });
166
+ const expected = [];
167
+ for (let i = 0; i < 8; i++) {
168
+ if ((await probe(who, s1, PERM_READ, item(i))).allowed)
169
+ expected.push(item(i).entityId);
170
+ }
171
+ const got = await grantedIds(who, 'item');
172
+ expect(got).not.toBe('all');
173
+ expect(got).not.toBe('incomplete');
174
+ expect(new Set(got)).toEqual(new Set(expected));
175
+ }
176
+ });
177
+ it('node roles return all, while an ungranted caller has an empty completed page', async () => {
178
+ expect(await grantedIds(alice, 'item')).toBe('all');
179
+ expect(await grantedIds(principalId.parse(ulid()), 'item')).toEqual([]);
180
+ });
181
+ it('an org entity grant is enumerated only for its live member', async () => {
182
+ const member = principalId.parse(ulid());
183
+ const other = principalId.parse(ulid());
184
+ const org = orgId.parse(ulid());
185
+ const entity = { entityType: 'item', entityId: `org-grant-${ulid()}` };
186
+ await host.admin.createOrg(staff, { id: org, tenantId: t1, slug: `grant-${ulid().toLowerCase()}`, name: 'Grant Org' });
187
+ await host.admin.addMember(staff, t1, member, org);
188
+ await host.admin.grantToOrg(staff, org, PERM_READ, { tenantId: t1, scopeId: s1 }, entity);
189
+ expect(await grantedIds(member, 'item')).toContain(entity.entityId);
190
+ expect(await grantedIds(other, 'item')).toEqual([]);
191
+ await host.admin.removeMember(staff, t1, member, org);
192
+ expect(await grantedIds(member, 'item')).toEqual([]);
193
+ });
194
+ it('a revoked entity grant leaves no id on the next read', async () => {
195
+ const who = principalId.parse(ulid());
196
+ const entity = { entityType: 'item', entityId: `revoked-${ulid()}` };
197
+ await host.admin.grant(staff, {
198
+ principalId: who, permission: PERM_READ, node: { tenantId: t1, scopeId: s1 },
199
+ entity, grantedBy: alice,
200
+ });
201
+ expect(await grantedIds(who, 'item')).toContain(entity.entityId);
202
+ const owner = await host.getScope(alice, t1, s1);
203
+ await owner.invoke('perm/revoke-grant', { principal: who, permission: PERM_READ, entity });
204
+ expect(await grantedIds(who, 'item')).toEqual([]);
205
+ });
206
+ it('an expired entity grant does not enter a grant page', async () => {
207
+ const who = principalId.parse(ulid());
208
+ const entity = { entityType: 'item', entityId: `expired-${ulid()}` };
209
+ await host.admin.grant(staff, {
210
+ principalId: who, permission: PERM_READ, node: { tenantId: t1, scopeId: s1 },
211
+ entity, grantedBy: alice,
212
+ expiresAt: (await import('@substrat-run/contracts')).instant.parse('2000-01-01T00:00:00Z'),
213
+ });
214
+ expect((await probe(who, s1, PERM_READ, entity)).allowed).toBe(false);
215
+ expect(await grantedIds(who, 'item')).toEqual([]);
216
+ });
112
217
  /**
113
218
  * §5.1's assignment bound (K-21): a principal may assign role `R` at node `N` only
114
219
  * if they already hold every permission `R` carries at `N`.
@@ -228,6 +333,81 @@ export function permissionContractSuite(adapterName, makeFixture) {
228
333
  expect((await probe(assignee, s1, PERM_READ)).allowed).toBe(false);
229
334
  });
230
335
  });
336
+ /**
337
+ * The scope's role roster and the two bounded writes over it (#1150) — what a dashboard
338
+ * managing an installed vertical's members reads and calls. Each write is one scope
339
+ * task: the bound over every role it touches, then all of its writes, or none.
340
+ */
341
+ describe('the scope role roster, and bounded change and removal', () => {
342
+ const fresh = () => principalId.parse(ulid());
343
+ const rolesOf = async (who, scope = s1) => (await host.listScopeRoleHolders(t1, scope)).filter((h) => h.principal === who).map((h) => h.roleKey).sort();
344
+ const seat = async (who, roleKey) => expect((await host.assignScopeRoleBounded(t1, s1, alice, who, roleKey)).covered).toBe(true);
345
+ it('lists who holds which scope role, at this scope only, and drops a revoked one', async () => {
346
+ const x = fresh();
347
+ await seat(x, 'tech');
348
+ expect(await rolesOf(x)).toEqual(['tech']);
349
+ expect(await rolesOf(x, s2)).toEqual([]);
350
+ expect((await host.revokeScopeRolesBounded(t1, s1, alice, x)).revoked).toEqual(['tech']);
351
+ expect(await rolesOf(x)).toEqual([]);
352
+ });
353
+ it('changes a role in one write when the caller covers both', async () => {
354
+ const x = fresh();
355
+ await seat(x, 'tech');
356
+ expect(await host.changeScopeRoleBounded(t1, s1, alice, x, 'tech', 'admin')).toEqual({ covered: true, missing: [] });
357
+ expect(await rolesOf(x)).toEqual(['admin']);
358
+ expect((await probe(x, s1, PERM_USE)).allowed).toBe(true);
359
+ });
360
+ /** Escalation: bob holds `tech` only, so he can neither promote someone nor demote an admin. */
361
+ it('refuses a change beyond the caller, leaving the old role in place', async () => {
362
+ const x = fresh();
363
+ await seat(x, 'tech');
364
+ expect(await host.changeScopeRoleBounded(t1, s1, bob, x, 'tech', 'admin')).toEqual({ covered: false, missing: [PERM_USE] });
365
+ expect(await rolesOf(x)).toEqual(['tech']);
366
+ const y = fresh();
367
+ await seat(y, 'admin');
368
+ expect(await host.changeScopeRoleBounded(t1, s1, bob, y, 'admin', 'tech')).toEqual({ covered: false, missing: [PERM_USE] });
369
+ expect(await rolesOf(y)).toEqual(['admin']);
370
+ expect((await probe(y, s1, PERM_USE)).allowed).toBe(true);
371
+ });
372
+ it('writes nothing for an unknown target role, or a role the principal does not hold', async () => {
373
+ const x = fresh();
374
+ await seat(x, 'tech');
375
+ await expectRefusal(host.changeScopeRoleBounded(t1, s1, alice, x, 'tech', 'no-such-role'), 'not_found', /no such role/);
376
+ await expectRefusal(host.changeScopeRoleBounded(t1, s1, alice, x, 'admin', 'tech'), 'conflict', /does not hold/);
377
+ expect(await rolesOf(x)).toEqual(['tech']);
378
+ });
379
+ it('removes every scope role a principal holds, bounded by the caller over each', async () => {
380
+ const x = fresh();
381
+ await seat(x, 'tech');
382
+ await seat(x, 'admin');
383
+ // bob covers `tech` but not `admin`: nothing is taken, not even the role he covers.
384
+ expect(await host.revokeScopeRolesBounded(t1, s1, bob, x)).toEqual({
385
+ coverage: { covered: false, missing: [PERM_USE] },
386
+ revoked: [],
387
+ });
388
+ expect(await rolesOf(x)).toEqual(['admin', 'tech']);
389
+ const removed = await host.revokeScopeRolesBounded(t1, s1, alice, x);
390
+ expect(removed.coverage.covered).toBe(true);
391
+ expect([...removed.revoked].sort()).toEqual(['admin', 'tech']);
392
+ expect(await rolesOf(x)).toEqual([]);
393
+ expect((await probe(x, s1, PERM_READ)).allowed).toBe(false);
394
+ });
395
+ it('lets a caller remove a role they hold themselves', async () => {
396
+ const x = fresh();
397
+ await seat(x, 'tech');
398
+ expect((await host.revokeScopeRolesBounded(t1, s1, bob, x)).revoked).toEqual(['tech']);
399
+ expect((await probe(x, s1, PERM_READ)).allowed).toBe(false);
400
+ });
401
+ it('refuses another tenant’s scope as one that does not exist, writing nothing', async () => {
402
+ const x = fresh();
403
+ await seat(x, 'tech');
404
+ const other = tenantId.parse(ulid());
405
+ await expectRefusal(host.listScopeRoleHolders(other, s1), 'not_found', /unknown scope/);
406
+ await expectRefusal(host.changeScopeRoleBounded(other, s1, alice, x, 'tech', 'admin'), 'not_found', /unknown scope/);
407
+ await expectRefusal(host.revokeScopeRolesBounded(other, s1, alice, x), 'not_found', /unknown scope/);
408
+ expect(await rolesOf(x)).toEqual(['tech']);
409
+ });
410
+ });
231
411
  });
232
412
  // -- runtime delegation: ctx.grant / ctx.revoke ---------------------------
233
413
  // The verb user-initiated sharing needs. Every OTHER entity-narrowed grant in
@@ -267,6 +447,321 @@ export function permissionContractSuite(adapterName, makeFixture) {
267
447
  });
268
448
  expect((await probe(frank, s1, PERM_USE, { entityType: 'item', entityId: 'i1' })).allowed).toBe(false);
269
449
  });
450
+ /**
451
+ * K-21 (#2071): a revoke tombstones rather than deletes, so "taken back" and "never held"
452
+ * are two facts. A declared entity-grant shape's top-up reads that difference: it must
453
+ * never hand a person back a key someone revoked from them.
454
+ */
455
+ describe('a revoke leaves a tombstone, and a re-grant clears it (K-21, #2071)', () => {
456
+ const i3 = { entityType: 'item', entityId: 'i3' };
457
+ const rows = async () => (await (await host.getScope(alice, t1, s1)).invoke('perm/grant-rows', { principal: frank })).filter((r) => r.object === 'item:i3');
458
+ const unshare = async () => (await host.getScope(alice, t1, s1)).invoke('perm/unshare', { principal: frank, permission: PERM_USE, entity: i3 });
459
+ it('the revoked row stays, tombstoned, and grants nothing', async () => {
460
+ await share(alice, PERM_USE, i3);
461
+ expect(await rows()).toEqual([{ relation: `granted:${PERM_USE}`, object: 'item:i3', revokedAt: null, expiresAt: null }]);
462
+ await unshare();
463
+ const [row] = await rows();
464
+ expect(row?.revokedAt).toEqual(expect.any(String));
465
+ expect((await probe(frank, s1, PERM_USE, i3)).allowed).toBe(false);
466
+ });
467
+ it('a repeat revoke changes nothing: the first tombstone keeps its instant', async () => {
468
+ const [before] = await rows();
469
+ await unshare();
470
+ expect(await rows()).toEqual([before]);
471
+ });
472
+ it('a re-grant clears the tombstone and grants again', async () => {
473
+ await share(alice, PERM_USE, i3);
474
+ expect(await rows()).toEqual([{ relation: `granted:${PERM_USE}`, object: 'item:i3', revokedAt: null, expiresAt: null }]);
475
+ expect((await probe(frank, s1, PERM_USE, i3)).allowed).toBe(true);
476
+ });
477
+ it('a grant over a live row leaves it as it was', async () => {
478
+ const before = await rows();
479
+ await share(alice, PERM_USE, i3);
480
+ expect(await rows()).toEqual(before);
481
+ });
482
+ it('a copy of the scope carries the tombstone, and it still grants nothing there', async () => {
483
+ await unshare();
484
+ const s3 = scopeId.parse(ulid());
485
+ await host.importScope(staff, { tenantId: t1, scopeId: s3, vertical: 'perm-vertical' }, await host.admin.exportScope(staff, t1, s1));
486
+ expect((await probe(frank, s3, PERM_USE, i3)).allowed).toBe(false);
487
+ const [carried] = (await (await host.getScope(alice, t1, s3)).invoke('perm/grant-rows', {
488
+ principal: frank,
489
+ })).filter((r) => r.object === 'item:i3');
490
+ expect(carried?.revokedAt).toEqual(expect.any(String));
491
+ });
492
+ });
493
+ });
494
+ /**
495
+ * #2071: a vertical's declared entity-grant SHAPE reaches the people who already held it
496
+ * when it grows. The kernel's own edges are in `kernel/test/entity-grant-shape.test.ts`;
497
+ * this is the same reconcile end to end on each adapter, through the admin verbs a
498
+ * vertical calls, with the checker as the judge.
499
+ */
500
+ describe('a declared entity-grant shape reaches existing holders when it grows (#2071)', () => {
501
+ const s4 = scopeId.parse(ulid());
502
+ const OLD = [PERM_READ];
503
+ const GROWN = [PERM_READ, PERM_USE];
504
+ const node = { tenantId: t1, scopeId: s4 };
505
+ const desk = (id) => ({ entityType: 'desk', entityId: id });
506
+ const hana = principalId.parse(ulid());
507
+ const ivo = principalId.parse(ulid());
508
+ const jon = principalId.parse(ulid());
509
+ const kim = principalId.parse(ulid());
510
+ const shapeTo = (who, id, permissions = OLD) => host.admin.grantEntityShape(staff, { principalId: who, node, entity: desk(id), permissions, grantedBy: alice });
511
+ const reconcile = (permissions = GROWN, batch) => host.admin.reconcileEntityGrantShapes(staff, node, [{ entityType: 'desk', permissions, bootstrap: true }], batch === undefined ? undefined : { batch });
512
+ const can = async (who, permission, id) => (await probe(who, s4, permission, desk(id))).allowed;
513
+ const toppedUp = async (id) => (await host.getScope(alice, t1, s4)).invoke('perm/topped-up', desk(id));
514
+ beforeAll(async () => {
515
+ await host.provisionScope(staff, { tenantId: t1, scopeId: s4, vertical: 'perm-vertical' });
516
+ await host.admin.activateScope(staff, t1, s4);
517
+ });
518
+ it('the shape grant gives every key of the shape, and is audited', async () => {
519
+ await shapeTo(hana, 'd1');
520
+ expect(await can(hana, PERM_READ, 'd1')).toBe(true);
521
+ expect(await can(hana, PERM_USE, 'd1')).toBe(false);
522
+ const log = await host.admin.auditLog(staff, { tenantId: t1 });
523
+ expect(log.some((e) => e.action === 'grantEntityShape')).toBe(true);
524
+ });
525
+ it('a principal granted before a key was added holds it after the reconcile', async () => {
526
+ expect(await reconcile()).toEqual({ toppedUp: 1, retired: 0 });
527
+ expect(await can(hana, PERM_USE, 'd1')).toBe(true);
528
+ // ...on that entity only: the shape is entity-narrowed, and so is the top-up.
529
+ expect(await can(hana, PERM_USE, 'd2')).toBe(false);
530
+ });
531
+ it('the top-up is an event on the entity, written by the kernel, and the reconcile is audited', async () => {
532
+ const [event, ...rest] = await toppedUp('d1');
533
+ expect(rest).toEqual([]);
534
+ expect(event).toEqual({
535
+ payload: { entity: desk('d1'), principal: hana, added: [PERM_USE] },
536
+ actor: { system: '@substrat-run/kernel' },
537
+ // No operation ran and no check passed: both say so rather than inventing one.
538
+ operation: null,
539
+ authorization: null,
540
+ });
541
+ const log = await host.admin.auditLog(staff, { tenantId: t1 });
542
+ expect(log.filter((e) => e.action === 'reconcileEntityGrantShapes')).toHaveLength(1);
543
+ });
544
+ it('a re-run is a no-op: nobody topped up, no event, no audit row', async () => {
545
+ expect(await reconcile()).toEqual({ toppedUp: 0, retired: 0 });
546
+ expect(await toppedUp('d1')).toHaveLength(1);
547
+ const log = await host.admin.auditLog(staff, { tenantId: t1 });
548
+ expect(log.filter((e) => e.action === 'reconcileEntityGrantShapes')).toHaveLength(1);
549
+ });
550
+ it('a key revoked from the holder (ctx.revoke) is not re-granted by the next top-up', async () => {
551
+ await shapeTo(ivo, 'd3');
552
+ await reconcile();
553
+ await (await host.getScope(alice, t1, s4)).invoke('perm/unshare', { principal: ivo, permission: PERM_USE, entity: desk('d3') });
554
+ expect(await can(ivo, PERM_USE, 'd3')).toBe(false);
555
+ expect(await reconcile([...GROWN, PERM_ADMIN])).toMatchObject({ toppedUp: expect.any(Number) });
556
+ expect(await can(ivo, PERM_USE, 'd3')).toBe(false);
557
+ // The positive twin: the key the shape gained this time DID arrive for the same person.
558
+ expect(await can(ivo, PERM_ADMIN, 'd3')).toBe(true);
559
+ });
560
+ it('someone ctx.granted one key of the shape is not a holder, and is not escalated to all of it', async () => {
561
+ await (await host.getScope(alice, t1, s4)).invoke('perm/share', { principal: jon, permission: PERM_READ, entity: desk('d1') });
562
+ await reconcile([...GROWN, PERM_ADMIN]);
563
+ expect(await can(jon, PERM_READ, 'd1')).toBe(true);
564
+ expect(await can(jon, PERM_USE, 'd1')).toBe(false);
565
+ });
566
+ it('passes are bounded: a small batch still reaches every holder, each exactly once', async () => {
567
+ const people = [0, 1, 2, 3, 4].map(() => principalId.parse(ulid()));
568
+ for (const [i, p] of people.entries())
569
+ await shapeTo(p, `b${i}`, OLD);
570
+ expect(await reconcile([...GROWN, PERM_ADMIN], 2)).toEqual({ toppedUp: 5, retired: 0 });
571
+ for (const [i, p] of people.entries()) {
572
+ expect(await can(p, PERM_ADMIN, `b${i}`)).toBe(true);
573
+ expect(await toppedUp(`b${i}`)).toHaveLength(1);
574
+ }
575
+ });
576
+ it('the shape grant brings back a key a revoke tombstoned — it is the explicit grant', async () => {
577
+ await shapeTo(kim, 'd9', GROWN);
578
+ await (await host.getScope(alice, t1, s4)).invoke('perm/unshare', { principal: kim, permission: PERM_USE, entity: desk('d9') });
579
+ expect(await can(kim, PERM_USE, 'd9')).toBe(false);
580
+ await shapeTo(kim, 'd9', GROWN);
581
+ expect(await can(kim, PERM_USE, 'd9')).toBe(true);
582
+ });
583
+ it('the backfill marks a person on their OWN record only — a one-key grant delegated on another’s is not', async () => {
584
+ const seat = (id) => ({ entityType: 'seat', entityId: id });
585
+ const shape = (permissions) => [{ entityType: 'seat', permissions, bootstrap: true, holder: 'self' }];
586
+ const lena = principalId.parse(ulid());
587
+ const max = principalId.parse(ulid());
588
+ // Both granted before markers existed: lena on her own seat, max on lena's (a delegation).
589
+ for (const who of [lena, max]) {
590
+ await host.admin.grant(staff, { principalId: who, permission: PERM_READ, node, entity: seat(lena), grantedBy: alice });
591
+ }
592
+ await host.admin.reconcileEntityGrantShapes(staff, node, shape([PERM_READ]));
593
+ await host.admin.reconcileEntityGrantShapes(staff, node, shape([PERM_READ, PERM_USE]));
594
+ expect((await probe(lena, s4, PERM_USE, seat(lena))).allowed).toBe(true);
595
+ expect((await probe(max, s4, PERM_USE, seat(lena))).allowed).toBe(false);
596
+ });
597
+ /**
598
+ * #2083: `holder: 'grantee'`, for a portal record that names no principal. Everyone holding a
599
+ * key of the shape there is marked, and from that reconcile on `ctx.grant` cannot give those
600
+ * keys on that type — so "holds one" keeps meaning "was given the shape".
601
+ */
602
+ it("a 'grantee' shape reaches people granted before markers, and ctx.grant can no longer give its keys", async () => {
603
+ const room = (id) => ({ entityType: 'room', entityId: id });
604
+ const shape = (permissions) => [{ entityType: 'room', permissions, bootstrap: true, holder: 'grantee' }];
605
+ const nils = principalId.parse(ulid());
606
+ const ola = principalId.parse(ulid());
607
+ const pia = principalId.parse(ulid());
608
+ // Two people on one portal record, granted key by key before the declaration.
609
+ for (const who of [nils, ola]) {
610
+ await host.admin.grant(staff, { principalId: who, permission: PERM_READ, node, entity: room('r1'), grantedBy: alice });
611
+ }
612
+ await host.admin.reconcileEntityGrantShapes(staff, node, shape([PERM_READ]));
613
+ const share = async (permission) => (await host.getScope(alice, t1, s4)).invoke('perm/share', { principal: pia, permission, entity: room('r2') }).then(() => 'granted', errorCodeOf);
614
+ expect(await share(PERM_READ)).toBe('permission_denied');
615
+ // The twin: a key outside the shape, on the same type, is still delegable.
616
+ expect(await share(PERM_USE)).toBe('granted');
617
+ await host.admin.reconcileEntityGrantShapes(staff, node, shape([PERM_READ, PERM_ADMIN]));
618
+ for (const who of [nils, ola])
619
+ expect((await probe(who, s4, PERM_ADMIN, room('r1'))).allowed).toBe(true);
620
+ // ...and the person ctx.granted the other key is no holder.
621
+ expect((await probe(pia, s4, PERM_ADMIN, room('r2'))).allowed).toBe(false);
622
+ });
623
+ it('a grantee retirement finds live retired-key holders, excludes tombstoned keys, and never tops up the retired key', async () => {
624
+ const scope = scopeId.parse(ulid());
625
+ const at = { tenantId: t1, scopeId: scope };
626
+ const room = (id) => ({ entityType: 'retiringRoom', entityId: id });
627
+ const oldLive = principalId.parse(ulid());
628
+ const oldDead = principalId.parse(ulid());
629
+ const current = principalId.parse(ulid());
630
+ const marked = principalId.parse(ulid());
631
+ await host.provisionScope(staff, { ...at, vertical: 'perm-vertical' });
632
+ await host.admin.activateScope(staff, t1, scope);
633
+ for (const [who, id] of [[oldLive, 'old-live'], [oldDead, 'old-dead']]) {
634
+ await host.admin.grant(staff, { principalId: who, permission: PERM_READ, node: at, entity: room(id), grantedBy: alice });
635
+ }
636
+ await (await host.getScope(alice, t1, scope)).invoke('perm/unshare', { principal: oldDead, permission: PERM_READ, entity: room('old-dead') });
637
+ await host.admin.grant(staff, { principalId: current, permission: PERM_USE, node: at, entity: room('current'), grantedBy: alice });
638
+ await host.admin.grantEntityShape(staff, { principalId: marked, node: at, entity: room('marked'), permissions: [PERM_READ], grantedBy: alice });
639
+ const shape = [{ entityType: 'retiringRoom', permissions: [PERM_USE, PERM_ADMIN], retired: [PERM_READ], bootstrap: true, holder: 'grantee' }];
640
+ expect(await host.admin.reconcileEntityGrantShapes(staff, at, shape)).toEqual({ toppedUp: 3, retired: 2 });
641
+ const marker = async (who, id) => (await host.getScope(alice, t1, scope)).invoke('perm/shape-marker', {
642
+ principal: who,
643
+ entity: room(id),
644
+ });
645
+ expect((await probe(oldDead, scope, PERM_ADMIN, room('old-dead'))).allowed).toBe(false);
646
+ expect((await probe(oldDead, scope, PERM_USE, room('old-dead'))).allowed).toBe(false);
647
+ expect(await marker(oldDead, 'old-dead')).toEqual([]);
648
+ // The live retired-key holder is marked before retirement; the tombstoned-key twin
649
+ // stays unmarked. Previously marked holders retain their markers.
650
+ expect((await probe(oldDead, scope, PERM_READ, room('old-dead'))).allowed).toBe(false);
651
+ for (const [who, id] of [[oldLive, 'old-live'], [current, 'current'], [marked, 'marked']]) {
652
+ expect((await probe(who, scope, PERM_ADMIN, room(id))).allowed).toBe(true);
653
+ expect((await probe(who, scope, PERM_USE, room(id))).allowed).toBe(true);
654
+ expect((await probe(who, scope, PERM_READ, room(id))).allowed).toBe(false);
655
+ expect(await marker(who, id)).toEqual([{ revoked_at: null }]);
656
+ }
657
+ expect(await host.admin.reconcileEntityGrantShapes(staff, at, shape)).toEqual({ toppedUp: 0, retired: 0 });
658
+ for (const [who, id] of [[oldLive, 'old-live'], [marked, 'marked']]) {
659
+ expect((await probe(who, scope, PERM_READ, room(id))).allowed).toBe(false);
660
+ }
661
+ });
662
+ it('after a grantee declaration, direct writers refuse current and retired keys; shape grants still work', async () => {
663
+ const scope = scopeId.parse(ulid());
664
+ const at = { tenantId: t1, scopeId: scope };
665
+ const room = (id) => ({ entityType: 'guardedRoom', entityId: id });
666
+ const [later, shaped] = [principalId.parse(ulid()), principalId.parse(ulid())];
667
+ await host.provisionScope(staff, { ...at, vertical: 'perm-vertical' });
668
+ await host.admin.activateScope(staff, t1, scope);
669
+ await host.admin.reconcileEntityGrantShapes(staff, at, [
670
+ { entityType: 'guardedRoom', permissions: [PERM_USE], retired: [PERM_READ], bootstrap: true, holder: 'grantee' },
671
+ ]);
672
+ for (const permission of [PERM_USE, PERM_READ]) {
673
+ await expectRefusal(host.admin.grant(staff, { principalId: later, permission, node: at, entity: room('r1'), grantedBy: alice }), 'permission_denied');
674
+ await expectRefusal(host.admin.grantToOrg(staff, acme, permission, at, room('r1')), 'permission_denied');
675
+ await expectRefusal((await host.getScope(alice, t1, scope)).invoke('perm/share', { principal: later, permission, entity: room('r1') }), 'permission_denied');
676
+ }
677
+ expect((await probe(later, scope, PERM_USE, room('r1'))).allowed).toBe(false);
678
+ // Protection is entity-type narrow; an unrelated key on the same type and a scoped
679
+ // grant of the same key remain valid, while the shape grant writes its marker atomically.
680
+ await host.admin.grant(staff, { principalId: later, permission: PERM_ADMIN, node: at, entity: room('r1'), grantedBy: alice });
681
+ await host.admin.grant(staff, { principalId: later, permission: PERM_USE, node: at, grantedBy: alice });
682
+ await host.admin.grantEntityShape(staff, { principalId: shaped, node: at, entity: room('r2'), permissions: [PERM_USE], grantedBy: alice });
683
+ expect((await probe(shaped, scope, PERM_USE, room('r2'))).allowed).toBe(true);
684
+ await host.admin.reconcileEntityGrantShapes(staff, at, [
685
+ { entityType: 'guardedRoom', permissions: [PERM_USE, PERM_ADMIN], retired: [PERM_READ], bootstrap: true, holder: 'grantee' },
686
+ ]);
687
+ expect((await probe(shaped, scope, PERM_ADMIN, room('r2'))).allowed).toBe(true);
688
+ expect((await probe(shaped, scope, PERM_READ, room('r2'))).allowed).toBe(false);
689
+ });
690
+ it.each([0, -1, 1.5, 5001])('refuses batch %s with validation_failed, before touching the scope', async (batch) => {
691
+ const err = await reconcile(GROWN, batch).then(() => undefined, (e) => e);
692
+ expect(errorCodeOf(err)).toBe('validation_failed');
693
+ });
694
+ it('a sharing shape (no `bootstrap`) passed in is never reconciled, even for a marked holder', async () => {
695
+ await shapeTo(kim, 's1', OLD);
696
+ expect(await host.admin.reconcileEntityGrantShapes(staff, node, [{ entityType: 'desk', permissions: GROWN }])).toEqual({
697
+ toppedUp: 0,
698
+ retired: 0,
699
+ });
700
+ expect(await can(kim, PERM_USE, 's1')).toBe(false);
701
+ });
702
+ });
703
+ /**
704
+ * #2082: a key the declared shape RETIRES is taken back from every holder, end to end on each
705
+ * adapter. The kernel's own edges are in `kernel/test/entity-grant-shape.test.ts`.
706
+ */
707
+ describe('a key a declared shape retires is taken back from its holders (#2082)', () => {
708
+ const s5 = scopeId.parse(ulid());
709
+ const node = { tenantId: t1, scopeId: s5 };
710
+ const desk = (id) => ({ entityType: 'desk', entityId: id });
711
+ const SHAPE = [PERM_READ, PERM_USE];
712
+ const hana = principalId.parse(ulid());
713
+ const jon = principalId.parse(ulid());
714
+ const shapeTo = (who, id) => host.admin.grantEntityShape(staff, { principalId: who, node, entity: desk(id), permissions: SHAPE, grantedBy: alice });
715
+ const retire = (batch) => host.admin.reconcileEntityGrantShapes(staff, node, [{ entityType: 'desk', permissions: [PERM_READ], bootstrap: true, retired: [PERM_USE] }], batch === undefined ? undefined : { batch });
716
+ const can = async (who, permission, id) => (await probe(who, s5, permission, desk(id))).allowed;
717
+ const scope = () => host.getScope(alice, t1, s5);
718
+ const retiredOn = async (id) => (await scope()).invoke('perm/topped-up', {
719
+ ...desk(id),
720
+ type: 'entity.grants-retired',
721
+ });
722
+ beforeAll(async () => {
723
+ await host.provisionScope(staff, { tenantId: t1, scopeId: s5, vertical: 'perm-vertical' });
724
+ await host.admin.activateScope(staff, t1, s5);
725
+ await shapeTo(hana, 'd1');
726
+ // alice holds the shape too, and perm:use through her tenant role as well.
727
+ await shapeTo(alice, 'd2');
728
+ // jon is not a holder: ctx.granted the key on hana's desk.
729
+ await (await scope()).invoke('perm/share', { principal: jon, permission: PERM_USE, entity: desk('d1') });
730
+ });
731
+ it('the holder loses the key after the reconcile, and keeps the rest of the shape', async () => {
732
+ expect(await can(hana, PERM_USE, 'd1')).toBe(true);
733
+ expect(await retire()).toEqual({ toppedUp: 0, retired: 2 });
734
+ expect(await can(hana, PERM_USE, 'd1')).toBe(false);
735
+ expect(await can(hana, PERM_READ, 'd1')).toBe(true);
736
+ });
737
+ it('...while the key held another way stays: a non-holder’s direct grant, and a role', async () => {
738
+ expect(await can(jon, PERM_USE, 'd1')).toBe(true);
739
+ expect(await can(alice, PERM_USE, 'd2')).toBe(true);
740
+ });
741
+ it('the row is tombstoned, not deleted, and the retirement is a kernel event and an audit row', async () => {
742
+ const [row] = (await (await scope()).invoke('perm/grant-rows', { principal: hana })).filter((r) => r.relation === `granted:${PERM_USE}`);
743
+ expect(row?.revokedAt).toEqual(expect.any(String));
744
+ expect(await retiredOn('d1')).toEqual([
745
+ { payload: { entity: desk('d1'), principal: hana, removed: [PERM_USE] }, actor: { system: '@substrat-run/kernel' }, operation: null, authorization: null },
746
+ ]);
747
+ const log = await host.admin.auditLog(staff, { tenantId: t1 });
748
+ expect(log.filter((e) => e.action === 'reconcileEntityGrantShapes' && e.scopeId === s5).map((e) => e.after.retired)).toEqual([2]);
749
+ });
750
+ it('runs once: a re-run retires nobody, and the key granted back afterwards stays', async () => {
751
+ await (await scope()).invoke('perm/share', { principal: hana, permission: PERM_USE, entity: desk('d1') });
752
+ expect(await retire()).toEqual({ toppedUp: 0, retired: 0 });
753
+ expect(await can(hana, PERM_USE, 'd1')).toBe(true);
754
+ expect(await retiredOn('d1')).toHaveLength(1);
755
+ });
756
+ it('the shape shrunk WITHOUT `retired` changes nothing for a holder', async () => {
757
+ const kim = principalId.parse(ulid());
758
+ await shapeTo(kim, 'd7');
759
+ expect(await host.admin.reconcileEntityGrantShapes(staff, node, [{ entityType: 'desk', permissions: [PERM_READ], bootstrap: true }])).toEqual({
760
+ toppedUp: 0,
761
+ retired: 0,
762
+ });
763
+ expect(await can(kim, PERM_USE, 'd7')).toBe(true);
764
+ });
270
765
  });
271
766
  it('denies by default with the checked permission and node', async () => {
272
767
  const d = await probe(principalId.parse(ulid()), s1, PERM_READ);
@@ -757,7 +1252,8 @@ export function permissionContractSuite(adapterName, makeFixture) {
757
1252
  const mine = out.deliveries.filter((d) => d.permission === PERM_READ);
758
1253
  expect(mine).toHaveLength(1);
759
1254
  expect(mine[0].error).toBeNull();
760
- expect(out.log.filter((r) => r.permission === PERM_READ).map((r) => r.allowed)).toEqual([1]);
1255
+ expect(out.log.filter((r) => r.permission === PERM_READ).map((r) => [r.allowed, r.grant_read_kind]))
1256
+ .toEqual([[1, 'all']]);
761
1257
  });
762
1258
  it('caught and ignored: no allow, and ctx.grant writes no tuple under the cast key', async () => {
763
1259
  const out = await requestCheck(CAST_KEY, true);
@@ -1437,6 +1933,98 @@ export function permissionContractSuite(adapterName, makeFixture) {
1437
1933
  await expect(host.admin.recordOwnerTransfer(operator, { ...base, phase: 'moved' })).rejects.toThrow();
1438
1934
  await expect(host.admin.recordOwnerTransfer(operator, { ...base, phase: 'intent', actor: staff })).rejects.toThrow();
1439
1935
  expect((await host.admin.auditLog(staff, { tenantId: t1 })).filter((r) => r.action === 'transferOwner')).toHaveLength(2);
1936
+ // #2064: the platform sweep closes an intent with no outcome as `unknown`, saying why. Its
1937
+ // reason is required, as a refusal's is.
1938
+ await expect(host.admin.recordOwnerTransfer(operator, { ...base, phase: 'unknown' })).rejects.toThrow();
1939
+ await host.admin.recordOwnerTransfer(operator, { ...base, phase: 'unknown', error: 'no outcome was recorded' });
1940
+ const settled = (await host.admin.auditLog(staff, { tenantId: t1, action: 'transferOwner' })).at(-1);
1941
+ expect(settled?.after).toEqual({ phase: 'unknown', error: 'no outcome was recorded', operationId, from, to });
1942
+ });
1943
+ it('settles an intent with no outcome ONCE, with its ops-failure row in the same unit (#2064)', async () => {
1944
+ // Held on both adapters: the settle's check-then-write is one transaction on each, which is
1945
+ // the whole reason two racing sweeps cannot both write `unknown`.
1946
+ const from = principalId.parse(ulid());
1947
+ const to = principalId.parse(ulid());
1948
+ const opsOf = async (operationId) => (await host.admin.auditLog(staff, { tenantId: t1, action: 'transferOwner' }))
1949
+ .filter((r) => r.after.operationId === operationId)
1950
+ .map((r) => r.after.phase);
1951
+ const intentOf = async (operationId) => {
1952
+ await host.admin.recordOwnerTransfer(staff, { tenantId: t1, scopeId: s1, operationId, from, to, phase: 'intent' });
1953
+ return (await host.admin.auditLog(staff, { tenantId: t1, action: 'transferOwner', order: 'desc', limit: 1 }))[0].id;
1954
+ };
1955
+ // Two settles racing on one orphan: exactly one writes, and one ops-failure row is left.
1956
+ const orphan = ulid();
1957
+ const intentId = await intentOf(orphan);
1958
+ const raced = await Promise.all([
1959
+ host.admin.settleUnrecordedOutcome(staff, { intentId, error: 'no outcome was recorded' }),
1960
+ host.admin.settleUnrecordedOutcome(staff, { intentId, error: 'no outcome was recorded' }),
1961
+ ]);
1962
+ expect(raced.sort()).toEqual([false, true]);
1963
+ expect(await opsOf(orphan)).toEqual(['intent', 'unknown']);
1964
+ const settled = (await host.admin.auditLog(staff, { tenantId: t1, action: 'transferOwner', order: 'desc', limit: 1 }))[0];
1965
+ expect(settled.after).toEqual({ phase: 'unknown', error: 'no outcome was recorded', operationId: orphan, from, to });
1966
+ const failures = (await host.admin.listOpsFailures(staff, { tenantId: t1, operation: 'audit.transferOwner' }))
1967
+ .filter((f) => f.message.includes(orphan));
1968
+ expect(failures.map((f) => [f.stage, f.scopeId])).toEqual([['outcome-unknown', s1]]);
1969
+ // A real outcome that lands later is still recorded, as the later row, and supersedes it.
1970
+ await host.admin.recordOwnerTransfer(staff, { tenantId: t1, scopeId: s1, operationId: orphan, from, to, phase: 'refused', error: 'late' });
1971
+ expect(await opsOf(orphan)).toEqual(['intent', 'unknown', 'refused']);
1972
+ expect(await host.admin.settleUnrecordedOutcome(staff, { intentId, error: 'again' })).toBe(false);
1973
+ // An intent whose outcome landed first is not settled — the twin of the orphan above.
1974
+ const answered = ulid();
1975
+ const answeredId = await intentOf(answered);
1976
+ await host.admin.recordOwnerTransfer(staff, { tenantId: t1, scopeId: s1, operationId: answered, from, to, phase: 'applied', outcome: 'transferred', fromRevoked: true });
1977
+ expect(await host.admin.settleUnrecordedOutcome(staff, { intentId: answeredId, error: 'no outcome' })).toBe(false);
1978
+ expect(await opsOf(answered)).toEqual(['intent', 'applied']);
1979
+ // Only an audited-change intent can be settled: an outcome row's id, or no row, is not_found.
1980
+ const outcomeId = (await host.admin.auditLog(staff, { tenantId: t1, action: 'transferOwner', order: 'desc', limit: 1 }))[0].id;
1981
+ await expect(host.admin.settleUnrecordedOutcome(staff, { intentId: outcomeId, error: 'x' })).rejects.toThrow(/no audited-change intent/);
1982
+ await expect(host.admin.settleUnrecordedOutcome(staff, { intentId: ulid(), error: 'x' })).rejects.toThrow(/no audited-change intent/);
1983
+ });
1984
+ it('the settle checks the WHOLE operation: the same id answered in another tenant or scope does not settle it (#2064)', async () => {
1985
+ const from = principalId.parse(ulid());
1986
+ const to = principalId.parse(ulid());
1987
+ const otherTenant = tenantId.parse(ulid());
1988
+ const settleOf = async (operationId) => {
1989
+ await host.admin.recordOwnerTransfer(staff, { tenantId: t1, scopeId: s1, operationId, from, to, phase: 'intent' });
1990
+ const [intent] = await host.admin.auditLog(staff, { tenantId: t1, action: 'transferOwner', order: 'desc', limit: 1 });
1991
+ return () => host.admin.settleUnrecordedOutcome(staff, { intentId: intent.id, error: 'no outcome was recorded' });
1992
+ };
1993
+ const failuresOf = async (operationId) => (await host.admin.listOpsFailures(staff, { tenantId: t1, operation: 'audit.transferOwner' })).filter((f) => f.reference === operationId);
1994
+ // The orphan's id, answered in another tenant AND in another scope: neither is its outcome.
1995
+ const collided = ulid();
1996
+ const settle = await settleOf(collided);
1997
+ const answered = { operationId: collided, from, to, phase: 'applied', outcome: 'transferred', fromRevoked: true };
1998
+ await host.admin.recordOwnerTransfer(staff, { ...answered, tenantId: otherTenant, scopeId: s1 });
1999
+ await host.admin.recordOwnerTransfer(staff, { ...answered, tenantId: t1, scopeId: s2 });
2000
+ expect(await settle()).toBe(true);
2001
+ expect((await failuresOf(collided)).map((f) => [f.stage, f.scopeId])).toEqual([['outcome-unknown', s1]]);
2002
+ // The twin: answered under its own whole key, the orphan is not one, and nothing is written.
2003
+ const own = ulid();
2004
+ const settleOwn = await settleOf(own);
2005
+ await host.admin.recordOwnerTransfer(staff, { ...answered, operationId: own, tenantId: t1, scopeId: s1 });
2006
+ expect(await settleOwn()).toBe(false);
2007
+ expect(await failuresOf(own)).toEqual([]);
2008
+ // An id that is not well-formed text never reaches the log: matched in SQL and in memory,
2009
+ // it would not read the same in both. Its well-formed twin is the case above.
2010
+ await expect(host.admin.recordOwnerTransfer(staff, { tenantId: t1, scopeId: s1, operationId: 'op\uD800', from, to, phase: 'intent' })).rejects.toThrow(/well-formed/);
2011
+ });
2012
+ it('reads exactly the audited operations asked about, in batches a Durable Object can bind (#2064)', async () => {
2013
+ const from = principalId.parse(ulid());
2014
+ const to = principalId.parse(ulid());
2015
+ // More operations than one batch binds, so a hosted read must split them.
2016
+ const ops = Array.from({ length: 120 }, () => ulid());
2017
+ for (const operationId of ops) {
2018
+ await host.admin.recordOwnerTransfer(staff, { tenantId: t1, scopeId: s1, operationId, from, to, phase: 'intent' });
2019
+ }
2020
+ await host.admin.recordOwnerTransfer(staff, { tenantId: t1, scopeId: s1, operationId: ops[0], from, to, phase: 'refused', error: 'no' });
2021
+ const refs = ops.map((operationId) => ({ action: 'transferOwner', operationId, tenantId: t1, scopeId: s1 }));
2022
+ const rows = await host.admin.auditedOperations(staff, refs);
2023
+ expect(rows).toHaveLength(121);
2024
+ expect(rows.filter((r) => r.operationId === ops[0]).map((r) => r.phase).sort()).toEqual(['intent', 'refused']);
2025
+ // An operation is its action and scope too: the same id asked under another is not it.
2026
+ expect(await host.admin.auditedOperations(staff, [{ ...refs[0], action: 'manageScopeMember' }])).toEqual([]);
2027
+ expect(await host.admin.auditedOperations(staff, [{ ...refs[0], scopeId: scopeId.parse(ulid()) }])).toEqual([]);
1440
2028
  });
1441
2029
  it('audits reading the audit trail, and reading the access log itself', async () => {
1442
2030
  const nosy = platformActorId.parse(ulid());