@capxul/sdk 4.1.4 → 4.2.0-rc.10

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)}`;
@@ -644,6 +902,7 @@ const COUNTRY_CODE_RE = /^[A-Z]{2}$/;
644
902
  const ACCOUNT_ID_RE = /^account_[0-9A-Za-z]+$/;
645
903
  const PARTY_ID_RE = /^party_[0-9A-Za-z]+$/;
646
904
  const APP_ID_RE = /^app_[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
905
+ const TX_HASH_RE = /^0x[0-9a-f]{64}$/i;
647
906
  const WEI_RE = /^[0-9]+$/;
648
907
  Math.floor(Number.MAX_SAFE_INTEGER / 1e3);
649
908
  function toAddress(raw) {
@@ -699,6 +958,10 @@ function toPublishableKey(raw) {
699
958
  if (typeof raw !== "string" || !PUBLISHABLE_KEY_PATTERN.test(raw)) throw Errors.invalidInput("publishableKey", invalidValueReason("must match cap_pk_(test|live) plus 32 Crockford base32 chars", raw));
700
959
  return raw;
701
960
  }
961
+ function toTxHash(raw) {
962
+ if (typeof raw !== "string" || !TX_HASH_RE.test(raw)) throw Errors.invalidInput("txHash", invalidValueReason("must be 0x + 64 hex chars", raw));
963
+ return raw.toLowerCase();
964
+ }
702
965
  function toEpochMs(raw) {
703
966
  assertSafeNonNegativeInteger(raw, "epochMs");
704
967
  return raw;
@@ -707,6 +970,17 @@ function toEpochSeconds(raw) {
707
970
  assertSafeNonNegativeInteger(raw, "epochSeconds");
708
971
  return raw;
709
972
  }
973
+ /** Validate an already canonical identity without changing signed input. */
974
+ function toAssetId(raw) {
975
+ const match = typeof raw === "string" ? ASSET_ID_RE.exec(raw) : null;
976
+ if (match === null || match[0] !== raw || !Number.isSafeInteger(Number(match[1]))) throw Errors.invalidInput("assetId", "must be a canonical ERC-20 CAIP-19 identifier");
977
+ if (match[2] === "0x0000000000000000000000000000000000000000") throw Errors.invalidInput("assetId", "must identify a nonzero token contract");
978
+ return raw;
979
+ }
980
+ /** Build an identity from verified chain facts. Normalize the token address. */
981
+ function assetIdFor(chainId, tokenAddress) {
982
+ return toAssetId(`eip155:${toChainId(chainId)}/erc20:${toAddress(tokenAddress)}`);
983
+ }
710
984
  function toChainId(raw) {
711
985
  if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw <= 0) throw Errors.invalidInput("chainId", invalidValueReason("must be a positive safe integer", raw));
712
986
  return raw;
@@ -769,31 +1043,121 @@ function invalidValueReason(prefix, raw) {
769
1043
  return `${prefix}: ${String(raw)}`;
770
1044
  }
771
1045
  //#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
1046
  //#region ../config/src/tokens.ts
794
1047
  /** `TestUSDC` ("USDX") — Base Sepolia, 6 decimals, open `mint`. (Canon §1.) */
795
1048
  const USDX_ADDRESS_BASE_SEPOLIA = "0xf09fcbb6df9f8f918e58f9c8ab15a76ade862b77";
796
- USDX_ADDRESS_BASE_SEPOLIA.toLowerCase();
1049
+ const USDX_DEPLOYMENT_START_BLOCK = 39860173;
1050
+ /**
1051
+ * The ERC-20 `symbol()` the deployed `TestUSDC` contract returns
1052
+ * (`packages/contracts/src/TestUSDC.sol:10`). It travels with the address and
1053
+ * the decimals so a holdings row can never carry one token's address with
1054
+ * another token's display symbol. This is the display symbol, not the
1055
+ * currency: see `USDX_CURRENCY` below.
1056
+ */
1057
+ const USDX_SYMBOL = "USDX";
1058
+ const SYNTHETIC_TEST_ASSET_SOURCE = "synthetic-test-liquidity";
1059
+ const USDX_ASSET = {
1060
+ assetId: assetIdFor(84532, USDX_ADDRESS_BASE_SEPOLIA),
1061
+ chainId: toChainId(84532),
1062
+ symbol: USDX_SYMBOL,
1063
+ peg: toCurrencyCode("USD"),
1064
+ tokenAddress: USDX_ADDRESS_BASE_SEPOLIA,
1065
+ deploymentStartBlock: USDX_DEPLOYMENT_START_BLOCK,
1066
+ decimals: 6,
1067
+ source: "settlement"
1068
+ };
1069
+ /**
1070
+ * The settlement tier: rows the exact-transfer path and the Movement scanner
1071
+ * admit, each carrying the legacy `currency` projection those readers expect.
1072
+ * Adding an owned test asset here would put candidate liquidity on the
1073
+ * money-write path, so M03's rows go to the candidate tier below instead.
1074
+ */
1075
+ const CONFIGURED_MONEY_ASSETS = [{
1076
+ ...USDX_ASSET,
1077
+ currency: USDX_ASSET.peg
1078
+ }];
1079
+ /**
1080
+ * The candidate tier: five owned test assets, every field read back from Base
1081
+ * Sepolia after the deployment that created it (chain 84532, blocks
1082
+ * 46,465,978–46,465,982). Real contracts, authored liquidity, no settlement
1083
+ * admission — so rollback is removing consumers, never relabelling a
1084
+ * deployed address or deleting its history.
1085
+ *
1086
+ * `symbol` is the display string the owned contract returns. A
1087
+ * provider-supported USDC would be a different contract at a different
1088
+ * address returning the same symbol, which is why the resolvers below key on
1089
+ * `assetId`, or on chain plus address, and never on a symbol.
1090
+ */
1091
+ const SYNTHETIC_TEST_ASSETS = [
1092
+ {
1093
+ assetId: assetIdFor(84532, "0xb3d8566fb90f7df939f4bc08a09b19047117f549"),
1094
+ chainId: toChainId(84532),
1095
+ tokenAddress: "0xb3d8566fb90f7df939f4bc08a09b19047117f549",
1096
+ symbol: "USDC",
1097
+ decimals: 6,
1098
+ peg: toCurrencyCode("USD"),
1099
+ deploymentStartBlock: 46465978,
1100
+ deploymentTransaction: "0xa7fb8bb9d1fd1cc5fe2fc42fa4cdccace7742efae8ea99f091a283e5709343de",
1101
+ source: SYNTHETIC_TEST_ASSET_SOURCE
1102
+ },
1103
+ {
1104
+ assetId: assetIdFor(84532, "0x7ddcfb6aaffb9908c4c2db308b784c269eec8a01"),
1105
+ chainId: toChainId(84532),
1106
+ tokenAddress: "0x7ddcfb6aaffb9908c4c2db308b784c269eec8a01",
1107
+ symbol: "USDT",
1108
+ decimals: 6,
1109
+ peg: toCurrencyCode("USD"),
1110
+ deploymentStartBlock: 46465979,
1111
+ deploymentTransaction: "0xa17ad76fe8b24cdf1fbcff245c3eb022444710cf9fa7b5ee59818be654f4ae10",
1112
+ source: SYNTHETIC_TEST_ASSET_SOURCE
1113
+ },
1114
+ {
1115
+ assetId: assetIdFor(84532, "0x1d93d525f73453fe18eebf044e8a3954bfe3721e"),
1116
+ chainId: toChainId(84532),
1117
+ tokenAddress: "0x1d93d525f73453fe18eebf044e8a3954bfe3721e",
1118
+ symbol: "WETH",
1119
+ decimals: 18,
1120
+ peg: null,
1121
+ deploymentStartBlock: 46465980,
1122
+ deploymentTransaction: "0xcb9b2d024686fac93ec1243ccf02d3567fd531da46176115660cba2ba644b09d",
1123
+ source: SYNTHETIC_TEST_ASSET_SOURCE
1124
+ },
1125
+ {
1126
+ assetId: assetIdFor(84532, "0x8fe52c70aa9f7b6d74d8bb26eca33d87413cb624"),
1127
+ chainId: toChainId(84532),
1128
+ tokenAddress: "0x8fe52c70aa9f7b6d74d8bb26eca33d87413cb624",
1129
+ symbol: "WBTC",
1130
+ decimals: 8,
1131
+ peg: null,
1132
+ deploymentStartBlock: 46465981,
1133
+ deploymentTransaction: "0x728ba7b430ffd141f83d0ed5ce429d050049d7b097c936fd11e9c572d1243854",
1134
+ source: SYNTHETIC_TEST_ASSET_SOURCE
1135
+ },
1136
+ {
1137
+ assetId: assetIdFor(84532, "0x5865fe9787ac214feb5facaebaec06969ac0a9a3"),
1138
+ chainId: toChainId(84532),
1139
+ tokenAddress: "0x5865fe9787ac214feb5facaebaec06969ac0a9a3",
1140
+ symbol: "cNGN",
1141
+ decimals: 6,
1142
+ peg: toCurrencyCode("NGN"),
1143
+ deploymentStartBlock: 46465982,
1144
+ deploymentTransaction: "0xcedf254bcef57fc64189261a5ab1161bc4fde370e470933f627c4d8ddb5a2e26",
1145
+ source: SYNTHETIC_TEST_ASSET_SOURCE
1146
+ }
1147
+ ];
1148
+ toChainId(84532);
1149
+ /**
1150
+ * Every known asset in ONE identity space. The tiers above declare admission;
1151
+ * this is the only list an identity resolves against, so an asset is never
1152
+ * two different things depending on which list a caller happened to read.
1153
+ */
1154
+ const MONEY_ASSET_REGISTRY = [...CONFIGURED_MONEY_ASSETS, ...SYNTHETIC_TEST_ASSETS];
1155
+ CONFIGURED_MONEY_ASSETS.map((asset) => asset.tokenAddress.toLowerCase());
1156
+ /** The registry row for `assetId` across every tier, or `null`. */
1157
+ function configuredMoneyAssetById(assetId) {
1158
+ const canonical = toAssetId(assetId);
1159
+ return MONEY_ASSET_REGISTRY.find((asset) => asset.assetId === canonical) ?? null;
1160
+ }
797
1161
  //#endregion
798
1162
  //#region ../config/src/org-payments.ts
799
1163
  /** Zodiac MultiSendCallOnly module on Base Sepolia. */
@@ -1682,6 +2046,12 @@ const USDX_BASE_SEPOLIA_TOKEN = {
1682
2046
  USDX_BASE_SEPOLIA_TOKEN.chainId;
1683
2047
  USDX_BASE_SEPOLIA_TOKEN.chainId;
1684
2048
  10n ** BigInt(6);
2049
+ /**
2050
+ * The bootstrap manage role. It administers people and roles, cannot spend,
2051
+ * and is not a signer of the Organization Safe; "Owner" implied the latter,
2052
+ * so the label is Admin (ruled 2026-09-06).
2053
+ */
2054
+ const ADMIN_ROLE_LABEL = "Admin";
1685
2055
  function normalizeOrgRoleLabel(label) {
1686
2056
  const normalized = label.trim().replace(/\s+/g, " ");
1687
2057
  if (normalized.length === 0) throw Errors.invalidInput("role.label", "must be a non-empty role label");
@@ -1762,6 +2132,26 @@ function errorKind(error) {
1762
2132
  var ClockError = class extends Data.TaggedError("ClockError") {};
1763
2133
  var ClockPortTag = class extends Context.Service()("@capxul/sdk/ports/ClockPort") {};
1764
2134
  //#endregion
2135
+ //#region src/adapters/clock/SystemClockAdapter.ts
2136
+ var SystemClockAdapter = class {
2137
+ now = Effect.try({
2138
+ try: () => toEpochMs(Date.now()),
2139
+ catch: (cause) => new ClockError({
2140
+ operation: "now",
2141
+ cause
2142
+ })
2143
+ });
2144
+ sleep(duration) {
2145
+ return Effect.callback((resume) => {
2146
+ const timeout = setTimeout(() => resume(Effect.void), duration);
2147
+ return Effect.sync(() => clearTimeout(timeout));
2148
+ });
2149
+ }
2150
+ };
2151
+ function SystemClockLayer() {
2152
+ return Layer.succeed(ClockPortTag, new SystemClockAdapter());
2153
+ }
2154
+ //#endregion
1765
2155
  //#region src/ports/auth-cache.ts
1766
2156
  var AuthCacheError = class extends Data.TaggedError("AuthCacheError") {};
1767
2157
  var AuthCachePortTag = class extends Context.Service()("@capxul/sdk/ports/AuthCachePort") {};
@@ -1890,4 +2280,192 @@ async function readClockNow(clock) {
1890
2280
  }
1891
2281
  }
1892
2282
  //#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 };
2283
+ //#region src/adapters/auth-client/OAuthBearerAuthClient.ts
2284
+ const PROVIDER = "agent-exchange";
2285
+ /** Re-exchange this far ahead of expiry, matching the JWT cache eviction margin. */
2286
+ const REFRESH_MARGIN_MS = 3e4;
2287
+ /**
2288
+ * `AuthSession.token` is a Better Auth session handle. This flow has none, and
2289
+ * the access token must never be copied into it, so the slot holds a placeholder.
2290
+ */
2291
+ const OAUTH_SESSION_TOKEN = toSessionToken("oauth-bearer");
2292
+ var OAuthBearerAuthClient = class {
2293
+ #exchangeUrl;
2294
+ #exchangeSecret;
2295
+ #accessToken;
2296
+ #fetchImpl;
2297
+ #clock;
2298
+ #cached = null;
2299
+ constructor(input) {
2300
+ this.#exchangeUrl = input.exchangeUrl;
2301
+ this.#exchangeSecret = input.exchangeSecret;
2302
+ this.#accessToken = input.accessToken;
2303
+ this.#fetchImpl = input.fetch ?? fetch;
2304
+ this.#clock = input.clock ?? new SystemClockAdapter();
2305
+ }
2306
+ async canSendOtp() {
2307
+ return refuse("canSendOtp");
2308
+ }
2309
+ async sendOtp() {
2310
+ return refuse("sendOtp");
2311
+ }
2312
+ async verifyOtp() {
2313
+ return refuse("verifyOtp");
2314
+ }
2315
+ /**
2316
+ * The authorization server owns the token's lifetime, so revoking it there is
2317
+ * the sign-out. Reporting success here would claim a token was dropped.
2318
+ */
2319
+ async signOut() {
2320
+ return refuse("signOut");
2321
+ }
2322
+ /**
2323
+ * The exchanged identity. Unlike the Better Auth adapters this never answers
2324
+ * `null`: the consumer holds a usable access token or a refused one, and a
2325
+ * refused one is a NOT_AUTHENTICATED failure, not the absence of a session.
2326
+ */
2327
+ async getSession(options) {
2328
+ const exchange = await this.#exchange("getSession", options);
2329
+ if (!exchange.ok) return exchange;
2330
+ return {
2331
+ ok: true,
2332
+ value: {
2333
+ authUserId: toAuthUserId(exchange.value.authUserId),
2334
+ email: toEmail(exchange.value.email),
2335
+ token: OAUTH_SESSION_TOKEN,
2336
+ expiresAt: toEpochMs(exchange.value.expiresAt)
2337
+ }
2338
+ };
2339
+ }
2340
+ async getConvexJwt(options) {
2341
+ const exchange = await this.#exchange("getConvexJwt", options);
2342
+ if (!exchange.ok) return exchange;
2343
+ return {
2344
+ ok: true,
2345
+ value: {
2346
+ token: toJwtToken(exchange.value.token),
2347
+ expEpochSeconds: toEpochSeconds(Math.floor(exchange.value.expiresAt / 1e3))
2348
+ }
2349
+ };
2350
+ }
2351
+ /**
2352
+ * The cached exchange while it is fresh, otherwise a new one. A failed
2353
+ * exchange never replaces the cache, so a stale token is never served.
2354
+ *
2355
+ * lazy: no in-flight de-duplication — two concurrent reads past the margin
2356
+ * each mint a JWT. Minting is idempotent, so the cost is one extra request.
2357
+ */
2358
+ async #exchange(operation, options) {
2359
+ if (options?.signal?.aborted) return {
2360
+ ok: false,
2361
+ error: Errors.cancelled({ operation })
2362
+ };
2363
+ const cached = this.#cached;
2364
+ if (cached !== null && options?.forceRefresh !== true) {
2365
+ const now = await readClockNow(this.#clock);
2366
+ if (!now.ok) return now;
2367
+ if (now.value < cached.expiresAt - REFRESH_MARGIN_MS) return {
2368
+ ok: true,
2369
+ value: cached
2370
+ };
2371
+ }
2372
+ return this.#postExchange(operation, options?.signal);
2373
+ }
2374
+ async #postExchange(operation, signal) {
2375
+ let res;
2376
+ try {
2377
+ res = await this.#fetchImpl(this.#exchangeUrl, {
2378
+ method: "POST",
2379
+ headers: {
2380
+ "content-type": "application/json",
2381
+ authorization: `Bearer ${this.#exchangeSecret}`
2382
+ },
2383
+ body: JSON.stringify({ accessToken: this.#accessToken }),
2384
+ ...signal === void 0 ? {} : { signal }
2385
+ });
2386
+ } catch (cause) {
2387
+ return {
2388
+ ok: false,
2389
+ error: signal?.aborted === true || isAbortError(cause) ? Errors.cancelled({ operation }) : Errors.networkError(operation, cause)
2390
+ };
2391
+ }
2392
+ if (signal?.aborted === true) return {
2393
+ ok: false,
2394
+ error: Errors.cancelled({ operation })
2395
+ };
2396
+ const body = await res.json().catch(() => null);
2397
+ if (!res.ok) return {
2398
+ ok: false,
2399
+ error: exchangeRefused(operation, res.status, readErrorCode(body))
2400
+ };
2401
+ const exchange = readExchange(body);
2402
+ if (exchange === null) return {
2403
+ ok: false,
2404
+ error: Errors.providerError(PROVIDER, operation, /* @__PURE__ */ new Error("unexpected body"), { httpStatus: res.status })
2405
+ };
2406
+ this.#cached = exchange;
2407
+ return {
2408
+ ok: true,
2409
+ value: exchange
2410
+ };
2411
+ }
2412
+ };
2413
+ /**
2414
+ * `AuthClientPort` over the backend agent-token exchange. Build one per OAuth
2415
+ * access token and hand it to `createCapxulClient({ authClient })`.
2416
+ */
2417
+ function oauthBearerAuthClient(input) {
2418
+ return authClientPortFromPromiseAdapter(new OAuthBearerAuthClient(input));
2419
+ }
2420
+ /** The OTP verbs belong to the email flow; `validStates` names it for the caller. */
2421
+ function refuse(method) {
2422
+ return {
2423
+ ok: false,
2424
+ error: Errors.wrongState({
2425
+ method,
2426
+ currentState: "oauth-bearer",
2427
+ validStates: ["email-otp"]
2428
+ })
2429
+ };
2430
+ }
2431
+ /**
2432
+ * The error catalog has no FORBIDDEN code, so 403 (`insufficient_scope`) and
2433
+ * 503 (`exchange_unconfigured`) both land on PROVIDER_ERROR. `httpStatus` and
2434
+ * `details.reason` carry which one the backend reported. Only 401 becomes
2435
+ * NOT_AUTHENTICATED, because only 401 is answered by a new access token.
2436
+ */
2437
+ function exchangeRefused(operation, status, reason) {
2438
+ if (status === 401) return Errors.notAuthenticated("The exchange refused this OAuth access token");
2439
+ return Errors.providerError(PROVIDER, operation, /* @__PURE__ */ new Error(`HTTP ${status}`), {
2440
+ httpStatus: status,
2441
+ ...reason === void 0 ? {} : { details: { reason } }
2442
+ });
2443
+ }
2444
+ /** An abort raised by something other than the caller's own signal. */
2445
+ function isAbortError(cause) {
2446
+ return readField(cause, "name") === "AbortError";
2447
+ }
2448
+ function readErrorCode(body) {
2449
+ const error = readField(body, "error");
2450
+ return typeof error === "string" ? error : void 0;
2451
+ }
2452
+ function readField(value, key) {
2453
+ if (typeof value !== "object" || value === null) return void 0;
2454
+ return value[key];
2455
+ }
2456
+ function readExchange(body) {
2457
+ if (typeof body !== "object" || body === null) return null;
2458
+ const { token, expiresAt, authUserId, email } = body;
2459
+ if (typeof token !== "string" || token.length === 0) return null;
2460
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
2461
+ if (typeof authUserId !== "string" || authUserId.length === 0) return null;
2462
+ if (typeof email !== "string" || email.length === 0) return null;
2463
+ return {
2464
+ token,
2465
+ expiresAt,
2466
+ authUserId,
2467
+ email
2468
+ };
2469
+ }
2470
+ //#endregion
2471
+ export { toRoleKey as $, currencySymbolFor as A, toCurrencyCode as B, ASSET_ID_RE as C, redactSecrets as Ct, WEI_RE as D, SUPPORTED_CURRENCY_CODES as E, toAssetId as F, toHandle as G, toEmail as H, toAuthUserId as I, toOrgId as J, toJwtToken as K, toBudgetId as L, toAddress as M, toAllowedOrigin as N, ZERO_BYTES32 as O, toAppId as P, toPublishableKey as Q, toChainId as R, APP_ID_RE as S, isCredentialField as St, EVM_ADDRESS_RE as T, toEpochMs as U, toDurationMs as V, toEpochSeconds as W, toPayrollGroupId as X, toPartyId as Y, toPayrollRunId as Z, normalizeBindingEmail as _, failureFingerprint as _t, parseAuthSession as a, decodeConvexError as at, configuredMoneyAssetById as b, revertSummaryText as bt, AuthCachePortTag as c, EXPECTED_OPERATION_OUTCOMES as ct, authClientPortFromPromiseAdapter as d, CHAIN_UPSTREAMS as dt, toSessionToken as et, AuthClientError as f, FAILURE_MODES as ft, CAPXUL_PAYMENTS_V2_ADDRESS as g, decodeChainCause as gt, orgRoleKeyForLabel as h, chainEvidenceLabel as ht, BrowserAuthCacheAdapter as i, HANDLE_RE as it, toAccountId as j, assetIdFor as k, SystemClockLayer as l, Errors as lt, ADMIN_ROLE_LABEL as m, chainCauseProperties as mt, readClockNow as n, toTxHash as nt, parseCachedJwt as o, CAPXUL_ERROR_CODES as ot, AuthClientPortTag as p, boundedResponseHeaders as pt, toKycTier as q, InMemoryAuthCacheAdapter as r, validateHandle as rt, AuthCacheError as s, CapxulError as st, oauthBearerAuthClient as t, toTesterKind as tt, ClockPortTag as u, isCapxulError as ut, BASE_SEPOLIA_CHAIN_ID as v, isChainUpstream as vt, BYTES32_RE as w, redactUrlSecrets as wt, ACCOUNT_ID_RE as x, containsSensitiveMaterial as xt, deriveCapxulSafeAddress as y, isFailureMode as yt, toCountryCode as z };