@aithos/sdk 0.1.0-alpha.4 → 0.1.0-alpha.41

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.
Files changed (80) hide show
  1. package/README.md +211 -7
  2. package/dist/src/apps.d.ts +155 -0
  3. package/dist/src/apps.js +288 -0
  4. package/dist/src/assets.d.ts +207 -0
  5. package/dist/src/assets.js +533 -0
  6. package/dist/src/auth-api.d.ts +138 -0
  7. package/dist/src/auth-api.js +168 -0
  8. package/dist/src/auth.d.ts +536 -119
  9. package/dist/src/auth.js +1207 -152
  10. package/dist/src/compute.d.ts +251 -9
  11. package/dist/src/compute.js +293 -16
  12. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  13. package/dist/src/data-schema-contacts-v1.js +28 -0
  14. package/dist/src/data.d.ts +153 -0
  15. package/dist/src/data.js +670 -0
  16. package/dist/src/endpoints.d.ts +9 -0
  17. package/dist/src/endpoints.js +5 -0
  18. package/dist/src/ethos.d.ts +202 -1
  19. package/dist/src/ethos.js +821 -16
  20. package/dist/src/index.d.ts +18 -6
  21. package/dist/src/index.js +39 -6
  22. package/dist/src/internal/delegate-bundle.d.ts +18 -0
  23. package/dist/src/internal/delegate-bundle.js +94 -0
  24. package/dist/src/internal/delegate-state.d.ts +45 -0
  25. package/dist/src/internal/delegate-state.js +120 -0
  26. package/dist/src/internal/envelope.d.ts +77 -0
  27. package/dist/src/internal/envelope.js +154 -0
  28. package/dist/src/internal/owner-signers.d.ts +78 -0
  29. package/dist/src/internal/owner-signers.js +179 -0
  30. package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
  31. package/dist/src/internal/protocol-client-bridge.js +20 -0
  32. package/dist/src/internal/recovery-file.d.ts +29 -0
  33. package/dist/src/internal/recovery-file.js +98 -0
  34. package/dist/src/internal/signer.d.ts +59 -0
  35. package/dist/src/internal/signer.js +86 -0
  36. package/dist/src/key-store.d.ts +128 -0
  37. package/dist/src/key-store.js +244 -0
  38. package/dist/src/mandates.d.ts +163 -1
  39. package/dist/src/mandates.js +286 -8
  40. package/dist/src/react/AithosAsset.d.ts +66 -0
  41. package/dist/src/react/AithosAsset.js +67 -0
  42. package/dist/src/react/context.d.ts +29 -0
  43. package/dist/src/react/context.js +31 -0
  44. package/dist/src/react/index.d.ts +28 -0
  45. package/dist/src/react/index.js +30 -0
  46. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  47. package/dist/src/react/use-aithos-asset.js +118 -0
  48. package/dist/src/sdk.d.ts +46 -3
  49. package/dist/src/sdk.js +49 -23
  50. package/dist/src/wallet.d.ts +4 -6
  51. package/dist/src/wallet.js +18 -8
  52. package/dist/src/web.d.ts +279 -0
  53. package/dist/src/web.js +186 -0
  54. package/dist/test/auth-j3.test.d.ts +2 -0
  55. package/dist/test/auth-j3.test.js +391 -0
  56. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  57. package/dist/test/compute-delegate-path.test.js +183 -0
  58. package/dist/test/compute.test.js +26 -11
  59. package/dist/test/endpoints.test.js +20 -1
  60. package/dist/test/envelope.test.d.ts +2 -0
  61. package/dist/test/envelope.test.js +318 -0
  62. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  63. package/dist/test/ethos-first-edition.test.js +248 -0
  64. package/dist/test/ethos.test.d.ts +2 -0
  65. package/dist/test/ethos.test.js +219 -0
  66. package/dist/test/key-store.test.d.ts +2 -0
  67. package/dist/test/key-store.test.js +161 -0
  68. package/dist/test/mandates-compute.test.d.ts +2 -0
  69. package/dist/test/mandates-compute.test.js +256 -0
  70. package/dist/test/mandates.test.d.ts +2 -0
  71. package/dist/test/mandates.test.js +93 -0
  72. package/dist/test/sdk.test.js +70 -30
  73. package/dist/test/signer.test.d.ts +2 -0
  74. package/dist/test/signer.test.js +117 -0
  75. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  76. package/dist/test/signup-bootstrap.test.js +311 -0
  77. package/dist/test/wallet.test.js +20 -9
  78. package/dist/test/web.test.d.ts +2 -0
  79. package/dist/test/web.test.js +270 -0
  80. package/package.json +18 -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
@@ -9,12 +9,23 @@
9
9
  import { strict as assert } from "node:assert";
10
10
  import { describe, it } from "node:test";
11
11
  import { createBrowserIdentity } from "@aithos/protocol-client";
12
- import { AithosSDK, AithosSDKError } from "../src/index.js";
12
+ import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
13
+ import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
13
14
  const APP_DID = "did:aithos:app:test";
