@bounded-sh/core 0.0.114 → 0.0.115
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/client/operations.d.ts +14 -0
- package/dist/client/reconcile.d.ts +18 -0
- package/dist/client/venue-grant-write.d.ts +62 -0
- package/dist/client/write-outcome.d.ts +1 -0
- package/dist/{index--ahzzmm5.js → index-5TMFraRK.js} +225 -60
- package/dist/index-5TMFraRK.js.map +1 -0
- package/dist/{index-DTTh5Wt5.mjs → index-BilGFb3V.mjs} +224 -61
- package/dist/index-BilGFb3V.mjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{realtime-store-Bcra04TH.mjs → realtime-store-Bc71mTWr.mjs} +2 -2
- package/dist/{realtime-store-Bcra04TH.mjs.map → realtime-store-Bc71mTWr.mjs.map} +1 -1
- package/dist/{realtime-store-CR80Jac5.js → realtime-store-BiChlioj.js} +2 -2
- package/dist/{realtime-store-CR80Jac5.js.map → realtime-store-BiChlioj.js.map} +1 -1
- package/dist/{solana-message-BwPW9Fst.mjs → solana-message-BNGEBst_.mjs} +2 -2
- package/dist/{solana-message-BwPW9Fst.mjs.map → solana-message-BNGEBst_.mjs.map} +1 -1
- package/dist/{solana-message-DPR9uw0N.js → solana-message-BVwD6WVm.js} +2 -2
- package/dist/{solana-message-DPR9uw0N.js.map → solana-message-BVwD6WVm.js.map} +1 -1
- package/dist/{solana-operations-BMB6sq_i.js → solana-operations-B3u6Arbf.js} +3 -3
- package/dist/{solana-operations-BMB6sq_i.js.map → solana-operations-B3u6Arbf.js.map} +1 -1
- package/dist/{solana-operations-BBQllFVm.mjs → solana-operations-BLbRO1rE.mjs} +3 -3
- package/dist/{solana-operations-BBQllFVm.mjs.map → solana-operations-BLbRO1rE.mjs.map} +1 -1
- package/dist/{solana-v1-operations-CCHbLEo-.js → solana-v1-operations-BxTmxUMN.js} +3 -3
- package/dist/{solana-v1-operations-CCHbLEo-.js.map → solana-v1-operations-BxTmxUMN.js.map} +1 -1
- package/dist/{solana-v1-operations-DBuA0PlD.mjs → solana-v1-operations-b1rmLBUP.mjs} +3 -3
- package/dist/{solana-v1-operations-DBuA0PlD.mjs.map → solana-v1-operations-b1rmLBUP.mjs.map} +1 -1
- package/dist/utils/refresh-error.d.ts +4 -0
- package/package.json +2 -2
- package/dist/index--ahzzmm5.js.map +0 -1
- package/dist/index-DTTh5Wt5.mjs.map +0 -1
|
@@ -30,6 +30,20 @@ export type RequestOverrides = {
|
|
|
30
30
|
* deterministically; production builds a `Connection` from `config.rpcUrl`.
|
|
31
31
|
*/
|
|
32
32
|
_solanaRpc?: SolanaWriteRpc;
|
|
33
|
+
/**
|
|
34
|
+
* @internal The venue grant write lane (venue-grant-write.ts). A single-use grant, not the
|
|
35
|
+
* caller's session, authenticates the write, so `setMany` also lets the lane refuse the
|
|
36
|
+
* execution lane a 202 reports, takes the poofnet commit's credential from the lane, and
|
|
37
|
+
* skips the post-confirm mirror read a grant cannot authorize. App code never sets it.
|
|
38
|
+
*/
|
|
39
|
+
_grantLane?: GrantLane;
|
|
40
|
+
};
|
|
41
|
+
/** @internal See {@link RequestOverrides._grantLane}. */
|
|
42
|
+
export type GrantLane = {
|
|
43
|
+
/** Throws to refuse `chain`, the execution lane the 202 reported, before anything is signed. */
|
|
44
|
+
assertLane(chain: string): void;
|
|
45
|
+
/** The credential for the poofnet `PUT items/signed` commit. A grant is single use, so each call mints a fresh one. */
|
|
46
|
+
commitHeaders(): Promise<Record<string, string>>;
|
|
33
47
|
};
|
|
34
48
|
export type SetOptions = {
|
|
35
49
|
shouldSubmitTx?: boolean;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliation for a `submitted` write receipt.
|
|
3
|
+
*
|
|
4
|
+
* A returned object cannot transition itself, and calling `set` again builds a
|
|
5
|
+
* FRESH transaction — which is exactly what must not happen while the first
|
|
6
|
+
* signature may still land. Without this call the `submitted` state is a dead
|
|
7
|
+
* end and its terminal exits are unreachable.
|
|
8
|
+
*
|
|
9
|
+
* Everything here polls the SAME signature. Nothing here ever builds a new
|
|
10
|
+
* transaction or broadcasts anything.
|
|
11
|
+
*/
|
|
12
|
+
import type { ClientConfig } from './config';
|
|
1
13
|
import type { RequestOverrides } from './operations';
|
|
2
14
|
import type { SetResult } from '../types';
|
|
3
15
|
export type ReconcileSetResultOptions = {
|
|
@@ -18,6 +30,12 @@ export type WaitForSetResultOptions = ReconcileSetResultOptions & {
|
|
|
18
30
|
/** Cancels the wait. The latest `submitted` receipt is returned, never discarded. */
|
|
19
31
|
signal?: AbortSignal;
|
|
20
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The Solana execution lane a client config REQUIRES, or `null` when the config
|
|
35
|
+
* carries no constraint. Only a positive contradiction rejects a receipt — an
|
|
36
|
+
* unmapped or unset `chain` is not evidence of a mismatch.
|
|
37
|
+
*/
|
|
38
|
+
export declare function requiredSolanaChain(config: ClientConfig): string | null;
|
|
21
39
|
/**
|
|
22
40
|
* Poll the SAME signature a `submitted` receipt carries and return the
|
|
23
41
|
* resulting `SetResult`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The venue grant write lane: create ONE document in another app (an OpenApps venue) as the
|
|
3
|
+
* wallet signed in here, authorized by a single-use grant instead of a venue session
|
|
4
|
+
* (VENUE-GRANT-DESIGN.md). The grant only lets the venue build that one transaction for that
|
|
5
|
+
* wallet; the wallet's own signature, checked by the venue's own rule, is the authority.
|
|
6
|
+
*
|
|
7
|
+
* The grant itself is minted by Bounded Auth at `POST {humanAuthApiUrl}/app-token/venue-grant`
|
|
8
|
+
* (contract: packages/cdk/cloudflare/shared/venue-grant.ts, which this package cannot import
|
|
9
|
+
* because the SDK packages publish separately). This module only spends one.
|
|
10
|
+
*
|
|
11
|
+
* It reuses `setMany`'s request and 202 handling through `_overrides`: the `_config` snapshot is
|
|
12
|
+
* this app's own config re-pointed at the VENUE app id, so the pre-sign validator (OA-0001) binds
|
|
13
|
+
* the venue app id and the full expected intent, and the signer stays this app's own AuthProvider
|
|
14
|
+
* (a Turnkey session or a Wallet Standard wallet). Nothing here is reachable from `createClient()`,
|
|
15
|
+
* which stays read-only.
|
|
16
|
+
*
|
|
17
|
+
* The chain a write runs on is the VENUE's, stated by its 202, never the app's: the app's own
|
|
18
|
+
* `chain` can only agree with it or be refused, and the transaction is broadcast through the app's
|
|
19
|
+
* `rpcUrl` only when the app configured that same chain, otherwise through `resolveRpcUrl(chain)`.
|
|
20
|
+
*/
|
|
21
|
+
import { type ClientConfig } from './config';
|
|
22
|
+
import { type SetOptions } from './operations';
|
|
23
|
+
import type { SetResult, SolanaWriteRpc } from '../types';
|
|
24
|
+
export interface VenueGrantWriteOptions {
|
|
25
|
+
/** The venue app the grant was minted for. The write and its 202 validation bind THIS app id, never the caller's own. */
|
|
26
|
+
venueAppId: string;
|
|
27
|
+
/** The single-use grant that authorizes this one create. */
|
|
28
|
+
grant: string;
|
|
29
|
+
/**
|
|
30
|
+
* Mints a fresh grant for the same kind and id. The `PUT items` that built the approval spent the
|
|
31
|
+
* first, so the poofnet `PUT items/signed` commit needs its own. Called only on poofnet, after the
|
|
32
|
+
* wallet has signed.
|
|
33
|
+
*/
|
|
34
|
+
commitGrant: () => Promise<string>;
|
|
35
|
+
/** As `set`'s `onBroadcast`: persist the signed transaction's receipt before it is broadcast (Solana lanes). */
|
|
36
|
+
onBroadcast?: SetOptions['onBroadcast'];
|
|
37
|
+
/**
|
|
38
|
+
* The RPC for a Solana lane the app did not configure itself: given the chain the venue's 202
|
|
39
|
+
* reported (`solana_devnet` or `solana_mainnet`), its RPC url, or null for none. The client SDK
|
|
40
|
+
* passes its platform default. See {@link resolveVenueRpcUrl} for how it ranks against the app's own.
|
|
41
|
+
*/
|
|
42
|
+
resolveRpcUrl?: (chain: string) => string | null | undefined;
|
|
43
|
+
/** @internal Test seam, as `RequestOverrides._solanaRpc`. */
|
|
44
|
+
_solanaRpc?: SolanaWriteRpc;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The RPC a venue write on Solana lane `chain` is broadcast through, and a saved receipt of it is
|
|
48
|
+
* reconciled through. The app's own `rpcUrl` counts only when the app also configured this chain
|
|
49
|
+
* (an `rpcUrl` alone does not say which chain it speaks); otherwise `resolveRpcUrl(chain)`. Null
|
|
50
|
+
* when neither yields one.
|
|
51
|
+
*
|
|
52
|
+
* @internal Shared by the lane and by the client SDK's reconciliation of a saved receipt.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveVenueRpcUrl(config: ClientConfig, chain: string, resolveRpcUrl?: (chain: string) => string | null | undefined): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Write `document` at `path` in the venue `venueAppId` with a single-use grant. Resolves to the
|
|
57
|
+
* same discriminated {@link SetResult} as `set`, and throws exactly as `set` does, so the same
|
|
58
|
+
* `isWriteNotApplied` reading applies.
|
|
59
|
+
*
|
|
60
|
+
* @internal Used by `fundVenueEscrow` and `disputeVenueEscrow` in @bounded-sh/client. Not a stable API.
|
|
61
|
+
*/
|
|
62
|
+
export declare function setWithVenueGrant(path: string, document: Record<string, unknown>, options: VenueGrantWriteOptions): Promise<SetResult>;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* `onBroadcast` hook refusing). Nothing was sent.
|
|
15
15
|
* - `broadcast_rejected`: the RPC definitively refused the signed bytes
|
|
16
16
|
* (preflight simulation, invalid transaction). They never entered the network.
|
|
17
|
+
* On poofnet, the realtime worker answering the signed commit with a 4xx.
|
|
17
18
|
* - `failed_onchain`: the transaction landed and failed, so the write reverted.
|
|
18
19
|
*
|
|
19
20
|
* The tag is plain data on the thrown object (`writeOutcome` + `writePhase`),
|
|
@@ -421,6 +421,97 @@ axiosRetry.exponentialDelay = exponentialDelay;
|
|
|
421
421
|
axiosRetry.linearDelay = linearDelay;
|
|
422
422
|
axiosRetry.isRetryableError = isRetryableError;
|
|
423
423
|
|
|
424
|
+
// Classify a session-refresh failure as a DEFINITIVE rejection of the credential
|
|
425
|
+
// vs a TRANSPORT failure (DS4-0054).
|
|
426
|
+
//
|
|
427
|
+
// The restore path refreshes an expired token at boot. The old code destroyed the
|
|
428
|
+
// local session on ANY refresh failure, so a network blip - common on mobile -
|
|
429
|
+
// signed the user out. Only a real HTTP rejection of the refresh token proves the
|
|
430
|
+
// session is invalid; anything else is unknown, and the safe move is to RETAIN local
|
|
431
|
+
// state and retry. Keeping a genuinely dead session locally is harmless: every
|
|
432
|
+
// request still fails closed server-side, and the next refresh that reaches the
|
|
433
|
+
// server clears it. Destroying a live session on a blip is user-visible damage.
|
|
434
|
+
//
|
|
435
|
+
// `/session/refresh` answers 401 (session_revoked / invalid_grant) when the token is
|
|
436
|
+
// dead. 400 (bad request), 408/429 (transient), and 5xx are NOT proof the session is
|
|
437
|
+
// invalid, and a network/DNS/timeout error carries no HTTP response at all.
|
|
438
|
+
//
|
|
439
|
+
// This is the single classifier for every refresh path: the per-request freshness
|
|
440
|
+
// check (auth-token-freshness) and the boot-time restore paths (web + RN session
|
|
441
|
+
// managers) all import it, so "definitive" means the same thing everywhere. It lives
|
|
442
|
+
// in this leaf module to avoid an import cycle between those consumers.
|
|
443
|
+
function isDefinitiveRejection(err) {
|
|
444
|
+
var _a;
|
|
445
|
+
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
446
|
+
return status === 401 || status === 403;
|
|
447
|
+
}
|
|
448
|
+
// How long past the id token's own `exp` a session may survive purely on
|
|
449
|
+
// transport failures.
|
|
450
|
+
//
|
|
451
|
+
// "Retain on a blip" must not become "retain forever". A statusless failure is
|
|
452
|
+
// unknown, so the first ones are absorbed - but a revoked session, or a gateway
|
|
453
|
+
// that rewrites the issuer's 401 into a 502, would otherwise keep
|
|
454
|
+
// isAuthenticated() reporting a signed-in user for as long as the device never
|
|
455
|
+
// reaches the issuer again. One day covers an offline flight or a bad-network day
|
|
456
|
+
// while making the retention finite: past it, an unrefreshable session is cleared
|
|
457
|
+
// and the user is asked to sign in again.
|
|
458
|
+
const TRANSPORT_RETENTION_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
459
|
+
/**
|
|
460
|
+
* May a session whose refresh failed WITHOUT a definitive rejection be kept?
|
|
461
|
+
*
|
|
462
|
+
* `expSeconds` is the id token's `exp` claim. Fails closed: a missing or
|
|
463
|
+
* non-numeric expiry is not a usable bound, so such a session is never retained.
|
|
464
|
+
*/
|
|
465
|
+
function mayRetainAfterTransportFailure(expSeconds, now = Date.now()) {
|
|
466
|
+
if (typeof expSeconds !== 'number' || !Number.isFinite(expSeconds))
|
|
467
|
+
return false;
|
|
468
|
+
return now <= expSeconds * 1000 + TRANSPORT_RETENTION_GRACE_MS;
|
|
469
|
+
}
|
|
470
|
+
// Backoff between refresh attempts that failed WITHOUT a definitive rejection.
|
|
471
|
+
//
|
|
472
|
+
// A transport failure retains the session (above), and every authenticated request
|
|
473
|
+
// and the realtime token timer asks for a fresh token - so without a pause, an issuer
|
|
474
|
+
// that answers 503 or 429 gets one refresh per request from every open tab, which is
|
|
475
|
+
// the exact load that keeps it down. After such a failure the same refresh token is
|
|
476
|
+
// not sent again until the backoff passes: 15s, doubling, capped at 10 minutes, or the
|
|
477
|
+
// issuer's own Retry-After when it is longer. Callers inside the window get a
|
|
478
|
+
// non-definitive error at once (`code: 'refresh_backoff'`) and treat it like the
|
|
479
|
+
// failure that started it. A success, or a different token, starts clean.
|
|
480
|
+
const REFRESH_BACKOFF_BASE_MS = 15000;
|
|
481
|
+
const REFRESH_BACKOFF_MAX_MS = 10 * 60000;
|
|
482
|
+
const refreshBackoff = new Map();
|
|
483
|
+
function retryAfterMs(err) {
|
|
484
|
+
var _a, _b;
|
|
485
|
+
const headers = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.headers;
|
|
486
|
+
const raw = headers && ((_b = headers['retry-after']) !== null && _b !== void 0 ? _b : headers['Retry-After']);
|
|
487
|
+
const seconds = Number(raw);
|
|
488
|
+
return Number.isFinite(seconds) && seconds > 0 ? Math.min(seconds * 1000, REFRESH_BACKOFF_MAX_MS) : 0;
|
|
489
|
+
}
|
|
490
|
+
/** Throws a non-definitive `refresh_backoff` error while `key` is still backing off. */
|
|
491
|
+
function assertRefreshAllowed(key, now = Date.now()) {
|
|
492
|
+
const entry = refreshBackoff.get(key);
|
|
493
|
+
if (!entry || now >= entry.until)
|
|
494
|
+
return;
|
|
495
|
+
const err = new Error('Session refresh is backing off after a failed attempt');
|
|
496
|
+
err.code = 'refresh_backoff';
|
|
497
|
+
err.retryAt = entry.until;
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
/** Record the outcome of a refresh attempt for `key`. */
|
|
501
|
+
function recordRefreshOutcome(key, err, now = Date.now()) {
|
|
502
|
+
var _a, _b;
|
|
503
|
+
if (!err || isDefinitiveRejection(err)) {
|
|
504
|
+
refreshBackoff.delete(key);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const failures = ((_b = (_a = refreshBackoff.get(key)) === null || _a === void 0 ? void 0 : _a.failures) !== null && _b !== void 0 ? _b : 0) + 1;
|
|
508
|
+
const backoff = Math.min(REFRESH_BACKOFF_BASE_MS * 2 ** (failures - 1), REFRESH_BACKOFF_MAX_MS);
|
|
509
|
+
refreshBackoff.set(key, { failures, until: now + Math.max(backoff, retryAfterMs(err)) });
|
|
510
|
+
// One entry per refresh token; a long-lived tab rotates through a handful at most.
|
|
511
|
+
if (refreshBackoff.size > 32)
|
|
512
|
+
refreshBackoff.delete(refreshBackoff.keys().next().value);
|
|
513
|
+
}
|
|
514
|
+
|
|
424
515
|
/**
|
|
425
516
|
* Resolve the refresh/revoke base URL to a CONFIGURED issuer only. The session's
|
|
426
517
|
* `issuer` is persisted in client storage; if that storage is tampered (e.g. XSS)
|
|
@@ -574,6 +665,9 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
574
665
|
const existing = refreshInFlightByToken.get(dedupeKey);
|
|
575
666
|
if (existing)
|
|
576
667
|
return existing;
|
|
668
|
+
// Inside a backoff window this throws at once, so a failing issuer is not asked again
|
|
669
|
+
// by every request and timer in the page (see refresh-error.ts).
|
|
670
|
+
assertRefreshAllowed(dedupeKey);
|
|
577
671
|
let currentFlight;
|
|
578
672
|
currentFlight = (async () => {
|
|
579
673
|
try {
|
|
@@ -587,8 +681,13 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
587
681
|
refreshToken,
|
|
588
682
|
appId
|
|
589
683
|
}, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
|
|
684
|
+
recordRefreshOutcome(dedupeKey, null);
|
|
590
685
|
return response.data;
|
|
591
686
|
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
recordRefreshOutcome(dedupeKey, error);
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
592
691
|
finally {
|
|
593
692
|
// Only remove our own entry, and only if it is still the active flight
|
|
594
693
|
// for this token (a later refresh of the same token cannot have replaced
|
|
@@ -646,53 +745,6 @@ async function revokeSession(refreshToken, issuer) {
|
|
|
646
745
|
async function signSessionCreateMessage(_signMessageFunction) {
|
|
647
746
|
}
|
|
648
747
|
|
|
649
|
-
// Classify a session-refresh failure as a DEFINITIVE rejection of the credential
|
|
650
|
-
// vs a TRANSPORT failure (DS4-0054).
|
|
651
|
-
//
|
|
652
|
-
// The restore path refreshes an expired token at boot. The old code destroyed the
|
|
653
|
-
// local session on ANY refresh failure, so a network blip - common on mobile -
|
|
654
|
-
// signed the user out. Only a real HTTP rejection of the refresh token proves the
|
|
655
|
-
// session is invalid; anything else is unknown, and the safe move is to RETAIN local
|
|
656
|
-
// state and retry. Keeping a genuinely dead session locally is harmless: every
|
|
657
|
-
// request still fails closed server-side, and the next refresh that reaches the
|
|
658
|
-
// server clears it. Destroying a live session on a blip is user-visible damage.
|
|
659
|
-
//
|
|
660
|
-
// `/session/refresh` answers 401 (session_revoked / invalid_grant) when the token is
|
|
661
|
-
// dead. 400 (bad request), 408/429 (transient), and 5xx are NOT proof the session is
|
|
662
|
-
// invalid, and a network/DNS/timeout error carries no HTTP response at all.
|
|
663
|
-
//
|
|
664
|
-
// This is the single classifier for every refresh path: the per-request freshness
|
|
665
|
-
// check (auth-token-freshness) and the boot-time restore paths (web + RN session
|
|
666
|
-
// managers) all import it, so "definitive" means the same thing everywhere. It lives
|
|
667
|
-
// in this leaf module to avoid an import cycle between those consumers.
|
|
668
|
-
function isDefinitiveRejection(err) {
|
|
669
|
-
var _a;
|
|
670
|
-
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
671
|
-
return status === 401 || status === 403;
|
|
672
|
-
}
|
|
673
|
-
// How long past the id token's own `exp` a session may survive purely on
|
|
674
|
-
// transport failures.
|
|
675
|
-
//
|
|
676
|
-
// "Retain on a blip" must not become "retain forever". A statusless failure is
|
|
677
|
-
// unknown, so the first ones are absorbed - but a revoked session, or a gateway
|
|
678
|
-
// that rewrites the issuer's 401 into a 502, would otherwise keep
|
|
679
|
-
// isAuthenticated() reporting a signed-in user for as long as the device never
|
|
680
|
-
// reaches the issuer again. One day covers an offline flight or a bad-network day
|
|
681
|
-
// while making the retention finite: past it, an unrefreshable session is cleared
|
|
682
|
-
// and the user is asked to sign in again.
|
|
683
|
-
const TRANSPORT_RETENTION_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
684
|
-
/**
|
|
685
|
-
* May a session whose refresh failed WITHOUT a definitive rejection be kept?
|
|
686
|
-
*
|
|
687
|
-
* `expSeconds` is the id token's `exp` claim. Fails closed: a missing or
|
|
688
|
-
* non-numeric expiry is not a usable bound, so such a session is never retained.
|
|
689
|
-
*/
|
|
690
|
-
function mayRetainAfterTransportFailure(expSeconds, now = Date.now()) {
|
|
691
|
-
if (typeof expSeconds !== 'number' || !Number.isFinite(expSeconds))
|
|
692
|
-
return false;
|
|
693
|
-
return now <= expSeconds * 1000 + TRANSPORT_RETENTION_GRACE_MS;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
748
|
// Shared session-binding guard for the web + React Native session managers, so
|
|
697
749
|
// the cross-principal refresh guard (#220 / #221) lives in ONE place and the two
|
|
698
750
|
// managers can't drift apart. Base64url decoding differs by platform (browser
|
|
@@ -4190,7 +4242,7 @@ class ServerSessionManager {
|
|
|
4190
4242
|
const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
|
|
4191
4243
|
/* fetch nonce from auth API (bound to the snapshotted app+issuer) */
|
|
4192
4244
|
const nonce = await genAuthNonce(sessionConfig);
|
|
4193
|
-
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-
|
|
4245
|
+
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-BVwD6WVm.js'); });
|
|
4194
4246
|
const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
|
|
4195
4247
|
/* sign the message */
|
|
4196
4248
|
const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
|
|
@@ -4539,6 +4591,16 @@ function classifyToken(token, nowMs = Date.now(), skewMs = TOKEN_EXPIRY_SKEW_MS)
|
|
|
4539
4591
|
return 'expiring-soon';
|
|
4540
4592
|
return 'fresh';
|
|
4541
4593
|
}
|
|
4594
|
+
function tokenExpSeconds(token) {
|
|
4595
|
+
var _a;
|
|
4596
|
+
try {
|
|
4597
|
+
const exp = (_a = JSON.parse(decodeBase64Url(token.split('.')[1]))) === null || _a === void 0 ? void 0 : _a.exp;
|
|
4598
|
+
return typeof exp === 'number' ? exp : undefined;
|
|
4599
|
+
}
|
|
4600
|
+
catch (_b) {
|
|
4601
|
+
return undefined;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4542
4604
|
/** Short, non-secret fingerprint used only to key the in-flight map. Never logged. */
|
|
4543
4605
|
function tokenFingerprint(token) {
|
|
4544
4606
|
let h = 5381;
|
|
@@ -4667,6 +4729,12 @@ async function getFreshAuthToken(isServer = false, options = {}) {
|
|
|
4667
4729
|
// it is valid right now, so use it rather than failing the request.
|
|
4668
4730
|
if (stillUsable)
|
|
4669
4731
|
return idToken;
|
|
4732
|
+
// ...but not forever: past its expiry plus the same grace the restore path
|
|
4733
|
+
// allows, a session that still cannot refresh is dropped, so an open tab stops
|
|
4734
|
+
// asking an issuer that will never answer it.
|
|
4735
|
+
if (!mayRetainAfterTransportFailure(tokenExpSeconds(idToken))) {
|
|
4736
|
+
await clearDeadSession(isServer, generation);
|
|
4737
|
+
}
|
|
4670
4738
|
throw makeAuthExpiredError('Authentication expired and refresh failed', status);
|
|
4671
4739
|
}
|
|
4672
4740
|
finally {
|
|
@@ -7533,7 +7601,7 @@ async function doReconnectWithNewAuth() {
|
|
|
7533
7601
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
7534
7602
|
}
|
|
7535
7603
|
try {
|
|
7536
|
-
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-
|
|
7604
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-BiChlioj.js'); });
|
|
7537
7605
|
await reconnectRealtimeStoreWithNewAuth();
|
|
7538
7606
|
}
|
|
7539
7607
|
catch (error) {
|
|
@@ -8911,7 +8979,7 @@ async function set(path, document, options) {
|
|
|
8911
8979
|
* state appears only under `observation`/`mirrorSync` once actually observed.
|
|
8912
8980
|
*/
|
|
8913
8981
|
async function setMany(many, options) {
|
|
8914
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
8982
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
8915
8983
|
// Returns the data that was set, or undefined if the document was already set.
|
|
8916
8984
|
try {
|
|
8917
8985
|
// A server WalletClient threads its immutable config snapshot; the default
|
|
@@ -9025,13 +9093,15 @@ async function setMany(many, options) {
|
|
|
9025
9093
|
throw new Error('The write response carried no execution-chain tag, so this receipt could not state where the write ran. ' +
|
|
9026
9094
|
'Nothing was signed or submitted.');
|
|
9027
9095
|
}
|
|
9096
|
+
const grantLane = (_l = options === null || options === void 0 ? void 0 : options._overrides) === null || _l === void 0 ? void 0 : _l._grantLane;
|
|
9097
|
+
grantLane === null || grantLane === void 0 ? void 0 : grantLane.assertLane(workerChain);
|
|
9028
9098
|
if (workerChain === 'realtime_offchain') {
|
|
9029
9099
|
// Poofnet: the wallet signs the worker's unsendable approval message and
|
|
9030
9100
|
// the worker verifies and commits it. Nothing is submitted to any chain.
|
|
9031
9101
|
if (typeof curTx.serializedTransaction !== 'string' || typeof curTx.poofnetIntentId !== 'string') {
|
|
9032
9102
|
throw new Error('Unsupported 202 transaction shape: a poofnet write needs serializedTransaction and poofnetIntentId');
|
|
9033
9103
|
}
|
|
9034
|
-
const { signPoofnetTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
9104
|
+
const { signPoofnetTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-B3u6Arbf.js'); });
|
|
9035
9105
|
const signedTransaction = await signPoofnetTransaction(curTx.serializedTransaction, authProvider, options, buildExpectedWriteIntent(documents));
|
|
9036
9106
|
// Signing-only, exactly as on a real chain: signed and NOT committed.
|
|
9037
9107
|
if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false) {
|
|
@@ -9041,12 +9111,25 @@ async function setMany(many, options) {
|
|
|
9041
9111
|
poofnetIntentId: curTx.poofnetIntentId,
|
|
9042
9112
|
};
|
|
9043
9113
|
}
|
|
9044
|
-
//
|
|
9045
|
-
//
|
|
9114
|
+
// A grant lane's first credential is spent by the PUT that built this approval, so the
|
|
9115
|
+
// commit's is minted now, before `sending`: failing to mint sends nothing.
|
|
9116
|
+
const commitHeaders = await (grantLane === null || grantLane === void 0 ? void 0 : grantLane.commitHeaders());
|
|
9117
|
+
// The commit PUT is the one ambiguous step of this lane: a lost response may follow a
|
|
9118
|
+
// commit, so a transport failure or a 5xx stays untagged (read the path back). A 4xx is
|
|
9119
|
+
// realtime stating it committed nothing (the intent expired or was taken, the signer did
|
|
9120
|
+
// not match, a rule refused it), as for the first PUT. 408 and 499 are a proxy's words,
|
|
9121
|
+
// not realtime's, and may follow a commit, so they stay ambiguous too.
|
|
9046
9122
|
sending = true;
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9123
|
+
let commitResponse;
|
|
9124
|
+
try {
|
|
9125
|
+
commitResponse = await makeApiRequest('PUT', 'items/signed', { intentId: curTx.poofnetIntentId, signedTransaction }, commitHeaders ? Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options._overrides), { _getAuthHeaders: async () => commitHeaders }) : options === null || options === void 0 ? void 0 : options._overrides);
|
|
9126
|
+
if (commitResponse.status !== 200) {
|
|
9127
|
+
throw Object.assign(new Error(`Poofnet signed write was not committed (status ${commitResponse.status})`), { statusCode: commitResponse.status });
|
|
9128
|
+
}
|
|
9129
|
+
}
|
|
9130
|
+
catch (error) {
|
|
9131
|
+
const status = Number(error === null || error === void 0 ? void 0 : error.statusCode);
|
|
9132
|
+
throw status >= 400 && status < 500 && status !== 408 && status !== 499 ? markWriteNotApplied(error, 'broadcast_rejected') : error;
|
|
9050
9133
|
}
|
|
9051
9134
|
return directCommitResult(commitResponse, documents);
|
|
9052
9135
|
}
|
|
@@ -9056,7 +9139,7 @@ async function setMany(many, options) {
|
|
|
9056
9139
|
'Nothing was signed or submitted.');
|
|
9057
9140
|
}
|
|
9058
9141
|
const solanaChain = workerChain;
|
|
9059
|
-
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
9142
|
+
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-B3u6Arbf.js'); });
|
|
9060
9143
|
// OA-0001: derive the expected intent from OUR OWN request (the one
|
|
9061
9144
|
// ground truth a tampered builder response cannot change) so the
|
|
9062
9145
|
// validator can reject any transaction whose operation semantics
|
|
@@ -9095,7 +9178,9 @@ async function setMany(many, options) {
|
|
|
9095
9178
|
}
|
|
9096
9179
|
// Confirmed: the receipt is already complete, so the mirror read is a
|
|
9097
9180
|
// best-effort enrichment reported in `mirrorSync`, never a rejection.
|
|
9098
|
-
const mirrorSync =
|
|
9181
|
+
const mirrorSync = grantLane
|
|
9182
|
+
? { observed: false, reason: 'not attempted: a venue grant authorizes only the write' }
|
|
9183
|
+
: await attemptMirrorSync(requestedPaths, options);
|
|
9099
9184
|
return {
|
|
9100
9185
|
status: 'confirmed', appId, requestedDocuments, chain: solanaChain,
|
|
9101
9186
|
transactionId: solanaOutcome.transactionSignature,
|
|
@@ -10118,6 +10203,84 @@ function createAppTokenAuthHeaders(options) {
|
|
|
10118
10203
|
};
|
|
10119
10204
|
}
|
|
10120
10205
|
|
|
10206
|
+
/**
|
|
10207
|
+
* The venue grant write lane: create ONE document in another app (an OpenApps venue) as the
|
|
10208
|
+
* wallet signed in here, authorized by a single-use grant instead of a venue session
|
|
10209
|
+
* (VENUE-GRANT-DESIGN.md). The grant only lets the venue build that one transaction for that
|
|
10210
|
+
* wallet; the wallet's own signature, checked by the venue's own rule, is the authority.
|
|
10211
|
+
*
|
|
10212
|
+
* The grant itself is minted by Bounded Auth at `POST {humanAuthApiUrl}/app-token/venue-grant`
|
|
10213
|
+
* (contract: packages/cdk/cloudflare/shared/venue-grant.ts, which this package cannot import
|
|
10214
|
+
* because the SDK packages publish separately). This module only spends one.
|
|
10215
|
+
*
|
|
10216
|
+
* It reuses `setMany`'s request and 202 handling through `_overrides`: the `_config` snapshot is
|
|
10217
|
+
* this app's own config re-pointed at the VENUE app id, so the pre-sign validator (OA-0001) binds
|
|
10218
|
+
* the venue app id and the full expected intent, and the signer stays this app's own AuthProvider
|
|
10219
|
+
* (a Turnkey session or a Wallet Standard wallet). Nothing here is reachable from `createClient()`,
|
|
10220
|
+
* which stays read-only.
|
|
10221
|
+
*
|
|
10222
|
+
* The chain a write runs on is the VENUE's, stated by its 202, never the app's: the app's own
|
|
10223
|
+
* `chain` can only agree with it or be refused, and the transaction is broadcast through the app's
|
|
10224
|
+
* `rpcUrl` only when the app configured that same chain, otherwise through `resolveRpcUrl(chain)`.
|
|
10225
|
+
*/
|
|
10226
|
+
function laneError(code, message) {
|
|
10227
|
+
return Object.assign(new Error(message), { code });
|
|
10228
|
+
}
|
|
10229
|
+
const isSolanaLane = (chain) => chain === 'solana_devnet' || chain === 'solana_mainnet';
|
|
10230
|
+
/**
|
|
10231
|
+
* The RPC a venue write on Solana lane `chain` is broadcast through, and a saved receipt of it is
|
|
10232
|
+
* reconciled through. The app's own `rpcUrl` counts only when the app also configured this chain
|
|
10233
|
+
* (an `rpcUrl` alone does not say which chain it speaks); otherwise `resolveRpcUrl(chain)`. Null
|
|
10234
|
+
* when neither yields one.
|
|
10235
|
+
*
|
|
10236
|
+
* @internal Shared by the lane and by the client SDK's reconciliation of a saved receipt.
|
|
10237
|
+
*/
|
|
10238
|
+
function resolveVenueRpcUrl(config, chain, resolveRpcUrl) {
|
|
10239
|
+
var _a, _b;
|
|
10240
|
+
const own = requiredSolanaChain(config) === chain ? (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim() : '';
|
|
10241
|
+
return own || ((_b = resolveRpcUrl === null || resolveRpcUrl === void 0 ? void 0 : resolveRpcUrl(chain)) === null || _b === void 0 ? void 0 : _b.trim()) || null;
|
|
10242
|
+
}
|
|
10243
|
+
/**
|
|
10244
|
+
* Refuse, before the wallet is asked to sign, a 202 whose execution lane this app is not
|
|
10245
|
+
* configured for: a mainnet app must never sign a devnet or poofnet approval (a poofnet "funding"
|
|
10246
|
+
* moves no real USDC). Then point the Solana lane's broadcast at the RPC for the venue's chain.
|
|
10247
|
+
* Poofnet broadcasts nothing, so it needs no RPC.
|
|
10248
|
+
*/
|
|
10249
|
+
function assertExecutionLane(config, chain, options) {
|
|
10250
|
+
const required = requiredSolanaChain(config);
|
|
10251
|
+
if (required && chain !== required) {
|
|
10252
|
+
throw laneError('chain_mismatch', `The venue answered on "${chain}" but this app is configured for "${config.chain}". Nothing was signed.`);
|
|
10253
|
+
}
|
|
10254
|
+
if (!isSolanaLane(chain))
|
|
10255
|
+
return;
|
|
10256
|
+
const rpcUrl = resolveVenueRpcUrl(config, chain, options.resolveRpcUrl);
|
|
10257
|
+
if (rpcUrl)
|
|
10258
|
+
config.rpcUrl = rpcUrl;
|
|
10259
|
+
else if (!options._solanaRpc) {
|
|
10260
|
+
throw laneError('chain_not_configured', `The venue runs on ${chain}, but no RPC could be resolved to broadcast through. ` +
|
|
10261
|
+
`Pass chain: '${chain}' and an rpcUrl to init(). Nothing was signed.`);
|
|
10262
|
+
}
|
|
10263
|
+
}
|
|
10264
|
+
/**
|
|
10265
|
+
* Write `document` at `path` in the venue `venueAppId` with a single-use grant. Resolves to the
|
|
10266
|
+
* same discriminated {@link SetResult} as `set`, and throws exactly as `set` does, so the same
|
|
10267
|
+
* `isWriteNotApplied` reading applies.
|
|
10268
|
+
*
|
|
10269
|
+
* @internal Used by `fundVenueEscrow` and `disputeVenueEscrow` in @bounded-sh/client. Not a stable API.
|
|
10270
|
+
*/
|
|
10271
|
+
async function setWithVenueGrant(path, document, options) {
|
|
10272
|
+
// A private copy of this app's config for this one write: the lane fills in its RPC below.
|
|
10273
|
+
const config = Object.assign(Object.assign({}, (await getConfig())), { appId: options.venueAppId });
|
|
10274
|
+
const headers = (grant) => ({ Authorization: `Bearer ${grant}` });
|
|
10275
|
+
return setMany([{ path, document }], {
|
|
10276
|
+
onBroadcast: options.onBroadcast,
|
|
10277
|
+
_overrides: Object.assign({ _config: config, _getAuthHeaders: async () => headers(options.grant), _grantLane: {
|
|
10278
|
+
assertLane: (chain) => assertExecutionLane(config, chain, options),
|
|
10279
|
+
commitHeaders: async () => headers(await options.commitGrant()),
|
|
10280
|
+
} }, (options._solanaRpc ? { _solanaRpc: options._solanaRpc } : {})),
|
|
10281
|
+
});
|
|
10282
|
+
}
|
|
10283
|
+
|
|
10121
10284
|
/**
|
|
10122
10285
|
* Live named-query subscriptions (subscribeQuery) - HTTP client poller (v1).
|
|
10123
10286
|
*
|
|
@@ -10820,7 +10983,7 @@ function toMillis(seconds) {
|
|
|
10820
10983
|
|
|
10821
10984
|
/** SIWS message creation is async already, so its onchain module can be lazy. */
|
|
10822
10985
|
async function genSolanaMessage(address, nonce) {
|
|
10823
|
-
const implementation = await Promise.resolve().then(function () { return require('./solana-message-
|
|
10986
|
+
const implementation = await Promise.resolve().then(function () { return require('./solana-message-BVwD6WVm.js'); });
|
|
10824
10987
|
return implementation.genSolanaMessage(address, nonce);
|
|
10825
10988
|
}
|
|
10826
10989
|
|
|
@@ -11444,6 +11607,7 @@ exports.queryAggregate = queryAggregate;
|
|
|
11444
11607
|
exports.reconcileSetResult = reconcileSetResult;
|
|
11445
11608
|
exports.reconnectWithNewAuth = reconnectWithNewAuth;
|
|
11446
11609
|
exports.refreshSession = refreshSession;
|
|
11610
|
+
exports.resolveVenueRpcUrl = resolveVenueRpcUrl;
|
|
11447
11611
|
exports.revokeSession = revokeSession;
|
|
11448
11612
|
exports.runExpression = runExpression;
|
|
11449
11613
|
exports.runExpressionMany = runExpressionMany;
|
|
@@ -11454,6 +11618,7 @@ exports.serverTimestamp = serverTimestamp;
|
|
|
11454
11618
|
exports.set = set;
|
|
11455
11619
|
exports.setFile = setFile;
|
|
11456
11620
|
exports.setMany = setMany;
|
|
11621
|
+
exports.setWithVenueGrant = setWithVenueGrant;
|
|
11457
11622
|
exports.signAndSubmitTransaction = signAndSubmitTransaction;
|
|
11458
11623
|
exports.signMessage = signMessage;
|
|
11459
11624
|
exports.signSessionCreateMessage = signSessionCreateMessage;
|
|
@@ -11468,4 +11633,4 @@ exports.toSeconds = toSeconds;
|
|
|
11468
11633
|
exports.waitForSetResult = waitForSetResult;
|
|
11469
11634
|
exports.withEffects = withEffects;
|
|
11470
11635
|
exports.writeNotAppliedPhase = writeNotAppliedPhase;
|
|
11471
|
-
//# sourceMappingURL=index
|
|
11636
|
+
//# sourceMappingURL=index-5TMFraRK.js.map
|