@tokono.ma/firkin 0.2.0 → 0.4.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/dist/client.d.ts CHANGED
@@ -1,5 +1,10 @@
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
+ import { type WebAuthnAssertion } from "./webauthn.ts";
7
+ import type { PlcAttestationMaterial } from "./plc.ts";
3
8
  /** Per-call options a TokenSource may honor. */
4
9
  export interface TokenOptions {
5
10
  /** Override the token's expiry (seconds from now). */
@@ -12,8 +17,150 @@ export interface TokenOptions {
12
17
  export type TokenSource = (command: string, options?: TokenOptions) => Promise<Uint8Array>;
13
18
  /** Signer-backed source: mints a fresh direct invocation per request. */
14
19
  export declare function signerToken(signer: Signer, expireIn?: number): TokenSource;
20
+ /**
21
+ * Source for an **attested session**: every invocation carries
22
+ * `meta["firkin/account"]`, the hint the server uses to resolve the
23
+ * session → account binding. The session key signs directly (no proofs); the
24
+ * binding, not the token, carries the account's authority.
25
+ *
26
+ * Obtain the binding with a passkey: `attestationChallenge(...)` →
27
+ * `credentials.get` → `Firkin.attest(...)`.
28
+ */
29
+ export declare function sessionTokenSource(signer: Signer, opts: {
30
+ account: string;
31
+ expireIn?: number;
32
+ }): TokenSource;
15
33
  /** Fixed-token source: only appropriate for public/read-only usage. */
16
34
  export declare function staticToken(token: Uint8Array): TokenSource;
35
+ export interface CarIngestResult {
36
+ /** Blocks found in the CAR. */
37
+ blocks: number;
38
+ /** Blocks written by this call. */
39
+ stored: number;
40
+ /** Blocks that were already stored (claim joined). */
41
+ already: number;
42
+ /** Total rejected blocks (the `failed` list may be capped). */
43
+ failed_total: number;
44
+ failed: {
45
+ cid: string;
46
+ error: string;
47
+ }[];
48
+ }
49
+ /** One page of a tag group. */
50
+ export interface TagPage {
51
+ name: string;
52
+ cids: string[];
53
+ count: number;
54
+ cursor: number;
55
+ /** Next page cursor, or null when this was the last page. */
56
+ next: number | null;
57
+ }
58
+ /** Result of `POST /v1/tokens/attestation`. */
59
+ export type AttestationResult = SessionAttestation | RecoveryAttestation;
60
+ /** A session binding (passkey or `did:plc`). */
61
+ export interface SessionAttestation {
62
+ kind: "session";
63
+ /** Content-addressed attestation id — revoke it with `revoke({cids})`. */
64
+ cid: string;
65
+ account: string;
66
+ /** The bound `did:key` that signs API calls. */
67
+ session: string;
68
+ commands: string[];
69
+ nbf: number;
70
+ exp: number;
71
+ }
72
+ /** A recovery grant: `recovery` may call `POST /v1/account/recover`. */
73
+ export interface RecoveryAttestation {
74
+ kind: "recovery";
75
+ cid: string;
76
+ account: string;
77
+ /** The DID authorised to recover the account. */
78
+ recovery: string;
79
+ /** Unix seconds; null for an indefinite grant. */
80
+ exp: number | null;
81
+ }
82
+ /**
83
+ * Canonicalize an instance origin: scheme + host + optional port, lowercased,
84
+ * with no trailing slash, path, query, fragment or userinfo.
85
+ *
86
+ * This matters twice over. Firkin compares the `firkin` audience field of its
87
+ * own records to `ORIGIN` exactly, and the ATProto indexers that can discover
88
+ * appeals — Constellation backlinks and Spacedust links — match URLs
89
+ * byte-for-byte, so `https://Firkin.example/` and `https://firkin.example`
90
+ * would be two unrelated values to them.
91
+ *
92
+ * Throws on a string that is not an absolute URL (e.g. a bare host).
93
+ */
94
+ export declare function normalizeOrigin(origin: string): string;
95
+ /** NSID of the access appeal an operator's firehose consumer reads.
96
+ * See `lexicons/ma.tokono.firkin.appeal.json`. */
97
+ export declare const APPEAL_COLLECTION = "ma.tokono.firkin.appeal";
98
+ /** An appeal to be registered on one Firkin instance. */
99
+ export interface AppealRequest {
100
+ $type: "ma.tokono.firkin.appeal";
101
+ /** The target instance's canonical origin, e.g. `https://firkin.example`
102
+ * (see `normalizeOrigin`). */
103
+ firkin: string;
104
+ /** ISO 8601 timestamp. */
105
+ createdAt: string;
106
+ /** Optional ISO 8601 timestamp after which the appeal is void. */
107
+ expiresAt?: string;
108
+ /** Optional opaque correlation value. */
109
+ nonce?: string;
110
+ /** Optional message for the operator. PUBLIC — never put secrets here. */
111
+ note?: string;
112
+ }
113
+ /**
114
+ * Build an access appeal to write into the requesting account's repo.
115
+ *
116
+ * Firkin's HTTP API is admin-gated; this record is how an account asks an
117
+ * operator to register it. An operator-side consumer tails the ATProto
118
+ * firehose, filters this collection, and applies its own policy. Nothing here
119
+ * grants service — the commit signature only proves who appealed.
120
+ *
121
+ * `firkin` is normalized with `normalizeOrigin`, because an operator-side
122
+ * consumer (and any indexer in front of it) matches the value against its own
123
+ * origin exactly.
124
+ *
125
+ * Use `appealRkey(firkin)` as the record key.
126
+ */
127
+ export declare function appealRequest(opts: {
128
+ firkin: string;
129
+ /** Seconds until `expiresAt` (omit for no expiry). */
130
+ expireIn?: number;
131
+ nonce?: string;
132
+ note?: string;
133
+ /** Override the creation time (unix seconds) — tests only. */
134
+ now?: number;
135
+ }): AppealRequest;
136
+ /** The conventional record key for an appeal: the instance host
137
+ * (e.g. `firkin.example`), which is idempotent and a valid rkey. */
138
+ export declare function appealRkey(firkin: string): string;
139
+ /** A did:plc session record (the value written into the account's repo). */
140
+ export interface PlcSessionRecord {
141
+ $type: "ma.tokono.firkin.session";
142
+ /** The API origin, matching server config (see `normalizeOrigin`). */
143
+ firkin: string;
144
+ /** The `did:key` session that will sign API calls. */
145
+ session: string;
146
+ /** Capability prefixes the session may exercise. */
147
+ commands: string[];
148
+ /** Unix seconds. */
149
+ nbf: number;
150
+ /** Unix seconds; the server caps `exp - nbf`. */
151
+ exp: number;
152
+ }
153
+ /** The record a did:plc account writes into its repo to attest a session
154
+ * (MST key `ma.tokono.firkin.session/<session did>`). Write it once with your
155
+ * ATProto agent, then submit the commit + MST path via `Firkin.attestPlc`. */
156
+ export declare function plcSessionRecord(opts: {
157
+ /** The API origin, matching server config (normalized). */
158
+ firkin: string;
159
+ session: string;
160
+ commands: string[];
161
+ expireIn?: number;
162
+ now?: number;
163
+ }): PlcSessionRecord;
17
164
  export interface UploadResult {
18
165
  cid: string;
19
166
  /** True when the blob was already stored (dedup; claim joined). */
@@ -55,16 +202,27 @@ export declare class Firkin {
55
202
  private readonly fetchImpl;
56
203
  private readonly blockCache;
57
204
  constructor(base: string, token: TokenSource, fetchImpl?: typeof fetch, blockCache?: BlockCache);
58
- /** Upload bytes; returns the CID and whether it was already stored. */
205
+ /** Upload bytes; returns the CID and whether it was already stored.
206
+ * `tags` is client-side sugar: after the bytes are stored (or deduped), the
207
+ * CID is added to each group with `tagAdd`. Tagging is a separate call, so
208
+ * if it fails the blob is already stored and `tagAdd` can be retried. */
59
209
  upload(data: Uint8Array | Blob, opts?: {
60
210
  codec?: number;
61
211
  hash?: "sha256" | "blake3";
212
+ tags?: string | string[];
62
213
  }): Promise<UploadResult>;
63
- /** Fetch a whole blob. Authenticated reads: the server's reads are public
64
- * by default, but a `PUBLIC_READS=false` deployment requires the
65
- * `/firkin/blob/read` capability — read tokens are reusable (reads are
66
- * not replay-tracked), so sending one is always correct and cheap. */
214
+ /** Fetch a whole blob. Reads are authenticated: the server requires the
215
+ * `/firkin/blob/read` capability (an operator can opt into anonymous
216
+ * reads with `PUBLIC_READS=true`, in which case the header is ignored).
217
+ * Read tokens are reusable — reads are not replay-tracked. */
67
218
  get(cid: string): Promise<Uint8Array>;
219
+ /**
220
+ * Read a byte range of a **BDASL (BLAKE3)** blob with verified streaming:
221
+ * the server sends a bao proof and every chunk is checked against the CID
222
+ * before it is returned — the server is not trusted for integrity. Needs a
223
+ * BLAKE3 CID (`cidForBytes(data, { hash: "blake3" })`).
224
+ */
225
+ verifiedRange(cid: string, range?: ByteRange): Promise<RangeResult>;
68
226
  /** Fetch a byte range (206 responses; server-trusted verification).
69
227
  * Authenticated like [`get`](#get). */
70
228
  getRange(cid: string, range?: ByteRange): Promise<RangeResult>;
@@ -90,20 +248,40 @@ export declare class Firkin {
90
248
  readUrl(cid: string, opts?: {
91
249
  expireIn?: number;
92
250
  }): Promise<string>;
93
- /** Attach an already-stored CID to the signer's account. */
94
- claim(cid: string): Promise<void>;
251
+ /** Attach an already-stored CID to the signer's account. `tags` is the
252
+ * same sugar as `upload`: the CID is added to each group afterwards. */
253
+ claim(cid: string, opts?: {
254
+ tags?: string | string[];
255
+ }): Promise<void>;
95
256
  /** Detach the signer's claim; bytes die with the last claimant. */
96
257
  delete(cid: string): Promise<void>;
97
- /** Revoke delegated sessions: specific delegation CIDs, or every
98
- * delegation minted before now (`all: true`). Must be authorized by the
99
- * account key itself — a direct invocation (seed accounts) or a fresh
100
- * passkey assertion delegating `/firkin/account/revoke` to the session key
101
- * (passkey-native accounts; the server caps its lifetime at 10 minutes).
102
- * A plain delegated session cannot revoke. */
258
+ /** Revoke delegations/sessions: specific delegation or attestation CIDs,
259
+ * or every grant minted before now (`all: true`). Seed/did:key accounts
260
+ * authorize this with a direct invocation by the account key. An attested
261
+ * session must additionally pass a fresh passkey assertion (`stepUp`,
262
+ * built with `stepUpChallenge`) — a stolen session key must not be able to
263
+ * revoke. */
103
264
  revoke(opts: {
104
265
  cids?: string[];
105
266
  all?: boolean;
106
- }): Promise<void>;
267
+ }, stepUp?: WebAuthnAssertion): Promise<void>;
268
+ /** Attest a session for a `did:plc` account. `material` is what the client
269
+ * fetched from the account's repo and the PLC directory (base64url DAG-CBOR):
270
+ *
271
+ * - `ops`: the PLC operation log, genesis first;
272
+ * - `commit`: the signed repo commit containing the record;
273
+ * - `record`: the `plcSessionRecord(...)` value;
274
+ * - `proof`: the MST nodes proving the record is in that commit.
275
+ *
276
+ * The server verifies everything offline (no PLC/PDS call). */
277
+ static attestPlc(base: string, material: PlcAttestationMaterial, fetchImpl?: typeof fetch): Promise<AttestationResult>;
278
+ /** Enrol a passkey session. The assertion's challenge must be
279
+ * `attestationChallenge(...)`; the server verifies the ceremony, stores the
280
+ * session → account binding, and returns its attestation CID. After this,
281
+ * requests are signed by the session key (see `sessionTokenSource`).
282
+ * Static: the endpoint is unauthenticated by design — the assertion IS the
283
+ * authentication. */
284
+ static attest(base: string, assertion: WebAuthnAssertion, fetchImpl?: typeof fetch): Promise<AttestationResult>;
107
285
  /** (Admin) register a DID as an account, optionally binding a WebAuthn
108
286
  * credential (`id` = base64url rawId, `spki` = base64url of the browser's
109
287
  * getPublicKey() DER). The binding is what lets a fresh browser re-attach
@@ -113,19 +291,6 @@ export declare class Firkin {
113
291
  id: string;
114
292
  spki: string;
115
293
  }): Promise<void>;
116
- /** (Account key) attest a session delegation with the bound passkey. */
117
- attest(body: {
118
- credential_id: string;
119
- auth_data: string;
120
- client_data: string;
121
- signature: string;
122
- delegation_cid: string;
123
- }): Promise<void>;
124
- /** (Account key) bind a WebAuthn credential to this account. */
125
- bindCredential(credential: {
126
- id: string;
127
- spki: string;
128
- }): Promise<void>;
129
294
  /** Resolve a passkey assertion to its account DID — the login primitive
130
295
  * for passkey-native accounts on a fresh browser. The client performs a
131
296
  * discoverable-credentials WebAuthn get (no `allowCredentials`); the
@@ -152,6 +317,52 @@ export declare class Firkin {
152
317
  registryDelete(name: string): Promise<void>;
153
318
  /** The account's whole registry: name → CID (sorted). */
154
319
  registryList(): Promise<Record<string, string>>;
320
+ /** Tag blobs: add CIDs to a per-account label group (idempotent). */
321
+ tagAdd(name: string, cids: string | string[]): Promise<{
322
+ name: string;
323
+ count: number;
324
+ }>;
325
+ /** One page of a tag group (groups are paged; `next` walks them). */
326
+ tagGet(name: string, opts?: {
327
+ cursor?: number;
328
+ }): Promise<TagPage>;
329
+ /** Every CID in a tag group (walks the pages). */
330
+ tagAll(name: string): Promise<string[]>;
331
+ /** The tag groups a CID belongs to (reverse lookup). Needs the full
332
+ * `/firkin/tag/read` command: a per-group proof cannot widen into it. */
333
+ blobTags(cid: string): Promise<string[]>;
334
+ /** Add a CID to each group in `tags` (sugar for `upload`/`claim`). */
335
+ private applyTags;
336
+ /** Every tag group with its CID count. */
337
+ tagList(): Promise<{
338
+ name: string;
339
+ count: number;
340
+ }[]>;
341
+ /** Untag one blob from a group. */
342
+ tagRemove(name: string, cid: string): Promise<{
343
+ name: string;
344
+ count: number;
345
+ }>;
346
+ /** Drop a tag group; the blobs stay stored. */
347
+ tagDelete(name: string): Promise<{
348
+ name: string;
349
+ cids: number;
350
+ }>;
351
+ /** Drop a tag group **and detach every blob in it** (bytes die where this
352
+ * account is the last claimant). Needs `/firkin/tag/delete`. */
353
+ tagDeleteBlobs(name: string): Promise<{
354
+ name: string;
355
+ blobs: number;
356
+ deleted: number;
357
+ }>;
358
+ /**
359
+ * Ingest a rootless CARv1 — "upload a `.car` file". Every block is verified
360
+ * against its own CID, policy-checked and claimed by the account; there is
361
+ * no DAG traversal and no root requirement (unlike [`dagPush`](#dagpush),
362
+ * which syncs one DAG over multiple rounds). Partial success is normal:
363
+ * `failed` lists the rejected blocks (digest mismatch, policy, size).
364
+ */
365
+ ingestCar(car: Uint8Array): Promise<CarIngestResult>;
155
366
  /**
156
367
  * Push a flat file as an ordered list of chunks: each chunk is stored as
157
368
  * a raw block, and a dag-cbor list of their CIDs becomes the root — the
@@ -201,7 +412,8 @@ export declare class Firkin {
201
412
  */
202
413
  pullFile(root: string): Promise<Uint8Array>;
203
414
  }
204
- export declare function carFile(root: CID, entries: [CID, Uint8Array][]): Uint8Array;
415
+ export declare function carFile(root: CID | null, entries: [CID, Uint8Array][]): Uint8Array;
416
+ export declare function parseCarBlocks(car: Uint8Array): Map<string, Uint8Array>;
205
417
  export declare function multihashSha256(data: Uint8Array): MultihashDigest;
206
418
  type MultihashDigest = {
207
419
  code: number;