@aithos/sdk 0.1.0-alpha.44 → 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.d.ts +128 -1
- package/dist/src/data.js +159 -15
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/internal/envelope.d.ts +16 -0
- package/dist/src/internal/envelope.js +6 -0
- package/dist/src/mandates.d.ts +22 -1
- package/dist/src/mandates.js +44 -3
- package/package.json +1 -1
package/dist/src/data.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SignedMandate } from "@aithos/protocol-client";
|
|
1
2
|
export interface AithosSchemaLite {
|
|
2
3
|
readonly schema: string;
|
|
3
4
|
readonly indexable: ReadonlySet<string>;
|
|
@@ -45,12 +46,26 @@ export interface CreateDataClientArgs {
|
|
|
45
46
|
export interface DataClient {
|
|
46
47
|
/** Get / create a collection handle. */
|
|
47
48
|
collection(name: string): DataCollection;
|
|
48
|
-
/** Initialize a new collection with an explicit schema.
|
|
49
|
+
/** Initialize a new collection with an explicit schema. Throws
|
|
50
|
+
* `-32073 AITHOS_DATA_COLLECTION_EXISTS` if it already exists. */
|
|
49
51
|
createCollection(args: {
|
|
50
52
|
name: string;
|
|
51
53
|
schema: string;
|
|
52
54
|
forwardSecrecy?: "best_effort" | "strict";
|
|
53
55
|
}): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Get-or-create: create the collection if it doesn't exist, otherwise
|
|
58
|
+
* succeed silently. Idempotent — safe to call on every app boot before
|
|
59
|
+
* writing. Absorbs the `-32073 AITHOS_DATA_COLLECTION_EXISTS` conflict
|
|
60
|
+
* (and the concurrent-create race) so callers don't have to special-case
|
|
61
|
+
* "already there". Avoids the friction where `collection(name).insert(…)`
|
|
62
|
+
* on a never-created collection fails with `-32020`.
|
|
63
|
+
*/
|
|
64
|
+
ensureCollection(args: {
|
|
65
|
+
name: string;
|
|
66
|
+
schema: string;
|
|
67
|
+
forwardSecrecy?: "best_effort" | "strict";
|
|
68
|
+
}): Promise<void>;
|
|
54
69
|
/** List collections owned by this subject. */
|
|
55
70
|
listCollections(): Promise<readonly {
|
|
56
71
|
name: string;
|
|
@@ -102,9 +117,83 @@ export interface DataClient {
|
|
|
102
117
|
getSchema(schemaId: string, opts?: {
|
|
103
118
|
subjectDid?: string;
|
|
104
119
|
}): Promise<object | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Grant a mandate-holding delegate read access to one of this owner's
|
|
122
|
+
* collections, by re-wrapping the collection's CMK to the grantee's
|
|
123
|
+
* key and posting `aithos.data.authorize_app`.
|
|
124
|
+
*
|
|
125
|
+
* Owner-only. The CMK is unwrapped locally (the owner holds it), then
|
|
126
|
+
* re-wrapped X25519-HKDF-AEAD to the grantee's X25519 key (derived
|
|
127
|
+
* from `mandate.grantee.pubkey`). The platform never sees the CMK in
|
|
128
|
+
* clear — it only appends the wrap to the collection's envelope after
|
|
129
|
+
* verifying the mandate (data spec §4.5).
|
|
130
|
+
*
|
|
131
|
+
* Idempotent at the server: re-authorizing the same grantee on the
|
|
132
|
+
* same collection is a no-op. One wrap per grantee covers every record
|
|
133
|
+
* in the collection (O(1) authorization — the CMK is stable).
|
|
134
|
+
*
|
|
135
|
+
* The mandate must carry a `data.<collectionName>.{read|write|admin}`
|
|
136
|
+
* or `data.*.*` scope and a `grantee.pubkey`.
|
|
137
|
+
*/
|
|
138
|
+
authorizeDelegate(args: {
|
|
139
|
+
collectionName: string;
|
|
140
|
+
mandate: SignedMandate;
|
|
141
|
+
}): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Revoke a delegate's access to a collection (`aithos.data.revoke_app`).
|
|
144
|
+
* Owner-only, forward-only: after revocation the PDS refuses the
|
|
145
|
+
* delegate's reads (the mandate is marked revoked), and the delegate's
|
|
146
|
+
* wrap is dropped from the collection's authorization index. Already-read
|
|
147
|
+
* / cached plaintext on the delegate side is out of scope (a known limit
|
|
148
|
+
* of any key-sharing scheme — revocation blocks FUTURE access).
|
|
149
|
+
*/
|
|
150
|
+
revokeDelegate(args: {
|
|
151
|
+
collectionName: string;
|
|
152
|
+
mandateId: string;
|
|
153
|
+
reason?: string;
|
|
154
|
+
}): Promise<void>;
|
|
155
|
+
/** Drop in-memory cache (CMK, collection metadata, …). */
|
|
156
|
+
reset(): void;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Read-only view over a subject's data collections, driven by a mandate
|
|
160
|
+
* the subject granted to a delegate (`data.<collection>.read`). Built by
|
|
161
|
+
* {@link createDelegateDataClient}.
|
|
162
|
+
*
|
|
163
|
+
* Mirror of {@link DataClient} minus every mutating verb: a delegate
|
|
164
|
+
* holding a read mandate can `get`/`list` and enumerate collections, but
|
|
165
|
+
* cannot insert, update, delete, create collections, register schemas, or
|
|
166
|
+
* re-delegate. Those throw `-32042` client-side (and the PDS rejects them
|
|
167
|
+
* server-side regardless).
|
|
168
|
+
*/
|
|
169
|
+
export interface ReadonlyDataClient {
|
|
170
|
+
/** Get a read-only collection handle. */
|
|
171
|
+
collection(name: string): ReadonlyDataCollection;
|
|
172
|
+
/** List collections the delegate's mandate scopes can reach. */
|
|
173
|
+
listCollections(): Promise<readonly {
|
|
174
|
+
name: string;
|
|
175
|
+
schema: string;
|
|
176
|
+
record_count: number;
|
|
177
|
+
}[]>;
|
|
178
|
+
/** List gamma audit entries (read). */
|
|
179
|
+
listGammaEntries(opts?: {
|
|
180
|
+
limit?: number;
|
|
181
|
+
opPrefix?: string;
|
|
182
|
+
verify?: boolean;
|
|
183
|
+
}): Promise<unknown>;
|
|
105
184
|
/** Drop in-memory cache (CMK, collection metadata, …). */
|
|
106
185
|
reset(): void;
|
|
107
186
|
}
|
|
187
|
+
export interface ReadonlyDataCollection {
|
|
188
|
+
readonly name: string;
|
|
189
|
+
/** Fetch one record by id (decrypted client-side via the re-wrapped CMK). */
|
|
190
|
+
get(recordId: string): Promise<Record<string, unknown> | null>;
|
|
191
|
+
/** List records, decrypted. Pagination via opaque cursor. */
|
|
192
|
+
list(opts?: ListOpts): Promise<{
|
|
193
|
+
items: Record<string, unknown>[];
|
|
194
|
+
nextCursor?: string;
|
|
195
|
+
}>;
|
|
196
|
+
}
|
|
108
197
|
export interface DataCollection {
|
|
109
198
|
readonly name: string;
|
|
110
199
|
/**
|
|
@@ -150,4 +239,42 @@ export interface ListOpts {
|
|
|
150
239
|
readonly cursor?: string;
|
|
151
240
|
}
|
|
152
241
|
export declare function createDataClient(args: CreateDataClientArgs): DataClient;
|
|
242
|
+
export interface CreateDelegateDataClientArgs {
|
|
243
|
+
/** PDS base URL (same endpoint the owner writes to). */
|
|
244
|
+
readonly pdsUrl: string;
|
|
245
|
+
/** DID of the SUBJECT whose data is being read (the mandate issuer). */
|
|
246
|
+
readonly subjectDid: string;
|
|
247
|
+
/**
|
|
248
|
+
* The full signed mandate the subject granted to this delegate. Must
|
|
249
|
+
* carry a `data.<collection>.read` (or wider) scope and a
|
|
250
|
+
* `grantee.pubkey` matching `delegateSeed`.
|
|
251
|
+
*/
|
|
252
|
+
readonly mandate: SignedMandate;
|
|
253
|
+
/** The delegate's Ed25519 seed (32 bytes) — the grantee key the mandate
|
|
254
|
+
* is bound to. Used to sign envelopes AND to derive the X25519 key that
|
|
255
|
+
* unwraps the re-wrapped CMK. */
|
|
256
|
+
readonly delegateSeed: Uint8Array;
|
|
257
|
+
/**
|
|
258
|
+
* The delegate's Ed25519 public key, multibase-encoded. Defaults to
|
|
259
|
+
* `mandate.grantee.pubkey`. This is the bare verificationMethod the PDS
|
|
260
|
+
* binds the delegate envelope to.
|
|
261
|
+
*/
|
|
262
|
+
readonly granteePubkeyMultibase?: string;
|
|
263
|
+
/** App-defined (vendor) schemas, as for {@link createDataClient}. */
|
|
264
|
+
readonly schemas?: readonly AithosSchemaLite[];
|
|
265
|
+
/** `fetch` override (tests). */
|
|
266
|
+
readonly fetch?: typeof fetch;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Build a read-only data client that reads a subject's collections under
|
|
270
|
+
* a mandate (delegate path). The returned {@link ReadonlyDataClient}
|
|
271
|
+
* signs every request as the delegate (bare-multibase verificationMethod
|
|
272
|
+
* + the mandate attached to the envelope), and decrypts records using the
|
|
273
|
+
* CMK the owner re-wrapped for this delegate via
|
|
274
|
+
* {@link DataClient.authorizeDelegate}.
|
|
275
|
+
*
|
|
276
|
+
* Writes are not available on the returned type and throw `-32042` if
|
|
277
|
+
* forced.
|
|
278
|
+
*/
|
|
279
|
+
export declare function createDelegateDataClient(args: CreateDelegateDataClientArgs): ReadonlyDataClient;
|
|
153
280
|
//# sourceMappingURL=data.d.ts.map
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -27,6 +27,6 @@ export type { AudienceSet, AppCreditPackId, CreateAppTopupSessionArgs, CreateApp
|
|
|
27
27
|
export * as onboarding from "./onboarding.js";
|
|
28
28
|
export { createBrowserIdentity, browserIdentityFromStored, type BrowserIdentity, } from "@aithos/protocol-client";
|
|
29
29
|
export type { Section } from "@aithos/protocol-client";
|
|
30
|
-
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";
|
|
31
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";
|
|
32
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.js
CHANGED
|
@@ -68,7 +68,7 @@ export { createBrowserIdentity, browserIdentityFromStored, } from "@aithos/proto
|
|
|
68
68
|
// `sdk.data` namespace — Aithos data sub-protocol PDS client. Manages
|
|
69
69
|
// the lifecycle of subject-owned, encrypted, schema-validated records.
|
|
70
70
|
// See spec/data/ in the aithos-protocol repo.
|
|
71
|
-
export { createDataClient, } from "./data.js";
|
|
71
|
+
export { createDataClient, createDelegateDataClient, } from "./data.js";
|
|
72
72
|
// `sdk.assets` — Aithos assets sub-protocol PDS client. Upload,
|
|
73
73
|
// fetch, list, ref/unref binary content (images, PDFs, audio, video)
|
|
74
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,
|
package/dist/src/mandates.d.ts
CHANGED
|
@@ -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
|
package/dist/src/mandates.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aithos/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.45",
|
|
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",
|