ckb-session-kit 1.0.0 → 2.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/CHANGELOG.md +299 -0
- package/README.md +181 -21
- package/dist/deployment.d.ts +21 -0
- package/dist/deployment.js +92 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/onchain.d.ts +270 -31
- package/dist/onchain.js +675 -86
- package/dist/paywall.d.ts +29 -0
- package/dist/paywall.js +84 -25
- package/dist/place.d.ts +18 -9
- package/dist/place.js +32 -28
- package/dist/session.d.ts +9 -1
- package/dist/session.js +4 -1
- package/dist/store.d.ts +2 -1
- package/dist/store.js +17 -6
- package/dist/subscription.d.ts +103 -7
- package/dist/subscription.js +456 -119
- package/package.json +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,305 @@
|
|
|
3
3
|
This project follows [semantic versioning](https://semver.org/). The session lock is
|
|
4
4
|
deployed on CKB **testnet only**; there is no mainnet deployment yet.
|
|
5
5
|
|
|
6
|
+
## 2.0.0 (2026-11-03)
|
|
7
|
+
|
|
8
|
+
### Upgrading from 1.0.0
|
|
9
|
+
Every 1.0.0 export is still there with the same signature; new parameters are
|
|
10
|
+
optional. Some calls that 1.0.0 accepted are now refused, on purpose:
|
|
11
|
+
- `assertSigningLock`, `openSession`, `openSubscription` refuse an owner (or
|
|
12
|
+
charger) lock that opens with no signature: PW-Lock (its deployed build is
|
|
13
|
+
anyone-can-pay) and the Nostr lock in proof of work mode
|
|
14
|
+
(`reason: "owner-lock-unsafe"`; no option lets these through). Sessions
|
|
15
|
+
already open under such a lock should be closed or recovered now.
|
|
16
|
+
- They also refuse configurations not reviewed: Omnilock auth 0x07 or mode bits
|
|
17
|
+
other than time-lock (0x04) and supply (0x08), secp256k1, multisig or Nostr
|
|
18
|
+
locks with args their code would not accept, and any unknown lock
|
|
19
|
+
(`reason: "owner-lock-unreviewed"`). Pass `allowUnreviewedOwnerLock: true`
|
|
20
|
+
once you have checked the lock to get the 1.0.0 behaviour back, with a warning;
|
|
21
|
+
for the creator's charger lock in `openSubscription`, pass
|
|
22
|
+
`allowUnreviewedChargerLock: true`.
|
|
23
|
+
- Anyone-can-pay, PW-Lock and always-success referenced by data hash
|
|
24
|
+
(`data`, `data1`, `data2`) are refused like their type-hash versions.
|
|
25
|
+
- A send whose outcome cannot be checked throws `SendOutcomeUnknownError`
|
|
26
|
+
(with the tx hash); it is never retried, so check the hash before resending.
|
|
27
|
+
- `openSession` and `openSubscription` refuse `TESTNET_LEGACY_DEPLOYMENTS`,
|
|
28
|
+
`hashType: "data"`, and a deployment whose cell dep does not hold its code on
|
|
29
|
+
the client's network (one `getCellLive` per client and deployment).
|
|
30
|
+
- `closeSession` refuses a signer whose lock is not the session's owner lock.
|
|
31
|
+
- `openSession` with no rate limit, or `minInterval` 1, calls `console.warn`
|
|
32
|
+
(or `opts.onWarning`).
|
|
33
|
+
- `SessionPolicyError` and `SessionBalanceError` carry a `reason`; code that
|
|
34
|
+
matched error messages should match `reason` instead.
|
|
35
|
+
|
|
36
|
+
The wallet matrix's locks (secp256k1, Omnilock 0x12 / Bitcoin / Dogecoin,
|
|
37
|
+
Nostr signing mode, JoyID) all pass unchanged.
|
|
38
|
+
|
|
39
|
+
### Fixed (Week 6 adversarial review, 2 Nov)
|
|
40
|
+
- Token subscriptions: look-alike cells typed `xUDT(token.args ++ extra)`
|
|
41
|
+
(xUDT accepts them, anyone can create them with 0 units, and the indexer's
|
|
42
|
+
type filter matched them by prefix) counted toward `MAX_SUBSCRIPTION_CELLS`
|
|
43
|
+
but were never swept. The filter now asks for the exact script length, only
|
|
44
|
+
cells really of the subscription's kind count, raw reads are bounded, and
|
|
45
|
+
`clearPlantedCells` and `cancelSubscription` sweep spendable look-alikes back
|
|
46
|
+
to the subscriber (M1).
|
|
47
|
+
- A status or a charge counted every recent transaction under the lock toward
|
|
48
|
+
its budget of 64, creations by anyone included, and before the age check.
|
|
49
|
+
Now only spends count, an entry older than the period is never counted, and
|
|
50
|
+
creation-only entries are paged past under their own budget of 1024
|
|
51
|
+
(`MAX_CREATED_HISTORY`); past either budget it still refuses (M2).
|
|
52
|
+
- `clearPlantedCells`: sweeps at most 30 cells per transaction, so a status
|
|
53
|
+
never dates the cleanup as a charge (L3); with cells left unread, sweeps the
|
|
54
|
+
planted cells it read instead of refusing (L1); marks its transaction with
|
|
55
|
+
`CLEAR_PLANTED_MEMO` in the subscriber's own witness, so a status does not
|
|
56
|
+
take it for a cancel and a charge of the whole balance stays paid (L2).
|
|
57
|
+
- `signAndSend` throws `SendOutcomeUnknownError` (new export) when a send fails
|
|
58
|
+
and the status lookup fails too, instead of rethrowing the send's error: a
|
|
59
|
+
`Dead` answer from CCC's fallback node after a lost reply was retried and
|
|
60
|
+
could pay twice (L4). It is never retried.
|
|
61
|
+
- Payout cells under `payTo` are listed by the indexer's filter (plain and
|
|
62
|
+
empty, or exactly the token with 16 data bytes), so junk planted under an
|
|
63
|
+
anyone-can-pay address no longer pushes the real cells out of reach (L5).
|
|
64
|
+
- `assertSigningLock` refuses anyone-can-pay, PW-Lock and always-success by the
|
|
65
|
+
data hash of their deployed binaries under every data hash_type (`data`,
|
|
66
|
+
`data1`, `data2`), not only by type id (L6).
|
|
67
|
+
|
|
68
|
+
### Changed (Week 6 adversarial review, 2 Nov)
|
|
69
|
+
- `openSubscription` checks the creator's charger lock with its own opt-in,
|
|
70
|
+
`allowUnreviewedChargerLock`; `allowUnreviewedOwnerLock` now covers the
|
|
71
|
+
subscriber's lock only (I1).
|
|
72
|
+
- `openSession` raises `no-rate-limit` for `minInterval` 1 too (I3).
|
|
73
|
+
- The testnet scripts keep `.env.smoke` at the repository root whatever the
|
|
74
|
+
current directory (I4).
|
|
75
|
+
- Docs: a multisig with a since or an Omnilock time-lock also delays the
|
|
76
|
+
owner's sweep (I2).
|
|
77
|
+
|
|
78
|
+
### Added (mentor review, tracker #47, 2 Nov)
|
|
79
|
+
- Payout cell contention. A top-up (`spendInSession` with `topUp`,
|
|
80
|
+
`chargeSubscription`, `openSubscription`) picks the recipient's cell with
|
|
81
|
+
`pickPayoutCell`: confirmed cells first, shuffled, and the first free one of
|
|
82
|
+
up to `MAX_POOL_CHECKS` (4) cells checked with `get_live_cell` and
|
|
83
|
+
`include_tx_pool`, so a cell another payment in the node's pool spends is
|
|
84
|
+
skipped. At most `MAX_PAYOUT_CELLS` (64) of the recipient's cells are read.
|
|
85
|
+
- `spendInSession` and `chargeSubscription` take `retry: { attempts, delayMs }`
|
|
86
|
+
(`RetryOptions`, `DEFAULT_RETRY`: 3 attempts, 1000 ms doubling, with jitter).
|
|
87
|
+
A send the node refuses for a spent input (`Dead`, `Unknown`, or an RBF
|
|
88
|
+
refusal, `poolConflict`) is built again with another payout cell; the cell a
|
|
89
|
+
`Dead` refusal names is not picked again. It used to be retried once, with no
|
|
90
|
+
wait, and only with a client cache. A send whose outcome is unknown is still
|
|
91
|
+
never retried.
|
|
92
|
+
- `SessionBalanceError` reason `"payout-busy"`: every payout cell checked is
|
|
93
|
+
spent in the pool and none is left. Nothing is sent; it is retried like a
|
|
94
|
+
refusal. The demo's charge route answers it with 503 and retry.
|
|
95
|
+
- `createPayoutCells(funder, payTo, { count, capacity?, token? })` gives an
|
|
96
|
+
anyone-can-pay address more cells to top up. `npm run setup:creator` uses it
|
|
97
|
+
and now gives the demo's creator eight cells (was three).
|
|
98
|
+
- `clearPlantedCells(subscriber, deployment, subscription)`: the subscriber
|
|
99
|
+
sweeps cells a third party created under the subscription's lock back to
|
|
100
|
+
itself, keeping the subscription's own cells, so it no longer has to cancel
|
|
101
|
+
and open again past `MAX_SUBSCRIPTION_CELLS`.
|
|
102
|
+
|
|
103
|
+
### Changed (mentor review, 2 Nov)
|
|
104
|
+
- `assertSigningLock` accepts only reviewed owner lock configurations:
|
|
105
|
+
secp256k1, multisig (v1, v2), Omnilock with auth 0x00-0x06 or 0x12 and no
|
|
106
|
+
mode other than time-lock or supply, the Nostr lock in signing mode
|
|
107
|
+
(`args[0]` = 0), and JoyID. The wallet matrix's locks all still pass.
|
|
108
|
+
- PW-Lock is now refused. The build deployed on mainnet and testnet is
|
|
109
|
+
lay2dev/pw-lock's anyone-can-pay variant: with no signature in the witness it
|
|
110
|
+
unlocks by payment, whatever the args, so anyone could sweep every session.
|
|
111
|
+
- The Nostr lock in proof of work mode (`args[0]` not 0) is now refused: it
|
|
112
|
+
unlocks with work, not the holder's key.
|
|
113
|
+
- Anyone-can-pay is refused by code hash on both networks (and the older
|
|
114
|
+
deployments pw-core lists), whatever the client's network; always-success
|
|
115
|
+
too. These refusals carry `reason: "owner-lock-unsafe"`.
|
|
116
|
+
- Locks and configurations not reviewed (Omnilock auth 0xFC-0xFE, other Omnilock
|
|
117
|
+
auth flags or mode bits, the ckb-proxy-locks, unknown locks, args the lock's
|
|
118
|
+
code would not accept, multisig v2 beta) are refused with
|
|
119
|
+
`reason: "owner-lock-unreviewed"`. Omnilock auth 0x07 and unknown Omnilock mode
|
|
120
|
+
bits used to pass.
|
|
121
|
+
- Subscription cells are listed with the indexer's type filter (no type, or the
|
|
122
|
+
subscription's token), so cells under any other type script no longer count
|
|
123
|
+
toward `MAX_SUBSCRIPTION_CELLS`.
|
|
124
|
+
- The last charge is the newest spend with an input under the charger's lock,
|
|
125
|
+
not any spend: a `clearPlantedCells` does not delay the next charge. A spend
|
|
126
|
+
that cannot be told apart still counts as a charge.
|
|
127
|
+
- Chained payments are not used; the README says why.
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
- `openSession` warns when a session has no rate limit (`minInterval` left out
|
|
131
|
+
or 0): every spend is within `maxPerTx`, but spends can repeat every block
|
|
132
|
+
until the session is empty. The warning (`SessionWarning`, code
|
|
133
|
+
`"no-rate-limit"`) goes to the new `opts.onWarning` before anything is
|
|
134
|
+
signed, which may throw to stop the opening, or to `console.warn` without one.
|
|
135
|
+
The demos' sessions open with no rate limit, so they now warn in the console.
|
|
136
|
+
- 23 CKB-VM tests of aggregate outflow across inputs (mentor review, 2 Nov):
|
|
137
|
+
cells with the same args share one limit per transaction (CKB and token), a
|
|
138
|
+
subscription's merged cells pay one price, cells with other args are checked
|
|
139
|
+
on their own, and owner mode sweeps several cells at once. No fault found; the
|
|
140
|
+
lock is unchanged.
|
|
141
|
+
- README and SECURITY-EVIDENCE: browser expiry revokes nothing on chain (funds
|
|
142
|
+
stay at risk until the owner's sweep is confirmed); a period is a block count,
|
|
143
|
+
not a fixed duration; the aggregate-outflow behaviour.
|
|
144
|
+
- `allowUnreviewedOwnerLock` (`SigningLockOptions`) on `assertSigningLock`,
|
|
145
|
+
`openSession` and `openSubscription`: lets an unreviewed owner or charger lock
|
|
146
|
+
through with a `console.warn`. It never lets an "owner-lock-unsafe" lock
|
|
147
|
+
through.
|
|
148
|
+
- `SessionPolicyReason` gains `"owner-lock-unsafe"` and `"owner-lock-unreviewed"`.
|
|
149
|
+
- `SessionBalanceError.reason` (`SessionBalanceReason`): `"no-fee-cell"` or
|
|
150
|
+
`"no-payout-cell"` when the cells missing are the payer's or the recipient's own
|
|
151
|
+
(a charger with no fee cell, a `payTo` with no cell to top up), so a server can
|
|
152
|
+
tell them from the subscription's state without matching messages. The demo's
|
|
153
|
+
charge route now uses it.
|
|
154
|
+
- `SessionPolicyError.reason` (`SessionPolicyReason`): `"payto-not-anyone-can-pay"`
|
|
155
|
+
when a charge needs to top up `payTo` and `payTo` is not anyone-can-pay. The
|
|
156
|
+
demo's charge route answers it with 503 (the deployment's setup, not the
|
|
157
|
+
subscription's state) instead of 409.
|
|
158
|
+
|
|
159
|
+
### Fixed (audit round 2, 31 Oct)
|
|
160
|
+
- Demo: enforced CSP now also sets `object-src 'none'`, `base-uri 'none'` and
|
|
161
|
+
`form-action 'self'`; the full policy (scripts, styles, images, testnet RPC and
|
|
162
|
+
JoyID aggregator only) is sent as `Content-Security-Policy-Report-Only`.
|
|
163
|
+
- CI: GitHub actions pinned to commit SHAs, checkouts with
|
|
164
|
+
`persist-credentials: false`.
|
|
165
|
+
- `openSession` and `openSubscription` refuse `TESTNET_LEGACY_DEPLOYMENTS`
|
|
166
|
+
(`assertCurrentDeployment`, new). `openSession` used to accept them: a token
|
|
167
|
+
session under the 13 Oct lock was stuck for good (that code refuses 128/160-byte
|
|
168
|
+
args before owner mode), and new sessions under the 8 Oct one lacked errors
|
|
169
|
+
16-18. Spending, closing, recovering, charging and cancelling still accept old
|
|
170
|
+
deployments, so funds under them can come back.
|
|
171
|
+
- Before building an opening, the deployment's cell dep is read from the
|
|
172
|
+
client's network (`getCellLive`; a dep group is resolved) and must hold code
|
|
173
|
+
hashing to `codeHash` (data hash, or type hash for `hashType: "type"`).
|
|
174
|
+
`TESTNET_DEPLOYMENT` on a mainnet client, a typo in `codeHash` or a wrong out
|
|
175
|
+
point used to put funds under a lock with no code. One lookup per client and
|
|
176
|
+
deployment. `hashType: "data"` is refused too: the lock faults under CKB-VM
|
|
177
|
+
version 0. There is still no mainnet deployment; README and API.md say how to
|
|
178
|
+
pass your own.
|
|
179
|
+
- `spendInSession` and `chargeSubscription`: a key or charger cell above its
|
|
180
|
+
floor but short of the fee threw CCC's plain "Insufficient CKB"; it is now
|
|
181
|
+
`SessionBalanceError` with reason `"no-fee-cell"`. The charger's fee cells must
|
|
182
|
+
hold at least the floor plus 0.01 CKB. Found by the round-2 property suite,
|
|
183
|
+
added in `test/property-library.test.ts` and `test/property-fuzz.test.ts`,
|
|
184
|
+
which now fails on any plain error.
|
|
185
|
+
- `addTopUp` (top-ups in `spendInSession`, `chargeSubscription`,
|
|
186
|
+
`openSubscription`) skips a payee token cell the payment would take past
|
|
187
|
+
2^128 - 1 units, and throws `SessionBalanceError` `"no-payout-cell"` if every
|
|
188
|
+
cell would, instead of a plain overflow error.
|
|
189
|
+
- `closeSession` refuses a signer whose lock is not `binding.ownerLock` with a
|
|
190
|
+
`SessionPolicyError`, before asking it to sign. After a wallet switch, the
|
|
191
|
+
new wallet was shown a sweep of the old wallet's session to itself (the lock
|
|
192
|
+
refused it on-chain, so nothing was lost).
|
|
193
|
+
- `closeSession` can finish once the session is empty on-chain: with the
|
|
194
|
+
funding committed and nothing indexed, it asks the node whether the funding's
|
|
195
|
+
outputs under the session and the key are still live, and reports
|
|
196
|
+
`nothingFound` when all are spent. Before, a close whose key return was taken
|
|
197
|
+
but whose reply was lost (or a close from a second tab) left a key that could
|
|
198
|
+
never be forgotten. The key return now goes through `signAndSend`, so a lost
|
|
199
|
+
reply the node did take is reported as `keyTx`.
|
|
200
|
+
It reports `nothingFound` only once the node's indexer has reached the tip
|
|
201
|
+
and a second search still finds nothing, so a later spend's key cell that is
|
|
202
|
+
not indexed yet never lets the key be forgotten.
|
|
203
|
+
- CCC's client cache lists a client's own outputs as live until cleared, so a
|
|
204
|
+
tab (or a warm server instance) built every later spend on cells another tab
|
|
205
|
+
had spent, and the node rejected each one. `spendInSession`, `closeSession`,
|
|
206
|
+
`chargeSubscription` and `cancelSubscription` now leave out cached-only cells
|
|
207
|
+
the node no longer has, and on a rejection for a spent or unknown input clear
|
|
208
|
+
the cache and build the transaction once more.
|
|
209
|
+
- `verifyAccess`, `verifySubscription` and `subscriptionStatus` read payments
|
|
210
|
+
with `getTransactionNoCache`. `getTransaction` answers from the cache when
|
|
211
|
+
the node no longer knows a transaction, so a payment that was reorged out kept
|
|
212
|
+
granting access on a long-lived server client.
|
|
213
|
+
- Demo: one "unknown" answer just after a lost send no longer makes a page
|
|
214
|
+
forget the session key (about 62 CKB stranded once the funding lands) or a
|
|
215
|
+
payment record. A key or record is forgotten only when the node rejected the
|
|
216
|
+
transaction, or has not known it for 10 minutes (the rule the payment polls
|
|
217
|
+
already used); meanwhile the page keeps polling it.
|
|
218
|
+
- Demo: every page's wallet lookup drops a late answer from the previous
|
|
219
|
+
wallet, and the subscribe page drops a status refresh started for the
|
|
220
|
+
previous wallet, asset or opening, and clears its status and notes on
|
|
221
|
+
disconnect.
|
|
222
|
+
- Demo: the CKB Place board asks again about a placement `get_transaction`
|
|
223
|
+
missed once, instead of leaving it off the board until a reload.
|
|
224
|
+
- Demo: the charge route builds a new CCC client per request.
|
|
225
|
+
|
|
226
|
+
### Fixed (security audit, 30 Oct)
|
|
227
|
+
- `recoverSessions`: when the one sweep is refused, sessions are now swept one
|
|
228
|
+
session lock at a time (full lock hash), not one session key hash at a time. A
|
|
229
|
+
planted cell under other args carrying the victim's public key hash (a foreign
|
|
230
|
+
type script the sweep cannot consume) joined the victim's group and blocked its
|
|
231
|
+
recovery on every retry. `findOwnedSessionCells` keeps a typed cell only if its
|
|
232
|
+
type is a plain xUDT on the network (`assertPlainXudt`'s rules) holding exactly
|
|
233
|
+
the 16-byte amount a session makes.
|
|
234
|
+
- `spendInSession` pays the fee from the largest key cell (ties: lowest out
|
|
235
|
+
point), not the first one listed: a 61 CKB cell anyone sent to the key address,
|
|
236
|
+
or the old key cell beside a top-up, blocked every spend. Still one key cell in
|
|
237
|
+
and one smaller one out, as the lock requires.
|
|
238
|
+
- `verifyAccess` and `checkPlacement` count only memos in witnesses a signature
|
|
239
|
+
covers: the first witness of each secp256k1 lock group, and any past the
|
|
240
|
+
inputs. The session cell's and an anyone-can-pay cell's witnesses are signed by
|
|
241
|
+
nobody, so whoever relayed a payment could add a memo there and void it for
|
|
242
|
+
good (or, on a CKB Place board, choose the pixels of a payment without one).
|
|
243
|
+
Several signed memos still fail. `placementMemo` takes the resolved inputs as
|
|
244
|
+
an optional third argument for the exact check; without them it is a prefilter.
|
|
245
|
+
- Docs: with a recipient set, up to `max_per_tx` per transaction can still leave
|
|
246
|
+
as the miner fee (the recipient rule covers outputs only). The README and
|
|
247
|
+
`openSession` now say so and recommend a rate limit (`minInterval`) whenever a
|
|
248
|
+
recipient is set. The lock is unchanged.
|
|
249
|
+
- `subscriptionStatus`, `chargeSubscription` and `findSubscriptionCells` read at
|
|
250
|
+
most `MAX_SUBSCRIPTION_CELLS` (16, exported) cells under a subscription's lock,
|
|
251
|
+
and at most 64 recent transactions of its history. Beyond that they throw
|
|
252
|
+
`SessionPolicyError` with the new reason `"too-many-cells"` instead of reading
|
|
253
|
+
part of the cells, which could deny a paid-up subscriber or let a cell left out
|
|
254
|
+
of a charge pay a second price in the period. Planted cells (anyone can create
|
|
255
|
+
them) made each status request look up every creating transaction and all its
|
|
256
|
+
inputs again: 100 cells cost 3,307 RPC calls. `cancelSubscription` still reads
|
|
257
|
+
and sweeps every cell, so the subscriber can clear planted ones.
|
|
258
|
+
- Subscription payments are checked once per transaction within a call, and a
|
|
259
|
+
transaction whose outputs to `payTo` already fall short is refused before its
|
|
260
|
+
inputs are looked up.
|
|
261
|
+
- `chargeSubscription` picks its fee cell from the first 64 cells under the
|
|
262
|
+
charger's lock instead of reading all of them.
|
|
263
|
+
- Demo: `/api/subscription` answers `"too-many-cells"` with 409 (the page shows
|
|
264
|
+
it), not 503 with retry.
|
|
265
|
+
- Demo: the charger address is the creator's key, so `CHARGER_KEY` on the server
|
|
266
|
+
could spend the creator's income and payout cells. New `npm run setup:charger`
|
|
267
|
+
creates a separate key (`.env.charger`, written once, mode 0600) and prints its
|
|
268
|
+
address and next steps; the docs now require a charger key of its own holding
|
|
269
|
+
only fee cells. `CHARGER` is replaced once that key is funded and set on Vercel.
|
|
270
|
+
|
|
271
|
+
### Fixed (30 Oct)
|
|
272
|
+
- `chargeSubscription` and `nextChargeIn` date the last charge from the
|
|
273
|
+
subscription lock's indexer history, not from its change cell: a charge of the
|
|
274
|
+
whole balance (the last 153 CKB with a price of 153 CKB or more) leaves no
|
|
275
|
+
change, so a younger top-up could pay a second price under a period later.
|
|
276
|
+
- `subscriptionStatus` counts that last whole-balance charge as a paid period;
|
|
277
|
+
it used to end access as soon as the last period was paid. A cancel still ends
|
|
278
|
+
access at once.
|
|
279
|
+
|
|
280
|
+
### Fixed (code review, 28 Oct)
|
|
281
|
+
- Token overflow guard: a planted cell of 2^128-1 free tokens could push the
|
|
282
|
+
subscriber's own cell out of a cancel (the guard kept cells in indexer order), and
|
|
283
|
+
blocked `closeSession` and `recoverSessions` of token sessions (no guard at all).
|
|
284
|
+
One guard now serves sessions, recovery and subscriptions; it keeps cells created
|
|
285
|
+
by the owner or the key first.
|
|
286
|
+
- `recoverSessions`, `closeSession`, `cancelSubscription`: an owner sweep the node
|
|
287
|
+
accepted is reported as sent even when the reply times out (it skipped the
|
|
288
|
+
check `signAndSend` does). `recoverSessions` now throws when the owner holds no
|
|
289
|
+
plain cell, instead of reporting every session as failed.
|
|
290
|
+
- `chargeSubscription` waits a full period after the last charge, not only after
|
|
291
|
+
the oldest cell: a top-up cell older than the last charge's change could pay a
|
|
292
|
+
second price in the same period. `nextChargeIn` counts the same way.
|
|
293
|
+
- Session stores: a save of the same key without the on-chain binding (from a
|
|
294
|
+
stale tab) is refused with `SessionStoreConflict` instead of dropping the binding.
|
|
295
|
+
- Demo: the charge route answers 503 with `retry` when the creator's own cells are
|
|
296
|
+
missing (no fee cell for the charger, no payout cell to top up), not 409.
|
|
297
|
+
- `chargeSubscription` pays its fee from a random one of the charger's cells, and a
|
|
298
|
+
CKB `openSubscription` tops up a random payout cell, so concurrent charges and
|
|
299
|
+
openings rarely compete for one cell.
|
|
300
|
+
|
|
301
|
+
### Changed
|
|
302
|
+
- `verifyAccess` resolves a payment's inputs in parallel once the memo check
|
|
303
|
+
passes; subscription charges and status fetch each creating transaction once.
|
|
304
|
+
|
|
6
305
|
## 1.0.0 (2026-10-27)
|
|
7
306
|
|
|
8
307
|
First stable release. Everything below is on testnet, with a smoke script and explorer
|
package/README.md
CHANGED
|
@@ -38,6 +38,42 @@ Its first example application is pay-per-read: a reader unlocks articles for
|
|
|
38
38
|
1 CKB each with no wallet prompt, and the session can only ever pay the creator.
|
|
39
39
|
The problem is not specific to that application.
|
|
40
40
|
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install ckb-session-kit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
[ckb-session-kit on npm](https://www.npmjs.com/package/ckb-session-kit) (v2.0.0, ESM, Node 20 or later
|
|
48
|
+
and browsers). The API is in [docs/API.md](docs/API.md); the walkthrough in
|
|
49
|
+
[docs/WALKTHROUGH.md](docs/WALKTHROUGH.md).
|
|
50
|
+
|
|
51
|
+
New in 2.0.0 (details and upgrade notes in [CHANGELOG.md](CHANGELOG.md)):
|
|
52
|
+
|
|
53
|
+
- `openSession(owner, session, deployment, { ..., onWarning })`: called with a
|
|
54
|
+
`SessionWarning` (`"no-rate-limit"`) before anything is signed; throw from it
|
|
55
|
+
to stop the opening. Without it the warning goes to `console.warn`.
|
|
56
|
+
- `SigningLockOptions` (`{ allowUnreviewedOwnerLock?: boolean }`), taken by
|
|
57
|
+
`assertSigningLock`, `openSession` and `openSubscription`: owner locks are
|
|
58
|
+
checked against a list of reviewed configurations (see
|
|
59
|
+
[Which wallets can own a session](#which-wallets-can-own-a-session)). Refusals
|
|
60
|
+
carry `reason: "owner-lock-unsafe"` (never allowed) or
|
|
61
|
+
`"owner-lock-unreviewed"` (allowed, with a warning, by this option).
|
|
62
|
+
- `RetryOptions` (`{ attempts?, delayMs? }`, defaults in `DEFAULT_RETRY`: 3
|
|
63
|
+
attempts from 1000 ms, doubling, with jitter), passed as `retry` to
|
|
64
|
+
`spendInSession` and `chargeSubscription`: a send the node refuses for a spent
|
|
65
|
+
input is built again with another payout cell (`poolConflict` tells an RBF
|
|
66
|
+
refusal apart). The retry loop itself is internal (not exported).
|
|
67
|
+
- `pickPayoutCell(client, lock, cells, avoid?)`: the payout cell a top-up uses,
|
|
68
|
+
confirmed and free in the node's pool (`MAX_POOL_CHECKS`, `MAX_PAYOUT_CELLS`).
|
|
69
|
+
When none is free, `SessionBalanceError` with reason `"payout-busy"`.
|
|
70
|
+
- `createPayoutCells(funder, payTo, { count, capacity?, token? })`: more
|
|
71
|
+
anyone-can-pay cells for a creator, so concurrent payments do not compete.
|
|
72
|
+
- `clearPlantedCells(subscriber, deployment, subscription)`: sweeps cells a third
|
|
73
|
+
party created under a subscription's lock, keeping the subscription's own.
|
|
74
|
+
- `assertCurrentDeployment(client, deployment)`, `MAX_SUBSCRIPTION_CELLS`, and the
|
|
75
|
+
`SessionPolicyReason` / `SessionBalanceReason` types for the errors' `reason`.
|
|
76
|
+
|
|
41
77
|
## Try it
|
|
42
78
|
|
|
43
79
|
```bash
|
|
@@ -131,7 +167,9 @@ this work on CKB:
|
|
|
131
167
|
The anyone-can-pay lock accepts the top-up without the creator's signature.
|
|
132
168
|
- **A stolen key can only pay the creator.** The session's recipient is the
|
|
133
169
|
creator's anyone-can-pay address, so the session lock refuses any other output,
|
|
134
|
-
and `max_per_tx` caps each unlock.
|
|
170
|
+
and `max_per_tx` caps each unlock. The fee is not an output: up to `max_per_tx`
|
|
171
|
+
per transaction can still go to the miner, so give a session with a recipient a
|
|
172
|
+
rate limit (`minInterval`) too.
|
|
135
173
|
|
|
136
174
|
```ts
|
|
137
175
|
const session = createSession({ expiresAt, scope: { maxPerTx: 5n * CKB, recipients: [creatorAcp] } });
|
|
@@ -177,8 +215,22 @@ then unlock articles for 1 CKB each with no popup.
|
|
|
177
215
|
|
|
178
216
|
A session cell with a recipient occupies 153 CKB, so a 200 CKB session has 47 CKB
|
|
179
217
|
to spend; the rest comes back on close. Each payment consumes one of the creator's
|
|
180
|
-
cells, so readers paying at the same moment compete for them
|
|
181
|
-
|
|
218
|
+
anyone-can-pay cells, so readers paying at the same moment compete for them. A
|
|
219
|
+
top-up picks a confirmed cell that no transaction in the node's pool spends
|
|
220
|
+
(`pickPayoutCell`; at most 4 cells checked, 64 read). If the node still refuses
|
|
221
|
+
the payment for a spent cell, it is built again with another cell after a short
|
|
222
|
+
wait, up to 3 attempts by default (`retry: { attempts, delayMs }`). A send whose
|
|
223
|
+
outcome is unknown is never retried. When every cell is busy, nothing is sent
|
|
224
|
+
and the error says `"payout-busy"`. Give a creator more cells with
|
|
225
|
+
`createPayoutCells(funder, payTo, { count })`; `npm run setup:creator` gives the
|
|
226
|
+
demo's creator eight.
|
|
227
|
+
`npm run smoke:contention` checks it on testnet: four payers pay one creator at the same moment, every payment must commit, and a hand-built double top-up shows the node's own refusal.
|
|
228
|
+
|
|
229
|
+
Chained payments (topping up a cell another payment created while that payment
|
|
230
|
+
is still in the pool) are not used: if the first payment is dropped or slow to
|
|
231
|
+
propagate, every payment built on it is stuck or dropped too, and a reader's
|
|
232
|
+
session would wait on someone else's transaction. More cells and a pool-aware
|
|
233
|
+
pick avoid that.
|
|
182
234
|
|
|
183
235
|
## Example: CKB Place, a pixel board on testnet
|
|
184
236
|
|
|
@@ -283,6 +335,13 @@ subscriber's money held; the creator's side did not, and the library now covers
|
|
|
283
335
|
- access follows payments, not the live cell, so re-creating the cell to reset its age gains nothing;
|
|
284
336
|
- the opening commits to an account id (hashed) under the subscriber's signature, so nobody can present someone else's subscription.
|
|
285
337
|
|
|
338
|
+
Several mature cells of one subscription can be merged in one charge, and
|
|
339
|
+
together they still pay at most one price. Each subscription is checked on its
|
|
340
|
+
own: two subscribers' cells charged in one transaction are refused (error 13),
|
|
341
|
+
since each sees the other's change as a third party, so a creator sends one
|
|
342
|
+
charge per subscriber. A subscriber's cancel sweeps every cell of the
|
|
343
|
+
subscription in one transaction.
|
|
344
|
+
|
|
286
345
|
A charge costs the session lock 20,999 cycles.
|
|
287
346
|
|
|
288
347
|
`npm run smoke:subscription` runs it on testnet. Run on 26 Oct 2026:
|
|
@@ -309,6 +368,28 @@ ran it on 27 Oct 2026 with the demo token:
|
|
|
309
368
|
**Try it in the browser:** [ckb-session-kit.vercel.app/subscribe](https://ckb-session-kit.vercel.app/subscribe):
|
|
310
369
|
2 CKB per 60 blocks; members' notes the server releases only while you are paid up.
|
|
311
370
|
|
|
371
|
+
**Deploying it.** The server that charges holds the charger key (`CHARGER_KEY` in
|
|
372
|
+
the deployment's environment, on Vercel for the demo). Make it a **key of its
|
|
373
|
+
own, never the creator's key** or the key behind the payout address: the lock
|
|
374
|
+
limits what the charger can take from subscriptions, but nothing limits what a
|
|
375
|
+
key can do with its own cells, so a leak of the server's environment would also
|
|
376
|
+
hand over all the creator's income. Give it only a few fee cells. `npm run
|
|
377
|
+
setup:charger` creates one in `.env.charger` (gitignored) and prints its address
|
|
378
|
+
and the next steps; nothing is sent. A status or a charge reads at most
|
|
379
|
+
`MAX_SUBSCRIPTION_CELLS` (16) cells under one subscription's lock and refuses
|
|
380
|
+
beyond that (`SessionPolicyError`, reason `"too-many-cells"`), so planted cells
|
|
381
|
+
cannot make one request cost thousands of RPC calls. Only cells of the
|
|
382
|
+
subscription's kind count (plain CKB, or exactly its token): the indexer leaves
|
|
383
|
+
other cells out, including look-alikes of the token with longer args. The
|
|
384
|
+
subscriber clears planted ones without cancelling:
|
|
385
|
+
`clearPlantedCells(wallet, deployment, subscription)` sweeps every cell not made
|
|
386
|
+
by the opening, a charge or the subscriber (and any look-alike of the token)
|
|
387
|
+
back to the subscriber, at most 30 per transaction, one signature each, and
|
|
388
|
+
leaves the subscription's own cells as they are. Only the subscriber's and the
|
|
389
|
+
charger's spends count toward the 64 recent transactions a status reads;
|
|
390
|
+
creations by anyone else are paged past, up to 1024 per period.
|
|
391
|
+
`npm run smoke:clear-planted` checks it on testnet: 16 planted cells block the status and charges, `clearPlantedCells` clears them, and the next charge stays on time.
|
|
392
|
+
|
|
312
393
|
## Which wallets can own a session
|
|
313
394
|
|
|
314
395
|
The owner signs to open, close and recover, so the owner's wallet must work. `npm
|
|
@@ -331,6 +412,30 @@ Ethereum wallets. Fixed, with a test built from the lock CCC derives for an
|
|
|
331
412
|
Ethereum account. **JoyID** signs with a passkey and cannot be scripted; it was
|
|
332
413
|
tested by hand in the demo (10 Oct).
|
|
333
414
|
|
|
415
|
+
`openSession` and `openSubscription` check the owner's lock first
|
|
416
|
+
(`assertSigningLock`). The session lock gives owner mode to any transaction that
|
|
417
|
+
spends a cell under the owner's lock, so a lock that opens without the owner's
|
|
418
|
+
signature would let anyone sweep every session. Since 2 Nov the check is a list
|
|
419
|
+
of reviewed configurations:
|
|
420
|
+
|
|
421
|
+
| Owner lock | Result |
|
|
422
|
+
|---|---|
|
|
423
|
+
| secp256k1 (CKB default), secp256k1 multisig, with or without a since | accepted |
|
|
424
|
+
| Omnilock, auth 0x00-0x06 or 0x12, no mode or only time-lock or supply | accepted |
|
|
425
|
+
| Nostr lock, signing mode (`args[0]` is 0) | accepted |
|
|
426
|
+
| JoyID | accepted (its code is closed: reviewed from its documentation) |
|
|
427
|
+
| Anyone-can-pay, PW-Lock, always-success (by type id, or their deployed binaries by data hash) | refused: they open with no signature |
|
|
428
|
+
| Omnilock in anyone-can-pay or administrator mode | refused |
|
|
429
|
+
| Nostr lock in proof of work mode (`args[0]` not 0) | refused: anyone who does the work can unlock it |
|
|
430
|
+
| Omnilock auth 0xFC-0xFE, the proxy locks, any other lock | refused unless you pass `allowUnreviewedOwnerLock: true` (`allowUnreviewedChargerLock` for a subscription's charger) |
|
|
431
|
+
|
|
432
|
+
PW-Lock is refused because the build deployed on CKB is its anyone-can-pay
|
|
433
|
+
variant: a transaction with no signature in the witness takes the payment path,
|
|
434
|
+
whatever the args. MetaMask and other Ethereum wallets get an Omnilock (0x12)
|
|
435
|
+
from CCC, which is accepted. `allowUnreviewedOwnerLock` lets an unreviewed lock
|
|
436
|
+
through with a warning; use it only once you have checked that the lock cannot
|
|
437
|
+
open without its holder's signature. It never lets a refused lock through.
|
|
438
|
+
|
|
334
439
|
## The on-chain session lock
|
|
335
440
|
|
|
336
441
|
[`contracts/session-lock`](contracts/session-lock/src/main.rs) is a CKB lock script in
|
|
@@ -341,15 +446,28 @@ Rust. A cell under it can be spent two ways:
|
|
|
341
446
|
- **Session mode** — the transaction also spends the session key's *key cell*. The
|
|
342
447
|
network then enforces, on-chain:
|
|
343
448
|
- **outflow limit** — at most `max_per_tx` shannons leave per transaction, fees
|
|
344
|
-
included, so the fee cannot be used to drain the cell
|
|
449
|
+
included, so the fee cannot be used to drain the cell. The limit is for all
|
|
450
|
+
the session's cells together: cells under the same lock (same args) are one
|
|
451
|
+
script group, so taking `max_per_tx` from each of three cells is refused.
|
|
452
|
+
Cells of another session or subscription (other args) are checked on their
|
|
453
|
+
own, against their own limit and recipient; one session's unused limit never
|
|
454
|
+
covers another's;
|
|
345
455
|
- **recipient allowlist** (optional) — payments may only go to one lock (plus
|
|
346
456
|
change, the key cell, and the owner). The key cell may come back smaller
|
|
347
457
|
(it pays the fee) but never larger, so the key cannot park session funds in
|
|
348
|
-
its own cell and pay anyone from there
|
|
458
|
+
its own cell and pay anyone from there. The fee is not an output, so the
|
|
459
|
+
allowlist does not see it: a leaked key can still give up to `max_per_tx`
|
|
460
|
+
per transaction to the miner as fee, paying the recipient nothing. Set a
|
|
461
|
+
rate limit (`min_interval`) whenever you set a recipient;
|
|
349
462
|
- **rate limit** (optional) — each session cell must have aged `min_interval`
|
|
350
463
|
blocks before it can be spent again (relative `since`). The limit is **per
|
|
351
464
|
cell**: with *n* session cells, the key can spend from each of them in the same
|
|
352
|
-
block. `openSession` creates one cell
|
|
465
|
+
block. `openSession` creates one cell. With `min_interval` 0 there is no
|
|
466
|
+
rate limit: every spend is within `max_per_tx`, but spends can repeat in
|
|
467
|
+
every block until the cell is empty, and `openSession` warns
|
|
468
|
+
(`SessionWarning`, `"no-rate-limit"`; also for `min_interval` 1, almost the same). The interval is a number of blocks,
|
|
469
|
+
not a fixed time: block time varies (see the
|
|
470
|
+
[average block time](https://explorer.nervos.org/charts/average-block-time));
|
|
353
471
|
- **no splitting** — a session spend may send at most one cell back to the
|
|
354
472
|
session lock, so the key can merge session cells but never multiply them,
|
|
355
473
|
which makes the per-cell rate limit a per-session one for sessions opened
|
|
@@ -364,13 +482,16 @@ Rust. A cell under it can be spent two ways:
|
|
|
364
482
|
Signatures are delegated to the standard secp256k1 locks of the owner and the
|
|
365
483
|
session key (the pattern sUDT's owner mode uses), so the script carries no
|
|
366
484
|
cryptography: **7.6 KB, no allocator, no C, 15,651 cycles per run** — about 0.9% of
|
|
367
|
-
a session spend (29,031 on a token spend, 1.7%).
|
|
485
|
+
a session spend (29,031 on a token spend, 1.7%). 144 tests run it in the real CKB-VM, most asserting a specific
|
|
368
486
|
rejection code, and CI builds the contract and requires them.
|
|
369
487
|
|
|
370
488
|
**What it cannot do:** enforce an expiry time. A CKB script can prove time has
|
|
371
489
|
passed (`since` is a lower bound) but never that it has not. Expiry is enforced
|
|
372
490
|
in the browser; the owner can revoke on-chain at any moment, and the session
|
|
373
|
-
cell's balance caps the total at risk.
|
|
491
|
+
cell's balance caps the total at risk. **Expiry in the browser revokes nothing
|
|
492
|
+
on chain.** After `expiresAt` the key still satisfies the lock, and the funds
|
|
493
|
+
stay at risk until the owner's sweep (`closeSession` or `recoverSessions`)
|
|
494
|
+
is confirmed.
|
|
374
495
|
|
|
375
496
|
**Enforced where:**
|
|
376
497
|
|
|
@@ -385,9 +506,15 @@ cell's balance caps the total at risk.
|
|
|
385
506
|
### If a session key leaks
|
|
386
507
|
|
|
387
508
|
The key cannot be revoked by expiry: after `expiresAt` it still satisfies the lock
|
|
388
|
-
until the owner
|
|
389
|
-
|
|
390
|
-
|
|
509
|
+
until the owner's sweep is confirmed on chain. The browser forgetting the key, or
|
|
510
|
+
refusing to use it, does not change that. Until then it can move at most `max_per_tx` per transaction,
|
|
511
|
+
only to the recipient if one is set (or to the miner as fee: the recipient rule
|
|
512
|
+
covers outputs, and the fee is not one), and, with a rate limit, once per
|
|
513
|
+
`min_interval` blocks per session cell. Without a rate limit, a leaked key can
|
|
514
|
+
repeat spends of `max_per_tx` in every block until the session is empty, each
|
|
515
|
+
one within the limit; a recipient does not slow it down, since it can burn
|
|
516
|
+
`max_per_tx` in fees every block. Set
|
|
517
|
+
`min_interval` whenever you set a recipient. The owner should sweep at once:
|
|
391
518
|
|
|
392
519
|
- with the key still at hand: `closeSession(wallet, session, deployment, binding)`;
|
|
393
520
|
- without it (device lost or wiped): `recoverSessions(wallet, deployment)` finds
|
|
@@ -399,9 +526,11 @@ paying a higher fee, so sweep early, and resweep what is left. Keep budgets
|
|
|
399
526
|
small: the budget is the most a session can ever lose.
|
|
400
527
|
|
|
401
528
|
**Owner and session locks must need a signature.** The lock trusts that an input
|
|
402
|
-
under the owner's lock means the owner signed. Plain secp256k1, JoyID
|
|
403
|
-
Omnilock are fine;
|
|
404
|
-
|
|
529
|
+
under the owner's lock means the owner signed. Plain secp256k1, JoyID, signing
|
|
530
|
+
Omnilock and the Nostr lock in signing mode are fine; anyone-can-pay, PW-Lock and
|
|
531
|
+
the Nostr lock in proof of work mode are not, since anyone could spend the
|
|
532
|
+
owner's cell and get owner mode. See [which wallets can own a
|
|
533
|
+
session](#which-wallets-can-own-a-session).
|
|
405
534
|
|
|
406
535
|
A session-lock cell occupies 121 CKB (153 CKB with a recipient), so its balance
|
|
407
536
|
and its change must stay above that.
|
|
@@ -413,6 +542,21 @@ see [`deployment/testnet.json`](deployment/testnet.json). The code is immutable.
|
|
|
413
542
|
Earlier deployments (8 Oct, before errors 16–18; 13 Oct, before token sessions)
|
|
414
543
|
are kept in `TESTNET_LEGACY_DEPLOYMENTS`, so `recoverSessions` still sweeps
|
|
415
544
|
sessions opened under them.
|
|
545
|
+
`openSession` and `openSubscription` refuse them: new cells go under the current
|
|
546
|
+
lock only.
|
|
547
|
+
|
|
548
|
+
Reference the lock with `hash_type` `data1` or `data2` (or `type`, for a
|
|
549
|
+
type-id deployment). Under `data` (CKB-VM version 0) the binary faults on load,
|
|
550
|
+
owner mode included, so a cell under it could never be spent; the library
|
|
551
|
+
refuses such a deployment when opening.
|
|
552
|
+
|
|
553
|
+
Before it builds an opening, the library reads the deployment's cell dep from
|
|
554
|
+
the client's network (`getCellLive`, a dep group resolved) and refuses unless it
|
|
555
|
+
holds code hashing to `codeHash` (`assertCurrentDeployment`), once per client
|
|
556
|
+
and deployment. `TESTNET_DEPLOYMENT` on a mainnet client is therefore refused.
|
|
557
|
+
There is no mainnet deployment and no `MAINNET_DEPLOYMENT` constant: to use the
|
|
558
|
+
lock on mainnet, deploy the binary yourself (byte-identical, see below) and pass
|
|
559
|
+
your own `SessionLockDeployment` (`codeHash`, `hashType: "data2"`, `cellDep`).
|
|
416
560
|
|
|
417
561
|
Verify the deployed lock yourself. The deployed binary was built with rustc
|
|
418
562
|
1.99.0 on the x86_64-pc-windows-gnu host (pinned in
|
|
@@ -431,7 +575,13 @@ The last command compares the build's data hash with `codeHash` in
|
|
|
431
575
|
the same three steps. Another host or rustc builds a working lock with other
|
|
432
576
|
bytes, so its hash will not match.
|
|
433
577
|
|
|
434
|
-
`
|
|
578
|
+
[`docs/SECURITY-EVIDENCE.md`](docs/SECURITY-EVIDENCE.md) collects the evidence
|
|
579
|
+
for each rule and error code in one report: the check in the script, the accepted
|
|
580
|
+
transactions, the node's rejection text, the CKB-VM tests, and which rules have
|
|
581
|
+
only been shown in CKB-VM tests. `npm run evidence` re-checks its logs against
|
|
582
|
+
testnet, read-only.
|
|
583
|
+
|
|
584
|
+
`npm run smoke:lock` runs the rules in the table below against public testnet. Run on 14 Oct 2026, on the current lock:
|
|
435
585
|
|
|
436
586
|
| Step | Network said |
|
|
437
587
|
|---|---|
|
|
@@ -465,13 +615,20 @@ that an over-limit spend is refused. Run on 8 Oct 2026:
|
|
|
465
615
|
## Limits
|
|
466
616
|
|
|
467
617
|
What this does not do, in one place. Each is stated where it applies above.
|
|
618
|
+
What has and has not been shown on testnet is in
|
|
619
|
+
[`docs/SECURITY-EVIDENCE.md`](docs/SECURITY-EVIDENCE.md#5-not-covered-and-limits).
|
|
468
620
|
|
|
469
621
|
- **Testnet only, not audited.** The lock has had community and adversarial
|
|
470
622
|
reviews, each finding fixed with a regression test, but no external audit.
|
|
471
623
|
Do not put mainnet funds behind it.
|
|
472
624
|
- **Expiry is not on-chain.** A CKB script cannot prove that time has *not*
|
|
473
|
-
passed, so `expiresAt` is checked in the browser.
|
|
474
|
-
|
|
625
|
+
passed, so `expiresAt` is checked in the browser. Browser expiry does not
|
|
626
|
+
revoke the key's on-chain authority: a leaked key stays valid, and the funds
|
|
627
|
+
stay at risk, until the owner's sweep transaction is confirmed. The session's
|
|
628
|
+
balance is the most it can lose.
|
|
629
|
+
- **No rate limit means repeated spends.** With `min_interval` 0, each spend
|
|
630
|
+
is within `max_per_tx`, but spends can repeat every block until the balance
|
|
631
|
+
is gone. `openSession` warns; set `minInterval`.
|
|
475
632
|
- **The rate limit is per cell.** `openSession` makes one cell and the lock
|
|
476
633
|
forbids splitting, but anyone adding more cells under the same session lock
|
|
477
634
|
gives the key more cells to spend from (with their own money, never the owner's).
|
|
@@ -486,11 +643,14 @@ What this does not do, in one place. Each is stated where it applies above.
|
|
|
486
643
|
- **An access proof does not expire.** It is a bearer token for one article:
|
|
487
644
|
whoever holds the signed proof can fetch that article again. It cannot be
|
|
488
645
|
used for another article or by another payment.
|
|
489
|
-
- **Owner locks must need a signature.** An
|
|
490
|
-
anyone
|
|
646
|
+
- **Owner locks must need a signature.** An owner lock that opens without one
|
|
647
|
+
(anyone-can-pay, PW-Lock, Nostr in proof of work mode) would give anyone owner
|
|
648
|
+
mode; `openSession` refuses it, and refuses locks it has not reviewed unless
|
|
649
|
+
`allowUnreviewedOwnerLock` is set.
|
|
491
650
|
- **Subscription periods are in blocks, and charges need the creator online.**
|
|
492
|
-
The lock's rate limit is a block count,
|
|
493
|
-
(
|
|
651
|
+
The lock's rate limit is a block count, not a fixed duration: block time
|
|
652
|
+
varies (see the [average block time](https://explorer.nervos.org/charts/average-block-time)), so a period is an
|
|
653
|
+
estimate of time (`blocksForSeconds`). Nothing charges by itself: the creator's server must
|
|
494
654
|
send each charge, one transaction per subscriber. Cancelling ends access at
|
|
495
655
|
once, and the current period's price is not refunded.
|
|
496
656
|
- **Sessions are not cheap to open.** A session cell occupies 121 CKB (153 with a
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import { type SessionLockDeployment } from "./lock.js";
|
|
3
|
+
/**
|
|
4
|
+
* Throws a `SessionPolicyError` unless new cells may go under `deployment` on
|
|
5
|
+
* `client`'s network:
|
|
6
|
+
*
|
|
7
|
+
* - it is not one of `TESTNET_LEGACY_DEPLOYMENTS` (older code with fewer rules;
|
|
8
|
+
* the 13 Oct one cannot even spend a token session's 128/160-byte args);
|
|
9
|
+
* - its `hashType` is not `data`: under CKB-VM version 0 the lock faults on
|
|
10
|
+
* load, owner mode included, so such cells could never be spent;
|
|
11
|
+
* - its cell dep is live on this network and holds the code: the code cell's
|
|
12
|
+
* data hashes to `codeHash` (`data1`, `data2`) or its type script hashes to
|
|
13
|
+
* it (`type`). A `depGroup` cell dep is resolved and any member may match.
|
|
14
|
+
* This catches a testnet deployment on a mainnet client, a typo in
|
|
15
|
+
* `codeHash` and a wrong out point, any of which would leave funds under a
|
|
16
|
+
* lock no code can run.
|
|
17
|
+
*
|
|
18
|
+
* A passing check is remembered for that client and deployment; a failing one
|
|
19
|
+
* is not, so the next call looks again.
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertCurrentDeployment(client: ccc.Client, deployment: SessionLockDeployment): Promise<void>;
|