@tokono.ma/firkin 0.3.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
@@ -3,6 +3,8 @@ import { CID } from "multiformats";
3
3
  * live outside it). */
4
4
  export declare const API_PREFIX = "/v1";
5
5
  import type { Signer } from "./keys.ts";
6
+ import { type WebAuthnAssertion } from "./webauthn.ts";
7
+ import type { PlcAttestationMaterial } from "./plc.ts";
6
8
  /** Per-call options a TokenSource may honor. */
7
9
  export interface TokenOptions {
8
10
  /** Override the token's expiry (seconds from now). */
@@ -15,6 +17,19 @@ export interface TokenOptions {
15
17
  export type TokenSource = (command: string, options?: TokenOptions) => Promise<Uint8Array>;
16
18
  /** Signer-backed source: mints a fresh direct invocation per request. */
17
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;
18
33
  /** Fixed-token source: only appropriate for public/read-only usage. */
19
34
  export declare function staticToken(token: Uint8Array): TokenSource;
20
35
  export interface CarIngestResult {
@@ -31,6 +46,121 @@ export interface CarIngestResult {
31
46
  error: string;
32
47
  }[];
33
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;
34
164
  export interface UploadResult {
35
165
  cid: string;
36
166
  /** True when the blob was already stored (dedup; claim joined). */
@@ -72,16 +202,27 @@ export declare class Firkin {
72
202
  private readonly fetchImpl;
73
203
  private readonly blockCache;
74
204
  constructor(base: string, token: TokenSource, fetchImpl?: typeof fetch, blockCache?: BlockCache);
75
- /** 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. */
76
209
  upload(data: Uint8Array | Blob, opts?: {
77
210
  codec?: number;
78
211
  hash?: "sha256" | "blake3";
212
+ tags?: string | string[];
79
213
  }): Promise<UploadResult>;
80
214
  /** Fetch a whole blob. Reads are authenticated: the server requires the
81
215
  * `/firkin/blob/read` capability (an operator can opt into anonymous
82
216
  * reads with `PUBLIC_READS=true`, in which case the header is ignored).
83
217
  * Read tokens are reusable — reads are not replay-tracked. */
84
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>;
85
226
  /** Fetch a byte range (206 responses; server-trusted verification).
86
227
  * Authenticated like [`get`](#get). */
87
228
  getRange(cid: string, range?: ByteRange): Promise<RangeResult>;
@@ -107,20 +248,40 @@ export declare class Firkin {
107
248
  readUrl(cid: string, opts?: {
108
249
  expireIn?: number;
109
250
  }): Promise<string>;
110
- /** Attach an already-stored CID to the signer's account. */
111
- 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>;
112
256
  /** Detach the signer's claim; bytes die with the last claimant. */
113
257
  delete(cid: string): Promise<void>;
114
- /** Revoke delegated sessions: specific delegation CIDs, or every
115
- * delegation minted before now (`all: true`). Must be authorized by the
116
- * account key itself — a direct invocation (seed accounts) or a fresh
117
- * passkey assertion delegating `/firkin/account/revoke` to the session key
118
- * (passkey-native accounts; the server caps its lifetime at 10 minutes).
119
- * 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. */
120
264
  revoke(opts: {
121
265
  cids?: string[];
122
266
  all?: boolean;
123
- }): 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>;
124
285
  /** (Admin) register a DID as an account, optionally binding a WebAuthn
125
286
  * credential (`id` = base64url rawId, `spki` = base64url of the browser's
126
287
  * getPublicKey() DER). The binding is what lets a fresh browser re-attach
@@ -156,6 +317,44 @@ export declare class Firkin {
156
317
  registryDelete(name: string): Promise<void>;
157
318
  /** The account's whole registry: name → CID (sorted). */
158
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
+ }>;
159
358
  /**
160
359
  * Ingest a rootless CARv1 — "upload a `.car` file". Every block is verified
161
360
  * against its own CID, policy-checked and claimed by the account; there is
@@ -214,6 +413,7 @@ export declare class Firkin {
214
413
  pullFile(root: string): Promise<Uint8Array>;
215
414
  }
216
415
  export declare function carFile(root: CID | null, entries: [CID, Uint8Array][]): Uint8Array;
416
+ export declare function parseCarBlocks(car: Uint8Array): Map<string, Uint8Array>;
217
417
  export declare function multihashSha256(data: Uint8Array): MultihashDigest;
218
418
  type MultihashDigest = {
219
419
  code: number;