@aithos/sdk 0.1.0-alpha.47 → 0.1.0-alpha.48

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/apps.js CHANGED
@@ -26,6 +26,7 @@
26
26
  // in V0.2 once the sponsorship-api Lambda exists; for now, devs check
27
27
  // their sponsorship via the AWS Console.
28
28
  import { base64url, buildSignedEnvelope, sign } from "@aithos/protocol-client";
29
+ import { canonicalize } from "@aithos/protocol-core/canonical";
29
30
  import { computeInvokeUrl, walletTopupCheckoutUrl } from "./endpoints.js";
30
31
  import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
31
32
  import { AithosSDKError } from "./types.js";
@@ -426,18 +427,6 @@ function randomBytes(n) {
426
427
  * the hash the signature commits to.
427
428
  */
428
429
  function jcsCanonicalize(value) {
429
- if (value === null || typeof value !== "object") {
430
- return JSON.stringify(value);
431
- }
432
- if (Array.isArray(value)) {
433
- return "[" + value.map(jcsCanonicalize).join(",") + "]";
434
- }
435
- const obj = value;
436
- const keys = Object.keys(obj).sort();
437
- return ("{" +
438
- keys
439
- .map((k) => JSON.stringify(k) + ":" + jcsCanonicalize(obj[k]))
440
- .join(",") +
441
- "}");
430
+ return canonicalize(value);
442
431
  }
443
432
  //# sourceMappingURL=apps.js.map
package/dist/src/data.js CHANGED
@@ -39,6 +39,7 @@ import { sha256, sha512 } from "@noble/hashes/sha2.js";
39
39
  import { XChaCha20Poly1305 } from "@stablelib/xchacha20poly1305";
40
40
  import * as ed from "@noble/ed25519";
41
41
  import { multibaseToEd25519PublicKey, edPubToX25519Pub, } from "@aithos/protocol-client";
42
+ import { canonicalize } from "@aithos/protocol-core/canonical";
42
43
  import { contactsV1 } from "./data-schema-contacts-v1.js";
43
44
  import { signOwnerEnvelope } from "./internal/envelope.js";
44
45
  // noble/ed25519 v2 needs sha512 wired in for sync sign/verify
@@ -989,30 +990,12 @@ function sha256Hex(s) {
989
990
  /* -------------------------------------------------------------------------- */
990
991
  /* JCS-style canonicalization (RFC 8785 subset) */
991
992
  /* -------------------------------------------------------------------------- */
993
+ // Single canonicalization source of truth: @aithos/protocol-core. Kept as a
994
+ // local alias so the encryption-AAD call sites read naturally; the byte output
995
+ // is proven identical to the former hand-rolled JCS by
996
+ // test/canonical-conformance.test.ts (critical: this feeds pre-encryption
997
+ // canonicalization, so any drift would corrupt data).
992
998
  function jcsCanonicalize(value) {
993
- if (value === null)
994
- return "null";
995
- if (value === undefined)
996
- throw new Error("Cannot canonicalize undefined");
997
- if (typeof value === "boolean")
998
- return value ? "true" : "false";
999
- if (typeof value === "number") {
1000
- if (!Number.isFinite(value))
1001
- throw new Error("non-finite number");
1002
- return value.toString();
1003
- }
1004
- if (typeof value === "string")
1005
- return JSON.stringify(value);
1006
- if (Array.isArray(value)) {
1007
- return "[" + value.map(jcsCanonicalize).join(",") + "]";
1008
- }
1009
- if (typeof value === "object") {
1010
- const obj = value;
1011
- const keys = Object.keys(obj).sort();
1012
- return ("{" +
1013
- keys.map((k) => JSON.stringify(k) + ":" + jcsCanonicalize(obj[k])).join(",") +
1014
- "}");
1015
- }
1016
- throw new Error(`Cannot canonicalize ${typeof value}`);
999
+ return canonicalize(value);
1017
1000
  }
1018
1001
  //# sourceMappingURL=data.js.map
@@ -19,7 +19,7 @@
19
19
  * with `src/data.ts` is intentional and tracked; consolidation into a
20
20
  * shared `src/internal/canonical.ts` is planned for a follow-up release.
21
21
  */
22
- import { sha256 } from "@noble/hashes/sha2.js";
22
+ import { signEnvelopeWith } from "@aithos/protocol-core/envelope";
23
23
  /**
24
24
  * Build, canonicalize and sign an owner-path envelope per spec §11.2.
25
25
  *
@@ -33,128 +33,27 @@ import { sha256 } from "@noble/hashes/sha2.js";
33
33
  * (non-extractable keys) drop in without breaking callers.
34
34
  */
35
35
  export async function signOwnerEnvelope(args) {
36
- const nowMs = (args.now ?? new Date()).getTime();
37
- const iat = Math.floor(nowMs / 1000);
38
- const exp = iat + (args.ttlSeconds ?? 60);
39
- const nonce = args.nonce ?? makeUlid();
40
- const paramsHash = "sha256-" + sha256Hex(jcsCanonicalize(args.params));
41
- const unsigned = {
42
- "aithos-envelope": "0.1.0",
36
+ // Delegate to the single envelope source of truth in @aithos/protocol-core.
37
+ // The canonicalization, params_hash, unsigned-envelope assembly and proof
38
+ // attachment all live there now; we only supply the pluggable async signer
39
+ // (preserving the WebCrypto-ready `EnvelopeSigner` abstraction). A
40
+ // conformance test proves this path is byte-identical to the seed-based
41
+ // core signer, which is itself byte-identical to the former hand-rolled
42
+ // implementation — so nothing changes on the wire.
43
+ const env = await signEnvelopeWith({
43
44
  iss: args.iss,
44
45
  aud: args.aud,
45
46
  method: args.method,
46
- iat,
47
- exp,
48
- nonce,
49
- params_hash: paramsHash,
50
- // Attach the mandate (delegate path) so the signature commits to the
51
- // delegation context. JCS sorts keys, so placement here is irrelevant
52
- // to the canonical bytes — what matters is that the server (which
53
- // canonicalizes the full envelope incl. mandate + proof/proofValue="")
54
- // sees the exact same object.
55
- ...(args.mandate !== undefined ? { mandate: args.mandate } : {}),
56
- proof: {
57
- type: "Ed25519Signature2020",
58
- verificationMethod: args.verificationMethod,
59
- created: new Date(iat * 1000).toISOString(),
60
- proofValue: "",
61
- },
62
- };
63
- const bytes = new TextEncoder().encode(jcsCanonicalize(unsigned));
64
- const sig = await args.signer.sign(bytes);
65
- return {
66
- ...unsigned,
67
- proof: { ...unsigned.proof, proofValue: base64url(sig) },
68
- };
69
- }
70
- /* -------------------------------------------------------------------------- */
71
- /* Self-contained helpers (duplicated with src/data.ts for isolation) */
72
- /* -------------------------------------------------------------------------- */
73
- /** Cross-platform CSPRNG: Web Crypto in browser, Node WebCrypto in Node 19+. */
74
- function cryptoRandom(n) {
75
- const buf = new Uint8Array(n);
76
- globalThis.crypto?.getRandomValues(buf);
77
- return buf;
78
- }
79
- function makeUlid() {
80
- // Lightweight ULID — millisecond timestamp + 80 bits of randomness.
81
- // Crockford base32. For tests this is sufficient; production uses
82
- // the canonical ulid package.
83
- const tsBuf = new Uint8Array(6);
84
- let ts = Date.now();
85
- for (let i = 5; i >= 0; i--) {
86
- tsBuf[i] = ts & 0xff;
87
- ts = Math.floor(ts / 256);
88
- }
89
- const rndBuf = cryptoRandom(10);
90
- const all = new Uint8Array(16);
91
- all.set(tsBuf, 0);
92
- all.set(rndBuf, 6);
93
- const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
94
- let bits = 0;
95
- let value = 0;
96
- let out = "";
97
- for (const b of all) {
98
- value = (value << 8) | b;
99
- bits += 8;
100
- while (bits >= 5) {
101
- out += alphabet[(value >> (bits - 5)) & 0x1f];
102
- bits -= 5;
103
- }
104
- }
105
- if (bits > 0)
106
- out += alphabet[(value << (5 - bits)) & 0x1f];
107
- return out.slice(0, 26);
108
- }
109
- /** Standard base64 (with `=` padding). Browser + Node compatible. */
110
- function base64Std(bytes) {
111
- let bin = "";
112
- for (let i = 0; i < bytes.length; i++)
113
- bin += String.fromCharCode(bytes[i]);
114
- return btoa(bin);
115
- }
116
- /** base64url (URL-safe, no padding). */
117
- function base64url(bytes) {
118
- return base64Std(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
119
- }
120
- function sha256Hex(s) {
121
- const d = sha256(new TextEncoder().encode(s));
122
- let hex = "";
123
- for (const b of d)
124
- hex += b.toString(16).padStart(2, "0");
125
- return hex;
126
- }
127
- /**
128
- * JCS-style canonicalization (RFC 8785 subset).
129
- *
130
- * Sorted object keys, no whitespace, finite numbers via `toString()`,
131
- * strings via `JSON.stringify` (escapes), arrays preserve order.
132
- * `undefined` is rejected (RFC 8785 §3.2.2).
133
- */
134
- function jcsCanonicalize(value) {
135
- if (value === null)
136
- return "null";
137
- if (value === undefined)
138
- throw new Error("Cannot canonicalize undefined");
139
- if (typeof value === "boolean")
140
- return value ? "true" : "false";
141
- if (typeof value === "number") {
142
- if (!Number.isFinite(value))
143
- throw new Error("non-finite number");
144
- return value.toString();
145
- }
146
- if (typeof value === "string")
147
- return JSON.stringify(value);
148
- if (Array.isArray(value)) {
149
- return "[" + value.map(jcsCanonicalize).join(",") + "]";
150
- }
151
- if (typeof value === "object") {
152
- const obj = value;
153
- const keys = Object.keys(obj).sort();
154
- return ("{" +
155
- keys.map((k) => JSON.stringify(k) + ":" + jcsCanonicalize(obj[k])).join(",") +
156
- "}");
157
- }
158
- throw new Error(`Cannot canonicalize ${typeof value}`);
47
+ params: args.params,
48
+ verificationMethod: args.verificationMethod,
49
+ sign: (bytes) => args.signer.sign(bytes),
50
+ ttlSeconds: args.ttlSeconds,
51
+ now: args.now,
52
+ nonce: args.nonce,
53
+ ...(args.mandate !== undefined
54
+ ? { mandate: args.mandate }
55
+ : {}),
56
+ });
57
+ return env;
159
58
  }
160
59
  //# sourceMappingURL=envelope.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=canonical-conformance.test.d.ts.map
@@ -0,0 +1,86 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ /**
4
+ * Safety gate for the canonicalization refactor.
5
+ *
6
+ * The SDK historically carried three hand-rolled copies of `jcsCanonicalize`
7
+ * (internal/envelope.ts, data.ts, apps.ts). data.ts uses it to canonicalize a
8
+ * record payload BEFORE encryption (it feeds the AAD/plaintext), so a single
9
+ * differing byte vs. the canonicalization used at decrypt/verify time would
10
+ * silently corrupt data. Before swapping those copies onto
11
+ * `@aithos/protocol-core`'s `canonicalize`, this test proves the two produce
12
+ * identical output across a representative corpus.
13
+ *
14
+ * Known, accepted divergence: lone UTF-16 surrogates. `JSON.stringify` escapes
15
+ * them (\uXXXX) whereas core emits the raw code unit. Lone surrogates never
16
+ * appear in Aithos payloads (record fields are well-formed JSON strings), so
17
+ * the corpus excludes them by design.
18
+ */
19
+ import { describe, test } from "node:test";
20
+ import { strict as assert } from "node:assert";
21
+ import { canonicalize } from "@aithos/protocol-core/canonical";
22
+ /** Exact copy of the SDK's legacy jcsCanonicalize (pre-refactor reference). */
23
+ function jcsCanonicalize(value) {
24
+ if (value === null)
25
+ return "null";
26
+ if (value === undefined)
27
+ throw new Error("Cannot canonicalize undefined");
28
+ if (typeof value === "boolean")
29
+ return value ? "true" : "false";
30
+ if (typeof value === "number") {
31
+ if (!Number.isFinite(value))
32
+ throw new Error("non-finite number");
33
+ return value.toString();
34
+ }
35
+ if (typeof value === "string")
36
+ return JSON.stringify(value);
37
+ if (Array.isArray(value)) {
38
+ return "[" + value.map(jcsCanonicalize).join(",") + "]";
39
+ }
40
+ if (typeof value === "object") {
41
+ const obj = value;
42
+ const keys = Object.keys(obj).sort();
43
+ return ("{" +
44
+ keys
45
+ .map((k) => JSON.stringify(k) + ":" + jcsCanonicalize(obj[k]))
46
+ .join(",") +
47
+ "}");
48
+ }
49
+ throw new Error(`Cannot canonicalize ${typeof value}`);
50
+ }
51
+ const corpus = [
52
+ null,
53
+ true,
54
+ false,
55
+ 0,
56
+ -1,
57
+ 42,
58
+ Number.MAX_SAFE_INTEGER,
59
+ "",
60
+ "hello",
61
+ "with \"quotes\" and \\ backslash",
62
+ "tab\tnewline\nreturn\rbackspace\bform\f",
63
+ "control end",
64
+ "accented éàùçö and emoji 😀🚀 and 漢字",
65
+ "slash / and at @ and unicode nbsp",
66
+ [],
67
+ [1, 2, 3],
68
+ ["z", "a", "m"],
69
+ [{ b: 1, a: 2 }, [3, [4, 5]]],
70
+ {},
71
+ { b: 2, a: 1, c: 3 },
72
+ { z: { y: { x: [1, "two", false, null] } } },
73
+ { "key with spaces": 1, "weird:char": 2, "": "empty key" },
74
+ { émoji: "😀", "漢字": 1, A: 0, a: 0 }, // mixed-case + non-ascii keys (UTF-16 order)
75
+ {
76
+ record: { name: "Aïko", tags: ["x", "y"], n: 7, active: true, meta: null },
77
+ },
78
+ ];
79
+ describe("canonicalize conformance — core vs legacy SDK jcsCanonicalize", () => {
80
+ for (const [i, value] of corpus.entries()) {
81
+ test(`corpus[${i}] is byte-identical`, () => {
82
+ assert.equal(canonicalize(value), jcsCanonicalize(value));
83
+ });
84
+ }
85
+ });
86
+ //# sourceMappingURL=canonical-conformance.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=envelope-core-conformance.test.d.ts.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.47",
3
+ "version": "0.1.0-alpha.48",
4
4
  "description": "Aithos SDK — high-level TypeScript developer kit for building agentic apps on the Aithos protocol. Wraps @aithos/protocol-client and exposes the Aithos compute proxy and wallet (Stripe top-up) endpoints.",
5
5
  "keywords": [
6
6
  "aithos",
@@ -58,6 +58,7 @@
58
58
  "peerDependencies": {
59
59
  "@aithos/assets-crypto": ">=0.1.0-alpha.1 <0.2.0",
60
60
  "@aithos/protocol-client": "^0.1.0-alpha.14",
61
+ "@aithos/protocol-core": ">=0.6.3 <0.7.0",
61
62
  "react": "^18.0.0 || ^19.0.0"
62
63
  },
63
64
  "peerDependenciesMeta": {
@@ -71,6 +72,7 @@
71
72
  "devDependencies": {
72
73
  "@aithos/assets-crypto": "^0.1.0-alpha.1",
73
74
  "@aithos/protocol-client": "^0.1.0-alpha.14",
75
+ "@aithos/protocol-core": ">=0.6.3 <0.7.0",
74
76
  "@types/node": "^24.12.2",
75
77
  "fake-indexeddb": "^6.2.5",
76
78
  "typescript": "^5.9.2"