@tokono.ma/firkin 0.1.1 → 0.3.0
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 +7 -3
- package/dist/client.d.ts +60 -20
- package/dist/client.js +77 -50
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rasl.d.ts +30 -0
- package/dist/rasl.js +107 -0
- package/dist/ucan.d.ts +21 -12
- package/dist/ucan.js +84 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,12 +46,13 @@ signs directly); delegates mint with [`invocation()`](#delegation--sessions).
|
|
|
46
46
|
## API
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
|
-
// blobs — reads are authenticated automatically (
|
|
49
|
+
// blobs — reads are authenticated automatically (read tokens)
|
|
50
50
|
rb.upload(data, { codec?, hash?: "sha256" | "blake3" }) → { cid, deduplicated }
|
|
51
51
|
rb.get(cid) → Uint8Array
|
|
52
52
|
rb.getRange(cid, { start?, end?, suffix? }) → { data, start, end, total }
|
|
53
53
|
|
|
54
54
|
// claims (multi-account sharing of one stored object)
|
|
55
|
+
rb.publishDelegation(d) // store a delegation so tokens can reference it by CID
|
|
55
56
|
rb.claim(cid) // attach an already-stored CID to your account
|
|
56
57
|
rb.delete(cid) // detach; bytes die with the last claimant
|
|
57
58
|
|
|
@@ -66,6 +67,7 @@ rb.pushFile(chunks) // ordered chunks → dag-cbor list root
|
|
|
66
67
|
rb.pullFile(root) // inverse of pushFile
|
|
67
68
|
rb.dagPush(root, blocks) // an existing DAG, as-is: [cid, bytes][] | Map
|
|
68
69
|
rb.dagPull(root) // Map<cid, bytes> — only the root is needed
|
|
70
|
+
rb.ingestCar(carBytes) // rootless CARv1: verify + claim each block (no DAG)
|
|
69
71
|
|
|
70
72
|
// account
|
|
71
73
|
rb.revoke({ cids? , all? }) // kill delegations (account key only)
|
|
@@ -93,15 +95,17 @@ with that key:
|
|
|
93
95
|
```ts
|
|
94
96
|
import { delegationPayloadBytes, invocation, delegate } from "@tokono.ma/firkin";
|
|
95
97
|
|
|
96
|
-
const
|
|
98
|
+
const delegation = await delegate(account, {
|
|
97
99
|
subject: account.did(), // rights holder (the account)
|
|
98
100
|
audience: session.did(), // recipient
|
|
99
101
|
command: "/firkin/blob/upload", // narrow prefixes only ever narrow
|
|
100
102
|
expireIn: 8 * 3600, // omit for an indefinite delegation
|
|
101
103
|
});
|
|
104
|
+
await accountClient.publishDelegation(delegation); // once — idempotent
|
|
102
105
|
|
|
106
|
+
// Tokens reference the proof by CID: no proof bytes on the wire.
|
|
103
107
|
const rb = new Firkin(base, async (command) =>
|
|
104
|
-
invocation(session, { command, proofs: [
|
|
108
|
+
invocation(session, { command, proofs: [delegation.cid], subject: account.did(), expireIn: 600 }),
|
|
105
109
|
);
|
|
106
110
|
|
|
107
111
|
// Revocation must be authorized by the account key — not the session:
|
package/dist/client.d.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import { CID } from "multiformats";
|
|
2
|
+
/** Wire version prefix for every API path (`/health` and `/.well-known/*`
|
|
3
|
+
* live outside it). */
|
|
4
|
+
export declare const API_PREFIX = "/v1";
|
|
2
5
|
import type { Signer } from "./keys.ts";
|
|
6
|
+
/** Per-call options a TokenSource may honor. */
|
|
7
|
+
export interface TokenOptions {
|
|
8
|
+
/** Override the token's expiry (seconds from now). */
|
|
9
|
+
expireIn?: number;
|
|
10
|
+
}
|
|
3
11
|
/** Supplies a fresh bearer token for a command. Mutating commands are
|
|
4
12
|
* single-use server-side — implementations MUST mint a new token per call
|
|
5
|
-
* (the default signer source does).
|
|
6
|
-
|
|
13
|
+
* (the default signer source does). `options.expireIn` is a hint used by
|
|
14
|
+
* `readUrl` to mint short-lived links; sources may ignore it. */
|
|
15
|
+
export type TokenSource = (command: string, options?: TokenOptions) => Promise<Uint8Array>;
|
|
7
16
|
/** Signer-backed source: mints a fresh direct invocation per request. */
|
|
8
17
|
export declare function signerToken(signer: Signer, expireIn?: number): TokenSource;
|
|
9
18
|
/** Fixed-token source: only appropriate for public/read-only usage. */
|
|
10
19
|
export declare function staticToken(token: Uint8Array): TokenSource;
|
|
20
|
+
export interface CarIngestResult {
|
|
21
|
+
/** Blocks found in the CAR. */
|
|
22
|
+
blocks: number;
|
|
23
|
+
/** Blocks written by this call. */
|
|
24
|
+
stored: number;
|
|
25
|
+
/** Blocks that were already stored (claim joined). */
|
|
26
|
+
already: number;
|
|
27
|
+
/** Total rejected blocks (the `failed` list may be capped). */
|
|
28
|
+
failed_total: number;
|
|
29
|
+
failed: {
|
|
30
|
+
cid: string;
|
|
31
|
+
error: string;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
11
34
|
export interface UploadResult {
|
|
12
35
|
cid: string;
|
|
13
36
|
/** True when the blob was already stored (dedup; claim joined). */
|
|
@@ -54,14 +77,36 @@ export declare class Firkin {
|
|
|
54
77
|
codec?: number;
|
|
55
78
|
hash?: "sha256" | "blake3";
|
|
56
79
|
}): Promise<UploadResult>;
|
|
57
|
-
/** Fetch a whole blob.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
80
|
+
/** Fetch a whole blob. Reads are authenticated: the server requires the
|
|
81
|
+
* `/firkin/blob/read` capability (an operator can opt into anonymous
|
|
82
|
+
* reads with `PUBLIC_READS=true`, in which case the header is ignored).
|
|
83
|
+
* Read tokens are reusable — reads are not replay-tracked. */
|
|
61
84
|
get(cid: string): Promise<Uint8Array>;
|
|
62
85
|
/** Fetch a byte range (206 responses; server-trusted verification).
|
|
63
86
|
* Authenticated like [`get`](#get). */
|
|
64
87
|
getRange(cid: string, range?: ByteRange): Promise<RangeResult>;
|
|
88
|
+
/** Publish a delegation to the server's delegation store so invocations
|
|
89
|
+
* can reference it by CID — the default token shape. Idempotent: the store
|
|
90
|
+
* dedups by CID, so publishing at session start is always safe. `token`
|
|
91
|
+
* bootstraps with an inline-proof token (`inlineInvocation`) when the
|
|
92
|
+
* caller's own chain is not stored yet. */
|
|
93
|
+
publishDelegation(minted: {
|
|
94
|
+
proof: Uint8Array;
|
|
95
|
+
cid: string;
|
|
96
|
+
}, opts?: {
|
|
97
|
+
token?: Uint8Array;
|
|
98
|
+
}): Promise<{
|
|
99
|
+
cid: string;
|
|
100
|
+
}>;
|
|
101
|
+
/** A short-lived, CID-scoped read URL — the presigned-URL equivalent for
|
|
102
|
+
* media elements (`<audio src>`, `<video src>`), which cannot set an
|
|
103
|
+
* Authorization header. The URL carries a `/firkin/blob/read/<cid>`
|
|
104
|
+
* invocation; the server accepts `?token=` on GET/HEAD only and, with
|
|
105
|
+
* `PUBLIC_READS=false`, enforces the CID narrowing — a leaked URL exposes
|
|
106
|
+
* just that blob until the token expires. Keep `expireIn` short (60s). */
|
|
107
|
+
readUrl(cid: string, opts?: {
|
|
108
|
+
expireIn?: number;
|
|
109
|
+
}): Promise<string>;
|
|
65
110
|
/** Attach an already-stored CID to the signer's account. */
|
|
66
111
|
claim(cid: string): Promise<void>;
|
|
67
112
|
/** Detach the signer's claim; bytes die with the last claimant. */
|
|
@@ -85,19 +130,6 @@ export declare class Firkin {
|
|
|
85
130
|
id: string;
|
|
86
131
|
spki: string;
|
|
87
132
|
}): Promise<void>;
|
|
88
|
-
/** (Account key) attest a session delegation with the bound passkey. */
|
|
89
|
-
attest(body: {
|
|
90
|
-
credential_id: string;
|
|
91
|
-
auth_data: string;
|
|
92
|
-
client_data: string;
|
|
93
|
-
signature: string;
|
|
94
|
-
delegation_cid: string;
|
|
95
|
-
}): Promise<void>;
|
|
96
|
-
/** (Account key) bind a WebAuthn credential to this account. */
|
|
97
|
-
bindCredential(credential: {
|
|
98
|
-
id: string;
|
|
99
|
-
spki: string;
|
|
100
|
-
}): Promise<void>;
|
|
101
133
|
/** Resolve a passkey assertion to its account DID — the login primitive
|
|
102
134
|
* for passkey-native accounts on a fresh browser. The client performs a
|
|
103
135
|
* discoverable-credentials WebAuthn get (no `allowCredentials`); the
|
|
@@ -124,6 +156,14 @@ export declare class Firkin {
|
|
|
124
156
|
registryDelete(name: string): Promise<void>;
|
|
125
157
|
/** The account's whole registry: name → CID (sorted). */
|
|
126
158
|
registryList(): Promise<Record<string, string>>;
|
|
159
|
+
/**
|
|
160
|
+
* Ingest a rootless CARv1 — "upload a `.car` file". Every block is verified
|
|
161
|
+
* against its own CID, policy-checked and claimed by the account; there is
|
|
162
|
+
* no DAG traversal and no root requirement (unlike [`dagPush`](#dagpush),
|
|
163
|
+
* which syncs one DAG over multiple rounds). Partial success is normal:
|
|
164
|
+
* `failed` lists the rejected blocks (digest mismatch, policy, size).
|
|
165
|
+
*/
|
|
166
|
+
ingestCar(car: Uint8Array): Promise<CarIngestResult>;
|
|
127
167
|
/**
|
|
128
168
|
* Push a flat file as an ordered list of chunks: each chunk is stored as
|
|
129
169
|
* a raw block, and a dag-cbor list of their CIDs becomes the root — the
|
|
@@ -173,7 +213,7 @@ export declare class Firkin {
|
|
|
173
213
|
*/
|
|
174
214
|
pullFile(root: string): Promise<Uint8Array>;
|
|
175
215
|
}
|
|
176
|
-
export declare function carFile(root: CID, entries: [CID, Uint8Array][]): Uint8Array;
|
|
216
|
+
export declare function carFile(root: CID | null, entries: [CID, Uint8Array][]): Uint8Array;
|
|
177
217
|
export declare function multihashSha256(data: Uint8Array): MultihashDigest;
|
|
178
218
|
type MultihashDigest = {
|
|
179
219
|
code: number;
|
package/dist/client.js
CHANGED
|
@@ -4,10 +4,13 @@ import { CID } from "multiformats";
|
|
|
4
4
|
import { sha256 } from "@noble/hashes/sha2";
|
|
5
5
|
import { cidForBytes, CODEC_DAG_CBOR, CODEC_DAG_PB, CODEC_RAW } from "./cid.js";
|
|
6
6
|
import { BloomFilter } from "./bloom.js";
|
|
7
|
+
/** Wire version prefix for every API path (`/health` and `/.well-known/*`
|
|
8
|
+
* live outside it). */
|
|
9
|
+
export const API_PREFIX = "/v1";
|
|
7
10
|
import { CMD, mintInvocation } from "./ucan.js";
|
|
8
11
|
/** Signer-backed source: mints a fresh direct invocation per request. */
|
|
9
12
|
export function signerToken(signer, expireIn = 600) {
|
|
10
|
-
return async (command) => mintInvocation(signer, { command, expireIn });
|
|
13
|
+
return async (command, options) => mintInvocation(signer, { command, expireIn: options?.expireIn ?? expireIn });
|
|
11
14
|
}
|
|
12
15
|
/** Fixed-token source: only appropriate for public/read-only usage. */
|
|
13
16
|
export function staticToken(token) {
|
|
@@ -54,7 +57,7 @@ export class Firkin {
|
|
|
54
57
|
cid = cidForBytes(data, opts);
|
|
55
58
|
body = new Blob([data]);
|
|
56
59
|
}
|
|
57
|
-
const res = await this.fetchImpl(`${this.base}/blob/${cid}`, {
|
|
60
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/blob/${cid}`, {
|
|
58
61
|
method: "PUT",
|
|
59
62
|
headers: {
|
|
60
63
|
Authorization: `Bearer ${toBase64Url(await this.token(CMD.blobUpload))}`,
|
|
@@ -67,12 +70,12 @@ export class Firkin {
|
|
|
67
70
|
const json = (await res.json());
|
|
68
71
|
return { cid, deduplicated: json.status === "already_stored" };
|
|
69
72
|
}
|
|
70
|
-
/** Fetch a whole blob.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
73
|
+
/** Fetch a whole blob. Reads are authenticated: the server requires the
|
|
74
|
+
* `/firkin/blob/read` capability (an operator can opt into anonymous
|
|
75
|
+
* reads with `PUBLIC_READS=true`, in which case the header is ignored).
|
|
76
|
+
* Read tokens are reusable — reads are not replay-tracked. */
|
|
74
77
|
async get(cid) {
|
|
75
|
-
const res = await this.fetchImpl(`${this.base}/blob/${cid}`, {
|
|
78
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/blob/${cid}`, {
|
|
76
79
|
headers: { Authorization: `Bearer ${toBase64Url(await this.token(CMD.blobRead))}` },
|
|
77
80
|
});
|
|
78
81
|
if (!res.ok)
|
|
@@ -82,7 +85,7 @@ export class Firkin {
|
|
|
82
85
|
/** Fetch a byte range (206 responses; server-trusted verification).
|
|
83
86
|
* Authenticated like [`get`](#get). */
|
|
84
87
|
async getRange(cid, range = {}) {
|
|
85
|
-
const res = await this.fetchImpl(`${this.base}/blob/${cid}`, {
|
|
88
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/blob/${cid}`, {
|
|
86
89
|
headers: {
|
|
87
90
|
Authorization: `Bearer ${toBase64Url(await this.token(CMD.blobRead))}`,
|
|
88
91
|
Range: rangeHeader(range),
|
|
@@ -102,10 +105,39 @@ export class Firkin {
|
|
|
102
105
|
};
|
|
103
106
|
}
|
|
104
107
|
// --- claims & deletion ------------------------------------------------------
|
|
108
|
+
/** Publish a delegation to the server's delegation store so invocations
|
|
109
|
+
* can reference it by CID — the default token shape. Idempotent: the store
|
|
110
|
+
* dedups by CID, so publishing at session start is always safe. `token`
|
|
111
|
+
* bootstraps with an inline-proof token (`inlineInvocation`) when the
|
|
112
|
+
* caller's own chain is not stored yet. */
|
|
113
|
+
async publishDelegation(minted, opts = {}) {
|
|
114
|
+
const token = opts.token ?? (await this.token(CMD.blobUpload));
|
|
115
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/delegation/${minted.cid}`, {
|
|
116
|
+
method: "PUT",
|
|
117
|
+
headers: { Authorization: `Bearer ${toBase64Url(token)}` },
|
|
118
|
+
body: new Blob([minted.proof]),
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok)
|
|
121
|
+
throw await ApiError.of(res);
|
|
122
|
+
const json = (await res.json());
|
|
123
|
+
return json;
|
|
124
|
+
}
|
|
125
|
+
/** A short-lived, CID-scoped read URL — the presigned-URL equivalent for
|
|
126
|
+
* media elements (`<audio src>`, `<video src>`), which cannot set an
|
|
127
|
+
* Authorization header. The URL carries a `/firkin/blob/read/<cid>`
|
|
128
|
+
* invocation; the server accepts `?token=` on GET/HEAD only and, with
|
|
129
|
+
* `PUBLIC_READS=false`, enforces the CID narrowing — a leaked URL exposes
|
|
130
|
+
* just that blob until the token expires. Keep `expireIn` short (60s). */
|
|
131
|
+
async readUrl(cid, opts = {}) {
|
|
132
|
+
const token = await this.token(`${CMD.blobRead}/${cid}`, {
|
|
133
|
+
expireIn: opts.expireIn ?? 60,
|
|
134
|
+
});
|
|
135
|
+
return `${this.base}${API_PREFIX}/blob/${cid}?token=${toBase64Url(token)}`;
|
|
136
|
+
}
|
|
105
137
|
/** Attach an already-stored CID to the signer's account. */
|
|
106
138
|
async claim(cid) {
|
|
107
139
|
const token = await this.token(CMD.blobClaim);
|
|
108
|
-
const res = await this.fetchImpl(`${this.base}/blob/${cid}/claim`, {
|
|
140
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/blob/${cid}/claim`, {
|
|
109
141
|
method: "POST",
|
|
110
142
|
headers: { Authorization: `Bearer ${toBase64Url(token)}` },
|
|
111
143
|
});
|
|
@@ -114,7 +146,7 @@ export class Firkin {
|
|
|
114
146
|
}
|
|
115
147
|
/** Detach the signer's claim; bytes die with the last claimant. */
|
|
116
148
|
async delete(cid) {
|
|
117
|
-
const res = await this.fetchImpl(`${this.base}/blob/${cid}`, {
|
|
149
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/blob/${cid}`, {
|
|
118
150
|
method: "DELETE",
|
|
119
151
|
headers: { Authorization: `Bearer ${toBase64Url(await this.token(CMD.blobDelete))}` },
|
|
120
152
|
});
|
|
@@ -128,7 +160,7 @@ export class Firkin {
|
|
|
128
160
|
* (passkey-native accounts; the server caps its lifetime at 10 minutes).
|
|
129
161
|
* A plain delegated session cannot revoke. */
|
|
130
162
|
async revoke(opts) {
|
|
131
|
-
const res = await this.fetchImpl(`${this.base}/account/revoke`, {
|
|
163
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/account/revoke`, {
|
|
132
164
|
method: "POST",
|
|
133
165
|
headers: {
|
|
134
166
|
Authorization: `Bearer ${toBase64Url(await this.token(CMD.accountRevoke))}`,
|
|
@@ -146,7 +178,7 @@ export class Firkin {
|
|
|
146
178
|
* other record of the credential↔account link. */
|
|
147
179
|
async register(did, credential) {
|
|
148
180
|
const token = await this.token(CMD.accountRegister);
|
|
149
|
-
const res = await this.fetchImpl(`${this.base}/accounts`, {
|
|
181
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/accounts`, {
|
|
150
182
|
method: "POST",
|
|
151
183
|
headers: {
|
|
152
184
|
Authorization: `Bearer ${toBase64Url(token)}`,
|
|
@@ -157,32 +189,6 @@ export class Firkin {
|
|
|
157
189
|
if (!res.ok)
|
|
158
190
|
throw await ApiError.of(res);
|
|
159
191
|
}
|
|
160
|
-
/** (Account key) attest a session delegation with the bound passkey. */
|
|
161
|
-
async attest(body) {
|
|
162
|
-
const res = await this.fetchImpl(`${this.base}/account/attest`, {
|
|
163
|
-
method: "POST",
|
|
164
|
-
headers: {
|
|
165
|
-
Authorization: `Bearer ${toBase64Url(await this.token(CMD.accountAttest))}`,
|
|
166
|
-
"Content-Type": "application/json",
|
|
167
|
-
},
|
|
168
|
-
body: JSON.stringify(body),
|
|
169
|
-
});
|
|
170
|
-
if (!res.ok)
|
|
171
|
-
throw await ApiError.of(res);
|
|
172
|
-
}
|
|
173
|
-
/** (Account key) bind a WebAuthn credential to this account. */
|
|
174
|
-
async bindCredential(credential) {
|
|
175
|
-
const res = await this.fetchImpl(`${this.base}/account/bind`, {
|
|
176
|
-
method: "POST",
|
|
177
|
-
headers: {
|
|
178
|
-
Authorization: `Bearer ${toBase64Url(await this.token(CMD.accountBind))}`,
|
|
179
|
-
"Content-Type": "application/json",
|
|
180
|
-
},
|
|
181
|
-
body: JSON.stringify({ credential }),
|
|
182
|
-
});
|
|
183
|
-
if (!res.ok)
|
|
184
|
-
throw await ApiError.of(res);
|
|
185
|
-
}
|
|
186
192
|
/** Resolve a passkey assertion to its account DID — the login primitive
|
|
187
193
|
* for passkey-native accounts on a fresh browser. The client performs a
|
|
188
194
|
* discoverable-credentials WebAuthn get (no `allowCredentials`); the
|
|
@@ -191,7 +197,7 @@ export class Firkin {
|
|
|
191
197
|
* the endpoint is unauthenticated by design: the verified signature IS
|
|
192
198
|
* the authentication. */
|
|
193
199
|
static async whoami(base, assertion, fetchImpl = globalThis.fetch) {
|
|
194
|
-
const res = await fetchImpl(`${base}/account/whoami`, {
|
|
200
|
+
const res = await fetchImpl(`${base}${API_PREFIX}/account/whoami`, {
|
|
195
201
|
method: "POST",
|
|
196
202
|
headers: { "Content-Type": "application/json" },
|
|
197
203
|
body: JSON.stringify({
|
|
@@ -208,7 +214,7 @@ export class Firkin {
|
|
|
208
214
|
}
|
|
209
215
|
/** (Admin) list registered accounts. */
|
|
210
216
|
async listAccounts() {
|
|
211
|
-
const res = await this.fetchImpl(`${this.base}/accounts`, {
|
|
217
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/accounts`, {
|
|
212
218
|
headers: {
|
|
213
219
|
Authorization: `Bearer ${toBase64Url(await this.token(CMD.accountList))}`,
|
|
214
220
|
},
|
|
@@ -224,7 +230,7 @@ export class Firkin {
|
|
|
224
230
|
// a resolved CID may be dangling (not yet uploaded or already GC'd).
|
|
225
231
|
/** Set `name` → `cid`. Overwrites silently (last-writer-wins). */
|
|
226
232
|
async registrySet(name, cid) {
|
|
227
|
-
const res = await this.fetchImpl(`${this.base}/registry/${encodeURIComponent(name)}`, {
|
|
233
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/registry/${encodeURIComponent(name)}`, {
|
|
228
234
|
method: "PUT",
|
|
229
235
|
headers: {
|
|
230
236
|
Authorization: `Bearer ${toBase64Url(await this.token(CMD.registryWrite))}`,
|
|
@@ -237,7 +243,7 @@ export class Firkin {
|
|
|
237
243
|
}
|
|
238
244
|
/** Resolve `name`; returns null when unset. */
|
|
239
245
|
async registryGet(name) {
|
|
240
|
-
const res = await this.fetchImpl(`${this.base}/registry/${encodeURIComponent(name)}`, {
|
|
246
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/registry/${encodeURIComponent(name)}`, {
|
|
241
247
|
headers: { Authorization: `Bearer ${toBase64Url(await this.token(CMD.registryRead))}` },
|
|
242
248
|
});
|
|
243
249
|
if (res.status === 404)
|
|
@@ -249,7 +255,7 @@ export class Firkin {
|
|
|
249
255
|
}
|
|
250
256
|
/** Unset `name` (idempotent). */
|
|
251
257
|
async registryDelete(name) {
|
|
252
|
-
const res = await this.fetchImpl(`${this.base}/registry/${encodeURIComponent(name)}`, {
|
|
258
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/registry/${encodeURIComponent(name)}`, {
|
|
253
259
|
method: "DELETE",
|
|
254
260
|
headers: { Authorization: `Bearer ${toBase64Url(await this.token(CMD.registryWrite))}` },
|
|
255
261
|
});
|
|
@@ -258,7 +264,7 @@ export class Firkin {
|
|
|
258
264
|
}
|
|
259
265
|
/** The account's whole registry: name → CID (sorted). */
|
|
260
266
|
async registryList() {
|
|
261
|
-
const res = await this.fetchImpl(`${this.base}/registry`, {
|
|
267
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/registry`, {
|
|
262
268
|
headers: { Authorization: `Bearer ${toBase64Url(await this.token(CMD.registryRead))}` },
|
|
263
269
|
});
|
|
264
270
|
if (!res.ok)
|
|
@@ -267,6 +273,26 @@ export class Firkin {
|
|
|
267
273
|
return json.names;
|
|
268
274
|
}
|
|
269
275
|
// --- CAR Mirror batch sync ---------------------------------------------------
|
|
276
|
+
/**
|
|
277
|
+
* Ingest a rootless CARv1 — "upload a `.car` file". Every block is verified
|
|
278
|
+
* against its own CID, policy-checked and claimed by the account; there is
|
|
279
|
+
* no DAG traversal and no root requirement (unlike [`dagPush`](#dagpush),
|
|
280
|
+
* which syncs one DAG over multiple rounds). Partial success is normal:
|
|
281
|
+
* `failed` lists the rejected blocks (digest mismatch, policy, size).
|
|
282
|
+
*/
|
|
283
|
+
async ingestCar(car) {
|
|
284
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/car`, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: {
|
|
287
|
+
Authorization: `Bearer ${toBase64Url(await this.token(CMD.blobUpload))}`,
|
|
288
|
+
"Content-Type": "application/vnd.ipld.car",
|
|
289
|
+
},
|
|
290
|
+
body: new Blob([car]),
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok)
|
|
293
|
+
throw await ApiError.of(res);
|
|
294
|
+
return (await res.json());
|
|
295
|
+
}
|
|
270
296
|
/**
|
|
271
297
|
* Push a flat file as an ordered list of chunks: each chunk is stored as
|
|
272
298
|
* a raw block, and a dag-cbor list of their CIDs becomes the root — the
|
|
@@ -363,7 +389,7 @@ export class Firkin {
|
|
|
363
389
|
throw new Error("push round has no blocks");
|
|
364
390
|
const first = send[0];
|
|
365
391
|
const car = carFile(first === rootCid.toString() ? rootCid : CID.parse(first), send.map((c) => [CID.parse(c), all.get(c)]));
|
|
366
|
-
const res = await this.fetchImpl(`${this.base}/dag/push/${rootCid}`, {
|
|
392
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/dag/push/${rootCid}`, {
|
|
367
393
|
method: "POST",
|
|
368
394
|
headers: {
|
|
369
395
|
Authorization: `Bearer ${toBase64Url(await this.token(CMD.blobUpload))}`,
|
|
@@ -418,7 +444,7 @@ export class Firkin {
|
|
|
418
444
|
};
|
|
419
445
|
if (body)
|
|
420
446
|
headers["Content-Type"] = "application/vnd.ipld.dag-cbor";
|
|
421
|
-
const res = await this.fetchImpl(`${this.base}/dag/pull/${root}`, {
|
|
447
|
+
const res = await this.fetchImpl(`${this.base}${API_PREFIX}/dag/pull/${root}`, {
|
|
422
448
|
method: "POST",
|
|
423
449
|
headers,
|
|
424
450
|
...(body ? { body: new Blob([body]) } : {}),
|
|
@@ -493,10 +519,11 @@ function rangeHeader(range) {
|
|
|
493
519
|
}
|
|
494
520
|
// --- CARv1 framing -----------------------------------------------------------
|
|
495
521
|
export function carFile(root, entries) {
|
|
496
|
-
// Header roots = the DAG root
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
|
|
522
|
+
// Header roots = the DAG root (`null` for a rootless bag — the DASL CAR
|
|
523
|
+
// format allows an empty roots array); entries = exactly the blocks being
|
|
524
|
+
// sent this round. Never repeat a block the server already has: the
|
|
525
|
+
// receiver interrupts the round at the first Have block.
|
|
526
|
+
const header = dagcbor.encode({ roots: root ? [root] : [], version: 1 });
|
|
500
527
|
const parts = [uvarintFrame(header.length), header];
|
|
501
528
|
for (const [cid, block] of entries) {
|
|
502
529
|
const entry = new Uint8Array(cid.bytes.length + block.length);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/rasl.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface RaslUrl {
|
|
2
|
+
/** The DASL/BDASL CID (the URL's authority). */
|
|
3
|
+
cid: string;
|
|
4
|
+
/** Hosts that may serve it (`?hint=` values, invalid ones dropped). */
|
|
5
|
+
hints: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Build a `rasl://` URL for a CID and optional hint hosts. */
|
|
8
|
+
export declare function raslUrl(cid: string, hints?: string[]): string;
|
|
9
|
+
/** Parse a `rasl://` URL per the spec: the host is the CID, `hint` entries
|
|
10
|
+
* are the (valid) hint hosts; everything else, including the path, is
|
|
11
|
+
* ignored. */
|
|
12
|
+
export declare function parseRasl(url: string): RaslUrl;
|
|
13
|
+
/** Check that `bytes` hash to `cid` (SHA-256 and BLAKE3 multihashes).
|
|
14
|
+
* Compares the digest, so CIDv0/CIDv1 spelling doesn't matter. */
|
|
15
|
+
export declare function verifyCid(cid: string, bytes: Uint8Array): boolean;
|
|
16
|
+
export interface ResolveOptions {
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
/** Cancels the whole resolution (including in-flight hint requests). */
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a `rasl://` URL: request every hint's
|
|
23
|
+
* `https://<hint>/.well-known/rasl/<cid>` in parallel (statelessly), take the
|
|
24
|
+
* first response whose bytes hash to the CID. Failing or mismatching hints
|
|
25
|
+
* are skipped; if none verify, it throws.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveRasl(url: string, opts?: ResolveOptions): Promise<{
|
|
28
|
+
cid: string;
|
|
29
|
+
bytes: Uint8Array;
|
|
30
|
+
}>;
|
package/dist/rasl.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// RASL (Retrieval of Arbitrary Structures & Links) — https://dasl.ing/rasl.html
|
|
2
|
+
//
|
|
3
|
+
// A `rasl://<cid>/?hint=<host>&hint=<host>` URL names a CID plus optional
|
|
4
|
+
// places to fetch it. Retrieval is stateless (no cookies, no credentials —
|
|
5
|
+
// the hash is the authority) and self-verifying: whatever comes back must
|
|
6
|
+
// hash to the CID.
|
|
7
|
+
import { sha256 } from "@noble/hashes/sha2";
|
|
8
|
+
import { blake3 } from "@noble/hashes/blake3";
|
|
9
|
+
import { CID } from "multiformats";
|
|
10
|
+
import { HASH_BLAKE3, HASH_SHA256 } from "./cid.js";
|
|
11
|
+
/** A valid `https` host: DNS labels + optional port (IPv6 literals too). */
|
|
12
|
+
const HOST_RE = /^(\[[0-9a-fA-F:]+\]|[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*)(:\d{1,5})?$/;
|
|
13
|
+
/** Build a `rasl://` URL for a CID and optional hint hosts. */
|
|
14
|
+
export function raslUrl(cid, hints = []) {
|
|
15
|
+
const query = hints.map((hint) => `hint=${encodeURIComponent(hint)}`).join("&");
|
|
16
|
+
return `rasl://${cid}/${query ? `?${query}` : ""}`;
|
|
17
|
+
}
|
|
18
|
+
/** Parse a `rasl://` URL per the spec: the host is the CID, `hint` entries
|
|
19
|
+
* are the (valid) hint hosts; everything else, including the path, is
|
|
20
|
+
* ignored. */
|
|
21
|
+
export function parseRasl(url) {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
if (parsed.protocol !== "rasl:") {
|
|
24
|
+
throw new Error(`not a rasl:// URL: ${url}`);
|
|
25
|
+
}
|
|
26
|
+
const cid = parsed.host;
|
|
27
|
+
if (!cid)
|
|
28
|
+
throw new Error(`rasl:// URL lacks a CID: ${url}`);
|
|
29
|
+
CID.parse(cid); // must be a well-formed CID
|
|
30
|
+
const hints = parsed.searchParams
|
|
31
|
+
.getAll("hint")
|
|
32
|
+
.map((hint) => hint.trim())
|
|
33
|
+
.filter((hint) => HOST_RE.test(hint));
|
|
34
|
+
return { cid, hints };
|
|
35
|
+
}
|
|
36
|
+
/** Check that `bytes` hash to `cid` (SHA-256 and BLAKE3 multihashes).
|
|
37
|
+
* Compares the digest, so CIDv0/CIDv1 spelling doesn't matter. */
|
|
38
|
+
export function verifyCid(cid, bytes) {
|
|
39
|
+
const parsed = CID.parse(cid);
|
|
40
|
+
const digest = parsed.multihash.code === HASH_SHA256
|
|
41
|
+
? sha256(bytes)
|
|
42
|
+
: parsed.multihash.code === HASH_BLAKE3
|
|
43
|
+
? blake3(bytes)
|
|
44
|
+
: undefined;
|
|
45
|
+
if (!digest || digest.length !== parsed.multihash.size)
|
|
46
|
+
return false;
|
|
47
|
+
return parsed.multihash.digest.every((byte, i) => byte === digest[i]);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a `rasl://` URL: request every hint's
|
|
51
|
+
* `https://<hint>/.well-known/rasl/<cid>` in parallel (statelessly), take the
|
|
52
|
+
* first response whose bytes hash to the CID. Failing or mismatching hints
|
|
53
|
+
* are skipped; if none verify, it throws.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolveRasl(url, opts = {}) {
|
|
56
|
+
const { cid, hints } = parseRasl(url);
|
|
57
|
+
if (hints.length === 0) {
|
|
58
|
+
throw new Error("rasl:// URL has no usable hints");
|
|
59
|
+
}
|
|
60
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const signals = [controller.signal];
|
|
63
|
+
if (opts.signal) {
|
|
64
|
+
if (opts.signal.aborted)
|
|
65
|
+
throw new Error("rasl resolution aborted");
|
|
66
|
+
opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
67
|
+
signals.push(opts.signal);
|
|
68
|
+
}
|
|
69
|
+
const attempts = hints.map(async (hint) => {
|
|
70
|
+
const res = await fetchImpl(`https://${hint}/.well-known/rasl/${cid}`, {
|
|
71
|
+
method: "GET",
|
|
72
|
+
// RASL is stateless: no cookies, no credentials, no content negotiation.
|
|
73
|
+
credentials: "omit",
|
|
74
|
+
headers: { Accept: "*/*" },
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`${hint}: HTTP ${res.status}`);
|
|
79
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
80
|
+
if (!verifyCid(cid, bytes))
|
|
81
|
+
throw new Error(`${hint}: bytes do not hash to ${cid}`);
|
|
82
|
+
return { cid, bytes };
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
return await firstSuccess(attempts);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
controller.abort(); // stop the losers
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The first fulfilled promise; rejects when all fail. */
|
|
92
|
+
async function firstSuccess(promises) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
let pending = promises.length;
|
|
95
|
+
const errors = [];
|
|
96
|
+
for (const promise of promises) {
|
|
97
|
+
promise.then(resolve, (error) => {
|
|
98
|
+
errors.push(error);
|
|
99
|
+
if (--pending === 0) {
|
|
100
|
+
reject(new Error(`RASL retrieval failed for all hints: ${errors
|
|
101
|
+
.map((e) => (e instanceof Error ? e.message : String(e)))
|
|
102
|
+
.join("; ")}`));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
package/dist/ucan.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CID } from "multiformats";
|
|
1
2
|
import type { UcanSigner } from "./keys.ts";
|
|
2
3
|
export declare const INV_TAG = "ucan/inv@1.0.0-rc.1";
|
|
3
4
|
export declare const DLG_TAG = "ucan/dlg@1.0.0-rc.1";
|
|
@@ -20,8 +21,6 @@ export declare const CMD: {
|
|
|
20
21
|
readonly accountList: "/firkin/account/list";
|
|
21
22
|
readonly accountRevoke: "/firkin/account/revoke";
|
|
22
23
|
readonly accountRecover: "/firkin/account/recover";
|
|
23
|
-
readonly accountAttest: "/firkin/account/attest";
|
|
24
|
-
readonly accountBind: "/firkin/account/bind";
|
|
25
24
|
readonly registryRead: "/firkin/registry/read";
|
|
26
25
|
readonly registryWrite: "/firkin/registry/write";
|
|
27
26
|
};
|
|
@@ -41,23 +40,34 @@ export interface DelegationOptions extends MintOptions {
|
|
|
41
40
|
/** Mint a direct invocation token (issuer == subject == the signer's DID). */
|
|
42
41
|
export declare function mintInvocation(signer: UcanSigner, opts: MintOptions): Promise<Uint8Array>;
|
|
43
42
|
/**
|
|
44
|
-
* Mint a delegated invocation token
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* Mint a delegated invocation token whose proofs travel **by reference**:
|
|
44
|
+
* the container carries only the invocation, and its `prf` names the CIDs of
|
|
45
|
+
* delegations previously published to the server's delegation store
|
|
46
|
+
* (`Firkin.publishDelegation`). This keeps tokens small regardless of chain
|
|
47
|
+
* depth or assertion size — a passkey session's read URL is ~700 B instead of
|
|
48
|
+
* ~1.7 kB.
|
|
48
49
|
*
|
|
49
|
-
* A proof
|
|
50
|
-
* cid}`
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* A proof input may carry its own bytes (`delegate`/`assertionToProof` return
|
|
51
|
+
* `{proof, cid}`), which are used locally only — to discover the chain
|
|
52
|
+
* subject and to narrow the command — and are never put on the wire. With a
|
|
53
|
+
* bare CID, pass `subject` (the account DID) yourself.
|
|
53
54
|
*/
|
|
54
|
-
type ProofInput =
|
|
55
|
+
export type ProofInput = string | CID | {
|
|
55
56
|
bytes?: Uint8Array;
|
|
56
57
|
proof?: Uint8Array;
|
|
57
58
|
cid: string;
|
|
58
59
|
};
|
|
59
60
|
export declare function invocation(signer: UcanSigner, opts: MintOptions & {
|
|
60
61
|
proofs: ProofInput[];
|
|
62
|
+
subject?: string;
|
|
63
|
+
}): Promise<Uint8Array>;
|
|
64
|
+
/**
|
|
65
|
+
* Mint an invocation **with its proofs inline** — bootstrap only: the
|
|
66
|
+
* delegation store's publish call is authorized by a chain that is not stored
|
|
67
|
+
* yet. Everything else should use [`invocation`](#invocation).
|
|
68
|
+
*/
|
|
69
|
+
export declare function inlineInvocation(signer: UcanSigner, opts: MintOptions & {
|
|
70
|
+
proofs: ProofInput[];
|
|
61
71
|
}): Promise<Uint8Array>;
|
|
62
72
|
export interface MintedDelegation {
|
|
63
73
|
/** The proof container (DAG-CBOR bytes) the delegate attaches to invocations. */
|
|
@@ -90,4 +100,3 @@ export declare function delegate(signer: UcanSigner, opts: DelegationOptions): P
|
|
|
90
100
|
export declare function parseContainer(container: Uint8Array): unknown[];
|
|
91
101
|
/** Decode a container's invocation payload (diagnostics / verification). */
|
|
92
102
|
export declare function decodeInvocation(container: Uint8Array): Record<string, unknown>;
|
|
93
|
-
export {};
|
package/dist/ucan.js
CHANGED
|
@@ -46,8 +46,6 @@ export const CMD = {
|
|
|
46
46
|
accountList: "/firkin/account/list",
|
|
47
47
|
accountRevoke: "/firkin/account/revoke",
|
|
48
48
|
accountRecover: "/firkin/account/recover",
|
|
49
|
-
accountAttest: "/firkin/account/attest",
|
|
50
|
-
accountBind: "/firkin/account/bind",
|
|
51
49
|
registryRead: "/firkin/registry/read",
|
|
52
50
|
registryWrite: "/firkin/registry/write",
|
|
53
51
|
};
|
|
@@ -85,33 +83,37 @@ export async function mintInvocation(signer, opts) {
|
|
|
85
83
|
return dagcbor.encode([invocation]);
|
|
86
84
|
}
|
|
87
85
|
export async function invocation(signer, opts) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const decoded = opts.proofs.map((
|
|
92
|
-
const
|
|
86
|
+
if (opts.proofs.length === 0) {
|
|
87
|
+
throw new Error("delegated invocation needs at least one proof");
|
|
88
|
+
}
|
|
89
|
+
const decoded = opts.proofs.map((input) => {
|
|
90
|
+
const cid = proofInputCid(input);
|
|
91
|
+
const bytes = proofInputBytes(input);
|
|
93
92
|
if (!bytes)
|
|
94
|
-
|
|
93
|
+
return { cid, payload: undefined };
|
|
95
94
|
const d = dagcbor.decode(bytes);
|
|
96
95
|
if (Array.isArray(d) &&
|
|
97
96
|
d.length === 2 &&
|
|
98
97
|
d[1] instanceof Object &&
|
|
99
98
|
Object.keys(d[1]).some((k) => k.startsWith("ucan/dlg@"))) {
|
|
100
99
|
const payload = d[1][Object.keys(d[1]).find((k) => k.startsWith("ucan/dlg@"))];
|
|
101
|
-
return {
|
|
100
|
+
return { cid, payload };
|
|
102
101
|
}
|
|
103
102
|
if (d instanceof Object && "ad" in d && "cd" in d && "sg" in d) {
|
|
104
103
|
// Assertion proof: the dlg payload rides in the challenge.
|
|
105
104
|
const cd = JSON.parse(d.cd);
|
|
106
105
|
const payload = dagcbor.decode(base64UrlDecode(cd.challenge));
|
|
107
|
-
return {
|
|
106
|
+
return { cid, payload };
|
|
108
107
|
}
|
|
109
108
|
throw new Error("unrecognized proof format");
|
|
110
109
|
});
|
|
111
110
|
const first = decoded[0];
|
|
112
111
|
if (!first)
|
|
113
112
|
throw new Error("delegated invocation needs at least one proof");
|
|
114
|
-
const subject = first.payload.
|
|
113
|
+
const subject = first.payload?.sub ?? opts.subject;
|
|
114
|
+
if (!subject) {
|
|
115
|
+
throw new Error("unknown chain subject: pass `subject` (the account DID) when proofs are referenced by CID only");
|
|
116
|
+
}
|
|
115
117
|
const payload = invocationPayload(signer, opts);
|
|
116
118
|
payload.aud = subject;
|
|
117
119
|
payload.sub = subject;
|
|
@@ -120,19 +122,80 @@ export async function invocation(signer, opts) {
|
|
|
120
122
|
// so `/firkin/blob/delete/<cid>` proofs can't be invoked with the bare
|
|
121
123
|
// `/firkin/blob/delete`. Narrow down to the proof's own command when it
|
|
122
124
|
// extends (never broadens) the requested one.
|
|
123
|
-
const dlgCmd = first.payload
|
|
124
|
-
if (dlgCmd !== opts.command && dlgCmd.startsWith(opts.command)) {
|
|
125
|
+
const dlgCmd = first.payload?.cmd;
|
|
126
|
+
if (dlgCmd && dlgCmd !== opts.command && dlgCmd.startsWith(opts.command)) {
|
|
125
127
|
payload.cmd = dlgCmd;
|
|
126
128
|
}
|
|
127
|
-
payload.prf = decoded.map((p
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
payload.prf = decoded.map((p) => p.cid);
|
|
130
|
+
const inv = await envelope(signer, INV_TAG, payload);
|
|
131
|
+
// By reference: no proof envelopes ride along.
|
|
132
|
+
return dagcbor.encode([inv]);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Mint an invocation **with its proofs inline** — bootstrap only: the
|
|
136
|
+
* delegation store's publish call is authorized by a chain that is not stored
|
|
137
|
+
* yet. Everything else should use [`invocation`](#invocation).
|
|
138
|
+
*/
|
|
139
|
+
export async function inlineInvocation(signer, opts) {
|
|
140
|
+
const decoded = opts.proofs.map((input) => {
|
|
141
|
+
const bytes = proofInputBytes(input);
|
|
142
|
+
if (!bytes)
|
|
143
|
+
throw new Error("inline proofs need their bytes");
|
|
144
|
+
const d = dagcbor.decode(bytes);
|
|
145
|
+
if (Array.isArray(d) &&
|
|
146
|
+
d.length === 2 &&
|
|
147
|
+
d[1] instanceof Object &&
|
|
148
|
+
Object.keys(d[1]).some((k) => k.startsWith("ucan/dlg@"))) {
|
|
149
|
+
return d;
|
|
150
|
+
}
|
|
151
|
+
if (d instanceof Object && "ad" in d && "cd" in d && "sg" in d) {
|
|
152
|
+
return d;
|
|
153
|
+
}
|
|
154
|
+
throw new Error("unrecognized proof format");
|
|
133
155
|
});
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
// The chain subject comes from the proof (`sub`), not from the signer.
|
|
157
|
+
const firstPayload = (() => {
|
|
158
|
+
const d = decoded[0];
|
|
159
|
+
if (Array.isArray(d) && d[1] instanceof Object) {
|
|
160
|
+
const tag = Object.keys(d[1]).find((key) => key.startsWith("ucan/dlg@"));
|
|
161
|
+
if (tag)
|
|
162
|
+
return d[1][tag];
|
|
163
|
+
}
|
|
164
|
+
if (d instanceof Object && "ad" in d && "cd" in d) {
|
|
165
|
+
const cd = JSON.parse(d.cd);
|
|
166
|
+
return dagcbor.decode(base64UrlDecode(cd.challenge));
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
})();
|
|
170
|
+
const subject = firstPayload?.sub;
|
|
171
|
+
const payload = invocationPayload(signer, opts);
|
|
172
|
+
if (subject) {
|
|
173
|
+
payload.aud = subject;
|
|
174
|
+
payload.sub = subject;
|
|
175
|
+
}
|
|
176
|
+
// Assertion proofs carry their synthesized-envelope CID from the caller —
|
|
177
|
+
// hashing the `{ad, cd, sg}` bytes would name the wrong envelope.
|
|
178
|
+
payload.prf = opts.proofs.map((input) => proofInputCid(input));
|
|
179
|
+
const inv = await envelope(signer, INV_TAG, payload);
|
|
180
|
+
return dagcbor.encode([inv, ...decoded]);
|
|
181
|
+
}
|
|
182
|
+
function proofInputCid(input) {
|
|
183
|
+
if (typeof input === "string")
|
|
184
|
+
return CID.parse(input);
|
|
185
|
+
const direct = CID.asCID(input);
|
|
186
|
+
if (direct)
|
|
187
|
+
return direct;
|
|
188
|
+
if (input instanceof Object && "cid" in input)
|
|
189
|
+
return CID.parse(input.cid);
|
|
190
|
+
throw new Error("proof input lacks a CID");
|
|
191
|
+
}
|
|
192
|
+
function proofInputBytes(input) {
|
|
193
|
+
if (input instanceof Uint8Array)
|
|
194
|
+
return input;
|
|
195
|
+
if (typeof input === "string" || !(input instanceof Object))
|
|
196
|
+
return undefined;
|
|
197
|
+
const holder = input;
|
|
198
|
+
return holder.bytes ?? holder.proof;
|
|
136
199
|
}
|
|
137
200
|
function base64UrlDecode(text) {
|
|
138
201
|
const padded = text.replace(/-/g, "+").replace(/_/g, "/");
|
package/package.json
CHANGED