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.
- package/README.md +311 -6
- package/THIRD-PARTY-LICENSES +23 -0
- package/browser/Agent.d.mts +5 -4
- package/browser/Agent.mjs +4 -0
- package/browser/Claude.d.mts +4 -0
- package/browser/Claude.mjs +11 -0
- package/browser/InlineAgent.mjs +19 -1
- package/browser/Voice.mjs +11 -4
- package/browser/VoiceSession.mjs +20 -6
- package/browser/host.d.mts +54 -1
- package/browser/host.mjs +65 -16
- package/browser/hostManagedWebSocket.mjs +11 -2
- package/browser/index.d.mts +3 -10
- package/browser/index.mjs +2 -5
- package/browser/socket-observations.mjs +203 -0
- package/cloud/Decorator.d.mts +2 -2
- package/cloud/Decorator.mjs +1 -0
- package/cloud/Dialog.d.mts +13 -0
- package/cloud/Dialog.mjs +52 -6
- package/cloud/Transport.d.mts +0 -11
- package/cloud/Transport.mjs +1 -386
- package/cloud/actions/account.d.mts +12 -0
- package/cloud/actions/account.mjs +11 -0
- package/cloud/actions/agent.mjs +3 -0
- package/cloud/actions/connection.d.mts +3 -1
- package/cloud/actions/connection.mjs +19 -4
- package/cloud/index.d.mts +1 -1
- package/cloud/index.mjs +1 -1
- package/cloud/internal.mjs +19 -4
- package/cloud/mercator.mjs +1 -1
- package/cloud/mock.d.mts +17 -0
- package/cloud/mock.mjs +392 -0
- package/cloud/types.d.mts +4 -1
- package/cloudflare/Agent.d.mts +6 -0
- package/cloudflare/Agent.mjs +190 -10
- package/cloudflare/agent-settings.d.mts +1 -1
- package/cloudflare/agent-settings.mjs +12 -3
- package/cloudflare/egress.mjs +9 -6
- package/cloudflare/gateway-binding-responses.mjs +3 -3
- package/cloudflare/gateway-responses.d.mts +1 -0
- package/cloudflare/gateway-responses.mjs +7 -7
- package/cloudflare/managed-auth.d.mts +4 -4
- package/cloudflare/managed-auth.mjs +20 -2
- package/cloudflare/managed-live.d.mts +6 -0
- package/cloudflare/managed-live.mjs +56 -1
- package/cloudflare/tracing.d.mts +16 -0
- package/cloudflare/tracing.mjs +47 -0
- package/cloudflare/workers-ai-responses.d.mts +3 -2
- package/cloudflare/workers-ai-responses.mjs +83 -8
- package/host/Agent.d.mts +6 -2
- package/host/Claude.d.mts +3 -0
- package/host/Claude.mjs +2 -0
- package/host/index.d.mts +6 -10
- package/host/index.mjs +2 -5
- package/index.d.mts +11 -10
- package/index.mjs +0 -5
- package/internal.mjs +33 -3
- package/managed/Agent.d.mts +10 -3
- package/managed/Agent.mjs +60 -10
- package/managed/ManagedError.d.mts +3 -0
- package/managed/Voice.mjs +3 -1
- package/managed/internal.mjs +2 -1
- package/node/Agent.d.mts +6 -2
- package/node/Agent.mjs +14 -1
- package/node/Claude.d.mts +4 -0
- package/node/Claude.mjs +24 -0
- package/node/host.mjs +18 -9
- package/node/index.d.mts +6 -10
- package/node/index.mjs +2 -5
- package/package.json +45 -9
- package/pkg-node/nanocodex.d.ts +123 -2
- package/pkg-node/nanocodex.js +879 -89
- package/pkg-node/nanocodex_bg.wasm.d.ts +49 -3
- package/pkg-web/.nanocodex-bindgen-stamp +4 -4
- package/pkg-web/nanocodex-build.json +1 -1
- package/pkg-web/nanocodex.d.ts +172 -5
- package/pkg-web/nanocodex.js +872 -88
- package/pkg-web/nanocodex_bg.js +872 -88
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +49 -3
- package/pkg-web/nanocodex_worker.js +1 -1
- package/runtime/claude-host.mjs +308 -0
- package/runtime/claude-subscription.mjs +79 -0
- package/runtime/claude.d.mts +112 -0
- package/runtime/claude.mjs +221 -0
- package/runtime/code-effect-identity.mjs +123 -0
- package/runtime/harnesses.mjs +51 -0
- package/runtime/mcp-payment.mjs +8 -0
- package/runtime/mcp-runtime.d.mts +10 -0
- package/runtime/mcp-runtime.mjs +242 -55
- package/runtime/mercator.mjs +2 -0
- package/runtime/quickjs-evaluator.mjs +23 -11
- package/runtime/response-controls.mjs +16 -10
- package/runtime/responses-http.mjs +4 -2
- package/runtime/retry-after.d.mts +10 -0
- package/runtime/retry-after.mjs +86 -0
- package/runtime/steer-receipt.mjs +8 -1
- package/runtime/subagent-routing.d.mts +4 -2
- package/runtime/subagent-routing.mjs +9 -3
- package/runtime/subagents.d.mts +8 -3
- package/services/account.d.mts +76 -0
- package/services/account.mjs +151 -0
- package/services/hosted.mjs +72 -0
- package/services/index.d.mts +95 -0
- package/services/index.mjs +192 -0
- package/services/scope.mjs +36 -0
- package/{runtime/tempo-provider.d.mts → tempo/index.d.mts} +20 -1
- package/{runtime/tempo-provider.mjs → tempo/index.mjs} +39 -3
- package/tools/browser/browserShell.mjs +18 -8
- package/tools/browser/python.worker.mjs +17 -1
- package/tools/browser/threadGit.mjs +12 -2
- package/tools/environment.d.mts +17 -0
- package/tools/environment.mjs +24 -1
- package/tools/index.d.mts +6 -3
- package/types.d.mts +107 -6
- package/worker/Claude.d.mts +4 -0
- package/worker/Claude.mjs +11 -0
- package/worker/ClaudeSubscription.d.mts +24 -0
- package/worker/ClaudeSubscription.mjs +9 -0
- package/worker/index.d.mts +9 -0
- 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
|
+
}
|
package/cloud/Decorator.d.mts
CHANGED
|
@@ -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: {
|
package/cloud/Decorator.mjs
CHANGED
package/cloud/Dialog.d.mts
CHANGED
|
@@ -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:
|
|
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=
|
|
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
|
+
}
|
package/cloud/Transport.d.mts
CHANGED
|
@@ -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">;
|