@substrat-run/contract-tests 0.161.0 → 0.163.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.
@@ -6,6 +6,8 @@ import { type ScopeHost } from '@substrat-run/kernel';
6
6
  export interface DirectoryAuditFixture {
7
7
  host: ScopeHost;
8
8
  directorySql(sql: string): Promise<void>;
9
+ /** #2526: one read of the directory, for state no HostAdmin read answers (a tenant tuple). */
10
+ directoryRows(sql: string): Promise<unknown[]>;
9
11
  /**
10
12
  * #2528: arm one statement to run against the directory, committed on its own, immediately
11
13
  * before the host's next audited directory write begins its unit — the write another caller
@@ -1 +1 @@
1
- {"version":3,"file":"directory-audit-suite.d.ts","sourceRoot":"","sources":["../src/directory-audit-suite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAG5D;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC;;;;OAIG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAgRD;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,GAAG,IAAI,CAkUhI"}
1
+ {"version":3,"file":"directory-audit-suite.d.ts","sourceRoot":"","sources":["../src/directory-audit-suite.ts"],"names":[],"mappings":"AAKA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAG5D;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,8FAA8F;IAC9F,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/C;;;;OAIG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAwhBD;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,GAAG,IAAI,CAkhBhI"}
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { errorCodeOf, platformActorId, scopeId, tenantId } from '@substrat-run/contracts';
2
+ import { connectionId, errorCodeOf, moduleId, orgId, permissionKey, platformActorId, principalId, scopeId, tenantId, } from '@substrat-run/contracts';
3
3
  import { ulid } from '@substrat-run/kernel';
4
4
  import { heldMigrations } from './held-migrations.js';
5
5
  /** A trigger refusing one admin action's row, or one kind of write to a directory table. */
@@ -30,6 +30,43 @@ const publish = async (host, staff, slug) => {
30
30
  const verticalOf = async (host, staff, slug) => (await host.admin.listVerticals(staff)).find((v) => v.slug === slug);
31
31
  /** #2501: how many of the slug's ownership periods are open (at most one). */
32
32
  const openPeriodsOf = async (host, staff, slug) => (await host.admin.listVerticalOwners(staff, slug)).filter((p) => p.until === null).length;
33
+ const AUDIT_READ = permissionKey.parse('audit:read');
34
+ /** #2526: how many live tenant tuples hold `relation` for `subject`. */
35
+ const tenantTuples = async (f, t, subject, relation) => (await f.directoryRows(`SELECT 1 FROM _substrat_tenant_tuples WHERE tenant_id = '${t}' AND subject = '${subject}'
36
+ AND relation = '${relation}' AND revoked_at IS NULL`)).length;
37
+ const newOrg = async (host, staff, t) => {
38
+ const id = orgId.parse(ulid());
39
+ await host.admin.createOrg(staff, { id, tenantId: t, slug: slugOf(), name: 'Audit org' });
40
+ return id;
41
+ };
42
+ /** #2526: a live connection in tenant `t`, for vertical `vertical`. */
43
+ const newConnection = async (host, staff, t, vertical = 'audit-vertical') => {
44
+ const id = connectionId.parse(ulid());
45
+ const provider = `audit-${id.toLowerCase()}`;
46
+ await host.admin.createConnection(staff, { id, tenantId: t, vertical, provider, label: 'Audit connection', secret: { accessToken: 'tok' } });
47
+ return { id, provider };
48
+ };
49
+ /** #2526: a provisioned scope in tenant `t`, with a lowercase hostname of its own to bind. */
50
+ const newScope = async (host, staff, t) => {
51
+ const s = scopeId.parse(ulid());
52
+ await host.provisionScope(staff, { tenantId: t, scopeId: s, vertical: await newVertical(host, staff) });
53
+ return s;
54
+ };
55
+ const hostnameOf = () => `audit-${ulid().toLowerCase()}.example.com`;
56
+ /**
57
+ * #2526: a hostname bound to scope `s` of tenant `t`, `pending` as a bind leaves it. Bound by an
58
+ * actor of its own, so its row is never one a case counts as the call's.
59
+ */
60
+ const bound = async (host, t, s, canonical = false) => {
61
+ const hostname = hostnameOf();
62
+ await host.admin.bindHostname(platformActorId.parse(ulid()), { hostname, tenantId: t, scopeId: s, surface: 'app', region: null, canonical });
63
+ return hostname;
64
+ };
65
+ /** #2526: a scope's hostnames as `[hostname, canonical]`, in name order. */
66
+ const scopeHostnames = async (host, staff, s) => (await host.admin.listHostnames(staff, { scopeId: s })).map((h) => [h.hostname, h.canonical]).sort();
67
+ const hostnameStatus = async (host, staff, s, hostname) => (await host.admin.listHostnames(staff, { scopeId: s })).find((h) => h.hostname === hostname)?.status ?? null;
68
+ const RECORDS = [{ type: 'txt', name: '_cf-custom-hostname', value: 'dcv-token', status: null }];
69
+ const entitlementsOf = async (host, staff, t) => (await host.admin.listEntitlements(staff, t)).map((e) => [e.entitlementKey, e.plan]);
33
70
  /** One vertical flag flipped by staff: the case is the same shape for each. */
34
71
  const flagCase = (action, field) => ({
35
72
  action,
@@ -243,6 +280,218 @@ const CASES = [
243
280
  };
244
281
  },
245
282
  },
283
+ // #2526: the role, grant and org writes at a tenant node.
284
+ {
285
+ action: 'defineRole',
286
+ write: { table: '_substrat_roles', verb: 'INSERT' },
287
+ arrange: async (host, staff) => {
288
+ const t = await newTenant(host, staff);
289
+ return {
290
+ call: () => host.admin.defineRole(staff, t, { key: 'auditor', permissions: [AUDIT_READ], source: 'vertical' }),
291
+ state: async () => (await host.admin.listRoles(staff, { tenantId: t })).find((r) => r.key === 'auditor')?.permissions ?? null,
292
+ changed: [AUDIT_READ],
293
+ row: { before: null, after: expect.objectContaining({ key: 'auditor', permissions: [AUDIT_READ], source: 'vertical' }) },
294
+ };
295
+ },
296
+ },
297
+ {
298
+ action: 'assignRole',
299
+ write: { table: '_substrat_tenant_tuples', verb: 'INSERT' },
300
+ arrange: async (host, staff, fixture) => {
301
+ const t = await newTenant(host, staff);
302
+ await host.admin.defineRole(staff, t, { key: 'auditor', permissions: [AUDIT_READ], source: 'vertical' });
303
+ const assignment = { principalId: principalId.parse(ulid()), roleKey: 'auditor', node: { tenantId: t, scopeId: null } };
304
+ return {
305
+ call: () => host.admin.assignRole(staff, assignment),
306
+ state: () => tenantTuples(fixture, t, `principal:${assignment.principalId}`, 'role:auditor'),
307
+ changed: 1,
308
+ row: { before: null, after: assignment },
309
+ };
310
+ },
311
+ },
312
+ {
313
+ action: 'grant',
314
+ write: { table: '_substrat_tenant_tuples', verb: 'INSERT' },
315
+ arrange: async (host, staff, fixture) => {
316
+ const t = await newTenant(host, staff);
317
+ const who = principalId.parse(ulid());
318
+ return {
319
+ call: () => host.admin.grant(staff, { principalId: who, permission: AUDIT_READ, node: { tenantId: t, scopeId: null }, grantedBy: who }),
320
+ state: () => tenantTuples(fixture, t, `principal:${who}`, `granted:${AUDIT_READ}`),
321
+ changed: 1,
322
+ row: { before: null, after: expect.objectContaining({ principalId: who, permission: AUDIT_READ }) },
323
+ };
324
+ },
325
+ },
326
+ {
327
+ action: 'grantToOrg',
328
+ write: { table: '_substrat_tenant_tuples', verb: 'INSERT' },
329
+ arrange: async (host, staff, fixture) => {
330
+ const t = await newTenant(host, staff);
331
+ const org = await newOrg(host, staff, t);
332
+ return {
333
+ call: () => host.admin.grantToOrg(staff, org, AUDIT_READ, { tenantId: t, scopeId: null }),
334
+ state: () => tenantTuples(fixture, t, `org:${org}`, `granted:${AUDIT_READ}`),
335
+ changed: 1,
336
+ row: { before: null, after: { orgId: org, permission: AUDIT_READ, node: { tenantId: t, scopeId: null } } },
337
+ };
338
+ },
339
+ },
340
+ {
341
+ action: 'grantToSystem',
342
+ write: { table: '_substrat_tenant_tuples', verb: 'INSERT' },
343
+ arrange: async (host, staff, fixture) => {
344
+ const t = await newTenant(host, staff);
345
+ const mod = moduleId.parse('audit-module');
346
+ const node = { tenantId: t, scopeId: null };
347
+ return {
348
+ call: () => host.admin.grantToSystem(staff, { moduleId: mod, permission: AUDIT_READ, node, grantedBy: staff }),
349
+ state: () => tenantTuples(fixture, t, `system:${mod}`, `granted:${AUDIT_READ}`),
350
+ changed: 1,
351
+ row: { before: null, after: { moduleId: mod, permission: AUDIT_READ, node } },
352
+ };
353
+ },
354
+ },
355
+ {
356
+ action: 'grantToConnection',
357
+ write: { table: '_substrat_connection_grants', verb: 'INSERT' },
358
+ arrange: async (host, staff, fixture) => {
359
+ const t = await newTenant(host, staff);
360
+ const conn = await newConnection(host, staff, t);
361
+ const node = { tenantId: t, scopeId: null };
362
+ return {
363
+ call: () => host.admin.grantToConnection(staff, { connectionId: conn.id, permission: AUDIT_READ, node, grantedBy: staff }),
364
+ // The record and, at the tenant node, the tuple go back together.
365
+ state: async () => [
366
+ (await host.admin.listConnectionGrants(staff, t)).filter((g) => g.connectionId === conn.id).length,
367
+ await tenantTuples(fixture, t, `connection:${conn.id}`, `granted:${AUDIT_READ}`),
368
+ ],
369
+ changed: [1, 1],
370
+ row: { before: null, after: { connectionId: conn.id, provider: conn.provider, permission: AUDIT_READ, node } },
371
+ };
372
+ },
373
+ },
374
+ {
375
+ action: 'createOrg',
376
+ write: { table: 'orgs', verb: 'INSERT' },
377
+ arrange: async (host, staff) => {
378
+ const t = await newTenant(host, staff);
379
+ const id = orgId.parse(ulid());
380
+ const slug = slugOf();
381
+ return {
382
+ call: () => host.admin.createOrg(staff, { id, tenantId: t, slug, name: 'Created org' }),
383
+ state: async () => (await host.admin.getOrg(staff, t, id))?.slug ?? null,
384
+ changed: slug,
385
+ row: { before: null, after: expect.objectContaining({ id, tenantId: t, slug, name: 'Created org' }) },
386
+ };
387
+ },
388
+ },
389
+ {
390
+ action: 'addMember',
391
+ write: { table: '_substrat_tenant_tuples', verb: 'INSERT' },
392
+ arrange: async (host, staff) => {
393
+ const t = await newTenant(host, staff);
394
+ const org = await newOrg(host, staff, t);
395
+ const who = principalId.parse(ulid());
396
+ return {
397
+ call: () => host.admin.addMember(staff, t, who, org),
398
+ state: async () => (await host.admin.listMembers(staff, t, org)).map((m) => m.principal),
399
+ changed: [who],
400
+ row: { before: null, after: expect.objectContaining({ principal: who, orgId: org }) },
401
+ };
402
+ },
403
+ },
404
+ // #2526: the hostname and entitlement writes.
405
+ {
406
+ action: 'bindHostname',
407
+ write: { table: 'hostnames', verb: 'INSERT' },
408
+ arrange: async (host, staff) => {
409
+ const t = await newTenant(host, staff);
410
+ const s = await newScope(host, staff, t);
411
+ // The surface's canonical already: the new canonical demotes it in the same unit, so a
412
+ // refused bind leaves it canonical, whichever write is refused.
413
+ const first = await bound(host, t, s, true);
414
+ const hostname = hostnameOf();
415
+ return {
416
+ call: () => host.admin.bindHostname(staff, { hostname, tenantId: t, scopeId: s, surface: 'app', region: null, canonical: true }),
417
+ state: () => scopeHostnames(host, staff, s),
418
+ changed: [[first, false], [hostname, true]].sort(),
419
+ row: { before: null, after: expect.objectContaining({ hostname, tenantId: t, scopeId: s, surface: 'app', canonical: true }) },
420
+ };
421
+ },
422
+ },
423
+ {
424
+ action: 'setHostnameStatus',
425
+ write: { table: 'hostnames', verb: 'UPDATE' },
426
+ arrange: async (host, staff) => {
427
+ const t = await newTenant(host, staff);
428
+ const s = await newScope(host, staff, t);
429
+ const hostname = await bound(host, t, s);
430
+ return {
431
+ call: () => host.admin.setHostnameStatus(staff, hostname, 'active'),
432
+ state: () => hostnameStatus(host, staff, s, hostname),
433
+ changed: 'active',
434
+ row: { before: { status: 'pending' }, after: { status: 'active', note: null } },
435
+ };
436
+ },
437
+ },
438
+ {
439
+ action: 'setHostnameIssuance',
440
+ write: { table: 'hostnames', verb: 'UPDATE' },
441
+ arrange: async (host, staff) => {
442
+ const t = await newTenant(host, staff);
443
+ const s = await newScope(host, staff, t);
444
+ const hostname = await bound(host, t, s);
445
+ return {
446
+ call: () => host.admin.setHostnameIssuance(staff, hostname, { status: 'verifying', customHostnameId: 'cf-audit', validationRecords: RECORDS }),
447
+ state: () => hostnameStatus(host, staff, s, hostname),
448
+ changed: 'verifying',
449
+ row: { before: { status: 'pending' }, after: { status: 'verifying', note: null } },
450
+ };
451
+ },
452
+ },
453
+ {
454
+ action: 'unbindHostname',
455
+ write: { table: 'hostnames', verb: 'DELETE' },
456
+ arrange: async (host, staff) => {
457
+ const t = await newTenant(host, staff);
458
+ const s = await newScope(host, staff, t);
459
+ const hostname = await bound(host, t, s);
460
+ return {
461
+ call: () => host.admin.unbindHostname(staff, hostname),
462
+ state: () => scopeHostnames(host, staff, s),
463
+ changed: [],
464
+ row: { before: { hostname, status: 'pending' }, after: null },
465
+ };
466
+ },
467
+ },
468
+ {
469
+ action: 'grantEntitlement',
470
+ write: { table: '_substrat_entitlements', verb: 'INSERT' },
471
+ arrange: async (host, staff) => {
472
+ const t = await newTenant(host, staff);
473
+ return {
474
+ call: () => host.admin.grantEntitlement(staff, t, 'audit-entitlement', { plan: 'pro' }),
475
+ state: () => entitlementsOf(host, staff, t),
476
+ changed: [['audit-entitlement', 'pro']],
477
+ row: { before: null, after: { entitlementKey: 'audit-entitlement', expiresAt: null, quota: null, plan: 'pro' } },
478
+ };
479
+ },
480
+ },
481
+ {
482
+ action: 'revokeEntitlement',
483
+ write: { table: '_substrat_entitlements', verb: 'DELETE' },
484
+ arrange: async (host, staff) => {
485
+ const t = await newTenant(host, staff);
486
+ await host.admin.grantEntitlement(staff, t, 'audit-entitlement', { plan: 'pro' });
487
+ return {
488
+ call: () => host.admin.revokeEntitlement(staff, t, 'audit-entitlement'),
489
+ state: () => entitlementsOf(host, staff, t),
490
+ changed: [],
491
+ row: { before: { entitlementKey: 'audit-entitlement', expiresAt: null, quota: null, plan: 'pro' }, after: null },
492
+ };
493
+ },
494
+ },
246
495
  ];
247
496
  /**
248
497
  * #2479: every tenant and vertical directory write commits with its admin row. A fault in the
@@ -261,9 +510,11 @@ export function directoryAdminAuditTransactionSuite(adapterName, makeFixture) {
261
510
  : refusal(`test_refuse_write_${tag}`, `${c.write.verb} ON ${c.write.table}`);
262
511
  let lifted = true;
263
512
  try {
264
- const { call, state, changed, row } = await c.arrange(host, staff);
513
+ const { call, state, changed, row } = await c.arrange(host, staff, fixture);
265
514
  const since = new Date().toISOString();
266
- const rows = () => host.admin.auditLog(staff, { action: [c.action], since });
515
+ // The call's own rows: an arrangement that made the same action as another actor, in the
516
+ // same millisecond as `since`, is not one of them.
517
+ const rows = async () => (await host.admin.auditLog(staff, { action: [c.action], since })).filter((r) => r.actor === staff);
267
518
  const before = await state();
268
519
  expect(before).not.toEqual(changed);
269
520
  await fixture.directorySql(trigger.create);
@@ -360,7 +611,9 @@ export function directoryAdminAuditTransactionSuite(adapterName, makeFixture) {
360
611
  }
361
612
  throw new Error('expected the call to be refused');
362
613
  };
363
- const rowsSince = async (host, staff, action, since) => (await host.admin.auditLog(staff, { action: [action], since })).map((r) => ({ before: r.before, after: r.after }));
614
+ const rowsSince = async (host, staff, action, since) => (await host.admin.auditLog(staff, { action: [action], since }))
615
+ .filter((r) => r.actor === staff)
616
+ .map((r) => ({ before: r.before, after: r.after }));
364
617
  /** A private vertical (tenant-owned, not listed), whose versions self-admit. */
365
618
  const privateVertical = async (host, staff) => {
366
619
  const owner = await newTenant(host, staff);
@@ -494,6 +747,53 @@ export function directoryAdminAuditTransactionSuite(adapterName, makeFixture) {
494
747
  .toEqual(['admitted']);
495
748
  });
496
749
  });
