@capxul/sdk 4.1.4 → 4.2.0-rc.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.
@@ -1,39 +1,275 @@
1
- import { Context, Data, Effect, Layer, Result } from "effect";
2
1
  import { concat, encodeAbiParameters, encodeFunctionData, encodePacked, getContractAddress, keccak256, padHex, stringToHex, toBytes, toEventSelector, toFunctionSelector, toFunctionSignature } from "viem";
2
+ import { Context, Data, Effect, Layer, Result } from "effect";
3
3
  //#region ../errors/src/secret-material.ts
4
- const SENSITIVE_MATERIAL_PATTERNS = [
5
- /(?:^|[^a-fA-F0-9])(?<!0[xX])[a-fA-F0-9]{64}(?:$|[^a-fA-F0-9])/u,
6
- /eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/u,
7
- /(?:(?:sk|pk|rk)_(?:live|test)|(?:phc|phx|ghp|gho|ghu|ghs|ghr|whsec)_|github_pat_|xox[baprs]-|AKIA[0-9A-Z]{16}|AIza)[A-Za-z0-9_-]*/iu,
8
- /\bBearer\s+[^\s,;]+/iu,
9
- /https?:\/\/[^\s"']+?\/v[0-9]\/[A-Za-z0-9_-]{16,}/iu,
10
- /\b(?:api[ _-]?key|access[ _-]?token|token|secret|password|passphrase|private[ _-]?key|authorization|cookie|signature|request[ _-]?body|credential|otp|one[ _-]?time[ _-]?(?:password|code)|verification[ _-]?code)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;&]+)/iu
11
- ];
12
- const MASKS = [
13
- [/\bBearer\s+[^\s,;]+/giu, "Bearer [REDACTED]"],
4
+ const CREDENTIAL_FIELDS = /* @__PURE__ */ new Set([
5
+ "password",
6
+ "passwd",
7
+ "pwd",
8
+ "pass",
9
+ "passphrase",
10
+ "currentpassword",
11
+ "newpassword",
12
+ "confirmpassword",
13
+ "passwordconfirmation",
14
+ "passwordhash",
15
+ "hashedpassword",
16
+ "pin",
17
+ "privatekey",
18
+ "walletprivatekey",
19
+ "secretkey",
20
+ "signingkey",
21
+ "signingsecret",
22
+ "seed",
23
+ "seedphrase",
24
+ "mnemonic",
25
+ "recoveryphrase",
26
+ "recovery",
27
+ "recoverycode",
28
+ "recoverycodes",
29
+ "keyshare",
30
+ "signingmaterial",
31
+ "recoverymaterial",
32
+ "privatekeybytes",
33
+ "signingprivatekey",
34
+ "encryptionkey",
35
+ "decryptionkey",
36
+ "authtoken",
37
+ "apitoken",
38
+ "bearertoken",
39
+ "oauthtoken",
40
+ "csrftoken",
41
+ "clientassertion",
42
+ "token",
43
+ "accesstoken",
44
+ "refreshtoken",
45
+ "idtoken",
46
+ "sessiontoken",
47
+ "session",
48
+ "sessionkey",
49
+ "sessionsecret",
50
+ "authsessionid",
51
+ "sessioncredential",
52
+ "sessioncredentials",
53
+ "sessioncookie",
54
+ "auth",
55
+ "authentication",
56
+ "authorization",
57
+ "proxyauthorization",
58
+ "cookie",
59
+ "cookies",
60
+ "setcookie",
61
+ "apikey",
62
+ "xapikey",
63
+ "xauthtoken",
64
+ "clientsecret",
65
+ "secret",
66
+ "secrets",
67
+ "credential",
68
+ "credentials",
69
+ "signature",
70
+ "otp",
71
+ "otpcode",
72
+ "totpsecret",
73
+ "onetimecode",
74
+ "onetimepassword",
75
+ "verificationcode",
76
+ "authcode",
77
+ "authorizationcode",
78
+ "oauthcode",
79
+ "codeverifier"
80
+ ]);
81
+ /** Match credential fields by meaning, without hiding token addresses or replay IDs. */
82
+ function isCredentialField(name, context) {
83
+ const normalized = name.toLowerCase().replace(/[^a-z0-9]/gu, "");
84
+ return CREDENTIAL_FIELDS.has(normalized) || /(?:password(?:hash)?|passwd|passphrase|privatekey|signingkey|apikey|clientsecret|accesstoken|refreshtoken|sessiontoken)$/u.test(normalized) || context === "authentication" && (normalized === "code" || normalized === "sessionid") || context === "header" && /(?:token|secret|apikey|privatekey|signingkey|password|authorization|cookie|session)$/u.test(normalized);
85
+ }
86
+ function decodeUrlComponent(text) {
87
+ try {
88
+ return decodeURIComponent(text);
89
+ } catch {
90
+ return text;
91
+ }
92
+ }
93
+ /** Preserve URL diagnostics while removing credentials from their declared components. */
94
+ function redactUrlSecrets(value, source) {
95
+ try {
96
+ const absolute = /^[a-z][a-z0-9+.-]*:/iu.test(value);
97
+ const protocolRelative = value.startsWith("//");
98
+ const url = new URL(value, "https://redaction.invalid");
99
+ if (url.protocol !== "https:" && url.protocol !== "http:") return maskText(value);
100
+ let changed = url.username.length > 0 || url.password.length > 0;
101
+ const maskParams = (params) => new URLSearchParams(Array.from(params, ([name, entry]) => {
102
+ const safeName = maskText(name);
103
+ const safeEntry = isCredentialField(name) || /^(?:code|draft|__posthog)$/iu.test(name) ? "[REDACTED]" : maskText(entry);
104
+ changed ||= safeName !== name || safeEntry !== entry;
105
+ return [safeName, safeEntry];
106
+ })).toString();
107
+ const provider = source !== void 0 || /(?:^|\.)(?:alchemy\.com|alchemyapi\.io|infura\.io)$/iu.test(url.hostname);
108
+ const segments = (absolute || protocolRelative ? url.pathname : value.split(/[?#]/u)[0] ?? "").split("/");
109
+ const path = segments.map((segment, index) => {
110
+ const decoded = decodeUrlComponent(segment);
111
+ const masked = provider && /^v[23]$/u.test(decodeUrlComponent(segments[index - 1] ?? "")) && decoded.length > 0 ? "[REDACTED]" : maskText(decoded);
112
+ changed ||= masked !== decoded;
113
+ return masked === decoded ? segment : encodeURIComponent(masked);
114
+ }).join("/");
115
+ const query = maskParams(url.searchParams);
116
+ const fragment = url.hash.slice(1);
117
+ const decodedFragment = decodeUrlComponent(fragment);
118
+ const maskedFragment = maskText(decodedFragment);
119
+ changed ||= maskedFragment !== decodedFragment;
120
+ const fragmentQuery = fragment.indexOf("?");
121
+ const fragmentPath = fragment.slice(0, fragmentQuery);
122
+ const maskedFragmentPath = maskText(decodeUrlComponent(fragmentPath));
123
+ changed ||= maskedFragmentPath !== decodeUrlComponent(fragmentPath);
124
+ const hash = fragmentQuery >= 0 ? `${maskedFragmentPath === decodeUrlComponent(fragmentPath) ? fragmentPath : encodeURIComponent(maskedFragmentPath)}?${maskParams(new URLSearchParams(fragment.slice(fragmentQuery + 1)))}` : fragment.includes("=") ? maskParams(new URLSearchParams(fragment)) : maskedFragment === decodedFragment ? fragment : encodeURIComponent(maskedFragment);
125
+ const origin = absolute ? url.origin : protocolRelative ? `//${url.host}` : "";
126
+ if (!changed) return value;
127
+ return `${origin}${path}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
128
+ } catch {
129
+ return maskText(value.replace(/(https?:\/\/)[^/\s@]+@/giu, "$1[REDACTED]@").replace(/([?&#])([^=&#]+)=([^&#]*)/gu, (whole, separator, name) => isCredentialField(decodeUrlComponent(name)) || /^(?:code|draft|__posthog)$/iu.test(decodeUrlComponent(name)) ? `${separator}${name}=[REDACTED]` : whole));
130
+ }
131
+ }
132
+ const JSON_FIELD = /("(?:[^"\\]|\\.)*")(\s*:\s*)("(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/gu;
133
+ const PROSE_FIELD = /(?=(\b([A-Za-z][A-Za-z0-9_-]*(?:[ \t]+[A-Za-z][A-Za-z0-9_-]*){0,3})\s*[:=]\s*((?:Bearer|Basic)[ \t]+[^\s,;"']+|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s,;&"']+)))/gu;
134
+ const EMBEDDED_URL = /(https?:\/\/[^\s"<>]*)/giu;
135
+ function isSecretLabel(name) {
136
+ return isCredentialField(name) || /^request[ _-]?body$/iu.test(name);
137
+ }
138
+ const JSON_ENCODING_MAX = 8;
139
+ const JSON_NESTING_MAX = 64;
140
+ const JSON_KEY = /("(?:[^"\\]|\\.)*")\s*:/gu;
141
+ const JSON_CONTAINER_START = /(?:\{\s*["}]|\[\s*[[\]{}"0-9tfn-])/uy;
142
+ function hasSecretJsonKey(value) {
143
+ for (const match of value.matchAll(JSON_KEY)) try {
144
+ if (match[1] !== void 0 && isSecretLabel(JSON.parse(match[1]))) return true;
145
+ } catch {}
146
+ return false;
147
+ }
148
+ /** Project a complete fragment once. Never retry all of its nested substrings. */
149
+ function projectJsonFragment(value, encodingDepth, overDepth) {
150
+ if (overDepth || encodingDepth >= JSON_ENCODING_MAX) return "[REDACTED]";
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(value);
154
+ } catch {
155
+ return hasSecretJsonKey(value) ? "[REDACTED]" : value;
156
+ }
157
+ try {
158
+ let changed = false;
159
+ const masked = JSON.stringify(parsed, (name, nested) => {
160
+ if (isSecretLabel(name)) {
161
+ changed ||= nested !== "[REDACTED]";
162
+ return "[REDACTED]";
163
+ }
164
+ if (typeof nested === "string") {
165
+ const projected = maskJsonFragments(nested, encodingDepth + 1);
166
+ changed ||= projected !== nested;
167
+ return projected;
168
+ }
169
+ return nested;
170
+ });
171
+ return changed ? masked : value;
172
+ } catch {
173
+ return "[REDACTED]";
174
+ }
175
+ }
176
+ /** Scan balanced object/array fragments once, respecting JSON quotes and escapes. */
177
+ function maskJsonFragments(value, encodingDepth = 0) {
178
+ if (value.trimStart().startsWith("\"")) try {
179
+ const decoded = JSON.parse(value);
180
+ if (typeof decoded === "string") {
181
+ if (encodingDepth >= JSON_ENCODING_MAX) return "\"[REDACTED]\"";
182
+ const masked = maskJsonFragments(decoded, encodingDepth + 1);
183
+ return masked === decoded ? value : JSON.stringify(masked);
184
+ }
185
+ } catch {}
186
+ let output = "";
187
+ let cursor = 0;
188
+ let start = -1;
189
+ let depth = 0;
190
+ let overDepth = false;
191
+ let quoted = false;
192
+ let escaped = false;
193
+ for (let index = 0; index < value.length; index++) {
194
+ const character = value[index];
195
+ if (start < 0) {
196
+ JSON_CONTAINER_START.lastIndex = index;
197
+ if (!JSON_CONTAINER_START.test(value)) continue;
198
+ start = index;
199
+ depth = 1;
200
+ overDepth = false;
201
+ continue;
202
+ }
203
+ if (quoted) {
204
+ if (escaped) escaped = false;
205
+ else if (character === "\\") escaped = true;
206
+ else if (character === "\"") quoted = false;
207
+ continue;
208
+ }
209
+ if (character === "\"") quoted = true;
210
+ else if (character === "{" || character === "[") {
211
+ depth++;
212
+ overDepth ||= depth > JSON_NESTING_MAX;
213
+ } else if (character === "}" || character === "]") {
214
+ depth--;
215
+ if (depth !== 0) continue;
216
+ output += value.slice(cursor, start) + projectJsonFragment(value.slice(start, index + 1), encodingDepth, overDepth);
217
+ cursor = index + 1;
218
+ start = -1;
219
+ }
220
+ }
221
+ if (start >= 0) {
222
+ const fragment = value.slice(start);
223
+ output += value.slice(cursor, start) + (overDepth || hasSecretJsonKey(fragment) ? "[REDACTED]" : fragment);
224
+ cursor = value.length;
225
+ }
226
+ return output + value.slice(cursor);
227
+ }
228
+ function maskLabelledText(value) {
229
+ const jsonMasked = maskJsonFragments(value).replace(JSON_FIELD, (whole, quotedName, separator) => {
230
+ try {
231
+ return isSecretLabel(JSON.parse(quotedName)) ? `${quotedName}${separator}"[REDACTED]"` : whole;
232
+ } catch {
233
+ return whole;
234
+ }
235
+ });
236
+ let output = "";
237
+ let cursor = 0;
238
+ for (const match of jsonMasked.matchAll(PROSE_FIELD)) {
239
+ const [, whole, label, entry] = match;
240
+ if (whole === void 0 || label === void 0 || entry === void 0 || match.index < cursor || /^Bearer[ \t]+/iu.test(entry)) continue;
241
+ const words = label.split(/[ \t]+/u);
242
+ for (let index = 0; index < words.length; index++) {
243
+ const name = words.slice(index).join(" ");
244
+ if (isSecretLabel(name)) {
245
+ const prefix = words.slice(0, index).join(" ");
246
+ output += jsonMasked.slice(cursor, match.index) + `${prefix ? `${prefix} ` : ""}${name}=[REDACTED]`;
247
+ cursor = match.index + whole.length;
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ return output + jsonMasked.slice(cursor);
253
+ }
254
+ const TOKEN_MASKS = [
255
+ [/\bBearer\s+(?!(?:realm|error|error_description|scope|authorization_uri|resource|claims)\s*=)[^\s,;"']+/giu, "Bearer [REDACTED]"],
14
256
  [/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[REDACTED]"],
15
257
  [/(^|[^a-fA-F0-9])(?<!0[xX])[a-fA-F0-9]{64}(?=$|[^a-fA-F0-9])/gu, "$1[REDACTED]"],
16
- [/(?:(?:sk|pk|rk)_(?:live|test)|(?:phc|phx|ghp|gho|ghu|ghs|ghr|whsec)_|github_pat_|xox[baprs]-|AKIA[0-9A-Z]{16}|AIza)[A-Za-z0-9_-]*/giu, "[REDACTED]"],
17
- [/(https?:\/\/[^\s"']+?\/v[0-9]\/)[A-Za-z0-9_-]{16,}/giu, "$1[REDACTED]"],
18
- [/\b(api[ _-]?key|access[ _-]?token|token|secret|password|passphrase|private[ _-]?key|authorization|cookie|signature|request[ _-]?body|credential|otp|one[ _-]?time[ _-]?(?:password|code)|verification[ _-]?code)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;&]+)/giu, "$1=[REDACTED]"]
258
+ [/(?:(?:sk|pk|rk)_(?:live|test)|(?:phc|phx|ghp|gho|ghu|ghs|ghr|whsec)_|github_pat_|xox[baprs]-|AKIA[0-9A-Z]{16}|AIza)[A-Za-z0-9_-]*/giu, "[REDACTED]"]
19
259
  ];
20
- /**
21
- * Reject: does the value carry any known secret material? Best effort — callers
22
- * drop the whole value on a match; a false negative is a leak, a false positive
23
- * merely omits an observation field.
24
- */
260
+ /** URL components and malformed URLs use this non-recursive masking layer. */
261
+ function maskText(value) {
262
+ let text = maskLabelledText(value);
263
+ for (const [pattern, replacement] of TOKEN_MASKS) text = text.replace(pattern, replacement);
264
+ return text;
265
+ }
266
+ /** Reject only when the same semantic sanitizer removes credential material. */
25
267
  function containsSensitiveMaterial(value) {
26
- return SENSITIVE_MATERIAL_PATTERNS.some((pattern) => pattern.test(value));
268
+ return redactSecrets(value) !== value;
27
269
  }
28
- /**
29
- * Mask: return the value with every known secret rewritten to `[REDACTED]`,
30
- * preserving surrounding text. Used where a value must still be shown (log
31
- * lines, provider messages, PostHog rows) but must not carry live credentials.
32
- */
33
- function redactSecrets(value) {
34
- let text = value;
35
- for (const [pattern, replacement] of MASKS) text = text.replace(pattern, replacement);
36
- return text;
270
+ /** Mask credential fields and embedded URLs while preserving surrounding evidence. */
271
+ function redactSecrets(value, source) {
272
+ return maskLabelledText(value).split(EMBEDDED_URL).map((part, index) => index % 2 === 1 ? redactUrlSecrets(part, source).replaceAll("%5BREDACTED%5D", "[REDACTED]") : maskText(part)).join("");
37
273
  }
38
274
  //#endregion
39
275
  //#region ../errors/src/chain-cause.ts
@@ -605,7 +841,29 @@ function decodeConvexError(err) {
605
841
  /** Canonical handle syntax shared by validation and branded constructors. */
606
842
  const HANDLE_RE = /^[a-z0-9_-]{3,30}$/;
607
843
  //#endregion
844
+ //#region ../config/src/handle.ts
845
+ const RESERVED_HANDLES = /* @__PURE__ */ new Set([
846
+ "admin",
847
+ "api",
848
+ "capxul",
849
+ "help",
850
+ "root",
851
+ "support",
852
+ "system",
853
+ "www"
854
+ ]);
855
+ function normalizeHandle(raw) {
856
+ return raw.trim().toLowerCase().replace(/^@/, "");
857
+ }
858
+ function validateHandle(raw) {
859
+ const handle = normalizeHandle(raw);
860
+ if (!HANDLE_RE.test(handle)) throw Errors.invalidInput("handle", "must be 3-30 characters of a-z, 0-9, underscore, or hyphen");
861
+ if (RESERVED_HANDLES.has(handle)) throw Errors.invalidInput("handle", "handle is reserved");
862
+ return handle;
863
+ }
864
+ //#endregion
608
865
  //#region ../types/src/index.ts
866
+ const ASSET_ID_RE = /^eip155:([1-9][0-9]*)\/erc20:(0x[0-9a-f]{40})$/;
609
867
  const EVM_ADDRESS_RE = /^0x[0-9a-f]{40}$/i;
610
868
  const BYTES32_RE = /^0x[0-9a-f]{64}$/i;
611
869
  const ZERO_BYTES32 = `0x${"0".repeat(64)}`;
@@ -707,6 +965,17 @@ function toEpochSeconds(raw) {
707
965
  assertSafeNonNegativeInteger(raw, "epochSeconds");
708
966
  return raw;
709
967
  }
968
+ /** Validate an already canonical identity without changing signed input. */
969
+ function toAssetId(raw) {
970
+ const match = typeof raw === "string" ? ASSET_ID_RE.exec(raw) : null;
971
+ if (match === null || match[0] !== raw || !Number.isSafeInteger(Number(match[1]))) throw Errors.invalidInput("assetId", "must be a canonical ERC-20 CAIP-19 identifier");
972
+ if (match[2] === "0x0000000000000000000000000000000000000000") throw Errors.invalidInput("assetId", "must identify a nonzero token contract");
973
+ return raw;
974
+ }
975
+ /** Build an identity from verified chain facts. Normalize the token address. */
976
+ function assetIdFor(chainId, tokenAddress) {
977
+ return toAssetId(`eip155:${toChainId(chainId)}/erc20:${toAddress(tokenAddress)}`);
978
+ }
710
979
  function toChainId(raw) {
711
980
  if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw <= 0) throw Errors.invalidInput("chainId", invalidValueReason("must be a positive safe integer", raw));
712
981
  return raw;
@@ -769,31 +1038,27 @@ function invalidValueReason(prefix, raw) {
769
1038
  return `${prefix}: ${String(raw)}`;
770
1039
  }
771
1040
  //#endregion
772
- //#region ../config/src/handle.ts
773
- const RESERVED_HANDLES = /* @__PURE__ */ new Set([
774
- "admin",
775
- "api",
776
- "capxul",
777
- "help",
778
- "root",
779
- "support",
780
- "system",
781
- "www"
782
- ]);
783
- function normalizeHandle(raw) {
784
- return raw.trim().toLowerCase().replace(/^@/, "");
785
- }
786
- function validateHandle(raw) {
787
- const handle = normalizeHandle(raw);
788
- if (!HANDLE_RE.test(handle)) throw Errors.invalidInput("handle", "must be 3-30 characters of a-z, 0-9, underscore, or hyphen");
789
- if (RESERVED_HANDLES.has(handle)) throw Errors.invalidInput("handle", "handle is reserved");
790
- return handle;
791
- }
792
- //#endregion
793
1041
  //#region ../config/src/tokens.ts
794
1042
  /** `TestUSDC` ("USDX") — Base Sepolia, 6 decimals, open `mint`. (Canon §1.) */
795
1043
  const USDX_ADDRESS_BASE_SEPOLIA = "0xf09fcbb6df9f8f918e58f9c8ab15a76ade862b77";
796
- USDX_ADDRESS_BASE_SEPOLIA.toLowerCase();
1044
+ const USDX_ASSET = {
1045
+ assetId: assetIdFor(84532, USDX_ADDRESS_BASE_SEPOLIA),
1046
+ chainId: toChainId(84532),
1047
+ symbol: "USDX",
1048
+ peg: toCurrencyCode("USD"),
1049
+ tokenAddress: USDX_ADDRESS_BASE_SEPOLIA,
1050
+ deploymentStartBlock: 39860173,
1051
+ decimals: 6
1052
+ };
1053
+ const CONFIGURED_MONEY_ASSETS = [{
1054
+ ...USDX_ASSET,
1055
+ currency: USDX_ASSET.peg
1056
+ }];
1057
+ CONFIGURED_MONEY_ASSETS.map((asset) => asset.tokenAddress.toLowerCase());
1058
+ function configuredMoneyAssetById(assetId) {
1059
+ const canonical = toAssetId(assetId);
1060
+ return CONFIGURED_MONEY_ASSETS.find((asset) => asset.assetId === canonical) ?? null;
1061
+ }
797
1062
  //#endregion
798
1063
  //#region ../config/src/org-payments.ts
799
1064
  /** Zodiac MultiSendCallOnly module on Base Sepolia. */
@@ -1762,6 +2027,26 @@ function errorKind(error) {
1762
2027
  var ClockError = class extends Data.TaggedError("ClockError") {};
1763
2028
  var ClockPortTag = class extends Context.Service()("@capxul/sdk/ports/ClockPort") {};
1764
2029
  //#endregion
2030
+ //#region src/adapters/clock/SystemClockAdapter.ts
2031
+ var SystemClockAdapter = class {
2032
+ now = Effect.try({
2033
+ try: () => toEpochMs(Date.now()),
2034
+ catch: (cause) => new ClockError({
2035
+ operation: "now",
2036
+ cause
2037
+ })
2038
+ });
2039
+ sleep(duration) {
2040
+ return Effect.callback((resume) => {
2041
+ const timeout = setTimeout(() => resume(Effect.void), duration);
2042
+ return Effect.sync(() => clearTimeout(timeout));
2043
+ });
2044
+ }
2045
+ };
2046
+ function SystemClockLayer() {
2047
+ return Layer.succeed(ClockPortTag, new SystemClockAdapter());
2048
+ }
2049
+ //#endregion
1765
2050
  //#region src/ports/auth-cache.ts
1766
2051
  var AuthCacheError = class extends Data.TaggedError("AuthCacheError") {};
1767
2052
  var AuthCachePortTag = class extends Context.Service()("@capxul/sdk/ports/AuthCachePort") {};
@@ -1890,4 +2175,192 @@ async function readClockNow(clock) {
1890
2175
  }
1891
2176
  }
1892
2177
  //#endregion
1893
- export { CapxulError as $, toAuthUserId as A, toJwtToken as B, WEI_RE as C, toAddress as D, toAccountId as E, toDurationMs as F, toPayrollRunId as G, toOrgId as H, toEmail as I, toSessionToken as J, toPublishableKey as K, toEpochMs as L, toChainId as M, toCountryCode as N, toAllowedOrigin as O, toCurrencyCode as P, CAPXUL_ERROR_CODES as Q, toEpochSeconds as R, SUPPORTED_CURRENCY_CODES as S, currencySymbolFor as T, toPartyId as U, toKycTier as V, toPayrollGroupId as W, HANDLE_RE as X, toTesterKind as Y, decodeConvexError as Z, deriveCapxulSafeAddress as _, parseCachedJwt as a, boundedResponseHeaders as at, BYTES32_RE as b, ClockError as c, decodeChainCause as ct, AuthClientError as d, isFailureMode as dt, EXPECTED_OPERATION_OUTCOMES as et, AuthClientPortTag as f, revertSummaryText as ft, BASE_SEPOLIA_CHAIN_ID as g, normalizeBindingEmail as h, parseAuthSession as i, FAILURE_MODES as it, toBudgetId as j, toAppId as k, ClockPortTag as l, failureFingerprint as lt, CAPXUL_PAYMENTS_V2_ADDRESS as m, redactSecrets as mt, InMemoryAuthCacheAdapter as n, isCapxulError as nt, AuthCacheError as o, chainCauseProperties as ot, orgRoleKeyForLabel as p, containsSensitiveMaterial as pt, toRoleKey as q, BrowserAuthCacheAdapter as r, CHAIN_UPSTREAMS as rt, AuthCachePortTag as s, chainEvidenceLabel as st, readClockNow as t, Errors as tt, authClientPortFromPromiseAdapter as u, isChainUpstream as ut, validateHandle as v, ZERO_BYTES32 as w, EVM_ADDRESS_RE as x, APP_ID_RE as y, toHandle as z };
2178
+ //#region src/adapters/auth-client/OAuthBearerAuthClient.ts
2179
+ const PROVIDER = "agent-exchange";
2180
+ /** Re-exchange this far ahead of expiry, matching the JWT cache eviction margin. */
2181
+ const REFRESH_MARGIN_MS = 3e4;
2182
+ /**
2183
+ * `AuthSession.token` is a Better Auth session handle. This flow has none, and
2184
+ * the access token must never be copied into it, so the slot holds a placeholder.
2185
+ */
2186
+ const OAUTH_SESSION_TOKEN = toSessionToken("oauth-bearer");
2187
+ var OAuthBearerAuthClient = class {
2188
+ #exchangeUrl;
2189
+ #exchangeSecret;
2190
+ #accessToken;
2191
+ #fetchImpl;
2192
+ #clock;
2193
+ #cached = null;
2194
+ constructor(input) {
2195
+ this.#exchangeUrl = input.exchangeUrl;
2196
+ this.#exchangeSecret = input.exchangeSecret;
2197
+ this.#accessToken = input.accessToken;
2198
+ this.#fetchImpl = input.fetch ?? fetch;
2199
+ this.#clock = input.clock ?? new SystemClockAdapter();
2200
+ }
2201
+ async canSendOtp() {
2202
+ return refuse("canSendOtp");
2203
+ }
2204
+ async sendOtp() {
2205
+ return refuse("sendOtp");
2206
+ }
2207
+ async verifyOtp() {
2208
+ return refuse("verifyOtp");
2209
+ }
2210
+ /**
2211
+ * The authorization server owns the token's lifetime, so revoking it there is
2212
+ * the sign-out. Reporting success here would claim a token was dropped.
2213
+ */
2214
+ async signOut() {
2215
+ return refuse("signOut");
2216
+ }
2217
+ /**
2218
+ * The exchanged identity. Unlike the Better Auth adapters this never answers
2219
+ * `null`: the consumer holds a usable access token or a refused one, and a
2220
+ * refused one is a NOT_AUTHENTICATED failure, not the absence of a session.
2221
+ */
2222
+ async getSession(options) {
2223
+ const exchange = await this.#exchange("getSession", options);
2224
+ if (!exchange.ok) return exchange;
2225
+ return {
2226
+ ok: true,
2227
+ value: {
2228
+ authUserId: toAuthUserId(exchange.value.authUserId),
2229
+ email: toEmail(exchange.value.email),
2230
+ token: OAUTH_SESSION_TOKEN,
2231
+ expiresAt: toEpochMs(exchange.value.expiresAt)
2232
+ }
2233
+ };
2234
+ }
2235
+ async getConvexJwt(options) {
2236
+ const exchange = await this.#exchange("getConvexJwt", options);
2237
+ if (!exchange.ok) return exchange;
2238
+ return {
2239
+ ok: true,
2240
+ value: {
2241
+ token: toJwtToken(exchange.value.token),
2242
+ expEpochSeconds: toEpochSeconds(Math.floor(exchange.value.expiresAt / 1e3))
2243
+ }
2244
+ };
2245
+ }
2246
+ /**
2247
+ * The cached exchange while it is fresh, otherwise a new one. A failed
2248
+ * exchange never replaces the cache, so a stale token is never served.
2249
+ *
2250
+ * lazy: no in-flight de-duplication — two concurrent reads past the margin
2251
+ * each mint a JWT. Minting is idempotent, so the cost is one extra request.
2252
+ */
2253
+ async #exchange(operation, options) {
2254
+ if (options?.signal?.aborted) return {
2255
+ ok: false,
2256
+ error: Errors.cancelled({ operation })
2257
+ };
2258
+ const cached = this.#cached;
2259
+ if (cached !== null && options?.forceRefresh !== true) {
2260
+ const now = await readClockNow(this.#clock);
2261
+ if (!now.ok) return now;
2262
+ if (now.value < cached.expiresAt - REFRESH_MARGIN_MS) return {
2263
+ ok: true,
2264
+ value: cached
2265
+ };
2266
+ }
2267
+ return this.#postExchange(operation, options?.signal);
2268
+ }
2269
+ async #postExchange(operation, signal) {
2270
+ let res;
2271
+ try {
2272
+ res = await this.#fetchImpl(this.#exchangeUrl, {
2273
+ method: "POST",
2274
+ headers: {
2275
+ "content-type": "application/json",
2276
+ authorization: `Bearer ${this.#exchangeSecret}`
2277
+ },
2278
+ body: JSON.stringify({ accessToken: this.#accessToken }),
2279
+ ...signal === void 0 ? {} : { signal }
2280
+ });
2281
+ } catch (cause) {
2282
+ return {
2283
+ ok: false,
2284
+ error: signal?.aborted === true || isAbortError(cause) ? Errors.cancelled({ operation }) : Errors.networkError(operation, cause)
2285
+ };
2286
+ }
2287
+ if (signal?.aborted === true) return {
2288
+ ok: false,
2289
+ error: Errors.cancelled({ operation })
2290
+ };
2291
+ const body = await res.json().catch(() => null);
2292
+ if (!res.ok) return {
2293
+ ok: false,
2294
+ error: exchangeRefused(operation, res.status, readErrorCode(body))
2295
+ };
2296
+ const exchange = readExchange(body);
2297
+ if (exchange === null) return {
2298
+ ok: false,
2299
+ error: Errors.providerError(PROVIDER, operation, /* @__PURE__ */ new Error("unexpected body"), { httpStatus: res.status })
2300
+ };
2301
+ this.#cached = exchange;
2302
+ return {
2303
+ ok: true,
2304
+ value: exchange
2305
+ };
2306
+ }
2307
+ };
2308
+ /**
2309
+ * `AuthClientPort` over the backend agent-token exchange. Build one per OAuth
2310
+ * access token and hand it to `createCapxulClient({ authClient })`.
2311
+ */
2312
+ function oauthBearerAuthClient(input) {
2313
+ return authClientPortFromPromiseAdapter(new OAuthBearerAuthClient(input));
2314
+ }
2315
+ /** The OTP verbs belong to the email flow; `validStates` names it for the caller. */
2316
+ function refuse(method) {
2317
+ return {
2318
+ ok: false,
2319
+ error: Errors.wrongState({
2320
+ method,
2321
+ currentState: "oauth-bearer",
2322
+ validStates: ["email-otp"]
2323
+ })
2324
+ };
2325
+ }
2326
+ /**
2327
+ * The error catalog has no FORBIDDEN code, so 403 (`insufficient_scope`) and
2328
+ * 503 (`exchange_unconfigured`) both land on PROVIDER_ERROR. `httpStatus` and
2329
+ * `details.reason` carry which one the backend reported. Only 401 becomes
2330
+ * NOT_AUTHENTICATED, because only 401 is answered by a new access token.
2331
+ */
2332
+ function exchangeRefused(operation, status, reason) {
2333
+ if (status === 401) return Errors.notAuthenticated("The exchange refused this OAuth access token");
2334
+ return Errors.providerError(PROVIDER, operation, /* @__PURE__ */ new Error(`HTTP ${status}`), {
2335
+ httpStatus: status,
2336
+ ...reason === void 0 ? {} : { details: { reason } }
2337
+ });
2338
+ }
2339
+ /** An abort raised by something other than the caller's own signal. */
2340
+ function isAbortError(cause) {
2341
+ return readField(cause, "name") === "AbortError";
2342
+ }
2343
+ function readErrorCode(body) {
2344
+ const error = readField(body, "error");
2345
+ return typeof error === "string" ? error : void 0;
2346
+ }
2347
+ function readField(value, key) {
2348
+ if (typeof value !== "object" || value === null) return void 0;
2349
+ return value[key];
2350
+ }
2351
+ function readExchange(body) {
2352
+ if (typeof body !== "object" || body === null) return null;
2353
+ const { token, expiresAt, authUserId, email } = body;
2354
+ if (typeof token !== "string" || token.length === 0) return null;
2355
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
2356
+ if (typeof authUserId !== "string" || authUserId.length === 0) return null;
2357
+ if (typeof email !== "string" || email.length === 0) return null;
2358
+ return {
2359
+ token,
2360
+ expiresAt,
2361
+ authUserId,
2362
+ email
2363
+ };
2364
+ }
2365
+ //#endregion
2366
+ export { toTesterKind as $, toAccountId as A, toEmail as B, BYTES32_RE as C, ZERO_BYTES32 as D, WEI_RE as E, toBudgetId as F, toKycTier as G, toEpochSeconds as H, toChainId as I, toPayrollGroupId as J, toOrgId as K, toCountryCode as L, toAllowedOrigin as M, toAppId as N, assetIdFor as O, toAuthUserId as P, toSessionToken as Q, toCurrencyCode as R, ASSET_ID_RE as S, SUPPORTED_CURRENCY_CODES as T, toHandle as U, toEpochMs as V, toJwtToken as W, toPublishableKey as X, toPayrollRunId as Y, toRoleKey as Z, BASE_SEPOLIA_CHAIN_ID as _, revertSummaryText as _t, parseAuthSession as a, EXPECTED_OPERATION_OUTCOMES as at, ACCOUNT_ID_RE as b, redactSecrets as bt, AuthCachePortTag as c, CHAIN_UPSTREAMS as ct, authClientPortFromPromiseAdapter as d, chainCauseProperties as dt, validateHandle as et, AuthClientError as f, chainEvidenceLabel as ft, normalizeBindingEmail as g, isFailureMode as gt, CAPXUL_PAYMENTS_V2_ADDRESS as h, isChainUpstream as ht, BrowserAuthCacheAdapter as i, CapxulError as it, toAddress as j, currencySymbolFor as k, SystemClockLayer as l, FAILURE_MODES as lt, orgRoleKeyForLabel as m, failureFingerprint as mt, readClockNow as n, decodeConvexError as nt, parseCachedJwt as o, Errors as ot, AuthClientPortTag as p, decodeChainCause as pt, toPartyId as q, InMemoryAuthCacheAdapter as r, CAPXUL_ERROR_CODES as rt, AuthCacheError as s, isCapxulError as st, oauthBearerAuthClient as t, HANDLE_RE as tt, ClockPortTag as u, boundedResponseHeaders as ut, deriveCapxulSafeAddress as v, containsSensitiveMaterial as vt, EVM_ADDRESS_RE as w, APP_ID_RE as x, redactUrlSecrets as xt, configuredMoneyAssetById as y, isCredentialField as yt, toDurationMs as z };