@aithos/sdk 0.1.0-alpha.43 → 0.1.0-alpha.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/data.js CHANGED
@@ -38,6 +38,7 @@ import { hkdf } from "@noble/hashes/hkdf.js";
38
38
  import { sha256, sha512 } from "@noble/hashes/sha2.js";
39
39
  import { XChaCha20Poly1305 } from "@stablelib/xchacha20poly1305";
40
40
  import * as ed from "@noble/ed25519";
41
+ import { multibaseToEd25519PublicKey, edPubToX25519Pub, } from "@aithos/protocol-client";
41
42
  import { contactsV1 } from "./data-schema-contacts-v1.js";
42
43
  import { signOwnerEnvelope } from "./internal/envelope.js";
43
44
  // noble/ed25519 v2 needs sha512 wired in for sync sign/verify
@@ -57,15 +58,48 @@ const SCHEMAS = new Map([
57
58
  export function createDataClient(args) {
58
59
  return new DataClientImpl(args);
59
60
  }
60
- /* -------------------------------------------------------------------------- */
61
- /* Implementation */
62
- /* -------------------------------------------------------------------------- */
61
+ /**
62
+ * Build a read-only data client that reads a subject's collections under
63
+ * a mandate (delegate path). The returned {@link ReadonlyDataClient}
64
+ * signs every request as the delegate (bare-multibase verificationMethod
65
+ * + the mandate attached to the envelope), and decrypts records using the
66
+ * CMK the owner re-wrapped for this delegate via
67
+ * {@link DataClient.authorizeDelegate}.
68
+ *
69
+ * Writes are not available on the returned type and throw `-32042` if
70
+ * forced.
71
+ */
72
+ export function createDelegateDataClient(args) {
73
+ const granteePubMb = args.granteePubkeyMultibase ??
74
+ args.mandate.grantee?.pubkey;
75
+ if (!granteePubMb) {
76
+ throw new Error("createDelegateDataClient: mandate.grantee.pubkey is missing; pass granteePubkeyMultibase explicitly");
77
+ }
78
+ return new DataClientImpl({
79
+ pdsUrl: args.pdsUrl,
80
+ did: args.subjectDid,
81
+ // In delegate mode the seed is used only to derive the X25519 key that
82
+ // unwraps the re-wrapped CMK; envelope signing goes through the
83
+ // delegate path (buildSignedEnvelope), never signOwnerEnvelope.
84
+ sphereSeed: args.delegateSeed,
85
+ verificationMethod: granteePubMb,
86
+ ...(args.schemas ? { schemas: args.schemas } : {}),
87
+ ...(args.fetch ? { fetch: args.fetch } : {}),
88
+ delegate: {
89
+ delegateSeed: args.delegateSeed,
90
+ granteePubkeyMultibase: granteePubMb,
91
+ mandate: args.mandate,
92
+ },
93
+ });
94
+ }
63
95
  class DataClientImpl {
64
96
  #pdsUrl;
65
97
  #did;
66
98
  #seed;
67
99
  #vm;
68
100
  #fetch;
101
+ /** Delegate session, or undefined for the owner path. */
102
+ #delegate;
69
103
  /**
70
104
  * Per-client schema overrides, populated from `args.schemas` at
71
105
  * construction. Looked up BEFORE the bundled SCHEMAS map (so an app
@@ -82,8 +116,22 @@ class DataClientImpl {
82
116
  this.#seed = args.sphereSeed;
83
117
  this.#vm = args.verificationMethod;
84
118
  this.#fetch = args.fetch ?? globalThis.fetch.bind(globalThis);
119
+ if (args.delegate)
120
+ this.#delegate = args.delegate;
85
121
  this.#localSchemas = new Map((args.schemas ?? []).map((s) => [s.schema, s]));
86
122
  }
123
+ /** Throw a read-only error when a mutating verb is called on a delegate
124
+ * client. The PDS rejects these server-side too; this is the fast,
125
+ * local guard with a precise message. */
126
+ #assertOwner(op) {
127
+ if (this.#delegate) {
128
+ const e = new Error(`sdk.data: "${op}" is not permitted for a delegate client (read-only mandate). ` +
129
+ `A data.<collection>.read mandate grants get/list only; writes require the owner ` +
130
+ `or a data.<collection>.write mandate (not yet supported on the delegate path).`);
131
+ e.code = -32042;
132
+ throw e;
133
+ }
134
+ }
87
135
  /**
88
136
  * Resolve a schema id to its lite definition. App-supplied schemas
89
137
  * (via `createDataClient({ schemas })`) take precedence over the
@@ -95,7 +143,45 @@ class DataClientImpl {
95
143
  collection(name) {
96
144
  return new DataCollectionImpl(this, name);
97
145
  }
146
+ async authorizeDelegate(args) {
147
+ this.#assertOwner("authorizeDelegate");
148
+ const granteePubMb = args.mandate.grantee?.pubkey;
149
+ if (!granteePubMb) {
150
+ throw new Error("sdk.data.authorizeDelegate: mandate.grantee.pubkey is required (data read mandates bind to a grantee key).");
151
+ }
152
+ // Ensure we hold the collection's CMK (owner unwrap path).
153
+ const state = await this._ensureCollection(args.collectionName);
154
+ const cmk = this.#cmkCache.get(args.collectionName);
155
+ if (!cmk) {
156
+ throw new Error(`sdk.data.authorizeDelegate: CMK for "${args.collectionName}" not loaded`);
157
+ }
158
+ // Derive the grantee's X25519 wrap-recipient key from its Ed25519
159
+ // public key (the same birational map the owner uses for its own key).
160
+ const granteeEdPub = multibaseToEd25519PublicKey(granteePubMb);
161
+ const granteeX25519Pub = edPubToX25519Pub(granteeEdPub);
162
+ const recipientDidUrl = delegateRecipientDidUrl(granteePubMb);
163
+ const wrap = this.#wrapCmkForRecipient({
164
+ cmk,
165
+ recipientPublicKey: granteeX25519Pub,
166
+ recipientDidUrl,
167
+ collectionUrn: state.urn,
168
+ });
169
+ await this.#call("/mcp/primitives/write", "aithos.data.authorize_app", {
170
+ collection_urn: state.urn,
171
+ mandate: args.mandate,
172
+ wrap,
173
+ });
174
+ }
175
+ async revokeDelegate(args) {
176
+ this.#assertOwner("revokeDelegate");
177
+ await this.#call("/mcp/primitives/write", "aithos.data.revoke_app", {
178
+ collection_urn: this.#collectionUrn(args.collectionName),
179
+ mandate_id: args.mandateId,
180
+ ...(args.reason ? { revocation: { reason: args.reason } } : {}),
181
+ });
182
+ }
98
183
  async createCollection(args) {
184
+ this.#assertOwner("createCollection");
99
185
  const cmk = randomBytes32();
100
186
  const recipientDidUrl = `${this.#did}#data-kex`;
101
187
  const collectionUrn = this.#collectionUrn(args.name);
@@ -124,6 +210,20 @@ class DataClientImpl {
124
210
  // CMK is retained in cache, only zero the local var if we didn't cache.
125
211
  }
126
212
  }
213
+ async ensureCollection(args) {
214
+ this.#assertOwner("ensureCollection");
215
+ try {
216
+ await this.createCollection(args);
217
+ }
218
+ catch (e) {
219
+ // -32073 AITHOS_DATA_COLLECTION_EXISTS → already created (possibly by a
220
+ // concurrent caller). That's the success case for get-or-create. Any
221
+ // other error propagates.
222
+ if (e.code === -32073)
223
+ return;
224
+ throw e;
225
+ }
226
+ }
127
227
  async listCollections() {
128
228
  const r = await this.#call("/mcp/primitives/read", "aithos.data.list_collections", {
129
229
  subject_did: this.#did,
@@ -146,6 +246,7 @@ class DataClientImpl {
146
246
  return this.#call("/mcp/primitives/read", "aithos.data.list_gamma_entries", params);
147
247
  }
148
248
  async registerSchema(schemaDoc) {
249
+ this.#assertOwner("registerSchema");
149
250
  if (schemaDoc === null || typeof schemaDoc !== "object" || Array.isArray(schemaDoc)) {
150
251
  throw new Error("sdk.data.registerSchema: schemaDoc must be a JSON object");
151
252
  }
@@ -210,16 +311,27 @@ class DataClientImpl {
210
311
  err.code = -32020;
211
312
  throw err;
212
313
  }
213
- // Look up our wrap and unwrap the CMK
214
- const ourRecipient = `${this.#did}#data-kex`;
314
+ // Look up our wrap and unwrap the CMK. Owner: the wrap addressed to
315
+ // `${did}#data-kex`, unwrapped with the owner sphere seed. Delegate:
316
+ // the wrap the owner re-wrapped for this grantee
317
+ // (`did:key:${granteePubkeyMultibase}#data-kex`), unwrapped with the
318
+ // delegate's own X25519 key (derived from its grantee seed).
319
+ const ourRecipient = this.#delegate
320
+ ? delegateRecipientDidUrl(this.#delegate.granteePubkeyMultibase)
321
+ : `${this.#did}#data-kex`;
215
322
  const wrap = meta.cmk_envelope.wraps.find((w) => w.recipient === ourRecipient);
216
323
  if (!wrap) {
217
- throw new Error(`sdk.data: no CMK wrap for ${ourRecipient} in collection ${name}. The collection was either created with a different recipient, or this client is not the owner.`);
324
+ throw new Error(this.#delegate
325
+ ? `sdk.data: no CMK wrap for ${ourRecipient} in collection "${name}". ` +
326
+ `The owner has not authorized this delegate on the collection yet — ` +
327
+ `ask them to call sdk.data...authorizeDelegate({ collectionName, mandate }).`
328
+ : `sdk.data: no CMK wrap for ${ourRecipient} in collection ${name}. The collection was either created with a different recipient, or this client is not the owner.`);
218
329
  }
330
+ const unwrapSeed = this.#delegate ? this.#delegate.delegateSeed : this.#seed;
219
331
  const cmk = this.#unwrapCmk({
220
332
  wrap,
221
333
  collectionUrn: meta.urn,
222
- privateKey: ed25519SeedToX25519PrivateKey(this.#seed),
334
+ privateKey: ed25519SeedToX25519PrivateKey(unwrapSeed),
223
335
  });
224
336
  this.#cmkCache.set(name, cmk);
225
337
  const schema = this.#resolveSchema(meta.schema);
@@ -234,6 +346,7 @@ class DataClientImpl {
234
346
  return state;
235
347
  }
236
348
  async _insert(state, record) {
349
+ this.#assertOwner("insert");
237
350
  const cmk = this.#cmkCache.get(state.name);
238
351
  if (!cmk)
239
352
  throw new Error("CMK not loaded");
@@ -288,6 +401,7 @@ class DataClientImpl {
288
401
  };
289
402
  }
290
403
  async _update(state, recordId, record) {
404
+ this.#assertOwner("update");
291
405
  const cmk = this.#cmkCache.get(state.name);
292
406
  if (!cmk)
293
407
  throw new Error("CMK not loaded");
@@ -306,6 +420,7 @@ class DataClientImpl {
306
420
  });
307
421
  }
308
422
  async _delete(state, recordId) {
423
+ this.#assertOwner("delete");
309
424
  await this.#call("/mcp/primitives/write", "aithos.data.delete_record", {
310
425
  collection_urn: state.urn,
311
426
  record_id: recordId,
@@ -314,14 +429,31 @@ class DataClientImpl {
314
429
  /* -- JSON-RPC dispatch -- */
315
430
  async #call(path, method, params) {
316
431
  const aud = `${this.#pdsUrl}${path}`;
317
- const envelope = await signOwnerEnvelope({
318
- iss: this.#did,
319
- aud,
320
- method,
321
- params,
322
- verificationMethod: this.#vm,
323
- signer: { sign: async (msg) => ed.sign(msg, this.#seed) },
324
- });
432
+ // Both paths use the SAME §11.2 envelope scheme (the data PDS verifies
433
+ // with @aithos/protocol-core, which canonicalizes the full envelope
434
+ // INCLUDING `proof` with proofValue=""). Delegate path: sign with the
435
+ // grantee key, bare-multibase verificationMethod, and attach the mandate
436
+ // so the PDS resolves the delegation and enforces its scopes.
437
+ const envelope = this.#delegate
438
+ ? await signOwnerEnvelope({
439
+ iss: this.#did, // the SUBJECT DID (mandate issuer), not the delegate
440
+ aud,
441
+ method,
442
+ params,
443
+ verificationMethod: this.#delegate.granteePubkeyMultibase,
444
+ signer: {
445
+ sign: async (msg) => ed.sign(msg, this.#delegate.delegateSeed),
446
+ },
447
+ mandate: this.#delegate.mandate,
448
+ })
449
+ : await signOwnerEnvelope({
450
+ iss: this.#did,
451
+ aud,
452
+ method,
453
+ params,
454
+ verificationMethod: this.#vm,
455
+ signer: { sign: async (msg) => ed.sign(msg, this.#seed) },
456
+ });
325
457
  const body = {
326
458
  jsonrpc: "2.0",
327
459
  id: makeUlid(),
@@ -508,6 +640,18 @@ function cryptoRandom(n) {
508
640
  function utf8(s) {
509
641
  return new TextEncoder().encode(s);
510
642
  }
643
+ /**
644
+ * Recipient DID URL used for a delegate's CMK wrap. Built from the
645
+ * grantee's Ed25519 public-key multibase so that (a) the owner side
646
+ * (`authorizeDelegate`) and the delegate side (`_ensureCollection`)
647
+ * derive the EXACT same string — it's bound into the wrap AAD and the
648
+ * HKDF info, so any mismatch fails the unwrap — and (b) the string
649
+ * contains `mandate.grantee.pubkey`, which the PDS `authorize_app`
650
+ * handler requires (`wrap.recipient.includes(grantee.pubkey)`).
651
+ */
652
+ function delegateRecipientDidUrl(granteePubkeyMultibase) {
653
+ return `did:key:${granteePubkeyMultibase}#data-kex`;
654
+ }
511
655
  function aadCmkWrap(collectionUrn, recipient) {
512
656
  const p = utf8("aithos-data-cmk-v1\0");
513
657
  const c = utf8(collectionUrn);
@@ -1,12 +1,14 @@
1
- export declare const VERSION = "0.1.0-alpha.43";
1
+ export declare const VERSION = "0.1.0-alpha.44";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
5
5
  export { AithosRpcError } from "@aithos/protocol-client";
6
6
  export type { AithosSdkEndpoints } from "./endpoints.js";
7
7
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
8
- export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, } from "./compute.js";
8
+ export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, TranscribeModelId, TranscribeProgressState, TranscribeSegment, TranscribeWord, InvokeTranscribeArgs, InvokeTranscribeResult, PrepareTranscribeArgs, PrepareTranscribeResult, StartTranscribeArgs, StartTranscribeResult, TranscribeStatusResult, TranscribeJobSummary, } from "./compute.js";
9
9
  export { ComputeNamespace } from "./compute.js";
10
+ export type { LocalPendingEntry, LocalPendingStatus, TranscribeDraftMeta, TranscribeDraftRecord, } from "./transcribe-resilience.js";
11
+ export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
10
12
  export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
11
13
  export { WalletNamespace } from "./wallet.js";
12
14
  export type { ComponentStyle, ExtractArgs, ExtractContent, ExtractData, ExtractForm, ExtractFormField, ExtractHeading, ExtractIconDeclaration, ExtractImage, ExtractLink, ExtractLogo, ExtractMeta, ExtractResult, ExtractSection, ExtractStructure, ExtractStyles, FetchAssetArgs, FetchAssetResult, PaletteEntry, VisualSignature, WebNamespaceDeps, } from "./web.js";
@@ -25,6 +27,6 @@ export type { AudienceSet, AppCreditPackId, CreateAppTopupSessionArgs, CreateApp
25
27
  export * as onboarding from "./onboarding.js";
26
28
  export { createBrowserIdentity, browserIdentityFromStored, type BrowserIdentity, } from "@aithos/protocol-client";
27
29
  export type { Section } from "@aithos/protocol-client";
28
- export { createDataClient, type CreateDataClientArgs, type DataClient, type DataCollection, type ListOpts, type AithosSchemaLite, } from "./data.js";
30
+ export { createDataClient, createDelegateDataClient, type CreateDataClientArgs, type CreateDelegateDataClientArgs, type DataClient, type DataCollection, type ReadonlyDataClient, type ReadonlyDataCollection, type ListOpts, type AithosSchemaLite, } from "./data.js";
29
31
  export { createAssetsClient, AssetsClient, type CreateAssetsClientArgs, type AttachedContext, type AssetUploadInput, type AssetUploadResult, type AssetFetchResult, type AssetBrief, type ListAssetsOpts, type ThumbnailUploadInput, type ThumbnailUploadResult, type RecipientResolver, type RecipientSet, } from "./assets.js";
30
32
  //# sourceMappingURL=index.d.ts.map
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.43";
20
+ export const VERSION = "0.1.0-alpha.44";
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
@@ -26,6 +26,7 @@ export { AithosSDKError } from "./types.js";
26
26
  export { AithosRpcError } from "@aithos/protocol-client";
27
27
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
28
28
  export { ComputeNamespace } from "./compute.js";
29
+ export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
29
30
  export { WalletNamespace } from "./wallet.js";
30
31
  export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
31
32
  // Sign-up, sign-in, sign-in-with-Google. Lives outside the AithosSDK
@@ -67,7 +68,7 @@ export { createBrowserIdentity, browserIdentityFromStored, } from "@aithos/proto
67
68
  // `sdk.data` namespace — Aithos data sub-protocol PDS client. Manages
68
69
  // the lifecycle of subject-owned, encrypted, schema-validated records.
69
70
  // See spec/data/ in the aithos-protocol repo.
70
- export { createDataClient, } from "./data.js";
71
+ export { createDataClient, createDelegateDataClient, } from "./data.js";
71
72
  // `sdk.assets` — Aithos assets sub-protocol PDS client. Upload,
72
73
  // fetch, list, ref/unref binary content (images, PDFs, audio, video)
73
74
  // owned by a subject. AEAD-encrypted per-asset under AMKs wrapped for
@@ -22,6 +22,15 @@ export interface SignedEnvelope {
22
22
  readonly exp: number;
23
23
  readonly nonce: string;
24
24
  readonly params_hash: string;
25
+ /**
26
+ * Full signed mandate — present ONLY for a delegate-signed envelope
27
+ * (§11.6). When present, `proof.verificationMethod` is the delegate's
28
+ * bare Ed25519 multibase (matching `mandate.grantee.pubkey`) and the
29
+ * server resolves the signer to that key after verifying the mandate.
30
+ * The field is part of the signed bytes (the signature commits to the
31
+ * delegation context), so it cannot be swapped out in transit.
32
+ */
33
+ readonly mandate?: unknown;
25
34
  readonly proof: {
26
35
  readonly type: "Ed25519Signature2020";
27
36
  readonly verificationMethod: string;
@@ -54,6 +63,13 @@ export interface SignOwnerEnvelopeArgs {
54
63
  * matching public key.
55
64
  */
56
65
  readonly verificationMethod: string;
66
+ /**
67
+ * Full signed mandate, attached to the envelope for a delegate-signed
68
+ * call (§11.6). When set, `verificationMethod` MUST be the delegate's
69
+ * bare Ed25519 multibase (matching `mandate.grantee.pubkey`) and
70
+ * `signer` MUST be the delegate's key. Omit for owner-path envelopes.
71
+ */
72
+ readonly mandate?: unknown;
57
73
  /** Envelope lifetime in seconds. Default 60. Server caps at 300. */
58
74
  readonly ttlSeconds?: number;
59
75
  /** Clock override for deterministic tests. Defaults to `new Date()`. */
@@ -47,6 +47,12 @@ export async function signOwnerEnvelope(args) {
47
47
  exp,
48
48
  nonce,
49
49
  params_hash: paramsHash,
50
+ // Attach the mandate (delegate path) so the signature commits to the
51
+ // delegation context. JCS sorts keys, so placement here is irrelevant
52
+ // to the canonical bytes — what matters is that the server (which
53
+ // canonicalizes the full envelope incl. mandate + proof/proofValue="")
54
+ // sees the exact same object.
55
+ ...(args.mandate !== undefined ? { mandate: args.mandate } : {}),
50
56
  proof: {
51
57
  type: "Ed25519Signature2020",
52
58
  verificationMethod: args.verificationMethod,
@@ -8,7 +8,28 @@ import type { AithosSdkEndpoints } from "./endpoints.js";
8
8
  * directly in `scopes` is rejected at runtime; the compiler can't enforce
9
9
  * it (callers who up-cast to string[] would slip through), so the runtime
10
10
  * check is the real gate. */
11
- export type Scope = "ethos.read.public" | "ethos.read.circle" | "ethos.read.self" | "ethos.write.public" | "ethos.write.circle" | "ethos.write.self";
11
+ export type Scope = "ethos.read.public" | "ethos.read.circle" | "ethos.read.self" | "ethos.write.public" | "ethos.write.circle" | "ethos.write.self" | DataScope;
12
+ /** Action a data mandate may authorize on a collection. `write` implies
13
+ * `read`; `admin` implies `write`. Mirrors the data sub-protocol grammar
14
+ * `data.<collection>.<action>` (Aithos-protocol `spec/data/04-mandates.md`
15
+ * §4.2) and the server-side check `requireScope` in data-backend. */
16
+ export type DataAction = "read" | "write" | "admin";
17
+ /**
18
+ * A data-access scope: `data.<collection>.<action>`, or the cross-collection
19
+ * wildcard `data.*.<action>`. Examples: `data.contacts.read`,
20
+ * `data.depots.write`, `data.*.read`.
21
+ *
22
+ * Note on `actor_sphere`: data mandates are minted under `actor_sphere:
23
+ * "self"` (the owner's highest-authority sphere). The sphere is *not* the
24
+ * access axis for data — the collection is. `actor_sphere` is informative
25
+ * here; the cryptographic binding is the grantee's key + the CMK wrap, per
26
+ * spec §4.4. A dedicated `#data` sphere key (independent rotation) MAY be
27
+ * introduced later without changing this scope grammar.
28
+ *
29
+ * Collection names MUST NOT contain `.` (the server splits the scope on
30
+ * `.` and reads the first three segments).
31
+ */
32
+ export type DataScope = `data.${string}.${DataAction}`;
12
33
  /**
13
34
  * The opt-in scope that authorizes a delegate to spend the subject's
14
35
  * compute credits via the Aithos compute proxy. Mirror of
@@ -64,6 +64,17 @@ export class MandatesNamespace {
64
64
  `not by adding "${COMPUTE_INVOKE_SCOPE}" to scopes[]. The namespace forces ` +
65
65
  `an explicit budget and is what a consent UI reviews.`);
66
66
  }
67
+ // Fail fast on malformed data scopes so the misuse surfaces at the SDK
68
+ // boundary, not as an opaque server rejection at first delegate call.
69
+ // Accepts `data.<collection>.<action>` and the wildcard `data.*.<action>`.
70
+ for (const s of input.scopes) {
71
+ if (s.startsWith("data.") &&
72
+ !isWellFormedDataScope(s)) {
73
+ throw new AithosSDKError("mandates_invalid_scopes", `Malformed data scope "${s}". Expected data.<collection>.<action> ` +
74
+ `(action = read | write | admin), e.g. "data.contacts.read" or ` +
75
+ `"data.*.read". Collection names must not contain ".".`);
76
+ }
77
+ }
67
78
  // Validate + project the compute namespace if present, then derive
68
79
  // the final scopes/constraints to send to the protocol layer.
69
80
  const computeProjection = projectCompute(input.compute);
@@ -207,11 +218,41 @@ export class MandatesNamespace {
207
218
  /* Helpers */
208
219
  /* -------------------------------------------------------------------------- */
209
220
  function defaultSphereFromScopes(scopes) {
210
- if (scopes.some((s) => s.endsWith(".self")))
221
+ // Data scopes are sphere-NEUTRAL: they're permitted under self & circle (and
222
+ // public once the allowlist includes them), and the data access axis is the
223
+ // collection, not the sphere (the sphere is informative — see {@link DataScope}).
224
+ // So the actor_sphere of a combined Ethos+data mandate is decided by the
225
+ // ETHOS scopes alone; data scopes neither raise nor lower it. This makes
226
+ // `ethos.read.public + data.X.read` default to `public`, `ethos.read.circle
227
+ // + data.X.read` to `circle`, etc. A caller can always override via
228
+ // `actorSphere`.
229
+ const ethos = scopes.filter((s) => !s.startsWith("data."));
230
+ // Ethos write scopes pin the sphere EXACTLY (a write mandate must be signed
231
+ // by the sphere it writes to — `validateScopesAgainstSphere`).
232
+ if (ethos.some((s) => s === "ethos.write.public"))
233
+ return "public";
234
+ if (ethos.some((s) => s === "ethos.write.circle"))
235
+ return "circle";
236
+ if (ethos.some((s) => s === "ethos.write.self"))
211
237
  return "self";
212
- if (scopes.some((s) => s.endsWith(".circle")))
238
+ // Ethos read scopes: narrowest sphere that permits them.
239
+ if (ethos.some((s) => s.endsWith(".self")))
240
+ return "self";
241
+ if (ethos.some((s) => s.endsWith(".circle")))
213
242
  return "circle";
214
- return "public";
243
+ // Remaining Ethos scopes (ethos.read.public / .all / gamma.read) → public.
244
+ if (ethos.length > 0)
245
+ return "public";
246
+ // Data-only mandate: sign under `self`. self is the owner's highest-trust
247
+ // sphere, always permits the scope at mint time (no dependency on the
248
+ // public-sphere allowlist), and keeps the data grant off the most-exposed
249
+ // #public key. (Override with `actorSphere` for a public/circle label.)
250
+ return "self";
251
+ }
252
+ /** `true` iff `s` is a well-formed data scope `data.<collection>.<action>`
253
+ * with no filter suffix and a non-empty, dot-free collection name. */
254
+ function isWellFormedDataScope(s) {
255
+ return /^data\.[^.]+\.(read|write|admin)$/.test(s);
215
256
  }
216
257
  /**
217
258
  * Validate the SDK-side `compute` namespace and project it onto the
@@ -25,4 +25,5 @@
25
25
  export { AssetsClientProvider, useAssetsClient, type AssetsClientProviderProps, } from "./context.js";
26
26
  export { useAithosAsset, type UseAithosAssetState, type UseAithosAssetOptions, } from "./use-aithos-asset.js";
27
27
  export { AithosAsset, type AithosAssetProps, type AithosImageProps, type AithosVideoProps, type AithosAudioProps, type AithosDownloadProps, } from "./AithosAsset.js";
28
+ export { useAithosTranscribePendingJobs, type UseAithosTranscribePendingJobs, } from "./use-transcribe-pending.js";
28
29
  //# sourceMappingURL=index.d.ts.map
@@ -27,4 +27,5 @@
27
27
  export { AssetsClientProvider, useAssetsClient, } from "./context.js";
28
28
  export { useAithosAsset, } from "./use-aithos-asset.js";
29
29
  export { AithosAsset, } from "./AithosAsset.js";
30
+ export { useAithosTranscribePendingJobs, } from "./use-transcribe-pending.js";
30
31
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ import type { ComputeNamespace, InvokeTranscribeResult, TranscribeProgressState } from "../compute.js";
2
+ import type { LocalPendingEntry } from "../transcribe-resilience.js";
3
+ export interface UseAithosTranscribePendingJobs {
4
+ /** Locally-tracked in-flight jobs (survives reloads via localStorage). */
5
+ readonly pending: readonly LocalPendingEntry[];
6
+ /** Resume polling a job by id; resolves with the final transcript. */
7
+ readonly resume: (jobId: string, opts?: {
8
+ readonly mandateId?: string;
9
+ readonly onProgress?: (state: TranscribeProgressState) => void;
10
+ readonly signal?: AbortSignal;
11
+ readonly pollIntervalMs?: number;
12
+ }) => Promise<InvokeTranscribeResult>;
13
+ }
14
+ /**
15
+ * Subscribe a React component to the SDK's local pending-transcription
16
+ * registry. Re-renders whenever a job is added, advances, or clears.
17
+ *
18
+ * @param compute the SDK compute namespace (`sdk.compute`).
19
+ */
20
+ export declare function useAithosTranscribePendingJobs(compute: ComputeNamespace): UseAithosTranscribePendingJobs;
21
+ //# sourceMappingURL=use-transcribe-pending.d.ts.map
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ /**
4
+ * `useAithosTranscribePendingJobs(sdk.compute)` — a thin React adapter over
5
+ * the framework-agnostic local pending-jobs tracker exposed by the compute
6
+ * namespace. The tracker itself (subscribe/getSnapshot) is plain vanilla JS
7
+ * and works with any framework; this hook just wires it into React's
8
+ * `useSyncExternalStore`. Vue/Svelte/vanilla users can call
9
+ * `sdk.compute.subscribeLocalPendingTranscribes` directly.
10
+ *
11
+ * function PendingBanner({ sdk }) {
12
+ * const { pending, resume } = useAithosTranscribePendingJobs(sdk.compute);
13
+ * if (pending.length === 0) return null;
14
+ * return (
15
+ * <div>
16
+ * {pending.map((p) => (
17
+ * <button key={p.jobId} onClick={() => resume(p.jobId)}>
18
+ * Resume {p.jobId} ({p.status})
19
+ * </button>
20
+ * ))}
21
+ * </div>
22
+ * );
23
+ * }
24
+ */
25
+ import { useCallback, useEffect, useState } from "react";
26
+ /**
27
+ * Subscribe a React component to the SDK's local pending-transcription
28
+ * registry. Re-renders whenever a job is added, advances, or clears.
29
+ *
30
+ * @param compute the SDK compute namespace (`sdk.compute`).
31
+ */
32
+ export function useAithosTranscribePendingJobs(compute) {
33
+ const [pending, setPending] = useState(() => compute.getLocalPendingTranscribesSnapshot());
34
+ useEffect(() => {
35
+ // Sync immediately (the snapshot may have changed before mount) then
36
+ // subscribe. getSnapshot returns a stable reference between mutations,
37
+ // so identical states bail out of a re-render.
38
+ setPending(compute.getLocalPendingTranscribesSnapshot());
39
+ const unsubscribe = compute.subscribeLocalPendingTranscribes(() => {
40
+ setPending(compute.getLocalPendingTranscribesSnapshot());
41
+ });
42
+ return unsubscribe;
43
+ }, [compute]);
44
+ const resume = useCallback((jobId, opts) => compute.resumeTranscribe(jobId, opts), [compute]);
45
+ return { pending, resume };
46
+ }
47
+ //# sourceMappingURL=use-transcribe-pending.js.map
@@ -0,0 +1,57 @@
1
+ export type LocalPendingStatus = "uploading" | "running" | "completed" | "failed";
2
+ export interface LocalPendingEntry {
3
+ readonly jobId: string;
4
+ readonly status: LocalPendingStatus;
5
+ readonly createdAt: number;
6
+ readonly updatedAt: number;
7
+ readonly meta?: Record<string, unknown>;
8
+ }
9
+ /**
10
+ * Framework-agnostic observable registry of in-flight transcription jobs.
11
+ * Persisted to localStorage when available (so it survives reloads), with
12
+ * an in-memory fallback otherwise. Subscribe with `subscribe(listener)`;
13
+ * read with `getSnapshot()` (stable reference between mutations, so it
14
+ * plugs directly into React's `useSyncExternalStore`).
15
+ */
16
+ export declare class LocalPendingTranscribeTracker {
17
+ #private;
18
+ constructor();
19
+ /** Current entries. Stable reference until the next mutation. */
20
+ getSnapshot(): readonly LocalPendingEntry[];
21
+ list(): readonly LocalPendingEntry[];
22
+ /** Subscribe to changes. Returns an unsubscribe function. */
23
+ subscribe(listener: () => void): () => void;
24
+ upsert(jobId: string, status: LocalPendingStatus, meta?: Record<string, unknown>): void;
25
+ remove(jobId: string): void;
26
+ clear(): void;
27
+ }
28
+ export interface TranscribeDraftMeta {
29
+ readonly title?: string;
30
+ readonly tag?: string;
31
+ readonly contentType?: string;
32
+ }
33
+ export interface TranscribeDraftRecord {
34
+ readonly draftId: string;
35
+ readonly blob: Blob;
36
+ readonly metadata: TranscribeDraftMeta;
37
+ readonly createdAt: number;
38
+ }
39
+ export declare class TranscribeDraftUnavailableError extends Error {
40
+ constructor();
41
+ }
42
+ /**
43
+ * IndexedDB-backed queue of recorded audio Blobs. Save a recording the
44
+ * instant it finishes (before any network), then `upload` it when the
45
+ * user confirms — so a flaky network or a closed tab never loses audio.
46
+ * Browser-only: methods reject with {@link TranscribeDraftUnavailableError}
47
+ * when IndexedDB is absent.
48
+ */
49
+ export declare class TranscribeDraftStore {
50
+ save(blob: Blob, meta?: TranscribeDraftMeta): Promise<{
51
+ readonly draftId: string;
52
+ }>;
53
+ list(): Promise<readonly TranscribeDraftRecord[]>;
54
+ get(draftId: string): Promise<TranscribeDraftRecord | null>;
55
+ delete(draftId: string): Promise<void>;
56
+ }
57
+ //# sourceMappingURL=transcribe-resilience.d.ts.map