750
+ // #2565: a promote reads the listing in the unit that moves prod, as listing reads prod.
751
+ it('promoteVersion refuses an auto-admitted version as prod of a vertical listed since it was read', async () => {
752
+ await withFixture(async (fixture, host, staff) => {
753
+ const { slug, release } = await privateVertical(host, staff);
754
+ const auto = await release();
755
+ const since = new Date().toISOString();
756
+ fixture.interleave(`UPDATE verticals SET listed = 1 WHERE slug = '${slug}'`);
757
+ const refused = await refusalOf(host.admin.promoteVersion(staff, slug, 'prod', auto));
758
+ expect(errorCodeOf(refused)).toBe('conflict');
759
+ expect(refused.message).toContain(`prod version ${auto} is auto-admitted`);
760
+ expect(await host.admin.listChannels(staff, slug)).toEqual([]);
761
+ expect(await rowsSince(host, staff, 'promoteVersion', since)).toEqual([]);
762
+ });
763
+ });
764
+ it('and, unlistedOnly, refuses any version of a vertical listed since it was read', async () => {
765
+ await withFixture(async (fixture, host, staff) => {
766
+ const { slug, release } = await privateVertical(host, staff);
767
+ const vouched = await release();
768
+ await host.admin.admitVersion(staff, vouched);
769
+ const since = new Date().toISOString();
770
+ fixture.interleave(`UPDATE verticals SET listed = 1 WHERE slug = '${slug}'`);
771
+ const refused = await refusalOf(host.admin.promoteVersion(staff, slug, 'prod', vouched, undefined, { unlistedOnly: true }));
772
+ expect(errorCodeOf(refused)).toBe('forbidden');
773
+ expect(await host.admin.listChannels(staff, slug)).toEqual([]);
774
+ expect(await rowsSince(host, staff, 'promoteVersion', since)).toEqual([]);
775
+ });
776
+ });
777
+ it('re-promoting the auto-admitted version a listed vertical names as prod is refused only if its serve would change what runs', async () => {
778
+ await withFixture(async (fixture, host, staff) => {
779
+ const { slug, release } = await privateVertical(host, staff);
780
+ const auto = await release();
781
+ const vouched = await release();
782
+ await host.admin.promoteVersion(staff, slug, 'prod', auto);
783
+ // Listed before listing refused an auto-admitted prod: the state is kept, not judged again.
784
+ await fixture.directorySql(`UPDATE verticals SET listed = 1 WHERE slug = '${slug}'`);
785
+ await host.admin.promoteVersion(staff, slug, 'prod', auto);
786
+ expect((await host.admin.listChannels(staff, slug)).find((c) => c.channel === 'prod')?.versionId).toBe(auto);
787
+ // Serving the same version (the usual hosted state): the retry changes nothing either.
788
+ await fixture.directorySql(`UPDATE verticals SET serving_ref = 'serving-a', serving_version_id = '${auto}' WHERE slug = '${slug}'`);
789
+ await host.admin.promoteVersion(staff, slug, 'prod', auto);
790
+ // A failed serve left another version serving: the retry would put `auto` on every install.
791
+ await fixture.directorySql(`UPDATE verticals SET serving_ref = 'serving-a', serving_version_id = '${vouched}' WHERE slug = '${slug}'`);
792
+ const refused = await refusalOf(host.admin.promoteVersion(staff, slug, 'prod', auto));
793
+ expect(errorCodeOf(refused)).toBe('conflict');
794
+ expect(refused.message).toContain(`prod version ${auto} is auto-admitted`);
795
+ });
796
+ });
497
797
  it('setVerticalServing refuses a serve whose serving version moved, and a retry after reading it again lands', async () => {
498
798
  await withFixture(async (fixture, host, staff) => {
499
799
  const slug = await newVertical(host, staff);
@@ -533,6 +833,49 @@ export function directoryAdminAuditTransactionSuite(adapterName, makeFixture) {
533
833
  .map((r) => r.after.ref)).toEqual(['serving-a']);
534
834
  });
535
835
  });
