openpay-x402-sdk 0.10.0 → 0.10.2

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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.2 — unreleased
4
+
5
+ - Fix `createDualGate` USDC authorization reuse before settlement. Reuse JPYC's
6
+ local claim manager for v1/v2 payments, keyed by pinned chain, asset, payer and
7
+ nonce; share the USDC ledger across gate instances and endpoints in one process
8
+ to reject duplicates before relay verification and upstream execution.
9
+ - Enforce the configured upstream plus settlement validity margin before USDC
10
+ verify and again before granting upstream work. Reject unclaimable payloads.
11
+ Release tentative claims on verify rejection/failure; retain claims after
12
+ verification through upstream abandonment, settlement success, rejection and
13
+ ambiguous settlement until authorization expiry. Margins remain specific to each
14
+ calling gate. JPYC behavior/wire is unchanged.
15
+ - Clarify README/types: USDC claims are process-local, are lost on restart, and
16
+ provide no coordination across processes/isolates. Holding after definitive
17
+ settlement rejection is deliberate; retrying upstream requires a fresh nonce
18
+ and signature. Claims do not reserve funds or guarantee settlement.
19
+ No dependencies added.
20
+
21
+ ## 0.10.1 — 2026-09-23
22
+
23
+ - Create spend-store directories with mode 0700, including compatibility saves,
24
+ so an env-key user's spend directory can later hold an MCP keystore without
25
+ failing its permission checks. Existing directories are never chmod-ed; inspect
26
+ and manually repair legacy 0755 directories before switching to keystore mode.
27
+ Mode 0700 also applies to newly created intermediate directories at custom
28
+ spend-store paths; pre-existing shared/group directories keep their permissions.
29
+ - Preserve spend reservations, locks, accounting and wallet 0600/0700 checks.
30
+ No dependencies added.
31
+
3
32
  ## 0.10.0 — 2026-09-23
4
33
 
5
34
  - Breaking: seller gates require a trusted `resourceId` and `expectedRecipient`;
@@ -16,8 +45,7 @@
16
45
  `SellerPinError` and still stop both rails.
17
46
  - Update seller types, examples and generated snippets. Existing sellers must
18
47
  configure pins and redeploy the SDK/snippet; deploy the matching relay update
19
- first. No dependencies added. Unpublished; rule-15 human review is required
20
- before adoption.
48
+ first. No dependencies added.
21
49
 
22
50
  ## 0.9.0 — 2026-09-21
23
51
 
package/README.md CHANGED
@@ -113,13 +113,13 @@ before a 402, verification or settlement; map these errors to an HTTP 500 respon
113
113
  Each payment retains its validated requirements through settlement. Pass
114
114
  `openpayOrigin` to use an origin other than `https://open-pay.jp`.
115
115
 
116
- Upgrade existing seller deployments to 0.10.0 and configure the required pins,
116
+ Upgrade existing seller deployments to 0.10.2 and configure the required pins,
117
117
  or regenerate and redeploy your snippet from your own dashboard. Old installed
118
118
  SDKs and pasted snippets remain vulnerable until replaced; a package release does
119
119
  not update them. Register the listing, then retrieve its pinned snippet from the
120
120
  dashboard before serving payments.
121
121
 
122
- Before `verify()` contacts the facilitator, each gate instance claims a canonical
122
+ Before `verify()` contacts the facilitator, each JPYC gate instance claims a canonical
123
123
  authorization identity until its `validBefore` time. Set `maxUpstreamSeconds`
124
124
  (default `60`) to the seller's worst-case upstream duration; the gate also
125
125
  retains `settlementGraceSeconds` (default `30`) and rejects a duplicate before
@@ -127,9 +127,9 @@ upstream work begins. A failed facilitator verification releases the tentative
127
127
  claim, while a successful verification keeps it through settlement so an
128
128
  unknown settlement result cannot expose the same authorization twice.
129
129
 
