ckb-session-kit 0.0.0-stage → 1.0.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/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { checkRequest, createSession, isActive, SessionPolicyError } from "./session.js";
2
+ export { deserializeSession, indexedDbStore, memoryStore, serializeSession, SessionStoreConflict } from "./store.js";
3
+ export { ScopeError, sendInScope, sessionAddress, sessionSigner } from "./transfer.js";
4
+ export { relativeBlocksSince, SESSION_LOCK_ERRORS, sessionLockArgs, sessionLockCellDep, sessionLockErrorFrom, sessionLockScript, TESTNET_DEPLOYMENT, TESTNET_LEGACY_DEPLOYMENTS, } from "./lock.js";
5
+ export { assertPlainXudt, assertSigningLock, closeSession, findOwnedSessionCells, findSessionCells, keyLockOf, matureCells, onchainRecipient, openSession, recoverSessions, SessionBalanceError, sessionLockOf, spendInSession, tokenAmount, } from "./onchain.js";
6
+ export { blocksForSeconds, cancelSubscription, chargeSubscription, findSubscriptionCells, openSubscription, subscriptionLock, subscriptionMemo, subscriptionStatus, verifySubscription, } from "./subscription.js";
7
+ export { paymentMemo, signAccess, verifyAccess } from "./paywall.js";
8
+ export { applyPlacements, checkPlacement, decodePlacement, encodePlacement, PLACE_BOARD, placementMemo, placementPrice, PlaceMemoError, } from "./place.js";
package/dist/lock.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ /**
3
+ * Client side of the on-chain session lock (contracts/session-lock).
4
+ *
5
+ * Args layout, 80 or 112 bytes:
6
+ * owner_lock_hash 32 | session_lock_hash 32 | max_per_tx u64 LE | min_interval u64 LE | [recipient_lock_hash 32]
7
+ *
8
+ * A token session (128 or 160 bytes) puts the token before the recipient:
9
+ * ... | min_interval | token_type_hash 32 | max_per_tx_token u128 LE | [recipient_lock_hash 32]
10
+ */
11
+ export interface SessionLockParams {
12
+ /** Lock hash whose presence in a transaction's inputs grants owner mode. */
13
+ ownerLockHash: ccc.HexLike;
14
+ /** Lock hash of the session key's own lock (its key cell); grants session mode. */
15
+ sessionLockHash: ccc.HexLike;
16
+ /** Most shannons one session-mode transaction may move out, fees included. */
17
+ maxPerTx: bigint;
18
+ /** Blocks each session-locked input must have aged (relative since); 0 = no rate limit. */
19
+ minInterval?: bigint;
20
+ /** If set, the only lock session-mode spends may pay (besides change, key cell, owner). */
21
+ recipientLockHash?: ccc.HexLike;
22
+ /** A token session: the one xUDT type its cells may hold, and the most units one spend may move. */
23
+ token?: {
24
+ typeHash: ccc.HexLike;
25
+ maxPerTx: bigint;
26
+ };
27
+ }
28
+ /** Where the session lock code lives on a network. */
29
+ export interface SessionLockDeployment {
30
+ codeHash: ccc.HexLike;
31
+ hashType: ccc.HashTypeLike;
32
+ cellDep: {
33
+ outPoint: {
34
+ txHash: ccc.HexLike;
35
+ index: ccc.NumLike;
36
+ };
37
+ depType: ccc.DepTypeLike;
38
+ };
39
+ }
40
+ /**
41
+ * The session lock deployed on CKB public testnet. Kept identical to
42
+ * deployment/testnet.json by a test.
43
+ */
44
+ export declare const TESTNET_DEPLOYMENT: SessionLockDeployment;
45
+ /**
46
+ * Earlier testnet deployments, newest first. Their code is immutable, so sessions
47
+ * opened under them keep their old rules; pass these to `recoverSessions` too so
48
+ * those sessions can still be swept. 13 Oct 2026: before token sessions (errors
49
+ * 19, 20) and untyped-only payments. 8 Oct 2026: before errors 16, 17 and 18.
50
+ */
51
+ export declare const TESTNET_LEGACY_DEPLOYMENTS: SessionLockDeployment[];
52
+ /** Error codes returned by the session lock, for reading node rejections. */
53
+ export declare const SESSION_LOCK_ERRORS: Record<number, string>;
54
+ export declare function sessionLockArgs(p: SessionLockParams): ccc.Hex;
55
+ export declare function sessionLockScript(d: SessionLockDeployment, p: SessionLockParams): ccc.Script;
56
+ export declare function sessionLockCellDep(d: SessionLockDeployment): ccc.CellDep;
57
+ /** A relative `since` of `blocks` block numbers (RFC 0017), as the rate limit requires. */
58
+ export declare function relativeBlocksSince(blocks: bigint): bigint;
59
+ /**
60
+ * Pulls a session-lock error code out of a node's rejection message, if there is
61
+ * one. The session lock only ever runs as an input's lock: a code from a type
62
+ * script (xUDT on a token payment, say) is not one, and gives undefined. An
63
+ * input lock can still be another lock in the same transaction (the recipient's
64
+ * anyone-can-pay cell on a top-up); pass `sessionInputs`, the indexes of the
65
+ * session cells, to rule that out too.
66
+ */
67
+ export declare function sessionLockErrorFrom(message: string, sessionInputs?: number[]): {
68
+ code: number;
69
+ meaning: string;
70
+ } | undefined;
package/dist/lock.js ADDED
@@ -0,0 +1,106 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ /**
3
+ * The session lock deployed on CKB public testnet. Kept identical to
4
+ * deployment/testnet.json by a test.
5
+ */
6
+ export const TESTNET_DEPLOYMENT = {
7
+ codeHash: "0xab419466529ea479590d4be7b23580ce2ddc640ba902735c9ad9a22dc64c08ce",
8
+ hashType: "data2",
9
+ cellDep: {
10
+ outPoint: { txHash: "0xf3098bdb9a7bb1c500990be0f1159b7e66f507fef6b7a4f904046024284877f2", index: "0x0" },
11
+ depType: "code",
12
+ },
13
+ };
14
+ /**
15
+ * Earlier testnet deployments, newest first. Their code is immutable, so sessions
16
+ * opened under them keep their old rules; pass these to `recoverSessions` too so
17
+ * those sessions can still be swept. 13 Oct 2026: before token sessions (errors
18
+ * 19, 20) and untyped-only payments. 8 Oct 2026: before errors 16, 17 and 18.
19
+ */
20
+ export const TESTNET_LEGACY_DEPLOYMENTS = [
21
+ {
22
+ codeHash: "0x1cb7724269e2e86359b5b47cad6b1860ccd6da254c25c651056c8431d11590cd",
23
+ hashType: "data2",
24
+ cellDep: {
25
+ outPoint: { txHash: "0x37cfe04f64fd4dbec3803a972a51b7080f9ca1a3042535e97b7a756d0e03b071", index: "0x0" },
26
+ depType: "code",
27
+ },
28
+ },
29
+ {
30
+ codeHash: "0x1ae8b8f71ed1846e2823ecccd8c3f80527be2895689f5dae54a53bd42526bc20",
31
+ hashType: "data2",
32
+ cellDep: {
33
+ outPoint: { txHash: "0xab2e69b8b97c15bddeb80954edd6d33befcd2fe9e4243377a46f14a3e1c343d2", index: "0x0" },
34
+ depType: "code",
35
+ },
36
+ },
37
+ ];
38
+ /** Error codes returned by the session lock, for reading node rejections. */
39
+ export const SESSION_LOCK_ERRORS = {
40
+ 10: "bad args",
41
+ 11: "not authorised: neither owner nor session key present",
42
+ 12: "outflow exceeds max_per_tx",
43
+ 13: "recipient not allowed",
44
+ 14: "rate limited: input too young",
45
+ 15: "overflow (capacity or token amounts)",
46
+ 16: "too many change cells: a session spend may not split the session cell",
47
+ 17: "the key cell may not grow in a session spend",
48
+ 18: "session cells and their change may not carry a type script (in a token session: none but the token)",
49
+ 19: "malformed token data: under 16 bytes, or token change not exactly 16",
50
+ 20: "token outflow exceeds max_per_tx_token",
51
+ };
52
+ const U64_MAX = (1n << 64n) - 1n;
53
+ function u64le(v) {
54
+ if (v < 0n || v > U64_MAX)
55
+ throw new RangeError(`not a u64: ${v}`);
56
+ const b = new Uint8Array(8);
57
+ new DataView(b.buffer).setBigUint64(0, v, true);
58
+ return b;
59
+ }
60
+ const U128_MAX = (1n << 128n) - 1n;
61
+ function u128le(v) {
62
+ if (v < 0n || v > U128_MAX)
63
+ throw new RangeError(`not a u128: ${v}`);
64
+ return ccc.numLeToBytes(v, 16);
65
+ }
66
+ function hash32(h, name) {
67
+ const b = ccc.bytesFrom(h);
68
+ if (b.length !== 32)
69
+ throw new RangeError(`${name} must be 32 bytes`);
70
+ return b;
71
+ }
72
+ export function sessionLockArgs(p) {
73
+ return ccc.hexFrom(ccc.bytesConcat(hash32(p.ownerLockHash, "ownerLockHash"), hash32(p.sessionLockHash, "sessionLockHash"), u64le(p.maxPerTx), u64le(p.minInterval ?? 0n), ...(p.token === undefined ? [] : [hash32(p.token.typeHash, "token.typeHash"), u128le(p.token.maxPerTx)]), ...(p.recipientLockHash === undefined ? [] : [hash32(p.recipientLockHash, "recipientLockHash")])));
74
+ }
75
+ export function sessionLockScript(d, p) {
76
+ return ccc.Script.from({ codeHash: d.codeHash, hashType: d.hashType, args: sessionLockArgs(p) });
77
+ }
78
+ export function sessionLockCellDep(d) {
79
+ return ccc.CellDep.from(d.cellDep);
80
+ }
81
+ /** A relative `since` of `blocks` block numbers (RFC 0017), as the rate limit requires. */
82
+ export function relativeBlocksSince(blocks) {
83
+ if (blocks < 0n || blocks >= 1n << 56n)
84
+ throw new RangeError(`blocks out of range: ${blocks}`);
85
+ return (1n << 63n) | blocks;
86
+ }
87
+ /**
88
+ * Pulls a session-lock error code out of a node's rejection message, if there is
89
+ * one. The session lock only ever runs as an input's lock: a code from a type
90
+ * script (xUDT on a token payment, say) is not one, and gives undefined. An
91
+ * input lock can still be another lock in the same transaction (the recipient's
92
+ * anyone-can-pay cell on a top-up); pass `sessionInputs`, the indexes of the
93
+ * session cells, to rule that out too.
94
+ */
95
+ export function sessionLockErrorFrom(message, sessionInputs) {
96
+ const m = message.match(/error code (-?\d+)/i) ?? message.match(/ValidationFailure\D+(-?\d+)/);
97
+ if (!m)
98
+ return undefined;
99
+ const source = message.match(/source: (Inputs|Outputs)\[(\d+)\]\.(Lock|Type)/);
100
+ if (source && (source[1] !== "Inputs" || source[3] !== "Lock"))
101
+ return undefined;
102
+ if (source && sessionInputs && !sessionInputs.includes(Number(source[2])))
103
+ return undefined;
104
+ const code = Number(m[1]);
105
+ return { code, meaning: SESSION_LOCK_ERRORS[code] ?? "unknown" };
106
+ }
@@ -0,0 +1,212 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ import { type SessionLockDeployment } from "./lock.js";
3
+ import { type Session } from "./session.js";
4
+ /**
5
+ * Sessions enforced by the on-chain session lock.
6
+ *
7
+ * - `openSession` — the owner (any CCC signer: a wallet) funds a session cell under
8
+ * the session lock and a key cell under the session key's own lock. One signature.
9
+ * - `spendInSession` — the session key alone spends from the session cell; the key
10
+ * cell pays the fee. No wallet involved; the network enforces the scope. With
11
+ * `topUp`, it adds to the recipient's anyone-can-pay cell instead of creating a
12
+ * new one, so a payment can be any size (a new cell needs at least 61 CKB).
13
+ * - `closeSession` — the key cell goes back to the owner (session key signs), then
14
+ * the owner sweeps the session cells (one wallet signature).
15
+ * - `recoverSessions` — the device holding the session key is gone: the owner's
16
+ * wallet alone finds every session it opened and sweeps them back.
17
+ *
18
+ * A token session (`openSession(..., { token })`) holds an xUDT token as well as
19
+ * CKB; `spendInSession(..., { to, token })` pays in it, and the lock caps the
20
+ * units per spend as it caps shannons. Sweeps return the tokens to the owner.
21
+ */
22
+ /** What the browser must remember, besides the key, to find and spend the session. */
23
+ export type OnChainBinding = NonNullable<Session["onchain"]>;
24
+ export interface SessionCells {
25
+ lock: ccc.Script;
26
+ keyLock: ccc.Script;
27
+ sessionCells: ccc.Cell[];
28
+ keyCells: ccc.Cell[];
29
+ /** Spendable balance of the session cells, in shannons. */
30
+ balance: bigint;
31
+ /** Fee budget left in the key cells, in shannons. */
32
+ feeBalance: bigint;
33
+ /** Token units in the session cells (token sessions; 0 otherwise). */
34
+ tokenBalance: bigint;
35
+ }
36
+ export declare class SessionBalanceError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ /**
40
+ * The smallest key cell that can still be returned to the owner: a cell under the
41
+ * owner's lock (63 CKB for JoyID or Omnilock, whose args are 22 bytes), and never
42
+ * below a key cell's own 61 CKB.
43
+ */
44
+ export declare function keyCellFloor(ownerLock: ccc.ScriptLike): bigint;
45
+ /**
46
+ * Signs `tx`, hands its final hash to `beforeSend`, then sends it. The hash is
47
+ * known before anything leaves, so a caller can record the payment or the key
48
+ * against it first. If the send throws but the node did take the transaction (a
49
+ * timeout on the reply, say), the hash is returned all the same.
50
+ */
51
+ export declare function signAndSend(signer: ccc.Signer, tx: ccc.Transaction, beforeSend?: (txHash: ccc.Hex) => Promise<void>): Promise<ccc.Hex>;
52
+ export declare function keyLockOf(session: Session, client: ccc.Client): Promise<ccc.Script>;
53
+ /** The xUDT amount in a cell's data (RFC 0052: u128 LE in the first 16 bytes). */
54
+ export declare function tokenAmount(data: ccc.HexLike): bigint;
55
+ export declare const tokenData: (amount: bigint, rest?: Uint8Array) => `0x${string}`;
56
+ /** A cell's data is exactly a 16-byte amount: what the session lock accepts as token change. */
57
+ export declare const plainTokenCell: (cell: ccc.Cell, type: ccc.Script) => boolean;
58
+ export declare function sessionLockOf(session: Session, client: ccc.Client, deployment: SessionLockDeployment, binding: OnChainBinding): Promise<ccc.Script>;
59
+ /** Plain cells under `lock`; with `token`, also cells holding exactly that token. */
60
+ export declare function collect(client: ccc.Client, lock: ccc.Script, token?: ccc.Script): Promise<ccc.Cell[]>;
61
+ export declare function findSessionCells(session: Session, client: ccc.Client, deployment: SessionLockDeployment, binding: OnChainBinding): Promise<SessionCells>;
62
+ /**
63
+ * The recipient the session lock will enforce, taken from the session's own scope
64
+ * so the browser and the chain cannot disagree. The lock holds one recipient, so a
65
+ * scope listing several cannot be enforced on-chain and is refused.
66
+ */
67
+ export declare function onchainRecipient(session: Session): string | undefined;
68
+ /**
69
+ * Throws unless `lock` is known to need a signature over the whole transaction:
70
+ * the session lock grants owner mode to any transaction with an input under the
71
+ * owner's lock, so that lock is the only thing standing between a stranger and
72
+ * every session. An allowlist, not a blocklist (code review, 14 Oct):
73
+ *
74
+ * - secp256k1-blake160 (single or multisig), JoyID, Nostr, PW lock;
75
+ * - Omnilock with a signature auth flag (0x00-0x07: CKB, Ethereum, EOS, Tron,
76
+ * Bitcoin, Dogecoin, multisig; 0x12: Ethereum with the message displayed) and neither administrator (0x01) nor
77
+ * anyone-can-pay (0x02) mode. Auth flags 0xFC-0xFE hand the decision to
78
+ * another lock or to arbitrary code, and are refused.
79
+ *
80
+ * Anyone-can-pay (RFC 0026) unlocks with no signature at all: anyone could spend
81
+ * the owner's cell back to itself unchanged and sweep every session.
82
+ *
83
+ * The owner must also never sign a partial (open) transaction with this lock.
84
+ */
85
+ export declare function assertSigningLock(lock: ccc.Script, client: ccc.Client): Promise<void>;
86
+ /**
87
+ * Owner funds the session: one transaction, one signature from `owner`.
88
+ * The on-chain recipient comes from `session.policy.scope.recipients`.
89
+ *
90
+ * `beforeSend` runs once the transaction is signed and before it is sent: save the
91
+ * session there (`store.save({ ...session, onchain: binding })`). Saved after
92
+ * sending, a failed save would leave funds under a key that exists nowhere. The
93
+ * binding then carries `openTx`, the funding transaction's hash, which
94
+ * `closeSession` checks before reporting that nothing is left.
95
+ *
96
+ * The key cell (`feeBudget`) must be able to return to the owner at the end: by
97
+ * default it is the size of a cell under the owner's lock plus 1 CKB for fees.
98
+ */
99
+ export declare function openSession(owner: ccc.Signer, session: Session, deployment: SessionLockDeployment, opts: {
100
+ budget: bigint;
101
+ feeBudget?: bigint;
102
+ minInterval?: bigint;
103
+ /** A token session: put `amount` units of the xUDT `type` in it, and let one spend move at most `maxPerTx`. */
104
+ token?: {
105
+ type: ccc.ScriptLike;
106
+ amount: bigint;
107
+ maxPerTx: bigint;
108
+ };
109
+ beforeSend?: (binding: OnChainBinding) => Promise<void>;
110
+ }): Promise<{
111
+ txHash: ccc.Hex;
112
+ binding: OnChainBinding;
113
+ }>;
114
+ /**
115
+ * Throws unless `type` is a plain xUDT (RFC 0052) on this network: the session
116
+ * lock reads amounts as xUDT stores them, and an xUDT with any flag set in its
117
+ * args either runs extension scripts the owner did not choose on every spend, or
118
+ * widens who may mint (owner mode by input or output type, code review 14 Oct).
119
+ */
120
+ export declare function assertPlainXudt(type: ccc.Script, client: ccc.Client): Promise<void>;
121
+ /**
122
+ * The session key pays `to`: `amount` shannons, or in a token session `token`
123
+ * units of its xUDT. The scope is checked here first, then the network checks it
124
+ * again in the session lock. No wallet is involved.
125
+ *
126
+ * `topUp: true` pays into an existing cell of `to` instead of creating one. `to`
127
+ * must be an anyone-can-pay address with at least one live cell to add to (plain
128
+ * for CKB, holding the session's token for `token`); that cell is consumed and
129
+ * recreated holding more, which the anyone-can-pay lock allows without the
130
+ * recipient's signature. A token paid without `topUp` goes in a new cell, whose
131
+ * capacity (about 142 CKB) the session also pays and must fit in `maxPerTx`.
132
+ *
133
+ * In a token session, tokens always stay in the change: a CKB payment from a
134
+ * cell holding tokens returns them, never burns them.
135
+ *
136
+ * `memo` is recorded in the witness of the session key's own input, which that
137
+ * key's signature covers: the payment commits to it (see `verifyAccess`).
138
+ */
139
+ export declare function spendInSession(session: Session, client: ccc.Client, deployment: SessionLockDeployment, binding: OnChainBinding, request: {
140
+ to: string;
141
+ amount: bigint;
142
+ token?: undefined;
143
+ topUp?: boolean;
144
+ memo?: ccc.HexLike;
145
+ beforeSend?: (txHash: ccc.Hex) => Promise<void>;
146
+ } | {
147
+ to: string;
148
+ token: bigint;
149
+ amount?: undefined;
150
+ topUp?: boolean;
151
+ memo?: ccc.HexLike;
152
+ beforeSend?: (txHash: ccc.Hex) => Promise<void>;
153
+ }, now?: Date): Promise<ccc.Hex>;
154
+ /** The cells created at least `blocks` blocks before the tip: those a relative `since` of `blocks` can spend. */
155
+ export declare function matureCells(client: ccc.Client, cells: ccc.Cell[], blocks: bigint): Promise<ccc.Cell[]>;
156
+ /**
157
+ * Ends the session: the owner sweeps the session cells (one wallet signature),
158
+ * then the key cell returns to the owner (session key signs, no wallet).
159
+ *
160
+ * The sweep goes first because it is what matters: a key cell too small to pay
161
+ * for the owner's lock (some wallets' locks need more than 61 CKB) must not block
162
+ * it. A key-cell failure is reported in `keyError`; the key stays usable for a retry.
163
+ *
164
+ * `nothingFound` means no cell is left under the session or the key: the key can
165
+ * be forgotten. If the funding transaction (`binding.openTx`) is still in flight
166
+ * or not yet indexed, this throws instead, so the key is never forgotten before
167
+ * the funds it controls arrive.
168
+ */
169
+ export declare function closeSession(owner: ccc.Signer, session: Session, deployment: SessionLockDeployment, binding: OnChainBinding): Promise<{
170
+ keyTx?: ccc.Hex;
171
+ sweepTx?: ccc.Hex;
172
+ keyError?: string;
173
+ nothingFound?: true;
174
+ }>;
175
+ /**
176
+ * Every live session cell this owner opened, found without any session key.
177
+ *
178
+ * Session-lock args begin with the owner's lock hash, so an indexer prefix search
179
+ * on the args finds them all. Only well-formed args (80 or 112 bytes; 128 or 160
180
+ * for a token session) are kept: a cell with malformed args would fail the lock
181
+ * even in owner mode, and anyone can create one with this owner's hash as a prefix
182
+ * to block a sweep. Likewise a typed cell is kept only if it holds the session's
183
+ * own token in well-formed data; anything else could make the sweep fail.
184
+ *
185
+ * Pass several deployments (`[TESTNET_DEPLOYMENT, ...TESTNET_LEGACY_DEPLOYMENTS]`)
186
+ * to find sessions opened under earlier versions of the lock as well.
187
+ */
188
+ export declare function findOwnedSessionCells(owner: ccc.Signer, deployments: SessionLockDeployment | SessionLockDeployment[]): Promise<ccc.Cell[]>;
189
+ /**
190
+ * Device-loss recovery: sweeps every session this owner opened back to the owner,
191
+ * in one transaction and one wallet signature. Needs nothing from the lost device.
192
+ *
193
+ * Anyone can create a cell under a session lock carrying this owner's hash and
194
+ * their own key, and spend it before the sweep lands, which would make a single
195
+ * sweep fail. So if the one transaction is refused, each session (cells sharing a
196
+ * session key) is swept on its own, one signature each: a cell someone else can
197
+ * spend fails alone, and `failed` counts the sessions not recovered.
198
+ *
199
+ * The key cell (the fee budget) is under the session key's own lock and cannot be
200
+ * recovered without that key; keep it small.
201
+ */
202
+ export declare function recoverSessions(owner: ccc.Signer, deployments: SessionLockDeployment | SessionLockDeployment[]): Promise<{
203
+ txHashes: ccc.Hex[];
204
+ recovered: bigint;
205
+ cells: number;
206
+ failed: number;
207
+ }>;
208
+ /**
209
+ * Spends `cells` in owner mode: everything returns to the owner. Tokens go back
210
+ * in one cell per token type (xUDT would let a sweep burn them otherwise).
211
+ */
212
+ export declare function ownerSweep(owner: ccc.Signer, deployments: SessionLockDeployment | SessionLockDeployment[], cells: ccc.Cell[]): Promise<ccc.Hex>;