836
+ // #2526: the org and the connection a grant names are judged in the unit that writes it.
837
+ it.each(['grantToOrg', 'addMember'])('%s refuses an org deleted since it was read, and writes nothing', async (action) => {
838
+ await withFixture(async (fixture, host, staff) => {
839
+ const t = await newTenant(host, staff);
840
+ const org = await newOrg(host, staff, t);
841
+ const who = principalId.parse(ulid());
842
+ const since = new Date().toISOString();
843
+ fixture.interleave(`DELETE FROM orgs WHERE org_id = '${org}'`);
844
+ const refused = await refusalOf(action === 'grantToOrg'
845
+ ? host.admin.grantToOrg(staff, org, AUDIT_READ, { tenantId: t, scopeId: null })
846
+ : host.admin.addMember(staff, t, who, org));
847
+ expect(errorCodeOf(refused)).toBe('not_found');
848
+ expect(refused.message).toBe(`unknown org ${org} in tenant ${t}`);
849
+ expect(await fixture.directoryRows(`SELECT 1 FROM _substrat_tenant_tuples WHERE tenant_id = '${t}'`)).toEqual([]);
850
+ expect(await rowsSince(host, staff, action, since)).toEqual([]);
851
+ });
852
+ });
853
+ it('grantToConnection refuses a connection revoked since it was read, and writes nothing', async () => {
854
+ await withFixture(async (fixture, host, staff) => {
855
+ const t = await newTenant(host, staff);
856
+ const conn = await newConnection(host, staff, t);
857
+ const since = new Date().toISOString();
858
+ fixture.interleave(`UPDATE _substrat_connections SET revoked_at = '${since}' WHERE id = '${conn.id}'`);
859
+ const refused = await refusalOf(host.admin.grantToConnection(staff, { connectionId: conn.id, permission: AUDIT_READ, node: { tenantId: t, scopeId: null }, grantedBy: staff }));
860
+ expect(refused.message).toContain(`connection ${conn.id} is revoked — grant nothing to it`);
861
+ expect(await fixture.directoryRows(`SELECT 1 FROM _substrat_connection_grants WHERE connection_id = '${conn.id}'`)).toEqual([]);
862
+ expect(await tenantTuples(fixture, t, `connection:${conn.id}`, `granted:${AUDIT_READ}`)).toBe(0);
863
+ expect(await rowsSince(host, staff, 'grantToConnection', since)).toEqual([]);
864
+ });
865
+ });
866
+ it('defineRole records the role its own unit read as its before', async () => {
867
+ await withFixture(async (fixture, host, staff) => {
868
+ const t = await newTenant(host, staff);
869
+ await host.admin.defineRole(staff, t, { key: 'auditor', permissions: [AUDIT_READ], source: 'vertical' });
870
+ fixture.interleave(`UPDATE _substrat_roles SET permissions = '["audit:write"]' WHERE tenant_id = '${t}' AND role_key = 'auditor'`);
871
+ await host.admin.defineRole(staff, t, { key: 'auditor', permissions: [AUDIT_READ], source: 'vertical' });
872
+ // The first definition's row has no `before`; the second's is the role as the interleave left it.
873
+ const befores = (await host.admin.auditLog(staff, { tenantId: t, action: ['defineRole'] }))
874
+ .map((r) => r.before?.permissions ?? null);
875
+ expect(befores).toHaveLength(2);
876
+ expect(befores).toEqual(expect.arrayContaining([null, ['audit:write']]));
877
+ });
878
+ });
536
879
  it('setTenantStatus records the status its own unit read', async () => {
537
880
  await withFixture(async (fixture, host, staff) => {
538
881
  const t = await newTenant(host, staff);
@@ -554,6 +897,104 @@ export function directoryAdminAuditTransactionSuite(adapterName, makeFixture) {
554
897
  .toEqual([[{ name: 'Middle' }, { name: 'After' }]]);
555
898
  });
556
899
  });
900
+ // #2526: the hostname and entitlement writes judge their guard, their no-op and their row's
901
+ // `before` in the unit that writes. A no-op writes nothing and records nothing; each is
902
+ // paired with the change beside it that does record.
903
+ /** A hostname row as another caller would write it, for an interleave. */
904
+ const hostnameInsert = (hostname, t, s) => `INSERT INTO hostnames (hostname, tenant_id, scope_id, vertical_slug, surface, region, status, status_note, canonical,
905
+ created_at, custom_hostname_id, validation_records)
906
+ VALUES ('${hostname}', '${t}', '${s}', NULL, 'app', NULL, 'pending', NULL, 0, '2026-10-25T00:00:00.000Z', NULL, NULL)`;
907
+ it('bindHostname refuses a name another live scope bound since it was read, demoting nothing', async () => {
908
+ await withFixture(async (fixture, host, staff) => {
909
+ const t = await newTenant(host, staff);
910
+ const [mine, theirs] = [await newScope(host, staff, t), await newScope(host, staff, t)];
911
+ const canonical = await bound(host, t, mine, true);
912
+ const hostname = hostnameOf();
913
+ const since = new Date().toISOString();
914
+ fixture.interleave(hostnameInsert(hostname, t, theirs));
915
+ const refused = await refusalOf(host.admin.bindHostname(staff, { hostname, tenantId: t, scopeId: mine, surface: 'app', region: null, canonical: true }));
916
+ expect(errorCodeOf(refused)).toBe('conflict');
917
+ expect(refused.message).toBe(`hostname '${hostname}' is already bound to another scope`);
918
+ expect(await scopeHostnames(host, staff, mine)).toEqual([[canonical, true]]);
919
+ expect(await scopeHostnames(host, staff, theirs)).toEqual([[hostname, false]]);
920
+ expect(await rowsSince(host, staff, 'bindHostname', since)).toEqual([]);
921
+ });
922
+ });
923
+ it('and reclaims a name whose holder was archived since it was read', async () => {
924
+ await withFixture(async (fixture, host, staff) => {
925
+ const t = await newTenant(host, staff);
926
+ const [mine, theirs] = [await newScope(host, staff, t), await newScope(host, staff, t)];
927
+ const hostname = await bound(host, t, theirs);
928
+ const since = new Date().toISOString();
929
+ fixture.interleave(`UPDATE scopes SET status = 'archived' WHERE scope_id = '${theirs}'`);
930
+ await host.admin.bindHostname(staff, { hostname, tenantId: t, scopeId: mine, surface: 'app', region: null, canonical: false });
931
+ expect(await scopeHostnames(host, staff, mine)).toEqual([[hostname, false]]);
932
+ expect((await rowsSince(host, staff, 'bindHostname', since)).map((r) => r.after.scopeId)).toEqual([mine]);
933
+ });
934
+ });
935
+ it('setHostnameStatus records nothing for a status set since it was read, and the status its unit read otherwise', async () => {
936
+ await withFixture(async (fixture, host, staff) => {
937
+ const t = await newTenant(host, staff);
938
+ const s = await newScope(host, staff, t);
939
+ const hostname = await bound(host, t, s);
940
+ const since = new Date().toISOString();
941
+ fixture.interleave(`UPDATE hostnames SET status = 'active' WHERE hostname = '${hostname}'`);
942
+ await host.admin.setHostnameStatus(staff, hostname, 'active');
943
+ expect(await rowsSince(host, staff, 'setHostnameStatus', since)).toEqual([]);
944
+ fixture.interleave(`UPDATE hostnames SET status = 'failed' WHERE hostname = '${hostname}'`);
945
+ await host.admin.setHostnameStatus(staff, hostname, 'active');
946
+ expect(await hostnameStatus(host, staff, s, hostname)).toBe('active');
947
+ expect(await rowsSince(host, staff, 'setHostnameStatus', since))
948
+ .toEqual([{ before: { status: 'failed' }, after: { status: 'active', note: null } }]);
949
+ });
950
+ });
951
+ it('setHostnameIssuance records nothing for a poll that finds what another write already stored, and one row for a poll that changes it', async () => {
952
+ await withFixture(async (fixture, host, staff) => {
953
+ const t = await newTenant(host, staff);
954
+ const s = await newScope(host, staff, t);
955
+ const hostname = await bound(host, t, s);
956
+ const poll = { status: 'verifying', customHostnameId: 'cf-audit', validationRecords: RECORDS };
957
+ const since = new Date().toISOString();
958
+ fixture.interleave(`UPDATE hostnames SET status = 'verifying', custom_hostname_id = 'cf-audit',
959
+ validation_records = '${JSON.stringify(RECORDS)}' WHERE hostname = '${hostname}'`);
960
+ await host.admin.setHostnameIssuance(staff, hostname, poll);
961
+ expect(await rowsSince(host, staff, 'setHostnameIssuance', since)).toEqual([]);
962
+ // Its twin: a poll that resolves it records one row, `before` the status the unit read.
963
+ await host.admin.setHostnameIssuance(staff, hostname, { status: 'active', validationRecords: [] });
964
+ expect(await hostnameStatus(host, staff, s, hostname)).toBe('active');
965
+ expect(await rowsSince(host, staff, 'setHostnameIssuance', since))
966
+ .toEqual([{ before: { status: 'verifying' }, after: { status: 'active', note: null } }]);
967
+ });
968
+ });
969
+ it('grantEntitlement records nothing for a grant another write already made, and the grant its unit read as before otherwise', async () => {
970
+ await withFixture(async (fixture, host, staff) => {
971
+ const t = await newTenant(host, staff);
972
+ const held = (plan) => `INSERT OR REPLACE INTO _substrat_entitlements
973
+ (tenant_id, entitlement_key, expires_at, quota, plan, granted_at, granted_by)
974
+ VALUES ('${t}', 'audit-entitlement', NULL, NULL, '${plan}', '2026-10-25T00:00:00.000Z', 'interleaved')`;
975
+ const since = new Date().toISOString();
976
+ fixture.interleave(held('pro'));
977
+ await host.admin.grantEntitlement(staff, t, 'audit-entitlement', { plan: 'pro' });
978
+ expect(await rowsSince(host, staff, 'grantEntitlement', since)).toEqual([]);
979
+ fixture.interleave(held('trial'));
980
+ await host.admin.grantEntitlement(staff, t, 'audit-entitlement', { plan: 'pro' });
981
+ expect(await entitlementsOf(host, staff, t)).toEqual([['audit-entitlement', 'pro']]);
982
+ expect(await rowsSince(host, staff, 'grantEntitlement', since)).toEqual([{
983
+ before: { entitlementKey: 'audit-entitlement', expiresAt: null, quota: null, plan: 'trial' },
984
+ after: { entitlementKey: 'audit-entitlement', expiresAt: null, quota: null, plan: 'pro' },
985
+ }]);
986
+ });
987
+ });
988
+ it('revokeEntitlement records nothing for a grant revoked since it was read', async () => {
989
+ await withFixture(async (fixture, host, staff) => {
990
+ const t = await newTenant(host, staff);
991
+ await host.admin.grantEntitlement(staff, t, 'audit-entitlement', { plan: 'pro' });
992
+ const since = new Date().toISOString();
993
+ fixture.interleave(`DELETE FROM _substrat_entitlements WHERE tenant_id = '${t}' AND entitlement_key = 'audit-entitlement'`);
994
+ await host.admin.revokeEntitlement(staff, t, 'audit-entitlement');
995
+ expect(await rowsSince(host, staff, 'revokeEntitlement', since)).toEqual([]);
996
+ });
997
+ });
557
998
  });
558
999
  });
559
1000
  }