@aithos/sdk 0.1.0-alpha.33 → 0.1.0-alpha.35
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 +53 -8
- package/dist/src/auth.d.ts +101 -0
- package/dist/src/auth.js +116 -0
- package/dist/src/data.js +9 -30
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +1 -1
- package/dist/src/internal/envelope.d.ts +77 -0
- package/dist/src/internal/envelope.js +154 -0
- package/dist/test/envelope.test.d.ts +2 -0
- package/dist/test/envelope.test.js +318 -0
- package/package.json +12 -11
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
|
|
219
|
-
|
|
|
220
|
-
| `sdk.
|
|
221
|
-
| `sdk.
|
|
222
|
-
| `sdk.
|
|
223
|
-
| `sdk.
|
|
224
|
-
| `sdk.
|
|
225
|
-
| `sdk.
|
|
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
|
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -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,
|
|
@@ -325,6 +390,42 @@ export declare class AithosAuth {
|
|
|
325
390
|
* @internal
|
|
326
391
|
*/
|
|
327
392
|
_findDelegateForSubject(did: string): DelegateActor | undefined;
|
|
393
|
+
/**
|
|
394
|
+
* Sign in with email + password, dispatching automatically between
|
|
395
|
+
* the legacy zero-knowledge flow ({@link signIn}) and the custodial
|
|
396
|
+
* flow ({@link signInCustodial}) based on which mode the account
|
|
397
|
+
* was provisioned with.
|
|
398
|
+
*
|
|
399
|
+
* Use this in apps that want a single sign-in form for users who
|
|
400
|
+
* may have been created under either mode (e.g. an app that's
|
|
401
|
+
* migrating from zk to custodial — pre-existing users stay zk
|
|
402
|
+
* forever, new ones go custodial, the SDK figures it out).
|
|
403
|
+
*
|
|
404
|
+
* Strategy: try {@link signInCustodial} first (the modern path).
|
|
405
|
+
* If the backend reports `auth_invalid_credentials` — which it
|
|
406
|
+
* uniformly returns for "wrong password", "unknown user", AND
|
|
407
|
+
* "user exists but not in custodial mode" (anti-enum) — fall
|
|
408
|
+
* back to {@link signIn} (zk).
|
|
409
|
+
*
|
|
410
|
+
* Other failure modes from the custodial path are NOT swallowed:
|
|
411
|
+
* - `auth_email_not_verified` → propagate (user is custodial but
|
|
412
|
+
* hasn't clicked the confirmation link yet; the app should
|
|
413
|
+
* surface a "resend mail" CTA rather than retrying as zk,
|
|
414
|
+
* which would also fail and mask the real cause)
|
|
415
|
+
* - server / network errors → propagate (don't double the
|
|
416
|
+
* incident by retrying through the other flow)
|
|
417
|
+
*
|
|
418
|
+
* Latency profile:
|
|
419
|
+
* - Pure custodial (success or wrong pwd) : 1 round-trip
|
|
420
|
+
* - Pure zk (any outcome) : 1 custodial probe + 2 zk
|
|
421
|
+
* - Unknown email : same as zk worst case
|
|
422
|
+
*
|
|
423
|
+
* Anti-enum note: timing slightly leaks the mode (custodial path is
|
|
424
|
+
* faster than zk). Acceptable for V1 — rate limiting + strong
|
|
425
|
+
* passwords are the real defenses. A future strict-anti-enum mode
|
|
426
|
+
* could race both paths in parallel and accept the 2x backend load.
|
|
427
|
+
*/
|
|
428
|
+
signInAuto(input: SignInInput): Promise<AithosSession>;
|
|
328
429
|
signIn(input: SignInInput): Promise<AithosSession>;
|
|
329
430
|
signUp(input: SignUpInput): Promise<SignUpResult>;
|
|
330
431
|
/**
|
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;
|
|
@@ -178,6 +237,63 @@ export class AithosAuth {
|
|
|
178
237
|
return this.#delegates.findForSubject(did);
|
|
179
238
|
}
|
|
180
239
|
/* ------------------------------------------------------------------------ */
|
|
240
|
+
/* Unified email + password — signInAuto (zk / custodial dispatch) */
|
|
241
|
+
/* ------------------------------------------------------------------------ */
|
|
242
|
+
/**
|
|
243
|
+
* Sign in with email + password, dispatching automatically between
|
|
244
|
+
* the legacy zero-knowledge flow ({@link signIn}) and the custodial
|
|
245
|
+
* flow ({@link signInCustodial}) based on which mode the account
|
|
246
|
+
* was provisioned with.
|
|
247
|
+
*
|
|
248
|
+
* Use this in apps that want a single sign-in form for users who
|
|
249
|
+
* may have been created under either mode (e.g. an app that's
|
|
250
|
+
* migrating from zk to custodial — pre-existing users stay zk
|
|
251
|
+
* forever, new ones go custodial, the SDK figures it out).
|
|
252
|
+
*
|
|
253
|
+
* Strategy: try {@link signInCustodial} first (the modern path).
|
|
254
|
+
* If the backend reports `auth_invalid_credentials` — which it
|
|
255
|
+
* uniformly returns for "wrong password", "unknown user", AND
|
|
256
|
+
* "user exists but not in custodial mode" (anti-enum) — fall
|
|
257
|
+
* back to {@link signIn} (zk).
|
|
258
|
+
*
|
|
259
|
+
* Other failure modes from the custodial path are NOT swallowed:
|
|
260
|
+
* - `auth_email_not_verified` → propagate (user is custodial but
|
|
261
|
+
* hasn't clicked the confirmation link yet; the app should
|
|
262
|
+
* surface a "resend mail" CTA rather than retrying as zk,
|
|
263
|
+
* which would also fail and mask the real cause)
|
|
264
|
+
* - server / network errors → propagate (don't double the
|
|
265
|
+
* incident by retrying through the other flow)
|
|
266
|
+
*
|
|
267
|
+
* Latency profile:
|
|
268
|
+
* - Pure custodial (success or wrong pwd) : 1 round-trip
|
|
269
|
+
* - Pure zk (any outcome) : 1 custodial probe + 2 zk
|
|
270
|
+
* - Unknown email : same as zk worst case
|
|
271
|
+
*
|
|
272
|
+
* Anti-enum note: timing slightly leaks the mode (custodial path is
|
|
273
|
+
* faster than zk). Acceptable for V1 — rate limiting + strong
|
|
274
|
+
* passwords are the real defenses. A future strict-anti-enum mode
|
|
275
|
+
* could race both paths in parallel and accept the 2x backend load.
|
|
276
|
+
*/
|
|
277
|
+
async signInAuto(input) {
|
|
278
|
+
if (!input.email || !input.password) {
|
|
279
|
+
throw new AithosSDKError("auth_invalid_input", "signInAuto: email and password are required");
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const r = await this.signInCustodial(input);
|
|
283
|
+
return r.session;
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
// Only fall back on the specific anti-enum sentinel — preserve
|
|
287
|
+
// other error codes (notably auth_email_not_verified) so the
|
|
288
|
+
// caller can surface the right UI hint.
|
|
289
|
+
if (e instanceof AithosSDKError &&
|
|
290
|
+
e.code === "auth_invalid_credentials") {
|
|
291
|
+
return await this.signIn(input);
|
|
292
|
+
}
|
|
293
|
+
throw e;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/* ------------------------------------------------------------------------ */
|
|
181
297
|
/* Email + password — signIn */
|
|
182
298
|
/* ------------------------------------------------------------------------ */
|
|
183
299
|
async signIn(input) {
|
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 =
|
|
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}`;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
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.
|
|
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,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
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aithos/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "Aithos SDK
|
|
3
|
+
"version": "0.1.0-alpha.35",
|
|
4
|
+
"description": "Aithos SDK \u2014 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",
|
|
7
7
|
"sdk",
|
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
"README.md",
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
45
|
+
"check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
46
|
+
"test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
|
|
47
|
+
"test:watch": "cd dist && node --test --watch",
|
|
48
|
+
"clean": "rm -rf dist",
|
|
49
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
50
|
+
},
|
|
42
51
|
"engines": {
|
|
43
52
|
"node": ">=20"
|
|
44
53
|
},
|
|
@@ -54,13 +63,5 @@
|
|
|
54
63
|
"publishConfig": {
|
|
55
64
|
"access": "public",
|
|
56
65
|
"tag": "alpha"
|
|
57
|
-
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"build": "tsc",
|
|
60
|
-
"build:test": "tsc -p tsconfig.test.json",
|
|
61
|
-
"check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
62
|
-
"test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
|
|
63
|
-
"test:watch": "cd dist && node --test --watch",
|
|
64
|
-
"clean": "rm -rf dist"
|
|
65
66
|
}
|
|
66
|
-
}
|
|
67
|
+
}
|