mtok-bridge 0.3.4 → 0.3.5

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/README.md CHANGED
@@ -41,10 +41,18 @@ tool, one layer on top.
41
41
  (and the workers-ai house seller) run: validate request => verify the on-chain DrawPaid
42
42
  (request-hash bound) => bound both legs against the payment => claim => upstream => complete,
43
43
  fail-closed. The redemption store is a pluggable interface (`{ state, get, claim, complete,
44
- retentionMs }`, each method sync or async: the core awaits every call), so a filesystem store and a Workers KV store drive the same code path. If you
44
+ retentionMs }`, each method sync or async: the core awaits every call). The store must make
45
+ claims atomic across every host serving that offer. Independent local files and eventually
46
+ consistent KV read/write pairs do not provide that guarantee. If you
45
47
  are just serving a model for a key, you never need it; it is here so every mtok seller host
46
48
  shares one money path.
47
49
 
50
+ `state(key, { claimKey })` and `get(key, { claimKey })` receive the canonical commitment
51
+ identity even when `key` names a legacy record. `claim(key, markerKey, { paidAtMs })` receives
52
+ the verifier's payment timestamp, so a store migrating old claims can refuse ambiguous
53
+ pre-cutover payments. Existing stores may ignore these additional arguments. The core still
54
+ verifies the payment before reading a completion, and known claims never run upstream again.
55
+
48
56
 
49
57
  ---
50
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-bridge",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Serve any model as an OpenAI-compatible API with a key. No payment, no market, runs anywhere node runs. The transport core behind mtok.market's seller relay.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  // payer-screen POLICY stay in the host.
9
9
  //
10
10
  // The redemption dependency is an INTERFACE the caller passes:
11
- // { state(key), get(key), claim(key, markerKey), complete(key, payload), retentionMs }
11
+ // { state(key, { claimKey }), get(key, { claimKey }), claim(key, markerKey, { paidAtMs }), complete(key, payload), retentionMs }
12
12
  // state(key) returns 'pending' | 'complete' | null. claim(key, markerKey) returns false when the
13
13
  // draw is already claimed and THROWS when it cannot claim durably (the core then refuses before
14
14
  // upstream spend). complete(key, payload) throws when the payload cannot be persisted; the claim
@@ -268,6 +268,7 @@ export function createServeCore({
268
268
  // Legacy redemption remains only to honor draws already paid by old SDKs.
269
269
  const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
270
270
  const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
271
+ const redemptionContext = { claimKey: cacheKey };
271
272
 
272
273
  // Contract mode is the ONLY mode (#487): the legacy direct-transfer FUND
273
274
  // lane and its /api/bookings/:id balance read are gone. If the platform is
@@ -281,18 +282,18 @@ export function createServeCore({
281
282
  // The redemption interface may be sync (fs store) or async (a Workers KV
282
283
  // store): await normalizes both, and identity-awaits cost nothing under the
283
284
  // host's per-booking lock.
284
- let redemptionState = await redemption.state(storedKey);
285
+ let redemptionState = await redemption.state(storedKey, redemptionContext);
285
286
  // Preserve an upgrade's pre-scheme redemption log; missing this alias
286
287
  // would let a legacy paid draw run upstream again after relay upgrade.
287
288
  if (!redemptionState && oldLegacyKey) {
288
- const oldLegacyState = await redemption.state(oldLegacyKey);
289
+ const oldLegacyState = await redemption.state(oldLegacyKey, redemptionContext);
289
290
  if (oldLegacyState) {
290
291
  storedKey = oldLegacyKey;
291
292
  redemptionState = oldLegacyState;
292
293
  } else {
293
294
  // Checking the legacy marker may have refreshed a prefixed record
294
295
  // appended by another upgraded process.
295
- redemptionState = await redemption.state(cacheKey);
296
+ redemptionState = await redemption.state(cacheKey, redemptionContext);
296
297
  }
297
298
  }
298
299
  let paid;
@@ -332,7 +333,11 @@ export function createServeCore({
332
333
  return { status: 403, body: { error: 'payer_denied', detail: 'payer screening failed: ' + e.message } };
333
334
  }
334
335
  }
335
- if (redemptionState === 'complete') return { status: 200, body: await redemption.get(storedKey) };
336
+ if (redemptionState === 'complete') {
337
+ const payload = await redemption.get(storedKey, redemptionContext);
338
+ if (payload == null) return { status: 503, body: { error: 'redemption_unavailable', detail: 'saved completion is no longer readable; retry this same paid draw', _bookingId: bookingId } };
339
+ return { status: 200, body: payload };
340
+ }
336
341
  if (redemptionState === 'pending') {
337
342
  return { status: 409, body: { error: 'draw_pending', detail: 'this paid draw was already claimed; refusing to run upstream again', _bookingId: bookingId } };
338
343
  }
@@ -376,7 +381,7 @@ export function createServeCore({
376
381
  try {
377
382
  // Legacy uses its old unprefixed identity only for the atomic marker so
378
383
  // parallel upgraded stores and an existing log converge on one claim.
379
- if (!(await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey))) {
384
+ if (!(await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey, { paidAtMs: paid.paidAtMs }))) {
380
385
  return { status: 409, body: { error: 'draw_pending', detail: 'this paid draw was already claimed; refusing to run upstream again', _bookingId: bookingId } };
381
386
  }
382
387
  } catch (e) {