@aithos/sdk 0.1.0-alpha.5 → 0.1.0-alpha.51
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 +245 -7
- package/dist/src/apps.d.ts +224 -0
- package/dist/src/apps.js +432 -0
- package/dist/src/assets.d.ts +209 -0
- package/dist/src/assets.js +534 -0
- package/dist/src/auth-api.d.ts +219 -0
- package/dist/src/auth-api.js +248 -0
- package/dist/src/auth.d.ts +543 -0
- package/dist/src/auth.js +937 -31
- package/dist/src/compute.d.ts +464 -6
- package/dist/src/compute.js +746 -20
- package/dist/src/data-schema-contacts-v1.d.ts +14 -0
- package/dist/src/data-schema-contacts-v1.js +28 -0
- package/dist/src/data.d.ts +342 -0
- package/dist/src/data.js +1002 -0
- package/dist/src/endpoints.d.ts +25 -0
- package/dist/src/endpoints.js +7 -0
- package/dist/src/ethos.d.ts +85 -0
- package/dist/src/ethos.js +463 -7
- package/dist/src/index.d.ts +17 -6
- package/dist/src/index.js +25 -3
- package/dist/src/internal/delegate-bundle.js +7 -2
- package/dist/src/internal/envelope.d.ts +93 -0
- package/dist/src/internal/envelope.js +59 -0
- package/dist/src/mandates.d.ts +111 -2
- package/dist/src/mandates.js +150 -7
- 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 +29 -0
- package/dist/src/react/index.js +31 -0
- package/dist/src/react/use-aithos-asset.d.ts +39 -0
- package/dist/src/react/use-aithos-asset.js +118 -0
- package/dist/src/react/use-transcribe-pending.d.ts +21 -0
- package/dist/src/react/use-transcribe-pending.js +47 -0
- package/dist/src/sdk.d.ts +10 -0
- package/dist/src/sdk.js +22 -0
- package/dist/src/transcribe-resilience.d.ts +57 -0
- package/dist/src/transcribe-resilience.js +203 -0
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/dist/test/auth-j3.test.js +32 -1
- package/dist/test/canonical-conformance.test.d.ts +2 -0
- package/dist/test/canonical-conformance.test.js +86 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +4 -0
- package/dist/test/endpoints.test.js +30 -1
- package/dist/test/envelope-core-conformance.test.d.ts +2 -0
- package/dist/test/envelope-core-conformance.test.js +75 -0
- package/dist/test/envelope.test.d.ts +2 -0
- package/dist/test/envelope.test.js +318 -0
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +371 -0
- package/dist/test/mandates-compute.test.d.ts +2 -0
- package/dist/test/mandates-compute.test.js +256 -0
- package/dist/test/sdk.test.js +11 -2
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +311 -0
- package/dist/test/transcribe-invoke.test.d.ts +2 -0
- package/dist/test/transcribe-invoke.test.js +204 -0
- package/dist/test/transcribe.test.d.ts +2 -0
- package/dist/test/transcribe.test.js +186 -0
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +20 -3
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for the delegate signing path on sdk.compute.invokeBedrock.
|
|
4
|
+
// Until alpha.9 the method ALWAYS required an owner — a session that
|
|
5
|
+
// only held a mandate (no owner signers) failed with sdk_no_owner
|
|
6
|
+
// before any network call. From alpha.9 onward, when no owner is
|
|
7
|
+
// loaded but a delegate matches the requested mandate id, the SDK
|
|
8
|
+
// signs the envelope with the delegate's keypair and attaches the
|
|
9
|
+
// SignedMandate.
|
|
10
|
+
import { strict as assert } from "node:assert";
|
|
11
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
12
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
13
|
+
import { AithosAuth, AithosSDKError, ComputeNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
14
|
+
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
15
|
+
/* -------------------------------------------------------------------------- */
|
|
16
|
+
/* Test plumbing */
|
|
17
|
+
/* -------------------------------------------------------------------------- */
|
|
18
|
+
let savedFetch;
|
|
19
|
+
let lastRequestBody = null;
|
|
20
|
+
function installFetchMock(response) {
|
|
21
|
+
savedFetch = globalThis.fetch;
|
|
22
|
+
lastRequestBody = null;
|
|
23
|
+
globalThis.fetch = (async (_input, init) => {
|
|
24
|
+
lastRequestBody = JSON.parse(init?.body);
|
|
25
|
+
return new Response(JSON.stringify(response.json), {
|
|
26
|
+
status: response.status ?? 200,
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function uninstallFetchMock() {
|
|
32
|
+
if (savedFetch)
|
|
33
|
+
globalThis.fetch = savedFetch;
|
|
34
|
+
savedFetch = undefined;
|
|
35
|
+
lastRequestBody = null;
|
|
36
|
+
}
|
|
37
|
+
const okResponse = {
|
|
38
|
+
json: {
|
|
39
|
+
jsonrpc: "2.0",
|
|
40
|
+
id: "x",
|
|
41
|
+
result: {
|
|
42
|
+
content: "ok",
|
|
43
|
+
stopReason: "end_turn",
|
|
44
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
45
|
+
creditsCharged: 1,
|
|
46
|
+
walletBalance: 999,
|
|
47
|
+
auditId: "audit-test",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
function freshAuth() {
|
|
52
|
+
return new AithosAuth({
|
|
53
|
+
sessionStore: noopStore(),
|
|
54
|
+
keyStore: memoryKeyStore(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function freshCompute(auth) {
|
|
58
|
+
return new ComputeNamespace({
|
|
59
|
+
auth,
|
|
60
|
+
appDid: "did:aithos:app:example-placeholder",
|
|
61
|
+
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
62
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a delegate bundle JSON that matches the wire shape importMandate
|
|
67
|
+
* accepts. Uses a real SignedMandate-like object minted from a fresh
|
|
68
|
+
* issuer so signature verification on import succeeds.
|
|
69
|
+
*/
|
|
70
|
+
function makeDelegateBundleText(args) {
|
|
71
|
+
const issuer = createBrowserIdentity("alice", "Alice");
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
aithos_delegate_version: "0.1.0",
|
|
74
|
+
mandate: {
|
|
75
|
+
"aithos-mandate": "0.4.0",
|
|
76
|
+
id: args.mandateId,
|
|
77
|
+
issuer: issuer.did,
|
|
78
|
+
issued_by_key: `${issuer.did}#self`,
|
|
79
|
+
grantee: {
|
|
80
|
+
id: args.granteeId ?? "urn:aithos:agent:test",
|
|
81
|
+
pubkey: "z6MkqGenericPubKey",
|
|
82
|
+
},
|
|
83
|
+
actor_sphere: "self",
|
|
84
|
+
scopes: args.scopes,
|
|
85
|
+
not_before: "2026-05-10T00:00:00Z",
|
|
86
|
+
not_after: "2026-05-11T00:00:00Z",
|
|
87
|
+
issued_at: "2026-05-10T00:00:00Z",
|
|
88
|
+
nonce: "abc",
|
|
89
|
+
signature: { alg: "ed25519", key: `${issuer.did}#self`, value: "..." },
|
|
90
|
+
},
|
|
91
|
+
delegate_seed_hex: "11".repeat(32),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/* -------------------------------------------------------------------------- */
|
|
95
|
+
/* Tests */
|
|
96
|
+
/* -------------------------------------------------------------------------- */
|
|
97
|
+
describe("ComputeNamespace.invokeBedrock — delegate path", () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
installFetchMock(okResponse);
|
|
100
|
+
});
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
uninstallFetchMock();
|
|
103
|
+
});
|
|
104
|
+
it("rejects with sdk_no_delegate_for_mandate when delegate-only and mandateId doesn't match", async () => {
|
|
105
|
+
const auth = freshAuth();
|
|
106
|
+
const compute = freshCompute(auth);
|
|
107
|
+
// Import a delegate with a DIFFERENT mandate id than the one the call
|
|
108
|
+
// requests. Should error with the new code (NOT sdk_no_owner anymore).
|
|
109
|
+
await auth.importMandate({
|
|
110
|
+
bundle: makeDelegateBundleText({
|
|
111
|
+
mandateId: "mandate:held",
|
|
112
|
+
scopes: ["compute.invoke"],
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
await assert.rejects(() => compute.invokeBedrock({
|
|
116
|
+
mandateId: "mandate:other-not-held",
|
|
117
|
+
model: "claude-haiku-4-5",
|
|
118
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
119
|
+
maxTokens: 1,
|
|
120
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
121
|
+
e.code === "sdk_no_delegate_for_mandate");
|
|
122
|
+
});
|
|
123
|
+
it("signs with the delegate keypair + attaches the mandate when delegate-only matches", async () => {
|
|
124
|
+
const auth = freshAuth();
|
|
125
|
+
const compute = freshCompute(auth);
|
|
126
|
+
const mandateId = "mandate:matching";
|
|
127
|
+
await auth.importMandate({
|
|
128
|
+
bundle: makeDelegateBundleText({
|
|
129
|
+
mandateId,
|
|
130
|
+
scopes: ["compute.invoke"],
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
await compute.invokeBedrock({
|
|
134
|
+
mandateId,
|
|
135
|
+
model: "claude-haiku-4-5",
|
|
136
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
137
|
+
maxTokens: 1,
|
|
138
|
+
});
|
|
139
|
+
const env = lastRequestBody?.params?._envelope;
|
|
140
|
+
assert.ok(env, "envelope must be present");
|
|
141
|
+
// verificationMethod must be the delegate's bare multibase pubkey
|
|
142
|
+
// (NOT a `#sphere` DID URL). And the SignedMandate must be inside.
|
|
143
|
+
assert.match(env.proof.verificationMethod, /^z[1-9A-HJ-NP-Za-km-z]+$/, `expected multibase pubkey, got ${env.proof.verificationMethod}`);
|
|
144
|
+
assert.ok(env.mandate, "delegate envelopes must attach the SignedMandate");
|
|
145
|
+
assert.equal(env.mandate.id, mandateId);
|
|
146
|
+
});
|
|
147
|
+
it("still works the owner path when an owner is signed in", async () => {
|
|
148
|
+
const auth = freshAuth();
|
|
149
|
+
const compute = freshCompute(auth);
|
|
150
|
+
// Owner sign-in via recovery — picks up four sphere keys from the
|
|
151
|
+
// recovery file format. Build one inline.
|
|
152
|
+
const issuer = createBrowserIdentity("owner", "Owner");
|
|
153
|
+
const seedHex = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
154
|
+
const recoveryText = JSON.stringify({
|
|
155
|
+
aithos_recovery_version: "0.1.0-plaintext",
|
|
156
|
+
handle: issuer.handle,
|
|
157
|
+
display_name: issuer.displayName,
|
|
158
|
+
did: issuer.did,
|
|
159
|
+
created_at: new Date().toISOString(),
|
|
160
|
+
seeds_hex: {
|
|
161
|
+
root: seedHex(issuer.root.seed),
|
|
162
|
+
public: seedHex(issuer.public.seed),
|
|
163
|
+
circle: seedHex(issuer.circle.seed),
|
|
164
|
+
self: seedHex(issuer.self.seed),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
await auth.signInWithRecovery({ file: recoveryText });
|
|
168
|
+
await compute.invokeBedrock({
|
|
169
|
+
mandateId: "mandate:owner-managed",
|
|
170
|
+
model: "claude-haiku-4-5",
|
|
171
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
172
|
+
maxTokens: 1,
|
|
173
|
+
});
|
|
174
|
+
const env = lastRequestBody?.params?._envelope;
|
|
175
|
+
assert.ok(env, "envelope must be present");
|
|
176
|
+
// Owner path: verificationMethod is a `#public` DID URL, and the
|
|
177
|
+
// envelope MUST NOT carry a mandate (server resolves from
|
|
178
|
+
// params.mandate_id).
|
|
179
|
+
assert.match(env.proof.verificationMethod, /#public$/);
|
|
180
|
+
assert.equal(env.mandate, undefined, "owner-signed envelopes should not attach a mandate");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=compute-delegate-path.test.js.map
|
|
@@ -187,4 +187,8 @@ describe("compute.invokeBedrock — abort", () => {
|
|
|
187
187
|
assert.equal(receivedSignal, ac.signal);
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
|
+
// `compute.invokeUrlFetch` was removed in alpha.24 (BREAKING). The
|
|
191
|
+
// Anthropic API-direct + web_fetch tool path is replaced by the
|
|
192
|
+
// `sdk.web.extract` namespace which routes through the deterministic
|
|
193
|
+
// web-extractor Lambda. No tests here anymore.
|
|
190
194
|
//# sourceMappingURL=compute.test.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { strict as assert } from "node:assert";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { DEFAULT_SDK_ENDPOINTS } from "../src/index.js";
|
|
7
|
-
import { computeInvokeUrl, resolveEndpoints, walletTopupCheckoutUrl, } from "../src/endpoints.js";
|
|
7
|
+
import { computeInvokeUrl, resolveEndpoints, walletTopupCheckoutUrl, webInvokeUrl, } from "../src/endpoints.js";
|
|
8
8
|
describe("resolveEndpoints", () => {
|
|
9
9
|
it("returns a fresh copy of the defaults when no override is given", () => {
|
|
10
10
|
const a = resolveEndpoints();
|
|
@@ -23,12 +23,18 @@ describe("computeInvokeUrl", () => {
|
|
|
23
23
|
assert.equal(computeInvokeUrl({
|
|
24
24
|
compute: "https://compute.aithos.be",
|
|
25
25
|
wallet: "https://wallet.aithos.be",
|
|
26
|
+
web: "https://extract.aithos.be",
|
|
27
|
+
pds: "https://pds.aithos.be",
|
|
28
|
+
assets: "https://assets.aithos.be",
|
|
26
29
|
}), "https://compute.aithos.be/v1/invoke");
|
|
27
30
|
});
|
|
28
31
|
it("trims a trailing slash on the compute base", () => {
|
|
29
32
|
assert.equal(computeInvokeUrl({
|
|
30
33
|
compute: "https://compute.aithos.be/",
|
|
31
34
|
wallet: "https://wallet.aithos.be",
|
|
35
|
+
web: "https://extract.aithos.be",
|
|
36
|
+
pds: "https://pds.aithos.be",
|
|
37
|
+
assets: "https://assets.aithos.be",
|
|
32
38
|
}), "https://compute.aithos.be/v1/invoke");
|
|
33
39
|
});
|
|
34
40
|
});
|
|
@@ -37,7 +43,30 @@ describe("walletTopupCheckoutUrl", () => {
|
|
|
37
43
|
assert.equal(walletTopupCheckoutUrl({
|
|
38
44
|
compute: "https://compute.aithos.be",
|
|
39
45
|
wallet: "https://wallet.aithos.be",
|
|
46
|
+
web: "https://extract.aithos.be",
|
|
47
|
+
pds: "https://pds.aithos.be",
|
|
48
|
+
assets: "https://assets.aithos.be",
|
|
40
49
|
}), "https://wallet.aithos.be/v1/wallet/topup/checkout");
|
|
41
50
|
});
|
|
42
51
|
});
|
|
52
|
+
describe("webInvokeUrl", () => {
|
|
53
|
+
it("appends /v1/invoke to the web base", () => {
|
|
54
|
+
assert.equal(webInvokeUrl({
|
|
55
|
+
compute: "https://compute.aithos.be",
|
|
56
|
+
wallet: "https://wallet.aithos.be",
|
|
57
|
+
web: "https://extract.aithos.be",
|
|
58
|
+
pds: "https://pds.aithos.be",
|
|
59
|
+
assets: "https://assets.aithos.be",
|
|
60
|
+
}), "https://extract.aithos.be/v1/invoke");
|
|
61
|
+
});
|
|
62
|
+
it("trims a trailing slash on the web base", () => {
|
|
63
|
+
assert.equal(webInvokeUrl({
|
|
64
|
+
compute: "https://compute.aithos.be",
|
|
65
|
+
wallet: "https://wallet.aithos.be",
|
|
66
|
+
web: "https://extract.aithos.be/",
|
|
67
|
+
pds: "https://pds.aithos.be",
|
|
68
|
+
assets: "https://assets.aithos.be",
|
|
69
|
+
}), "https://extract.aithos.be/v1/invoke");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
43
72
|
//# sourceMappingURL=endpoints.test.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* Conformance for the envelope-signing refactor.
|
|
5
|
+
*
|
|
6
|
+
* `signOwnerEnvelope` now delegates to `@aithos/protocol-core`'s
|
|
7
|
+
* `signEnvelopeWith` (pluggable async signer) instead of carrying a private
|
|
8
|
+
* JCS + signing implementation. These tests assert the migrated path is
|
|
9
|
+
* correct and stable:
|
|
10
|
+
*
|
|
11
|
+
* 1. the produced envelope's `proof.proofValue` verifies against the signer's
|
|
12
|
+
* public key over core's canonical signing bytes (i.e. the server will
|
|
13
|
+
* accept it);
|
|
14
|
+
* 2. `params_hash` equals core's `envelopeParamsHash(params)`;
|
|
15
|
+
* 3. with a fixed clock + nonce the output is deterministic (snapshot), which
|
|
16
|
+
* is what guarantees the wire format did not shift under the refactor.
|
|
17
|
+
*
|
|
18
|
+
* Note: this suite needs the SDK to resolve a build of `@aithos/protocol-core`
|
|
19
|
+
* that exposes `signEnvelopeWith` (>= 0.6.3). Run via `npm test`.
|
|
20
|
+
*/
|
|
21
|
+
import { describe, test } from "node:test";
|
|
22
|
+
import { strict as assert } from "node:assert";
|
|
23
|
+
import * as ed from "@noble/ed25519";
|
|
24
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
25
|
+
import { envelopeParamsHash, envelopeSigningBytes, } from "@aithos/protocol-core/envelope";
|
|
26
|
+
import { signOwnerEnvelope } from "../src/internal/envelope.js";
|
|
27
|
+
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
28
|
+
const SEED = new Uint8Array(32).fill(11);
|
|
29
|
+
const PUB = ed.getPublicKey(SEED);
|
|
30
|
+
const NOW = new Date("2026-05-31T12:00:00.000Z");
|
|
31
|
+
const NONCE = "01J0ENVELOPECONFORMANCE0000";
|
|
32
|
+
const ISS = "did:aithos:z6MkEnvelopeConformance";
|
|
33
|
+
const VM = `${ISS}#public`;
|
|
34
|
+
const base = {
|
|
35
|
+
iss: ISS,
|
|
36
|
+
aud: "https://api.aithos.be/mcp/primitives/write",
|
|
37
|
+
method: "aithos.data.insert_record",
|
|
38
|
+
params: { b: 2, a: 1, nested: { y: [3, 1, 2], x: "z" } },
|
|
39
|
+
verificationMethod: VM,
|
|
40
|
+
signer: { sign: async (bytes) => ed.sign(bytes, SEED) },
|
|
41
|
+
ttlSeconds: 60,
|
|
42
|
+
now: NOW,
|
|
43
|
+
nonce: NONCE,
|
|
44
|
+
};
|
|
45
|
+
describe("signOwnerEnvelope — core delegation conformance", () => {
|
|
46
|
+
test("proofValue verifies against signing bytes (server will accept)", async () => {
|
|
47
|
+
const env = await signOwnerEnvelope({ ...base });
|
|
48
|
+
const sig = b64urlDecode(env.proof.proofValue);
|
|
49
|
+
assert.equal(ed.verify(sig, envelopeSigningBytes(env), PUB), true);
|
|
50
|
+
});
|
|
51
|
+
test("params_hash matches core.envelopeParamsHash", async () => {
|
|
52
|
+
const env = await signOwnerEnvelope({ ...base });
|
|
53
|
+
assert.equal(env.params_hash, envelopeParamsHash(base.params));
|
|
54
|
+
});
|
|
55
|
+
test("deterministic under fixed clock + nonce (wire-format snapshot)", async () => {
|
|
56
|
+
const a = await signOwnerEnvelope({ ...base });
|
|
57
|
+
const b = await signOwnerEnvelope({ ...base });
|
|
58
|
+
assert.equal(JSON.stringify(a), JSON.stringify(b));
|
|
59
|
+
assert.equal(a["aithos-envelope"], "0.1.0");
|
|
60
|
+
assert.equal(a.iat, 1780228800);
|
|
61
|
+
assert.equal(a.exp, 1780228860);
|
|
62
|
+
assert.equal(a.nonce, NONCE);
|
|
63
|
+
assert.equal(a.proof.verificationMethod, VM);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
function b64urlDecode(s) {
|
|
67
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
|
|
68
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
69
|
+
const bin = atob(b64);
|
|
70
|
+
const out = new Uint8Array(bin.length);
|
|
71
|
+
for (let i = 0; i < bin.length; i++)
|
|
72
|
+
out[i] = bin.charCodeAt(i);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=envelope-core-conformance.test.js.map
|
|
@@ -0,0 +1,318 @@
|
|
|
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
|