@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.
@@ -6,15 +6,62 @@ export interface WebAuthnAssertion {
6
6
  /** The authenticator's signature over authData || SHA-256(clientDataJSON). */
7
7
  signature: Uint8Array;
8
8
  }
9
+ /** Default session lease: 30 days (the server caps it). */
10
+ export declare const ATTESTATION_DEFAULT_TTL: number;
9
11
  /**
10
- * Package an assertion (whose challenge was the bare delegation payload)
11
- * into a proof: dag-cbor `{ad, cd, sg}`. Also returns the proof's CID —
12
- * computed over the envelope the server synthesizes
13
- * (`[signature, {h, "ucan/dlg@…": payload}]`, with the varsig header of the
14
- * payload issuer's key type — ed25519 or P-256), which is what the
15
- * invocation's `prf` must reference.
12
+ * The challenge for a passkey session attestation: DAG-CBOR naming the
13
+ * account, the session key and the granted commands. Every field is inside
14
+ * `clientDataJSON.challenge`, hence covered by the authenticator's signature.
15
+ *
16
+ * Pass it to `navigator.credentials.get` as `publicKey.challenge`, then send
17
+ * the resulting assertion to `Firkin.attest()`.
16
18
  */
17
- export declare function assertionToProof(challengePayload: Uint8Array, assertion: WebAuthnAssertion): {
18
- proof: Uint8Array;
19
- cid: string;
19
+ export declare function attestationChallenge(opts: {
20
+ /** The API origin (`https://firkin.example`) — must match server config. */
21
+ firkin: string;
22
+ /** The passkey account DID. */
23
+ account: string;
24
+ /** The `did:key` session that will sign API calls. */
25
+ session: string;
26
+ /** Capability prefixes the session may exercise. */
27
+ commands: string[];
28
+ /** Lease in seconds (default 30 days, server-capped). */
29
+ expireIn?: number;
30
+ /** Override the start time (unix seconds) — tests only. */
31
+ now?: number;
32
+ }): Uint8Array;
33
+ /**
34
+ * The challenge for a step-up action (revocation): bound to one account and
35
+ * one command, short-lived and single-use. Sent in the `X-Firkin-Assertion`
36
+ * header alongside an ordinary session invocation.
37
+ */
38
+ export declare function stepUpChallenge(opts: {
39
+ firkin: string;
40
+ account: string;
41
+ /** The capability the action requires, e.g. "/firkin/account/revoke". */
42
+ action: string;
43
+ /** Seconds until expiry (default 120; server caps at 300). */
44
+ expireIn?: number;
45
+ /** Override the start time (unix seconds) — tests only. */
46
+ now?: number;
47
+ }): Uint8Array;
48
+ /**
49
+ * The challenge for a recovery grant: authorizes `recovery` (a did:key) to
50
+ * call `POST /v1/account/recover` for this account. Omit `expireIn` for an
51
+ * indefinite grant (revoke it by its attestation CID).
52
+ */
53
+ export declare function recoveryChallenge(opts: {
54
+ firkin: string;
55
+ account: string;
56
+ recovery: string;
57
+ expireIn?: number;
58
+ now?: number;
59
+ }): Uint8Array;
60
+ /** The JSON body expected by `POST /v1/tokens/attestation`. */
61
+ export declare function packageAssertion(assertion: WebAuthnAssertion): {
62
+ ad: string;
63
+ cd: string;
64
+ sg: string;
20
65
  };
66
+ /** The `X-Firkin-Assertion` header value (base64url DAG-CBOR). */
67
+ export declare function assertionHeader(assertion: WebAuthnAssertion): string;
package/dist/webauthn.js CHANGED
@@ -1,29 +1,90 @@
1
- // Assertion-wrapped UCAN delegations: a WebAuthn assertion whose challenge
2
- // is the bare dag-cbor delegation payload. The authenticator signs
3
- // `authData || SHA-256(clientDataJSON)` — the server verifies that against
4
- // the payload's issuer DID and reconstructs the delegation.
1
+ // Passkey attestations: a WebAuthn assertion whose challenge is a Firkin
2
+ // attestation (`firkin/attestation/passkey`) or step-up (`firkin/step-up`)
3
+ // payload.
4
+ //
5
+ // An assertion is NOT a UCAN proof. The account key lives in the
6
+ // authenticator and only signs `authData || SHA-256(clientDataJSON)`, so the
7
+ // assertion enrols a `did:key` session (or authorizes one account-level
8
+ // action) and is verified once by the server. Tokens are then ordinary
9
+ // self-issued invocations by the session key.
5
10
  import * as dagcbor from "@ipld/dag-cbor";
6
- import { cidForBytes } from "./cid.js";
7
- import { DLG_TAG, headerForDid } from "./ucan.js";
11
+ import { randomBytes } from "@noble/hashes/utils";
12
+ /** Default session lease: 30 days (the server caps it). */
13
+ export const ATTESTATION_DEFAULT_TTL = 30 * 24 * 60 * 60;
8
14
  /**
9
- * Package an assertion (whose challenge was the bare delegation payload)
10
- * into a proof: dag-cbor `{ad, cd, sg}`. Also returns the proof's CID —
11
- * computed over the envelope the server synthesizes
12
- * (`[signature, {h, "ucan/dlg@…": payload}]`, with the varsig header of the
13
- * payload issuer's key type — ed25519 or P-256), which is what the
14
- * invocation's `prf` must reference.
15
+ * The challenge for a passkey session attestation: DAG-CBOR naming the
16
+ * account, the session key and the granted commands. Every field is inside
17
+ * `clientDataJSON.challenge`, hence covered by the authenticator's signature.
18
+ *
19
+ * Pass it to `navigator.credentials.get` as `publicKey.challenge`, then send
20
+ * the resulting assertion to `Firkin.attest()`.
15
21
  */
16
- export function assertionToProof(challengePayload, assertion) {
17
- const proof = dagcbor.encode({
22
+ export function attestationChallenge(opts) {
23
+ const nbf = opts.now ?? Math.floor(Date.now() / 1000);
24
+ const exp = nbf + (opts.expireIn ?? ATTESTATION_DEFAULT_TTL);
25
+ return dagcbor.encode({
26
+ $type: "firkin/attestation/passkey",
27
+ firkin: opts.firkin,
28
+ account: opts.account,
29
+ session: opts.session,
30
+ commands: opts.commands,
31
+ nbf,
32
+ exp,
33
+ nonce: randomBytes(16),
34
+ });
35
+ }
36
+ /**
37
+ * The challenge for a step-up action (revocation): bound to one account and
38
+ * one command, short-lived and single-use. Sent in the `X-Firkin-Assertion`
39
+ * header alongside an ordinary session invocation.
40
+ */
41
+ export function stepUpChallenge(opts) {
42
+ const now = opts.now ?? Math.floor(Date.now() / 1000);
43
+ return dagcbor.encode({
44
+ $type: "firkin/step-up",
45
+ firkin: opts.firkin,
46
+ account: opts.account,
47
+ action: opts.action,
48
+ exp: now + (opts.expireIn ?? 120),
49
+ nonce: randomBytes(16),
50
+ });
51
+ }
52
+ /**
53
+ * The challenge for a recovery grant: authorizes `recovery` (a did:key) to
54
+ * call `POST /v1/account/recover` for this account. Omit `expireIn` for an
55
+ * indefinite grant (revoke it by its attestation CID).
56
+ */
57
+ export function recoveryChallenge(opts) {
58
+ const now = opts.now ?? Math.floor(Date.now() / 1000);
59
+ return dagcbor.encode({
60
+ $type: "firkin/recovery/passkey",
61
+ firkin: opts.firkin,
62
+ account: opts.account,
63
+ recovery: opts.recovery,
64
+ exp: opts.expireIn === undefined ? null : now + opts.expireIn,
65
+ nonce: randomBytes(16),
66
+ });
67
+ }
68
+ /** The JSON body expected by `POST /v1/tokens/attestation`. */
69
+ export function packageAssertion(assertion) {
70
+ return {
71
+ ad: b64url(assertion.authData),
72
+ // The EXACT clientDataJSON text — the server hashes those bytes.
73
+ cd: b64url(new TextEncoder().encode(assertion.clientDataJSON)),
74
+ sg: b64url(assertion.signature),
75
+ };
76
+ }
77
+ /** The `X-Firkin-Assertion` header value (base64url DAG-CBOR). */
78
+ export function assertionHeader(assertion) {
79
+ return b64url(dagcbor.encode({
18
80
  ad: assertion.authData,
19
81
  cd: assertion.clientDataJSON,
20
82
  sg: assertion.signature,
21
- });
22
- const payload = dagcbor.decode(challengePayload);
23
- const h = headerForDid(payload.iss);
24
- const envelope = dagcbor.encode([
25
- assertion.signature,
26
- { h, [DLG_TAG]: payload },
27
- ]);
28
- return { proof, cid: cidForBytes(envelope, { codec: 0x71 }) };
83
+ }));
84
+ }
85
+ function b64url(bytes) {
86
+ let binary = "";
87
+ for (const byte of bytes)
88
+ binary += String.fromCharCode(byte);
89
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
29
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokono.ma/firkin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Browser-first TypeScript client SDK for the Firkin API (UCAN 1.0, DASL CIDs, CAR Mirror sync)",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "repository": {
@@ -42,13 +42,14 @@
42
42
  "@ipld/dag-cbor": "^9.2.0",
43
43
  "@noble/ed25519": "^1.7.3",
44
44
  "@noble/hashes": "^1.5.0",
45
+ "@scure/bip39": "^1.5.0",
45
46
  "bs58": "^6.0.0",
46
- "multiformats": "^13.0.0",
47
47
  "hash-wasm": "^4.12.0",
48
- "@scure/bip39": "^1.5.0"
48
+ "multiformats": "^13.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "typescript": "^5.7.0"
51
+ "typescript": "^5.7.0",
52
+ "blake3-bao": "1.3.4"
52
53
  },
53
54
  "engines": {
54
55
  "node": ">=22"