14
- function makeSdk(fetchImpl) {
15
- const identity = createBrowserIdentity("test-handle", "Test User");
15
+ async function makeSdk(fetchImpl) {
16
+ const id = createBrowserIdentity("test-handle", "Test User");
17
+ const auth = new AithosAuth({
18
+ authBaseUrl: "https://auth.test",
19
+ fetch: (() => {
20
+ throw new Error("auth not used in compute tests");
21
+ }),
22
+ sessionStore: noopStore(),
23
+ keyStore: memoryKeyStore(),
24
+ });
25
+ const { text } = serializeRecoveryFile(id);
26
+ await auth.signInWithRecovery({ file: text });
16
27
  return new AithosSDK({
17
- identity,
28
+ auth,
18
29
  appDid: APP_DID,
19
30
  endpoints: { compute: "https://compute.example.test" },
20
31
  fetch: fetchImpl,
@@ -40,7 +51,7 @@ describe("compute.invokeBedrock — happy path", () => {
40
51
  headers: { "content-type": "application/json" },
41
52
  });
42
53
  };
43
- const sdk = makeSdk(fakeFetch);
54
+ const sdk = await makeSdk(fakeFetch);
44
55
  const out = await sdk.compute.invokeBedrock({
45
56
  mandateId: "mandate:abc",
46
57
  model: "claude-sonnet-4-6",
@@ -71,7 +82,7 @@ describe("compute.invokeBedrock — happy path", () => {
71
82
  headers: { "content-type": "application/json" },
72
83
  });
73
84
  };
74
- const sdk = makeSdk(fakeFetch);
85
+ const sdk = await makeSdk(fakeFetch);
75
86
  await sdk.compute.invokeBedrock({
76
87
  mandateId: "mandate:abc",
77
88
  model: "claude-sonnet-4-6",
@@ -92,7 +103,7 @@ describe("compute.invokeBedrock — errors", () => {
92
103
  const fakeFetch = async () => {
93
104
  throw new Error("fetch failed: ECONNREFUSED");
94
105
  };
95
- const sdk = makeSdk(fakeFetch);
106
+ const sdk = await makeSdk(fakeFetch);
96
107
  await assert.rejects(sdk.compute.invokeBedrock({
97
108
  mandateId: "mandate:abc",
98
109
  model: "claude-sonnet-4-6",
@@ -106,7 +117,7 @@ describe("compute.invokeBedrock — errors", () => {
106
117
  });
107
118
  it("wraps an HTTP non-2xx as AithosSDKError(code='http')", async () => {
108
119
  const fakeFetch = async () => new Response("nope", { status: 503, statusText: "Service Unavailable" });
109
- const sdk = makeSdk(fakeFetch);
120
+ const sdk = await makeSdk(fakeFetch);
110
121
  await assert.rejects(sdk.compute.invokeBedrock({
111
122
  mandateId: "mandate:abc",
112
123
  model: "claude-sonnet-4-6",
@@ -126,7 +137,7 @@ describe("compute.invokeBedrock — errors", () => {
126
137
  data: { balance: 0, required: 1 },
127
138
  },
128
139
  }), { status: 200, headers: { "content-type": "application/json" } });
129
- const sdk = makeSdk(fakeFetch);
140
+ const sdk = await makeSdk(fakeFetch);
130
141
  await assert.rejects(sdk.compute.invokeBedrock({
131
142
  mandateId: "mandate:abc",
132
143
  model: "claude-sonnet-4-6",
@@ -143,7 +154,7 @@ describe("compute.invokeBedrock — errors", () => {
143
154
  status: 200,
144
155
  headers: { "content-type": "application/json" },
145
156
  });
146
- const sdk = makeSdk(fakeFetch);
157
+ const sdk = await makeSdk(fakeFetch);
147
158
  await assert.rejects(sdk.compute.invokeBedrock({
148
159
  mandateId: "mandate:abc",
149
160
  model: "claude-sonnet-4-6",
@@ -165,7 +176,7 @@ describe("compute.invokeBedrock — abort", () => {
165
176
  headers: { "content-type": "application/json" },
166
177
  });
167
178
  };
168
- const sdk = makeSdk(fakeFetch);
179
+ const sdk = await makeSdk(fakeFetch);
169
180
  const ac = new AbortController();
170
181
  await sdk.compute.invokeBedrock({
171
182
  mandateId: "mandate:abc",
@@ -176,4 +187,8 @@ describe("compute.invokeBedrock — abort", () => {
176
187
  assert.equal(receivedSignal, ac.signal);
177
188
  });
178
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.
179
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,14 @@ 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",
26
27
  }), "https://compute.aithos.be/v1/invoke");
27
28
  });
28
29
  it("trims a trailing slash on the compute base", () => {
29
30
  assert.equal(computeInvokeUrl({
30
31
  compute: "https://compute.aithos.be/",
31
32
  wallet: "https://wallet.aithos.be",
33
+ web: "https://extract.aithos.be",
32
34
  }), "https://compute.aithos.be/v1/invoke");
33
35
  });
34
36
  });
@@ -37,7 +39,24 @@ describe("walletTopupCheckoutUrl", () => {
37
39
  assert.equal(walletTopupCheckoutUrl({
38
40
  compute: "https://compute.aithos.be",
39
41
  wallet: "https://wallet.aithos.be",
42
+ web: "https://extract.aithos.be",
40
43
  }), "https://wallet.aithos.be/v1/wallet/topup/checkout");
41
44
  });
42
45
  });
46
+ describe("webInvokeUrl", () => {
47
+ it("appends /v1/invoke to the web base", () => {
48
+ assert.equal(webInvokeUrl({
49
+ compute: "https://compute.aithos.be",
50
+ wallet: "https://wallet.aithos.be",
51
+ web: "https://extract.aithos.be",
52
+ }), "https://extract.aithos.be/v1/invoke");
53
+ });
54
+ it("trims a trailing slash on the web base", () => {
55
+ assert.equal(webInvokeUrl({
56
+ compute: "https://compute.aithos.be",
57
+ wallet: "https://wallet.aithos.be",
58
+ web: "https://extract.aithos.be/",
59
+ }), "https://extract.aithos.be/v1/invoke");
60
+ });
61
+ });
43
62
  //# sourceMappingURL=endpoints.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=envelope.test.d.ts.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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ethos-first-edition.test.d.ts.map