@dharma-ai-labs/agent-fabric 0.2.109 → 0.2.110
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/README.md +41 -0
- package/dist/codexInboxSession.d.ts +1 -1
- package/dist/cooperativeInboxSession.d.ts +1 -1
- package/dist/demoEnrollment.d.ts +2 -1
- package/dist/demoEnrollment.d.ts.map +1 -1
- package/dist/demoEnrollment.js +99 -21
- package/dist/demoEnrollment.js.map +1 -1
- package/dist/demoEnrollment.test.js +288 -2
- package/dist/demoEnrollment.test.js.map +1 -1
- package/dist/demoPackage.js +1 -1
- package/dist/demoPackage.js.map +1 -1
- package/dist/demoSigningTrust.d.ts +24 -0
- package/dist/demoSigningTrust.d.ts.map +1 -0
- package/dist/demoSigningTrust.js +326 -0
- package/dist/demoSigningTrust.js.map +1 -0
- package/dist/demoSigningTrust.test.d.ts +2 -0
- package/dist/demoSigningTrust.test.d.ts.map +1 -0
- package/dist/demoSigningTrust.test.js +445 -0
- package/dist/demoSigningTrust.test.js.map +1 -0
- package/dist/demoSigningUpgradeProof.d.ts +52 -0
- package/dist/demoSigningUpgradeProof.d.ts.map +1 -0
- package/dist/demoSigningUpgradeProof.js +137 -0
- package/dist/demoSigningUpgradeProof.js.map +1 -0
- package/dist/demoSigningUpgradeProof.test.d.ts +2 -0
- package/dist/demoSigningUpgradeProof.test.d.ts.map +1 -0
- package/dist/demoSigningUpgradeProof.test.js +564 -0
- package/dist/demoSigningUpgradeProof.test.js.map +1 -0
- package/dist/demoSigningUpgradeSubmission.d.ts +21 -0
- package/dist/demoSigningUpgradeSubmission.d.ts.map +1 -0
- package/dist/demoSigningUpgradeSubmission.js +178 -0
- package/dist/demoSigningUpgradeSubmission.js.map +1 -0
- package/dist/demoSupervisor.d.ts.map +1 -1
- package/dist/demoSupervisor.js +2 -1
- package/dist/demoSupervisor.js.map +1 -1
- package/dist/demoSupervisor.test.js +35 -0
- package/dist/demoSupervisor.test.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/providerSessionReplyRecovery.d.ts +1 -1
- package/dist/schemas/demo-enrollment-approval.schema.json +19 -0
- package/dist/schemas/demo-signing-update.schema.json +18 -0
- package/dist/schemas/lifecycle-event.schema.json +178 -0
- package/dist/schemas/signing-upgrade-context.schema.json +28 -0
- package/dist/schemas/signing-upgrade-source.schema.json +32 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +1 -1
- package/dist/usage.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { createHash, createPublicKey, generateKeyPairSync, verify } from 'node:crypto';
|
|
4
|
+
import { mkdtemp, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { canonicalize, signCanonicalObject, verifyCanonicalObject } from '@dharma-ai-labs/agent-fabric-contracts';
|
|
9
|
+
import { connectDemoDevice, scopePath } from './demoEnrollment.js';
|
|
10
|
+
import { prepareDemoSigningUpgradeProof, readDemoSigningUpgradeContext } from './demoSigningUpgradeProof.js';
|
|
11
|
+
import { submitDemoSigningUpgradeProof } from './demoSigningUpgradeSubmission.js';
|
|
12
|
+
import { run } from './index.js';
|
|
13
|
+
const now = new Date('2026-09-27T10:00:00Z');
|
|
14
|
+
const repositoryId = '10000000-0000-4000-8000-000000000001';
|
|
15
|
+
const deviceId = '30000000-0000-4000-8000-000000000001';
|
|
16
|
+
const otherClient = '30000000-0000-4000-8000-000000000002';
|
|
17
|
+
const predecessorHash = `sha256:${'a'.repeat(64)}`;
|
|
18
|
+
const candidateHash = `sha256:${'b'.repeat(64)}`;
|
|
19
|
+
const cliVersion = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
20
|
+
const contractVersion = JSON.parse(await readFile(new URL('../package.json', import.meta.resolve('@dharma-ai-labs/agent-fabric-contracts')), 'utf8')).version;
|
|
21
|
+
const [major, minor, patch] = cliVersion.split('.').map(Number);
|
|
22
|
+
const uninstalledCliVersion = `${major}.${minor}.${patch + 1}`;
|
|
23
|
+
function historicalHash(value) {
|
|
24
|
+
const ordered = (item) => Array.isArray(item) ? item.map(ordered)
|
|
25
|
+
: item && typeof item === 'object' ? Object.fromEntries(Object.entries(item)
|
|
26
|
+
.sort(([a], [b]) => a.localeCompare(b)).map(([key, val]) => [key, ordered(val)])) : item;
|
|
27
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(ordered(value))).digest('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
async function fixture(t, preloaded = true) {
|
|
30
|
+
t.mock.timers.enable({ apis: ['Date'], now });
|
|
31
|
+
const signer = generateKeyPairSync('ed25519'), next = generateKeyPairSync('ed25519');
|
|
32
|
+
const keyVersion = 'projects/test/locations/global/keyRings/demo/cryptoKeys/signing/cryptoKeyVersions/1';
|
|
33
|
+
const successor = keyVersion.slice(0, -1) + '2';
|
|
34
|
+
const publicKey = signer.publicKey.export({ format: 'jwk' }).x;
|
|
35
|
+
const body = { schema: 'dharma.server-signing-keyset/v1', organizationId: 'org_fixture',
|
|
36
|
+
generation: preloaded ? 2 : 1, keys: [
|
|
37
|
+
{ keyVersion, publicKeyEd25519: publicKey, status: 'active',
|
|
38
|
+
notBefore: '2026-09-27T08:00:00Z', notAfter: '2026-09-28T08:00:00Z' },
|
|
39
|
+
{ keyVersion: successor, publicKeyEd25519: next.publicKey.export({ format: 'jwk' }).x, status: 'overlap',
|
|
40
|
+
notBefore: '2026-09-27T09:00:00Z', notAfter: '2026-10-20T08:00:00Z' },
|
|
41
|
+
], signedByKeyVersion: keyVersion, issuedAt: '2026-09-27T09:00:00Z', expiresAt: '2026-09-28T08:00:00Z' };
|
|
42
|
+
if (!preloaded)
|
|
43
|
+
body.keys = body.keys.slice(0, 1);
|
|
44
|
+
const keyset = { ...body, signature: signCanonicalObject(body, signer.privateKey) };
|
|
45
|
+
const values = new Map();
|
|
46
|
+
const store = { backend: 'linux-secret-service',
|
|
47
|
+
async get(account) { return values.get(account) ?? null; },
|
|
48
|
+
async put(account, value) { values.set(account, value); },
|
|
49
|
+
async delete(account) { values.delete(account); } };
|
|
50
|
+
const stateRoot = await mkdtemp(resolve(tmpdir(), 'demo-upgrade-proof-'));
|
|
51
|
+
const scope = { hqUrl: 'https://dharma.example', organizationId: 'org_fixture', repositoryId,
|
|
52
|
+
normalizedRepository: 'github.com/example/private', installationId: '40000000-0000-4000-8000-000000000001', stateRoot };
|
|
53
|
+
await connectDemoDevice({ ...scope, grant: 'A'.repeat(43), deviceName: 'Fixture device', platform: 'linux', maximumWaitMs: 1000 }, {
|
|
54
|
+
store, sleep: async () => { }, fetcher: async (resource) => {
|
|
55
|
+
const url = new URL(String(resource));
|
|
56
|
+
return new Response(JSON.stringify(url.pathname.endsWith('/enrollments')
|
|
57
|
+
? { ok: true, status: 'pending', organizationId: scope.organizationId, repositoryId,
|
|
58
|
+
deviceCode: 'B'.repeat(43), expiresAt: '2026-09-27T10:01:00Z',
|
|
59
|
+
verificationUri: `${scope.hqUrl}/demo/fabric/approve?orgId=org_fixture&repositoryId=${repositoryId}&code=ABCDEF0123456789ABCD` }
|
|
60
|
+
: url.pathname.endsWith('/poll') ? { ok: true, status: 'approved', deviceId, repositoryId,
|
|
61
|
+
serverPublicKeyEd25519: publicKey, serverSigningKeyset: keyset }
|
|
62
|
+
: { ok: true, organizationId: scope.organizationId, repositoryId, deviceId,
|
|
63
|
+
normalizedRepository: scope.normalizedRepository, acceptedSequence: 1 }));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const context = { schema: 'dharma.signing-upgrade-context/v1', organizationId: scope.organizationId, repositoryId,
|
|
67
|
+
globalEpoch: 'fixture-epoch', predecessorHash, candidateHash, installedKeysetHash: historicalHash(keyset),
|
|
68
|
+
baselineConsumers: [{ name: 'cli', version: '0.2.107' }, { name: 'contracts', version: '0.1.10' }],
|
|
69
|
+
requiredConsumers: [{ name: 'cli', version: cliVersion }, { name: 'contracts', version: contractVersion }],
|
|
70
|
+
requiredClientIds: [deviceId, otherClient], expiresAt: '2026-09-27T10:10:00Z' };
|
|
71
|
+
const configPath = scopePath(scope, scope.hqUrl);
|
|
72
|
+
const config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
73
|
+
return { scope, store, values, context, config, configPath, keyset };
|
|
74
|
+
}
|
|
75
|
+
test('prepares an original-client proof using the actual device and installed package versions without submission', async (t) => {
|
|
76
|
+
const f = await fixture(t), before = JSON.stringify([...f.values]), disk = await readFile(f.configPath, 'utf8');
|
|
77
|
+
const result = await prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store: f.store, now });
|
|
78
|
+
assert.equal(result.stage, 'proof_prepared');
|
|
79
|
+
assert.equal(result.submitted, false);
|
|
80
|
+
assert.ok(result.proof.schema === 'dharma.signing-client-upgrade-proof/v1');
|
|
81
|
+
assert.equal(result.proof.principalId, deviceId);
|
|
82
|
+
assert.equal(result.proof.cliVersion, cliVersion);
|
|
83
|
+
assert.equal(result.proof.contractVersion, contractVersion);
|
|
84
|
+
const { sourceHash, ...signed } = result.proof, { signature, ...unsigned } = signed;
|
|
85
|
+
assert.equal(sourceHash, `sha256:${createHash('sha256').update(canonicalize(signed)).digest('hex')}`);
|
|
86
|
+
assert.equal(verifyCanonicalObject(unsigned, signature, createPublicKey({
|
|
87
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: f.config.publicKeyEd25519 }, format: 'jwk'
|
|
88
|
+
})), true);
|
|
89
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
90
|
+
assert.equal(await readFile(f.configPath, 'utf8'), disk);
|
|
91
|
+
assert.doesNotMatch(JSON.stringify(result), /privateJwk|privateKey|grant|authorization|accessToken/);
|
|
92
|
+
});
|
|
93
|
+
test('owner proof preparation remains unapproved and binds exact inventories for separate browser confirmation', async (t) => {
|
|
94
|
+
const f = await fixture(t);
|
|
95
|
+
const result = await prepareDemoSigningUpgradeProof(f.scope, f.context, 'owner', { store: f.store, now });
|
|
96
|
+
assert.equal(result.proof.principalId, `owner:${deviceId}`);
|
|
97
|
+
assert.ok(result.proof.schema === 'dharma.signing-consumer-approval/v1');
|
|
98
|
+
assert.deepEqual(result.proof.requiredClientIds, f.context.requiredClientIds);
|
|
99
|
+
assert.deepEqual(result.proof.baselineConsumers, f.context.baselineConsumers);
|
|
100
|
+
assert.deepEqual(result.proof.requiredConsumers, f.context.requiredConsumers);
|
|
101
|
+
assert.equal(result.submitted, false);
|
|
102
|
+
assert.equal(result.browserApprovalRequired, true);
|
|
103
|
+
assert.equal('confirmed' in result, false);
|
|
104
|
+
});
|
|
105
|
+
const faults = [
|
|
106
|
+
['foreign organization', v => { v.organizationId = 'org_other'; }],
|
|
107
|
+
['foreign repository', v => { v.repositoryId = '10000000-0000-4000-8000-000000000009'; }],
|
|
108
|
+
['unknown credential', v => { v.grant = 'A'.repeat(43); }],
|
|
109
|
+
['false installed versions', v => { v.cliVersion = '9.9.9'; }],
|
|
110
|
+
['missing original device', v => { v.requiredClientIds = [otherClient]; }],
|
|
111
|
+
['duplicate clients', v => { v.requiredClientIds = [deviceId, deviceId]; }],
|
|
112
|
+
['duplicate consumers', v => { v.requiredConsumers = [{ name: 'cli', version: cliVersion }, { name: 'cli', version: cliVersion }]; }],
|
|
113
|
+
['omitted consumer', v => { v.requiredConsumers = [{ name: 'cli', version: cliVersion }]; }],
|
|
114
|
+
['downgrade', v => { v.requiredConsumers = [{ name: 'cli', version: '0.2.106' }, { name: 'contracts', version: contractVersion }]; }],
|
|
115
|
+
['uninstalled required version', v => { v.requiredConsumers = [{ name: 'cli', version: uninstalledCliVersion }, { name: 'contracts', version: contractVersion }]; }],
|
|
116
|
+
['wrong preload hash', v => { v.installedKeysetHash = candidateHash; }],
|
|
117
|
+
['same candidate', v => { v.candidateHash = predecessorHash; }],
|
|
118
|
+
['expired', v => { v.expiresAt = now.toISOString(); }],
|
|
119
|
+
['unbounded proof', v => { v.expiresAt = '2026-09-28T10:00:00Z'; }],
|
|
120
|
+
['invalid timestamp', v => { v.expiresAt = 'later'; }],
|
|
121
|
+
];
|
|
122
|
+
for (const [label, mutate] of faults)
|
|
123
|
+
test(`rejects ${label} without changing protected trust or device state`, async (t) => {
|
|
124
|
+
const f = await fixture(t), before = JSON.stringify([...f.values]), disk = await readFile(f.configPath, 'utf8');
|
|
125
|
+
const context = structuredClone(f.context);
|
|
126
|
+
mutate(context);
|
|
127
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, context, 'client', { store: f.store, now }), /Signing upgrade|Demo signing/);
|
|
128
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
129
|
+
assert.equal(await readFile(f.configPath, 'utf8'), disk);
|
|
130
|
+
});
|
|
131
|
+
test('rejects invalid clock and mode', async (t) => {
|
|
132
|
+
const f = await fixture(t);
|
|
133
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store: f.store, now: new Date(NaN) }), /Signing upgrade/);
|
|
134
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'admin', { store: f.store, now }), /Signing upgrade/);
|
|
135
|
+
});
|
|
136
|
+
test('rejects a tampered device public key without creating a replacement identity', async (t) => {
|
|
137
|
+
const f = await fixture(t), before = JSON.stringify([...f.values]);
|
|
138
|
+
await writeFile(f.configPath, JSON.stringify({ ...f.config, publicKeyEd25519: 'A'.repeat(43) }));
|
|
139
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store: f.store, now }), /protected device identity/);
|
|
140
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
141
|
+
});
|
|
142
|
+
test('expired protected trust requires browser re-enrollment, not a new upgrade proof', async (t) => {
|
|
143
|
+
const f = await fixture(t), before = JSON.stringify([...f.values]);
|
|
144
|
+
t.mock.timers.setTime(new Date('2026-09-28T09:00:00Z').getTime());
|
|
145
|
+
const context = { ...f.context, expiresAt: '2026-09-28T09:10:00Z' };
|
|
146
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, context, 'client', {
|
|
147
|
+
store: f.store, now: new Date('2026-09-28T09:00:00Z')
|
|
148
|
+
}), /browser|Signing upgrade|Demo signing/i);
|
|
149
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
150
|
+
});
|
|
151
|
+
test('caller context mutation while reading the secure store cannot change the signed candidate', async (t) => {
|
|
152
|
+
const f = await fixture(t), context = structuredClone(f.context);
|
|
153
|
+
const store = { ...f.store, get: async (account) => {
|
|
154
|
+
context.candidateHash = `sha256:${'c'.repeat(64)}`;
|
|
155
|
+
return f.store.get(account);
|
|
156
|
+
} };
|
|
157
|
+
const result = await prepareDemoSigningUpgradeProof(f.scope, context, 'client', { store, now });
|
|
158
|
+
assert.equal(result.proof.candidateHash, candidateHash);
|
|
159
|
+
});
|
|
160
|
+
test('bootstrap-only enrollment is not an installed preload proof', async (t) => {
|
|
161
|
+
const f = await fixture(t, false);
|
|
162
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store: f.store, now }), /original_preloaded_client_required/);
|
|
163
|
+
});
|
|
164
|
+
test('missing enrollment cannot create a replacement enrolled device', async (t) => {
|
|
165
|
+
const f = await fixture(t), before = JSON.stringify([...f.values]);
|
|
166
|
+
await unlink(f.configPath);
|
|
167
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store: f.store, now }));
|
|
168
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
169
|
+
});
|
|
170
|
+
test('a missing OS device key cannot be replaced while preparing a proof', async (t) => {
|
|
171
|
+
const f = await fixture(t);
|
|
172
|
+
const keyAccount = [...f.values].find(([, value]) => {
|
|
173
|
+
try {
|
|
174
|
+
return typeof JSON.parse(value).d === 'string';
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
})?.[0];
|
|
180
|
+
assert.ok(keyAccount);
|
|
181
|
+
f.values.delete(keyAccount);
|
|
182
|
+
const before = JSON.stringify([...f.values]), disk = await readFile(f.configPath, 'utf8');
|
|
183
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store: f.store, now }), /existing_protected_state_required/);
|
|
184
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
185
|
+
assert.equal(await readFile(f.configPath, 'utf8'), disk);
|
|
186
|
+
});
|
|
187
|
+
test('a missing protected signing anchor is not recreated from the device file', async (t) => {
|
|
188
|
+
const f = await fixture(t), account = [...f.values.keys()].find(key => /^demo-signing-[0-9a-f]{32}$/.test(key));
|
|
189
|
+
assert.ok(account);
|
|
190
|
+
f.values.delete(account);
|
|
191
|
+
const before = JSON.stringify([...f.values]), disk = await readFile(f.configPath, 'utf8');
|
|
192
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store: f.store, now }), /existing_protected_state_required/);
|
|
193
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
194
|
+
assert.equal(await readFile(f.configPath, 'utf8'), disk);
|
|
195
|
+
});
|
|
196
|
+
test('the second identity read cannot create a key after concurrent removal', async (t) => {
|
|
197
|
+
const f = await fixture(t), account = [...f.values].find(([, value]) => {
|
|
198
|
+
try {
|
|
199
|
+
return typeof JSON.parse(value).d === 'string';
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
})?.[0];
|
|
205
|
+
assert.ok(account);
|
|
206
|
+
let reads = 0;
|
|
207
|
+
const store = { ...f.store, get: async (key) => {
|
|
208
|
+
if (key === account && ++reads >= 2)
|
|
209
|
+
return null;
|
|
210
|
+
return f.store.get(key);
|
|
211
|
+
} };
|
|
212
|
+
const before = JSON.stringify([...f.values]), disk = await readFile(f.configPath, 'utf8');
|
|
213
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store, now }), /existing_protected_state_required/);
|
|
214
|
+
assert.equal(reads, 2);
|
|
215
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
216
|
+
assert.equal(await readFile(f.configPath, 'utf8'), disk);
|
|
217
|
+
});
|
|
218
|
+
for (const kind of ['identity', 'anchor'])
|
|
219
|
+
test(`fresh OS reads reject a removed ${kind} hidden by a process cache`, async (t) => {
|
|
220
|
+
const f = await fixture(t), cached = new Map(f.values);
|
|
221
|
+
const account = kind === 'anchor' ? [...f.values.keys()].find(key => /^demo-signing-[0-9a-f]{32}$/.test(key))
|
|
222
|
+
: [...f.values].find(([, value]) => { try {
|
|
223
|
+
return typeof JSON.parse(value).d === 'string';
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
} })?.[0];
|
|
228
|
+
assert.ok(account);
|
|
229
|
+
f.values.delete(account);
|
|
230
|
+
let freshReads = 0, cachedReads = 0;
|
|
231
|
+
const store = { ...f.store,
|
|
232
|
+
get: async (key) => { cachedReads++; return cached.get(key) ?? null; },
|
|
233
|
+
getFresh: async (key) => { freshReads++; return f.values.get(key) ?? null; } };
|
|
234
|
+
const before = JSON.stringify([...f.values]), disk = await readFile(f.configPath, 'utf8');
|
|
235
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store, now }), /existing_protected_state_required/);
|
|
236
|
+
assert.ok(freshReads > 0);
|
|
237
|
+
assert.equal(cachedReads, 0);
|
|
238
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
239
|
+
assert.equal(await readFile(f.configPath, 'utf8'), disk);
|
|
240
|
+
});
|
|
241
|
+
test('a cached identity removed before its second fresh read cannot sign', async (t) => {
|
|
242
|
+
const f = await fixture(t), account = [...f.values].find(([, value]) => {
|
|
243
|
+
try {
|
|
244
|
+
return typeof JSON.parse(value).d === 'string';
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
})?.[0];
|
|
250
|
+
assert.ok(account);
|
|
251
|
+
let reads = 0;
|
|
252
|
+
const store = { ...f.store, getFresh: async (key) => {
|
|
253
|
+
if (key === account && ++reads >= 2)
|
|
254
|
+
return null;
|
|
255
|
+
return f.values.get(key) ?? null;
|
|
256
|
+
} };
|
|
257
|
+
const before = JSON.stringify([...f.values]);
|
|
258
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store, now }), /existing_protected_state_required/);
|
|
259
|
+
assert.equal(reads, 2);
|
|
260
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
261
|
+
});
|
|
262
|
+
test('the protected anchor removed after initial verification is checked again before signing', async (t) => {
|
|
263
|
+
const f = await fixture(t), cached = new Map(f.values);
|
|
264
|
+
const anchor = [...f.values.keys()].find(key => /^demo-signing-[0-9a-f]{32}$/.test(key));
|
|
265
|
+
const identity = [...f.values].find(([, value]) => {
|
|
266
|
+
try {
|
|
267
|
+
return typeof JSON.parse(value).d === 'string';
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
})?.[0];
|
|
273
|
+
assert.ok(anchor && identity);
|
|
274
|
+
let reads = 0, writes = 0;
|
|
275
|
+
const store = { ...f.store, get: async (key) => cached.get(key) ?? null,
|
|
276
|
+
getFresh: async (key) => {
|
|
277
|
+
if (key === identity && ++reads === 2)
|
|
278
|
+
f.values.delete(anchor);
|
|
279
|
+
return f.values.get(key) ?? null;
|
|
280
|
+
}, put: async () => { writes++; }, delete: async () => { writes++; } };
|
|
281
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store, now }), /existing_protected_state_required/);
|
|
282
|
+
assert.equal(reads, 3);
|
|
283
|
+
assert.equal(writes, 0);
|
|
284
|
+
assert.equal(f.values.has(anchor), false);
|
|
285
|
+
});
|
|
286
|
+
test('context expiring during secure-store reads cannot produce a signed proof', async (t) => {
|
|
287
|
+
const f = await fixture(t);
|
|
288
|
+
const store = { ...f.store, get: async (account) => {
|
|
289
|
+
t.mock.timers.setTime(now.getTime() + 11 * 60_000);
|
|
290
|
+
return f.store.get(account);
|
|
291
|
+
} };
|
|
292
|
+
await assert.rejects(prepareDemoSigningUpgradeProof(f.scope, f.context, 'client', { store }), /context_expired_during_preparation/);
|
|
293
|
+
});
|
|
294
|
+
test('context reader accepts JSON and rejects empty, HTML and oversized files without exposing their contents', async () => {
|
|
295
|
+
const root = await mkdtemp(resolve(tmpdir(), 'demo-upgrade-context-')), path = resolve(root, 'review.json');
|
|
296
|
+
await writeFile(path, '{"fixture":true}');
|
|
297
|
+
assert.deepEqual(await readDemoSigningUpgradeContext(path), { fixture: true });
|
|
298
|
+
for (const content of ['', '<html>private-test-value</html>', ' '.repeat(32769)]) {
|
|
299
|
+
await writeFile(path, content);
|
|
300
|
+
await assert.rejects(readDemoSigningUpgradeContext(path), error => {
|
|
301
|
+
assert.ok(error instanceof Error);
|
|
302
|
+
assert.match(error.message, /context_(size|json)_invalid/);
|
|
303
|
+
assert.doesNotMatch(error.message, /private-test-value/);
|
|
304
|
+
return true;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
test('both actual CLI commands expose a grant-free dry-run and enforce the current Git remote', async () => {
|
|
309
|
+
const workspace = await mkdtemp(resolve(tmpdir(), 'demo-signing-command-'));
|
|
310
|
+
execFileSync('git', ['init', workspace], { stdio: 'ignore' });
|
|
311
|
+
execFileSync('git', ['-C', workspace, 'remote', 'add', 'origin', 'https://github.com/example/private.git']);
|
|
312
|
+
const flags = ['--organization-id', 'org_fixture', '--repository-id', repositoryId,
|
|
313
|
+
'--normalized-repository', 'github.com/example/private', '--workspace', workspace, '--dry-run'];
|
|
314
|
+
for (const command of ['signing-client-proof', 'signing-owner-proof']) {
|
|
315
|
+
const result = await run(['demo', command, ...flags]);
|
|
316
|
+
assert.deepEqual(result, { ok: true, stage: 'demo_signing_proof_plan', organizationId: 'org_fixture',
|
|
317
|
+
repositoryId, normalizedRepository: 'github.com/example/private', workspaceVerified: true, repositoryPackageState: 'not_connected' });
|
|
318
|
+
assert.doesNotMatch(JSON.stringify(result), /signature|grant|privateKey|confirmed/);
|
|
319
|
+
await assert.rejects(run(['demo', command, ...flags, '--normalized-repository', 'github.com/example/other']), /does not match/);
|
|
320
|
+
}
|
|
321
|
+
assert.equal((await run(['demo', 'signing-client-proof', ...flags, '--submit'])).stage, 'demo_signing_proof_plan');
|
|
322
|
+
await assert.rejects(run(['demo', 'signing-owner-proof', ...flags, '--submit']), /browser/i);
|
|
323
|
+
});
|
|
324
|
+
function submissionServer(f, options = {}) {
|
|
325
|
+
let sequence = f.config.nextSequence, posts = 0, lostStatus = false;
|
|
326
|
+
const proofs = [], stored = new Set(), calls = [];
|
|
327
|
+
const fetcher = async (resource, init) => {
|
|
328
|
+
const url = new URL(String(resource)), headers = new Headers(init?.headers);
|
|
329
|
+
calls.push(url.pathname);
|
|
330
|
+
assert.equal(url.origin, f.scope.hqUrl);
|
|
331
|
+
assert.equal(url.searchParams.get('orgId'), f.scope.organizationId);
|
|
332
|
+
assert.equal(init?.redirect, 'error');
|
|
333
|
+
assert.equal(init?.cache, 'no-store');
|
|
334
|
+
assert.ok(init?.signal);
|
|
335
|
+
assert.equal(headers.has('authorization'), false);
|
|
336
|
+
assert.equal(headers.has('cookie'), false);
|
|
337
|
+
const raw = String(init?.body || '');
|
|
338
|
+
const payload = { bodyHash: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
|
|
339
|
+
deviceId, messageId: headers.get('x-dharma-message-id'), method: init?.method,
|
|
340
|
+
nonce: headers.get('x-dharma-nonce'), organizationId: f.scope.organizationId,
|
|
341
|
+
pathname: `${url.pathname}${url.search}`, sequence: Number(headers.get('x-dharma-sequence')),
|
|
342
|
+
sessionId: headers.get('x-dharma-session-id'), timestamp: headers.get('x-dharma-timestamp') };
|
|
343
|
+
assert.equal(verify(null, Buffer.from(JSON.stringify(payload)), createPublicKey({ key: {
|
|
344
|
+
kty: 'OKP', crv: 'Ed25519', x: f.config.publicKeyEd25519
|
|
345
|
+
}, format: 'jwk' }), Buffer.from(headers.get('x-dharma-signature'), 'base64url')), true);
|
|
346
|
+
if (payload.sequence !== sequence)
|
|
347
|
+
return Response.json({ ok: false,
|
|
348
|
+
error: { code: 'demo_fabric_sequence_out_of_order', message: 'Sequence rejected.' } }, { status: 409 });
|
|
349
|
+
sequence++;
|
|
350
|
+
if (url.pathname.endsWith('/status')) {
|
|
351
|
+
if (options.loseStatus && posts === 1 && !lostStatus) {
|
|
352
|
+
lostStatus = true;
|
|
353
|
+
throw new TypeError('Status response lost after proof recorded.');
|
|
354
|
+
}
|
|
355
|
+
options.afterStatus?.();
|
|
356
|
+
return Response.json({ ok: true, organizationId: f.scope.organizationId, repositoryId, deviceId,
|
|
357
|
+
normalizedRepository: f.scope.normalizedRepository, acceptedSequence: payload.sequence });
|
|
358
|
+
}
|
|
359
|
+
assert.equal(url.pathname, `/api/demo/fabric/repositories/${repositoryId}/signing-upgrade-sources`);
|
|
360
|
+
assert.equal(headers.get('content-type'), 'application/json');
|
|
361
|
+
posts++;
|
|
362
|
+
const { proof } = JSON.parse(raw);
|
|
363
|
+
proofs.push(proof);
|
|
364
|
+
const duplicate = stored.has(proof.sourceHash);
|
|
365
|
+
stored.add(proof.sourceHash);
|
|
366
|
+
if (options.loseResponse && posts === 1)
|
|
367
|
+
throw new TypeError('Response lost after write.');
|
|
368
|
+
const receipt = { ok: true, organizationId: f.scope.organizationId, repositoryId, deviceId,
|
|
369
|
+
sourceHash: proof.sourceHash, duplicate, correlationId: headers.get('x-dharma-correlation-id') };
|
|
370
|
+
return options.receipt?.(receipt) ?? Response.json(receipt, { headers: {
|
|
371
|
+
'x-dharma-correlation-id': String(receipt.correlationId)
|
|
372
|
+
} });
|
|
373
|
+
};
|
|
374
|
+
return { fetcher, proofs, calls, stored };
|
|
375
|
+
}
|
|
376
|
+
test('submits only an existing client proof and validates signed transport and exact receipt', async (t) => {
|
|
377
|
+
const f = await fixture(t), server = submissionServer(f), before = JSON.stringify([...f.values]);
|
|
378
|
+
const result = await submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher });
|
|
379
|
+
assert.equal(result.stage, 'client_proof_recorded');
|
|
380
|
+
assert.equal(result.submitted, true);
|
|
381
|
+
assert.equal(result.activated, false);
|
|
382
|
+
assert.equal(result.duplicate, false);
|
|
383
|
+
assert.equal(result.deviceId, deviceId);
|
|
384
|
+
assert.equal(server.proofs.length, 1);
|
|
385
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
386
|
+
assert.doesNotMatch(JSON.stringify(result), /privateKey|privateJwk|signature|grant|confirmed/);
|
|
387
|
+
await assert.rejects(readFile(`${f.configPath}.pending-signing-upgrade.json`), { code: 'ENOENT' });
|
|
388
|
+
});
|
|
389
|
+
test('lost response retries the same immutable proof with sequence reconciliation and duplicate receipt', async (t) => {
|
|
390
|
+
const f = await fixture(t), server = submissionServer(f, { loseResponse: true });
|
|
391
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }), /Response lost/);
|
|
392
|
+
const pending = await readFile(`${f.configPath}.pending-signing-upgrade.json`, 'utf8');
|
|
393
|
+
t.mock.timers.setTime(now.getTime() + 30_000);
|
|
394
|
+
const result = await submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher });
|
|
395
|
+
assert.equal(result.duplicate, true);
|
|
396
|
+
assert.equal(result.resumed, true);
|
|
397
|
+
assert.deepEqual(server.proofs[0], server.proofs[1]);
|
|
398
|
+
assert.equal(server.stored.size, 1);
|
|
399
|
+
assert.doesNotMatch(pending, /privateKey|privateJwk|grant|authorization/);
|
|
400
|
+
});
|
|
401
|
+
test('a lost final status keeps the recorded proof recoverable across the next invocation', async (t) => {
|
|
402
|
+
const f = await fixture(t), server = submissionServer(f, { loseStatus: true });
|
|
403
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }), /Status response lost/);
|
|
404
|
+
assert.ok(await readFile(`${f.configPath}.pending-signing-upgrade.json`, 'utf8'));
|
|
405
|
+
const result = await submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher });
|
|
406
|
+
assert.equal(result.duplicate, true);
|
|
407
|
+
assert.equal(result.resumed, true);
|
|
408
|
+
assert.deepEqual(server.proofs[0], server.proofs[1]);
|
|
409
|
+
assert.equal(server.stored.size, 1);
|
|
410
|
+
});
|
|
411
|
+
for (const status of [401, 403, 409, 500])
|
|
412
|
+
test(`HTTP ${status} cannot acknowledge a proof or expose provider text`, async (t) => {
|
|
413
|
+
let requestedCorrelation = '';
|
|
414
|
+
const f = await fixture(t), server = submissionServer(f, { receipt: value => {
|
|
415
|
+
requestedCorrelation = String(value.correlationId);
|
|
416
|
+
return Response.json({ ok: false,
|
|
417
|
+
error: { code: 'demo_signing_source_rejected', message: 'private-provider-error-content' } }, { status, headers: { 'x-dharma-correlation-id': requestedCorrelation } });
|
|
418
|
+
} });
|
|
419
|
+
const before = JSON.stringify([...f.values]);
|
|
420
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }), error => {
|
|
421
|
+
assert.ok(error instanceof Error);
|
|
422
|
+
assert.doesNotMatch(error.message, /private-provider-error-content/);
|
|
423
|
+
const details = error;
|
|
424
|
+
assert.equal(details.stage, 'client_proof_submission');
|
|
425
|
+
assert.equal(details.code, 'demo_signing_source_rejected');
|
|
426
|
+
assert.equal(details.status, status);
|
|
427
|
+
assert.equal(details.correlationId, requestedCorrelation);
|
|
428
|
+
assert.ok(error.message.includes(requestedCorrelation));
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
431
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
432
|
+
assert.ok(await readFile(`${f.configPath}.pending-signing-upgrade.json`, 'utf8'));
|
|
433
|
+
});
|
|
434
|
+
for (const header of [null, 'malformed-private-value', otherClient])
|
|
435
|
+
test('a missing or untrusted error correlation cannot attest a typed provider failure', async (t) => {
|
|
436
|
+
let requestedCorrelation = '';
|
|
437
|
+
const f = await fixture(t), server = submissionServer(f, { receipt: value => {
|
|
438
|
+
requestedCorrelation = String(value.correlationId);
|
|
439
|
+
return Response.json({ ok: false, error: { code: 'demo_signing_source_rejected',
|
|
440
|
+
message: 'private-provider-error-content', correlationId: requestedCorrelation } }, { status: 403, headers: header ? { 'x-dharma-correlation-id': header } : {} });
|
|
441
|
+
} });
|
|
442
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }), error => {
|
|
443
|
+
assert.ok(error instanceof Error);
|
|
444
|
+
const details = error;
|
|
445
|
+
assert.equal(details.code, 'receipt_invalid');
|
|
446
|
+
assert.equal(details.correlationId, requestedCorrelation);
|
|
447
|
+
assert.equal(details.stage, 'client_proof_submission');
|
|
448
|
+
assert.equal(details.status, 403);
|
|
449
|
+
assert.doesNotMatch(error.message, /malformed-private-value|private-provider-error-content/);
|
|
450
|
+
return true;
|
|
451
|
+
});
|
|
452
|
+
assert.ok(await readFile(`${f.configPath}.pending-signing-upgrade.json`, 'utf8'));
|
|
453
|
+
});
|
|
454
|
+
for (const [label, mutate] of Object.entries({
|
|
455
|
+
organization: (v) => { v.organizationId = 'org_foreign'; },
|
|
456
|
+
repository: (v) => { v.repositoryId = otherClient; },
|
|
457
|
+
device: (v) => { v.deviceId = otherClient; },
|
|
458
|
+
source: (v) => { v.sourceHash = predecessorHash; },
|
|
459
|
+
duplicate: (v) => { v.duplicate = 'yes'; },
|
|
460
|
+
success: (v) => { v.ok = false; },
|
|
461
|
+
correlation: (v) => { v.correlationId = otherClient; },
|
|
462
|
+
}))
|
|
463
|
+
test(`does not acknowledge a mismatched ${label} receipt`, async (t) => {
|
|
464
|
+
const f = await fixture(t), server = submissionServer(f, { receipt: value => {
|
|
465
|
+
const correlation = String(value.correlationId);
|
|
466
|
+
mutate(value);
|
|
467
|
+
return Response.json(value, { headers: { 'x-dharma-correlation-id': correlation } });
|
|
468
|
+
} });
|
|
469
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }), /receipt/i);
|
|
470
|
+
assert.ok(await readFile(`${f.configPath}.pending-signing-upgrade.json`, 'utf8'));
|
|
471
|
+
});
|
|
472
|
+
for (const [label, response] of [
|
|
473
|
+
['HTML', () => new Response('<html>private-error-content</html>', { headers: { 'content-type': 'text/html' } })],
|
|
474
|
+
['empty', () => Response.json({})],
|
|
475
|
+
['malformed', () => new Response('{', { headers: { 'content-type': 'application/json' } })],
|
|
476
|
+
['redirect', () => new Response('', { status: 302, headers: { location: 'https://foreign.example' } })],
|
|
477
|
+
['generic 404', () => Response.json({ error: 'missing' }, { status: 404 })],
|
|
478
|
+
['oversized', () => new Response(' '.repeat(8193), { headers: { 'content-type': 'application/json' } })],
|
|
479
|
+
])
|
|
480
|
+
test(`rejects ${label} without claiming submission success or exposing content`, async (t) => {
|
|
481
|
+
const f = await fixture(t), server = submissionServer(f, { receipt: response });
|
|
482
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }), error => {
|
|
483
|
+
assert.ok(error instanceof Error);
|
|
484
|
+
assert.doesNotMatch(error.message, /private-error-content/);
|
|
485
|
+
return true;
|
|
486
|
+
});
|
|
487
|
+
assert.ok(await readFile(`${f.configPath}.pending-signing-upgrade.json`, 'utf8'));
|
|
488
|
+
});
|
|
489
|
+
test('tampered pending proof is preserved and cannot send a network request', async (t) => {
|
|
490
|
+
const f = await fixture(t), server = submissionServer(f, { loseResponse: true });
|
|
491
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }));
|
|
492
|
+
const path = `${f.configPath}.pending-signing-upgrade.json`, pending = JSON.parse(await readFile(path, 'utf8'));
|
|
493
|
+
pending.signature = 'A'.repeat(86);
|
|
494
|
+
await writeFile(path, JSON.stringify(pending));
|
|
495
|
+
const before = server.calls.length;
|
|
496
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }), /pending|proof/i);
|
|
497
|
+
assert.equal(server.calls.length, before);
|
|
498
|
+
assert.equal((JSON.parse(await readFile(path, 'utf8'))).signature, pending.signature);
|
|
499
|
+
});
|
|
500
|
+
test('a changed context cannot silently replace an unresolved proof', async (t) => {
|
|
501
|
+
const f = await fixture(t), server = submissionServer(f, { loseResponse: true });
|
|
502
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }));
|
|
503
|
+
const path = `${f.configPath}.pending-signing-upgrade.json`, before = await readFile(path, 'utf8'), calls = server.calls.length;
|
|
504
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, { ...f.context, candidateHash: predecessorHash.replace(/a/g, 'c') }, { store: f.store, fetcher: server.fetcher }), /pending|proof/i);
|
|
505
|
+
assert.equal(server.calls.length, calls);
|
|
506
|
+
assert.equal(await readFile(path, 'utf8'), before);
|
|
507
|
+
});
|
|
508
|
+
test('expired or missing protected state blocks submission before networking', async (t) => {
|
|
509
|
+
const f = await fixture(t), server = submissionServer(f), before = JSON.stringify([...f.values]);
|
|
510
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, { ...f.context, expiresAt: now.toISOString() }, { store: f.store, fetcher: server.fetcher }));
|
|
511
|
+
assert.equal(server.calls.length, 0);
|
|
512
|
+
assert.equal(JSON.stringify([...f.values]), before);
|
|
513
|
+
f.values.clear();
|
|
514
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }));
|
|
515
|
+
assert.equal(server.calls.length, 0);
|
|
516
|
+
assert.equal(f.values.size, 0);
|
|
517
|
+
});
|
|
518
|
+
test('device removal during signed status cannot mint a replacement key or dispatch a proof', async (t) => {
|
|
519
|
+
const f = await fixture(t), key = [...f.values].find(([, value]) => { try {
|
|
520
|
+
return !!JSON.parse(value).d;
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return false;
|
|
524
|
+
} })?.[0];
|
|
525
|
+
assert.ok(key);
|
|
526
|
+
let writes = 0;
|
|
527
|
+
const store = { ...f.store, put: async () => { writes++; }, delete: async () => { writes++; } };
|
|
528
|
+
const server = submissionServer(f, { afterStatus: () => { f.values.delete(key); } });
|
|
529
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store, fetcher: server.fetcher }));
|
|
530
|
+
assert.equal(server.proofs.length, 0);
|
|
531
|
+
assert.equal(writes, 0);
|
|
532
|
+
assert.equal(f.values.has(key), false);
|
|
533
|
+
});
|
|
534
|
+
test('the anchor removed during the final envelope identity read is checked again before dispatch', async (t) => {
|
|
535
|
+
const f = await fixture(t), anchor = [...f.values.keys()].find(key => /^demo-signing-[0-9a-f]{32}$/.test(key));
|
|
536
|
+
const key = [...f.values].find(([, value]) => { try {
|
|
537
|
+
return !!JSON.parse(value).d;
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return false;
|
|
541
|
+
} })?.[0];
|
|
542
|
+
assert.ok(anchor && key);
|
|
543
|
+
let reads = 0, writes = 0;
|
|
544
|
+
const store = { ...f.store, getFresh: async (account) => {
|
|
545
|
+
if (account === key && ++reads === 8)
|
|
546
|
+
f.values.delete(anchor);
|
|
547
|
+
return f.values.get(account) ?? null;
|
|
548
|
+
}, put: async () => { writes++; }, delete: async () => { writes++; } };
|
|
549
|
+
const server = submissionServer(f);
|
|
550
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store, fetcher: server.fetcher }));
|
|
551
|
+
assert.equal(f.values.has(anchor), false);
|
|
552
|
+
assert.equal(writes, 0);
|
|
553
|
+
assert.equal(server.proofs.length, 0);
|
|
554
|
+
});
|
|
555
|
+
test('expired proof pending after a lost response remains preserved and does not retry', async (t) => {
|
|
556
|
+
const f = await fixture(t), server = submissionServer(f, { loseResponse: true });
|
|
557
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }));
|
|
558
|
+
const path = `${f.configPath}.pending-signing-upgrade.json`, before = await readFile(path, 'utf8'), calls = server.calls.length;
|
|
559
|
+
t.mock.timers.setTime(now.getTime() + 11 * 60_000);
|
|
560
|
+
await assert.rejects(submitDemoSigningUpgradeProof(f.scope, f.context, { store: f.store, fetcher: server.fetcher }));
|
|
561
|
+
assert.equal(server.calls.length, calls);
|
|
562
|
+
assert.equal(await readFile(path, 'utf8'), before);
|
|
563
|
+
});
|
|
564
|
+
//# sourceMappingURL=demoSigningUpgradeProof.test.js.map
|