@tatchi-xyz/sdk 0.54.0 → 0.55.0
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/cjs/plugins/headers.js +16 -3
- package/dist/cjs/plugins/headers.js.map +1 -1
- package/dist/cjs/plugins/next.js +20 -7
- package/dist/cjs/plugins/next.js.map +1 -1
- package/dist/cjs/plugins/plugin-utils.js +18 -6
- package/dist/cjs/plugins/plugin-utils.js.map +1 -1
- package/dist/cjs/plugins/vite.js +36 -18
- package/dist/cjs/plugins/vite.js.map +1 -1
- package/dist/esm/plugins/headers.js +16 -3
- package/dist/esm/plugins/headers.js.map +1 -1
- package/dist/esm/plugins/next.js +20 -7
- package/dist/esm/plugins/next.js.map +1 -1
- package/dist/esm/plugins/plugin-utils.js +18 -6
- package/dist/esm/plugins/plugin-utils.js.map +1 -1
- package/dist/esm/plugins/vite.js +37 -19
- package/dist/esm/plugins/vite.js.map +1 -1
- package/dist/esm/wasm_vrf_worker/pkg/wasm_vrf_worker_bg.wasm +0 -0
- package/dist/types/src/plugins/headers.d.ts +1 -1
- package/dist/types/src/plugins/headers.d.ts.map +1 -1
- package/dist/types/src/plugins/next.d.ts +5 -5
- package/dist/types/src/plugins/next.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-utils.d.ts +1 -1
- package/dist/types/src/plugins/plugin-utils.d.ts.map +1 -1
- package/dist/types/src/plugins/vite.d.ts +7 -7
- package/dist/types/src/plugins/vite.d.ts.map +1 -1
- package/dist/workers/wasm_vrf_worker_bg.wasm +0 -0
- package/package.json +1 -1
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
const require_validation = require('./utils/validation.js');
|
|
2
2
|
|
|
3
3
|
//#region src/plugins/headers.ts
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
const
|
|
4
|
+
function normalizeWalletOrigins(walletOrigins) {
|
|
5
|
+
if (!Array.isArray(walletOrigins) || walletOrigins.length === 0) return [];
|
|
6
|
+
const out = [];
|
|
7
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8
|
+
for (const origin of walletOrigins) {
|
|
9
|
+
const normalized = require_validation.toOriginOrUndefined(origin);
|
|
10
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
11
|
+
seen.add(normalized);
|
|
12
|
+
out.push(normalized);
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function buildPermissionsPolicy(walletOrigins) {
|
|
17
|
+
const origins = normalizeWalletOrigins(walletOrigins);
|
|
18
|
+
const originPart = origins.length ? " " + origins.map((o) => `"${o}"`).join(" ") : "";
|
|
19
|
+
const part = (name) => `${name}=(self${originPart})`;
|
|
7
20
|
return [
|
|
8
21
|
part("publickey-credentials-get"),
|
|
9
22
|
part("publickey-credentials-create"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","names":["toOriginOrUndefined","mode: CspMode","base: string[]"],"sources":["../../../src/plugins/headers.ts"],"sourcesContent":["// Framework-agnostic header helpers to minimize configuration surface.\n// Keep types local to avoid coupling to framework packages.\n\nimport { toOriginOrUndefined } from '../utils/validation'\n\nexport type CspMode = 'strict' | 'compatible'\n\
|
|
1
|
+
{"version":3,"file":"headers.js","names":["out: string[]","toOriginOrUndefined","mode: CspMode","base: string[]"],"sources":["../../../src/plugins/headers.ts"],"sourcesContent":["// Framework-agnostic header helpers to minimize configuration surface.\n// Keep types local to avoid coupling to framework packages.\n\nimport { toOriginOrUndefined } from '../utils/validation'\n\nexport type CspMode = 'strict' | 'compatible'\n\nfunction normalizeWalletOrigins(walletOrigins?: string[]): string[] {\n if (!Array.isArray(walletOrigins) || walletOrigins.length === 0) return []\n const out: string[] = []\n const seen = new Set<string>()\n for (const origin of walletOrigins) {\n const normalized = toOriginOrUndefined(origin)\n if (!normalized || seen.has(normalized)) continue\n seen.add(normalized)\n out.push(normalized)\n }\n return out\n}\n\nexport function buildPermissionsPolicy(walletOrigins?: string[]): string {\n const origins = normalizeWalletOrigins(walletOrigins)\n const originPart = origins.length ? ' ' + origins.map((o) => `\"${o}\"`).join(' ') : ''\n const part = (name: string) => `${name}=(self${originPart})`\n return [\n part('publickey-credentials-get'),\n part('publickey-credentials-create'),\n part('clipboard-read'),\n part('clipboard-write'),\n ].join(', ')\n}\n\n/**\n * Build a wallet-friendly Content Security Policy string.\n *\n * mode:\n * - 'strict' (default): no inline styles, injects \"style-src-attr 'none'\", and forbids 'unsafe-eval'.\n * - 'compatible': allows inline styles/scripts via 'unsafe-inline' for friendlier dev/local setups.\n *\n * allowUnsafeEval:\n * - false by default. Set to true only for development servers that require eval (e.g., Next.js Fast Refresh).\n * - Tatchi SDK does not require 'unsafe-eval' in production.\n *\n * Typical usage: apply strict CSP only to wallet HTML routes\n * (/wallet-service, /export-viewer); do not attach CSP to host app routes.\n */\nexport function buildWalletCsp(opts: {\n frameSrc?: string[]\n mode?: CspMode\n allowUnsafeEval?: boolean\n scriptSrcAllowlist?: string[]\n} = {}): string {\n const mode: CspMode = opts.mode || 'strict'\n const frame = (opts.frameSrc || []).filter(Boolean)\n const scriptAllow = (opts.scriptSrcAllowlist || [])\n .map((s) => toOriginOrUndefined(s) || s)\n .filter(Boolean) as string[]\n const scriptUnsafeInline = mode === 'compatible' ? \" 'unsafe-inline'\" : ''\n const styleUnsafeInline = mode === 'compatible' ? \" 'unsafe-inline'\" : ''\n const scriptUnsafeEval = opts.allowUnsafeEval ? \" 'unsafe-eval'\" : ''\n const base: string[] = [\n \"default-src 'self'\",\n `script-src 'self'${scriptUnsafeInline}${scriptUnsafeEval}${scriptAllow.length ? ' ' + scriptAllow.join(' ') : ''}`,\n `style-src 'self'${styleUnsafeInline}`,\n \"img-src 'self' data:\",\n \"font-src 'self'\",\n \"connect-src 'self' https:\",\n \"worker-src 'self' blob:\",\n `frame-src 'self'${frame.length ? ' ' + frame.join(' ') : ''}`,\n \"object-src 'none'\",\n \"base-uri 'none'\",\n \"form-action 'none'\",\n ]\n if (mode === 'strict') base.splice(2, 0, \"style-src-attr 'none'\")\n return base.join('; ')\n}\n"],"mappings":";;;AAOA,SAAS,uBAAuB,eAAoC;AAClE,KAAI,CAAC,MAAM,QAAQ,kBAAkB,cAAc,WAAW,EAAG,QAAO;CACxE,MAAMA,MAAgB;CACtB,MAAM,uBAAO,IAAI;AACjB,MAAK,MAAM,UAAU,eAAe;EAClC,MAAM,aAAaC,uCAAoB;AACvC,MAAI,CAAC,cAAc,KAAK,IAAI,YAAa;AACzC,OAAK,IAAI;AACT,MAAI,KAAK;;AAEX,QAAO;;AAGT,SAAgB,uBAAuB,eAAkC;CACvE,MAAM,UAAU,uBAAuB;CACvC,MAAM,aAAa,QAAQ,SAAS,MAAM,QAAQ,KAAK,MAAM,IAAI,EAAE,IAAI,KAAK,OAAO;CACnF,MAAM,QAAQ,SAAiB,GAAG,KAAK,QAAQ,WAAW;AAC1D,QAAO;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;GACL,KAAK;;;;;;;;;;;;;;;;AAiBT,SAAgB,eAAe,OAK3B,IAAY;CACd,MAAMC,OAAgB,KAAK,QAAQ;CACnC,MAAM,SAAS,KAAK,YAAY,IAAI,OAAO;CAC3C,MAAM,eAAe,KAAK,sBAAsB,IAC7C,KAAK,MAAMD,uCAAoB,MAAM,GACrC,OAAO;CACV,MAAM,qBAAqB,SAAS,eAAe,qBAAqB;CACxE,MAAM,oBAAoB,SAAS,eAAe,qBAAqB;CACvE,MAAM,mBAAmB,KAAK,kBAAkB,mBAAmB;CACnE,MAAME,OAAiB;EACrB;EACA,oBAAoB,qBAAqB,mBAAmB,YAAY,SAAS,MAAM,YAAY,KAAK,OAAO;EAC/G,mBAAmB;EACnB;EACA;EACA;EACA;EACA,mBAAmB,MAAM,SAAS,MAAM,MAAM,KAAK,OAAO;EAC1D;EACA;EACA;;AAEF,KAAI,SAAS,SAAU,MAAK,OAAO,GAAG,GAAG;AACzC,QAAO,KAAK,KAAK"}
|
package/dist/cjs/plugins/next.js
CHANGED
|
@@ -54,9 +54,22 @@ function toOriginOrUndefined(input) {
|
|
|
54
54
|
|
|
55
55
|
//#endregion
|
|
56
56
|
//#region src/plugins/headers.ts
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
const
|
|
57
|
+
function normalizeWalletOrigins(walletOrigins) {
|
|
58
|
+
if (!Array.isArray(walletOrigins) || walletOrigins.length === 0) return [];
|
|
59
|
+
const out = [];
|
|
60
|
+
const seen = /* @__PURE__ */ new Set();
|
|
61
|
+
for (const origin of walletOrigins) {
|
|
62
|
+
const normalized = toOriginOrUndefined(origin);
|
|
63
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
64
|
+
seen.add(normalized);
|
|
65
|
+
out.push(normalized);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function buildPermissionsPolicy(walletOrigins) {
|
|
70
|
+
const origins = normalizeWalletOrigins(walletOrigins);
|
|
71
|
+
const originPart = origins.length ? " " + origins.map((o) => `"${o}"`).join(" ") : "";
|
|
72
|
+
const part = (name) => `${name}=(self${originPart})`;
|
|
60
73
|
return [
|
|
61
74
|
part("publickey-credentials-get"),
|
|
62
75
|
part("publickey-credentials-create"),
|
|
@@ -378,13 +391,13 @@ function emitOfflineExportAssets(opts) {
|
|
|
378
391
|
//#endregion
|
|
379
392
|
//#region src/plugins/next.ts
|
|
380
393
|
function tatchiNextHeaders(opts) {
|
|
381
|
-
const
|
|
382
|
-
const permissions = buildPermissionsPolicy(
|
|
394
|
+
const walletOrigins = (opts.walletOrigins || []).filter(Boolean);
|
|
395
|
+
const permissions = buildPermissionsPolicy(walletOrigins);
|
|
383
396
|
const isDev = process.env.NODE_ENV !== "production";
|
|
384
397
|
const mode = opts.cspMode ?? (isDev && (opts.compatibleInDev ?? true) ? "compatible" : "strict");
|
|
385
398
|
const allowUnsafeEval = isDev && (opts.allowUnsafeEvalDev ?? true);
|
|
386
399
|
const csp = buildWalletCsp({
|
|
387
|
-
frameSrc: [
|
|
400
|
+
frameSrc: [...walletOrigins, ...opts.extraFrameSrc || []],
|
|
388
401
|
scriptSrcAllowlist: [...opts.extraScriptSrc || []],
|
|
389
402
|
mode,
|
|
390
403
|
allowUnsafeEval
|
|
@@ -419,7 +432,7 @@ function tatchiNextApp(opts) {
|
|
|
419
432
|
};
|
|
420
433
|
}
|
|
421
434
|
/**
|
|
422
|
-
* Convenience wrapper for Next.js wallet
|
|
435
|
+
* Convenience wrapper for Next.js wallet origins.
|
|
423
436
|
* Same behavior as tatchiNextApp — Next.js does not serve the SDK/wallet HTML; this
|
|
424
437
|
* helper only sets headers via headers() so the wallet host can be prepped if you
|
|
425
438
|
* proxy wallet routes through Next in dev.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next.js","names":["mode: CspMode","base: string[]","bytes: unknown","parsed: unknown","path","fs","list: string[]","path","fs","m: RegExpExecArray | null","mode: CspMode","origins: string[]","path"],"sources":["../../../src/utils/validation.ts","../../../src/plugins/headers.ts","../../../src/plugins/plugin-utils.ts","../../../src/plugins/offline.ts","../../../src/plugins/next.ts"],"sourcesContent":["\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n// ==============================\n// Normalization helpers (shared)\n// ==============================\n\n/** Strict string coercion: returns the value only when it's already a string. */\nexport function toOptionalString(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\n/** Strict string coercion + trimming. */\nexport function toOptionalTrimmedString(value: unknown): string {\n return toOptionalString(value).trim();\n}\n\n/** String coercion + trimming (useful at IO boundaries). */\nexport function toTrimmedString(value: unknown): string {\n return String(value ?? '').trim();\n}\n\n/** Remove trailing `/` characters (e.g. base URL normalization). */\nexport function stripTrailingSlashes(value: string): string {\n return String(value ?? '').replace(/\\/+$/, '');\n}\n\n/** Ensure a non-empty string starts with `/` (path normalization). */\nexport function ensureLeadingSlash(value: string): string {\n const trimmed = String(value ?? '').trim();\n if (!trimmed) return '';\n return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;\n}\n\n/** Normalize an app base path like `/sdk` (leading slash, no trailing slashes except `/`). */\nexport function toBasePath(value?: string, fallback = '/sdk'): string {\n const base = ensureLeadingSlash(typeof value === 'string' ? value : fallback) || ensureLeadingSlash(fallback) || '/';\n if (base === '/') return '/';\n return base.replace(/\\/+$/, '');\n}\n\n/** Best-effort origin normalization (used by CSP/Permissions-Policy helpers). */\nexport function toOriginOrUndefined(input?: string): string | undefined {\n try {\n const v = (input || '').trim();\n if (!v) return undefined;\n // Next/Caddy/etc. expect an origin, not a path\n return new URL(v, 'http://dummy').origin === 'http://dummy' ? new URL(v).origin : v;\n } catch {\n return input?.trim() || undefined;\n }\n}\n\n/**\n * Strict origin sanitizer for Related Origin Requests (ROR).\n * - Allows `https://<host>[:port]` and `http://localhost[:port]` only.\n * - Rejects paths (except `/`), queries, and hashes.\n * - Normalizes hostname casing.\n */\nexport function toRorOriginOrNull(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const raw = toTrimmedString(value);\n if (!raw) return null;\n try {\n const u = new URL(raw);\n const scheme = u.protocol;\n const host = u.hostname.toLowerCase();\n const port = u.port ? `:${u.port}` : '';\n const isHttps = scheme === 'https:';\n const isLocalhostHttp = scheme === 'http:' && host === 'localhost';\n if (!isHttps && !isLocalhostHttp) return null;\n if ((u.pathname && u.pathname !== '/') || u.search || u.hash) return null;\n return `${scheme}//${host}${port}`;\n } catch {\n return null;\n }\n}\n\n/** Collapse a string into a single line by normalizing whitespace. */\nexport function toSingleLine(value: unknown): string {\n return String(value ?? '')\n .replace(/[\\r\\n]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n/**\n * Ensure a key string has the NEAR Ed25519 prefix (`ed25519:`).\n *\n * - Accepts either `ed25519:<base58>` or a bare `<base58>` string.\n * - Canonicalizes `ED25519:` → `ed25519:`.\n * - If a different prefix is present (e.g. `secp256k1:`), returns the input unchanged.\n */\nexport function ensureEd25519Prefix(value: string): string {\n const raw = String(value ?? '').trim();\n if (!raw) return '';\n\n if (/^[a-z0-9_]+:/i.test(raw)) {\n if (/^ed25519:/i.test(raw)) {\n return `ed25519:${raw.replace(/^ed25519:/i, '')}`;\n }\n return raw;\n }\n\n return `ed25519:${raw}`;\n}\n\nexport interface NearAccountValidationOptions {\n /** Restrict to specific suffixes (e.g., ['testnet', 'near']) */\n allowedSuffixes?: string[];\n /** Require Top-level domains with exactly 2 parts (username.suffix) instead of allowing subdomains */\n requireTopLevelDomain?: boolean;\n}\n\n/**\n * Validate NEAR account ID format with optional suffix restrictions\n * @param nearAccountId - The account ID to validate\n * @param options - Optional validation constraints\n */\nexport function validateNearAccountId(\n nearAccountId: string,\n options: NearAccountValidationOptions = {\n allowedSuffixes: ['testnet', 'near'],\n requireTopLevelDomain: false\n }\n): ValidationResult {\n if (!nearAccountId || typeof nearAccountId !== 'string') {\n return { valid: false, error: 'Account ID must be a non-empty string' };\n }\n\n const parts = nearAccountId.split('.');\n if (parts.length < 2) {\n return { valid: false, error: 'Account ID must contain at least one dot (e.g., username.testnet)' };\n }\n\n // Check for exact two parts requirement (e.g., server registration)\n if (options.requireTopLevelDomain && parts.length !== 2) {\n const suffixList = options.allowedSuffixes?.join(', ') || 'valid suffixes';\n return {\n valid: false,\n error: `Invalid NEAR account ID format. Expected format: <username>.<suffix> where suffix is one of: ${suffixList}`\n };\n }\n\n const username = parts[0];\n const suffix = parts[parts.length - 1]; // Last part for suffix checking\n const domain = parts.slice(1).join('.');\n\n // Validate username part\n if (!username || username.length === 0) {\n return { valid: false, error: 'Username part cannot be empty' };\n }\n\n if (!/^[a-z0-9_\\-]+$/.test(username)) {\n return { valid: false, error: 'Username can only contain lowercase letters, numbers, underscores, and hyphens' };\n }\n\n // Validate domain part\n if (!domain || domain.length === 0) {\n return { valid: false, error: 'Domain part cannot be empty' };\n }\n\n // Check allowed suffixes if specified\n if (options.allowedSuffixes && options.allowedSuffixes.length > 0) {\n // Check if the account ID ends with any of the allowed suffixes\n const matchesAnySuffix = options.allowedSuffixes.some(allowedSuffix => {\n // For single-part suffixes, check the last part\n if (!allowedSuffix.includes('.')) {\n return suffix === allowedSuffix;\n }\n // For multi-part suffixes, check if the account ID ends with the full suffix\n return nearAccountId.endsWith(`.${allowedSuffix}`);\n });\n\n if (!matchesAnySuffix) {\n return {\n valid: false,\n error: `Invalid NEAR account ID suffix. Expected account to end with one of: ${options.allowedSuffixes.join(', ')}`\n };\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Lightweight NEAR account ID validation used by server-side helpers.\n * - length: 2..64\n * - chars: lowercase letters, digits, `_`, `.`, `-`\n */\nexport function isValidAccountId(accountId: unknown): accountId is string {\n if (typeof accountId !== 'string') return false;\n if (!accountId || accountId.length < 2 || accountId.length > 64) return false;\n return /^[a-z0-9_.-]+$/.test(accountId);\n}\n\n// ===========================\n// Runtime validation helpers\n// ===========================\n\nexport function isObject(x: unknown): x is Record<string, unknown> {\n return x !== null && typeof x === 'object';\n}\n\nexport function isString(x: unknown): x is string {\n return typeof x === 'string';\n}\n\nexport function isNonEmptyString(x: unknown): x is string {\n return typeof x === 'string' && x.length > 0;\n}\n\nexport function isNumber(x: unknown): x is number {\n return typeof x === 'number';\n}\n\nexport function isFiniteNumber(x: unknown): x is number {\n return typeof x === 'number' && Number.isFinite(x);\n}\n\nexport function isFunction(x: unknown): x is Function {\n return typeof x === 'function';\n}\n\nexport function isBoolean(x: unknown): x is boolean {\n return typeof x === 'boolean';\n}\n\nexport function isArray<T = unknown>(x: unknown): x is T[] {\n return Array.isArray(x);\n}\n\nexport function assertString(val: unknown, name = 'value'): string {\n if (typeof val !== 'string') throw new Error(`Invalid ${name}: expected string`);\n return val;\n}\n\nexport function assertNumber(val: unknown, name = 'value'): number {\n if (typeof val !== 'number' || !Number.isFinite(val)) throw new Error(`Invalid ${name}: expected finite number`);\n return val;\n}\n\nexport function assertBoolean(val: unknown, name = 'value'): boolean {\n if (typeof val !== 'boolean') throw new Error(`Invalid ${name}: expected boolean`);\n return val;\n}\n\nexport function assertObject<T extends Record<string, unknown> = Record<string, unknown>>(val: unknown, name = 'value'): T {\n if (!isObject(val)) throw new Error(`Invalid ${name}: expected object`);\n return val as T;\n}\n\nexport function assertArray<T = unknown>(val: unknown, name = 'value'): T[] {\n if (!Array.isArray(val)) throw new Error(`Invalid ${name}: expected array`);\n return val as T[];\n}\n\nexport function stripFunctionsShallow<T extends Record<string, unknown>>(obj?: T): Partial<T> | undefined {\n if (!obj || !isObject(obj)) return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!isFunction(v)) out[k] = v as unknown;\n }\n return out as Partial<T>;\n}\n\nexport interface PlainSignedTransactionLike {\n transaction: unknown;\n signature: unknown;\n borsh_bytes?: unknown;\n borshBytes?: unknown;\n base64Encode?: unknown;\n}\n\nexport function isPlainSignedTransactionLike(x: unknown): x is PlainSignedTransactionLike {\n if (!isObject(x)) return false;\n const hasTx = 'transaction' in x;\n const hasSig = 'signature' in x;\n const bytes = x as { borsh_bytes?: unknown; borshBytes?: unknown };\n const hasBytes = Array.isArray(bytes.borsh_bytes) || bytes.borshBytes instanceof Uint8Array;\n const hasMethod = typeof (x as { base64Encode?: unknown }).base64Encode === 'function';\n return hasTx && hasSig && hasBytes && !hasMethod;\n}\n\nexport function extractBorshBytesFromPlainSignedTx(x: PlainSignedTransactionLike): number[] {\n const asArray = Array.isArray(x.borsh_bytes) ? (x.borsh_bytes as number[]) : undefined;\n if (asArray) return asArray;\n const asU8 = (x.borshBytes instanceof Uint8Array) ? x.borshBytes : undefined;\n return Array.from(asU8 || new Uint8Array());\n}\n","// Framework-agnostic header helpers to minimize configuration surface.\n// Keep types local to avoid coupling to framework packages.\n\nimport { toOriginOrUndefined } from '../utils/validation'\n\nexport type CspMode = 'strict' | 'compatible'\n\nexport function buildPermissionsPolicy(walletOrigin?: string): string {\n const o = toOriginOrUndefined(walletOrigin)\n const part = (name: string) => `${name}=(self${o ? ` \"${o}\"` : ''})`\n return [\n part('publickey-credentials-get'),\n part('publickey-credentials-create'),\n part('clipboard-read'),\n part('clipboard-write'),\n ].join(', ')\n}\n\n/**\n * Build a wallet-friendly Content Security Policy string.\n *\n * mode:\n * - 'strict' (default): no inline styles, injects \"style-src-attr 'none'\", and forbids 'unsafe-eval'.\n * - 'compatible': allows inline styles/scripts via 'unsafe-inline' for friendlier dev/local setups.\n *\n * allowUnsafeEval:\n * - false by default. Set to true only for development servers that require eval (e.g., Next.js Fast Refresh).\n * - Tatchi SDK does not require 'unsafe-eval' in production.\n *\n * Typical usage: apply strict CSP only to wallet HTML routes\n * (/wallet-service, /export-viewer); do not attach CSP to host app routes.\n */\nexport function buildWalletCsp(opts: {\n frameSrc?: string[]\n mode?: CspMode\n allowUnsafeEval?: boolean\n scriptSrcAllowlist?: string[]\n} = {}): string {\n const mode: CspMode = opts.mode || 'strict'\n const frame = (opts.frameSrc || []).filter(Boolean)\n const scriptAllow = (opts.scriptSrcAllowlist || [])\n .map((s) => toOriginOrUndefined(s) || s)\n .filter(Boolean) as string[]\n const scriptUnsafeInline = mode === 'compatible' ? \" 'unsafe-inline'\" : ''\n const styleUnsafeInline = mode === 'compatible' ? \" 'unsafe-inline'\" : ''\n const scriptUnsafeEval = opts.allowUnsafeEval ? \" 'unsafe-eval'\" : ''\n const base: string[] = [\n \"default-src 'self'\",\n `script-src 'self'${scriptUnsafeInline}${scriptUnsafeEval}${scriptAllow.length ? ' ' + scriptAllow.join(' ') : ''}`,\n `style-src 'self'${styleUnsafeInline}`,\n \"img-src 'self' data:\",\n \"font-src 'self'\",\n \"connect-src 'self' https:\",\n \"worker-src 'self' blob:\",\n `frame-src 'self'${frame.length ? ' ' + frame.join(' ') : ''}`,\n \"object-src 'none'\",\n \"base-uri 'none'\",\n \"form-action 'none'\",\n ]\n if (mode === 'strict') base.splice(2, 0, \"style-src-attr 'none'\")\n return base.join('; ')\n}\n","// Small shared helpers for Vite/Next plugins\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { createRequire } from 'node:module'\n\nexport function addPreconnectLink(res: any, origin?: string) {\n if (!origin) return\n try {\n const link = `<${origin}>; rel=preconnect; crossorigin`\n const existing = res.getHeader?.('Link')\n if (!existing) {\n res.setHeader?.('Link', link)\n return\n }\n if (typeof existing === 'string') {\n if (!existing.includes(link)) res.setHeader?.('Link', existing + ', ' + link)\n return\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(link)) res.setHeader?.('Link', [...existing, link])\n }\n } catch {}\n}\n\n// Builds wallet service HTML that links only external CSS/JS (no inline),\n// so strict CSP (style-src 'self'; style-src-attr 'none') works in dev/prod.\nexport function buildWalletServiceHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Web3Authn Wallet Service</title>\n <!-- Surface styles are external so strict CSP can keep style-src 'self' -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\" />\n <!-- Prefetch component styles so they are warmed without triggering preload warnings -->\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/halo-border.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/passkey-halo-loading.css\" />\n <!-- Component theme CSS: shared tokens + component-scoped tokens -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\" />\n <!-- Minimal shims some ESM bundles expect (externalized to enable strict CSP) -->\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <!-- Hint the browser to fetch the host script earlier -->\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/wallet-iframe-host.js\" crossorigin>\n </head>\n <body>\n <!-- sdkBasePath points to the SDK root (e.g. '/sdk'). Load the host directly. -->\n <script type=\"module\" src=\"${sdkBasePath}/wallet-iframe-host.js\"></script>\n </body>\n</html>`\n}\n\n// Export viewer HTML is also fully externalized (no inline) to keep CSP strict.\nexport function buildExportViewerHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\">\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin>\n </head>\n <body>\n <w3a-drawer id=\"exp\" theme=\"dark\"></w3a-drawer>\n <script type=\"module\" src=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin></script>\n <script type=\"module\" src=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin></script>\n </body>\n</html>`\n}\n\nexport function resolveCoepMode(explicit?: 'strict' | 'off'): 'strict' | 'off' {\n if (explicit === 'strict' || explicit === 'off') return explicit\n const raw = String((globalThis as any)?.process?.env?.VITE_COEP_MODE || '').trim().toLowerCase()\n if (raw === 'strict' || raw === 'on' || raw === '1' || raw === 'require-corp') return 'strict'\n if (raw === 'off' || raw === '0' || raw === 'false') return 'off'\n return 'off'\n}\n\nexport function applyCoepCorp(res: any) {\n res.setHeader?.('Cross-Origin-Embedder-Policy', 'require-corp')\n res.setHeader?.('Cross-Origin-Resource-Policy', 'cross-origin')\n}\n\nexport function applyCoepCorpIfNeeded(res: any, coepMode?: 'strict' | 'off') {\n if (resolveCoepMode(coepMode) !== 'off') applyCoepCorp(res)\n}\n\nexport function echoCorsFromRequest(\n res: any,\n req: any,\n opts: {\n honorExistingAcaOrigin?: boolean\n allowCredentialsWhenExplicit?: boolean\n methods?: string\n headers?: string\n handlePreflight?: boolean\n } = {}\n) {\n const honorExisting = opts.honorExistingAcaOrigin === true\n const allowCreds = opts.allowCredentialsWhenExplicit !== false\n const methods = opts.methods || 'GET,OPTIONS'\n const headers = opts.headers || 'Content-Type,Authorization'\n const handlePreflight = opts.handlePreflight === true\n\n const origin = (req?.headers && (req.headers.origin as string)) || '*'\n const hasExisting = typeof res.getHeader === 'function' && !!res.getHeader('Access-Control-Allow-Origin')\n if (!honorExisting || !hasExisting) {\n res.setHeader?.('Access-Control-Allow-Origin', origin)\n }\n res.setHeader?.('Vary', 'Origin')\n res.setHeader?.('Access-Control-Allow-Methods', methods)\n res.setHeader?.('Access-Control-Allow-Headers', headers)\n if (origin !== '*' && allowCreds) res.setHeader?.('Access-Control-Allow-Credentials', 'true')\n if (handlePreflight) {\n const method = req?.method && String(req.method).toUpperCase()\n if (method === 'OPTIONS') {\n res.statusCode = 204\n res.end?.()\n return true\n }\n }\n return false\n}\n\n/**\n * Log and validate Related Origin Requests (ROR) configuration.\n * - Prints the well-known endpoint and the configured origins list.\n * - Warns if any origins are not absolute (e.g., missing protocol/hostname).\n */\nexport function logRorConfig(origins: string[], endpoint = '/.well-known/webauthn') {\n if (!Array.isArray(origins) || origins.length === 0) return\n const invalid: string[] = []\n for (const o of origins) {\n try {\n const u = new URL(o)\n if (!u.protocol || !u.hostname) invalid.push(o)\n } catch {\n invalid.push(o)\n }\n }\n const msg = `[tatchi] ROR enabled: GET ${endpoint} -> { origins: [${origins.join(', ')}] }`\n console.log(msg)\n if (invalid.length > 0) {\n console.warn(\n `[tatchi] ROR warning: invalid origins: ${invalid.join(\n ', '\n )} (expected absolute origins like https://app.example.com)`\n )\n }\n}\n\n// Sanitize a dynamic allowlist into a normalized set of absolute origins.\nexport function sanitizeOrigins(values: unknown): string[] {\n const out = new Set<string>()\n if (Array.isArray(values)) {\n for (const v of values) {\n if (typeof v !== 'string') continue\n try {\n const u = new URL(v.trim())\n const scheme = u.protocol\n const host = u.hostname.toLowerCase()\n const port = u.port ? `:${u.port}` : ''\n const isHttps = scheme === 'https:'\n const isLocalhostHttp = scheme === 'http:' && host === 'localhost'\n if (!isHttps && !isLocalhostHttp) continue\n if ((u.pathname && u.pathname !== '/') || u.search || u.hash) continue\n out.add(`${scheme}//${host}${port}`)\n } catch {}\n }\n }\n return Array.from(out)\n}\n\n/**\n * Fetch the wallet ROR allowlist from NEAR RPC and return sanitized origins.\n * Throws on transport/HTTP errors; caller may respond with an empty list on failure.\n */\nconst __rorCache = new Map<string, { origins: string[]; expiresAt: number }>()\nconst __rorInflight = new Map<string, Promise<string[]>>()\n\nexport async function fetchRorOriginsFromNear(opts: {\n rpcUrl: string\n contractId: string\n method: string\n cacheTtlMs?: number\n}): Promise<string[]> {\n const { rpcUrl, contractId } = opts\n const method = opts.method || 'get_allowed_origins'\n const ttl = Math.max(0, Number(opts.cacheTtlMs ?? (process.env.VITE_ROR_CACHE_TTL_MS as any) ?? 60000)) || 60000\n const key = `${rpcUrl}|${contractId}|${method}`\n\n const now = Date.now()\n const cached = __rorCache.get(key)\n if (cached && now < cached.expiresAt) {\n return cached.origins\n }\n\n if (!__rorInflight.has(key)) {\n const body = JSON.stringify({\n jsonrpc: '2.0',\n id: String(Date.now()),\n method: 'query',\n params: {\n request_type: 'call_function',\n finality: 'final',\n account_id: contractId,\n method_name: method,\n args_base64: Buffer.from(JSON.stringify({})).toString('base64'),\n },\n })\n\n const p = (async () => {\n const resp = await fetch(rpcUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!resp.ok) {\n throw new Error(`RPC ${resp.status} ${resp.statusText}`)\n }\n const jsonResponse = await resp.json()\n const bytes: unknown = jsonResponse?.result?.result\n let parsed: unknown = []\n if (Array.isArray(bytes)) {\n const str = String.fromCharCode(...(bytes as number[]))\n try { parsed = JSON.parse(str) } catch { parsed = [] }\n }\n const origins = sanitizeOrigins(parsed as unknown[])\n __rorCache.set(key, { origins, expiresAt: Date.now() + ttl })\n return origins\n })()\n\n __rorInflight.set(key, p.finally(() => __rorInflight.delete(key)))\n }\n\n return __rorInflight.get(key) as Promise<string[]>\n}\n\n/**\n * Infer and set a proper Content-Type header for a given file path.\n * Shared by both app and wallet-iframe dev servers.\n */\nexport function setContentType(res: any, filePath: string) {\n const ext = path.extname(filePath)\n switch (ext) {\n case '.js':\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n break\n case '.css':\n res.setHeader('Content-Type', 'text/css; charset=utf-8')\n break\n case '.map':\n case '.json':\n res.setHeader('Content-Type', 'application/json; charset=utf-8')\n break\n case '.wasm':\n res.setHeader('Content-Type', 'application/wasm')\n break\n case '.html':\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n break\n default:\n res.setHeader('Content-Type', 'application/octet-stream')\n }\n}\n\n// === Shared path helpers across Vite/Next plugins ===\n\nexport { toBasePath } from '../utils/validation'\n\nconst requireCjs = createRequire(import.meta.url)\n\nexport function resolveSdkDistRoot(explicit?: string): string {\n if (explicit) return path.resolve(explicit)\n const pkgPath = requireCjs.resolve('@tatchi-xyz/sdk/package.json')\n const pkgDir = path.dirname(pkgPath)\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { module?: string }\n const esmEntry = pkgJson.module || 'dist/esm/index.js'\n const esmAbs = path.resolve(pkgDir, esmEntry)\n return path.resolve(path.dirname(esmAbs), '..')\n } catch {\n return path.join(pkgDir, 'dist')\n }\n}\n","import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { applyCoepCorpIfNeeded, resolveCoepMode, setContentType } from './plugin-utils'\n\n// Offline export route HTML: fully externalized (no inline) so strict CSP works.\nexport function buildOfflineExportHtml(sdkBasePath: string): string {\n const rpIdBase = (process?.env?.VITE_RP_ID_BASE || '').toString().trim();\n const rpIdMeta = rpIdBase ? `\\n <meta name=\"tatchi-rpid-base\" content=\"${rpIdBase}\">` : '';\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n <title>Emergency Export (Offline)</title>\n <link rel=\"manifest\" href=\"/offline-export/manifest.webmanifest\">${rpIdMeta}\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/offline-export.css\">\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <link rel=\"modulepreload\" href=\"/offline-export/offline-export-app.js\" crossorigin>\n </head>\n <body>\n <script type=\"module\" src=\"/offline-export/offline-export-app.js\" crossorigin></script>\n </body>\n </html>`\n}\n\n// Build a curated list of assets to precache for the offline-export route.\n// - Includes the route HTML + manifest\n// - Includes the SDK CSS/JS used by the route\n// - Includes worker JS + WASM\n// - Also scans offline-export-app.js for direct ESM imports (hashed files) in the SDK dist\nexport function computeOfflinePrecacheList(sdkBasePath: string, sdkDistRoot: string): string[] {\n const list: string[] = []\n const add = (p?: string) => {\n if (!p) return\n if (!list.includes(p)) list.push(p)\n }\n // Route assets\n add('/offline-export/index.html')\n add('/offline-export/manifest.webmanifest')\n // Offline-scoped app entry so SW-controlled loads work on offline refresh\n add('/offline-export/offline-export-app.js')\n // SDK surface/css/js\n add(`${sdkBasePath}/wallet-service.css`)\n add(`${sdkBasePath}/w3a-components.css`)\n add(`${sdkBasePath}/drawer.css`)\n add(`${sdkBasePath}/tx-tree.css`)\n add(`${sdkBasePath}/tx-confirmer.css`)\n add(`${sdkBasePath}/offline-export.css`)\n // Export viewer CSS used by offline export drawer flow\n add(`${sdkBasePath}/export-viewer.css`)\n add(`${sdkBasePath}/export-iframe.css`)\n add(`${sdkBasePath}/wallet-shims.js`)\n add(`${sdkBasePath}/offline-export-app.js`)\n // Workers and WASM\n add(`${sdkBasePath}/workers/web3authn-signer.worker.js`)\n add(`${sdkBasePath}/workers/web3authn-vrf.worker.js`)\n add(`${sdkBasePath}/workers/wasm_signer_worker_bg.wasm`)\n add(`${sdkBasePath}/workers/wasm_vrf_worker_bg.wasm`)\n\n // Scan offline-export-app.js for sibling ESM imports and include them\n try {\n const entry = path.join(sdkDistRoot, 'esm', 'sdk', 'offline-export-app.js')\n if (fs.existsSync(entry)) {\n const src = fs.readFileSync(entry, 'utf-8')\n const re = /(?:import\\(|from)\\s*[\"']\\.\\/([^\"']+\\.js)[\"']/g\n let m: RegExpExecArray | null\n while ((m = re.exec(src))) {\n const file = m[1]\n if (file && !file.includes('..')) add(`${sdkBasePath}/${file.replace(/^\\.\\//, '')}`)\n }\n }\n } catch {}\n\n // Include all JS chunks in esm/sdk (covers dynamic imports such as localOnly-*.js, transactions-*.js, etc.)\n try {\n const sdkDir = path.join(sdkDistRoot, 'esm', 'sdk')\n const files = fs.readdirSync(sdkDir, { withFileTypes: true })\n for (const f of files) {\n if (!f.isFile()) continue\n if (f.name.endsWith('.js')) add(`${sdkBasePath}/${f.name}`)\n }\n } catch {}\n\n return list\n}\n\n\nexport function addOfflineExportDevRoutes(\n server: any,\n opts: {\n sdkDistRoot: string\n sdkBasePath: string\n offlineHtml: string\n includeAppModule?: boolean\n coepMode?: 'strict' | 'off'\n }\n) {\n const { sdkDistRoot, sdkBasePath, offlineHtml, includeAppModule } = opts\n const coepMode = resolveCoepMode(opts.coepMode)\n\n // Route: /offline-export (HTML)\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export' && url !== '/offline-export/') return next()\n res.statusCode = 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n // HTML should never be cached; keeps SW updates and app changes visible\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n res.end(offlineHtml)\n })\n\n // Route: /offline-export/manifest.webmanifest\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/manifest.webmanifest') return next()\n const manifest = {\n name: 'Emergency Export',\n short_name: 'Export',\n start_url: '/offline-export/',\n scope: '/offline-export/',\n display: 'standalone',\n background_color: '#0b0b0c',\n theme_color: '#0b0b0c',\n icons: [] as any[],\n }\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/manifest+json; charset=utf-8')\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n res.end(JSON.stringify(manifest))\n })\n\n // Route: /offline-export/sw.js\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/sw.js') return next()\n try {\n const filePath = path.join(sdkDistRoot, 'workers', 'offline-export-sw.js')\n if (!fs.existsSync(filePath)) {\n res.statusCode = 404\n res.end('offline-export-sw.js not found in SDK dist')\n return\n }\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n // Ensure SW updates are fetched fresh during dev\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch (e) {\n res.statusCode = 500\n res.end('failed to serve offline SW')\n }\n })\n\n // Route: /offline-export/workers/* (map to SDK dist workers)\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (!url.startsWith('/offline-export/workers/')) return next()\n try {\n const rel = url.replace('/offline-export/workers/', '')\n const filePath = path.join(sdkDistRoot, 'workers', rel)\n if (!fs.existsSync(filePath)) {\n res.statusCode = 404\n res.end('worker asset not found')\n return\n }\n res.statusCode = 200\n setContentType(res, filePath)\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch (e) {\n res.statusCode = 500\n res.end('failed to serve offline-export worker asset')\n }\n })\n\n // Route: /offline-export/precache.manifest.json\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/precache.manifest.json') return next()\n try {\n const entries = computeOfflinePrecacheList(sdkBasePath, sdkDistRoot)\n ;[\n '/offline-export/workers/web3authn-signer.worker.js',\n '/offline-export/workers/web3authn-vrf.worker.js',\n '/offline-export/workers/wasm_signer_worker_bg.wasm',\n '/offline-export/workers/wasm_vrf_worker_bg.wasm',\n ].forEach((p) => {\n if (!entries.includes(p)) entries.push(p)\n })\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json; charset=utf-8')\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n res.end(JSON.stringify(entries))\n } catch (e) {\n res.statusCode = 500\n res.end('failed to build precache manifest')\n }\n })\n\n if (includeAppModule) {\n // Route: /offline-export/offline-export-app.js\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/offline-export-app.js') return next()\n try {\n const filePath = path.join(sdkDistRoot, 'esm', 'sdk', 'offline-export-app.js')\n if (!fs.existsSync(filePath)) {\n res.statusCode = 404\n res.end('offline-export-app.js not found in SDK dist')\n return\n }\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch (e) {\n res.statusCode = 500\n res.end('failed to serve offline-export-app.js')\n }\n })\n\n // Route: offline-export sibling ESM chunks\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n const isChunk = url.startsWith('/offline-export/') && url.endsWith('.js') && !url.startsWith('/offline-export/workers/')\n if (!isChunk) return next()\n try {\n const filename = url.replace('/offline-export/', '')\n const filePath = path.join(sdkDistRoot, 'esm', 'sdk', filename)\n if (!fs.existsSync(filePath)) return next()\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch {\n next()\n }\n })\n }\n}\n\n// ===== Build-time helpers =====\n\nfunction ensureDir(p: string): void {\n try { fs.mkdirSync(p, { recursive: true }) } catch {}\n}\n\nfunction writeTextIfMissing(filePath: string, content: string): void {\n if (fs.existsSync(filePath)) return\n ensureDir(path.dirname(filePath))\n fs.writeFileSync(filePath, content, 'utf-8')\n}\n\nfunction writeJsonIfMissing(filePath: string, obj: unknown): void {\n if (fs.existsSync(filePath)) return\n ensureDir(path.dirname(filePath))\n fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), 'utf-8')\n}\n\nfunction copyIfExists(src: string, dst: string): boolean {\n if (!fs.existsSync(src)) return false\n ensureDir(path.dirname(dst))\n fs.copyFileSync(src, dst)\n return true\n}\n\nfunction copyBatchIfExists(srcDir: string, files: string[], dstDir: string): void {\n ensureDir(dstDir)\n for (const f of files) {\n const src = path.join(srcDir, f)\n const dst = path.join(dstDir, f)\n if (fs.existsSync(src)) fs.copyFileSync(src, dst)\n }\n}\n\nexport function emitOfflineExportAssets(opts: { outDir: string; sdkBasePath: string; sdkDistRoot: string }): void {\n const { outDir, sdkBasePath, sdkDistRoot } = opts\n const offlineDir = path.join(outDir, 'offline-export')\n const workersDir = path.join(offlineDir, 'workers')\n\n let swCopied = false\n let workersCopied = false\n let appCopied = false\n let htmlEnsured = false\n let manifestEnsured = false\n let precacheEmitted = false\n\n // 1) Copy Service Worker\n try {\n const swSrc = path.join(sdkDistRoot, 'workers', 'offline-export-sw.js')\n const swDst = path.join(offlineDir, 'sw.js')\n swCopied = copyIfExists(swSrc, swDst)\n if (!swCopied) console.warn('[tatchi][offline] SW not found in SDK dist; skipping copy')\n } catch (e) {\n console.warn('[tatchi][offline] failed to copy SW:', e)\n }\n\n // 2) Copy workers + WASM\n try {\n const srcDir = path.join(sdkDistRoot, 'workers')\n copyBatchIfExists(srcDir, [\n 'web3authn-signer.worker.js',\n 'web3authn-vrf.worker.js',\n 'wasm_signer_worker_bg.wasm',\n 'wasm_vrf_worker_bg.wasm',\n ], workersDir)\n workersCopied = true\n } catch (e) {\n console.warn('[tatchi][offline] failed to copy workers:', e)\n }\n\n // 3) Copy offline-export app module\n try {\n const src = path.join(sdkDistRoot, 'esm', 'sdk', 'offline-export-app.js')\n const dst = path.join(offlineDir, 'offline-export-app.js')\n appCopied = copyIfExists(src, dst)\n if (!appCopied) console.warn('[tatchi][offline] app module not found in SDK dist; skipping copy')\n } catch (e) {\n console.warn('[tatchi][offline] failed to copy app module:', e)\n }\n\n // 4) Emit HTML + manifest if missing\n try {\n const offHtml = path.join(offlineDir, 'index.html')\n writeTextIfMissing(offHtml, buildOfflineExportHtml(sdkBasePath))\n htmlEnsured = fs.existsSync(offHtml)\n } catch (e) {\n console.warn('[tatchi][offline] failed to emit index.html:', e)\n }\n try {\n const manifestPath = path.join(offlineDir, 'manifest.webmanifest')\n writeJsonIfMissing(manifestPath, {\n name: 'Emergency Export',\n short_name: 'Export',\n start_url: '/offline-export/',\n scope: '/offline-export/',\n display: 'standalone',\n background_color: '#0b0b0c',\n theme_color: '#0b0b0c',\n icons: [] as any[],\n })\n manifestEnsured = fs.existsSync(manifestPath)\n } catch (e) {\n console.warn('[tatchi][offline] failed to emit manifest.webmanifest:', e)\n }\n\n // 5) Emit precache manifest\n try {\n const precachePath = path.join(offlineDir, 'precache.manifest.json');\n const entries = computeOfflinePrecacheList(sdkBasePath, sdkDistRoot);\n ;[\n '/offline-export/offline-export-app.js',\n '/offline-export/workers/web3authn-signer.worker.js',\n '/offline-export/workers/web3authn-vrf.worker.js',\n '/offline-export/workers/wasm_signer_worker_bg.wasm',\n '/offline-export/workers/wasm_vrf_worker_bg.wasm',\n ].forEach((p) => { if (!entries.includes(p)) entries.push(p) });\n ensureDir(offlineDir)\n fs.writeFileSync(precachePath, JSON.stringify(entries, null, 2), 'utf-8');\n precacheEmitted = true\n } catch (e) {\n console.warn('[tatchi][offline] failed to emit precache.manifest.json:', e)\n }\n\n // Consolidated summary\n console.log(\n `[tatchi][offline] emitted assets: sw=${swCopied ? 'ok' : 'skip'} workers=${workersCopied ? 'ok' : 'skip'} app=${appCopied ? 'ok' : 'skip'} html=${htmlEnsured ? 'ok' : 'skip'} manifest=${manifestEnsured ? 'ok' : 'skip'} precache=${precacheEmitted ? 'ok' : 'skip'}`\n )\n}\n\n// Re-export helpers so Offline surface stays co-located\nexport { }\n\nexport type { }\n","// Minimal Next.js helpers: compose a headers() entry for cross-origin wallet embedding.\n// Avoid importing Next types; keep shapes generic.\n//\n// CSP policy note:\n// - We RELAX CSP ONLY FOR NEXT DEV to accommodate the framework's dev runtime (Fast Refresh/overlay),\n// which requires 'unsafe-eval' and inline styles. This relaxation is not required by the Tatchi SDK itself.\n// - In PRODUCTION you should keep a strict CSP (no 'unsafe-eval', no inline styles, and include \"style-src-attr 'none'\").\n\nimport { buildPermissionsPolicy, buildWalletCsp, type CspMode } from './headers'\nimport { fetchRorOriginsFromNear, resolveSdkDistRoot, toBasePath } from './plugin-utils'\nimport { emitOfflineExportAssets as emitOfflineAssetsCore } from './offline'\nimport * as path from 'node:path'\n\nexport type NextHeader = { key: string; value: string }\nexport type NextHeaderEntry = { source: string; headers: NextHeader[] }\n\nexport function tatchiNextHeaders(opts: {\n walletOrigin: string\n cspMode?: CspMode\n extraFrameSrc?: string[]\n /** Optional allowlist for script-src (e.g., wallet origin for modulepreload in dev) */\n extraScriptSrc?: string[]\n allowUnsafeEvalDev?: boolean\n compatibleInDev?: boolean\n}): NextHeaderEntry[] {\n const wallet = opts.walletOrigin\n const permissions = buildPermissionsPolicy(wallet)\n const isDev = process.env.NODE_ENV !== 'production'\n const mode: CspMode = opts.cspMode ?? (isDev && (opts.compatibleInDev ?? true) ? 'compatible' : 'strict')\n const allowUnsafeEval = isDev && (opts.allowUnsafeEvalDev ?? true)\n const csp = buildWalletCsp({\n frameSrc: [wallet, ...(opts.extraFrameSrc || [])],\n scriptSrcAllowlist: [...(opts.extraScriptSrc || [])],\n mode,\n allowUnsafeEval,\n })\n return [\n { source: '/:path*', headers: [\n { key: 'Permissions-Policy', value: permissions },\n { key: 'Content-Security-Policy', value: csp },\n ]}\n ]\n}\n\n/**\n * Convenience wrapper for Next.js app origin.\n * Adds Permissions-Policy and a wallet-friendly CSP via Next's headers() API.\n * emitHeaders has no effect for Next.js; kept for parity with Vite wrappers.\n */\nexport function tatchiNextApp(opts: {\n walletOrigin: string\n emitHeaders?: boolean\n cspMode?: CspMode\n extraFrameSrc?: string[]\n extraScriptSrc?: string[]\n allowUnsafeEvalDev?: boolean\n compatibleInDev?: boolean\n}) {\n if (opts.emitHeaders) {\n console.warn('[tatchi] tatchiNextApp: emitHeaders has no effect in Next.js; headers are applied via next.config.js headers().')\n }\n return (config: any) => {\n const existing = config?.headers\n return {\n ...config,\n async headers() {\n const user = typeof existing === 'function' ? await existing() : []\n return [...(user || []), ...tatchiNextHeaders(opts)]\n },\n }\n }\n}\n\n/**\n * Convenience wrapper for Next.js wallet origin.\n * Same behavior as tatchiNextApp — Next.js does not serve the SDK/wallet HTML; this\n * helper only sets headers via headers() so the wallet host can be prepped if you\n * proxy wallet routes through Next in dev.\n */\nexport function tatchiNextWallet(opts: {\n walletOrigin: string\n emitHeaders?: boolean\n cspMode?: CspMode\n extraFrameSrc?: string[]\n extraScriptSrc?: string[]\n allowUnsafeEvalDev?: boolean\n compatibleInDev?: boolean\n}) {\n if (opts.emitHeaders) {\n console.warn('[tatchi] tatchiNextWallet: emitHeaders has no effect in Next.js; headers are applied via next.config.js headers().')\n }\n return (config: any) => {\n const existing = config?.headers\n return {\n ...config,\n async headers() {\n const user = typeof existing === 'function' ? await existing() : []\n return [...(user || []), ...tatchiNextHeaders(opts)]\n },\n}\n }\n}\n\n// === Well-known (/.well-known/webauthn) helpers for Next.js ===\n// These helpers mirror the Vite dev server behavior and let Next apps expose\n// a dynamic allowlist fetched from chain without a relay in development.\n\ntype RorOpts = {\n rpcUrl?: string\n contractId?: string\n method?: string\n cacheTtlMs?: number\n}\n\nfunction resolveRorParams(opts: RorOpts) {\n const rpcUrl = (opts.rpcUrl || process.env.VITE_NEAR_RPC_URL || 'https://test.rpc.fastnear.com').toString().trim()\n const contractId = (opts.contractId || process.env.VITE_WEBAUTHN_CONTRACT_ID || '').toString().trim()\n const method = (opts.method || process.env.VITE_ROR_METHOD || 'get_allowed_origins').toString().trim()\n const cacheTtlMs = Number(opts.cacheTtlMs ?? process.env.VITE_ROR_CACHE_TTL_MS ?? 60000)\n return { rpcUrl, contractId, method, cacheTtlMs }\n}\n\n/**\n * Pages Router compatible handler (Node runtime).\n * Usage (pages/api/.well-known/webauthn.ts):\n * export default (req, res) => handleWellKnownRorNode(req, res)\n */\nexport async function handleWellKnownRorNode(req: any, res: any, opts: RorOpts = {}) {\n try {\n const params = resolveRorParams(opts)\n const origins = params.contractId\n ? await fetchRorOriginsFromNear(params)\n : []\n res.statusCode = 200\n res.setHeader?.('Content-Type', 'application/json; charset=utf-8')\n res.setHeader?.('Cache-Control', 'max-age=60, stale-while-revalidate=600')\n res.end?.(JSON.stringify({ origins }))\n } catch (e) {\n console.warn('[tatchi][next] ROR fetch failed:', e)\n res.statusCode = 200\n res.setHeader?.('Content-Type', 'application/json; charset=utf-8')\n res.setHeader?.('Cache-Control', 'max-age=60, stale-while-revalidate=600')\n res.end?.(JSON.stringify({ origins: [] }))\n }\n}\n\n/**\n * App Router compatible handler (Edge/Route Handler style).\n * Usage (app/.well-known/webauthn/route.ts):\n * export async function GET(req: Request) { return handleWellKnownRorEdge(req) }\n */\nexport async function handleWellKnownRorEdge(_request: Request, opts: RorOpts = {}): Promise<Response> {\n try {\n const params = resolveRorParams(opts)\n const origins = params.contractId\n ? await fetchRorOriginsFromNear(params)\n : []\n return new Response(JSON.stringify({ origins }), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Cache-Control': 'max-age=60, stale-while-revalidate=600',\n },\n })\n } catch (e) {\n console.warn('[tatchi][next] ROR fetch failed:', e)\n const origins: string[] = []\n return new Response(JSON.stringify({ origins }), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Cache-Control': 'max-age=60, stale-while-revalidate=600',\n },\n })\n }\n}\n\n// === Build-time helper: emit offline-export assets (Next.js parity) ===\n// Exported for parity with the Vite plugin helper. Can be invoked from a\n// custom Next build script or post-build step to copy SW/workers and emit\n// offline-export HTML, manifest, and precache manifest into your public dir.\n\nexport function nextEmitOfflineExportAssets(opts: { outDir: string; sdkBasePath?: string; sdkDistRoot?: string }): void {\n const outDir = path.resolve(opts.outDir)\n const sdkBasePath = toBasePath(opts.sdkBasePath, '/sdk')\n const sdkDistRoot = resolveSdkDistRoot(opts.sdkDistRoot)\n emitOfflineAssetsCore({ outDir, sdkBasePath, sdkDistRoot })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"next.js","names":["out: string[]","mode: CspMode","base: string[]","bytes: unknown","parsed: unknown","path","fs","list: string[]","path","fs","m: RegExpExecArray | null","mode: CspMode","origins: string[]","path"],"sources":["../../../src/utils/validation.ts","../../../src/plugins/headers.ts","../../../src/plugins/plugin-utils.ts","../../../src/plugins/offline.ts","../../../src/plugins/next.ts"],"sourcesContent":["\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n// ==============================\n// Normalization helpers (shared)\n// ==============================\n\n/** Strict string coercion: returns the value only when it's already a string. */\nexport function toOptionalString(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\n/** Strict string coercion + trimming. */\nexport function toOptionalTrimmedString(value: unknown): string {\n return toOptionalString(value).trim();\n}\n\n/** String coercion + trimming (useful at IO boundaries). */\nexport function toTrimmedString(value: unknown): string {\n return String(value ?? '').trim();\n}\n\n/** Remove trailing `/` characters (e.g. base URL normalization). */\nexport function stripTrailingSlashes(value: string): string {\n return String(value ?? '').replace(/\\/+$/, '');\n}\n\n/** Ensure a non-empty string starts with `/` (path normalization). */\nexport function ensureLeadingSlash(value: string): string {\n const trimmed = String(value ?? '').trim();\n if (!trimmed) return '';\n return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;\n}\n\n/** Normalize an app base path like `/sdk` (leading slash, no trailing slashes except `/`). */\nexport function toBasePath(value?: string, fallback = '/sdk'): string {\n const base = ensureLeadingSlash(typeof value === 'string' ? value : fallback) || ensureLeadingSlash(fallback) || '/';\n if (base === '/') return '/';\n return base.replace(/\\/+$/, '');\n}\n\n/** Best-effort origin normalization (used by CSP/Permissions-Policy helpers). */\nexport function toOriginOrUndefined(input?: string): string | undefined {\n try {\n const v = (input || '').trim();\n if (!v) return undefined;\n // Next/Caddy/etc. expect an origin, not a path\n return new URL(v, 'http://dummy').origin === 'http://dummy' ? new URL(v).origin : v;\n } catch {\n return input?.trim() || undefined;\n }\n}\n\n/**\n * Strict origin sanitizer for Related Origin Requests (ROR).\n * - Allows `https://<host>[:port]` and `http://localhost[:port]` only.\n * - Rejects paths (except `/`), queries, and hashes.\n * - Normalizes hostname casing.\n */\nexport function toRorOriginOrNull(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const raw = toTrimmedString(value);\n if (!raw) return null;\n try {\n const u = new URL(raw);\n const scheme = u.protocol;\n const host = u.hostname.toLowerCase();\n const port = u.port ? `:${u.port}` : '';\n const isHttps = scheme === 'https:';\n const isLocalhostHttp = scheme === 'http:' && host === 'localhost';\n if (!isHttps && !isLocalhostHttp) return null;\n if ((u.pathname && u.pathname !== '/') || u.search || u.hash) return null;\n return `${scheme}//${host}${port}`;\n } catch {\n return null;\n }\n}\n\n/** Collapse a string into a single line by normalizing whitespace. */\nexport function toSingleLine(value: unknown): string {\n return String(value ?? '')\n .replace(/[\\r\\n]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n/**\n * Ensure a key string has the NEAR Ed25519 prefix (`ed25519:`).\n *\n * - Accepts either `ed25519:<base58>` or a bare `<base58>` string.\n * - Canonicalizes `ED25519:` → `ed25519:`.\n * - If a different prefix is present (e.g. `secp256k1:`), returns the input unchanged.\n */\nexport function ensureEd25519Prefix(value: string): string {\n const raw = String(value ?? '').trim();\n if (!raw) return '';\n\n if (/^[a-z0-9_]+:/i.test(raw)) {\n if (/^ed25519:/i.test(raw)) {\n return `ed25519:${raw.replace(/^ed25519:/i, '')}`;\n }\n return raw;\n }\n\n return `ed25519:${raw}`;\n}\n\nexport interface NearAccountValidationOptions {\n /** Restrict to specific suffixes (e.g., ['testnet', 'near']) */\n allowedSuffixes?: string[];\n /** Require Top-level domains with exactly 2 parts (username.suffix) instead of allowing subdomains */\n requireTopLevelDomain?: boolean;\n}\n\n/**\n * Validate NEAR account ID format with optional suffix restrictions\n * @param nearAccountId - The account ID to validate\n * @param options - Optional validation constraints\n */\nexport function validateNearAccountId(\n nearAccountId: string,\n options: NearAccountValidationOptions = {\n allowedSuffixes: ['testnet', 'near'],\n requireTopLevelDomain: false\n }\n): ValidationResult {\n if (!nearAccountId || typeof nearAccountId !== 'string') {\n return { valid: false, error: 'Account ID must be a non-empty string' };\n }\n\n const parts = nearAccountId.split('.');\n if (parts.length < 2) {\n return { valid: false, error: 'Account ID must contain at least one dot (e.g., username.testnet)' };\n }\n\n // Check for exact two parts requirement (e.g., server registration)\n if (options.requireTopLevelDomain && parts.length !== 2) {\n const suffixList = options.allowedSuffixes?.join(', ') || 'valid suffixes';\n return {\n valid: false,\n error: `Invalid NEAR account ID format. Expected format: <username>.<suffix> where suffix is one of: ${suffixList}`\n };\n }\n\n const username = parts[0];\n const suffix = parts[parts.length - 1]; // Last part for suffix checking\n const domain = parts.slice(1).join('.');\n\n // Validate username part\n if (!username || username.length === 0) {\n return { valid: false, error: 'Username part cannot be empty' };\n }\n\n if (!/^[a-z0-9_\\-]+$/.test(username)) {\n return { valid: false, error: 'Username can only contain lowercase letters, numbers, underscores, and hyphens' };\n }\n\n // Validate domain part\n if (!domain || domain.length === 0) {\n return { valid: false, error: 'Domain part cannot be empty' };\n }\n\n // Check allowed suffixes if specified\n if (options.allowedSuffixes && options.allowedSuffixes.length > 0) {\n // Check if the account ID ends with any of the allowed suffixes\n const matchesAnySuffix = options.allowedSuffixes.some(allowedSuffix => {\n // For single-part suffixes, check the last part\n if (!allowedSuffix.includes('.')) {\n return suffix === allowedSuffix;\n }\n // For multi-part suffixes, check if the account ID ends with the full suffix\n return nearAccountId.endsWith(`.${allowedSuffix}`);\n });\n\n if (!matchesAnySuffix) {\n return {\n valid: false,\n error: `Invalid NEAR account ID suffix. Expected account to end with one of: ${options.allowedSuffixes.join(', ')}`\n };\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Lightweight NEAR account ID validation used by server-side helpers.\n * - length: 2..64\n * - chars: lowercase letters, digits, `_`, `.`, `-`\n */\nexport function isValidAccountId(accountId: unknown): accountId is string {\n if (typeof accountId !== 'string') return false;\n if (!accountId || accountId.length < 2 || accountId.length > 64) return false;\n return /^[a-z0-9_.-]+$/.test(accountId);\n}\n\n// ===========================\n// Runtime validation helpers\n// ===========================\n\nexport function isObject(x: unknown): x is Record<string, unknown> {\n return x !== null && typeof x === 'object';\n}\n\nexport function isString(x: unknown): x is string {\n return typeof x === 'string';\n}\n\nexport function isNonEmptyString(x: unknown): x is string {\n return typeof x === 'string' && x.length > 0;\n}\n\nexport function isNumber(x: unknown): x is number {\n return typeof x === 'number';\n}\n\nexport function isFiniteNumber(x: unknown): x is number {\n return typeof x === 'number' && Number.isFinite(x);\n}\n\nexport function isFunction(x: unknown): x is Function {\n return typeof x === 'function';\n}\n\nexport function isBoolean(x: unknown): x is boolean {\n return typeof x === 'boolean';\n}\n\nexport function isArray<T = unknown>(x: unknown): x is T[] {\n return Array.isArray(x);\n}\n\nexport function assertString(val: unknown, name = 'value'): string {\n if (typeof val !== 'string') throw new Error(`Invalid ${name}: expected string`);\n return val;\n}\n\nexport function assertNumber(val: unknown, name = 'value'): number {\n if (typeof val !== 'number' || !Number.isFinite(val)) throw new Error(`Invalid ${name}: expected finite number`);\n return val;\n}\n\nexport function assertBoolean(val: unknown, name = 'value'): boolean {\n if (typeof val !== 'boolean') throw new Error(`Invalid ${name}: expected boolean`);\n return val;\n}\n\nexport function assertObject<T extends Record<string, unknown> = Record<string, unknown>>(val: unknown, name = 'value'): T {\n if (!isObject(val)) throw new Error(`Invalid ${name}: expected object`);\n return val as T;\n}\n\nexport function assertArray<T = unknown>(val: unknown, name = 'value'): T[] {\n if (!Array.isArray(val)) throw new Error(`Invalid ${name}: expected array`);\n return val as T[];\n}\n\nexport function stripFunctionsShallow<T extends Record<string, unknown>>(obj?: T): Partial<T> | undefined {\n if (!obj || !isObject(obj)) return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!isFunction(v)) out[k] = v as unknown;\n }\n return out as Partial<T>;\n}\n\nexport interface PlainSignedTransactionLike {\n transaction: unknown;\n signature: unknown;\n borsh_bytes?: unknown;\n borshBytes?: unknown;\n base64Encode?: unknown;\n}\n\nexport function isPlainSignedTransactionLike(x: unknown): x is PlainSignedTransactionLike {\n if (!isObject(x)) return false;\n const hasTx = 'transaction' in x;\n const hasSig = 'signature' in x;\n const bytes = x as { borsh_bytes?: unknown; borshBytes?: unknown };\n const hasBytes = Array.isArray(bytes.borsh_bytes) || bytes.borshBytes instanceof Uint8Array;\n const hasMethod = typeof (x as { base64Encode?: unknown }).base64Encode === 'function';\n return hasTx && hasSig && hasBytes && !hasMethod;\n}\n\nexport function extractBorshBytesFromPlainSignedTx(x: PlainSignedTransactionLike): number[] {\n const asArray = Array.isArray(x.borsh_bytes) ? (x.borsh_bytes as number[]) : undefined;\n if (asArray) return asArray;\n const asU8 = (x.borshBytes instanceof Uint8Array) ? x.borshBytes : undefined;\n return Array.from(asU8 || new Uint8Array());\n}\n","// Framework-agnostic header helpers to minimize configuration surface.\n// Keep types local to avoid coupling to framework packages.\n\nimport { toOriginOrUndefined } from '../utils/validation'\n\nexport type CspMode = 'strict' | 'compatible'\n\nfunction normalizeWalletOrigins(walletOrigins?: string[]): string[] {\n if (!Array.isArray(walletOrigins) || walletOrigins.length === 0) return []\n const out: string[] = []\n const seen = new Set<string>()\n for (const origin of walletOrigins) {\n const normalized = toOriginOrUndefined(origin)\n if (!normalized || seen.has(normalized)) continue\n seen.add(normalized)\n out.push(normalized)\n }\n return out\n}\n\nexport function buildPermissionsPolicy(walletOrigins?: string[]): string {\n const origins = normalizeWalletOrigins(walletOrigins)\n const originPart = origins.length ? ' ' + origins.map((o) => `\"${o}\"`).join(' ') : ''\n const part = (name: string) => `${name}=(self${originPart})`\n return [\n part('publickey-credentials-get'),\n part('publickey-credentials-create'),\n part('clipboard-read'),\n part('clipboard-write'),\n ].join(', ')\n}\n\n/**\n * Build a wallet-friendly Content Security Policy string.\n *\n * mode:\n * - 'strict' (default): no inline styles, injects \"style-src-attr 'none'\", and forbids 'unsafe-eval'.\n * - 'compatible': allows inline styles/scripts via 'unsafe-inline' for friendlier dev/local setups.\n *\n * allowUnsafeEval:\n * - false by default. Set to true only for development servers that require eval (e.g., Next.js Fast Refresh).\n * - Tatchi SDK does not require 'unsafe-eval' in production.\n *\n * Typical usage: apply strict CSP only to wallet HTML routes\n * (/wallet-service, /export-viewer); do not attach CSP to host app routes.\n */\nexport function buildWalletCsp(opts: {\n frameSrc?: string[]\n mode?: CspMode\n allowUnsafeEval?: boolean\n scriptSrcAllowlist?: string[]\n} = {}): string {\n const mode: CspMode = opts.mode || 'strict'\n const frame = (opts.frameSrc || []).filter(Boolean)\n const scriptAllow = (opts.scriptSrcAllowlist || [])\n .map((s) => toOriginOrUndefined(s) || s)\n .filter(Boolean) as string[]\n const scriptUnsafeInline = mode === 'compatible' ? \" 'unsafe-inline'\" : ''\n const styleUnsafeInline = mode === 'compatible' ? \" 'unsafe-inline'\" : ''\n const scriptUnsafeEval = opts.allowUnsafeEval ? \" 'unsafe-eval'\" : ''\n const base: string[] = [\n \"default-src 'self'\",\n `script-src 'self'${scriptUnsafeInline}${scriptUnsafeEval}${scriptAllow.length ? ' ' + scriptAllow.join(' ') : ''}`,\n `style-src 'self'${styleUnsafeInline}`,\n \"img-src 'self' data:\",\n \"font-src 'self'\",\n \"connect-src 'self' https:\",\n \"worker-src 'self' blob:\",\n `frame-src 'self'${frame.length ? ' ' + frame.join(' ') : ''}`,\n \"object-src 'none'\",\n \"base-uri 'none'\",\n \"form-action 'none'\",\n ]\n if (mode === 'strict') base.splice(2, 0, \"style-src-attr 'none'\")\n return base.join('; ')\n}\n","// Small shared helpers for Vite/Next plugins\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { createRequire } from 'node:module'\n\nexport function addPreconnectLink(res: any, walletOrigins?: string[]) {\n if (!Array.isArray(walletOrigins) || walletOrigins.length === 0) return\n const links: string[] = []\n const seen = new Set<string>()\n for (const origin of walletOrigins) {\n const value = (origin || '').trim()\n if (!value || seen.has(value)) continue\n seen.add(value)\n links.push(`<${value}>; rel=preconnect; crossorigin`)\n }\n if (links.length === 0) return\n try {\n const existing = res.getHeader?.('Link')\n if (!existing) {\n res.setHeader?.('Link', links.join(', '))\n return\n }\n if (typeof existing === 'string') {\n let out = existing\n for (const link of links) {\n if (!out.includes(link)) out = out + ', ' + link\n }\n res.setHeader?.('Link', out)\n return\n }\n if (Array.isArray(existing)) {\n const out = [...existing]\n for (const link of links) {\n if (!out.includes(link)) out.push(link)\n }\n res.setHeader?.('Link', out)\n }\n } catch {}\n}\n\n// Builds wallet service HTML that links only external CSS/JS (no inline),\n// so strict CSP (style-src 'self'; style-src-attr 'none') works in dev/prod.\nexport function buildWalletServiceHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Web3Authn Wallet Service</title>\n <!-- Surface styles are external so strict CSP can keep style-src 'self' -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\" />\n <!-- Prefetch component styles so they are warmed without triggering preload warnings -->\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/halo-border.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/passkey-halo-loading.css\" />\n <!-- Component theme CSS: shared tokens + component-scoped tokens -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\" />\n <!-- Minimal shims some ESM bundles expect (externalized to enable strict CSP) -->\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <!-- Hint the browser to fetch the host script earlier -->\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/wallet-iframe-host.js\" crossorigin>\n </head>\n <body>\n <!-- sdkBasePath points to the SDK root (e.g. '/sdk'). Load the host directly. -->\n <script type=\"module\" src=\"${sdkBasePath}/wallet-iframe-host.js\"></script>\n </body>\n</html>`\n}\n\n// Export viewer HTML is also fully externalized (no inline) to keep CSP strict.\nexport function buildExportViewerHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\">\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin>\n </head>\n <body>\n <w3a-drawer id=\"exp\" theme=\"dark\"></w3a-drawer>\n <script type=\"module\" src=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin></script>\n <script type=\"module\" src=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin></script>\n </body>\n</html>`\n}\n\nexport function resolveCoepMode(explicit?: 'strict' | 'off'): 'strict' | 'off' {\n if (explicit === 'strict' || explicit === 'off') return explicit\n const raw = String((globalThis as any)?.process?.env?.VITE_COEP_MODE || '').trim().toLowerCase()\n if (raw === 'strict' || raw === 'on' || raw === '1' || raw === 'require-corp') return 'strict'\n if (raw === 'off' || raw === '0' || raw === 'false') return 'off'\n return 'off'\n}\n\nexport function applyCoepCorp(res: any) {\n res.setHeader?.('Cross-Origin-Embedder-Policy', 'require-corp')\n res.setHeader?.('Cross-Origin-Resource-Policy', 'cross-origin')\n}\n\nexport function applyCoepCorpIfNeeded(res: any, coepMode?: 'strict' | 'off') {\n if (resolveCoepMode(coepMode) !== 'off') applyCoepCorp(res)\n}\n\nexport function echoCorsFromRequest(\n res: any,\n req: any,\n opts: {\n honorExistingAcaOrigin?: boolean\n allowCredentialsWhenExplicit?: boolean\n methods?: string\n headers?: string\n handlePreflight?: boolean\n } = {}\n) {\n const honorExisting = opts.honorExistingAcaOrigin === true\n const allowCreds = opts.allowCredentialsWhenExplicit !== false\n const methods = opts.methods || 'GET,OPTIONS'\n const headers = opts.headers || 'Content-Type,Authorization'\n const handlePreflight = opts.handlePreflight === true\n\n const origin = (req?.headers && (req.headers.origin as string)) || '*'\n const hasExisting = typeof res.getHeader === 'function' && !!res.getHeader('Access-Control-Allow-Origin')\n if (!honorExisting || !hasExisting) {\n res.setHeader?.('Access-Control-Allow-Origin', origin)\n }\n res.setHeader?.('Vary', 'Origin')\n res.setHeader?.('Access-Control-Allow-Methods', methods)\n res.setHeader?.('Access-Control-Allow-Headers', headers)\n if (origin !== '*' && allowCreds) res.setHeader?.('Access-Control-Allow-Credentials', 'true')\n if (handlePreflight) {\n const method = req?.method && String(req.method).toUpperCase()\n if (method === 'OPTIONS') {\n res.statusCode = 204\n res.end?.()\n return true\n }\n }\n return false\n}\n\n/**\n * Log and validate Related Origin Requests (ROR) configuration.\n * - Prints the well-known endpoint and the configured origins list.\n * - Warns if any origins are not absolute (e.g., missing protocol/hostname).\n */\nexport function logRorConfig(origins: string[], endpoint = '/.well-known/webauthn') {\n if (!Array.isArray(origins) || origins.length === 0) return\n const invalid: string[] = []\n for (const o of origins) {\n try {\n const u = new URL(o)\n if (!u.protocol || !u.hostname) invalid.push(o)\n } catch {\n invalid.push(o)\n }\n }\n const msg = `[tatchi] ROR enabled: GET ${endpoint} -> { origins: [${origins.join(', ')}] }`\n console.log(msg)\n if (invalid.length > 0) {\n console.warn(\n `[tatchi] ROR warning: invalid origins: ${invalid.join(\n ', '\n )} (expected absolute origins like https://app.example.com)`\n )\n }\n}\n\n// Sanitize a dynamic allowlist into a normalized set of absolute origins.\nexport function sanitizeOrigins(values: unknown): string[] {\n const out = new Set<string>()\n if (Array.isArray(values)) {\n for (const v of values) {\n if (typeof v !== 'string') continue\n try {\n const u = new URL(v.trim())\n const scheme = u.protocol\n const host = u.hostname.toLowerCase()\n const port = u.port ? `:${u.port}` : ''\n const isHttps = scheme === 'https:'\n const isLocalhostHttp = scheme === 'http:' && host === 'localhost'\n if (!isHttps && !isLocalhostHttp) continue\n if ((u.pathname && u.pathname !== '/') || u.search || u.hash) continue\n out.add(`${scheme}//${host}${port}`)\n } catch {}\n }\n }\n return Array.from(out)\n}\n\n/**\n * Fetch the wallet ROR allowlist from NEAR RPC and return sanitized origins.\n * Throws on transport/HTTP errors; caller may respond with an empty list on failure.\n */\nconst __rorCache = new Map<string, { origins: string[]; expiresAt: number }>()\nconst __rorInflight = new Map<string, Promise<string[]>>()\n\nexport async function fetchRorOriginsFromNear(opts: {\n rpcUrl: string\n contractId: string\n method: string\n cacheTtlMs?: number\n}): Promise<string[]> {\n const { rpcUrl, contractId } = opts\n const method = opts.method || 'get_allowed_origins'\n const ttl = Math.max(0, Number(opts.cacheTtlMs ?? (process.env.VITE_ROR_CACHE_TTL_MS as any) ?? 60000)) || 60000\n const key = `${rpcUrl}|${contractId}|${method}`\n\n const now = Date.now()\n const cached = __rorCache.get(key)\n if (cached && now < cached.expiresAt) {\n return cached.origins\n }\n\n if (!__rorInflight.has(key)) {\n const body = JSON.stringify({\n jsonrpc: '2.0',\n id: String(Date.now()),\n method: 'query',\n params: {\n request_type: 'call_function',\n finality: 'final',\n account_id: contractId,\n method_name: method,\n args_base64: Buffer.from(JSON.stringify({})).toString('base64'),\n },\n })\n\n const p = (async () => {\n const resp = await fetch(rpcUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!resp.ok) {\n throw new Error(`RPC ${resp.status} ${resp.statusText}`)\n }\n const jsonResponse = await resp.json()\n const bytes: unknown = jsonResponse?.result?.result\n let parsed: unknown = []\n if (Array.isArray(bytes)) {\n const str = String.fromCharCode(...(bytes as number[]))\n try { parsed = JSON.parse(str) } catch { parsed = [] }\n }\n const origins = sanitizeOrigins(parsed as unknown[])\n __rorCache.set(key, { origins, expiresAt: Date.now() + ttl })\n return origins\n })()\n\n __rorInflight.set(key, p.finally(() => __rorInflight.delete(key)))\n }\n\n return __rorInflight.get(key) as Promise<string[]>\n}\n\n/**\n * Infer and set a proper Content-Type header for a given file path.\n * Shared by both app and wallet-iframe dev servers.\n */\nexport function setContentType(res: any, filePath: string) {\n const ext = path.extname(filePath)\n switch (ext) {\n case '.js':\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n break\n case '.css':\n res.setHeader('Content-Type', 'text/css; charset=utf-8')\n break\n case '.map':\n case '.json':\n res.setHeader('Content-Type', 'application/json; charset=utf-8')\n break\n case '.wasm':\n res.setHeader('Content-Type', 'application/wasm')\n break\n case '.html':\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n break\n default:\n res.setHeader('Content-Type', 'application/octet-stream')\n }\n}\n\n// === Shared path helpers across Vite/Next plugins ===\n\nexport { toBasePath } from '../utils/validation'\n\nconst requireCjs = createRequire(import.meta.url)\n\nexport function resolveSdkDistRoot(explicit?: string): string {\n if (explicit) return path.resolve(explicit)\n const pkgPath = requireCjs.resolve('@tatchi-xyz/sdk/package.json')\n const pkgDir = path.dirname(pkgPath)\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { module?: string }\n const esmEntry = pkgJson.module || 'dist/esm/index.js'\n const esmAbs = path.resolve(pkgDir, esmEntry)\n return path.resolve(path.dirname(esmAbs), '..')\n } catch {\n return path.join(pkgDir, 'dist')\n }\n}\n","import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { applyCoepCorpIfNeeded, resolveCoepMode, setContentType } from './plugin-utils'\n\n// Offline export route HTML: fully externalized (no inline) so strict CSP works.\nexport function buildOfflineExportHtml(sdkBasePath: string): string {\n const rpIdBase = (process?.env?.VITE_RP_ID_BASE || '').toString().trim();\n const rpIdMeta = rpIdBase ? `\\n <meta name=\"tatchi-rpid-base\" content=\"${rpIdBase}\">` : '';\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n <title>Emergency Export (Offline)</title>\n <link rel=\"manifest\" href=\"/offline-export/manifest.webmanifest\">${rpIdMeta}\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/offline-export.css\">\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <link rel=\"modulepreload\" href=\"/offline-export/offline-export-app.js\" crossorigin>\n </head>\n <body>\n <script type=\"module\" src=\"/offline-export/offline-export-app.js\" crossorigin></script>\n </body>\n </html>`\n}\n\n// Build a curated list of assets to precache for the offline-export route.\n// - Includes the route HTML + manifest\n// - Includes the SDK CSS/JS used by the route\n// - Includes worker JS + WASM\n// - Also scans offline-export-app.js for direct ESM imports (hashed files) in the SDK dist\nexport function computeOfflinePrecacheList(sdkBasePath: string, sdkDistRoot: string): string[] {\n const list: string[] = []\n const add = (p?: string) => {\n if (!p) return\n if (!list.includes(p)) list.push(p)\n }\n // Route assets\n add('/offline-export/index.html')\n add('/offline-export/manifest.webmanifest')\n // Offline-scoped app entry so SW-controlled loads work on offline refresh\n add('/offline-export/offline-export-app.js')\n // SDK surface/css/js\n add(`${sdkBasePath}/wallet-service.css`)\n add(`${sdkBasePath}/w3a-components.css`)\n add(`${sdkBasePath}/drawer.css`)\n add(`${sdkBasePath}/tx-tree.css`)\n add(`${sdkBasePath}/tx-confirmer.css`)\n add(`${sdkBasePath}/offline-export.css`)\n // Export viewer CSS used by offline export drawer flow\n add(`${sdkBasePath}/export-viewer.css`)\n add(`${sdkBasePath}/export-iframe.css`)\n add(`${sdkBasePath}/wallet-shims.js`)\n add(`${sdkBasePath}/offline-export-app.js`)\n // Workers and WASM\n add(`${sdkBasePath}/workers/web3authn-signer.worker.js`)\n add(`${sdkBasePath}/workers/web3authn-vrf.worker.js`)\n add(`${sdkBasePath}/workers/wasm_signer_worker_bg.wasm`)\n add(`${sdkBasePath}/workers/wasm_vrf_worker_bg.wasm`)\n\n // Scan offline-export-app.js for sibling ESM imports and include them\n try {\n const entry = path.join(sdkDistRoot, 'esm', 'sdk', 'offline-export-app.js')\n if (fs.existsSync(entry)) {\n const src = fs.readFileSync(entry, 'utf-8')\n const re = /(?:import\\(|from)\\s*[\"']\\.\\/([^\"']+\\.js)[\"']/g\n let m: RegExpExecArray | null\n while ((m = re.exec(src))) {\n const file = m[1]\n if (file && !file.includes('..')) add(`${sdkBasePath}/${file.replace(/^\\.\\//, '')}`)\n }\n }\n } catch {}\n\n // Include all JS chunks in esm/sdk (covers dynamic imports such as localOnly-*.js, transactions-*.js, etc.)\n try {\n const sdkDir = path.join(sdkDistRoot, 'esm', 'sdk')\n const files = fs.readdirSync(sdkDir, { withFileTypes: true })\n for (const f of files) {\n if (!f.isFile()) continue\n if (f.name.endsWith('.js')) add(`${sdkBasePath}/${f.name}`)\n }\n } catch {}\n\n return list\n}\n\n\nexport function addOfflineExportDevRoutes(\n server: any,\n opts: {\n sdkDistRoot: string\n sdkBasePath: string\n offlineHtml: string\n includeAppModule?: boolean\n coepMode?: 'strict' | 'off'\n }\n) {\n const { sdkDistRoot, sdkBasePath, offlineHtml, includeAppModule } = opts\n const coepMode = resolveCoepMode(opts.coepMode)\n\n // Route: /offline-export (HTML)\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export' && url !== '/offline-export/') return next()\n res.statusCode = 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n // HTML should never be cached; keeps SW updates and app changes visible\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n res.end(offlineHtml)\n })\n\n // Route: /offline-export/manifest.webmanifest\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/manifest.webmanifest') return next()\n const manifest = {\n name: 'Emergency Export',\n short_name: 'Export',\n start_url: '/offline-export/',\n scope: '/offline-export/',\n display: 'standalone',\n background_color: '#0b0b0c',\n theme_color: '#0b0b0c',\n icons: [] as any[],\n }\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/manifest+json; charset=utf-8')\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n res.end(JSON.stringify(manifest))\n })\n\n // Route: /offline-export/sw.js\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/sw.js') return next()\n try {\n const filePath = path.join(sdkDistRoot, 'workers', 'offline-export-sw.js')\n if (!fs.existsSync(filePath)) {\n res.statusCode = 404\n res.end('offline-export-sw.js not found in SDK dist')\n return\n }\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n // Ensure SW updates are fetched fresh during dev\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch (e) {\n res.statusCode = 500\n res.end('failed to serve offline SW')\n }\n })\n\n // Route: /offline-export/workers/* (map to SDK dist workers)\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (!url.startsWith('/offline-export/workers/')) return next()\n try {\n const rel = url.replace('/offline-export/workers/', '')\n const filePath = path.join(sdkDistRoot, 'workers', rel)\n if (!fs.existsSync(filePath)) {\n res.statusCode = 404\n res.end('worker asset not found')\n return\n }\n res.statusCode = 200\n setContentType(res, filePath)\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch (e) {\n res.statusCode = 500\n res.end('failed to serve offline-export worker asset')\n }\n })\n\n // Route: /offline-export/precache.manifest.json\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/precache.manifest.json') return next()\n try {\n const entries = computeOfflinePrecacheList(sdkBasePath, sdkDistRoot)\n ;[\n '/offline-export/workers/web3authn-signer.worker.js',\n '/offline-export/workers/web3authn-vrf.worker.js',\n '/offline-export/workers/wasm_signer_worker_bg.wasm',\n '/offline-export/workers/wasm_vrf_worker_bg.wasm',\n ].forEach((p) => {\n if (!entries.includes(p)) entries.push(p)\n })\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json; charset=utf-8')\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n res.end(JSON.stringify(entries))\n } catch (e) {\n res.statusCode = 500\n res.end('failed to build precache manifest')\n }\n })\n\n if (includeAppModule) {\n // Route: /offline-export/offline-export-app.js\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n if (url !== '/offline-export/offline-export-app.js') return next()\n try {\n const filePath = path.join(sdkDistRoot, 'esm', 'sdk', 'offline-export-app.js')\n if (!fs.existsSync(filePath)) {\n res.statusCode = 404\n res.end('offline-export-app.js not found in SDK dist')\n return\n }\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n res.setHeader('Cache-Control', 'no-cache')\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch (e) {\n res.statusCode = 500\n res.end('failed to serve offline-export-app.js')\n }\n })\n\n // Route: offline-export sibling ESM chunks\n server.middlewares.use((req: any, res: any, next: any) => {\n const url = (req.url || '').split('?')[0]\n const isChunk = url.startsWith('/offline-export/') && url.endsWith('.js') && !url.startsWith('/offline-export/workers/')\n if (!isChunk) return next()\n try {\n const filename = url.replace('/offline-export/', '')\n const filePath = path.join(sdkDistRoot, 'esm', 'sdk', filename)\n if (!fs.existsSync(filePath)) return next()\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n applyCoepCorpIfNeeded(res, coepMode)\n fs.createReadStream(filePath).pipe(res)\n } catch {\n next()\n }\n })\n }\n}\n\n// ===== Build-time helpers =====\n\nfunction ensureDir(p: string): void {\n try { fs.mkdirSync(p, { recursive: true }) } catch {}\n}\n\nfunction writeTextIfMissing(filePath: string, content: string): void {\n if (fs.existsSync(filePath)) return\n ensureDir(path.dirname(filePath))\n fs.writeFileSync(filePath, content, 'utf-8')\n}\n\nfunction writeJsonIfMissing(filePath: string, obj: unknown): void {\n if (fs.existsSync(filePath)) return\n ensureDir(path.dirname(filePath))\n fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), 'utf-8')\n}\n\nfunction copyIfExists(src: string, dst: string): boolean {\n if (!fs.existsSync(src)) return false\n ensureDir(path.dirname(dst))\n fs.copyFileSync(src, dst)\n return true\n}\n\nfunction copyBatchIfExists(srcDir: string, files: string[], dstDir: string): void {\n ensureDir(dstDir)\n for (const f of files) {\n const src = path.join(srcDir, f)\n const dst = path.join(dstDir, f)\n if (fs.existsSync(src)) fs.copyFileSync(src, dst)\n }\n}\n\nexport function emitOfflineExportAssets(opts: { outDir: string; sdkBasePath: string; sdkDistRoot: string }): void {\n const { outDir, sdkBasePath, sdkDistRoot } = opts\n const offlineDir = path.join(outDir, 'offline-export')\n const workersDir = path.join(offlineDir, 'workers')\n\n let swCopied = false\n let workersCopied = false\n let appCopied = false\n let htmlEnsured = false\n let manifestEnsured = false\n let precacheEmitted = false\n\n // 1) Copy Service Worker\n try {\n const swSrc = path.join(sdkDistRoot, 'workers', 'offline-export-sw.js')\n const swDst = path.join(offlineDir, 'sw.js')\n swCopied = copyIfExists(swSrc, swDst)\n if (!swCopied) console.warn('[tatchi][offline] SW not found in SDK dist; skipping copy')\n } catch (e) {\n console.warn('[tatchi][offline] failed to copy SW:', e)\n }\n\n // 2) Copy workers + WASM\n try {\n const srcDir = path.join(sdkDistRoot, 'workers')\n copyBatchIfExists(srcDir, [\n 'web3authn-signer.worker.js',\n 'web3authn-vrf.worker.js',\n 'wasm_signer_worker_bg.wasm',\n 'wasm_vrf_worker_bg.wasm',\n ], workersDir)\n workersCopied = true\n } catch (e) {\n console.warn('[tatchi][offline] failed to copy workers:', e)\n }\n\n // 3) Copy offline-export app module\n try {\n const src = path.join(sdkDistRoot, 'esm', 'sdk', 'offline-export-app.js')\n const dst = path.join(offlineDir, 'offline-export-app.js')\n appCopied = copyIfExists(src, dst)\n if (!appCopied) console.warn('[tatchi][offline] app module not found in SDK dist; skipping copy')\n } catch (e) {\n console.warn('[tatchi][offline] failed to copy app module:', e)\n }\n\n // 4) Emit HTML + manifest if missing\n try {\n const offHtml = path.join(offlineDir, 'index.html')\n writeTextIfMissing(offHtml, buildOfflineExportHtml(sdkBasePath))\n htmlEnsured = fs.existsSync(offHtml)\n } catch (e) {\n console.warn('[tatchi][offline] failed to emit index.html:', e)\n }\n try {\n const manifestPath = path.join(offlineDir, 'manifest.webmanifest')\n writeJsonIfMissing(manifestPath, {\n name: 'Emergency Export',\n short_name: 'Export',\n start_url: '/offline-export/',\n scope: '/offline-export/',\n display: 'standalone',\n background_color: '#0b0b0c',\n theme_color: '#0b0b0c',\n icons: [] as any[],\n })\n manifestEnsured = fs.existsSync(manifestPath)\n } catch (e) {\n console.warn('[tatchi][offline] failed to emit manifest.webmanifest:', e)\n }\n\n // 5) Emit precache manifest\n try {\n const precachePath = path.join(offlineDir, 'precache.manifest.json');\n const entries = computeOfflinePrecacheList(sdkBasePath, sdkDistRoot);\n ;[\n '/offline-export/offline-export-app.js',\n '/offline-export/workers/web3authn-signer.worker.js',\n '/offline-export/workers/web3authn-vrf.worker.js',\n '/offline-export/workers/wasm_signer_worker_bg.wasm',\n '/offline-export/workers/wasm_vrf_worker_bg.wasm',\n ].forEach((p) => { if (!entries.includes(p)) entries.push(p) });\n ensureDir(offlineDir)\n fs.writeFileSync(precachePath, JSON.stringify(entries, null, 2), 'utf-8');\n precacheEmitted = true\n } catch (e) {\n console.warn('[tatchi][offline] failed to emit precache.manifest.json:', e)\n }\n\n // Consolidated summary\n console.log(\n `[tatchi][offline] emitted assets: sw=${swCopied ? 'ok' : 'skip'} workers=${workersCopied ? 'ok' : 'skip'} app=${appCopied ? 'ok' : 'skip'} html=${htmlEnsured ? 'ok' : 'skip'} manifest=${manifestEnsured ? 'ok' : 'skip'} precache=${precacheEmitted ? 'ok' : 'skip'}`\n )\n}\n\n// Re-export helpers so Offline surface stays co-located\nexport { }\n\nexport type { }\n","// Minimal Next.js helpers: compose a headers() entry for cross-origin wallet embedding.\n// Avoid importing Next types; keep shapes generic.\n//\n// CSP policy note:\n// - We RELAX CSP ONLY FOR NEXT DEV to accommodate the framework's dev runtime (Fast Refresh/overlay),\n// which requires 'unsafe-eval' and inline styles. This relaxation is not required by the Tatchi SDK itself.\n// - In PRODUCTION you should keep a strict CSP (no 'unsafe-eval', no inline styles, and include \"style-src-attr 'none'\").\n\nimport { buildPermissionsPolicy, buildWalletCsp, type CspMode } from './headers'\nimport { fetchRorOriginsFromNear, resolveSdkDistRoot, toBasePath } from './plugin-utils'\nimport { emitOfflineExportAssets as emitOfflineAssetsCore } from './offline'\nimport * as path from 'node:path'\n\nexport type NextHeader = { key: string; value: string }\nexport type NextHeaderEntry = { source: string; headers: NextHeader[] }\n\nexport function tatchiNextHeaders(opts: {\n walletOrigins?: string[]\n cspMode?: CspMode\n extraFrameSrc?: string[]\n /** Optional allowlist for script-src (e.g., wallet origins for modulepreload in dev) */\n extraScriptSrc?: string[]\n allowUnsafeEvalDev?: boolean\n compatibleInDev?: boolean\n}): NextHeaderEntry[] {\n const walletOrigins = (opts.walletOrigins || []).filter(Boolean)\n const permissions = buildPermissionsPolicy(walletOrigins)\n const isDev = process.env.NODE_ENV !== 'production'\n const mode: CspMode = opts.cspMode ?? (isDev && (opts.compatibleInDev ?? true) ? 'compatible' : 'strict')\n const allowUnsafeEval = isDev && (opts.allowUnsafeEvalDev ?? true)\n const csp = buildWalletCsp({\n frameSrc: [...walletOrigins, ...(opts.extraFrameSrc || [])],\n scriptSrcAllowlist: [...(opts.extraScriptSrc || [])],\n mode,\n allowUnsafeEval,\n })\n return [\n { source: '/:path*', headers: [\n { key: 'Permissions-Policy', value: permissions },\n { key: 'Content-Security-Policy', value: csp },\n ]}\n ]\n}\n\n/**\n * Convenience wrapper for Next.js app origin.\n * Adds Permissions-Policy and a wallet-friendly CSP via Next's headers() API.\n * emitHeaders has no effect for Next.js; kept for parity with Vite wrappers.\n */\nexport function tatchiNextApp(opts: {\n walletOrigins?: string[]\n emitHeaders?: boolean\n cspMode?: CspMode\n extraFrameSrc?: string[]\n extraScriptSrc?: string[]\n allowUnsafeEvalDev?: boolean\n compatibleInDev?: boolean\n}) {\n if (opts.emitHeaders) {\n console.warn('[tatchi] tatchiNextApp: emitHeaders has no effect in Next.js; headers are applied via next.config.js headers().')\n }\n return (config: any) => {\n const existing = config?.headers\n return {\n ...config,\n async headers() {\n const user = typeof existing === 'function' ? await existing() : []\n return [...(user || []), ...tatchiNextHeaders(opts)]\n },\n }\n }\n}\n\n/**\n * Convenience wrapper for Next.js wallet origins.\n * Same behavior as tatchiNextApp — Next.js does not serve the SDK/wallet HTML; this\n * helper only sets headers via headers() so the wallet host can be prepped if you\n * proxy wallet routes through Next in dev.\n */\nexport function tatchiNextWallet(opts: {\n walletOrigins?: string[]\n emitHeaders?: boolean\n cspMode?: CspMode\n extraFrameSrc?: string[]\n extraScriptSrc?: string[]\n allowUnsafeEvalDev?: boolean\n compatibleInDev?: boolean\n}) {\n if (opts.emitHeaders) {\n console.warn('[tatchi] tatchiNextWallet: emitHeaders has no effect in Next.js; headers are applied via next.config.js headers().')\n }\n return (config: any) => {\n const existing = config?.headers\n return {\n ...config,\n async headers() {\n const user = typeof existing === 'function' ? await existing() : []\n return [...(user || []), ...tatchiNextHeaders(opts)]\n },\n}\n }\n}\n\n// === Well-known (/.well-known/webauthn) helpers for Next.js ===\n// These helpers mirror the Vite dev server behavior and let Next apps expose\n// a dynamic allowlist fetched from chain without a relay in development.\n\ntype RorOpts = {\n rpcUrl?: string\n contractId?: string\n method?: string\n cacheTtlMs?: number\n}\n\nfunction resolveRorParams(opts: RorOpts) {\n const rpcUrl = (opts.rpcUrl || process.env.VITE_NEAR_RPC_URL || 'https://test.rpc.fastnear.com').toString().trim()\n const contractId = (opts.contractId || process.env.VITE_WEBAUTHN_CONTRACT_ID || '').toString().trim()\n const method = (opts.method || process.env.VITE_ROR_METHOD || 'get_allowed_origins').toString().trim()\n const cacheTtlMs = Number(opts.cacheTtlMs ?? process.env.VITE_ROR_CACHE_TTL_MS ?? 60000)\n return { rpcUrl, contractId, method, cacheTtlMs }\n}\n\n/**\n * Pages Router compatible handler (Node runtime).\n * Usage (pages/api/.well-known/webauthn.ts):\n * export default (req, res) => handleWellKnownRorNode(req, res)\n */\nexport async function handleWellKnownRorNode(req: any, res: any, opts: RorOpts = {}) {\n try {\n const params = resolveRorParams(opts)\n const origins = params.contractId\n ? await fetchRorOriginsFromNear(params)\n : []\n res.statusCode = 200\n res.setHeader?.('Content-Type', 'application/json; charset=utf-8')\n res.setHeader?.('Cache-Control', 'max-age=60, stale-while-revalidate=600')\n res.end?.(JSON.stringify({ origins }))\n } catch (e) {\n console.warn('[tatchi][next] ROR fetch failed:', e)\n res.statusCode = 200\n res.setHeader?.('Content-Type', 'application/json; charset=utf-8')\n res.setHeader?.('Cache-Control', 'max-age=60, stale-while-revalidate=600')\n res.end?.(JSON.stringify({ origins: [] }))\n }\n}\n\n/**\n * App Router compatible handler (Edge/Route Handler style).\n * Usage (app/.well-known/webauthn/route.ts):\n * export async function GET(req: Request) { return handleWellKnownRorEdge(req) }\n */\nexport async function handleWellKnownRorEdge(_request: Request, opts: RorOpts = {}): Promise<Response> {\n try {\n const params = resolveRorParams(opts)\n const origins = params.contractId\n ? await fetchRorOriginsFromNear(params)\n : []\n return new Response(JSON.stringify({ origins }), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Cache-Control': 'max-age=60, stale-while-revalidate=600',\n },\n })\n } catch (e) {\n console.warn('[tatchi][next] ROR fetch failed:', e)\n const origins: string[] = []\n return new Response(JSON.stringify({ origins }), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Cache-Control': 'max-age=60, stale-while-revalidate=600',\n },\n })\n }\n}\n\n// === Build-time helper: emit offline-export assets (Next.js parity) ===\n// Exported for parity with the Vite plugin helper. Can be invoked from a\n// custom Next build script or post-build step to copy SW/workers and emit\n// offline-export HTML, manifest, and precache manifest into your public dir.\n\nexport function nextEmitOfflineExportAssets(opts: { outDir: string; sdkBasePath?: string; sdkDistRoot?: string }): void {\n const outDir = path.resolve(opts.outDir)\n const sdkBasePath = toBasePath(opts.sdkBasePath, '/sdk')\n const sdkDistRoot = resolveSdkDistRoot(opts.sdkDistRoot)\n emitOfflineAssetsCore({ outDir, sdkBasePath, sdkDistRoot })\n}\n"],"mappings":"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"}
|
|
@@ -8,21 +8,33 @@ let node_module = require("node:module");
|
|
|
8
8
|
node_module = require_rolldown_runtime.__toESM(node_module);
|
|
9
9
|
|
|
10
10
|
//#region src/plugins/plugin-utils.ts
|
|
11
|
-
function addPreconnectLink(res,
|
|
12
|
-
if (!
|
|
11
|
+
function addPreconnectLink(res, walletOrigins) {
|
|
12
|
+
if (!Array.isArray(walletOrigins) || walletOrigins.length === 0) return;
|
|
13
|
+
const links = [];
|
|
14
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15
|
+
for (const origin of walletOrigins) {
|
|
16
|
+
const value = (origin || "").trim();
|
|
17
|
+
if (!value || seen.has(value)) continue;
|
|
18
|
+
seen.add(value);
|
|
19
|
+
links.push(`<${value}>; rel=preconnect; crossorigin`);
|
|
20
|
+
}
|
|
21
|
+
if (links.length === 0) return;
|
|
13
22
|
try {
|
|
14
|
-
const link = `<${origin}>; rel=preconnect; crossorigin`;
|
|
15
23
|
const existing = res.getHeader?.("Link");
|
|
16
24
|
if (!existing) {
|
|
17
|
-
res.setHeader?.("Link",
|
|
25
|
+
res.setHeader?.("Link", links.join(", "));
|
|
18
26
|
return;
|
|
19
27
|
}
|
|
20
28
|
if (typeof existing === "string") {
|
|
21
|
-
|
|
29
|
+
let out = existing;
|
|
30
|
+
for (const link of links) if (!out.includes(link)) out = out + ", " + link;
|
|
31
|
+
res.setHeader?.("Link", out);
|
|
22
32
|
return;
|
|
23
33
|
}
|
|
24
34
|
if (Array.isArray(existing)) {
|
|
25
|
-
|
|
35
|
+
const out = [...existing];
|
|
36
|
+
for (const link of links) if (!out.includes(link)) out.push(link);
|
|
37
|
+
res.setHeader?.("Link", out);
|
|
26
38
|
}
|
|
27
39
|
} catch {}
|
|
28
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-utils.js","names":["bytes: unknown","parsed: unknown","path","fs"],"sources":["../../../src/plugins/plugin-utils.ts"],"sourcesContent":["// Small shared helpers for Vite/Next plugins\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { createRequire } from 'node:module'\n\nexport function addPreconnectLink(res: any, origin?: string) {\n if (!origin) return\n try {\n const link = `<${origin}>; rel=preconnect; crossorigin`\n const existing = res.getHeader?.('Link')\n if (!existing) {\n res.setHeader?.('Link', link)\n return\n }\n if (typeof existing === 'string') {\n if (!existing.includes(link)) res.setHeader?.('Link', existing + ', ' + link)\n return\n }\n if (Array.isArray(existing)) {\n if (!existing.includes(link)) res.setHeader?.('Link', [...existing, link])\n }\n } catch {}\n}\n\n// Builds wallet service HTML that links only external CSS/JS (no inline),\n// so strict CSP (style-src 'self'; style-src-attr 'none') works in dev/prod.\nexport function buildWalletServiceHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Web3Authn Wallet Service</title>\n <!-- Surface styles are external so strict CSP can keep style-src 'self' -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\" />\n <!-- Prefetch component styles so they are warmed without triggering preload warnings -->\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/halo-border.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/passkey-halo-loading.css\" />\n <!-- Component theme CSS: shared tokens + component-scoped tokens -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\" />\n <!-- Minimal shims some ESM bundles expect (externalized to enable strict CSP) -->\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <!-- Hint the browser to fetch the host script earlier -->\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/wallet-iframe-host.js\" crossorigin>\n </head>\n <body>\n <!-- sdkBasePath points to the SDK root (e.g. '/sdk'). Load the host directly. -->\n <script type=\"module\" src=\"${sdkBasePath}/wallet-iframe-host.js\"></script>\n </body>\n</html>`\n}\n\n// Export viewer HTML is also fully externalized (no inline) to keep CSP strict.\nexport function buildExportViewerHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\">\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin>\n </head>\n <body>\n <w3a-drawer id=\"exp\" theme=\"dark\"></w3a-drawer>\n <script type=\"module\" src=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin></script>\n <script type=\"module\" src=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin></script>\n </body>\n</html>`\n}\n\nexport function resolveCoepMode(explicit?: 'strict' | 'off'): 'strict' | 'off' {\n if (explicit === 'strict' || explicit === 'off') return explicit\n const raw = String((globalThis as any)?.process?.env?.VITE_COEP_MODE || '').trim().toLowerCase()\n if (raw === 'strict' || raw === 'on' || raw === '1' || raw === 'require-corp') return 'strict'\n if (raw === 'off' || raw === '0' || raw === 'false') return 'off'\n return 'off'\n}\n\nexport function applyCoepCorp(res: any) {\n res.setHeader?.('Cross-Origin-Embedder-Policy', 'require-corp')\n res.setHeader?.('Cross-Origin-Resource-Policy', 'cross-origin')\n}\n\nexport function applyCoepCorpIfNeeded(res: any, coepMode?: 'strict' | 'off') {\n if (resolveCoepMode(coepMode) !== 'off') applyCoepCorp(res)\n}\n\nexport function echoCorsFromRequest(\n res: any,\n req: any,\n opts: {\n honorExistingAcaOrigin?: boolean\n allowCredentialsWhenExplicit?: boolean\n methods?: string\n headers?: string\n handlePreflight?: boolean\n } = {}\n) {\n const honorExisting = opts.honorExistingAcaOrigin === true\n const allowCreds = opts.allowCredentialsWhenExplicit !== false\n const methods = opts.methods || 'GET,OPTIONS'\n const headers = opts.headers || 'Content-Type,Authorization'\n const handlePreflight = opts.handlePreflight === true\n\n const origin = (req?.headers && (req.headers.origin as string)) || '*'\n const hasExisting = typeof res.getHeader === 'function' && !!res.getHeader('Access-Control-Allow-Origin')\n if (!honorExisting || !hasExisting) {\n res.setHeader?.('Access-Control-Allow-Origin', origin)\n }\n res.setHeader?.('Vary', 'Origin')\n res.setHeader?.('Access-Control-Allow-Methods', methods)\n res.setHeader?.('Access-Control-Allow-Headers', headers)\n if (origin !== '*' && allowCreds) res.setHeader?.('Access-Control-Allow-Credentials', 'true')\n if (handlePreflight) {\n const method = req?.method && String(req.method).toUpperCase()\n if (method === 'OPTIONS') {\n res.statusCode = 204\n res.end?.()\n return true\n }\n }\n return false\n}\n\n/**\n * Log and validate Related Origin Requests (ROR) configuration.\n * - Prints the well-known endpoint and the configured origins list.\n * - Warns if any origins are not absolute (e.g., missing protocol/hostname).\n */\nexport function logRorConfig(origins: string[], endpoint = '/.well-known/webauthn') {\n if (!Array.isArray(origins) || origins.length === 0) return\n const invalid: string[] = []\n for (const o of origins) {\n try {\n const u = new URL(o)\n if (!u.protocol || !u.hostname) invalid.push(o)\n } catch {\n invalid.push(o)\n }\n }\n const msg = `[tatchi] ROR enabled: GET ${endpoint} -> { origins: [${origins.join(', ')}] }`\n console.log(msg)\n if (invalid.length > 0) {\n console.warn(\n `[tatchi] ROR warning: invalid origins: ${invalid.join(\n ', '\n )} (expected absolute origins like https://app.example.com)`\n )\n }\n}\n\n// Sanitize a dynamic allowlist into a normalized set of absolute origins.\nexport function sanitizeOrigins(values: unknown): string[] {\n const out = new Set<string>()\n if (Array.isArray(values)) {\n for (const v of values) {\n if (typeof v !== 'string') continue\n try {\n const u = new URL(v.trim())\n const scheme = u.protocol\n const host = u.hostname.toLowerCase()\n const port = u.port ? `:${u.port}` : ''\n const isHttps = scheme === 'https:'\n const isLocalhostHttp = scheme === 'http:' && host === 'localhost'\n if (!isHttps && !isLocalhostHttp) continue\n if ((u.pathname && u.pathname !== '/') || u.search || u.hash) continue\n out.add(`${scheme}//${host}${port}`)\n } catch {}\n }\n }\n return Array.from(out)\n}\n\n/**\n * Fetch the wallet ROR allowlist from NEAR RPC and return sanitized origins.\n * Throws on transport/HTTP errors; caller may respond with an empty list on failure.\n */\nconst __rorCache = new Map<string, { origins: string[]; expiresAt: number }>()\nconst __rorInflight = new Map<string, Promise<string[]>>()\n\nexport async function fetchRorOriginsFromNear(opts: {\n rpcUrl: string\n contractId: string\n method: string\n cacheTtlMs?: number\n}): Promise<string[]> {\n const { rpcUrl, contractId } = opts\n const method = opts.method || 'get_allowed_origins'\n const ttl = Math.max(0, Number(opts.cacheTtlMs ?? (process.env.VITE_ROR_CACHE_TTL_MS as any) ?? 60000)) || 60000\n const key = `${rpcUrl}|${contractId}|${method}`\n\n const now = Date.now()\n const cached = __rorCache.get(key)\n if (cached && now < cached.expiresAt) {\n return cached.origins\n }\n\n if (!__rorInflight.has(key)) {\n const body = JSON.stringify({\n jsonrpc: '2.0',\n id: String(Date.now()),\n method: 'query',\n params: {\n request_type: 'call_function',\n finality: 'final',\n account_id: contractId,\n method_name: method,\n args_base64: Buffer.from(JSON.stringify({})).toString('base64'),\n },\n })\n\n const p = (async () => {\n const resp = await fetch(rpcUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!resp.ok) {\n throw new Error(`RPC ${resp.status} ${resp.statusText}`)\n }\n const jsonResponse = await resp.json()\n const bytes: unknown = jsonResponse?.result?.result\n let parsed: unknown = []\n if (Array.isArray(bytes)) {\n const str = String.fromCharCode(...(bytes as number[]))\n try { parsed = JSON.parse(str) } catch { parsed = [] }\n }\n const origins = sanitizeOrigins(parsed as unknown[])\n __rorCache.set(key, { origins, expiresAt: Date.now() + ttl })\n return origins\n })()\n\n __rorInflight.set(key, p.finally(() => __rorInflight.delete(key)))\n }\n\n return __rorInflight.get(key) as Promise<string[]>\n}\n\n/**\n * Infer and set a proper Content-Type header for a given file path.\n * Shared by both app and wallet-iframe dev servers.\n */\nexport function setContentType(res: any, filePath: string) {\n const ext = path.extname(filePath)\n switch (ext) {\n case '.js':\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n break\n case '.css':\n res.setHeader('Content-Type', 'text/css; charset=utf-8')\n break\n case '.map':\n case '.json':\n res.setHeader('Content-Type', 'application/json; charset=utf-8')\n break\n case '.wasm':\n res.setHeader('Content-Type', 'application/wasm')\n break\n case '.html':\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n break\n default:\n res.setHeader('Content-Type', 'application/octet-stream')\n }\n}\n\n// === Shared path helpers across Vite/Next plugins ===\n\nexport { toBasePath } from '../utils/validation'\n\nconst requireCjs = createRequire(import.meta.url)\n\nexport function resolveSdkDistRoot(explicit?: string): string {\n if (explicit) return path.resolve(explicit)\n const pkgPath = requireCjs.resolve('@tatchi-xyz/sdk/package.json')\n const pkgDir = path.dirname(pkgPath)\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { module?: string }\n const esmEntry = pkgJson.module || 'dist/esm/index.js'\n const esmAbs = path.resolve(pkgDir, esmEntry)\n return path.resolve(path.dirname(esmAbs), '..')\n } catch {\n return path.join(pkgDir, 'dist')\n }\n}\n"],"mappings":";;;;;;;;;;AAKA,SAAgB,kBAAkB,KAAU,QAAiB;AAC3D,KAAI,CAAC,OAAQ;AACb,KAAI;EACF,MAAM,OAAO,IAAI,OAAO;EACxB,MAAM,WAAW,IAAI,YAAY;AACjC,MAAI,CAAC,UAAU;AACb,OAAI,YAAY,QAAQ;AACxB;;AAEF,MAAI,OAAO,aAAa,UAAU;AAChC,OAAI,CAAC,SAAS,SAAS,MAAO,KAAI,YAAY,QAAQ,WAAW,OAAO;AACxE;;AAEF,MAAI,MAAM,QAAQ,WAChB;OAAI,CAAC,SAAS,SAAS,MAAO,KAAI,YAAY,QAAQ,CAAC,GAAG,UAAU;;SAEhE;;AAKV,SAAgB,uBAAuB,aAA6B;AAClE,QAAO;;;;;;;mCAO0B,YAAY;;4CAEH,YAAY;4CACZ,YAAY;4CACZ,YAAY;4CACZ,YAAY;;mCAErB,YAAY;mCACZ,YAAY;mCACZ,YAAY;mCACZ,YAAY;;mBAE5B,YAAY;;sCAEO,YAAY;;;;iCAIjB,YAAY;;;;AAM7C,SAAgB,sBAAsB,aAA6B;AACjE,QAAO;;;;;mCAK0B,YAAY;mCACZ,YAAY;mCACZ,YAAY;mCACZ,YAAY;mCACZ,YAAY;mBAC5B,YAAY;sCACO,YAAY;sCACZ,YAAY;;;;iCAIjB,YAAY;iCACZ,YAAY;;;;AAK7C,SAAgB,gBAAgB,UAA+C;AAC7E,KAAI,aAAa,YAAY,aAAa,MAAO,QAAO;CACxD,MAAM,MAAM,OAAQ,YAAoB,SAAS,KAAK,kBAAkB,IAAI,OAAO;AACnF,KAAI,QAAQ,YAAY,QAAQ,QAAQ,QAAQ,OAAO,QAAQ,eAAgB,QAAO;AACtF,KAAI,QAAQ,SAAS,QAAQ,OAAO,QAAQ,QAAS,QAAO;AAC5D,QAAO;;AAGT,SAAgB,cAAc,KAAU;AACtC,KAAI,YAAY,gCAAgC;AAChD,KAAI,YAAY,gCAAgC;;AAGlD,SAAgB,sBAAsB,KAAU,UAA6B;AAC3E,KAAI,gBAAgB,cAAc,MAAO,eAAc;;AAGzD,SAAgB,oBACd,KACA,KACA,OAMI,IACJ;CACA,MAAM,gBAAgB,KAAK,2BAA2B;CACtD,MAAM,aAAa,KAAK,iCAAiC;CACzD,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,kBAAkB,KAAK,oBAAoB;CAEjD,MAAM,SAAU,KAAK,WAAY,IAAI,QAAQ,UAAsB;CACnE,MAAM,cAAc,OAAO,IAAI,cAAc,cAAc,CAAC,CAAC,IAAI,UAAU;AAC3E,KAAI,CAAC,iBAAiB,CAAC,YACrB,KAAI,YAAY,+BAA+B;AAEjD,KAAI,YAAY,QAAQ;AACxB,KAAI,YAAY,gCAAgC;AAChD,KAAI,YAAY,gCAAgC;AAChD,KAAI,WAAW,OAAO,WAAY,KAAI,YAAY,oCAAoC;AACtF,KAAI,iBAAiB;EACnB,MAAM,SAAS,KAAK,UAAU,OAAO,IAAI,QAAQ;AACjD,MAAI,WAAW,WAAW;AACxB,OAAI,aAAa;AACjB,OAAI;AACJ,UAAO;;;AAGX,QAAO;;AA+BT,SAAgB,gBAAgB,QAA2B;CACzD,MAAM,sBAAM,IAAI;AAChB,KAAI,MAAM,QAAQ,QAChB,MAAK,MAAM,KAAK,QAAQ;AACtB,MAAI,OAAO,MAAM,SAAU;AAC3B,MAAI;GACF,MAAM,IAAI,IAAI,IAAI,EAAE;GACpB,MAAM,SAAS,EAAE;GACjB,MAAM,OAAO,EAAE,SAAS;GACxB,MAAM,OAAO,EAAE,OAAO,IAAI,EAAE,SAAS;GACrC,MAAM,UAAU,WAAW;GAC3B,MAAM,kBAAkB,WAAW,WAAW,SAAS;AACvD,OAAI,CAAC,WAAW,CAAC,gBAAiB;AAClC,OAAK,EAAE,YAAY,EAAE,aAAa,OAAQ,EAAE,UAAU,EAAE,KAAM;AAC9D,OAAI,IAAI,GAAG,OAAO,IAAI,OAAO;UACvB;;AAGZ,QAAO,MAAM,KAAK;;;;;;AAOpB,MAAM,6BAAa,IAAI;AACvB,MAAM,gCAAgB,IAAI;AAE1B,eAAsB,wBAAwB,MAKxB;CACpB,MAAM,EAAE,QAAQ,eAAe;CAC/B,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,MAAM,KAAK,IAAI,GAAG,OAAO,KAAK,cAAe,QAAQ,IAAI,yBAAiC,SAAW;CAC3G,MAAM,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG;CAEvC,MAAM,MAAM,KAAK;CACjB,MAAM,SAAS,WAAW,IAAI;AAC9B,KAAI,UAAU,MAAM,OAAO,UACzB,QAAO,OAAO;AAGhB,KAAI,CAAC,cAAc,IAAI,MAAM;EAC3B,MAAM,OAAO,KAAK,UAAU;GAC1B,SAAS;GACT,IAAI,OAAO,KAAK;GAChB,QAAQ;GACR,QAAQ;IACN,cAAc;IACd,UAAU;IACV,YAAY;IACZ,aAAa;IACb,aAAa,OAAO,KAAK,KAAK,UAAU,KAAK,SAAS;;;EAI1D,MAAM,KAAK,YAAY;GACrB,MAAM,OAAO,MAAM,MAAM,QAAQ;IAC/B,QAAQ;IACR,SAAS,EAAE,gBAAgB;IAC3B;;AAEF,OAAI,CAAC,KAAK,GACR,OAAM,IAAI,MAAM,OAAO,KAAK,OAAO,GAAG,KAAK;GAE7C,MAAM,eAAe,MAAM,KAAK;GAChC,MAAMA,QAAiB,cAAc,QAAQ;GAC7C,IAAIC,SAAkB;AACtB,OAAI,MAAM,QAAQ,QAAQ;IACxB,MAAM,MAAM,OAAO,aAAa,GAAI;AACpC,QAAI;AAAE,cAAS,KAAK,MAAM;YAAa;AAAE,cAAS;;;GAEpD,MAAM,UAAU,gBAAgB;AAChC,cAAW,IAAI,KAAK;IAAE;IAAS,WAAW,KAAK,QAAQ;;AACvD,UAAO;;AAGT,gBAAc,IAAI,KAAK,EAAE,cAAc,cAAc,OAAO;;AAG9D,QAAO,cAAc,IAAI;;;;;;AAO3B,SAAgB,eAAe,KAAU,UAAkB;CACzD,MAAM,MAAMC,UAAK,QAAQ;AACzB,SAAQ,KAAR;EACE,KAAK;AACH,OAAI,UAAU,gBAAgB;AAC9B;EACF,KAAK;AACH,OAAI,UAAU,gBAAgB;AAC9B;EACF,KAAK;EACL,KAAK;AACH,OAAI,UAAU,gBAAgB;AAC9B;EACF,KAAK;AACH,OAAI,UAAU,gBAAgB;AAC9B;EACF,KAAK;AACH,OAAI,UAAU,gBAAgB;AAC9B;EACF,QACE,KAAI,UAAU,gBAAgB;;;AAQpC,MAAM;AAEN,SAAgB,mBAAmB,UAA2B;AAC5D,KAAI,SAAU,QAAOA,UAAK,QAAQ;CAClC,MAAM,UAAU,WAAW,QAAQ;CACnC,MAAM,SAASA,UAAK,QAAQ;AAC5B,KAAI;EACF,MAAM,UAAU,KAAK,MAAMC,QAAG,aAAa,SAAS;EACpD,MAAM,WAAW,QAAQ,UAAU;EACnC,MAAM,SAASD,UAAK,QAAQ,QAAQ;AACpC,SAAOA,UAAK,QAAQA,UAAK,QAAQ,SAAS;SACpC;AACN,SAAOA,UAAK,KAAK,QAAQ"}
|
|
1
|
+
{"version":3,"file":"plugin-utils.js","names":["links: string[]","bytes: unknown","parsed: unknown","path","fs"],"sources":["../../../src/plugins/plugin-utils.ts"],"sourcesContent":["// Small shared helpers for Vite/Next plugins\nimport * as path from 'node:path'\nimport * as fs from 'node:fs'\nimport { createRequire } from 'node:module'\n\nexport function addPreconnectLink(res: any, walletOrigins?: string[]) {\n if (!Array.isArray(walletOrigins) || walletOrigins.length === 0) return\n const links: string[] = []\n const seen = new Set<string>()\n for (const origin of walletOrigins) {\n const value = (origin || '').trim()\n if (!value || seen.has(value)) continue\n seen.add(value)\n links.push(`<${value}>; rel=preconnect; crossorigin`)\n }\n if (links.length === 0) return\n try {\n const existing = res.getHeader?.('Link')\n if (!existing) {\n res.setHeader?.('Link', links.join(', '))\n return\n }\n if (typeof existing === 'string') {\n let out = existing\n for (const link of links) {\n if (!out.includes(link)) out = out + ', ' + link\n }\n res.setHeader?.('Link', out)\n return\n }\n if (Array.isArray(existing)) {\n const out = [...existing]\n for (const link of links) {\n if (!out.includes(link)) out.push(link)\n }\n res.setHeader?.('Link', out)\n }\n } catch {}\n}\n\n// Builds wallet service HTML that links only external CSS/JS (no inline),\n// so strict CSP (style-src 'self'; style-src-attr 'none') works in dev/prod.\nexport function buildWalletServiceHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Web3Authn Wallet Service</title>\n <!-- Surface styles are external so strict CSP can keep style-src 'self' -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\" />\n <!-- Prefetch component styles so they are warmed without triggering preload warnings -->\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/halo-border.css\" />\n <link rel=\"prefetch\" as=\"style\" href=\"${sdkBasePath}/passkey-halo-loading.css\" />\n <!-- Component theme CSS: shared tokens + component-scoped tokens -->\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\" />\n <!-- Minimal shims some ESM bundles expect (externalized to enable strict CSP) -->\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <!-- Hint the browser to fetch the host script earlier -->\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/wallet-iframe-host.js\" crossorigin>\n </head>\n <body>\n <!-- sdkBasePath points to the SDK root (e.g. '/sdk'). Load the host directly. -->\n <script type=\"module\" src=\"${sdkBasePath}/wallet-iframe-host.js\"></script>\n </body>\n</html>`\n}\n\n// Export viewer HTML is also fully externalized (no inline) to keep CSP strict.\nexport function buildExportViewerHtml(sdkBasePath: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/wallet-service.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/w3a-components.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/drawer.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-tree.css\">\n <link rel=\"stylesheet\" href=\"${sdkBasePath}/tx-confirmer.css\">\n <script src=\"${sdkBasePath}/wallet-shims.js\"></script>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin>\n <link rel=\"modulepreload\" href=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin>\n </head>\n <body>\n <w3a-drawer id=\"exp\" theme=\"dark\"></w3a-drawer>\n <script type=\"module\" src=\"${sdkBasePath}/export-private-key-viewer.js\" crossorigin></script>\n <script type=\"module\" src=\"${sdkBasePath}/iframe-export-bootstrap.js\" crossorigin></script>\n </body>\n</html>`\n}\n\nexport function resolveCoepMode(explicit?: 'strict' | 'off'): 'strict' | 'off' {\n if (explicit === 'strict' || explicit === 'off') return explicit\n const raw = String((globalThis as any)?.process?.env?.VITE_COEP_MODE || '').trim().toLowerCase()\n if (raw === 'strict' || raw === 'on' || raw === '1' || raw === 'require-corp') return 'strict'\n if (raw === 'off' || raw === '0' || raw === 'false') return 'off'\n return 'off'\n}\n\nexport function applyCoepCorp(res: any) {\n res.setHeader?.('Cross-Origin-Embedder-Policy', 'require-corp')\n res.setHeader?.('Cross-Origin-Resource-Policy', 'cross-origin')\n}\n\nexport function applyCoepCorpIfNeeded(res: any, coepMode?: 'strict' | 'off') {\n if (resolveCoepMode(coepMode) !== 'off') applyCoepCorp(res)\n}\n\nexport function echoCorsFromRequest(\n res: any,\n req: any,\n opts: {\n honorExistingAcaOrigin?: boolean\n allowCredentialsWhenExplicit?: boolean\n methods?: string\n headers?: string\n handlePreflight?: boolean\n } = {}\n) {\n const honorExisting = opts.honorExistingAcaOrigin === true\n const allowCreds = opts.allowCredentialsWhenExplicit !== false\n const methods = opts.methods || 'GET,OPTIONS'\n const headers = opts.headers || 'Content-Type,Authorization'\n const handlePreflight = opts.handlePreflight === true\n\n const origin = (req?.headers && (req.headers.origin as string)) || '*'\n const hasExisting = typeof res.getHeader === 'function' && !!res.getHeader('Access-Control-Allow-Origin')\n if (!honorExisting || !hasExisting) {\n res.setHeader?.('Access-Control-Allow-Origin', origin)\n }\n res.setHeader?.('Vary', 'Origin')\n res.setHeader?.('Access-Control-Allow-Methods', methods)\n res.setHeader?.('Access-Control-Allow-Headers', headers)\n if (origin !== '*' && allowCreds) res.setHeader?.('Access-Control-Allow-Credentials', 'true')\n if (handlePreflight) {\n const method = req?.method && String(req.method).toUpperCase()\n if (method === 'OPTIONS') {\n res.statusCode = 204\n res.end?.()\n return true\n }\n }\n return false\n}\n\n/**\n * Log and validate Related Origin Requests (ROR) configuration.\n * - Prints the well-known endpoint and the configured origins list.\n * - Warns if any origins are not absolute (e.g., missing protocol/hostname).\n */\nexport function logRorConfig(origins: string[], endpoint = '/.well-known/webauthn') {\n if (!Array.isArray(origins) || origins.length === 0) return\n const invalid: string[] = []\n for (const o of origins) {\n try {\n const u = new URL(o)\n if (!u.protocol || !u.hostname) invalid.push(o)\n } catch {\n invalid.push(o)\n }\n }\n const msg = `[tatchi] ROR enabled: GET ${endpoint} -> { origins: [${origins.join(', ')}] }`\n console.log(msg)\n if (invalid.length > 0) {\n console.warn(\n `[tatchi] ROR warning: invalid origins: ${invalid.join(\n ', '\n )} (expected absolute origins like https://app.example.com)`\n )\n }\n}\n\n// Sanitize a dynamic allowlist into a normalized set of absolute origins.\nexport function sanitizeOrigins(values: unknown): string[] {\n const out = new Set<string>()\n if (Array.isArray(values)) {\n for (const v of values) {\n if (typeof v !== 'string') continue\n try {\n const u = new URL(v.trim())\n const scheme = u.protocol\n const host = u.hostname.toLowerCase()\n const port = u.port ? `:${u.port}` : ''\n const isHttps = scheme === 'https:'\n const isLocalhostHttp = scheme === 'http:' && host === 'localhost'\n if (!isHttps && !isLocalhostHttp) continue\n if ((u.pathname && u.pathname !== '/') || u.search || u.hash) continue\n out.add(`${scheme}//${host}${port}`)\n } catch {}\n }\n }\n return Array.from(out)\n}\n\n/**\n * Fetch the wallet ROR allowlist from NEAR RPC and return sanitized origins.\n * Throws on transport/HTTP errors; caller may respond with an empty list on failure.\n */\nconst __rorCache = new Map<string, { origins: string[]; expiresAt: number }>()\nconst __rorInflight = new Map<string, Promise<string[]>>()\n\nexport async function fetchRorOriginsFromNear(opts: {\n rpcUrl: string\n contractId: string\n method: string\n cacheTtlMs?: number\n}): Promise<string[]> {\n const { rpcUrl, contractId } = opts\n const method = opts.method || 'get_allowed_origins'\n const ttl = Math.max(0, Number(opts.cacheTtlMs ?? (process.env.VITE_ROR_CACHE_TTL_MS as any) ?? 60000)) || 60000\n const key = `${rpcUrl}|${contractId}|${method}`\n\n const now = Date.now()\n const cached = __rorCache.get(key)\n if (cached && now < cached.expiresAt) {\n return cached.origins\n }\n\n if (!__rorInflight.has(key)) {\n const body = JSON.stringify({\n jsonrpc: '2.0',\n id: String(Date.now()),\n method: 'query',\n params: {\n request_type: 'call_function',\n finality: 'final',\n account_id: contractId,\n method_name: method,\n args_base64: Buffer.from(JSON.stringify({})).toString('base64'),\n },\n })\n\n const p = (async () => {\n const resp = await fetch(rpcUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n })\n if (!resp.ok) {\n throw new Error(`RPC ${resp.status} ${resp.statusText}`)\n }\n const jsonResponse = await resp.json()\n const bytes: unknown = jsonResponse?.result?.result\n let parsed: unknown = []\n if (Array.isArray(bytes)) {\n const str = String.fromCharCode(...(bytes as number[]))\n try { parsed = JSON.parse(str) } catch { parsed = [] }\n }\n const origins = sanitizeOrigins(parsed as unknown[])\n __rorCache.set(key, { origins, expiresAt: Date.now() + ttl })\n return origins\n })()\n\n __rorInflight.set(key, p.finally(() => __rorInflight.delete(key)))\n }\n\n return __rorInflight.get(key) as Promise<string[]>\n}\n\n/**\n * Infer and set a proper Content-Type header for a given file path.\n * Shared by both app and wallet-iframe dev servers.\n */\nexport function setContentType(res: any, filePath: string) {\n const ext = path.extname(filePath)\n switch (ext) {\n case '.js':\n res.setHeader('Content-Type', 'application/javascript; charset=utf-8')\n break\n case '.css':\n res.setHeader('Content-Type', 'text/css; charset=utf-8')\n break\n case '.map':\n case '.json':\n res.setHeader('Content-Type', 'application/json; charset=utf-8')\n break\n case '.wasm':\n res.setHeader('Content-Type', 'application/wasm')\n break\n case '.html':\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n break\n default:\n res.setHeader('Content-Type', 'application/octet-stream')\n }\n}\n\n// === Shared path helpers across Vite/Next plugins ===\n\nexport { toBasePath } from '../utils/validation'\n\nconst requireCjs = createRequire(import.meta.url)\n\nexport function resolveSdkDistRoot(explicit?: string): string {\n if (explicit) return path.resolve(explicit)\n const pkgPath = requireCjs.resolve('@tatchi-xyz/sdk/package.json')\n const pkgDir = path.dirname(pkgPath)\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { module?: string }\n const esmEntry = pkgJson.module || 'dist/esm/index.js'\n const esmAbs = path.resolve(pkgDir, esmEntry)\n return path.resolve(path.dirname(esmAbs), '..')\n } catch {\n return path.join(pkgDir, 'dist')\n }\n}\n"],"mappings":"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"}
|