Harden the backend's trust surface: an allow-listed public surface, decisions taken on data re-read from the pod

The backend served its settings dataset (including the app actor's private key) to anonymous callers, through the SPARQL endpoint and through the semapps LDP catch-all route, since WebACL is off. An allow-list mounted on every route now exposes only what the app and the pod need (/app, /as/actor/app and its Client ID Document, /catalog, the public key, access needs, the JSON-LD context, nodeinfo, the app inbox and the webhook endpoint); everything else answers 404.

Every watcher-fed service (public read, catalog, attendees, contacts, registration) now decides on the activity and object re-read from the pod as the app, checks they belong to the outbox owner and that the stored object has the expected type, and never acts on the notification body. Decisions for one object are serialized and taken on a read made when acting, with a 30 s bound and an abort check before every write. The anonymous read follows the event's visibility both ways, attendees collection included; Festipod records the grants it makes and revokes only those, leaving any other public share untouched; an unreadable ACL changes nothing. A participant's attendance follows their latest act by the pod's timestamp, not the order notifications arrive. Known limits (no conditional write on the pod, a write in flight at the timeout) are documented in the code.
This commit is contained in:
Sylvain Duchesne
2026-10-13 16:30:52 +02:00
parent d38f8a7384
commit 88f23b7023
21 changed files with 3362 additions and 243 deletions
@@ -0,0 +1,613 @@
const { test, expect, describe } = require('bun:test');
const publicRead = require('./public-read.service');
const { createKeyedSerializer } = require('./keyed-serializer');
const { GRANTED_AT, grantedQuery, recordGrantUpdate, forgetGrantUpdate } = require('./public-read-ledger');
const ALICE = 'http://localhost:3000/alice';
const BOB = 'http://localhost:3000/bob';
const E = `${ALICE}/data/events/e1`;
const C = `${ALICE}/data/collections/e1-attendees`;
const FOAF_AGENT = 'http://xmlns.com/foaf/0.1/Agent';
const APP = 'http://localhost:3003/as/actor/app';
// The ACL document shape `pod-permissions.get` returns (compacted with the acl:/foaf: prefixes).
// Note: for an event, the owner's rights come from the container's inherited #Default rules — the
// event's only OWN rule is #Read foaf:Agent (confirmed live 2026-10-12).
const PUBLIC_READ_AUTH = { '@id': '#Read', 'acl:mode': 'acl:Read', 'acl:agentClass': 'foaf:Agent' };
const DEFAULT_READ = { '@id': '#DefaultRead', 'acl:mode': 'acl:Read', 'acl:agent': ALICE };
// A revoke PUT returns 204; capture its body so tests can assert the reconstructed graph.
const okProxy = captured => params => {
captured.put = params;
return { status: 204 };
};
const publicEvent = (extra = {}) => ({ id: E, type: 'Event', to: 'as:Public', ...extra });
const privateEvent = (extra = {}) => ({ id: E, type: 'Event', name: 'now private', ...extra });
/** `pod-resources.get` answering from a map of uri → stored body (404 for anything else). */
const stored = (...bodies) => ({ resourceUri }) => {
const body = bodies.find(b => b.id === resourceUri);
return body ? { ok: true, status: 200, body } : { ok: false, status: 404 };
};
/**
* The backend's grant ledger, answering exactly the queries `public-read-ledger.js` builds.
* `granted` lists the resources Festipod is recorded as having made public.
*/
function ledgerCall(granted, action, params) {
const match = /<([^>]+)> <http:\/\/festipod\.org\/publicReadGrantedAt>/.exec(params.query);
if (!match) throw new Error(`unexpected store call ${params.query}`);
const uri = match[1];
if (action === 'triplestore.query' && params.query === grantedQuery(uri)) return granted.has(uri);
if (action === 'triplestore.update' && params.query === recordGrantUpdate(uri)) return void granted.add(uri);
if (action === 'triplestore.update' && params.query === forgetGrantUpdate(uri)) return void granted.delete(uri);
throw new Error(`unexpected store call ${params.query}`);
}
/**
* The service with its actions+methods bound to a recording logger, over a programmable pod and a
* grant ledger (by default: Festipod made both the event and its collection public).
*/
function service(pod, { granted = [E, C], timeoutMs } = {}) {
const logs = { info: [], warn: [], error: [] };
const calls = [];
const ledger = new Set(granted);
const svc = {
logger: { info: m => logs.info.push(m), warn: m => logs.warn.push(m), error: m => logs.error.push(m) },
actions: {}
};
if (publicRead.created) publicRead.created.call(svc);
if (timeoutMs) svc.serialize = createKeyedSerializer({ timeoutMs });
const ctx = {
call: async (action, params) => {
calls.push({ action, params });
if (action.startsWith('triplestore.')) return ledgerCall(ledger, action, params);
if (action === 'pod-permissions.fetch' && !pod['pod-permissions.fetch']) {
// The ACL read, served from a test's `pod-permissions.get` (authorizations, or `false` = a failed read).
const uri = params.url.replace(/^(https?:\/\/[^/]+)\/_acl/, '$1');
const authorizations = pod['pod-permissions.get'] ? await pod['pod-permissions.get']({ uri }) : undefined;
return authorizations === false || authorizations === undefined ? { status: 500 } : { status: 200, body: { '@graph': authorizations } };
}
return pod[action] ? pod[action](params) : undefined;
}
};
// Bind actions so `this.actions.reconcile|grant|revoke(...)` work from the methods.
for (const [name, def] of Object.entries(publicRead.actions)) {
svc.actions[name] = (params, opts) => def.handler.call(svc, { ...opts, params, call: ctx.call });
}
for (const [name, fn] of Object.entries(publicRead.methods)) svc[name] = fn.bind(svc);
return { svc, ctx, logs, calls, ledger };
}
// The agent triples a PUT revocation graph carries, flattened to {auth, p, o} for assertions.
function putTriples(putParams) {
const graph = JSON.parse(putParams.body)['@graph'];
const out = [];
for (const node of graph) {
for (const p of ['acl:agent', 'acl:agentClass', 'acl:agentGroup', 'acl:origin']) {
for (const o of [].concat(node[p] ?? [])) out.push({ auth: node['@id'], p, o });
}
}
return out;
}
describe('public-read.onUpdate — the grant and the ACL stay consistent with visibility', () => {
test('private → public: grants acl:Read to foaf:Agent (and is idempotent)', async () => {
const pod = { 'pod-resources.get': stored(publicEvent()), 'pod-permissions.get': () => [DEFAULT_READ], 'pod-permissions.add': () => true };
const { svc, ctx, calls } = service(pod);
await svc.onUpdate(ctx, publicEvent(), ALICE);
const add = calls.find(c => c.action === 'pod-permissions.add');
expect(add).toBeDefined();
expect(add.params).toMatchObject({ uri: E, agentPredicate: 'acl:agentClass', agentUri: FOAF_AGENT, mode: 'acl:Read' });
expect(calls.some(c => c.action === 'signature.proxy.query')).toBe(false);
});
test('already public twin: grant finds the rule present and adds nothing', async () => {
const pod = {
'pod-resources.get': stored(publicEvent()),
'pod-permissions.get': () => [DEFAULT_READ, PUBLIC_READ_AUTH],
'pod-permissions.add': () => {
throw new Error('must not add');
}
};
const { svc, ctx, calls } = service(pod);
await svc.onUpdate(ctx, publicEvent(), ALICE);
expect(calls.some(c => c.action === 'pod-permissions.add')).toBe(false);
});
test('public → private: PUTs a graph that drops foaf:Agent and keeps the owner (set not empty)', async () => {
const captured = {};
const pod = {
'pod-resources.get': stored(privateEvent()),
'pod-permissions.get': () => [DEFAULT_READ, PUBLIC_READ_AUTH],
'app.get': () => ({ id: APP }),
'signature.proxy.query': okProxy(captured)
};
const { svc, ctx, calls } = service(pod);
await svc.onUpdate(ctx, privateEvent(), ALICE);
const put = calls.find(c => c.action === 'signature.proxy.query');
expect(put).toBeDefined();
expect(put.params).toMatchObject({ method: 'PUT', url: `http://localhost:3000/_acl/alice/data/events/e1`, actorUri: APP });
const triples = putTriples(captured.put);
// No foaf:Agent anywhere in the PUT; the #Default rule is NOT re-emitted (a PUT leaves it be).
expect(triples.some(t => t.p === 'acl:agentClass' && (t.o === 'foaf:Agent' || t.o === FOAF_AGENT))).toBe(false);
expect(triples.some(t => t.auth.includes('#Default'))).toBe(false);
// The own set is non-empty: the owner is kept as acl:agent of #Read.
expect(triples.some(t => t.p === 'acl:agent' && t.o === ALICE && t.auth.includes('#Read'))).toBe(true);
});
test('public → private when the ACL stored the FULL foaf IRI: still dropped', async () => {
const captured = {};
const fullIriAuth = { '@id': '#Read', 'acl:mode': 'acl:Read', 'acl:agentClass': FOAF_AGENT };
const pod = {
'pod-resources.get': stored(privateEvent()),
'pod-permissions.get': () => [DEFAULT_READ, fullIriAuth],
'app.get': () => ({ id: APP }),
'signature.proxy.query': okProxy(captured)
};
const { svc, ctx } = service(pod);
await svc.onUpdate(ctx, privateEvent(), ALICE);
expect(putTriples(captured.put).some(t => t.o === FOAF_AGENT || t.o === 'foaf:Agent')).toBe(false);
});
test('a per-contact own grant is preserved when the public rule is removed', async () => {
const captured = {};
const friend = 'http://localhost:3000/carol';
const readWithFriend = { '@id': '#Read', 'acl:mode': 'acl:Read', 'acl:agentClass': 'foaf:Agent', 'acl:agent': friend };
const pod = {
'pod-resources.get': stored(privateEvent()),
'pod-permissions.get': () => [DEFAULT_READ, readWithFriend],
'app.get': () => ({ id: APP }),
'signature.proxy.query': okProxy(captured)
};
const { svc, ctx } = service(pod);
await svc.onUpdate(ctx, privateEvent(), ALICE);
const triples = putTriples(captured.put);
expect(triples.some(t => t.p === 'acl:agent' && t.o === friend)).toBe(true); // friend kept
expect(triples.some(t => t.o === 'foaf:Agent' || t.o === FOAF_AGENT)).toBe(false); // public dropped
});
test('update to private on an already-private event: nothing to revoke, no PUT', async () => {
const pod = {
'pod-resources.get': stored(privateEvent({ name: 'still private' })),
'pod-permissions.get': () => [DEFAULT_READ],
'signature.proxy.query': () => {
throw new Error('must not PUT');
}
};
const { svc, ctx, calls } = service(pod);
await svc.onUpdate(ctx, privateEvent({ name: 'still private' }), ALICE);
expect(calls.some(c => c.action === 'signature.proxy.query')).toBe(false);
});
test('a pod refusal of the revoke PUT is logged, never thrown', async () => {
const pod = {
'pod-resources.get': stored(privateEvent()),
'pod-permissions.get': () => [DEFAULT_READ, PUBLIC_READ_AUTH],
'app.get': () => ({ id: APP }),
'signature.proxy.query': () => ({ status: 400 })
};
const { svc, ctx, logs } = service(pod);
await expect(svc.onUpdate(ctx, privateEvent(), ALICE)).resolves.toBeUndefined();
expect(logs.error.some(m => m.includes('HTTP 400'))).toBe(true);
});
test('a pod failure is logged, never thrown (the watcher is not awaited)', async () => {
const pod = {
'pod-resources.get': stored(publicEvent()),
'pod-permissions.get': () => {
throw new Error('pod down');
}
};
const { svc, ctx, logs } = service(pod);
await expect(svc.onUpdate(ctx, publicEvent(), ALICE)).resolves.toBeUndefined();
expect(logs.error[0]).toContain('pod down');
});
test('a failing read of the event itself is logged, never thrown', async () => {
const pod = {
'pod-resources.get': () => {
throw new Error('proxy unreachable');
}
};
const { svc, ctx, logs } = service(pod);
await expect(svc.onUpdate(ctx, publicEvent(), ALICE)).resolves.toBeUndefined();
expect(logs.error[0]).toContain('proxy unreachable');
});
});
describe('public-read.onCreate — grants only on public intent', () => {
test('a created public event is granted', async () => {
const pod = { 'pod-resources.get': stored(publicEvent()), 'pod-permissions.get': () => [DEFAULT_READ], 'pod-permissions.add': () => true };
const { svc, ctx, calls } = service(pod);
await svc.onCreate(ctx, publicEvent(), ALICE);
expect(calls.some(c => c.action === 'pod-permissions.add')).toBe(true);
});
test('a created private event is not granted', async () => {
const pod = { 'pod-resources.get': stored(privateEvent({ name: 'private' })), 'pod-permissions.get': () => [DEFAULT_READ] };
const { svc, ctx, calls, logs } = service(pod);
await svc.onCreate(ctx, privateEvent({ name: 'private' }), ALICE);
expect(calls.some(c => c.action === 'pod-permissions.add')).toBe(false);
expect(calls.some(c => c.action === 'signature.proxy.query')).toBe(false);
expect(logs.info.some(m => m.includes('not addressed to the Public collection'))).toBe(true);
});
});
describe('public-read.reconcile — decides on the STORED event, read when acting', () => {
test('the notification\'s copy is only a pointer: a "public" copy of an event the pod holds private grants nothing', async () => {
const pod = { 'pod-resources.get': stored(privateEvent()), 'pod-permissions.get': () => [DEFAULT_READ], 'pod-permissions.add': () => true };
const { svc, ctx, calls } = service(pod);
await svc.onUpdate(ctx, publicEvent(), ALICE);
expect(calls.some(c => c.action === 'pod-permissions.add')).toBe(false);
});
test('a public Note stored at the URI is not an event: nothing granted (the type is re-checked)', async () => {
const note = { id: E, type: 'Note', to: 'as:Public' };
const pod = { 'pod-resources.get': stored(note), 'pod-permissions.get': () => [DEFAULT_READ], 'pod-permissions.add': () => true };
const { svc, ctx, calls, logs } = service(pod);
await svc.onCreate(ctx, E, ALICE);
expect(calls.some(c => c.action.startsWith('pod-permissions'))).toBe(false);
expect(logs.warn[0]).toContain('not an as:Event');
});
test("an event outside the actor's pod is neither read nor touched", async () => {
const bobs = `${BOB}/data/events/e1`;
const { svc, ctx, calls, logs } = service({});
await svc.onUpdate(ctx, bobs, ALICE);
expect(calls).toEqual([]);
expect(logs.warn[0]).toContain(`not in the pod of ${ALICE}`);
});
test('a deleted event is left alone (its ACL went with it)', async () => {
const { svc, ctx, calls } = service({ 'pod-resources.get': () => ({ status: 404 }) });
await svc.onUpdate(ctx, E, ALICE);
expect(calls.map(c => c.action)).toEqual(['pod-resources.get']);
});
test('CONCURRENT notifications: the newest state wins (no grant lands after the revoke)', async () => {
// A stateful pod: the event's addressing, and whether its ACL holds the public-read rule.
// The grant is slow (the pod's latency); the event is made private while the first handler acts.
let current = publicEvent();
let aclIsPublic = false;
const pod = {
'pod-resources.get': ({ resourceUri }) => (resourceUri === E ? { status: 200, body: current } : { status: 404 }),
'pod-permissions.get': () => (aclIsPublic ? [DEFAULT_READ, PUBLIC_READ_AUTH] : [DEFAULT_READ]),
'pod-permissions.add': async () => {
await new Promise(resolve => setTimeout(resolve, 30));
aclIsPublic = true;
return true;
},
'app.get': () => ({ id: APP }),
'signature.proxy.query': ({ body }) => {
aclIsPublic = putTriples({ body }).some(t => t.o === 'foaf:Agent' || t.o === FOAF_AGENT);
return { status: 204 };
}
};
const { svc, ctx } = service(pod);
const first = svc.onUpdate(ctx, publicEvent(), ALICE); // notified while public
await new Promise(resolve => setTimeout(resolve, 5));
current = privateEvent(); // the owner makes it private; a second notification follows
const second = svc.onUpdate(ctx, privateEvent(), ALICE);
await Promise.all([first, second]);
expect(aclIsPublic).toBe(false);
});
});
describe("public-read.reconcile — the event's attendees collection follows its visibility, both ways", () => {
const COLLECTION_DEFAULT = { '@id': '#Read', 'acl:mode': 'acl:Read', 'acl:agentClass': 'foaf:Agent' };
test('public: the event AND its collection are granted (the collection already is: nothing added)', async () => {
const granted = [];
const pod = {
'pod-resources.get': stored(publicEvent({ 'apods:attendees': C })),
'pod-permissions.get': ({ uri }) => (uri === C ? [COLLECTION_DEFAULT] : [DEFAULT_READ]),
'pod-permissions.add': ({ uri }) => granted.push(uri)
};
const { svc, ctx, calls } = service(pod);
await svc.onUpdate(ctx, E, ALICE);
expect(calls.filter(c => c.action === 'pod-permissions.fetch').map(c => c.params.url)).toEqual([
'http://localhost:3000/_acl/alice/data/events/e1',
'http://localhost:3000/_acl/alice/data/collections/e1-attendees'
]);
expect(granted).toEqual([E]);
});
test('private: the public read of the event AND of its collection are revoked', async () => {
const puts = [];
const pod = {
'pod-resources.get': stored(privateEvent({ 'apods:attendees': C })),
'pod-permissions.get': ({ uri }) => (uri === C ? [COLLECTION_DEFAULT] : [DEFAULT_READ, PUBLIC_READ_AUTH]),
'app.get': () => ({ id: APP }),
'signature.proxy.query': params => {
puts.push(params.url);
return { status: 204 };
}
};
const { svc, ctx } = service(pod);
await svc.onUpdate(ctx, E, ALICE);
expect(puts).toEqual(['http://localhost:3000/_acl/alice/data/events/e1', 'http://localhost:3000/_acl/alice/data/collections/e1-attendees']);
});
test('private → public: the collection is granted back', async () => {
const granted = [];
const pod = {
'pod-resources.get': stored(publicEvent({ 'http://activitypods.org/ns/core#attendees': { id: C } })),
'pod-permissions.get': () => [DEFAULT_READ],
'pod-permissions.add': ({ uri }) => granted.push(uri)
};
const { svc, ctx } = service(pod);
await svc.onUpdate(ctx, E, ALICE);
expect(granted).toEqual([E, C]);
});
test('a collection named outside the event\'s pod is left alone', async () => {
const foreign = `${BOB}/data/collections/x`;
const pod = { 'pod-resources.get': stored(privateEvent({ 'apods:attendees': foreign })), 'pod-permissions.get': () => [DEFAULT_READ] };
const { svc, ctx, calls, logs } = service(pod);
await svc.onUpdate(ctx, E, ALICE);
expect(calls.some(c => c.params && c.params.uri === foreign)).toBe(false);
expect(logs.warn.some(m => m.includes('outside its pod'))).toBe(true);
});
test('a public read INHERITED from the container cannot be revoked: reported, no PUT', async () => {
const inherited = { '@id': '#DefaultRead', 'acl:mode': 'acl:Read', 'acl:agentClass': 'foaf:Agent' };
const pod = {
'pod-resources.get': stored(privateEvent({ 'apods:attendees': C })),
'pod-permissions.get': ({ uri }) => (uri === C ? [inherited] : [DEFAULT_READ]),
'signature.proxy.query': () => {
throw new Error('must not PUT');
}
};
const { svc, ctx, logs } = service(pod);
await svc.onUpdate(ctx, E, ALICE);
expect(logs.warn.some(m => m.includes(`${C} is publicly readable through a rule inherited`))).toBe(true);
expect(logs.error).toEqual([]);
});
});
describe('public-read — revokes ONLY a public read Festipod granted (the persisted grant ledger)', () => {
test('a public read Festipod did not add (owner, another app) is left untouched and logged', async () => {
const pod = {
'pod-resources.get': stored(privateEvent()),
'pod-permissions.get': () => [DEFAULT_READ, PUBLIC_READ_AUTH],
'signature.proxy.query': () => {
throw new Error('must not PUT');
}
};
const { svc, ctx, calls, logs } = service(pod, { granted: [] });
await svc.onUpdate(ctx, privateEvent(), ALICE);
expect(calls.some(c => c.action === 'signature.proxy.query')).toBe(false);
expect(logs.info.some(m => m.includes('not through a grant Festipod made: left untouched'))).toBe(true);
expect(logs.error).toEqual([]);
});
test('a grant is recorded BEFORE it is made, and a revocation forgets it', async () => {
let aclIsPublic = false;
let recordedWhenAdded;
const pod = {
'pod-resources.get': stored(publicEvent()),
'pod-permissions.get': () => (aclIsPublic ? [DEFAULT_READ, PUBLIC_READ_AUTH] : [DEFAULT_READ]),
'app.get': () => ({ id: APP }),
'signature.proxy.query': () => {
aclIsPublic = false;
return { status: 204 };
}
};
const w = service(pod, { granted: [] });
pod['pod-permissions.add'] = () => {
recordedWhenAdded = w.ledger.has(E);
aclIsPublic = true;
return true;
};
await w.svc.onUpdate(w.ctx, E, ALICE);
expect(recordedWhenAdded).toBe(true);
expect(w.ledger.has(E)).toBe(true);
pod['pod-resources.get'] = stored(privateEvent());
await w.svc.onUpdate(w.ctx, E, ALICE);
expect(aclIsPublic).toBe(false);
expect(w.ledger.has(E)).toBe(false);
});
test('already world-readable before Festipod: nothing granted, nothing recorded', async () => {
const pod = { 'pod-resources.get': stored(publicEvent()), 'pod-permissions.get': () => [DEFAULT_READ, PUBLIC_READ_AUTH] };
const w = service(pod, { granted: [] });
await w.svc.onUpdate(w.ctx, E, ALICE);
expect(w.ledger.has(E)).toBe(false);
});
test('a grant the pod refuses is not left in the record', async () => {
const pod = { 'pod-resources.get': stored(publicEvent()), 'pod-permissions.get': () => [DEFAULT_READ], 'pod-permissions.add': () => false };
const w = service(pod, { granted: [] });
await w.svc.onUpdate(w.ctx, E, ALICE);
expect(w.ledger.has(E)).toBe(false);
expect(w.logs.error.some(m => m.includes('refused the public-read grant'))).toBe(true);
});
test('recordCreated records a collection Festipod created, without depending on an ACL read', async () => {
const w = service({}, { granted: [] }); // no ACL reachable at all
expect(await w.svc.actions.recordCreated({ resourceUri: C, actorUri: ALICE })).toBe(true);
expect([...w.ledger]).toEqual([C]);
});
test('the ledger predicate is Festipod\'s own', () => {
expect(GRANTED_AT).toBe('http://festipod.org/publicReadGrantedAt');
expect(grantedQuery(E)).toBe(`ASK { <${E}> <${GRANTED_AT}> ?at }`);
});
});
describe('public-read — an unreadable ACL is UNKNOWN, never "not public" (second review)', () => {
const OWNER_SHARE = [DEFAULT_READ, PUBLIC_READ_AUTH];
test('revoke on a transient ACL failure keeps Festipod\'s record; the next notification revokes', async () => {
let gets = 0;
let puts = 0;
const pod = {
'pod-resources.get': stored(privateEvent()),
'pod-permissions.get': () => (gets++ === 0 ? false : OWNER_SHARE), // one non-200, then the real ACL
'app.get': () => ({ id: APP }),
'signature.proxy.query': () => {
puts++;
return { status: 204 };
}
};
const w = service(pod, { granted: [E] });
await w.svc.onUpdate(w.ctx, E, ALICE);
expect(puts).toBe(0);
expect(w.ledger.has(E)).toBe(true); // kept: the state was unknown
expect(w.logs.warn.some(m => m.includes('could not be read'))).toBe(true);
await w.svc.onUpdate(w.ctx, E, ALICE);
expect(puts).toBe(1);
expect(w.ledger.has(E)).toBe(false);
});
test("grant on a transient ACL failure records nothing, so the owner's own share is never taken for Festipod's", async () => {
let gets = 0;
let puts = 0;
let current = publicEvent();
const pod = {
'pod-resources.get': () => ({ status: 200, body: current }),
'pod-permissions.get': () => (gets++ === 0 ? false : OWNER_SHARE), // the owner already shared it publicly
'pod-permissions.add': () => true,
'app.get': () => ({ id: APP }),
'signature.proxy.query': () => {
puts++;
return { status: 204 };
}
};
const w = service(pod, { granted: [] });
await w.svc.onUpdate(w.ctx, E, ALICE); // ACL unreadable: no grant, no record
expect(w.ledger.has(E)).toBe(false);
current = privateEvent();
await w.svc.onUpdate(w.ctx, E, ALICE);
expect(puts).toBe(0); // the owner's public share is left untouched
});
});
describe('public-read — a timed-out reconcile writes nothing more (second review)', () => {
test('a stale "public" reconcile stalled in its ACL read neither records nor grants after a newer "private" one', async () => {
let reads = 0;
let aclGets = 0;
let aclPublic = false;
const pod = {
'pod-resources.get': () => ({ status: 200, body: reads++ === 0 ? publicEvent() : privateEvent() }),
'pod-permissions.get': async () => {
if (aclGets++ === 0) await new Promise(resolve => setTimeout(resolve, 120)); // stalls past the timeout
return aclPublic ? [DEFAULT_READ, PUBLIC_READ_AUTH] : [DEFAULT_READ];
},
'pod-permissions.add': () => {
aclPublic = true;
return true;
}
};
const w = service(pod, { granted: [], timeoutMs: 50 });
const stale = w.svc.reconcileLogged(w.ctx, E, ALICE); // reads: public
await new Promise(resolve => setTimeout(resolve, 10));
const newer = w.svc.reconcileLogged(w.ctx, E, ALICE); // reads: private (the owner's newer edit)
await Promise.all([stale, newer]);
await new Promise(resolve => setTimeout(resolve, 250)); // the stale one resumes; its re-decision runs
expect(aclPublic).toBe(false);
expect(w.ledger.has(E)).toBe(false);
});
test('a stale "private" reconcile stalled in its ACL read does not revoke after a newer "public" one', async () => {
let reads = 0;
let aclGets = 0;
let aclPublic = true;
const pod = {
'pod-resources.get': () => ({ status: 200, body: reads++ === 0 ? privateEvent() : publicEvent() }),
'pod-permissions.get': async () => {
if (aclGets++ === 0) await new Promise(resolve => setTimeout(resolve, 120));
return aclPublic ? [DEFAULT_READ, PUBLIC_READ_AUTH] : [DEFAULT_READ];
},
'pod-permissions.add': () => {
aclPublic = true;
return true;
},
'app.get': () => ({ id: APP }),
'signature.proxy.query': () => {
aclPublic = false;
return { status: 204 };
}
};
const w = service(pod, { granted: [E], timeoutMs: 50 });
const stale = w.svc.reconcileLogged(w.ctx, E, ALICE); // reads: private
await new Promise(resolve => setTimeout(resolve, 10));
const newer = w.svc.reconcileLogged(w.ctx, E, ALICE); // reads: public
await Promise.all([stale, newer]);
await new Promise(resolve => setTimeout(resolve, 250));
expect(aclPublic).toBe(true);
});
test('a grant that lands AFTER its timeout is undone by the re-decision that follows', async () => {
let current = publicEvent();
let aclPublic = false;
let adds = 0;
const pod = {
'pod-resources.get': () => ({ status: 200, body: current }),
'pod-permissions.get': () => (aclPublic ? [DEFAULT_READ, PUBLIC_READ_AUTH] : [DEFAULT_READ]),
'pod-permissions.add': async () => {
if (adds++ === 0) await new Promise(resolve => setTimeout(resolve, 120)); // sent in time, lands late
aclPublic = true;
return true;
},
'app.get': () => ({ id: APP }),
'signature.proxy.query': () => {
aclPublic = false;
return { status: 204 };
}
};
const w = service(pod, { granted: [], timeoutMs: 50 });
const stale = w.svc.reconcileLogged(w.ctx, E, ALICE);
await new Promise(resolve => setTimeout(resolve, 60)); // timed out with its PATCH in flight
current = privateEvent();
const newer = w.svc.reconcileLogged(w.ctx, E, ALICE); // runs before the late PATCH lands
await Promise.all([stale, newer]);
await new Promise(resolve => setTimeout(resolve, 250));
expect(aclPublic).toBe(false);
});
});
describe('public-read — late-settle handling only after a late WRITE, and never chained (round-4 review)', () => {
test('a slow pod and NO late write: a timed-out reconcile schedules nothing', async () => {
let reads = 0;
const pod = {
'pod-resources.get': async () => {
reads++;
await new Promise(resolve => setTimeout(resolve, 120));
return { status: 200, body: publicEvent() };
},
'pod-permissions.get': () => [DEFAULT_READ, PUBLIC_READ_AUTH]
};
const w = service(pod, { granted: [], timeoutMs: 50 });
await w.svc.onUpdate(w.ctx, E, ALICE);
await new Promise(resolve => setTimeout(resolve, 1000));
expect(reads).toBe(1);
});
test('a re-decision after a late write schedules no other, even when it writes late itself', async () => {
let reads = 0;
let aclPublic = false;
const slow = () => new Promise(resolve => setTimeout(resolve, 120));
const pod = {
// The owner keeps flipping the event: every read sees the other visibility.
'pod-resources.get': () => ({ status: 200, body: reads++ % 2 === 0 ? publicEvent() : privateEvent() }),
'pod-permissions.get': () => (aclPublic ? [DEFAULT_READ, PUBLIC_READ_AUTH] : [DEFAULT_READ]),
'pod-permissions.add': async () => {
await slow(); // every write lands after the limit
aclPublic = true;
return true;
},
'app.get': () => ({ id: APP }),
'signature.proxy.query': async () => {
await slow();
aclPublic = false;
return { status: 204 };
}
};
const w = service(pod, { granted: [], timeoutMs: 50 });
await w.svc.onUpdate(w.ctx, E, ALICE);
await new Promise(resolve => setTimeout(resolve, 1200));
expect(reads).toBe(2); // the reconcile and its ONE re-decision — no chain
});
});