nanocodex 0.6.5 → 0.6.7

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.
Files changed (121) hide show
  1. package/README.md +311 -6
  2. package/THIRD-PARTY-LICENSES +23 -0
  3. package/browser/Agent.d.mts +5 -4
  4. package/browser/Agent.mjs +4 -0
  5. package/browser/Claude.d.mts +4 -0
  6. package/browser/Claude.mjs +11 -0
  7. package/browser/InlineAgent.mjs +19 -1
  8. package/browser/Voice.mjs +11 -4
  9. package/browser/VoiceSession.mjs +20 -6
  10. package/browser/host.d.mts +54 -1
  11. package/browser/host.mjs +65 -16
  12. package/browser/hostManagedWebSocket.mjs +11 -2
  13. package/browser/index.d.mts +3 -10
  14. package/browser/index.mjs +2 -5
  15. package/browser/socket-observations.mjs +203 -0
  16. package/cloud/Decorator.d.mts +2 -2
  17. package/cloud/Decorator.mjs +1 -0
  18. package/cloud/Dialog.d.mts +13 -0
  19. package/cloud/Dialog.mjs +52 -6
  20. package/cloud/Transport.d.mts +0 -11
  21. package/cloud/Transport.mjs +1 -386
  22. package/cloud/actions/account.d.mts +12 -0
  23. package/cloud/actions/account.mjs +11 -0
  24. package/cloud/actions/agent.mjs +3 -0
  25. package/cloud/actions/connection.d.mts +3 -1
  26. package/cloud/actions/connection.mjs +19 -4
  27. package/cloud/index.d.mts +1 -1
  28. package/cloud/index.mjs +1 -1
  29. package/cloud/internal.mjs +19 -4
  30. package/cloud/mercator.mjs +1 -1
  31. package/cloud/mock.d.mts +17 -0
  32. package/cloud/mock.mjs +392 -0
  33. package/cloud/types.d.mts +4 -1
  34. package/cloudflare/Agent.d.mts +6 -0
  35. package/cloudflare/Agent.mjs +190 -10
  36. package/cloudflare/agent-settings.d.mts +1 -1
  37. package/cloudflare/agent-settings.mjs +12 -3
  38. package/cloudflare/egress.mjs +9 -6
  39. package/cloudflare/gateway-binding-responses.mjs +3 -3
  40. package/cloudflare/gateway-responses.d.mts +1 -0
  41. package/cloudflare/gateway-responses.mjs +7 -7
  42. package/cloudflare/managed-auth.d.mts +4 -4
  43. package/cloudflare/managed-auth.mjs +20 -2
  44. package/cloudflare/managed-live.d.mts +6 -0
  45. package/cloudflare/managed-live.mjs +56 -1
  46. package/cloudflare/tracing.d.mts +16 -0
  47. package/cloudflare/tracing.mjs +47 -0
  48. package/cloudflare/workers-ai-responses.d.mts +3 -2
  49. package/cloudflare/workers-ai-responses.mjs +83 -8
  50. package/host/Agent.d.mts +6 -2
  51. package/host/Claude.d.mts +3 -0
  52. package/host/Claude.mjs +2 -0
  53. package/host/index.d.mts +6 -10
  54. package/host/index.mjs +2 -5
  55. package/index.d.mts +11 -10
  56. package/index.mjs +0 -5
  57. package/internal.mjs +33 -3
  58. package/managed/Agent.d.mts +10 -3
  59. package/managed/Agent.mjs +60 -10
  60. package/managed/ManagedError.d.mts +3 -0
  61. package/managed/Voice.mjs +3 -1
  62. package/managed/internal.mjs +2 -1
  63. package/node/Agent.d.mts +6 -2
  64. package/node/Agent.mjs +14 -1
  65. package/node/Claude.d.mts +4 -0
  66. package/node/Claude.mjs +24 -0
  67. package/node/host.mjs +18 -9
  68. package/node/index.d.mts +6 -10
  69. package/node/index.mjs +2 -5
  70. package/package.json +45 -9
  71. package/pkg-node/nanocodex.d.ts +123 -2
  72. package/pkg-node/nanocodex.js +879 -89
  73. package/pkg-node/nanocodex_bg.wasm.d.ts +49 -3
  74. package/pkg-web/.nanocodex-bindgen-stamp +4 -4
  75. package/pkg-web/nanocodex-build.json +1 -1
  76. package/pkg-web/nanocodex.d.ts +172 -5
  77. package/pkg-web/nanocodex.js +872 -88
  78. package/pkg-web/nanocodex_bg.js +872 -88
  79. package/pkg-web/nanocodex_bg.wasm +0 -0
  80. package/pkg-web/nanocodex_bg.wasm.d.ts +49 -3
  81. package/pkg-web/nanocodex_worker.js +1 -1
  82. package/runtime/claude-host.mjs +308 -0
  83. package/runtime/claude-subscription.mjs +79 -0
  84. package/runtime/claude.d.mts +112 -0
  85. package/runtime/claude.mjs +221 -0
  86. package/runtime/code-effect-identity.mjs +123 -0
  87. package/runtime/harnesses.mjs +51 -0
  88. package/runtime/mcp-payment.mjs +8 -0
  89. package/runtime/mcp-runtime.d.mts +10 -0
  90. package/runtime/mcp-runtime.mjs +242 -55
  91. package/runtime/mercator.mjs +2 -0
  92. package/runtime/quickjs-evaluator.mjs +23 -11
  93. package/runtime/response-controls.mjs +16 -10
  94. package/runtime/responses-http.mjs +4 -2
  95. package/runtime/retry-after.d.mts +10 -0
  96. package/runtime/retry-after.mjs +86 -0
  97. package/runtime/steer-receipt.mjs +8 -1
  98. package/runtime/subagent-routing.d.mts +4 -2
  99. package/runtime/subagent-routing.mjs +9 -3
  100. package/runtime/subagents.d.mts +8 -3
  101. package/services/account.d.mts +76 -0
  102. package/services/account.mjs +151 -0
  103. package/services/hosted.mjs +72 -0
  104. package/services/index.d.mts +95 -0
  105. package/services/index.mjs +192 -0
  106. package/services/scope.mjs +36 -0
  107. package/{runtime/tempo-provider.d.mts → tempo/index.d.mts} +20 -1
  108. package/{runtime/tempo-provider.mjs → tempo/index.mjs} +39 -3
  109. package/tools/browser/browserShell.mjs +18 -8
  110. package/tools/browser/python.worker.mjs +17 -1
  111. package/tools/browser/threadGit.mjs +12 -2
  112. package/tools/environment.d.mts +17 -0
  113. package/tools/environment.mjs +24 -1
  114. package/tools/index.d.mts +6 -3
  115. package/types.d.mts +107 -6
  116. package/worker/Claude.d.mts +4 -0
  117. package/worker/Claude.mjs +11 -0
  118. package/worker/ClaudeSubscription.d.mts +24 -0
  119. package/worker/ClaudeSubscription.mjs +9 -0
  120. package/worker/index.d.mts +9 -0
  121. package/worker/index.mjs +4 -0