130
- The facilitator reservation is an optional additional defense. The gate forwards
130
+ The JPYC facilitator reservation is an optional additional defense. The gate forwards
131
131
  its token when one is returned and continues on the established token-less wire
132
- when it is not. The local claim covers concurrent requests sharing one gate
132
+ when it is not. The JPYC local claim covers concurrent requests sharing one gate
133
133
  instance; separate processes or serverless isolates do not share its in-memory
134
134
  ledger.
135
135
 
@@ -167,7 +167,33 @@ const gate = createDualGate({
167
167
  });
168
168
  ```
169
169
 
170
- `handle()` / `verify()` work exactly like `createJpycGate`, on both rails.
170
+ `handle()` / `verify()` support the same verify → upstream → settle pattern on
171
+ both rails. Starting in 0.10.2, USDC v1 and v2 requests share a claim keyed by the
172
+ pinned chain and asset plus the authorization's payer and nonce. Header formatting,
173
+ address/nonce casing, request URL and other envelope metadata do not create new
174
+ claims. USDC authorizations are not bound to a resource, so the USDC ledger is
175
+ shared across gate instances and endpoints in the same process. It is lost on
176
+ restart; separate processes and serverless isolates do not coordinate. JPYC
177
+ claims remain per gate instance.
178
+
179
+ USDC rejects duplicate authorizations with `authorization_reserved` and requires
180
+ at least `maxUpstreamSeconds + settlementGraceSeconds` remaining before relay
181
+ verification and again before returning the upstream capability (defaults: 60 + 30
182
+ seconds). Each call uses its gate's configured margin even though USDC claims
183
+ are shared. Insufficient time returns `insufficient_validity_window`; an unreadable
184
+ authorization identity or expiry returns `invalid_payment_payload`.
185
+
186
+ Verify rejection or failure releases the tentative USDC claim because upstream
187
+ work has not been authorized and verify cannot settle. After verification grants
188
+ upstream work, the claim remains until `validBefore`, including when upstream is
189
+ abandoned, settlement succeeds, settlement rejects (including a relay 409), or its
190
+ outcome is unknown due to transport/JSON/503 errors. Holding after a definitive
191
+ settle rejection is deliberate: it does not undo upstream work. Even after a
192
+ balance top-up, that signature cannot start new work before `validBefore`; a retry
193
+ requires a newly signed authorization with a fresh nonce. Claims do not reserve
194
+ buyer funds: different nonces may all verify against the same balance and later
195
+ fail to settle. This local claim does not guarantee settlement or replace
196
+ facilitator/on-chain verification, and USDC does not use JPYC's reservation token.
171
197
 
172
198
  ### Register listings without the web form
173
199
 
package/index.d.ts CHANGED
@@ -322,7 +322,7 @@ export interface JpycGateOptions {
322
322
  openpayOrigin?: string;
323
323
  fetchImpl?: typeof globalThis.fetch;
324
324
  now?: () => number;
325
- /** Worst-case seller upstream duration protected by the local claim. Default: 60. */
325
+ /** Worst-case seller upstream duration; applies to both rails in createDualGate. Default: 60. */
326
326
  maxUpstreamSeconds?: number;
327
327
  /** Extra validity retained for settlement after upstream work. Default: 30. */
328
328
  settlementGraceSeconds?: number;
@@ -515,6 +515,14 @@ export interface DualGateOptions extends JpycGateOptions {
515
515
  * relayed to the CDP facilitator via OpenPay). If the USDC face cannot be fetched (relay off
516
516
  * or unavailable), the gate degrades to JPYC-only. Identity/recipient mismatches throw and log
517
517
  * before any 402 or payment call; they never trigger a rail fallback.
518
+ * USDC authorizations are not resource-bound: all gate instances in one process share a pinned
519
+ * chain/asset + payer/nonce ledger, lost on restart and not shared across processes/isolates.
520
+ * JPYC claims remain per instance. USDC v1/v2 require the calling gate's upstream + settlement
521
+ * validity margin before relay verify and before granting upstream work. Tentative verify failures
522
+ * release the claim; after upstream is granted, it remains until validBefore even if upstream is
523
+ * abandoned or settlement fails/has an unknown outcome. Holding after definitive rejection is
524
+ * deliberate: retrying upstream requires a fresh nonce/signature. Claims do not reserve buyer
525
+ * funds or guarantee settlement. USDC has no JPYC reservation token.
518
526
  */
519
527
  export function createDualGate(options: DualGateOptions): JpycGate;
520
528
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpay-x402-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Guarded Node.js buyer SDK for OpenPay x402 JPYC resources",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -0,0 +1,143 @@
1
+ // Process-local claims prevent one authorization from starting upstream work twice.
2
+ const DEFAULT_MAX_UPSTREAM_SECONDS = 60;
3
+ const DEFAULT_SETTLEMENT_GRACE_SECONDS = 30;
4
+
5
+ function isObject(value) {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+
9
+ function canonicalAddress(value) {
10
+ return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)
11
+ ? value.toLowerCase()
12
+ : null;
13
+ }
14
+
15
+ function canonicalBytes32(value) {
16
+ return typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value)
17
+ ? value.toLowerCase()
18
+ : null;
19
+ }
20
+
21
+ function canonicalDecimal(value) {
22
+ return typeof value === 'string' && /^[0-9]+$/.test(value)
23
+ ? BigInt(value).toString()
24
+ : null;
25
+ }
26
+
27
+ function canonicalNetwork(value) {
28
+ if (typeof value !== 'string') return null;
29
+ const match = /^eip155:([0-9]+)$/.exec(value);
30
+ return match ? `eip155:${BigInt(match[1]).toString()}` : null;
31
+ }
32
+
33
+ export function jpycAuthorizationClaim(paymentPayload, paymentRequirements) {
34
+ if (!isObject(paymentPayload) || !isObject(paymentRequirements)) return null;
35
+ const inner = paymentPayload.payload;
36
+ const extra = paymentRequirements.extra;
37
+ if (!isObject(inner) || !isObject(extra)) return null;
38
+ const authorization = inner.authorization;
39
+ const openpay = extra.openpay;
40
+ if (!isObject(authorization) || !isObject(openpay)) return null;
41
+
42
+ const network = canonicalNetwork(paymentPayload.network);
43
+ const asset = canonicalAddress(paymentRequirements.asset);
44
+ const from = canonicalAddress(authorization.from);
45
+ const forwarder = canonicalAddress(openpay.forwarder);
46
+ const merchant = canonicalAddress(openpay.merchant);
47
+ const merchantValue = canonicalDecimal(openpay.merchantValue);
48
+ const feeReceiver = canonicalAddress(openpay.feeReceiver);
49
+ const feeValue = canonicalDecimal(openpay.feeValue);
50
+ const validAfter = canonicalDecimal(authorization.validAfter);
51
+ const validBefore = canonicalDecimal(authorization.validBefore);
52
+ const intentSalt = canonicalBytes32(authorization.intentSalt);
53
+ const parts = [
54
+ network,
55
+ asset,
56
+ from,
57
+ forwarder,
58
+ merchant,
59
+ merchantValue,
60
+ feeReceiver,
61
+ feeValue,
62
+ validAfter,
63
+ validBefore,
64
+ intentSalt,
65
+ ];
66
+ if (parts.some((part) => part === null)) return null;
67
+ return {
68
+ key: JSON.stringify(parts),
69
+ validBefore: BigInt(validBefore),
70
+ };
71
+ }
72
+
73
+ export function usdcAuthorizationClaim(paymentPayload, paymentRequirements) {
74
+ const authorization = paymentPayload?.payload?.authorization;
75
+ if (!isObject(authorization)) return null;
76
+ // The relay rebuilds the signed domain from these pinned requirements. Untrusted
77
+ // envelope metadata must not split one on-chain nonce into separate upstream claims.
78
+ const network = canonicalNetwork(paymentRequirements.network);
79
+ const asset = canonicalAddress(paymentRequirements.asset);
80
+ const from = canonicalAddress(authorization.from);
81
+ const nonce = canonicalBytes32(authorization.nonce);
82
+ const validBefore = canonicalDecimal(authorization.validBefore);
83
+ const parts = [network, asset, from, nonce];
84
+ if (parts.some((part) => part === null) || validBefore === null) return null;
85
+ return { key: JSON.stringify(parts), validBefore: BigInt(validBefore) };
86
+ }
87
+
88
+ export function createAuthorizationClaims({
89
+ now = Date.now,
90
+ maxUpstreamSeconds = DEFAULT_MAX_UPSTREAM_SECONDS,
91
+ settlementGraceSeconds = DEFAULT_SETTLEMENT_GRACE_SECONDS,
92
+ } = {}, authorizationClaims = new Map()) {
93
+ if (!Number.isSafeInteger(maxUpstreamSeconds) || maxUpstreamSeconds < 0) {
94
+ throw new Error('maxUpstreamSeconds must be a non-negative integer');
95
+ }
96
+ if (
97
+ !Number.isSafeInteger(settlementGraceSeconds) ||
98
+ settlementGraceSeconds <= 0
99
+ ) {
100
+ throw new Error('settlementGraceSeconds must be a positive integer');
101
+ }
102
+ if (!Number.isSafeInteger(maxUpstreamSeconds + settlementGraceSeconds)) {
103
+ throw new Error('reservation validity window must be a safe integer');
104
+ }
105
+ function claimAuthorization(claim) {
106
+ if (claim === null) return { ok: true, claim: null };
107
+
108
+ const nowSec = BigInt(Math.ceil(now() / 1000));
109
+ for (const [key, existing] of authorizationClaims) {
110
+ if (existing.validBefore <= nowSec) authorizationClaims.delete(key);
111
+ }
112
+
113
+ if (!hasValidityWindow(claim, nowSec)) {
114
+ return { ok: false, reason: 'insufficient_validity_window' };
115
+ }
116
+ if (authorizationClaims.has(claim.key)) {
117
+ return { ok: false, reason: 'authorization_reserved' };
118
+ }
119
+
120
+ const owner = Symbol();
121
+ authorizationClaims.set(claim.key, {
122
+ owner,
123
+ validBefore: claim.validBefore,
124
+ });
125
+ return { ok: true, claim: { key: claim.key, owner } };
126
+ }
127
+
128
+ function releaseAuthorization(claim) {
129
+ // An expired verify attempt must not release a newer owner's claim for the same identity.
130
+ if (
131
+ claim !== null &&
132
+ authorizationClaims.get(claim.key)?.owner === claim.owner
133
+ ) {
134
+ authorizationClaims.delete(claim.key);
135
+ }
136
+ }
137
+
138
+ function hasValidityWindow(claim, nowSec = BigInt(Math.ceil(now() / 1000))) {
139
+ return claim.validBefore - nowSec >= BigInt(maxUpstreamSeconds + settlementGraceSeconds);
140
+ }
141
+
142
+ return { claimAuthorization, releaseAuthorization, hasValidityWindow };
143
+ }
package/src/dualGate.mjs CHANGED
@@ -12,10 +12,14 @@
12
12
  // JPYC レールの 402 には USDC accepts を追記 (decorate) して両面を常に見せる。
13
13
 
14
14
  import { createJpycGate } from './gate.mjs';
15
+ import { createAuthorizationClaims, usdcAuthorizationClaim } from './authorizationClaims.mjs';
15
16
  import { assertSellerPins, SellerPinError, validateUsdcFace } from './sellerPins.mjs';
16
17
 
17
18
  const DEFAULT_OPENPAY_ORIGIN = 'https://open-pay.jp';
18
19
  const USDC_FACE_CACHE_MS = 5 * 60_000;
20
+ // USDC authorizations are not resource-bound: share claims so another endpoint's gate
21
+ // cannot start duplicate upstream work with the same chain/asset/payer/nonce.
22
+ const usdcAuthorizationClaims = new Map();
19
23
 
20
24
  function decodeBase64Json(value) {
21
25
  const binary = atob(value);
@@ -54,6 +58,9 @@ export function createDualGate({
54
58
  ...(settlementGraceSeconds === undefined ? {} : { settlementGraceSeconds }),
55
59
  });
56
60
  const origin = openpayOrigin.replace(/\/+$/, '');
61
+ const { claimAuthorization, releaseAuthorization, hasValidityWindow } = createAuthorizationClaims({
62
+ now, maxUpstreamSeconds, settlementGraceSeconds,
63
+ }, usdcAuthorizationClaims);
57
64
 
58
65
  let usdcCache = null;
59
66
  let usdcCachedAt = 0;
@@ -152,6 +159,19 @@ export function createDualGate({
152
159
 
153
160
  if (usdcRail) {
154
161
  const jpycAccepts = await availableJpycAccepts(request);
162
+ let paymentPayload;
163
+ try {
164
+ paymentPayload = decodeBase64Json(signatureHeader || v1Header);
165
+ } catch {
166
+ // A malformed preferred header must not bypass the claim via the other rail/header.
167
+ return usdcChallenge(usdc, jpycAccepts, 'invalid_payment_payload');
168
+ }
169
+ const authorization = usdcAuthorizationClaim(paymentPayload, usdc.v2Accept);
170
+ // Without a usable identity/expiry, verification cannot safely grant upstream work.
171
+ if (authorization === null) return usdcChallenge(usdc, jpycAccepts, 'invalid_payment_payload');
172
+ const claimed = claimAuthorization(authorization);
173
+ if (!claimed.ok) return usdcChallenge(usdc, jpycAccepts, claimed.reason);
174
+
155
175
  const relay = async (path) => {
156
176
  const response = await fetchImpl(`${origin}/api/x402/relay/${path}`, {
157
177
  method: 'POST',
@@ -174,11 +194,29 @@ export function createDualGate({
174
194
  }
175
195
  return response.json();
176
196
  };
177
- const verification = await relay('verify');
178
- if (verification instanceof Response) return verification;
197
+ let verification;
198
+ try {
199
+ verification = await relay('verify');
200
+ } catch (error) {
201
+ // Verify cannot settle or grant upstream work on failure; release only this tentative owner.
202
+ releaseAuthorization(claimed.claim);
203
+ throw error;
204
+ }
205
+ if (verification instanceof Response) {
206
+ releaseAuthorization(claimed.claim);
207
+ return verification;
208
+ }
179
209
  if (verification.isValid !== true) {
210
+ releaseAuthorization(claimed.claim);
180
211
  return usdcChallenge(usdc, jpycAccepts, verification.invalidReason || 'payment_invalid');
181
212
  }
213
+ // A slow verify must not consume the margin promised to the seller's upstream work.
214
+ if (!hasValidityWindow(authorization)) {
215
+ return usdcChallenge(usdc, jpycAccepts, 'insufficient_validity_window');
216
+ }
217
+ // As in JPYC, keep the claim until validBefore once upstream can run. Settlement
218
+ // errors (including ambiguous transport/JSON/503 and 409) must not repeat that work;
219
+ // even a definitive settle rejection cannot undo an already executed upstream call.
182
220
  return {
183
221
  async settle() {
184
222
  const settlement = await relay('settle');
package/src/gate.mjs CHANGED
@@ -1,9 +1,8 @@
1
+ import { createAuthorizationClaims, jpycAuthorizationClaim } from './authorizationClaims.mjs';
1
2
  import { assertSellerPins, validateJpycListing } from './sellerPins.mjs';
2
3
 
3
4
  const DEFAULT_OPENPAY_ORIGIN = 'https://open-pay.jp';
4
5
  const ACCEPTS_CACHE_MS = 5 * 60_000;
5
- const DEFAULT_MAX_UPSTREAM_SECONDS = 60;
6
- const DEFAULT_SETTLEMENT_GRACE_SECONDS = 30;
7
6
 
8
7
  function json402(accepts, error) {
9
8
  return new Response(JSON.stringify({ x402Version: 1, accepts, error }), {
@@ -25,74 +24,6 @@ function encodeBase64Json(value) {
25
24
  return btoa(binary);
26
25
  }
27
26
 
28
- function isObject(value) {
29
- return typeof value === 'object' && value !== null && !Array.isArray(value);
30
- }
31
-
32
- function canonicalAddress(value) {
33
- return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)
34
- ? value.toLowerCase()
35
- : null;
36
- }
37
-
38
- function canonicalBytes32(value) {
39
- return typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value)
40
- ? value.toLowerCase()
41
- : null;
42
- }
43
-
44
- function canonicalDecimal(value) {
45
- return typeof value === 'string' && /^[0-9]+$/.test(value)
46
- ? BigInt(value).toString()
47
- : null;
48
- }
49
-
50
- function canonicalNetwork(value) {
51
- if (typeof value !== 'string') return null;
52
- const match = /^eip155:([0-9]+)$/.exec(value);
53
- return match ? `eip155:${BigInt(match[1]).toString()}` : null;
54
- }
55
-
56
- function authorizationClaim(paymentPayload, paymentRequirements) {
57
- if (!isObject(paymentPayload) || !isObject(paymentRequirements)) return null;
58
- const inner = paymentPayload.payload;
59
- const extra = paymentRequirements.extra;
60
- if (!isObject(inner) || !isObject(extra)) return null;
61
- const authorization = inner.authorization;
62
- const openpay = extra.openpay;
63
- if (!isObject(authorization) || !isObject(openpay)) return null;
64
-
65
- const network = canonicalNetwork(paymentPayload.network);
66
- const asset = canonicalAddress(paymentRequirements.asset);
67
- const from = canonicalAddress(authorization.from);
68
- const forwarder = canonicalAddress(openpay.forwarder);
69
- const merchant = canonicalAddress(openpay.merchant);
70
- const merchantValue = canonicalDecimal(openpay.merchantValue);
71
- const feeReceiver = canonicalAddress(openpay.feeReceiver);
72
- const feeValue = canonicalDecimal(openpay.feeValue);
73
- const validAfter = canonicalDecimal(authorization.validAfter);
74
- const validBefore = canonicalDecimal(authorization.validBefore);
75
- const intentSalt = canonicalBytes32(authorization.intentSalt);
76
- const parts = [
77
- network,
78
- asset,
79
- from,
80
- forwarder,
81
- merchant,
82
- merchantValue,
83
- feeReceiver,
84
- feeValue,
85
- validAfter,
86
- validBefore,
87
- intentSalt,
88
- ];
89
- if (parts.some((part) => part === null)) return null;
90
- return {
91
- key: JSON.stringify(parts),
92
- validBefore: BigInt(validBefore),
93
- };
94
- }
95
-
96
27
  export function createJpycGate({
97
28
  resourceUrl,
98
29
  resourceId,
@@ -100,62 +31,16 @@ export function createJpycGate({
100
31
  openpayOrigin = DEFAULT_OPENPAY_ORIGIN,
101
32
  fetchImpl = globalThis.fetch,
102
33
  now = Date.now,
103
- maxUpstreamSeconds = DEFAULT_MAX_UPSTREAM_SECONDS,
104
- settlementGraceSeconds = DEFAULT_SETTLEMENT_GRACE_SECONDS,
34
+ maxUpstreamSeconds,
35
+ settlementGraceSeconds,
105
36
  } = {}) {
106
37
  assertSellerPins(resourceId, expectedRecipient);
107
- if (!Number.isSafeInteger(maxUpstreamSeconds) || maxUpstreamSeconds < 0) {
108
- throw new Error('maxUpstreamSeconds must be a non-negative integer');
109
- }
110
- if (
111
- !Number.isSafeInteger(settlementGraceSeconds) ||
112
- settlementGraceSeconds <= 0
113
- ) {
114
- throw new Error('settlementGraceSeconds must be a positive integer');
115
- }
116
- if (!Number.isSafeInteger(maxUpstreamSeconds + settlementGraceSeconds)) {
117
- throw new Error('reservation validity window must be a safe integer');
118
- }
38
+ const { claimAuthorization, releaseAuthorization } = createAuthorizationClaims({
39
+ now, maxUpstreamSeconds, settlementGraceSeconds,
40
+ });
119
41
  const origin = openpayOrigin.replace(/\/+$/, '');
120
42
  let acceptsCache = null;
121
43
  let acceptsCachedAt = 0;
122
- const authorizationClaims = new Map();
123
-
124
- function claimAuthorization(paymentPayload, paymentRequirements) {
125
- const claim = authorizationClaim(paymentPayload, paymentRequirements);
126
- if (claim === null) return { ok: true, claim: null };
127
-
128
- const nowSec = BigInt(Math.ceil(now() / 1000));
129
- for (const [key, existing] of authorizationClaims) {
130
- if (existing.validBefore <= nowSec) authorizationClaims.delete(key);
131
- }
132
-
133
- const requiredValidity = BigInt(
134
- maxUpstreamSeconds + settlementGraceSeconds,
135
- );
136
- if (claim.validBefore - nowSec < requiredValidity) {
137
- return { ok: false, reason: 'insufficient_validity_window' };
138
- }
139
- if (authorizationClaims.has(claim.key)) {
140
- return { ok: false, reason: 'authorization_reserved' };
141
- }
142
-
143
- const owner = Symbol();
144
- authorizationClaims.set(claim.key, {
145
- owner,
146
- validBefore: claim.validBefore,
147
- });
148
- return { ok: true, claim: { key: claim.key, owner } };
149
- }
150
-
151
- function releaseAuthorization(claim) {
152
- if (
153
- claim !== null &&
154
- authorizationClaims.get(claim.key)?.owner === claim.owner
155
- ) {
156
- authorizationClaims.delete(claim.key);
157
- }
158
- }
159
44
 
160
45
  async function catalogAccepts() {
161
46
  if (
@@ -211,7 +96,7 @@ export function createJpycGate({
211
96
  }
212
97
 
213
98
  const paymentRequirements = accepts[0];
214
- const claimed = claimAuthorization(paymentPayload, paymentRequirements);
99
+ const claimed = claimAuthorization(jpycAuthorizationClaim(paymentPayload, paymentRequirements));
215
100
  if (!claimed.ok) return json402(accepts, claimed.reason);
216
101
 
217
102
  let verification;
@@ -256,7 +256,9 @@ export function createFileSpendStore({ path, fsImpl } = {}) {
256
256
  if (!supportsAtomicFileUpdates(runtime.fileSystem)) {
257
257
  throw new Error('atomic spend store file operations are unavailable');
258
258
  }
259
- await runtime.fileSystem.mkdir(runtime.directory, { recursive: true });
259
+ // Spend storage shares the default wallet directory: public directory bits would block
260
+ // a later keystore migration. Keep new directories private without chmod-ing existing ones.
261
+ await runtime.fileSystem.mkdir(runtime.directory, { recursive: true, mode: 0o700 });
260
262
  const handle = await acquireLock(runtime.fileSystem, runtime.lockPath);
261
263
  try {
262
264
  return await operation(runtime);
@@ -438,7 +440,8 @@ export function createFileSpendStore({ path, fsImpl } = {}) {
438
440
  }
439
441
  const current = documentForDate(document, key.slice(-10));
440
442
  current[key] = atomicString;
441
- await runtime.fileSystem.mkdir(runtime.directory, { recursive: true });
443
+ // Compatibility saves must not create a shared directory the keystore would reject.
444
+ await runtime.fileSystem.mkdir(runtime.directory, { recursive: true, mode: 0o700 });
442
445
  await runtime.fileSystem.writeFile(
443
446
  runtime.targetPath,
444
447
  `${JSON.stringify(current, null, 2)}\n`,