@aithos/sdk 0.1.0-alpha.39 → 0.1.0-alpha.40
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/dist/src/assets.d.ts +207 -0
- package/dist/src/assets.js +533 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/react/AithosAsset.d.ts +66 -0
- package/dist/src/react/AithosAsset.js +67 -0
- package/dist/src/react/context.d.ts +29 -0
- package/dist/src/react/context.js +31 -0
- package/dist/src/react/index.d.ts +28 -0
- package/dist/src/react/index.js +30 -0
- package/dist/src/react/use-aithos-asset.d.ts +39 -0
- package/dist/src/react/use-aithos-asset.js +118 -0
- package/package.json +16 -2
- package/dist/test/auth-j3.test.d.ts +0 -2
- package/dist/test/auth-j3.test.js +0 -391
- package/dist/test/auth.test.d.ts +0 -2
- package/dist/test/auth.test.js +0 -175
- package/dist/test/compute-delegate-path.test.d.ts +0 -2
- package/dist/test/compute-delegate-path.test.js +0 -183
- package/dist/test/compute.test.d.ts +0 -2
- package/dist/test/compute.test.js +0 -194
- package/dist/test/endpoints.test.d.ts +0 -2
- package/dist/test/endpoints.test.js +0 -62
- package/dist/test/envelope.test.d.ts +0 -2
- package/dist/test/envelope.test.js +0 -318
- package/dist/test/ethos-first-edition.test.d.ts +0 -2
- package/dist/test/ethos-first-edition.test.js +0 -248
- package/dist/test/ethos.test.d.ts +0 -2
- package/dist/test/ethos.test.js +0 -219
- package/dist/test/key-store.test.d.ts +0 -2
- package/dist/test/key-store.test.js +0 -161
- package/dist/test/mandates-compute.test.d.ts +0 -2
- package/dist/test/mandates-compute.test.js +0 -256
- package/dist/test/mandates.test.d.ts +0 -2
- package/dist/test/mandates.test.js +0 -93
- package/dist/test/sdk.test.d.ts +0 -2
- package/dist/test/sdk.test.js +0 -126
- package/dist/test/signer.test.d.ts +0 -2
- package/dist/test/signer.test.js +0 -117
- package/dist/test/signup-bootstrap.test.d.ts +0 -2
- package/dist/test/signup-bootstrap.test.js +0 -311
- package/dist/test/wallet.test.d.ts +0 -2
- package/dist/test/wallet.test.js +0 -121
- package/dist/test/web.test.d.ts +0 -2
- package/dist/test/web.test.js +0 -270
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright 2026 Mathieu Colla
|
|
3
|
-
// Unit tests for the signed-envelope primitive — both the SDK-internal
|
|
4
|
-
// helper `signOwnerEnvelope` and the public surface
|
|
5
|
-
// `AithosAuth.signEnvelope`.
|
|
6
|
-
//
|
|
7
|
-
// Two levels of testing:
|
|
8
|
-
//
|
|
9
|
-
// 1. Internal helper: tests that can inject deterministic `now` and
|
|
10
|
-
// `nonce` to lock byte-for-byte output. These guard against any
|
|
11
|
-
// future drift in canonicalization, default TTL, field order, or
|
|
12
|
-
// crypto wiring.
|
|
13
|
-
//
|
|
14
|
-
// 2. Public surface: tests that exercise the full path through
|
|
15
|
-
// `AithosAuth.signEnvelope`, including sphere resolution, throw
|
|
16
|
-
// semantics, and binding to the loaded owner's DID. These guard
|
|
17
|
-
// the developer contract documented in the JSDoc.
|
|
18
|
-
import { strict as assert } from "node:assert";
|
|
19
|
-
import { describe, it } from "node:test";
|
|
20
|
-
import { createBrowserIdentity, sign as ed25519Sign, verify as ed25519Verify, } from "@aithos/protocol-client";
|
|
21
|
-
import { AithosAuth, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
22
|
-
import { signOwnerEnvelope } from "../src/internal/envelope.js";
|
|
23
|
-
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
24
|
-
/* -------------------------------------------------------------------------- */
|
|
25
|
-
/* Test helpers */
|
|
26
|
-
/* -------------------------------------------------------------------------- */
|
|
27
|
-
/** Build a minimal AithosAuth, no network, no persistence. */
|
|
28
|
-
function makeAuth() {
|
|
29
|
-
return new AithosAuth({
|
|
30
|
-
authBaseUrl: "https://auth.test",
|
|
31
|
-
fetch: (() => {
|
|
32
|
-
throw new Error("network not expected in this test");
|
|
33
|
-
}),
|
|
34
|
-
sessionStore: noopStore(),
|
|
35
|
-
keyStore: memoryKeyStore(),
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
/** Recovery-file text + DID for a fresh BrowserIdentity. */
|
|
39
|
-
function recoveryTextFor(handle, displayName) {
|
|
40
|
-
const id = createBrowserIdentity(handle, displayName);
|
|
41
|
-
return { text: serializeRecoveryFile(id).text, did: id.did };
|
|
42
|
-
}
|
|
43
|
-
/** Inline signer wrapping a raw seed — matches what data.ts does. */
|
|
44
|
-
function inlineSigner(seed) {
|
|
45
|
-
return {
|
|
46
|
-
async sign(message) {
|
|
47
|
-
return ed25519Sign(message, seed);
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
/** base64url decoder for envelope.proof.proofValue. */
|
|
52
|
-
function base64urlDecode(s) {
|
|
53
|
-
const std = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
54
|
-
const padded = std + "=".repeat((4 - (std.length % 4)) % 4);
|
|
55
|
-
const bin = atob(padded);
|
|
56
|
-
const out = new Uint8Array(bin.length);
|
|
57
|
-
for (let i = 0; i < bin.length; i++)
|
|
58
|
-
out[i] = bin.charCodeAt(i);
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
/** Canonicalize a JS value the same way envelope.ts does — for the
|
|
62
|
-
* test that recomputes the bytes the server would verify against. */
|
|
63
|
-
function canonical(value) {
|
|
64
|
-
if (value === null)
|
|
65
|
-
return "null";
|
|
66
|
-
if (typeof value === "boolean")
|
|
67
|
-
return value ? "true" : "false";
|
|
68
|
-
if (typeof value === "number")
|
|
69
|
-
return value.toString();
|
|
70
|
-
if (typeof value === "string")
|
|
71
|
-
return JSON.stringify(value);
|
|
72
|
-
if (Array.isArray(value)) {
|
|
73
|
-
return "[" + value.map(canonical).join(",") + "]";
|
|
74
|
-
}
|
|
75
|
-
if (typeof value === "object") {
|
|
76
|
-
const obj = value;
|
|
77
|
-
const keys = Object.keys(obj).sort();
|
|
78
|
-
return ("{" +
|
|
79
|
-
keys.map((k) => JSON.stringify(k) + ":" + canonical(obj[k])).join(",") +
|
|
80
|
-
"}");
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`Cannot canonicalize ${typeof value}`);
|
|
83
|
-
}
|
|
84
|
-
/* -------------------------------------------------------------------------- */
|
|
85
|
-
/* signOwnerEnvelope — internal helper */
|
|
86
|
-
/* -------------------------------------------------------------------------- */
|
|
87
|
-
describe("signOwnerEnvelope (internal helper)", () => {
|
|
88
|
-
it("signs an envelope whose signature verifies under the signer's pubkey", async () => {
|
|
89
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
90
|
-
const envelope = await signOwnerEnvelope({
|
|
91
|
-
iss: id.did,
|
|
92
|
-
aud: "https://api.example.com/v1/widgets",
|
|
93
|
-
method: "myapp.widgets.create",
|
|
94
|
-
params: { name: "Widget #1" },
|
|
95
|
-
signer: inlineSigner(id.public.seed),
|
|
96
|
-
verificationMethod: `${id.did}#public`,
|
|
97
|
-
});
|
|
98
|
-
// Re-derive the exact bytes the server would canonicalize and check.
|
|
99
|
-
const { proof, ...unsignedWithEmptyProof } = envelope;
|
|
100
|
-
const unsigned = {
|
|
101
|
-
...unsignedWithEmptyProof,
|
|
102
|
-
proof: { ...proof, proofValue: "" },
|
|
103
|
-
};
|
|
104
|
-
const bytes = new TextEncoder().encode(canonical(unsigned));
|
|
105
|
-
const sig = base64urlDecode(envelope.proof.proofValue);
|
|
106
|
-
assert.ok(ed25519Verify(sig, bytes, id.public.publicKey), "envelope signature must verify under the public-sphere pubkey");
|
|
107
|
-
});
|
|
108
|
-
it("produces an envelope conforming to spec §11.2 shape", async () => {
|
|
109
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
110
|
-
const envelope = await signOwnerEnvelope({
|
|
111
|
-
iss: id.did,
|
|
112
|
-
aud: "https://api.example.com/v1/x",
|
|
113
|
-
method: "x.do",
|
|
114
|
-
params: {},
|
|
115
|
-
signer: inlineSigner(id.public.seed),
|
|
116
|
-
verificationMethod: `${id.did}#public`,
|
|
117
|
-
});
|
|
118
|
-
assert.equal(envelope["aithos-envelope"], "0.1.0");
|
|
119
|
-
assert.equal(envelope.iss, id.did);
|
|
120
|
-
assert.equal(envelope.aud, "https://api.example.com/v1/x");
|
|
121
|
-
assert.equal(envelope.method, "x.do");
|
|
122
|
-
assert.equal(typeof envelope.iat, "number");
|
|
123
|
-
assert.equal(typeof envelope.exp, "number");
|
|
124
|
-
assert.equal(typeof envelope.nonce, "string");
|
|
125
|
-
assert.ok(envelope.params_hash.startsWith("sha256-"));
|
|
126
|
-
assert.equal(envelope.proof.type, "Ed25519Signature2020");
|
|
127
|
-
assert.equal(envelope.proof.verificationMethod, `${id.did}#public`);
|
|
128
|
-
assert.equal(typeof envelope.proof.created, "string");
|
|
129
|
-
assert.ok(envelope.proof.proofValue.length > 0);
|
|
130
|
-
});
|
|
131
|
-
it("params_hash is canonical — key order does not affect the hash", async () => {
|
|
132
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
133
|
-
const common = {
|
|
134
|
-
iss: id.did,
|
|
135
|
-
aud: "https://api.example.com/v1/x",
|
|
136
|
-
method: "x.do",
|
|
137
|
-
signer: inlineSigner(id.public.seed),
|
|
138
|
-
verificationMethod: `${id.did}#public`,
|
|
139
|
-
now: new Date("2024-01-01T00:00:00.000Z"),
|
|
140
|
-
nonce: "01HXJZK7MK8VPN0FQR5T6Y2A3Z",
|
|
141
|
-
};
|
|
142
|
-
const env1 = await signOwnerEnvelope({
|
|
143
|
-
...common,
|
|
144
|
-
params: { alpha: 1, beta: 2, gamma: [3, 4] },
|
|
145
|
-
});
|
|
146
|
-
const env2 = await signOwnerEnvelope({
|
|
147
|
-
...common,
|
|
148
|
-
params: { gamma: [3, 4], alpha: 1, beta: 2 },
|
|
149
|
-
});
|
|
150
|
-
assert.equal(env1.params_hash, env2.params_hash, "params_hash must be stable across JS object key order");
|
|
151
|
-
});
|
|
152
|
-
it("respects an explicit ttlSeconds", async () => {
|
|
153
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
154
|
-
const envelope = await signOwnerEnvelope({
|
|
155
|
-
iss: id.did,
|
|
156
|
-
aud: "https://api.example.com/v1/x",
|
|
157
|
-
method: "x.do",
|
|
158
|
-
params: {},
|
|
159
|
-
signer: inlineSigner(id.public.seed),
|
|
160
|
-
verificationMethod: `${id.did}#public`,
|
|
161
|
-
now: new Date("2024-01-01T00:00:00.000Z"),
|
|
162
|
-
ttlSeconds: 173,
|
|
163
|
-
});
|
|
164
|
-
assert.equal(envelope.exp - envelope.iat, 173);
|
|
165
|
-
});
|
|
166
|
-
it("defaults ttlSeconds to 60 — guards against drift from data.ts's prior behavior", async () => {
|
|
167
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
168
|
-
const envelope = await signOwnerEnvelope({
|
|
169
|
-
iss: id.did,
|
|
170
|
-
aud: "https://api.example.com/v1/x",
|
|
171
|
-
method: "x.do",
|
|
172
|
-
params: {},
|
|
173
|
-
signer: inlineSigner(id.public.seed),
|
|
174
|
-
verificationMethod: `${id.did}#public`,
|
|
175
|
-
now: new Date("2024-01-01T00:00:00.000Z"),
|
|
176
|
-
});
|
|
177
|
-
assert.equal(envelope.exp - envelope.iat, 60);
|
|
178
|
-
});
|
|
179
|
-
it("nonce defaults to a fresh value per call (unique across calls)", async () => {
|
|
180
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
181
|
-
const args = {
|
|
182
|
-
iss: id.did,
|
|
183
|
-
aud: "https://api.example.com/v1/x",
|
|
184
|
-
method: "x.do",
|
|
185
|
-
params: {},
|
|
186
|
-
signer: inlineSigner(id.public.seed),
|
|
187
|
-
verificationMethod: `${id.did}#public`,
|
|
188
|
-
};
|
|
189
|
-
const e1 = await signOwnerEnvelope(args);
|
|
190
|
-
const e2 = await signOwnerEnvelope(args);
|
|
191
|
-
assert.notEqual(e1.nonce, e2.nonce, "two successive calls must produce different nonces");
|
|
192
|
-
});
|
|
193
|
-
it("is deterministic when now and nonce are both injected (regression lock)", async () => {
|
|
194
|
-
// With identical inputs INCLUDING the override hooks, two calls must
|
|
195
|
-
// produce byte-for-byte identical envelopes — including the
|
|
196
|
-
// signature. This catches any future regression in canonicalization,
|
|
197
|
-
// ordering, default values, or signing path.
|
|
198
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
199
|
-
const args = {
|
|
200
|
-
iss: id.did,
|
|
201
|
-
aud: "https://api.example.com/v1/widgets",
|
|
202
|
-
method: "myapp.widgets.create",
|
|
203
|
-
params: { name: "Widget #1", count: 3, tags: ["a", "b"] },
|
|
204
|
-
signer: inlineSigner(id.public.seed),
|
|
205
|
-
verificationMethod: `${id.did}#public`,
|
|
206
|
-
now: new Date("2024-01-01T00:00:00.000Z"),
|
|
207
|
-
nonce: "01HXJZK7MK8VPN0FQR5T6Y2A3Z",
|
|
208
|
-
ttlSeconds: 120,
|
|
209
|
-
};
|
|
210
|
-
const e1 = await signOwnerEnvelope(args);
|
|
211
|
-
const e2 = await signOwnerEnvelope(args);
|
|
212
|
-
assert.deepEqual(e1, e2, "envelope must be deterministic under fixed inputs");
|
|
213
|
-
// Belt and braces: also lock the timestamp arithmetic.
|
|
214
|
-
assert.equal(e1.iat, 1704067200);
|
|
215
|
-
assert.equal(e1.exp, 1704067320);
|
|
216
|
-
assert.equal(e1.nonce, "01HXJZK7MK8VPN0FQR5T6Y2A3Z");
|
|
217
|
-
assert.equal(e1.proof.created, "2024-01-01T00:00:00.000Z");
|
|
218
|
-
assert.equal(e1.proof.verificationMethod, `${id.did}#public`);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
/* -------------------------------------------------------------------------- */
|
|
222
|
-
/* AithosAuth.signEnvelope — public surface */
|
|
223
|
-
/* -------------------------------------------------------------------------- */
|
|
224
|
-
describe("AithosAuth.signEnvelope (public surface)", () => {
|
|
225
|
-
it("defaults to the public sphere when no sphere is passed", async () => {
|
|
226
|
-
const auth = makeAuth();
|
|
227
|
-
const { text, did } = recoveryTextFor("alice", "Alice");
|
|
228
|
-
await auth.signInWithRecovery({ file: text });
|
|
229
|
-
const envelope = await auth.signEnvelope({
|
|
230
|
-
aud: "https://api.example.com/v1/x",
|
|
231
|
-
method: "x.do",
|
|
232
|
-
params: { ok: true },
|
|
233
|
-
});
|
|
234
|
-
assert.equal(envelope.iss, did);
|
|
235
|
-
assert.equal(envelope.proof.verificationMethod, `${did}#public`);
|
|
236
|
-
});
|
|
237
|
-
it("honors explicit sphere overrides (root / public / circle / self)", async () => {
|
|
238
|
-
const auth = makeAuth();
|
|
239
|
-
const { text, did } = recoveryTextFor("alice", "Alice");
|
|
240
|
-
await auth.signInWithRecovery({ file: text });
|
|
241
|
-
for (const sphere of ["root", "public", "circle", "self"]) {
|
|
242
|
-
const envelope = await auth.signEnvelope({
|
|
243
|
-
aud: "https://api.example.com/v1/x",
|
|
244
|
-
method: "x.do",
|
|
245
|
-
params: {},
|
|
246
|
-
sphere,
|
|
247
|
-
});
|
|
248
|
-
assert.equal(envelope.proof.verificationMethod, `${did}#${sphere}`, `verificationMethod must end with #${sphere}`);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
it("throws auth_not_signed_in when no owner is loaded", async () => {
|
|
252
|
-
const auth = makeAuth();
|
|
253
|
-
await assert.rejects(() => auth.signEnvelope({
|
|
254
|
-
aud: "https://api.example.com/v1/x",
|
|
255
|
-
method: "x.do",
|
|
256
|
-
params: {},
|
|
257
|
-
}), (e) => e instanceof AithosSDKError &&
|
|
258
|
-
e.code === "auth_not_signed_in");
|
|
259
|
-
});
|
|
260
|
-
it("throws auth_invalid_sphere when an unknown sphere is passed (untyped caller)", async () => {
|
|
261
|
-
const auth = makeAuth();
|
|
262
|
-
const { text } = recoveryTextFor("alice", "Alice");
|
|
263
|
-
await auth.signInWithRecovery({ file: text });
|
|
264
|
-
await assert.rejects(() => auth.signEnvelope({
|
|
265
|
-
aud: "https://api.example.com/v1/x",
|
|
266
|
-
method: "x.do",
|
|
267
|
-
params: {},
|
|
268
|
-
// Untyped callers (e.g. plain JS) could pass anything.
|
|
269
|
-
sphere: "bogus",
|
|
270
|
-
}), (e) => e instanceof AithosSDKError &&
|
|
271
|
-
e.code === "auth_invalid_sphere");
|
|
272
|
-
});
|
|
273
|
-
it("envelope ttlSeconds defaults to 60 — non-regression of internal default", async () => {
|
|
274
|
-
const auth = makeAuth();
|
|
275
|
-
const { text } = recoveryTextFor("alice", "Alice");
|
|
276
|
-
await auth.signInWithRecovery({ file: text });
|
|
277
|
-
const envelope = await auth.signEnvelope({
|
|
278
|
-
aud: "https://api.example.com/v1/x",
|
|
279
|
-
method: "x.do",
|
|
280
|
-
params: {},
|
|
281
|
-
});
|
|
282
|
-
assert.equal(envelope.exp - envelope.iat, 60);
|
|
283
|
-
});
|
|
284
|
-
it("envelope ttlSeconds is honored when provided", async () => {
|
|
285
|
-
const auth = makeAuth();
|
|
286
|
-
const { text } = recoveryTextFor("alice", "Alice");
|
|
287
|
-
await auth.signInWithRecovery({ file: text });
|
|
288
|
-
const envelope = await auth.signEnvelope({
|
|
289
|
-
aud: "https://api.example.com/v1/x",
|
|
290
|
-
method: "x.do",
|
|
291
|
-
params: {},
|
|
292
|
-
ttlSeconds: 240,
|
|
293
|
-
});
|
|
294
|
-
assert.equal(envelope.exp - envelope.iat, 240);
|
|
295
|
-
});
|
|
296
|
-
it("envelope signs with the correct sphere key (sig verifies under that pubkey)", async () => {
|
|
297
|
-
const auth = makeAuth();
|
|
298
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
299
|
-
const { text } = serializeRecoveryFile(id);
|
|
300
|
-
await auth.signInWithRecovery({ file: text });
|
|
301
|
-
const envelope = await auth.signEnvelope({
|
|
302
|
-
aud: "https://api.example.com/v1/x",
|
|
303
|
-
method: "x.do",
|
|
304
|
-
params: { x: 1 },
|
|
305
|
-
sphere: "circle",
|
|
306
|
-
});
|
|
307
|
-
// Reconstruct what the server would canonicalize-and-verify.
|
|
308
|
-
const { proof, ...unsignedWithEmptyProof } = envelope;
|
|
309
|
-
const unsigned = {
|
|
310
|
-
...unsignedWithEmptyProof,
|
|
311
|
-
proof: { ...proof, proofValue: "" },
|
|
312
|
-
};
|
|
313
|
-
const bytes = new TextEncoder().encode(canonical(unsigned));
|
|
314
|
-
const sig = base64urlDecode(envelope.proof.proofValue);
|
|
315
|
-
assert.ok(ed25519Verify(sig, bytes, id.circle.publicKey), "envelope signed with sphere 'circle' must verify under circle.publicKey");
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
//# sourceMappingURL=envelope.test.js.map
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright 2026 Mathieu Colla
|
|
3
|
-
// Tests for the alpha.7 first-edition path in EthosClient — the case
|
|
4
|
-
// where an Ethos identity exists on api.aithos.be (provisioned by
|
|
5
|
-
// auth.signUp() since alpha.6) but no edition has been published yet.
|
|
6
|
-
//
|
|
7
|
-
// Two flows must work:
|
|
8
|
-
// 1. Reading any zone returns an empty list (instead of throwing
|
|
9
|
-
// "not found: edition").
|
|
10
|
-
// 2. Publishing for the first time builds height=1 from staged
|
|
11
|
-
// mutations and POSTs publish_ethos_edition directly, instead
|
|
12
|
-
// of going through publishZoneEdit (which requires a previous
|
|
13
|
-
// manifest).
|
|
14
|
-
//
|
|
15
|
-
// We mock global fetch end-to-end so the tests run offline.
|
|
16
|
-
import { strict as assert } from "node:assert";
|
|
17
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
18
|
-
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
19
|
-
import { AithosAuth, AithosSDKError, EthosNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
20
|
-
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
21
|
-
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
22
|
-
let fetchCalls = [];
|
|
23
|
-
let savedFetch;
|
|
24
|
-
function installFetchMock(handlers) {
|
|
25
|
-
savedFetch = globalThis.fetch;
|
|
26
|
-
fetchCalls = [];
|
|
27
|
-
globalThis.fetch = (async (input, init) => {
|
|
28
|
-
const url = String(input);
|
|
29
|
-
const method = init?.method ?? "GET";
|
|
30
|
-
const bodyText = typeof init?.body === "string"
|
|
31
|
-
? init.body
|
|
32
|
-
: init?.body == null
|
|
33
|
-
? null
|
|
34
|
-
: String(init.body);
|
|
35
|
-
const body = bodyText ? JSON.parse(bodyText) : null;
|
|
36
|
-
const call = { url, method, body };
|
|
37
|
-
fetchCalls.push(call);
|
|
38
|
-
for (const h of handlers) {
|
|
39
|
-
if (!url.includes(h.url))
|
|
40
|
-
continue;
|
|
41
|
-
if (h.rpcMethod && body?.method !== h.rpcMethod)
|
|
42
|
-
continue;
|
|
43
|
-
const out = h.respond(call);
|
|
44
|
-
const status = out.status ?? 200;
|
|
45
|
-
return new Response(JSON.stringify(out.json), {
|
|
46
|
-
status,
|
|
47
|
-
headers: { "content-type": "application/json" },
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
throw new Error(`unhandled fetch: ${method} ${url} (rpc: ${body?.method ?? "n/a"})`);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
function uninstallFetchMock() {
|
|
54
|
-
if (savedFetch) {
|
|
55
|
-
globalThis.fetch = savedFetch;
|
|
56
|
-
savedFetch = undefined;
|
|
57
|
-
}
|
|
58
|
-
fetchCalls = [];
|
|
59
|
-
}
|
|
60
|
-
function makeAuth() {
|
|
61
|
-
return new AithosAuth({
|
|
62
|
-
authBaseUrl: "https://auth.test",
|
|
63
|
-
apiBaseUrl: "https://api.test",
|
|
64
|
-
fetch: (() => {
|
|
65
|
-
throw new Error("AithosAuth.fetch must not be called in these tests");
|
|
66
|
-
}),
|
|
67
|
-
sessionStore: noopStore(),
|
|
68
|
-
keyStore: memoryKeyStore(),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
function makeNamespace(auth) {
|
|
72
|
-
return new EthosNamespace({
|
|
73
|
-
auth,
|
|
74
|
-
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
75
|
-
// EthosNamespace itself doesn't read fetch from this slot today;
|
|
76
|
-
// protocol-client uses the global fetch we mock above.
|
|
77
|
-
fetch: globalThis.fetch.bind(globalThis),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
async function signInAsAlice(auth) {
|
|
81
|
-
const id = createBrowserIdentity("alice", "Alice");
|
|
82
|
-
const { text } = serializeRecoveryFile(id);
|
|
83
|
-
const info = await auth.signInWithRecovery({ file: text });
|
|
84
|
-
return { did: info.did };
|
|
85
|
-
}
|
|
86
|
-
function noEditionYetResponse() {
|
|
87
|
-
// Mirrors the server's primitives-read `notFound("edition for <did>")`:
|
|
88
|
-
// JSON-RPC error code -32020, message starts with "not found: edition for ".
|
|
89
|
-
return {
|
|
90
|
-
json: {
|
|
91
|
-
jsonrpc: "2.0",
|
|
92
|
-
id: "aithos.get_ethos_manifest",
|
|
93
|
-
error: {
|
|
94
|
-
code: -32020,
|
|
95
|
-
message: "not found: edition for did:aithos:zSomething",
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
function publishOkResponse() {
|
|
101
|
-
return {
|
|
102
|
-
json: {
|
|
103
|
-
jsonrpc: "2.0",
|
|
104
|
-
id: "publish_ethos_edition",
|
|
105
|
-
result: { ok: true, height: 1, manifest_uri: "s3://aithos/.../manifest.json" },
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/* -------------------------------------------------------------------------- */
|
|
110
|
-
/* Tests */
|
|
111
|
-
/* -------------------------------------------------------------------------- */
|
|
112
|
-
describe("EthosClient — fresh Ethos (no edition published yet)", () => {
|
|
113
|
-
beforeEach(() => {
|
|
114
|
-
fetchCalls = [];
|
|
115
|
-
});
|
|
116
|
-
afterEach(() => {
|
|
117
|
-
uninstallFetchMock();
|
|
118
|
-
});
|
|
119
|
-
it("zone(public).sections() returns [] when server says 'not found: edition'", async () => {
|
|
120
|
-
installFetchMock([
|
|
121
|
-
{
|
|
122
|
-
url: "/mcp/primitives/read",
|
|
123
|
-
rpcMethod: "aithos.get_ethos_manifest",
|
|
124
|
-
respond: noEditionYetResponse,
|
|
125
|
-
},
|
|
126
|
-
]);
|
|
127
|
-
const auth = makeAuth();
|
|
128
|
-
await signInAsAlice(auth);
|
|
129
|
-
const me = makeNamespace(auth).me();
|
|
130
|
-
const sections = await me.zone("public").sections();
|
|
131
|
-
assert.deepEqual(sections, []);
|
|
132
|
-
});
|
|
133
|
-
it("zone(public).sections() reflects locally staged adds even when no edition exists", async () => {
|
|
134
|
-
installFetchMock([
|
|
135
|
-
{
|
|
136
|
-
url: "/mcp/primitives/read",
|
|
137
|
-
rpcMethod: "aithos.get_ethos_manifest",
|
|
138
|
-
respond: noEditionYetResponse,
|
|
139
|
-
},
|
|
140
|
-
]);
|
|
141
|
-
const auth = makeAuth();
|
|
142
|
-
await signInAsAlice(auth);
|
|
143
|
-
const me = makeNamespace(auth).me();
|
|
144
|
-
me.zone("public").addSection({ title: "Hello", body: "World" });
|
|
145
|
-
const sections = await me.zone("public").sections();
|
|
146
|
-
assert.equal(sections.length, 1);
|
|
147
|
-
assert.equal(sections[0].title, "Hello");
|
|
148
|
-
});
|
|
149
|
-
it("publish() routes to publish_ethos_edition with height=1 on first publish", async () => {
|
|
150
|
-
let publishBody = null;
|
|
151
|
-
installFetchMock([
|
|
152
|
-
{
|
|
153
|
-
url: "/mcp/primitives/read",
|
|
154
|
-
rpcMethod: "aithos.get_ethos_manifest",
|
|
155
|
-
respond: noEditionYetResponse,
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
url: "/mcp/primitives/write",
|
|
159
|
-
rpcMethod: "aithos.publish_ethos_edition",
|
|
160
|
-
respond: (call) => {
|
|
161
|
-
publishBody = call.body;
|
|
162
|
-
return publishOkResponse();
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
]);
|
|
166
|
-
const auth = makeAuth();
|
|
167
|
-
const alice = await signInAsAlice(auth);
|
|
168
|
-
const me = makeNamespace(auth).me();
|
|
169
|
-
me.zone("public").addSection({ title: "First", body: "Hello." });
|
|
170
|
-
me.zone("public").addSection({ title: "Second", body: "World." });
|
|
171
|
-
const r = await me.publish();
|
|
172
|
-
assert.equal(r.editionHeight, 1);
|
|
173
|
-
assert.equal(r.subjectDid, alice.did);
|
|
174
|
-
assert.deepEqual(r.zonesPublished, ["public"]);
|
|
175
|
-
// Verify the wire shape: JSON-RPC publish_ethos_edition with a height=1
|
|
176
|
-
// manifest containing both staged sections.
|
|
177
|
-
assert.equal(publishBody.method, "aithos.publish_ethos_edition");
|
|
178
|
-
const manifest = publishBody.params.manifest;
|
|
179
|
-
assert.equal(manifest.edition.height, 1);
|
|
180
|
-
assert.equal(manifest.edition.prev_hash, null);
|
|
181
|
-
assert.equal(manifest.edition.supersedes, null);
|
|
182
|
-
assert.deepEqual(manifest.zones.public.section_titles, ["First", "Second"]);
|
|
183
|
-
// Envelope is signed under #public.
|
|
184
|
-
const env = publishBody.params._envelope;
|
|
185
|
-
assert.equal(env.method, "aithos.publish_ethos_edition");
|
|
186
|
-
assert.match(env.proof.verificationMethod, /#public$/);
|
|
187
|
-
});
|
|
188
|
-
it("publish() rejects circle/self mutations on first edition", async () => {
|
|
189
|
-
installFetchMock([
|
|
190
|
-
{
|
|
191
|
-
url: "/mcp/primitives/read",
|
|
192
|
-
rpcMethod: "aithos.get_ethos_manifest",
|
|
193
|
-
respond: noEditionYetResponse,
|
|
194
|
-
},
|
|
195
|
-
]);
|
|
196
|
-
const auth = makeAuth();
|
|
197
|
-
await signInAsAlice(auth);
|
|
198
|
-
const me = makeNamespace(auth).me();
|
|
199
|
-
me.zone("circle").addSection({ title: "Private", body: "..." });
|
|
200
|
-
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_public_only");
|
|
201
|
-
});
|
|
202
|
-
it("publish() rejects update/delete operations on a fresh Ethos", async () => {
|
|
203
|
-
installFetchMock([
|
|
204
|
-
{
|
|
205
|
-
url: "/mcp/primitives/read",
|
|
206
|
-
rpcMethod: "aithos.get_ethos_manifest",
|
|
207
|
-
respond: noEditionYetResponse,
|
|
208
|
-
},
|
|
209
|
-
]);
|
|
210
|
-
const auth = makeAuth();
|
|
211
|
-
await signInAsAlice(auth);
|
|
212
|
-
const me = makeNamespace(auth).me();
|
|
213
|
-
// Stage a delete for a section that doesn't exist (no edition exists at all).
|
|
214
|
-
me.zone("public")["_parent"]; // type-safety placeholder; we use the public API
|
|
215
|
-
// EthosZone exposes deleteSection — go via that.
|
|
216
|
-
me.zone("public").deleteSection("sec_doesnotexist000");
|
|
217
|
-
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_invalid_op");
|
|
218
|
-
});
|
|
219
|
-
it("publish() surfaces server JSON-RPC errors as ethos_first_edition_rejected", async () => {
|
|
220
|
-
installFetchMock([
|
|
221
|
-
{
|
|
222
|
-
url: "/mcp/primitives/read",
|
|
223
|
-
rpcMethod: "aithos.get_ethos_manifest",
|
|
224
|
-
respond: noEditionYetResponse,
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
url: "/mcp/primitives/write",
|
|
228
|
-
rpcMethod: "aithos.publish_ethos_edition",
|
|
229
|
-
respond: () => ({
|
|
230
|
-
json: {
|
|
231
|
-
jsonrpc: "2.0",
|
|
232
|
-
id: "publish_ethos_edition",
|
|
233
|
-
error: {
|
|
234
|
-
code: -32020,
|
|
235
|
-
message: "subject identity not published (call publish_identity first)",
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
}),
|
|
239
|
-
},
|
|
240
|
-
]);
|
|
241
|
-
const auth = makeAuth();
|
|
242
|
-
await signInAsAlice(auth);
|
|
243
|
-
const me = makeNamespace(auth).me();
|
|
244
|
-
me.zone("public").addSection({ title: "Hi", body: "There." });
|
|
245
|
-
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_rejected");
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
//# sourceMappingURL=ethos-first-edition.test.js.map
|