@@ -0,0 +1,203 @@
1
+ // Passive, content-free observations. Timers report silence; they never cancel
2
+ // inference. Each socket retains only one active request and 32 response links.
3
+ export function createSocketObservations(observe) {
4
+ if (observe === undefined) return;
5
+ if (typeof observe !== "function") throw new TypeError("host socket event hook must be a function");
6
+ const turns = new Map();
7
+ const calls = new Map();
8
+ const sockets = new Set();
9
+ const emit = (value) => {
10
+ try {
11
+ const result = observe(value);
12
+ if (result?.then) void Promise.resolve(result).catch(() => {});
13
+ } catch { /* Observation cannot change transport behavior. */ }
14
+ };
15
+ const retain = (map, key, value, limit = 128) => {
16
+ if (!map.has(key) && map.size >= limit) map.delete(map.keys().next().value);
17
+ map.set(key, value);
18
+ };
19
+ return {
20
+ runtime(encoded) {
21
+ if (typeof encoded === "string" && !/"(?:input\.accepted|model\.(?:call|compaction|warmup)\.|run\.)/.test(encoded.slice(0, 512))) return;
22
+ let event;
23
+ try { event = typeof encoded === "string" ? JSON.parse(encoded) : encoded; } catch { return; }
24
+ const p = event?.payload;
25
+ if (!p) return;
26
+ if (event.type === "input.accepted" && typeof p.turn_id === "string" && typeof p.session_id === "string") {
27
+ retain(turns, p.turn_id, p.session_id);
28
+ }
29
+ const sessionId = turns.get(p.turn_id) ?? event.request_id;
30
+ if (event.type === "model.call.started" || event.type === "model.compaction.started" || event.type === "model.warmup.started") {
31
+ retain(calls, sessionId, { ...(Number.isSafeInteger(p.call_index ?? p.after_model_call_index)
32
+ ? { model_call_index: p.call_index ?? p.after_model_call_index } : {}),
33
+ phase: event.type.startsWith("model.compaction.") ? "compaction" : event.type.startsWith("model.warmup.") ? "warmup" : "generation" });
34
+ } else if (/^model\.(call|compaction|warmup)\.(completed|failed)$/.test(event.type)) {
35
+ for (const socket of sockets) {
36
+ if (socket.sessionId === sessionId) socket.finished(p.call_index ?? p.after_model_call_index, p.response_id, event.type.endsWith(".failed") ? "failed" : "completed");
37
+ }
38
+ calls.delete(sessionId);
39
+ }
40
+ },
41
+ release(sessionId) {
42
+ calls.delete(sessionId);
43
+ for (const [turn, session] of turns) if (session === sessionId) turns.delete(turn);
44
+ },
45
+ connect(sessionId, snapshot) {
46
+ const measure = () => { try { return snapshot(); } catch { return {}; } };
47
+ const socketId = crypto.randomUUID();
48
+ const began = performance.now();
49
+ const responses = new Map();
50
+ let handshake = {};
51
+ let active, last, timer, ordinal = 0, closed = false;
52
+ const record = (event, fields = {}, request = active ?? last) => emit({
53
+ event, socket_id: socketId, request_id: sessionId,
54
+ ...(uuid(handshake.egressRequestId) ? { egress_request_id: handshake.egressRequestId } : {}),
55
+ ...(identifier(handshake.requestId, "req_") || uuid(handshake.requestId) ? { provider_request_id: handshake.requestId } : {}),
56
+ ...(request ? { socket_request_index: request.index, ...request.context,
57
+ ...(request.responseId ? { response_id: request.responseId } : {}) } : {}),
58
+ ...fields,
59
+ });
60
+ const watch = (event, started) => {
61
+ clearTimeout(timer);
62
+ let after = 1_000;
63
+ const waiting = () => {
64
+ if (closed) return;
65
+ const now = performance.now();
66
+ record(event, { elapsed_ms: now - started,
67
+ ...(active ? { received_message_count: active.messages,
68
+ ...(active.firstMessage === undefined ? {} : { first_message_ms: active.firstMessage - active.started }),
69
+ ...milestones(active),
70
+ last_message_age_ms: now - (active.lastMessage ?? active.started), ...measure() } : {}) });
71
+ after = Math.min(after * 2, 5_000);
72
+ timer = setTimeout(waiting, after);
73
+ };
74
+ timer = setTimeout(waiting, after);
75
+ };
76
+ const responseLink = (request, id) => {
77
+ if (!request || !identifier(id, "resp_")) return;
78
+ request.responseId = id;
79
+ retain(responses, id, request, 32);
80
+ };
81
+ const finish = (outcome) => {
82
+ if (!active) return;
83
+ clearTimeout(timer);
84
+ record("request.finished", { outcome, elapsed_ms: performance.now() - active.started,
85
+ received_message_count: active.messages, ...milestones(active), ...measure() });
86
+ last = active;
87
+ active = undefined;
88
+ };
89
+ const socket = {
90
+ sessionId,
91
+ connected(value) {
92
+ handshake = value;
93
+ clearTimeout(timer);
94
+ record("socket.opened", { elapsed_ms: performance.now() - began });
95
+ },
96
+ sendStarted() {
97
+ finish("superseded");
98
+ active = { index: ++ordinal, context: calls.get(sessionId) ?? {}, started: performance.now(), messages: 0 };
99
+ record("request.send_started", measure());
100
+ watch("request.send_waiting", active.started);
101
+ },
102
+ sent() {
103
+ if (!active) return;
104
+ const now = performance.now();
105
+ record("request.sent", { send_wait_ms: now - active.started, ...measure() });
106
+ if (active.firstMessage === undefined) active.started = now;
107
+ active.sentAt = now;
108
+ watch("request.waiting", active.started);
109
+ },
110
+ sendFailed() { finish("send_failed"); },
111
+ message(text) {
112
+ const now = performance.now();
113
+ if (!active) return;
114
+ active.messages++;
115
+ active.lastMessage = now;
116
+ // Only parse bounded envelopes while a relevant milestone is missing.
117
+ // A large/unrecognized envelope remains unclassified, never copied to logs.
118
+ const candidates = typeof text === "string" && text.length <= 16_384
119
+ && (active.firstMessage === undefined || active.firstOutput === undefined
120
+ || DELTA_STAGES.some(([field, , types]) => active[field] === undefined
121
+ && types.some(type => text.includes(`"${type}"`))));
122
+ let frame;
123
+ if (candidates) {
124
+ try { frame = JSON.parse(text); } catch { /* Invalid frames remain the runtime's responsibility. */ }
125
+ }
126
+ const type = EVENT_TYPES.has(frame?.type) ? frame.type : "unclassified";
127
+ const classification = { provider_event_type: type };
128
+ if (type !== "unclassified") responseLink(active, frame?.response?.id);
129
+ // A new frame ends the current silence interval. Streaming calls
130
+ // produce no waiting records while frames keep arriving.
131
+ if (active.sentAt !== undefined) watch("request.waiting", active.started);
132
+ if (active.firstMessage === undefined) {
133
+ active.firstMessage = now;
134
+ record("request.first_message", { elapsed_ms: now - active.started, ...classification, ...measure() });
135
+ }
136
+ // Keep the historical output-item milestone, but identify what caused
137
+ // it. Empty item announcements are not evidence of generated text.
138
+ if (active.firstOutput === undefined && OUTPUT_EVENTS.has(type)) {
139
+ active.firstOutput = now;
140
+ record("request.first_output", { elapsed_ms: now - active.started, ...classification,
141
+ output_kind: OUTPUT_KINDS.get(type) ?? "item", ...measure() });
142
+ }
143
+ for (const [field, stage, types] of DELTA_STAGES) {
144
+ if (active[field] === undefined && types.includes(type) && typeof frame.delta === "string" && frame.delta.length > 0) {
145
+ active[field] = now;
146
+ record(stage, { elapsed_ms: now - active.started, ...classification, ...measure() });
147
+ }
148
+ }
149
+ },
150
+ provider(timing) {
151
+ const request = timing.response_id ? responses.get(timing.response_id) : active ?? last;
152
+ if (request?.timingReported) return;
153
+ if (request) request.timingReported = true;
154
+ record("provider.timing", timing, request ?? null);
155
+ },
156
+ finished(index, responseId, outcome) {
157
+ if (active?.context.model_call_index !== index) return;
158
+ responseLink(active, responseId);
159
+ finish(outcome);
160
+ },
161
+ close(event, intentional = false) {
162
+ if (closed) return;
163
+ closed = true;
164
+ clearTimeout(timer);
165
+ record("socket.closed", { elapsed_ms: performance.now() - began, intentional,
166
+ ...(Number.isInteger(event?.code) ? { close_code: event.code } : {}),
167
+ ...(typeof event?.wasClean === "boolean" ? { close_clean: event.wasClean } : {}),
168
+ ...(active ? { received_message_count: active.messages,
169
+ last_message_age_ms: performance.now() - (active.lastMessage ?? active.started) } : {}), ...measure() });
170
+ active = undefined;
171
+ sockets.delete(socket);
172
+ },
173
+ error() { record("socket.error", { elapsed_ms: performance.now() - began, ...measure() }); },
174
+ };
175
+ sockets.add(socket);
176
+ record("socket.connecting", {}, undefined);
177
+ watch("socket.connect_waiting", began);
178
+ return socket;
179
+ },
180
+ };
181
+ }
182
+
183
+ function uuid(value) { return typeof value === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(value); }
184
+ function identifier(value, prefix) { return typeof value === "string" && value.startsWith(prefix) && /^[A-Za-z0-9_-]{1,160}$/.test(value); }
185
+ const OUTPUT_EVENTS = new Set(["response.output_text.delta", "response.reasoning_summary_text.delta",
186
+ "response.reasoning_summary.delta", "response.reasoning_content.delta", "response.output_item.added", "response.output_item.done"]);
187
+
188
+ // These names are a fixed protocol allowlist, never provider-supplied log text.
189
+ const DELTA_STAGES = [
190
+ ["firstReasoning", "request.first_reasoning_delta", ["response.reasoning_text.delta", "response.reasoning_summary_text.delta",
191
+ "response.reasoning_summary.delta", "response.reasoning_content.delta"]],
192
+ ["firstAnswer", "request.first_answer_delta", ["response.output_text.delta"]],
193
+ ["firstTool", "request.first_tool_delta", ["response.function_call_arguments.delta", "response.custom_tool_call_input.delta"]],
194
+ ];
195
+ const OUTPUT_KINDS = new Map(DELTA_STAGES.flatMap(([, stage, types]) => types.map(type => [type,
196
+ stage === "request.first_reasoning_delta" ? "reasoning" : stage === "request.first_answer_delta" ? "answer" : "tool"])));
197
+ const EVENT_TYPES = new Set([...OUTPUT_EVENTS, ...OUTPUT_KINDS.keys(), "response.created", "response.in_progress", "response.queued",
198
+ "response.completed", "response.failed", "response.incomplete", "error", "responsesapi.websocket_timing"]);
199
+ function milestones(request) {
200
+ return Object.fromEntries([["firstOutput", "first_output_ms"], ["firstReasoning", "first_reasoning_delta_ms"],
201
+ ["firstAnswer", "first_answer_delta_ms"], ["firstTool", "first_tool_delta_ms"]]
202
+ .filter(([field]) => request[field] !== undefined).map(([field, key]) => [key, request[field] - request.started]));
203
+ }
@@ -1,4 +1,4 @@
1
- import type { logout } from "./actions/account.mjs";
1
+ import type { links, logout } from "./actions/account.mjs";
2
2
  import type { create as createAgent } from "./actions/agent.mjs";
3
3
  import type { request as connectorRequest } from "./actions/connector.mjs";
4
4
  import type { connect, disconnect, reconnect } from "./actions/connection.mjs";
@@ -10,7 +10,7 @@ import type { Client } from "./Client.mjs";
10
10
  import type { Connection, HostConnection } from "./types.mjs";
11
11
 
12
12
  export type ConnectActions<connection extends Connection | HostConnection = Connection> = {
13
- account: { logout(): logout.ReturnType };
13
+ account: { links(options?: links.Options): links.ReturnType; logout(): logout.ReturnType };
14
14
  agent: { create(options: createAgent.Options): Promise<createAgent.ReturnType> };
15
15
  connector: { request(options: connectorRequest.Options): connectorRequest.ReturnType };
16
16
  connectors: {
@@ -11,6 +11,7 @@ import * as mpp from "./actions/mpp.mjs";
11
11
  export function connectActions() {
12
12
  return (client) => ({
13
13
  account: {
14
+ links: (options) => account.links(client, options),
14
15
  logout: () => account.logout(client),
15
16
  },
16
17
  agent: {
@@ -1,5 +1,16 @@
1
1
  export const DEFAULT_HOST: "https://nanocodex.gakonst.workers.dev/connect-dialog/";
2
2
 
3
+ /** Visual tokens for the hosted dialog. Arbitrary CSS is not supported. */
4
+ export type Appearance = Readonly<{
5
+ theme?: "light" | "dark" | "system" | undefined;
6
+ /** Six-digit hex color, for example #635bff. */
7
+ accentColor?: `#${string}` | undefined;
8
+ /** Installed or self-hosted font list, at most 160 characters. No CSS functions. */
9
+ fontFamily?: string | undefined;
10
+ /** Control corner radius in pixels, from 0 to 24. */
11
+ borderRadius?: number | undefined;
12
+ }>;
13
+
3
14
  export type Request = ConnectionRequest | FundingRequest;
4
15
 
5
16
  export type ConnectionRequest = Readonly<{
@@ -93,12 +104,14 @@ export function from<const type extends string>(parameters: Readonly<{
93
104
  }>): Dialog<type>;
94
105
 
95
106
  export function iframe(options?: Readonly<{
107
+ appearance?: Appearance | undefined;
96
108
  host?: string | undefined;
97
109
  key?: string | undefined;
98
110
  name?: string | undefined;
99
111
  }>): Dialog<"iframe">;
100
112
 
101
113
  export function popup(options?: Readonly<{
114
+ appearance?: Appearance | undefined;
102
115
  host?: string | undefined;
103
116
  key?: string | undefined;
104
117
  name?: string | undefined;
package/cloud/Dialog.mjs CHANGED
@@ -2,6 +2,9 @@ import { DialogBusyError, UserRejectedRequestError } from "./Errors.mjs";
2
2
 
3
3
  export const DEFAULT_HOST = "https://nanocodex.gakonst.workers.dev/connect-dialog/";
4
4
 
5
+ const APPEARANCE_PARAMETER = "nanocodex_appearance";
6
+ const MAX_APPEARANCE_LENGTH = 1024;
7
+
5
8
  const iframeInstances = new Map();
6
9
  const popupInstances = new Map();
7
10
 
@@ -22,6 +25,7 @@ export function from(parameters) {
22
25
 
23
26
  export function iframe(options = {}) {
24
27
  const host = new URL(options.host ?? DEFAULT_HOST).toString();
28
+ const appearance = serializeAppearance(options.appearance);
25
29
  return from({
26
30
  key: options.key ?? "nanocodex-iframe",
27
31
  name: options.name ?? "Nanocodex Connect",
@@ -35,7 +39,7 @@ export function iframe(options = {}) {
35
39
  },
36
40
  };
37
41
  }
38
- const source = dialogUrl(host, "iframe", appId);
42
+ const source = dialogUrl(host, "iframe", appId, appearance);
39
43
  let instance = iframeInstances.get(source);
40
44
  if (!instance) {
41
45
  instance = createIframeInstance(source);
@@ -48,6 +52,7 @@ export function iframe(options = {}) {
48
52
 
49
53
  export function popup(options = {}) {
50
54
  const host = new URL(options.host ?? DEFAULT_HOST).toString();
55
+ const appearance = serializeAppearance(options.appearance);
51
56
  return from({
52
57
  key: options.key ?? "nanocodex-popup",
53
58
  name: options.name ?? "Nanocodex Connect",
@@ -61,7 +66,7 @@ export function popup(options = {}) {
61
66
  },
62
67
  };
63
68
  }
64
- const source = dialogUrl(host, "popup", appId);
69
+ const source = dialogUrl(host, "popup", appId, appearance);
65
70
  let instance = popupInstances.get(source);
66
71
  if (!instance) {
67
72
  instance = createPopupInstance(source, options);
@@ -141,7 +146,7 @@ function createIframeInstance(host) {
141
146
  if (frame && modal) return;
142
147
  modal = document.createElement("dialog");
143
148
  modal.setAttribute("aria-label", "Nanocodex Connect permissions");
144
- modal.style.cssText = "border:0;padding:0;background:transparent;max-width:none;max-height:none";
149
+ modal.style.cssText = "border:0;outline:0;padding:0;margin:0;background:transparent;max-width:none;max-height:none;width:100vw;height:100dvh";
145
150
  modal.addEventListener("cancel", (event) => {
146
151
  event.preventDefault();
147
152
  rejectActive(new UserRejectedRequestError());
@@ -161,7 +166,7 @@ function createIframeInstance(host) {
161
166
  `publickey-credentials-create ${dialogOrigin}`,
162
167
  "payment",
163
168
  ].join("; ");
164
- frame.style.cssText = "border:0;width:min(440px,calc(100vw - 24px));height:min(720px,calc(100vh - 24px));background:#161616";
169
+ frame.style.cssText = "border:0;width:100vw;height:100dvh;background:transparent";
165
170
  frame.style.display = "none";
166
171
  modal.append(frame);
167
172
  document.body.append(modal);
@@ -202,6 +207,8 @@ function createIframeInstance(host) {
202
207
  const url = new URL(nextHost ?? host);
203
208
  if (!url.searchParams.has("origin")) url.searchParams.set("origin", window.location.origin);
204
209
  url.searchParams.set("mode", "iframe");
210
+ // Accounts may supply a fresh host URL; retain this dialog's visual options.
211
+ setAppearance(url, new URL(host).searchParams.get(APPEARANCE_PARAMETER));
205
212
  const source = url.toString();
206
213
  if (walletFrame && walletModal && walletHost === source) return;
207
214
  walletModal?.remove();
@@ -319,7 +326,7 @@ function createIframeInstance(host) {
319
326
  function createPopupInstance(host, options) {
320
327
  const source = host;
321
328
  const targetName = options.target ?? "nanocodex-connect";
322
- const features = options.features ?? "popup=yes,width=440,height=720,resizable=yes,scrollbars=yes";
329
+ const features = options.features ?? "popup=yes,width=808,height=780,resizable=yes,scrollbars=yes";
323
330
  let walletWindow;
324
331
 
325
332
  function showWallet() {
@@ -350,11 +357,12 @@ function createPopupInstance(host, options) {
350
357
  };
351
358
  }
352
359
 
353
- function dialogUrl(host, mode, appId) {
360
+ function dialogUrl(host, mode, appId, appearance) {
354
361
  const url = new URL(host);
355
362
  url.searchParams.set("app_id", appId);
356
363
  url.searchParams.set("origin", window.location.origin);
357
364
  url.searchParams.set("mode", mode);
365
+ setAppearance(url, appearance);
358
366
  return url.toString();
359
367
  }
360
368
 
@@ -364,3 +372,41 @@ function requiredString(value, label) {
364
372
  }
365
373
  return value;
366
374
  }
375
+
376
+ function setAppearance(url, appearance) {
377
+ // Explicit SDK options own this parameter, including clearing stale host values.
378
+ url.searchParams.delete(APPEARANCE_PARAMETER);
379
+ if (appearance) url.searchParams.set(APPEARANCE_PARAMETER, appearance);
380
+ }
381
+
382
+ function serializeAppearance(appearance) {
383
+ if (appearance === undefined) return undefined;
384
+ if (!appearance || typeof appearance !== "object" || Array.isArray(appearance)) {
385
+ throw new TypeError("Dialog appearance must be an object");
386
+ }
387
+ const keys = ["theme", "accentColor", "fontFamily", "borderRadius"];
388
+ if (Object.keys(appearance).some((key) => !keys.includes(key))) {
389
+ throw new TypeError("Dialog appearance contains an unsupported option");
390
+ }
391
+ const { theme, accentColor, fontFamily, borderRadius } = appearance;
392
+ if (theme !== undefined && !["light", "dark", "system"].includes(theme)) {
393
+ throw new TypeError("Dialog appearance theme must be light, dark, or system");
394
+ }
395
+ if (accentColor !== undefined && (typeof accentColor !== "string" || !/^#[\da-f]{6}$/i.test(accentColor))) {
396
+ throw new TypeError("Dialog appearance accentColor must be a six-digit hex color");
397
+ }
398
+ if (fontFamily !== undefined && (typeof fontFamily !== "string" || fontFamily.length > 160
399
+ || !/^[a-zA-Z0-9 ,"'_-]+$/.test(fontFamily) || !fontFamily.trim())) {
400
+ throw new TypeError("Dialog appearance fontFamily must be a font list of at most 160 characters");
401
+ }
402
+ if (borderRadius !== undefined && (typeof borderRadius !== "number" || !Number.isFinite(borderRadius)
403
+ || borderRadius < 0 || borderRadius > 24)) {
404
+ throw new TypeError("Dialog appearance borderRadius must be between 0 and 24 pixels");
405
+ }
406
+ // Pick known scalar fields, never invoking a caller's toJSON or carrying CSS.
407
+ const serialized = JSON.stringify({ theme, accentColor, fontFamily, borderRadius });
408
+ if (serialized.length > MAX_APPEARANCE_LENGTH) {
409
+ throw new TypeError("Dialog appearance is too large");
410
+ }
411
+ return serialized;
412
+ }
@@ -1,6 +1,4 @@
1
1
  export const DEFAULT_API_URL: "https://api.nanocodex.xyz";
2
- export const MOCK_ACCOUNT_ADDRESS: `0x${string}`;
3
- export const MOCK_MACHINE_USD_ADDRESS: `0x${string}`;
4
2
 
5
3
  export type Request = Readonly<{
6
4
  path: string;
@@ -36,12 +34,3 @@ export function http(url?: string | undefined, options?: Readonly<{
36
34
  key?: string | undefined;
37
35
  name?: string | undefined;
38
36
  }>): Transport<"http">;
39
-
40
- export function mock(options?: Readonly<{
41
- accountAddress?: `0x${string}` | undefined;
42
- appName?: string | undefined;
43
- appOrigin?: string | undefined;
44
- key?: string | undefined;
45
- machineUsdAddress?: `0x${string}` | undefined;
46
- name?: string | undefined;
47
- }>): Transport<"mock">;