@aithos/sdk 0.1.0-alpha.34 → 0.1.0-alpha.36

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 CHANGED
@@ -213,16 +213,61 @@ await sdk.mandates.create({
213
213
  });
214
214
  ```
215
215
 
216
+ ## Calling a third-party Aithos-aware backend
217
+
218
+ If your app talks to its own backend (a service you built that verifies
219
+ Aithos envelopes per spec §11.2 using
220
+ `@aithos/protocol-core/envelope`), use `sdk.auth.signEnvelope` to sign
221
+ the request with the same primitive that SDK namespaces use internally
222
+ for `api.aithos.be`. No JWT, no shadow session — the user's DID in the
223
+ envelope's `iss` field is the identity.
224
+
225
+ ```ts
226
+ import { AithosSDK, type SignedEnvelope } from "@aithos/sdk";
227
+
228
+ // Sign a request to your own backend with the active owner's
229
+ // public-sphere key. Default TTL is 60 s.
230
+ const envelope: SignedEnvelope = await sdk.auth.signEnvelope({
231
+ aud: "https://api.example.com/v1/widgets",
232
+ method: "myapp.widgets.create",
233
+ params: { name: "Widget #1" },
234
+ });
235
+
236
+ await fetch("https://api.example.com/v1/widgets", {
237
+ method: "POST",
238
+ headers: { "content-type": "application/json" },
239
+ body: JSON.stringify({
240
+ jsonrpc: "2.0",
241
+ id: crypto.randomUUID(),
242
+ method: "myapp.widgets.create",
243
+ params: { name: "Widget #1", _envelope: envelope },
244
+ }),
245
+ });
246
+ ```
247
+
248
+ The envelope binds the signature to `(iss, aud, method, params_hash,
249
+ nonce, iat, exp)`, so a single envelope cannot be replayed against a
250
+ different endpoint, method, or payload. Throws
251
+ `AithosSDKError("auth_not_signed_in")` if no owner is loaded; throws
252
+ `AithosSDKError("auth_invalid_sphere")` if you pass a sphere outside
253
+ `"root" | "public" | "circle" | "self"` (default is `"public"`).
254
+
255
+ Server-side, your backend verifies the envelope with
256
+ `@aithos/protocol-core`'s `verifyEnvelope` (the 9-step check from spec
257
+ §11.4) — same algorithm that `api.aithos.be` uses, no re-implementation
258
+ needed.
259
+
216
260
  ## What lives where
217
261
 
218
- | Namespace | Purpose |
219
- | ---------------- | ------------------------------------------------------------------------------------------ |
220
- | `sdk.compute` | Bedrock invocation through the Aithos compute proxy (signed envelope, wallet enforcement). |
221
- | `sdk.web` | Webpage extraction without an LLM through the web extractor proxy (1 mc / call). |
222
- | `sdk.wallet` | Stripe Checkout sessions for credit-pack top-ups, balance helpers. |
223
- | `sdk.ethos` | Ethos-zone composition / parsing re-exported from `@aithos/protocol-client`. |
224
- | `sdk.onboarding` | First-run identity / DID flows — re-exported. |
225
- | `sdk.mandates` | Mint / verify mandates — re-exported. |
262
+ | Namespace | Purpose |
263
+ | -------------------------- | ------------------------------------------------------------------------------------------ |
264
+ | `sdk.auth` | Sign-in, sign-up, key custody and `signEnvelope` for calls to your own Aithos-aware backend. |
265
+ | `sdk.compute` | Bedrock invocation through the Aithos compute proxy (signed envelope, wallet enforcement). |
266
+ | `sdk.web` | Webpage extraction without an LLM through the web extractor proxy (1 mc / call). |
267
+ | `sdk.wallet` | Stripe Checkout sessions for credit-pack top-ups, balance helpers. |
268
+ | `sdk.ethos` | Ethos-zone composition / parsing — re-exported from `@aithos/protocol-client`. |
269
+ | `sdk.onboarding` | First-run identity / DID flows — re-exported. |
270
+ | `sdk.mandates` | Mint / verify mandates — re-exported. |
226
271
 
227
272
  ## License
228
273
 
@@ -1,6 +1,7 @@
1
1
  import { type AithosSessionStore } from "./session-store.js";
2
2
  import { type AithosKeyStore } from "./key-store.js";
3
3
  import { DelegateActor } from "./internal/delegate-state.js";
4
+ import { type SignedEnvelope } from "./internal/envelope.js";
4
5
  import { OwnerSigners } from "./internal/owner-signers.js";
5
6
  /** Default URL of the Aithos auth backend. */
6
7
  export declare const DEFAULT_AUTH_BASE_URL = "https://auth.aithos.be";
@@ -304,6 +305,70 @@ export declare class AithosAuth {
304
305
  getOwnerInfo(): OwnerInfo | null;
305
306
  getDelegates(): readonly DelegateInfo[];
306
307
  canSignAsOwner(): boolean;
308
+ /**
309
+ * Sign an envelope (spec §11.2) as the active owner, to authenticate
310
+ * a call to a third-party Aithos-aware backend.
311
+ *
312
+ * Same primitive that SDK namespaces (`sdk.data`, `sdk.ethos`,
313
+ * `sdk.mandates`, ...) use internally to sign their own writes to
314
+ * `api.aithos.be`. Exposed here so apps can sign envelopes for their
315
+ * own backends — any service that verifies a `SignedEnvelope` per
316
+ * spec §11.2 (typically using `@aithos/protocol-core/envelope`'s
317
+ * `verifyEnvelope`) accepts the resulting object.
318
+ *
319
+ * The envelope binds the signature to `(iss, aud, method,
320
+ * params_hash, nonce, iat, exp)`, so it cannot be replayed against a
321
+ * different endpoint, method, or payload, and expires after
322
+ * `ttlSeconds` (default 60s, server-side typically caps at 300s).
323
+ *
324
+ * Usage:
325
+ *
326
+ * ```ts
327
+ * const envelope = await sdk.auth.signEnvelope({
328
+ * aud: "https://api.example.com/v1/widgets",
329
+ * method: "myapp.widgets.create",
330
+ * params: { name: "Widget #1" },
331
+ * });
332
+ * await fetch("https://api.example.com/v1/widgets", {
333
+ * method: "POST",
334
+ * headers: { "content-type": "application/json" },
335
+ * body: JSON.stringify({ ...payload, _envelope: envelope }),
336
+ * });
337
+ * ```
338
+ *
339
+ * @throws {AithosSDKError} `auth_not_signed_in` if no owner identity
340
+ * is loaded (call `signIn` / `signUp` / `signInCustodial` first).
341
+ * @throws {AithosSDKError} `auth_invalid_sphere` if `sphere` is not
342
+ * one of `"root" | "public" | "circle" | "self"`.
343
+ */
344
+ signEnvelope(args: {
345
+ /**
346
+ * Absolute URL of the target endpoint (scheme + host + path, no
347
+ * query, no fragment). The receiving server rejects the envelope if
348
+ * `aud` does not match the actual request URL.
349
+ */
350
+ readonly aud: string;
351
+ /** Fully-qualified JSON-RPC method name. */
352
+ readonly method: string;
353
+ /**
354
+ * Tool payload — what `params_hash` commits to. Will be
355
+ * JCS-canonicalized (RFC 8785 subset) before hashing, so JS object
356
+ * key order does not affect the result.
357
+ */
358
+ readonly params: unknown;
359
+ /**
360
+ * Which of the owner's four sphere keys signs. Default: `"public"`,
361
+ * which matches what SDK namespaces use for everyday writes.
362
+ * Choose `"root"`, `"circle"`, or `"self"` only if the receiving
363
+ * server specifically expects one of those (rare).
364
+ */
365
+ readonly sphere?: "root" | "public" | "circle" | "self";
366
+ /**
367
+ * Envelope lifetime in seconds. Default 60. Aithos servers cap
368
+ * at 300; third-party servers may apply their own cap.
369
+ */
370
+ readonly ttlSeconds?: number;
371
+ }): Promise<SignedEnvelope>;
307
372
  canSignAsDelegateFor(did: string): boolean;
308
373
  /**
309
374
  * Internal accessor used by sibling SDK namespaces (compute, wallet,
package/dist/src/auth.js CHANGED
@@ -26,6 +26,7 @@ import { defaultSessionStore, } from "./session-store.js";
26
26
  import { defaultKeyStore, } from "./key-store.js";
27
27
  import { parseDelegateBundle, readDelegateBundleText, } from "./internal/delegate-bundle.js";
28
28
  import { DelegateActor, DelegateRegistry, } from "./internal/delegate-state.js";
29
+ import { signOwnerEnvelope, } from "./internal/envelope.js";
29
30
  import { OwnerSigners } from "./internal/owner-signers.js";
30
31
  import { parseRecoveryFile, readRecoveryFileText, serializeRecoveryFile, } from "./internal/recovery-file.js";
31
32
  import { AithosSDKError } from "./types.js";
@@ -147,6 +148,64 @@ export class AithosAuth {
147
148
  canSignAsOwner() {
148
149
  return this.#ownerSigners !== null && !this.#ownerSigners.destroyed;
149
150
  }
151
+ /**
152
+ * Sign an envelope (spec §11.2) as the active owner, to authenticate
153
+ * a call to a third-party Aithos-aware backend.
154
+ *
155
+ * Same primitive that SDK namespaces (`sdk.data`, `sdk.ethos`,
156
+ * `sdk.mandates`, ...) use internally to sign their own writes to
157
+ * `api.aithos.be`. Exposed here so apps can sign envelopes for their
158
+ * own backends — any service that verifies a `SignedEnvelope` per
159
+ * spec §11.2 (typically using `@aithos/protocol-core/envelope`'s
160
+ * `verifyEnvelope`) accepts the resulting object.
161
+ *
162
+ * The envelope binds the signature to `(iss, aud, method,
163
+ * params_hash, nonce, iat, exp)`, so it cannot be replayed against a
164
+ * different endpoint, method, or payload, and expires after
165
+ * `ttlSeconds` (default 60s, server-side typically caps at 300s).
166
+ *
167
+ * Usage:
168
+ *
169
+ * ```ts
170
+ * const envelope = await sdk.auth.signEnvelope({
171
+ * aud: "https://api.example.com/v1/widgets",
172
+ * method: "myapp.widgets.create",
173
+ * params: { name: "Widget #1" },
174
+ * });
175
+ * await fetch("https://api.example.com/v1/widgets", {
176
+ * method: "POST",
177
+ * headers: { "content-type": "application/json" },
178
+ * body: JSON.stringify({ ...payload, _envelope: envelope }),
179
+ * });
180
+ * ```
181
+ *
182
+ * @throws {AithosSDKError} `auth_not_signed_in` if no owner identity
183
+ * is loaded (call `signIn` / `signUp` / `signInCustodial` first).
184
+ * @throws {AithosSDKError} `auth_invalid_sphere` if `sphere` is not
185
+ * one of `"root" | "public" | "circle" | "self"`.
186
+ */
187
+ async signEnvelope(args) {
188
+ if (!this.#ownerSigners || this.#ownerSigners.destroyed) {
189
+ throw new AithosSDKError("auth_not_signed_in", "signEnvelope: no owner is signed in. Call signIn / signUp / signInCustodial first.");
190
+ }
191
+ const sphere = args.sphere ?? "public";
192
+ if (sphere !== "root" &&
193
+ sphere !== "public" &&
194
+ sphere !== "circle" &&
195
+ sphere !== "self") {
196
+ throw new AithosSDKError("auth_invalid_sphere", `signEnvelope: invalid sphere "${sphere}". Expected one of: root, public, circle, self.`);
197
+ }
198
+ const signer = this.#ownerSigners.signerForSphere(sphere);
199
+ return signOwnerEnvelope({
200
+ iss: this.#ownerSigners.did,
201
+ aud: args.aud,
202
+ method: args.method,
203
+ params: args.params,
204
+ verificationMethod: `${this.#ownerSigners.did}#${sphere}`,
205
+ signer,
206
+ ttlSeconds: args.ttlSeconds,
207
+ });
208
+ }
150
209
  canSignAsDelegateFor(did) {
151
210
  const a = this.#delegates.findForSubject(did);
152
211
  return a !== undefined && !a.destroyed;
@@ -1235,6 +1294,24 @@ export class AithosAuth {
1235
1294
  }
1236
1295
  const json = (await res.json());
1237
1296
  if (json.error) {
1297
+ // Backward-compat shim for backends without the semantic-equality
1298
+ // fix on publish-identity (alpha.33+ regression): the server may
1299
+ // reject a republish with -32022 because the client regenerates
1300
+ // `aithos.created_at` (and `proof.created`) on every
1301
+ // `signedDidDocument()` call, breaking the strict byte-equal
1302
+ // idempotence the server enforces. For an honest signer (same
1303
+ // root key, same DID) the only way to hit this code path is the
1304
+ // timestamp-drift case — which is semantically a no-op. Treat as
1305
+ // success.
1306
+ //
1307
+ // Server-side fix (publish-identity.ts switched to semantic
1308
+ // equality on cryptographic fields only) makes this branch dead
1309
+ // code on upgraded backends. Kept here as defense-in-depth for
1310
+ // SDK consumers pointing at older deployments.
1311
+ if (json.error.code === -32022 &&
1312
+ /different did\.json already published/i.test(json.error.message)) {
1313
+ return; // already published with same crypto material — no-op
1314
+ }
1238
1315
  // JSON-RPC error: don't retry — these are deterministic
1239
1316
  // (validation, permission, identity-already-tombstoned, …).
1240
1317
  throw new AithosSDKError("ethos_bootstrap_failed", `publish_identity rejected: ${json.error.message}`, {
package/dist/src/data.js CHANGED
@@ -32,6 +32,7 @@ import { sha256, sha512 } from "@noble/hashes/sha2.js";
32
32
  import { XChaCha20Poly1305 } from "@stablelib/xchacha20poly1305";
33
33
  import * as ed from "@noble/ed25519";
34
34
  import { contactsV1 } from "./data-schema-contacts-v1.js";
35
+ import { signOwnerEnvelope } from "./internal/envelope.js";
35
36
  // noble/ed25519 v2 needs sha512 wired in for sync sign/verify
36
37
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
38
  ed.etc.sha512Sync = (...m) =>
@@ -256,7 +257,14 @@ class DataClientImpl {
256
257
  /* -- JSON-RPC dispatch -- */
257
258
  async #call(path, method, params) {
258
259
  const aud = `${this.#pdsUrl}${path}`;
259
- const envelope = this.#signEnvelope({ aud, method, params });
260
+ const envelope = await signOwnerEnvelope({
261
+ iss: this.#did,
262
+ aud,
263
+ method,
264
+ params,
265
+ verificationMethod: this.#vm,
266
+ signer: { sign: async (msg) => ed.sign(msg, this.#seed) },
267
+ });
260
268
  const body = {
261
269
  jsonrpc: "2.0",
262
270
  id: makeUlid(),
@@ -277,35 +285,6 @@ class DataClientImpl {
277
285
  }
278
286
  return json.result ?? {};
279
287
  }
280
- /* -- Envelope signing (inlined subset of @aithos/protocol-core/envelope) -- */
281
- #signEnvelope(args) {
282
- const now = Math.floor(Date.now() / 1000);
283
- const exp = now + 60;
284
- const nonce = makeUlid();
285
- const paramsHash = "sha256-" + sha256Hex(jcsCanonicalize(args.params));
286
- const unsigned = {
287
- "aithos-envelope": "0.1.0",
288
- iss: this.#did,
289
- aud: args.aud,
290
- method: args.method,
291
- iat: now,
292
- exp,
293
- nonce,
294
- params_hash: paramsHash,
295
- proof: {
296
- type: "Ed25519Signature2020",
297
- verificationMethod: this.#vm,
298
- created: new Date(now * 1000).toISOString(),
299
- proofValue: "",
300
- },
301
- };
302
- const bytes = new TextEncoder().encode(jcsCanonicalize(unsigned));
303
- const sig = ed.sign(bytes, this.#seed);
304
- return {
305
- ...unsigned,
306
- proof: { ...unsigned.proof, proofValue: base64url(sig) },
307
- };
308
- }
309
288
  /* -- Crypto helpers -- */
310
289
  #collectionUrn(name) {
311
290
  return `urn:aithos:collection:${this.#did}:${name}`;
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0-alpha.34";
1
+ export declare const VERSION = "0.1.0-alpha.35";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
@@ -13,6 +13,7 @@ export type { ComponentStyle, ExtractArgs, ExtractContent, ExtractData, ExtractF
13
13
  export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
14
14
  export { AithosAuth, DEFAULT_API_BASE_URL, DEFAULT_AUTH_BASE_URL, } from "./auth.js";
15
15
  export type { AithosAuthConfig, AithosSession, ApplyPasswordResetInput, ApplyPasswordResetResult, CompleteSsoFirstLoginInput, CompleteSsoFirstLoginResult, CustodialSignInInput, CustodialSignInResult, CustodialSignUpInput, CustodialSignUpResult, DelegateInfo, ImportMandateInput, OwnerInfo, RequestPasswordResetInput, ResendVerificationInput, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, VerifyEmailInput, VerifyEmailResult, } from "./auth.js";
16
+ export type { SignedEnvelope } from "./internal/envelope.js";
16
17
  export { DEFAULT_SESSION_STORAGE_KEY, defaultSessionStore, localStorageStore, noopStore, sessionStorageStore, type AithosSessionStore, } from "./session-store.js";
17
18
  export { DEFAULT_KEYSTORE_DB_NAME, defaultKeyStore, indexedDbKeyStore, memoryKeyStore, type AithosKeyStore, type StoredDelegateKeys, type StoredOwnerKeys, } from "./key-store.js";
18
19
  export { EthosClient, EthosNamespace, EthosZone, ZONE_NAMES, } from "./ethos.js";
package/dist/src/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
18
18
  // are exported from here. Endpoint config (`AithosSdkEndpoints`,
19
19
  // `DEFAULT_SDK_ENDPOINTS`) likewise.
20
- export const VERSION = "0.1.0-alpha.34";
20
+ export const VERSION = "0.1.0-alpha.35";
21
21
  export { AithosSDK } from "./sdk.js";
22
22
  export { AithosSDKError } from "./types.js";
23
23
  // Re-export protocol-client's JSON-RPC error type so consumers can
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Minimal signing surface required to produce an envelope's
3
+ * `proof.proofValue`. Intentionally narrower than {@link Signer} so
4
+ * callers that don't want to construct a full `RawSeedSigner` can pass
5
+ * a one-shot inline adapter. Every {@link Signer} satisfies this
6
+ * interface structurally.
7
+ */
8
+ export interface EnvelopeSigner {
9
+ sign(message: Uint8Array): Promise<Uint8Array>;
10
+ }
11
+ /**
12
+ * The wire-format envelope per spec §11.2. Apps that POST this to a
13
+ * backend serialize the whole object as JSON (alongside the rest of
14
+ * `params`) under `params._envelope`.
15
+ */
16
+ export interface SignedEnvelope {
17
+ readonly "aithos-envelope": "0.1.0";
18
+ readonly iss: string;
19
+ readonly aud: string;
20
+ readonly method: string;
21
+ readonly iat: number;
22
+ readonly exp: number;
23
+ readonly nonce: string;
24
+ readonly params_hash: string;
25
+ readonly proof: {
26
+ readonly type: "Ed25519Signature2020";
27
+ readonly verificationMethod: string;
28
+ readonly created: string;
29
+ readonly proofValue: string;
30
+ };
31
+ }
32
+ export interface SignOwnerEnvelopeArgs {
33
+ /** Subject DID — issuer of the envelope (`iss` field). */
34
+ readonly iss: string;
35
+ /**
36
+ * Absolute URL of the target endpoint (scheme + host + path, no query,
37
+ * no fragment). The server verifier rejects envelopes where `aud`
38
+ * does not match the actual endpoint being called.
39
+ */
40
+ readonly aud: string;
41
+ /** Fully-qualified JSON-RPC method name. */
42
+ readonly method: string;
43
+ /** Tool payload — what `params_hash` commits to. */
44
+ readonly params: unknown;
45
+ /**
46
+ * Anything that can produce an Ed25519 signature over a byte
47
+ * sequence. In practice: one of the four owner sphere signers
48
+ * loaded post sign-in, or an inline adapter wrapping a raw seed.
49
+ */
50
+ readonly signer: EnvelopeSigner;
51
+ /**
52
+ * Verification method URL — typically `${did}#${sphere}`. The server
53
+ * resolves this against the issuer's DID document to find the
54
+ * matching public key.
55
+ */
56
+ readonly verificationMethod: string;
57
+ /** Envelope lifetime in seconds. Default 60. Server caps at 300. */
58
+ readonly ttlSeconds?: number;
59
+ /** Clock override for deterministic tests. Defaults to `new Date()`. */
60
+ readonly now?: Date;
61
+ /** Nonce override for deterministic tests. Defaults to a fresh ULID. */
62
+ readonly nonce?: string;
63
+ }
64
+ /**
65
+ * Build, canonicalize and sign an owner-path envelope per spec §11.2.
66
+ *
67
+ * The unsigned envelope is JCS-canonicalized (RFC 8785 subset), the
68
+ * bytes signed with Ed25519, and the resulting signature attached as
69
+ * `proof.proofValue` (base64url).
70
+ *
71
+ * Async return type — even though `Signer.sign` may resolve
72
+ * synchronously today (via `RawSeedSigner`), the interface is shaped
73
+ * async so that future implementations backed by `crypto.subtle.sign`
74
+ * (non-extractable keys) drop in without breaking callers.
75
+ */
76
+ export declare function signOwnerEnvelope(args: SignOwnerEnvelopeArgs): Promise<SignedEnvelope>;
77
+ //# sourceMappingURL=envelope.d.ts.map
@@ -0,0 +1,154 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ /**
4
+ * Aithos signed-envelope helper — SDK-internal.
5
+ *
6
+ * Implements the `aithos-envelope` v0.1.0 wire format from spec §11.2.
7
+ * Used by SDK namespaces (sdk.data, sdk.ethos, sdk.mandates, ...) to
8
+ * sign their writes to api.aithos.be, AND by the public method
9
+ * {@link AithosAuth.signEnvelope} so apps can sign envelopes for
10
+ * arbitrary third-party Aithos-aware backends with the same primitive.
11
+ *
12
+ * NOT exported from the package barrel. The corresponding `SignedEnvelope`
13
+ * type IS re-exported from `src/index.ts` for consumer typing.
14
+ *
15
+ * Reference algorithm: `@aithos/protocol-core/envelope.ts:signEnvelope`.
16
+ * This file duplicates a self-contained subset of the helpers (JCS,
17
+ * SHA-256, base64url, ULID, CSPRNG) to keep the SDK's signing path
18
+ * decoupled from internal changes elsewhere in the SDK. The duplication
19
+ * with `src/data.ts` is intentional and tracked; consolidation into a
20
+ * shared `src/internal/canonical.ts` is planned for a follow-up release.
21
+ */
22
+ import { sha256 } from "@noble/hashes/sha2.js";
23
+ /**
24
+ * Build, canonicalize and sign an owner-path envelope per spec §11.2.
25
+ *
26
+ * The unsigned envelope is JCS-canonicalized (RFC 8785 subset), the
27
+ * bytes signed with Ed25519, and the resulting signature attached as
28
+ * `proof.proofValue` (base64url).
29
+ *
30
+ * Async return type — even though `Signer.sign` may resolve
31
+ * synchronously today (via `RawSeedSigner`), the interface is shaped
32
+ * async so that future implementations backed by `crypto.subtle.sign`
33
+ * (non-extractable keys) drop in without breaking callers.
34
+ */
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",
43
+ iss: args.iss,
44
+ aud: args.aud,
45
+ method: args.method,
46
+ iat,
47
+ exp,
48
+ nonce,
49
+ params_hash: paramsHash,
50
+ proof: {
51
+ type: "Ed25519Signature2020",
52
+ verificationMethod: args.verificationMethod,
53
+ created: new Date(iat * 1000).toISOString(),
54
+ proofValue: "",
55
+ },
56
+ };
57
+ const bytes = new TextEncoder().encode(jcsCanonicalize(unsigned));
58
+ const sig = await args.signer.sign(bytes);
59
+ return {
60
+ ...unsigned,
61
+ proof: { ...unsigned.proof, proofValue: base64url(sig) },
62
+ };
63
+ }
64
+ /* -------------------------------------------------------------------------- */
65
+ /* Self-contained helpers (duplicated with src/data.ts for isolation) */
66
+ /* -------------------------------------------------------------------------- */
67
+ /** Cross-platform CSPRNG: Web Crypto in browser, Node WebCrypto in Node 19+. */
68
+ function cryptoRandom(n) {
69
+ const buf = new Uint8Array(n);
70
+ globalThis.crypto?.getRandomValues(buf);
71
+ return buf;
72
+ }
73
+ function makeUlid() {
74
+ // Lightweight ULID — millisecond timestamp + 80 bits of randomness.
75
+ // Crockford base32. For tests this is sufficient; production uses
76
+ // the canonical ulid package.
77
+ const tsBuf = new Uint8Array(6);
78
+ let ts = Date.now();
79
+ for (let i = 5; i >= 0; i--) {
80
+ tsBuf[i] = ts & 0xff;
81
+ ts = Math.floor(ts / 256);
82
+ }
83
+ const rndBuf = cryptoRandom(10);
84
+ const all = new Uint8Array(16);
85
+ all.set(tsBuf, 0);
86
+ all.set(rndBuf, 6);
87
+ const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
88
+ let bits = 0;
89
+ let value = 0;
90
+ let out = "";
91
+ for (const b of all) {
92
+ value = (value << 8) | b;
93
+ bits += 8;
94
+ while (bits >= 5) {
95
+ out += alphabet[(value >> (bits - 5)) & 0x1f];
96
+ bits -= 5;
97
+ }
98
+ }
99
+ if (bits > 0)
100
+ out += alphabet[(value << (5 - bits)) & 0x1f];
101
+ return out.slice(0, 26);
102
+ }
103
+ /** Standard base64 (with `=` padding). Browser + Node compatible. */
104
+ function base64Std(bytes) {
105
+ let bin = "";
106
+ for (let i = 0; i < bytes.length; i++)
107
+ bin += String.fromCharCode(bytes[i]);
108
+ return btoa(bin);
109
+ }
110
+ /** base64url (URL-safe, no padding). */
111
+ function base64url(bytes) {
112
+ return base64Std(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
113
+ }
114
+ function sha256Hex(s) {
115
+ const d = sha256(new TextEncoder().encode(s));
116
+ let hex = "";
117
+ for (const b of d)
118
+ hex += b.toString(16).padStart(2, "0");
119
+ return hex;
120
+ }
121
+ /**
122
+ * JCS-style canonicalization (RFC 8785 subset).
123
+ *
124
+ * Sorted object keys, no whitespace, finite numbers via `toString()`,
125
+ * strings via `JSON.stringify` (escapes), arrays preserve order.
126
+ * `undefined` is rejected (RFC 8785 §3.2.2).
127
+ */
128
+ function jcsCanonicalize(value) {
129
+ if (value === null)
130
+ return "null";
131
+ if (value === undefined)
132
+ throw new Error("Cannot canonicalize undefined");
133
+ if (typeof value === "boolean")
134
+ return value ? "true" : "false";
135
+ if (typeof value === "number") {
136
+ if (!Number.isFinite(value))
137
+ throw new Error("non-finite number");
138
+ return value.toString();
139
+ }
140
+ if (typeof value === "string")
141
+ return JSON.stringify(value);
142
+ if (Array.isArray(value)) {
143
+ return "[" + value.map(jcsCanonicalize).join(",") + "]";
144
+ }
145
+ if (typeof value === "object") {
146
+ const obj = value;
147
+ const keys = Object.keys(obj).sort();
148
+ return ("{" +
149
+ keys.map((k) => JSON.stringify(k) + ":" + jcsCanonicalize(obj[k])).join(",") +
150
+ "}");
151
+ }
152
+ throw new Error(`Cannot canonicalize ${typeof value}`);
153
+ }
154
+ //# sourceMappingURL=envelope.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
@@ -218,5 +218,94 @@ describe("AithosAuth.signUp — Ethos bootstrap", () => {
218
218
  assert.equal(publishCalls, 3);
219
219
  assert.equal(auth.canSignAsOwner(), false);
220
220
  });
221
+ /* ------------------------------------------------------------------------ */
222
+ /* alpha.36 — defense-in-depth for legacy backends without semantic-equal */
223
+ /* ------------------------------------------------------------------------ */
224
+ it("treats -32022 'different did.json already published' as a no-op success", async () => {
225
+ // This is the regression introduced in alpha.33: republishing the same
226
+ // identity returns -32022 because `signedDidDocument()` regenerates
227
+ // `aithos.created_at` on every call. For an honest signer, this is
228
+ // semantically a no-op — the Ethos is published, crypto material matches.
229
+ // The SDK swallows this specific case so chatty publish_identity callers
230
+ // (signInCustodial, verifyEmail) don't break on every subsequent sign-in.
231
+ const { fetch: f, calls } = makeMockFetch([
232
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
233
+ {
234
+ url: "/mcp/primitives/write",
235
+ method: "POST",
236
+ respond: () => ({
237
+ json: {
238
+ jsonrpc: "2.0",
239
+ id: "publish_identity",
240
+ error: {
241
+ code: -32022,
242
+ message: "different did.json already published for this DID",
243
+ data: { existing_doc_url: "https://cdn.aithos.be/did.json" },
244
+ },
245
+ },
246
+ }),
247
+ },
248
+ ]);
249
+ const auth = makeAuth(f);
250
+ // Must succeed, not throw — the republish-conflict is masked.
251
+ await auth.signUp(validInput);
252
+ // Hydrate worked: owner is loaded.
253
+ assert.equal(auth.canSignAsOwner(), true);
254
+ // No retry on -32022 (deterministic) — exactly 1 publish call.
255
+ assert.equal(calls.filter((c) => c.url.includes("/mcp/primitives/write")).length, 1, "publish_identity must not retry on -32022");
256
+ });
257
+ it("still throws ethos_bootstrap_failed on -32022 with a DIFFERENT message", async () => {
258
+ // The shim is narrow: it ONLY swallows the specific
259
+ // "different did.json already published" message. Other -32022 cases
260
+ // (server might use the same code for different semantics) must still
261
+ // bubble up as ethos_bootstrap_failed.
262
+ const { fetch: f } = makeMockFetch([
263
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
264
+ {
265
+ url: "/mcp/primitives/write",
266
+ method: "POST",
267
+ respond: () => ({
268
+ json: {
269
+ jsonrpc: "2.0",
270
+ id: "publish_identity",
271
+ error: {
272
+ code: -32022,
273
+ message: "subject identity is tombstoned",
274
+ },
275
+ },
276
+ }),
277
+ },
278
+ ]);
279
+ const auth = makeAuth(f);
280
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError &&
281
+ e.code === "ethos_bootstrap_failed" &&
282
+ /tombstoned/i.test(e.message));
283
+ assert.equal(auth.canSignAsOwner(), false);
284
+ });
285
+ it("still throws ethos_bootstrap_failed on the conflict message under a different code", async () => {
286
+ // Conversely, an error matching the message but with a code OTHER than
287
+ // -32022 is not swallowed — we anchor on both (code, message) to keep
288
+ // the shim narrow.
289
+ const { fetch: f } = makeMockFetch([
290
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
291
+ {
292
+ url: "/mcp/primitives/write",
293
+ method: "POST",
294
+ respond: () => ({
295
+ json: {
296
+ jsonrpc: "2.0",
297
+ id: "publish_identity",
298
+ error: {
299
+ code: -32000,
300
+ message: "different did.json already published for this DID",
301
+ },
302
+ },
303
+ }),
304
+ },
305
+ ]);
306
+ const auth = makeAuth(f);
307
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
308
+ assert.equal(auth.canSignAsOwner(), false);
309
+ });
221
310
  });
222
311
  //# sourceMappingURL=signup-bootstrap.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.34",
3
+ "version": "0.1.0-alpha.36",
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",