mtok-bridge 0.3.2 → 0.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-bridge",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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": {
package/src/bridge.mjs CHANGED
@@ -55,7 +55,7 @@ export async function serveChat({ body, authHeader, apiKey, models, upstream })
55
55
  // `baseUrl` is the API root (e.g. https://api.openai.com/v1 or a local model server); `key` is
56
56
  // its bearer token (optional for a keyless local server). This is what makes the bridge portable:
57
57
  // point it at a provider, or at ollama / LM Studio / vLLM on localhost.
58
- export function httpUpstream({ baseUrl, key }) {
58
+ export function httpUpstream({ baseUrl, key, timeoutMs }) {
59
59
  const url = String(baseUrl || '').replace(/\/$/, '') + '/chat/completions';
60
60
  return async (payload) => {
61
61
  const res = await fetch(url, {
@@ -65,6 +65,7 @@ export function httpUpstream({ baseUrl, key }) {
65
65
  ...(key ? { authorization: `Bearer ${key}` } : {}),
66
66
  },
67
67
  body: JSON.stringify(payload),
68
+ ...(timeoutMs == null ? {} : { signal: AbortSignal.timeout(timeoutMs) }),
68
69
  });
69
70
  const text = await res.text();
70
71
  let json;
@@ -243,11 +243,6 @@ export function createServeCore({
243
243
  maxOutputTokens,
244
244
  }) {
245
245
  const serve = async (body) => {
246
- // #651: feeBps/feeRecipient may be getters so a long-running relay tracks a
247
- // platform fee change instead of pinning the boot rate (a fee DECREASE with a
248
- // stale higher rate would refuse an already-paid draw as fee_amount_too_low).
249
- // Resolve once per serve and use the resolved values everywhere below.
250
- const currentFeeBps = typeof feeBps === 'function' ? feeBps() : feeBps;
251
246
  const currentFeeRecipient = typeof feeRecipient === 'function' ? feeRecipient() : feeRecipient;
252
247
  const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
253
248
  const hasRequestNonce = Object.hasOwn(body, 'requestNonce');
@@ -282,6 +277,24 @@ export function createServeCore({
282
277
  return { status: 402, body: { error: 'contract_mode_required', detail: 'this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress' } };
283
278
  }
284
279
  if (!drawPaidTxHash) return { status: 402, body: { error: 'draw_payment_required', detail: 'contract mode requires drawPaidTxHash before upstream delivery' } };
280
+ let storedKey = cacheKey;
281
+ // The redemption interface may be sync (fs store) or async (a Workers KV
282
+ // store): await normalizes both, and identity-awaits cost nothing under the
283
+ // host's per-booking lock.
284
+ let redemptionState = await redemption.state(storedKey);
285
+ // Preserve an upgrade's pre-scheme redemption log; missing this alias
286
+ // would let a legacy paid draw run upstream again after relay upgrade.
287
+ if (!redemptionState && oldLegacyKey) {
288
+ const oldLegacyState = await redemption.state(oldLegacyKey);
289
+ if (oldLegacyState) {
290
+ storedKey = oldLegacyKey;
291
+ redemptionState = oldLegacyState;
292
+ } else {
293
+ // Checking the legacy marker may have refreshed a prefixed record
294
+ // appended by another upgraded process.
295
+ redemptionState = await redemption.state(cacheKey);
296
+ }
297
+ }
285
298
  let paid;
286
299
  try {
287
300
  paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
@@ -295,25 +308,16 @@ export function createServeCore({
295
308
  requestHash,
296
309
  sellerWallet,
297
310
  feeRecipient: currentFeeRecipient,
298
- // #580: refuse a payment older than the redemption window. The JSONL
299
- // payload cache AND the claim markers are both aged out at boot (#600),
300
- // so past retention this age bound is the sole replay defense (its
301
- // skip-on-unreadable-block residual is named in redemption.mjs). An
302
- // honest retry is seconds-to-minutes old, never days.
303
- maxPaidAgeMs: redemption.retentionMs,
311
+ // A known claim spends no new inference. New claims need a verified
312
+ // age because both payload and claim markers expire after retention.
313
+ maxPaidAgeMs: redemptionState === 'complete' || redemptionState === 'pending' ? undefined : redemption.retentionMs,
304
314
  });
305
315
  } catch (e) {
316
+ if (e.name === 'TimeoutError') return { status: 503, body: { error: 'relay_timeout', _bookingId: bookingId } };
306
317
  return { status: 402, body: { error: 'payment_unverified', detail: e.message } };
307
318
  }
319
+ if (paid?.reason === 'payment_age_unavailable') return { status: 503, body: { error: 'payment_age_unavailable', detail: 'payment age could not be verified; retry this same paid draw', _bookingId: bookingId } };
308
320
  if (!paid?.ok) return { status: 402, body: { error: 'payment_unverified', detail: paid?.reason || 'unknown' } };
309
- const expectedFee = configuredFeeAtomic({
310
- sellerUsdAtomic: paid.event.sellerUsdAtomic,
311
- feeAddress: currentFeeRecipient,
312
- feeBps: currentFeeBps,
313
- });
314
- if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
315
- return { status: 402, body: { error: 'payment_unverified', detail: 'fee_amount_too_low' } };
316
- }
317
321
  // Screen the verified payer before spending upstream capacity. The
318
322
  // payment already settled on-chain (that money is the buyer's loss);
319
323
  // this refuses the SERVICE, which is the only refusal an edge can
@@ -328,28 +332,23 @@ export function createServeCore({
328
332
  return { status: 403, body: { error: 'payer_denied', detail: 'payer screening failed: ' + e.message } };
329
333
  }
330
334
  }
331
- let storedKey = cacheKey;
332
- // The redemption interface may be sync (fs store) or async (a Workers KV
333
- // store): await normalizes both, and identity-awaits cost nothing under the
334
- // host's per-booking lock.
335
- let redemptionState = await redemption.state(storedKey);
336
- // Preserve an upgrade's pre-scheme redemption log; missing this alias
337
- // would let a legacy paid draw run upstream again after relay upgrade.
338
- if (!redemptionState && oldLegacyKey) {
339
- const oldLegacyState = await redemption.state(oldLegacyKey);
340
- if (oldLegacyState) {
341
- storedKey = oldLegacyKey;
342
- redemptionState = oldLegacyState;
343
- } else {
344
- // Checking the legacy marker may have refreshed a prefixed record
345
- // appended by another upgraded process.
346
- redemptionState = await redemption.state(cacheKey);
347
- }
348
- }
349
335
  if (redemptionState === 'complete') return { status: 200, body: await redemption.get(storedKey) };
350
336
  if (redemptionState === 'pending') {
351
337
  return { status: 409, body: { error: 'draw_pending', detail: 'this paid draw was already claimed; refusing to run upstream again', _bookingId: bookingId } };
352
338
  }
339
+ // Completed/pending claims already crossed the fee gate and cannot spend
340
+ // again. New claims use the policy at the verified payment time.
341
+ let currentFeeBps;
342
+ try {
343
+ currentFeeBps = typeof feeBps === 'function' ? await feeBps(paid) : feeBps ?? 0;
344
+ if (!Number.isSafeInteger(currentFeeBps) || currentFeeBps < 0 || currentFeeBps > 10_000) throw new TypeError('invalid fee rate');
345
+ } catch {
346
+ return { status: 503, body: { error: 'fee_policy_unavailable', detail: 'fee policy could not be verified; retry this same paid draw', _bookingId: bookingId } };
347
+ }
348
+ const expectedFee = configuredFeeAtomic({ sellerUsdAtomic: paid.event.sellerUsdAtomic, feeAddress: currentFeeRecipient, feeBps: currentFeeBps });
349
+ if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
350
+ return { status: 402, body: { error: 'payment_unverified', detail: 'fee_amount_too_low' } };
351
+ }
353
352
  const paidEvent = paid.event;
354
353
  const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
355
354
  if (remainingUsd < BALANCE_EPSILON) {
@@ -388,6 +387,7 @@ export function createServeCore({
388
387
  try {
389
388
  completion = await upstream(safeRequest);
390
389
  } catch (e) {
390
+ if (e.name === 'TimeoutError') return { status: 503, body: { error: 'relay_timeout', _bookingId: bookingId } };
391
391
  return { status: 502, body: { error: 'upstream_error', detail: e.message } };
392
392
  }
393
393