nanocodex 0.6.6 → 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 +297 -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/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 +3 -1
- package/cloudflare/Agent.d.mts +2 -0
- package/cloudflare/Agent.mjs +88 -2
- package/cloudflare/agent-settings.d.mts +1 -1
- package/cloudflare/agent-settings.mjs +9 -0
- package/cloudflare/egress.mjs +9 -6
- package/cloudflare/gateway-responses.d.mts +1 -0
- package/cloudflare/gateway-responses.mjs +2 -2
- package/cloudflare/managed-auth.d.mts +4 -4
- package/cloudflare/managed-auth.mjs +14 -2
- package/cloudflare/managed-live.d.mts +6 -0
- package/cloudflare/managed-live.mjs +56 -1
- package/cloudflare/workers-ai-responses.d.mts +2 -1
- package/cloudflare/workers-ai-responses.mjs +80 -6
- 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 +28 -3
- package/managed/Agent.d.mts +6 -2
- package/managed/Agent.mjs +39 -5
- package/managed/ManagedError.d.mts +3 -0
- 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 +41 -9
- package/pkg-node/nanocodex.d.ts +109 -0
- package/pkg-node/nanocodex.js +818 -51
- package/pkg-node/nanocodex_bg.wasm.d.ts +47 -3
- package/pkg-web/.nanocodex-bindgen-stamp +3 -3
- package/pkg-web/nanocodex-build.json +1 -1
- package/pkg-web/nanocodex.d.ts +156 -3
- package/pkg-web/nanocodex.js +813 -51
- package/pkg-web/nanocodex_bg.js +813 -51
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +47 -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 -57
- package/runtime/mercator.mjs +2 -0
- package/runtime/quickjs-evaluator.mjs +23 -11
- package/runtime/response-controls.mjs +1 -6
- 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 +7 -2
- 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 +8 -0
- package/tools/environment.mjs +10 -0
- package/tools/index.d.mts +6 -3
- package/types.d.mts +104 -4
- 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
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">;
|
package/cloud/Transport.mjs
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { HttpError
|
|
2
|
-
import { Hex, PersonalMessage } from "ox";
|
|
1
|
+
import { HttpError } from "./Errors.mjs";
|
|
3
2
|
|
|
4
3
|
export const DEFAULT_API_URL = "https://api.nanocodex.xyz";
|
|
5
|
-
export const MOCK_ACCOUNT_ADDRESS = "0x8ba1f109551bd432803012645ac136ddd64dba72";
|
|
6
|
-
export const MOCK_MACHINE_USD_ADDRESS = "0x20c0000000000000000000000000000000000001";
|
|
7
4
|
|
|
8
5
|
export function from(parameters) {
|
|
9
6
|
if (!parameters || typeof parameters !== "object") {
|
|
@@ -70,388 +67,6 @@ export function http(url = DEFAULT_API_URL, options = {}) {
|
|
|
70
67
|
});
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
export function mock(options = {}) {
|
|
74
|
-
const accountAddress = options.accountAddress ?? MOCK_ACCOUNT_ADDRESS;
|
|
75
|
-
const machineUsdAddress = options.machineUsdAddress ?? MOCK_MACHINE_USD_ADDRESS;
|
|
76
|
-
const appName = options.appName ?? "Acme Workspace";
|
|
77
|
-
const grants = new Map();
|
|
78
|
-
let sequence = 0;
|
|
79
|
-
|
|
80
|
-
return from({
|
|
81
|
-
key: options.key ?? "mock",
|
|
82
|
-
name: options.name ?? "Nanocodex mock",
|
|
83
|
-
type: "mock",
|
|
84
|
-
setup({ appId }) {
|
|
85
|
-
return {
|
|
86
|
-
baseUrl: "memory://nanocodex",
|
|
87
|
-
async request(request) {
|
|
88
|
-
if (request.method === "POST" && request.path === "/v1/connections") {
|
|
89
|
-
const authorization = request.body?.key_authorization;
|
|
90
|
-
const keyId = authorization?.keyId ?? authorization?.address;
|
|
91
|
-
if (typeof keyId !== "string") throw new InvalidResponseError("key authorization is missing keyId");
|
|
92
|
-
const expiry = authorization.expiry ?? Math.floor(Date.now() / 1000) + 30 * 86_400;
|
|
93
|
-
const connectors = requestedConnectors(request.body?.requested_connectors);
|
|
94
|
-
const mcpConnections = requestedMcpConnections(request.body?.requested_mcp_connections);
|
|
95
|
-
const grantId = mockHex(`${appId}:${accountAddress}:${keyId}`, 32);
|
|
96
|
-
const connectorConnections = mockConnectorConnections(connectors, grantId);
|
|
97
|
-
const wire = {
|
|
98
|
-
grant_token: `mock-grant-${grantId.slice(2)}`,
|
|
99
|
-
account_address: accountAddress,
|
|
100
|
-
agent_id: `agent_${grantId.slice(2, 14)}`,
|
|
101
|
-
grant: {
|
|
102
|
-
id: grantId,
|
|
103
|
-
permission: request.body?.permission ?? "agent.run",
|
|
104
|
-
status: "active",
|
|
105
|
-
expires_at: expiry,
|
|
106
|
-
capabilities: [
|
|
107
|
-
"nanocodex.agent",
|
|
108
|
-
"mercator.boost",
|
|
109
|
-
"mpp.mach",
|
|
110
|
-
...connectors,
|
|
111
|
-
...mcpConnections.map(({ id }) => `mcp:${id}`),
|
|
112
|
-
],
|
|
113
|
-
connector_connections: connectorConnections,
|
|
114
|
-
mcp_connections: mcpConnections,
|
|
115
|
-
},
|
|
116
|
-
access_key: {
|
|
117
|
-
address: keyId,
|
|
118
|
-
chain_id: String(authorization.chainId ?? 4217),
|
|
119
|
-
key_id: keyId,
|
|
120
|
-
key_type: authorization.keyType ?? "p256",
|
|
121
|
-
limits: authorization.limits ?? [{ token: machineUsdAddress, limit: "10000000", period: 86_400 }],
|
|
122
|
-
scopes: authorization.scopes ?? [],
|
|
123
|
-
witness: authorization.witness ?? mockHex(`${grantId}:witness`, 32),
|
|
124
|
-
expiry,
|
|
125
|
-
authorization: request.body?.signed_key_authorization ?? "0x01",
|
|
126
|
-
},
|
|
127
|
-
mpp: {
|
|
128
|
-
token: machineUsdAddress,
|
|
129
|
-
symbol: "MACH",
|
|
130
|
-
balance_status: "ready",
|
|
131
|
-
settlement_token: "0x20C000000000000000000000b9537d11c60E8b50",
|
|
132
|
-
settlement_symbol: "USDC.e",
|
|
133
|
-
settlement_balance_atomics: "0",
|
|
134
|
-
limit_atomics: "10000000",
|
|
135
|
-
max_per_request_atomics: "250000",
|
|
136
|
-
period: 86_400,
|
|
137
|
-
balance_atomics: "0",
|
|
138
|
-
spent_atomics: "0",
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
grants.set(grantId, wire);
|
|
142
|
-
return wire;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (request.method === "GET" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+$/)) {
|
|
146
|
-
return requiredGrant(grants, request.path.split("/")[3]);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (request.method === "POST" && request.path === "/v1/connections/prepare") {
|
|
150
|
-
const permission = request.body?.permission;
|
|
151
|
-
if (typeof permission !== "string" || permission.length === 0) {
|
|
152
|
-
throw new TypeError("connection permission must be a non-empty string");
|
|
153
|
-
}
|
|
154
|
-
const resources = request.body?.resources ?? [];
|
|
155
|
-
if (!Array.isArray(resources) || resources.some((resource) => typeof resource !== "string" || resource.length === 0)) {
|
|
156
|
-
throw new TypeError("connection resources must be non-empty strings");
|
|
157
|
-
}
|
|
158
|
-
const connectors = connectorResources(resources);
|
|
159
|
-
const id = `req_${++sequence}`;
|
|
160
|
-
const accessPolicy = request.body?.authorize_access_key;
|
|
161
|
-
const keyId = accessPolicy?.address ?? mockHex(`${appId}:${accountAddress}:${permission}:key`, 20);
|
|
162
|
-
const tokenLimit = accessPolicy?.limits?.find((limit) => limit.token === machineUsdAddress);
|
|
163
|
-
const expiry = accessPolicy?.expiry ?? Math.floor(Date.now() / 1000) + 30 * 86_400;
|
|
164
|
-
const dailyLimit = tokenLimit?.limit ?? "10000000";
|
|
165
|
-
const period = tokenLimit?.period ?? 86_400;
|
|
166
|
-
const appOrigin = options.appOrigin ?? "https://app.example";
|
|
167
|
-
const appUrl = new URL(appOrigin);
|
|
168
|
-
const nonce = mockHex(`${id}:siwe-nonce`, 8).slice(2);
|
|
169
|
-
const issuedAt = new Date().toISOString();
|
|
170
|
-
const authMessage = [
|
|
171
|
-
`${appUrl.host} wants you to sign in with your Ethereum account:`,
|
|
172
|
-
"0x0000000000000000000000000000000000000000",
|
|
173
|
-
"",
|
|
174
|
-
`${appName} wants to connect your Nanocodex agent.`,
|
|
175
|
-
"",
|
|
176
|
-
`URI: ${appUrl.origin}`,
|
|
177
|
-
"Version: 1",
|
|
178
|
-
"Chain ID: 4217",
|
|
179
|
-
`Nonce: ${nonce}`,
|
|
180
|
-
`Issued At: ${issuedAt}`,
|
|
181
|
-
...(resources.length > 0
|
|
182
|
-
? ["Resources:", ...resources.map((resource) => `- ${resource}`)]
|
|
183
|
-
: []),
|
|
184
|
-
].join("\n");
|
|
185
|
-
return {
|
|
186
|
-
request_id: id,
|
|
187
|
-
app: { id: appId, name: appName, origin: appOrigin },
|
|
188
|
-
account_address: accountAddress,
|
|
189
|
-
auth: {
|
|
190
|
-
message: authMessage,
|
|
191
|
-
resources,
|
|
192
|
-
},
|
|
193
|
-
permission: {
|
|
194
|
-
id: permission,
|
|
195
|
-
title: "Use your Nanocodex agent",
|
|
196
|
-
description: "Run an app-owned Nanocodex agent with your approved capabilities.",
|
|
197
|
-
connectors: [
|
|
198
|
-
{ id: "agent", name: "Nanocodex", detail: "Instantiate the real Nanocodex runtime in this app" },
|
|
199
|
-
{ id: "mercator", name: "BOOST with Mercator", detail: "Find and compose the right tools, paying per call through MPP" },
|
|
200
|
-
...connectors.map((provider) => ({
|
|
201
|
-
id: provider,
|
|
202
|
-
name: connectorName(provider),
|
|
203
|
-
detail: `Use the connected ${connectorName(provider)} account through the grant`,
|
|
204
|
-
})),
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
access_key: {
|
|
208
|
-
address: keyId,
|
|
209
|
-
chain_id: accessPolicy?.chainId ?? "4217",
|
|
210
|
-
key_id: keyId,
|
|
211
|
-
public_key: accessPolicy?.publicKey ?? mockHex(`${id}:public`, 65),
|
|
212
|
-
key_type: accessPolicy?.keyType ?? "p256",
|
|
213
|
-
limits: accessPolicy?.limits ?? [{ token: machineUsdAddress, limit: dailyLimit, period }],
|
|
214
|
-
scopes: accessPolicy?.scopes ?? [],
|
|
215
|
-
witness: PersonalMessage.getSignPayload(Hex.fromString(authMessage)),
|
|
216
|
-
expiry,
|
|
217
|
-
},
|
|
218
|
-
mpp: {
|
|
219
|
-
token: machineUsdAddress,
|
|
220
|
-
symbol: "MACH",
|
|
221
|
-
settlement_token: "0x20C000000000000000000000b9537d11c60E8b50",
|
|
222
|
-
settlement_symbol: "USDC.e",
|
|
223
|
-
settlement_balance_atomics: "0",
|
|
224
|
-
limit_atomics: dailyLimit,
|
|
225
|
-
period,
|
|
226
|
-
max_per_request_atomics: "250000",
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (request.method === "POST" && request.path === "/v1/connections/complete") {
|
|
232
|
-
const prepared = request.body?.prepared;
|
|
233
|
-
const approval = request.body?.approval;
|
|
234
|
-
if (!prepared || !approval || approval.approved !== true) {
|
|
235
|
-
throw new InvalidResponseError("connection approval is malformed");
|
|
236
|
-
}
|
|
237
|
-
const approvedCapabilities = approval.capabilities;
|
|
238
|
-
if (
|
|
239
|
-
approvedCapabilities?.keyAuthorization?.witness !== prepared.access_key.witness
|
|
240
|
-
|| approvedCapabilities?.personalSign?.message !== prepared.auth.message
|
|
241
|
-
|| typeof approvedCapabilities?.personalSign?.keyAuthorization !== "string"
|
|
242
|
-
|| typeof approvedCapabilities?.signature !== "string"
|
|
243
|
-
) {
|
|
244
|
-
throw new InvalidResponseError("connection approval is not bound to the SIWE challenge and key authorization");
|
|
245
|
-
}
|
|
246
|
-
const grantId = mockHex(`${prepared.app.id}:${prepared.account_address}:${prepared.access_key.key_id}`, 32);
|
|
247
|
-
const wire = {
|
|
248
|
-
grant_token: `mock-grant-${grantId.slice(2)}`,
|
|
249
|
-
account_address: prepared.account_address,
|
|
250
|
-
agent_id: `agent_${grantId.slice(2, 14)}`,
|
|
251
|
-
grant: {
|
|
252
|
-
id: grantId,
|
|
253
|
-
permission: prepared.permission.id,
|
|
254
|
-
status: "active",
|
|
255
|
-
expires_at: prepared.access_key.expiry,
|
|
256
|
-
capabilities: prepared.permission.connectors.map(({ id }) => id),
|
|
257
|
-
},
|
|
258
|
-
access_key: prepared.access_key,
|
|
259
|
-
mpp: {
|
|
260
|
-
...prepared.mpp,
|
|
261
|
-
balance_atomics: "0",
|
|
262
|
-
balance_status: "ready",
|
|
263
|
-
spent_atomics: "0",
|
|
264
|
-
},
|
|
265
|
-
};
|
|
266
|
-
grants.set(grantId, wire);
|
|
267
|
-
return wire;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (request.method === "POST" && request.path === "/v1/connections/disconnect") {
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (request.method === "GET" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/mpp\/balance$/)) {
|
|
275
|
-
const wire = requiredGrant(grants, request.path.split("/")[3]);
|
|
276
|
-
wire.mpp = { ...wire.mpp, balance_status: "ready" };
|
|
277
|
-
return wire;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (request.method === "POST" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/revoke$/)) {
|
|
281
|
-
const grantId = request.path.split("/")[3];
|
|
282
|
-
const wire = requiredGrant(grants, grantId);
|
|
283
|
-
wire.grant = { ...wire.grant, status: "revoked" };
|
|
284
|
-
return wire.grant;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (request.method === "GET" && request.path === "/v1/machine-usd/config") {
|
|
288
|
-
return {
|
|
289
|
-
chain_id: 4217,
|
|
290
|
-
min_usd_amount_cents: 500,
|
|
291
|
-
max_usd_amount_cents: 10_000,
|
|
292
|
-
stripe_publishable_key: "pk_test_nanocodex",
|
|
293
|
-
token_address: machineUsdAddress,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (request.method === "POST" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/fund$/)) {
|
|
298
|
-
const grantId = request.path.split("/")[3];
|
|
299
|
-
const wire = requiredActiveGrant(grants, grantId);
|
|
300
|
-
const usdAmountCents = request.body?.usd_amount_cents;
|
|
301
|
-
if (!Number.isSafeInteger(usdAmountCents) || usdAmountCents < 500 || usdAmountCents > 10_000) {
|
|
302
|
-
throw new TypeError("MACH amount must be from 500 through 10000 cents");
|
|
303
|
-
}
|
|
304
|
-
const amount = BigInt(usdAmountCents) * 10_000n;
|
|
305
|
-
wire.mpp = {
|
|
306
|
-
...wire.mpp,
|
|
307
|
-
balance_atomics: String(BigInt(wire.mpp.balance_atomics) + amount),
|
|
308
|
-
};
|
|
309
|
-
return {
|
|
310
|
-
order: {
|
|
311
|
-
id: `ord_${++sequence}`,
|
|
312
|
-
status: "complete",
|
|
313
|
-
usd_amount_cents: usdAmountCents,
|
|
314
|
-
machine_usd_amount_atomics: String(amount),
|
|
315
|
-
issuance_transaction_hash: mockHex(`fund:${sequence}`, 32),
|
|
316
|
-
},
|
|
317
|
-
connection: wire,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (request.method === "POST" && request.path.match(/^\/v1\/grants\/0x[0-9a-f]+\/mpp\/charge$/)) {
|
|
322
|
-
const grantId = request.path.split("/")[3];
|
|
323
|
-
const wire = requiredActiveGrant(grants, grantId);
|
|
324
|
-
const amount = BigInt(request.body?.amount_atomics ?? "-1");
|
|
325
|
-
const max = BigInt(wire.mpp.max_per_request_atomics);
|
|
326
|
-
const spent = BigInt(wire.mpp.spent_atomics);
|
|
327
|
-
const limit = BigInt(wire.mpp.limit_atomics);
|
|
328
|
-
const balance = BigInt(wire.mpp.balance_atomics);
|
|
329
|
-
if (amount <= 0n) throw new TypeError("MPP amount must be positive");
|
|
330
|
-
if (amount > max) throw new HttpError(403, "This payment exceeds the per-request permission.", { code: "mpp_request_limit_exceeded" });
|
|
331
|
-
if (spent + amount > limit) throw new HttpError(403, "This payment exceeds the daily MPP permission.", { code: "mpp_period_limit_exceeded" });
|
|
332
|
-
if (amount > balance) throw new HttpError(402, "Add MACH before paying for this capability.", { code: "machine_usd_required" });
|
|
333
|
-
wire.mpp = {
|
|
334
|
-
...wire.mpp,
|
|
335
|
-
balance_atomics: String(balance - amount),
|
|
336
|
-
spent_atomics: String(spent + amount),
|
|
337
|
-
};
|
|
338
|
-
return {
|
|
339
|
-
receipt: {
|
|
340
|
-
id: `mpp_${++sequence}`,
|
|
341
|
-
amount_atomics: String(amount),
|
|
342
|
-
origin: request.body?.origin,
|
|
343
|
-
transaction_hash: mockHex(`mpp:${sequence}`, 32),
|
|
344
|
-
},
|
|
345
|
-
connection: wire,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
throw new HttpError(404, `No mock route for ${request.method ?? "GET"} ${request.path}`, {
|
|
350
|
-
code: "mock_route_not_found",
|
|
351
|
-
});
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function requiredGrant(grants, grantId) {
|
|
359
|
-
const grant = grants.get(grantId);
|
|
360
|
-
if (!grant) throw new HttpError(404, "Nanocodex grant not found", { code: "grant_not_found" });
|
|
361
|
-
return grant;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function requestedConnectors(value) {
|
|
365
|
-
if (value === undefined) return [];
|
|
366
|
-
if (!Array.isArray(value)) throw new TypeError("requested_connectors must be an array");
|
|
367
|
-
const supported = [
|
|
368
|
-
"github",
|
|
369
|
-
"gmail",
|
|
370
|
-
"gdrive",
|
|
371
|
-
"gcalendar",
|
|
372
|
-
"gtasks",
|
|
373
|
-
"gdocs",
|
|
374
|
-
"gsheets",
|
|
375
|
-
"gslides",
|
|
376
|
-
"gcontacts",
|
|
377
|
-
"slack",
|
|
378
|
-
"x",
|
|
379
|
-
"spotify",
|
|
380
|
-
"soundcloud",
|
|
381
|
-
"link",
|
|
382
|
-
"chatgpt",
|
|
383
|
-
];
|
|
384
|
-
if (value.some((provider) => !supported.includes(provider))) {
|
|
385
|
-
throw new TypeError("requested_connectors contains an unsupported provider");
|
|
386
|
-
}
|
|
387
|
-
return supported.filter((provider) => value.includes(provider));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function requestedMcpConnections(value) {
|
|
391
|
-
if (value === undefined) return [];
|
|
392
|
-
if (!Array.isArray(value) || value.length > 16
|
|
393
|
-
|| value.some((id) => typeof id !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(id))
|
|
394
|
-
|| new Set(value).size !== value.length) {
|
|
395
|
-
throw new TypeError("requested_mcp_connections must contain exact hosted connection IDs");
|
|
396
|
-
}
|
|
397
|
-
return value.map((id) => ({ id, name: "MCP connection" }));
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function connectorResources(resources) {
|
|
401
|
-
return requestedConnectors(resources
|
|
402
|
-
.filter((resource) => resource.startsWith("urn:nanocodex:connector:"))
|
|
403
|
-
.map((resource) => resource.slice("urn:nanocodex:connector:".length)));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function connectorName(provider) {
|
|
407
|
-
return ({
|
|
408
|
-
github: "GitHub",
|
|
409
|
-
gmail: "Gmail",
|
|
410
|
-
gdrive: "Google Drive",
|
|
411
|
-
gcalendar: "Google Calendar",
|
|
412
|
-
gtasks: "Google Tasks",
|
|
413
|
-
gdocs: "Google Docs",
|
|
414
|
-
gsheets: "Google Sheets",
|
|
415
|
-
gslides: "Google Slides",
|
|
416
|
-
gcontacts: "Google Contacts",
|
|
417
|
-
slack: "Slack",
|
|
418
|
-
x: "X",
|
|
419
|
-
spotify: "Spotify",
|
|
420
|
-
soundcloud: "SoundCloud",
|
|
421
|
-
link: "Stripe Link",
|
|
422
|
-
chatgpt: "ChatGPT",
|
|
423
|
-
})[provider];
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function mockConnectorConnections(connectors, grantId) {
|
|
427
|
-
return Object.fromEntries(connectors
|
|
428
|
-
.filter((connector) => connector !== "chatgpt")
|
|
429
|
-
.map((connector) => [
|
|
430
|
-
connector,
|
|
431
|
-
[mockHex(`${grantId}:${connector}:connection`, 32).slice(2, 45)],
|
|
432
|
-
]));
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function requiredActiveGrant(grants, grantId) {
|
|
436
|
-
const grant = requiredGrant(grants, grantId);
|
|
437
|
-
if (grant.grant.status !== "active") {
|
|
438
|
-
throw new HttpError(403, "This Nanocodex grant has been revoked.", { code: "grant_revoked" });
|
|
439
|
-
}
|
|
440
|
-
return grant;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function mockHex(seed, bytes) {
|
|
444
|
-
let state = 2166136261;
|
|
445
|
-
let output = "";
|
|
446
|
-
for (let index = 0; output.length < bytes * 2; index += 1) {
|
|
447
|
-
const code = seed.charCodeAt(index % seed.length) + index;
|
|
448
|
-
state ^= code;
|
|
449
|
-
state = Math.imul(state, 16777619) >>> 0;
|
|
450
|
-
output += state.toString(16).padStart(8, "0");
|
|
451
|
-
}
|
|
452
|
-
return `0x${output.slice(0, bytes * 2)}`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
70
|
function requiredString(value, label) {
|
|
456
71
|
if (typeof value !== "string" || value.length === 0) {
|
|
457
72
|
throw new TypeError(`${label} must be a non-empty string`);
|
|
@@ -7,3 +7,15 @@ export declare namespace logout {
|
|
|
7
7
|
|
|
8
8
|
/** Signs out the Nanocodex account without revoking its app grant or access key. */
|
|
9
9
|
export function logout(client: Client): logout.ReturnType;
|
|
10
|
+
|
|
11
|
+
export declare namespace links {
|
|
12
|
+
type Options = Readonly<{
|
|
13
|
+
connect?: "claude" | "chatgpt" | "openai" | "mcp" | "cloudflare" | "github" | "google" | "slack" | "x" | "spotify" | "soundcloud" | "link" | "whatsapp" | undefined;
|
|
14
|
+
add?: "login" | "api_key" | "card" | "address" | "phone" | "totp" | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
type Result = Readonly<{ connections: string; vault: string; wallet: string; access: string }>;
|
|
17
|
+
type ReturnType = Promise<Result>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Public navigation only; these URLs do not carry account authorization. */
|
|
21
|
+
export function links(client: Client, options?: links.Options): links.ReturnType;
|
|
@@ -17,3 +17,14 @@ export async function logout(client) {
|
|
|
17
17
|
const cleanupFailure = cleanup.find((result) => result.status === "rejected");
|
|
18
18
|
if (cleanupFailure?.status === "rejected") throw cleanupFailure.reason;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/** Discovers ordinary account navigation URLs; opening them uses browser sign-in. */
|
|
22
|
+
export function links(client, options = {}) {
|
|
23
|
+
const query = new URLSearchParams();
|
|
24
|
+
for (const key of Object.keys(options)) {
|
|
25
|
+
if (key !== "connect" && key !== "add") throw new TypeError(`Unknown account links option: ${key}`);
|
|
26
|
+
if (options[key] !== undefined) query.set(key, options[key]);
|
|
27
|
+
}
|
|
28
|
+
const suffix = query.size ? `?${query}` : "";
|
|
29
|
+
return client.request({ method: "GET", path: `/v1/account/links${suffix}` });
|
|
30
|
+
}
|
package/cloud/actions/agent.mjs
CHANGED
|
@@ -17,6 +17,9 @@ export async function create(client, options) {
|
|
|
17
17
|
if (!connection || typeof connection !== "object") {
|
|
18
18
|
throw new TypeError("agent.create requires an active connection");
|
|
19
19
|
}
|
|
20
|
+
if (connection.grant?.permission === "services.use" || !connection.agentId) {
|
|
21
|
+
throw new Error("This Connect grant has no agent authority.");
|
|
22
|
+
}
|
|
20
23
|
if (connection.grant?.status !== "active") {
|
|
21
24
|
throw new Error("The Connect authorization is not active.");
|
|
22
25
|
}
|
|
@@ -47,6 +47,8 @@ export type AuthorizeAccessKey = Readonly<{
|
|
|
47
47
|
}>;
|
|
48
48
|
|
|
49
49
|
export type Capabilities = Readonly<{
|
|
50
|
+
/** Exact standalone Vault and phone authority. */
|
|
51
|
+
services?: import("../../services/index.mjs").ServiceCapabilities | undefined;
|
|
50
52
|
/** SIWE authentication folded into the same passkey ceremony. */
|
|
51
53
|
auth?: Auth | undefined;
|
|
52
54
|
/** Access key authorized by the same passkey ceremony. */
|
|
@@ -111,7 +113,7 @@ export function disconnect(client: Client, options?: disconnect.Options | undefi
|
|
|
111
113
|
export declare namespace reconnect {
|
|
112
114
|
type Options = Readonly<{
|
|
113
115
|
/** Reject a retained grant outside these app capability boundaries. */
|
|
114
|
-
capabilities?: Pick<Capabilities, "agent" | "cloudAccounts"> | undefined;
|
|
116
|
+
capabilities?: Pick<Capabilities, "agent" | "cloudAccounts" | "services"> | undefined;
|
|
115
117
|
/** Defaults to the retained connection mode, then to hosted. */
|
|
116
118
|
authorization?: "access_key" | "hosted" | undefined;
|
|
117
119
|
tools?: readonly NamedTool[] | undefined;
|