@gregiteen/ssss-cli 0.9.0 → 0.9.2
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.
- package/CHANGELOG.md +83 -4
- package/README.md +41 -82
- package/VERSION +1 -1
- package/conformance/reference-bundle.ucw.json +19 -1
- package/docs/help/concurrency.md +45 -0
- package/docs/help/overview.md +8 -4
- package/docs/ssss-spec.md +11 -10
- package/package.json +1 -1
- package/registry/core.json +1 -1
- package/scripts/conformance-09.mjs +250 -8
- package/scripts/conformance.mjs +47 -0
- package/scripts/generate-sbom.mjs +96 -0
- package/scripts/phase9-cross-host-corpus.mjs +223 -0
- package/scripts/release.sh +20 -13
- package/skills/ssss/SKILL.md +32 -204
- package/src/authorization.mjs +2 -1
- package/src/bundle.mjs +37 -1
- package/src/events.mjs +83 -10
- package/src/file-lock.mjs +112 -0
- package/src/idempotency.mjs +15 -7
- package/src/leases.mjs +77 -16
- package/src/projections.mjs +72 -1
- package/src/vfs.mjs +33 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { definePrimitive, definitionsToExtensionRegistry } from '../src/primitive.mjs';
|
|
@@ -13,14 +14,206 @@ import {
|
|
|
13
14
|
import { createValidator } from '../src/validator.mjs';
|
|
14
15
|
import { FileSystemVfs, MemoryVfs, runVfsContract } from '../src/vfs.mjs';
|
|
15
16
|
import { JsonlEventStore, MemoryEventStore, createCanonicalEvent } from '../src/events.mjs';
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
ProjectionCoordinator,
|
|
19
|
+
createSqlProjectionAdapter,
|
|
20
|
+
createSearchProjectionAdapter,
|
|
21
|
+
createQueueProjectionAdapter,
|
|
22
|
+
createViewModelProjectionAdapter,
|
|
23
|
+
} from '../src/projections.mjs';
|
|
17
24
|
import { createKernel } from '../src/kernel.mjs';
|
|
18
25
|
import { UiRegistry, actionToEnvelope, createDeterministicUi, planUi, validateUiManifest } from '../src/ui.mjs';
|
|
19
26
|
import { createCommandHandler, createDomainCommand } from '../src/http.mjs';
|
|
20
27
|
import { detectDirectWrites } from '../src/guard.mjs';
|
|
21
|
-
import { MemoryLeaseStore, FileLeaseStore, runLeaseContract } from '../src/leases.mjs';
|
|
28
|
+
import { MemoryLeaseStore, FileLeaseStore, leaseStatePath, runLeaseContract } from '../src/leases.mjs';
|
|
22
29
|
import { MemoryIdempotencyStore, FileIdempotencyStore, runIdempotencyContract } from '../src/idempotency.mjs';
|
|
23
30
|
import { createCapabilityAuthorizer } from '../src/authorization.mjs';
|
|
31
|
+
import { validateVerifiedPrincipal } from '../src/authorization.mjs';
|
|
32
|
+
|
|
33
|
+
async function rejects(promise, pattern) {
|
|
34
|
+
try { await promise; return false; } catch (error) { return pattern.test(error.message); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function replayed(store, options) {
|
|
38
|
+
const items = [];
|
|
39
|
+
for await (const item of store.replay(options)) items.push(item);
|
|
40
|
+
return items;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Counts bytes read from `file` during `fn`, to prove appends do not re-read the log.
|
|
44
|
+
async function bytesReadFrom(file, fn) {
|
|
45
|
+
const original = { openSync: fs.openSync, closeSync: fs.closeSync, readSync: fs.readSync, readFileSync: fs.readFileSync };
|
|
46
|
+
const fds = new Set();
|
|
47
|
+
let bytes = 0;
|
|
48
|
+
fs.openSync = (target, ...rest) => { const fd = original.openSync(target, ...rest); if (target === file) fds.add(fd); return fd; };
|
|
49
|
+
fs.closeSync = (fd) => { fds.delete(fd); return original.closeSync(fd); };
|
|
50
|
+
fs.readSync = (fd, ...rest) => { const read = original.readSync(fd, ...rest); if (fds.has(fd)) bytes += read; return read; };
|
|
51
|
+
fs.readFileSync = (target, ...rest) => {
|
|
52
|
+
const out = original.readFileSync(target, ...rest);
|
|
53
|
+
if (target === file || fds.has(target)) bytes += Buffer.byteLength(out);
|
|
54
|
+
return out;
|
|
55
|
+
};
|
|
56
|
+
try { await fn(); } finally { Object.assign(fs, original); }
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Runs `body` in `count` child processes released on one shared deadline, so
|
|
61
|
+
// their check-then-write sections overlap. Each resolves to { ok, racer, error }.
|
|
62
|
+
function raceProcesses(count, imports, body) {
|
|
63
|
+
const startAt = Date.now() + 1000;
|
|
64
|
+
const code = [
|
|
65
|
+
...Object.entries(imports).map(([name, file]) => `import { ${name} } from ${JSON.stringify(new URL(file, import.meta.url).href)};`),
|
|
66
|
+
'const racer = Number(process.env.SSSS_RACER);',
|
|
67
|
+
`while (Date.now() < ${startAt});`,
|
|
68
|
+
`try { ${body}; console.log(JSON.stringify({ ok: true, racer })); }`,
|
|
69
|
+
'catch (error) { console.log(JSON.stringify({ ok: false, racer, error: error.message })); }',
|
|
70
|
+
].join('\n');
|
|
71
|
+
return Promise.all(Array.from({ length: count }, (_, racer) => new Promise((resolve) => {
|
|
72
|
+
const child = spawn(process.execPath, ['--input-type=module', '-e', code], { env: { ...process.env, SSSS_RACER: String(racer) } });
|
|
73
|
+
let out = '';
|
|
74
|
+
let err = '';
|
|
75
|
+
child.stdout.on('data', (chunk) => { out += chunk; });
|
|
76
|
+
child.stderr.on('data', (chunk) => { err += chunk; });
|
|
77
|
+
child.on('close', () => {
|
|
78
|
+
try { resolve(JSON.parse(out.trim().split('\n').pop())); }
|
|
79
|
+
catch { resolve({ ok: false, racer, error: `no result: ${err || out}` }); }
|
|
80
|
+
});
|
|
81
|
+
})));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const winners = (results) => results.filter((result) => result.ok);
|
|
85
|
+
const describe = (results) => JSON.stringify(results.map(({ ok, racer, error }) => (ok ? racer : `${racer}:${error}`)));
|
|
86
|
+
|
|
87
|
+
async function checkFileAdapterRaces(root, principal, check) {
|
|
88
|
+
const racers = 6;
|
|
89
|
+
const eventsRoot = path.join(root, 'events');
|
|
90
|
+
const event = createCanonicalEvent({ workspace_id: 'acme', action: 'event', subject: 'events/race.md', operation_id: 'race', idempotency_key: 'race', principal });
|
|
91
|
+
const appends = await raceProcesses(racers, { JsonlEventStore: '../src/events.mjs' },
|
|
92
|
+
`await new JsonlEventStore(${JSON.stringify(eventsRoot)}).append(${JSON.stringify(event)})`);
|
|
93
|
+
const lines = fs.readFileSync(path.join(eventsRoot, 'acme.jsonl'), 'utf8').split('\n').filter(Boolean);
|
|
94
|
+
check('jsonl event store lets one of several processes append the same event_id', winners(appends).length === 1 && lines.length === 1, describe(appends));
|
|
95
|
+
|
|
96
|
+
const vfsRoot = path.join(root, 'vfs');
|
|
97
|
+
const creates = await raceProcesses(racers, { FileSystemVfs: '../src/vfs.mjs' },
|
|
98
|
+
`await new FileSystemVfs(${JSON.stringify(vfsRoot)}).writeAtomic('race/create.md', 'racer-' + racer, { ifAbsent: true })`);
|
|
99
|
+
const created = fs.readFileSync(path.join(vfsRoot, 'race', 'create.md'), 'utf8');
|
|
100
|
+
check('filesystem VFS lets one of several processes create the same path',
|
|
101
|
+
winners(creates).length === 1 && created === `racer-${winners(creates)[0]?.racer}`, describe(creates));
|
|
102
|
+
const base = await new FileSystemVfs(vfsRoot).writeAtomic('race/cas.md', 'base', { ifAbsent: true });
|
|
103
|
+
const swaps = await raceProcesses(racers, { FileSystemVfs: '../src/vfs.mjs' },
|
|
104
|
+
`await new FileSystemVfs(${JSON.stringify(vfsRoot)}).writeAtomic('race/cas.md', 'racer-' + racer, { version: ${JSON.stringify(base.version)} })`);
|
|
105
|
+
const swapped = fs.readFileSync(path.join(vfsRoot, 'race', 'cas.md'), 'utf8');
|
|
106
|
+
check('filesystem VFS lets one of several processes compare-and-swap the same version',
|
|
107
|
+
winners(swaps).length === 1 && swapped === `racer-${winners(swaps)[0]?.racer}`, describe(swaps));
|
|
108
|
+
|
|
109
|
+
const leaseRoot = path.join(root, 'leases');
|
|
110
|
+
const leases = await raceProcesses(racers, { FileLeaseStore: '../src/leases.mjs' },
|
|
111
|
+
`await new FileLeaseStore(${JSON.stringify(leaseRoot)}).acquire({ workspace_id: 'acme', target: 'race.md', principal_id: 'p' + racer, operation_id: 'o' + racer }, 60000)`);
|
|
112
|
+
const held = JSON.parse(fs.readFileSync(leaseStatePath(fs.realpathSync(leaseRoot), 'acme', 'race.md'), 'utf8'));
|
|
113
|
+
check('filesystem lease store grants one of several processes the same lease',
|
|
114
|
+
winners(leases).length === 1 && held.principal_id === `p${winners(leases)[0]?.racer}`, describe(leases));
|
|
115
|
+
|
|
116
|
+
const idempotencyRoot = path.join(root, 'idempotency');
|
|
117
|
+
const puts = await raceProcesses(racers, { FileIdempotencyStore: '../src/idempotency.mjs' },
|
|
118
|
+
`await new FileIdempotencyStore(${JSON.stringify(idempotencyRoot)}).put('acme', 'race', { request_hash: 'racer-' + racer })`);
|
|
119
|
+
const stored = await new FileIdempotencyStore(idempotencyRoot).get('acme', 'race');
|
|
120
|
+
check('filesystem idempotency store accepts one of several processes putting the same key',
|
|
121
|
+
winners(puts).length === 1 && stored?.request_hash === `racer-${winners(puts)[0]?.racer}`, describe(puts));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function checkFileLocks(root, principal, check) {
|
|
125
|
+
const make = (id) => createCanonicalEvent({ workspace_id: 'acme', action: 'event', subject: 'events/lock.md', operation_id: id, idempotency_key: id, principal });
|
|
126
|
+
const lock = path.join(root, 'acme.jsonl.lock');
|
|
127
|
+
const holder = (fields) => fs.writeFileSync(lock, JSON.stringify({ token: 'other', host: os.hostname(), ...fields }));
|
|
128
|
+
const store = new JsonlEventStore(root, { lock: { timeoutMs: 500, staleMs: 60_000 } });
|
|
129
|
+
|
|
130
|
+
holder({ pid: process.pid });
|
|
131
|
+
const started = Date.now();
|
|
132
|
+
check('file locks make a writer wait for a live holder and then time out',
|
|
133
|
+
await rejects(store.append(make('lock-live')), /Timed out waiting for lock/) && Date.now() - started >= 450 && fs.existsSync(lock));
|
|
134
|
+
const exited = spawnSync(process.execPath, ['-e', '']).pid;
|
|
135
|
+
holder({ pid: exited });
|
|
136
|
+
await store.append(make('lock-dead'));
|
|
137
|
+
check('file locks break a lock whose holder process has exited', !fs.existsSync(lock));
|
|
138
|
+
holder({ pid: process.pid, host: 'another-host' });
|
|
139
|
+
const past = new Date(Date.now() - 120_000);
|
|
140
|
+
fs.utimesSync(lock, past, past);
|
|
141
|
+
await new JsonlEventStore(root, { lock: { timeoutMs: 500, staleMs: 30_000 } }).append(make('lock-stale'));
|
|
142
|
+
check('file locks break a lock older than the stale threshold', !fs.existsSync(lock));
|
|
143
|
+
const decoy = path.join(root, 'decoy.txt');
|
|
144
|
+
fs.writeFileSync(decoy, 'untouched');
|
|
145
|
+
fs.symlinkSync(decoy, lock);
|
|
146
|
+
check('file locks refuse a symlinked lock file',
|
|
147
|
+
await rejects(store.append(make('lock-symlink')), /not a regular file/) && fs.readFileSync(decoy, 'utf8') === 'untouched');
|
|
148
|
+
fs.rmSync(lock);
|
|
149
|
+
|
|
150
|
+
const vfs = new FileSystemVfs(path.join(root, 'vfs'));
|
|
151
|
+
await vfs.writeAtomic('docs/one.md', 'one', { ifAbsent: true });
|
|
152
|
+
fs.writeFileSync(path.join(vfs.root, '.ssss-locks', 'leftover.lock'), '{}');
|
|
153
|
+
const listed = [];
|
|
154
|
+
for await (const entry of vfs.list()) listed.push(entry.path);
|
|
155
|
+
check('filesystem VFS reserves its lock directory',
|
|
156
|
+
await rejects(vfs.writeAtomic('.ssss-locks/forged.lock', 'x'), /reserved/)
|
|
157
|
+
&& await rejects(vfs.writeAtomic('.SSSS-Locks/forged.lock', 'x'), /reserved/)
|
|
158
|
+
&& JSON.stringify(listed) === JSON.stringify(['docs/one.md']));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function checkJsonlEventStoreIndex(root, principal, check) {
|
|
162
|
+
let seq = 0;
|
|
163
|
+
const make = (workspace = 'acme') => createCanonicalEvent({
|
|
164
|
+
workspace_id: workspace, action: 'event', subject: 'events/indexed.md',
|
|
165
|
+
operation_id: `indexed-${++seq}`, idempotency_key: `indexed-${seq}`, principal,
|
|
166
|
+
});
|
|
167
|
+
const first = new JsonlEventStore(root);
|
|
168
|
+
const second = new JsonlEventStore(root);
|
|
169
|
+
const log = path.join(first.root, 'acme.jsonl');
|
|
170
|
+
const appended = [];
|
|
171
|
+
const add = async (store, event) => { await store.append(event); appended.push(event.event_id); return event; };
|
|
172
|
+
|
|
173
|
+
const own = await add(first, make());
|
|
174
|
+
check('jsonl event store rejects a duplicate from its own index', await rejects(first.append(own), /Duplicate event_id/));
|
|
175
|
+
const foreign = await add(second, make());
|
|
176
|
+
check('jsonl event store rejects a duplicate appended by another store instance', await rejects(first.append(foreign), /Duplicate event_id/));
|
|
177
|
+
const external = make();
|
|
178
|
+
execFileSync(process.execPath, ['--input-type=module', '-e', [
|
|
179
|
+
`import { JsonlEventStore } from ${JSON.stringify(new URL('../src/events.mjs', import.meta.url).href)};`,
|
|
180
|
+
`await new JsonlEventStore(${JSON.stringify(root)}).append(${JSON.stringify(external)});`,
|
|
181
|
+
].join('\n')]);
|
|
182
|
+
appended.push(external.event_id);
|
|
183
|
+
check('jsonl event store rejects a duplicate appended by another process', await rejects(second.append(external), /Duplicate event_id/));
|
|
184
|
+
await add(first, make());
|
|
185
|
+
check('jsonl event store rejects a duplicate after interleaved writers', await rejects(second.append(own), /Duplicate event_id/));
|
|
186
|
+
|
|
187
|
+
const seeded = fs.statSync(log).size;
|
|
188
|
+
const fresh = new JsonlEventStore(root);
|
|
189
|
+
const batchBytes = await bytesReadFrom(log, async () => { for (let i = 0; i < 200; i++) await add(fresh, make()); });
|
|
190
|
+
check('jsonl event store reads the log once for many appends from one instance', batchBytes === seeded, `read ${batchBytes} bytes; log was ${seeded}`);
|
|
191
|
+
await add(second, make());
|
|
192
|
+
const revalidateBytes = await bytesReadFrom(log, () => add(fresh, make()));
|
|
193
|
+
check('jsonl event store rebuilds its index after another writer appends', revalidateBytes > seeded);
|
|
194
|
+
|
|
195
|
+
const other = [];
|
|
196
|
+
for (let i = 0; i < 3; i++) other.push((await (i % 2 ? second : first).append(make('aaa'))).event_id);
|
|
197
|
+
const acme = await replayed(fresh, { workspaceId: 'acme' });
|
|
198
|
+
check('jsonl event store replays one log in append order',
|
|
199
|
+
JSON.stringify(acme.map((item) => item.event.event_id)) === JSON.stringify(appended)
|
|
200
|
+
&& acme.every((item, index) => item.cursor === index + 1));
|
|
201
|
+
const all = await replayed(fresh, {});
|
|
202
|
+
const expected = [...other, ...appended];
|
|
203
|
+
const resumed = await replayed(fresh, { cursor: 2 });
|
|
204
|
+
check('jsonl event store replays all logs in file order and resumes from a cursor',
|
|
205
|
+
JSON.stringify(all.map((item) => item.event.event_id)) === JSON.stringify(expected)
|
|
206
|
+
&& JSON.stringify(resumed.map((item) => item.event.event_id)) === JSON.stringify(expected.slice(2)));
|
|
207
|
+
|
|
208
|
+
if (process.platform !== 'win32') {
|
|
209
|
+
check('jsonl event logs are created with mode 0600', (fs.statSync(path.join(root, 'aaa.jsonl')).mode & 0o777) === 0o600);
|
|
210
|
+
}
|
|
211
|
+
const target = path.join(root, 'target.txt');
|
|
212
|
+
fs.writeFileSync(target, '');
|
|
213
|
+
fs.symlinkSync(target, path.join(root, 'linked.jsonl'));
|
|
214
|
+
check('jsonl event store refuses symlinked logs',
|
|
215
|
+
await rejects(fresh.append(make('linked')), /Symlinked event logs are forbidden/) && fs.readFileSync(target, 'utf8') === '');
|
|
216
|
+
}
|
|
24
217
|
|
|
25
218
|
export async function runKernel09Conformance() {
|
|
26
219
|
const checks = [];
|
|
@@ -93,11 +286,23 @@ export async function runKernel09Conformance() {
|
|
|
93
286
|
['filesystem', new FileLeaseStore(path.join(temp, 'leases'))],
|
|
94
287
|
]) check(`${name} lease store passes the shared contract`, (await runLeaseContract(store)).passed);
|
|
95
288
|
|
|
96
|
-
const authorize = createCapabilityAuthorizer(
|
|
289
|
+
const authorize = createCapabilityAuthorizer({
|
|
290
|
+
policyFloors: { [definition.primitive_id]: { delete: ['booking:admin'] } },
|
|
291
|
+
});
|
|
292
|
+
check('verified principal contract requires provenance', !validateVerifiedPrincipal({ id: 'x', kind: 'human', workspaceIds: [] }).valid);
|
|
293
|
+
check('verified principal contract accepts complete identity', validateVerifiedPrincipal({
|
|
294
|
+
id: 'greg', kind: 'human', workspaceIds: ['acme'], authentication: { provider: 'conformance', assurance: 'verified' },
|
|
295
|
+
}).valid);
|
|
97
296
|
const protectedPrincipal = { id: 'agent', kind: 'agent', workspaceIds: ['acme'], capabilities: ['booking:update'], authentication: { provider: 'test', assurance: 'verified' } };
|
|
98
297
|
const protectedContext = { principal: protectedPrincipal, workspaceId: 'acme', definition, action: 'patch', context: { requiredAssurance: 'elevated' } };
|
|
99
298
|
check('protected actions require authentication step-up', !(await authorize(protectedContext)).allowed);
|
|
100
299
|
check('protected actions require explicit verified-human confirmation', !(await authorize({ ...protectedContext, principal: { ...protectedPrincipal, kind: 'human', authentication: { provider: 'test', assurance: 'elevated' } }, context: { requiresHumanConfirmation: true } })).allowed);
|
|
300
|
+
check('policy floors fail closed without elevated capabilities', !(await authorize({
|
|
301
|
+
principal: { ...protectedPrincipal, capabilities: ['booking:delete'] },
|
|
302
|
+
workspaceId: 'acme',
|
|
303
|
+
definition,
|
|
304
|
+
action: 'delete',
|
|
305
|
+
})).allowed);
|
|
101
306
|
|
|
102
307
|
const vfs = new MemoryVfs();
|
|
103
308
|
const eventStore = new MemoryEventStore();
|
|
@@ -110,27 +315,61 @@ export async function runKernel09Conformance() {
|
|
|
110
315
|
reset: async () => projected.clear(),
|
|
111
316
|
snapshot: async () => Object.fromEntries([...projected.entries()].sort()),
|
|
112
317
|
});
|
|
113
|
-
const
|
|
318
|
+
const sql = createSqlProjectionAdapter();
|
|
319
|
+
const search = createSearchProjectionAdapter();
|
|
320
|
+
const queue = createQueueProjectionAdapter();
|
|
321
|
+
const viewModel = createViewModelProjectionAdapter();
|
|
322
|
+
for (const adapter of [sql, search, queue, viewModel]) coordinator.register(adapter);
|
|
323
|
+
const leaseStore = new MemoryLeaseStore();
|
|
324
|
+
const kernel = createKernel({
|
|
325
|
+
vfs, eventStore, projectionCoordinator: coordinator, validator, leaseStore, authorize,
|
|
326
|
+
});
|
|
114
327
|
const principal = {
|
|
115
|
-
id: 'greg', kind: 'human', workspaceIds: ['acme'], capabilities: ['booking:create', 'booking:update', 'booking:delete'],
|
|
328
|
+
id: 'greg', kind: 'human', workspaceIds: ['acme'], capabilities: ['booking:create', 'booking:update', 'booking:delete', 'booking:admin'],
|
|
116
329
|
authentication: { provider: 'conformance', assurance: 'verified' },
|
|
117
330
|
};
|
|
118
331
|
const createEnvelope = { type: 'operation', workspace_id: 'acme', idempotency_key: 'create-booking', path: 'bookings/one.md', content };
|
|
119
332
|
const created = await kernel.execute(createEnvelope, { principal });
|
|
120
333
|
check('shared kernel commits an authorized runtime primitive', created.success, JSON.stringify(created));
|
|
334
|
+
const leaseDenied = await kernel.execute({
|
|
335
|
+
type: 'patch', workspace_id: 'acme', idempotency_key: 'lease-denied', path: 'bookings/one.md', patches: { status: 'confirmed' },
|
|
336
|
+
}, { principal, requireLease: true });
|
|
337
|
+
check('kernel fails closed when a required lease is missing', leaseDenied.success === false && /lease/i.test(leaseDenied.validation.errors.join(' ')));
|
|
338
|
+
const lease = await leaseStore.acquire({
|
|
339
|
+
workspace_id: 'acme', target: 'bookings/one.md', principal_id: principal.id, operation_id: 'lease-ok',
|
|
340
|
+
}, 30_000);
|
|
341
|
+
const leased = await kernel.execute({
|
|
342
|
+
type: 'patch', workspace_id: 'acme', idempotency_key: 'lease-ok', path: 'bookings/one.md',
|
|
343
|
+
patches: { status: 'confirmed' }, lease_id: lease.lease_id, operation_id: 'lease-ok',
|
|
344
|
+
}, { principal, requireLease: true });
|
|
345
|
+
check('kernel accepts a matching verified lease', leased.success, JSON.stringify(leased));
|
|
121
346
|
const replay = await kernel.execute(createEnvelope, { principal });
|
|
122
347
|
check('exact retries replay idempotently', replay.replay === true);
|
|
123
348
|
const conflict = await kernel.execute({ ...createEnvelope, content: content.replace('status: pending', 'status: confirmed') }, { principal });
|
|
124
349
|
check('idempotency key reuse with changed content conflicts', conflict.success === false);
|
|
125
|
-
const patched = await kernel.execute({ type: 'patch', workspace_id: 'acme', idempotency_key: 'patch-booking', path: 'bookings/one.md', patches: { status: '
|
|
126
|
-
check('shared kernel patches and events the primitive', patched.success && await eventStore.size() ===
|
|
350
|
+
const patched = await kernel.execute({ type: 'patch', workspace_id: 'acme', idempotency_key: 'patch-booking', path: 'bookings/one.md', patches: { status: 'pending' } }, { principal });
|
|
351
|
+
check('shared kernel patches and events the primitive', patched.success && await eventStore.size() === 3);
|
|
127
352
|
check('projection dispatch follows committed events', projected.has('bookings/one.md'));
|
|
353
|
+
const sqlSnap = await sql.snapshot();
|
|
354
|
+
const searchSnap = await search.snapshot();
|
|
355
|
+
const queueSnap = await queue.snapshot();
|
|
356
|
+
const viewSnap = await viewModel.snapshot();
|
|
357
|
+
check('SQL/search/queue/view-model adapters receive events',
|
|
358
|
+
Object.values(sqlSnap).some((row) => row.subject === 'bookings/one.md')
|
|
359
|
+
&& Object.hasOwn(searchSnap, 'bookings/one.md')
|
|
360
|
+
&& queueSnap.length >= 1
|
|
361
|
+
&& viewSnap['bookings/one.md']?.last_action);
|
|
128
362
|
const rebuilt = await coordinator.replay('bookings', { rebuild: true });
|
|
129
|
-
check('projection is rebuildable from the event log', rebuilt.applied ===
|
|
363
|
+
check('projection is rebuildable from the event log', rebuilt.applied === 3 && projected.has('bookings/one.md'));
|
|
130
364
|
const expectedProjectionHash = rebuilt.output_hash;
|
|
131
365
|
projected.set('tampered', 'direct-write');
|
|
132
366
|
check('projection drift is detectable', (await coordinator.detectDrift('bookings', expectedProjectionHash)).drift);
|
|
133
367
|
projected.delete('tampered');
|
|
368
|
+
const sqlBefore = await sql.snapshot();
|
|
369
|
+
await coordinator.replay(sql.id, { rebuild: true });
|
|
370
|
+
check('projections can be deleted and rebuilt without losing canonical meaning',
|
|
371
|
+
JSON.stringify(await sql.snapshot()) === JSON.stringify(sqlBefore)
|
|
372
|
+
&& (await vfs.read('bookings/one.md'))?.hash);
|
|
134
373
|
coordinator.register({ id: 'broken', apply: async () => { throw new Error('projection offline'); } });
|
|
135
374
|
const beforeFailure = (await vfs.read('bookings/one.md')).hash;
|
|
136
375
|
const failureDispatch = await coordinator.dispatch(createCanonicalEvent({ workspace_id: 'acme', action: 'patch', subject: 'bookings/one.md', operation_id: 'projection-failure', idempotency_key: 'projection-failure', principal }));
|
|
@@ -145,6 +384,9 @@ export async function runKernel09Conformance() {
|
|
|
145
384
|
for await (const item of store.replay({ workspaceId: 'acme' })) if (item.event.event_id === sample.event_id) replayed++;
|
|
146
385
|
check(`${name} event store is append-only and replayable`, duplicate && replayed === 1);
|
|
147
386
|
}
|
|
387
|
+
await checkJsonlEventStoreIndex(path.join(temp, 'events-index'), principal, check);
|
|
388
|
+
await checkFileAdapterRaces(path.join(temp, 'races'), principal, check);
|
|
389
|
+
await checkFileLocks(path.join(temp, 'locks'), principal, check);
|
|
148
390
|
const denied = await kernel.execute({ type: 'patch', workspace_id: 'acme', idempotency_key: 'denied', path: 'bookings/one.md', patches: { status: 'pending' } }, {
|
|
149
391
|
principal: { ...principal, id: 'outsider', workspaceIds: [], capabilities: ['*:*'] },
|
|
150
392
|
});
|
package/scripts/conformance.mjs
CHANGED
|
@@ -691,6 +691,53 @@ async function runCliSmokeConformance() {
|
|
|
691
691
|
check('ssss import --dry-run reports would-commit counts and leaves target empty',
|
|
692
692
|
dryRunOut.includes('would commit') && dryRunFiles.length === 0,
|
|
693
693
|
dryRunOut.trim());
|
|
694
|
+
|
|
695
|
+
const primitiveOut = JSON.parse(execFileSync('node', [
|
|
696
|
+
path.join(__dirname, 'ssss.mjs'),
|
|
697
|
+
'primitive', 'create',
|
|
698
|
+
'--namespace', 'acme',
|
|
699
|
+
'--name', '顧客予約',
|
|
700
|
+
'--language', 'ja',
|
|
701
|
+
'--fields', JSON.stringify([{ id: 'status', name: '状態', kind: 'enum', values: ['pending', 'confirmed'], required: true }]),
|
|
702
|
+
], { encoding: 'utf8' }));
|
|
703
|
+
check('ssss primitive create emits a stable qualified identity',
|
|
704
|
+
/^acme:p_[a-f0-9]{20}$/.test(primitiveOut.primitive_id),
|
|
705
|
+
JSON.stringify(primitiveOut));
|
|
706
|
+
|
|
707
|
+
const defPath = path.join(tmp, 'definition.json');
|
|
708
|
+
fs.writeFileSync(defPath, `${JSON.stringify(primitiveOut)}\n`);
|
|
709
|
+
const inspectOut = JSON.parse(execFileSync('node', [
|
|
710
|
+
path.join(__dirname, 'ssss.mjs'),
|
|
711
|
+
'primitive', 'inspect',
|
|
712
|
+
defPath,
|
|
713
|
+
], { encoding: 'utf8' }));
|
|
714
|
+
const extensionPath = path.join(tmp, 'extension.json');
|
|
715
|
+
fs.writeFileSync(extensionPath, `${JSON.stringify(inspectOut.registry)}\n`);
|
|
716
|
+
const lockOut = JSON.parse(execFileSync('node', [
|
|
717
|
+
path.join(__dirname, 'ssss.mjs'),
|
|
718
|
+
'registry', 'lock',
|
|
719
|
+
'--extension', extensionPath,
|
|
720
|
+
], { encoding: 'utf8' }));
|
|
721
|
+
check('ssss registry lock emits integrity metadata',
|
|
722
|
+
typeof lockOut.integrity === 'string' && lockOut.integrity.startsWith('sha256:'),
|
|
723
|
+
JSON.stringify(lockOut));
|
|
724
|
+
|
|
725
|
+
const adapterOut = JSON.parse(execFileSync('node', [
|
|
726
|
+
path.join(__dirname, 'ssss.mjs'),
|
|
727
|
+
'adapter', 'conformance',
|
|
728
|
+
], { encoding: 'utf8' }));
|
|
729
|
+
check('ssss adapter conformance report is machine-readable and green',
|
|
730
|
+
adapterOut.passed === true && adapterOut.suites?.vfs_memory?.passed === true,
|
|
731
|
+
JSON.stringify(adapterOut));
|
|
732
|
+
|
|
733
|
+
const migrateOut = JSON.parse(execFileSync('node', [
|
|
734
|
+
path.join(__dirname, 'ssss.mjs'),
|
|
735
|
+
'migrate', '0.8-to-0.9',
|
|
736
|
+
vault,
|
|
737
|
+
], { encoding: 'utf8' }));
|
|
738
|
+
check('ssss migrate 0.8-to-0.9 dry-run emits backup manifest without writes',
|
|
739
|
+
migrateOut.dry_run === true && migrateOut.writes_performed === 0 && Array.isArray(migrateOut.backup_manifest),
|
|
740
|
+
JSON.stringify(migrateOut));
|
|
694
741
|
} finally {
|
|
695
742
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
696
743
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate a minimal SPDX-lite SBOM for @gregiteen/ssss-cli publish artifacts.
|
|
4
|
+
* Dependency-free: packages only what is declared in package.json (zero runtime deps).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
13
|
+
|
|
14
|
+
function walk(dir, out = []) {
|
|
15
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
17
|
+
const abs = path.join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) walk(abs, out);
|
|
19
|
+
else if (entry.isFile()) out.push(abs);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const files = walk(root).filter(
|
|
25
|
+
(f) =>
|
|
26
|
+
!f.includes(`${path.sep}artifacts${path.sep}`) &&
|
|
27
|
+
!f.endsWith('package-lock.json'),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const packages = [
|
|
31
|
+
{
|
|
32
|
+
name: pkg.name,
|
|
33
|
+
version: pkg.version,
|
|
34
|
+
downloadLocation: `https://registry.npmjs.org/${pkg.name}/-/${pkg.name.split('/').pop()}-${pkg.version}.tgz`,
|
|
35
|
+
filesAnalyzed: true,
|
|
36
|
+
licenseConcluded: pkg.license || 'NOASSERTION',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// Zero runtime dependencies by design — still list optional peer/dev if present
|
|
41
|
+
for (const [section, deps] of Object.entries({
|
|
42
|
+
dependencies: pkg.dependencies || {},
|
|
43
|
+
optionalDependencies: pkg.optionalDependencies || {},
|
|
44
|
+
})) {
|
|
45
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
46
|
+
packages.push({
|
|
47
|
+
name,
|
|
48
|
+
version: String(version).replace(/^[\^~]/, ''),
|
|
49
|
+
relationship: section,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fileEntries = files.map((abs) => {
|
|
55
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
56
|
+
const buf = fs.readFileSync(abs);
|
|
57
|
+
return {
|
|
58
|
+
fileName: rel,
|
|
59
|
+
checksums: [
|
|
60
|
+
{
|
|
61
|
+
algorithm: 'SHA256',
|
|
62
|
+
checksumValue: crypto.createHash('sha256').update(buf).digest('hex'),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const sbom = {
|
|
69
|
+
spdxVersion: 'SPDX-2.3',
|
|
70
|
+
dataLicense: 'CC0-1.0',
|
|
71
|
+
SPDXID: 'SPDXRef-DOCUMENT',
|
|
72
|
+
name: `${pkg.name}-${pkg.version}`,
|
|
73
|
+
documentNamespace: `https://github.com/gregiteen/ssss/sbom/${pkg.version}`,
|
|
74
|
+
creationInfo: {
|
|
75
|
+
created: new Date().toISOString(),
|
|
76
|
+
creators: ['Tool: ssss-generate-sbom'],
|
|
77
|
+
},
|
|
78
|
+
packages,
|
|
79
|
+
files: fileEntries,
|
|
80
|
+
relationships: [
|
|
81
|
+
{
|
|
82
|
+
spdxElementId: 'SPDXRef-DOCUMENT',
|
|
83
|
+
relationshipType: 'DESCRIBES',
|
|
84
|
+
relatedSpdxElement: 'SPDXRef-Package',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
comment:
|
|
88
|
+
'SSSS reference package is intentionally dependency-free at runtime. Host apps supply adapters.',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const outDir = path.join(root, 'artifacts');
|
|
92
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
93
|
+
const outFile = path.join(outDir, `sbom-${pkg.version}.spdx.json`);
|
|
94
|
+
fs.writeFileSync(outFile, `${JSON.stringify(sbom, null, 2)}\n`);
|
|
95
|
+
console.log(`SBOM written: ${outFile}`);
|
|
96
|
+
console.log(`Packages: ${packages.length}, files hashed: ${fileEntries.length}`);
|