neozip-mcp 0.1.0-beta
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/.cursor/mcp.json.global.example +10 -0
- package/CHANGELOG.md +16 -0
- package/DOCUMENTATION.md +40 -0
- package/LICENSE +16 -0
- package/README.md +223 -0
- package/SECURITY.md +37 -0
- package/dist/account/account-state.js +86 -0
- package/dist/account/format-account-status.js +37 -0
- package/dist/account/identity-provision.js +75 -0
- package/dist/account/identity-wrap.js +69 -0
- package/dist/account/profile-crypto.js +47 -0
- package/dist/account/profile-store.js +108 -0
- package/dist/account/require-account.js +29 -0
- package/dist/account/token-service-identity.js +395 -0
- package/dist/account/types.js +2 -0
- package/dist/account/wallet-evm.js +39 -0
- package/dist/archive/blockchain-status.js +303 -0
- package/dist/archive/crypto-self.js +114 -0
- package/dist/archive/detect-text.js +56 -0
- package/dist/archive/embed-metadata.js +283 -0
- package/dist/archive/encryption.js +166 -0
- package/dist/archive/extract-entry-buffer.js +18 -0
- package/dist/archive/find-entry.js +21 -0
- package/dist/archive/grep-content.js +141 -0
- package/dist/archive/identity-key.js +176 -0
- package/dist/archive/manifest.js +55 -0
- package/dist/archive/merkle.js +31 -0
- package/dist/archive/metadata-paths.js +14 -0
- package/dist/archive/mint-archive.js +61 -0
- package/dist/archive/open-archive.js +23 -0
- package/dist/archive/read-entry-buffer.js +11 -0
- package/dist/archive/read-entry-content.js +51 -0
- package/dist/archive/recipient-access.js +26 -0
- package/dist/archive/recipient-decrypt.js +21 -0
- package/dist/archive/recipient-lookup.js +55 -0
- package/dist/archive/timestamp-network.js +54 -0
- package/dist/config/capabilities.js +37 -0
- package/dist/config/index.js +74 -0
- package/dist/connect-cli.js +312 -0
- package/dist/connection/coordinator.js +74 -0
- package/dist/connection/credentials.js +29 -0
- package/dist/connection/crypto.js +56 -0
- package/dist/connection/dump.js +79 -0
- package/dist/connection/incomplete-setup.js +81 -0
- package/dist/connection/interactive.js +814 -0
- package/dist/connection/legacy-profile-reader.js +47 -0
- package/dist/connection/magic-link.js +138 -0
- package/dist/connection/migrate.js +76 -0
- package/dist/connection/onboarding.js +524 -0
- package/dist/connection/origin.js +63 -0
- package/dist/connection/phase.js +93 -0
- package/dist/connection/phone.js +20 -0
- package/dist/connection/promote-active.js +53 -0
- package/dist/connection/reset.js +20 -0
- package/dist/connection/setup-guidance.js +154 -0
- package/dist/connection/status-report.js +40 -0
- package/dist/connection/store.js +352 -0
- package/dist/connection/token-auth.js +42 -0
- package/dist/connection/types.js +2 -0
- package/dist/connection/wallet-setup.js +70 -0
- package/dist/constants/wallet-identity.js +11 -0
- package/dist/index.js +47 -0
- package/dist/load-env.js +16 -0
- package/dist/neozipkit-node.js +11 -0
- package/dist/register/resources.js +14 -0
- package/dist/register/tools.js +77 -0
- package/dist/resources/zip-resource.js +40 -0
- package/dist/resources/zip-uri.js +23 -0
- package/dist/security/auth.js +28 -0
- package/dist/security/capabilities.js +85 -0
- package/dist/security/rate-limiter.js +43 -0
- package/dist/security/resource-limiter.js +44 -0
- package/dist/security/sandbox.js +61 -0
- package/dist/server.js +32 -0
- package/dist/startup-account-gate.js +101 -0
- package/dist/startup-summary.js +40 -0
- package/dist/token-service/require-configured.js +23 -0
- package/dist/tools/account.js +504 -0
- package/dist/tools/compress.js +237 -0
- package/dist/tools/connect-status.js +143 -0
- package/dist/tools/extract.js +62 -0
- package/dist/tools/grep-entries.js +42 -0
- package/dist/tools/identity-status.js +157 -0
- package/dist/tools/info.js +147 -0
- package/dist/tools/list.js +118 -0
- package/dist/tools/lookup-recipient.js +37 -0
- package/dist/tools/mint.js +41 -0
- package/dist/tools/read-entry.js +35 -0
- package/dist/tools/search-entries.js +71 -0
- package/dist/tools/stamp.js +60 -0
- package/dist/tools/test.js +90 -0
- package/dist/tools/token-service-account.js +143 -0
- package/dist/tools/upgrade.js +60 -0
- package/dist/tools/verify.js +75 -0
- package/dist/tools/wallet-config-status.js +119 -0
- package/dist/tools/wallet-info.js +64 -0
- package/dist/translators/index.js +106 -0
- package/dist/types/index.js +7 -0
- package/dist/util/mask.js +30 -0
- package/dist/util/token-service-fetch.js +23 -0
- package/dist/vendor/neozipkit-pro.js +3 -0
- package/docs/NEOZIP_CONNECTION_STORE.md +238 -0
- package/docs/NEOZIP_CONNECT_CLI.md +185 -0
- package/docs/OPERATIONS.md +992 -0
- package/docs/examples/CLAUDE.md.example +22 -0
- package/docs/examples/claude/skills/neozip-mcp/SKILL.md +54 -0
- package/docs/examples/claude/skills/neozip-notarization/SKILL.md +75 -0
- package/docs/examples/mcp.json.claude.example +11 -0
- package/docs/examples/neozip-mcp-cursor-rule.mdc +31 -0
- package/docs/installation-guides/INSTALL_CLAUDE_CODE.md +286 -0
- package/docs/installation-guides/INSTALL_CLAUDE_WORKSPACE.md +301 -0
- package/docs/installation-guides/README.md +76 -0
- package/package.json +99 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { applyActiveConnectionToEnv, clearConnectionCredentialsFromEnv, createConnection, getActiveConnection, getActiveConnectionId, getConnectionDir, listConnections, logoutConnection, readActiveConnectionSecrets, readConnectionSecrets, saveAccessToken, saveEvmWallet, saveIdentityProvision, savePhoneVerified, saveWalletLink, selectConnection, updateConnection, } from "../connection/store.js";
|
|
2
|
+
import { hasLegacyMcpProfiles, migrateFromMcpProfileStore } from "../connection/migrate.js";
|
|
3
|
+
export function getMcpProfileDir() {
|
|
4
|
+
return getConnectionDir();
|
|
5
|
+
}
|
|
6
|
+
function ensureMigrated() {
|
|
7
|
+
if (hasLegacyMcpProfiles()) {
|
|
8
|
+
migrateFromMcpProfileStore();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function toProfile(meta) {
|
|
12
|
+
if (!meta)
|
|
13
|
+
return null;
|
|
14
|
+
return {
|
|
15
|
+
id: meta.id,
|
|
16
|
+
label: meta.label,
|
|
17
|
+
tokenServiceUrl: meta.tokenServiceOrigin,
|
|
18
|
+
email: meta.email,
|
|
19
|
+
emailVerified: meta.emailVerified,
|
|
20
|
+
evmAddress: meta.evmAddress,
|
|
21
|
+
walletId: meta.walletId,
|
|
22
|
+
linkId: meta.linkId,
|
|
23
|
+
identityKeyId: meta.identityKeyId,
|
|
24
|
+
x25519Fingerprint: meta.x25519Fingerprint,
|
|
25
|
+
createdAt: meta.createdAt,
|
|
26
|
+
updatedAt: meta.updatedAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function toSecrets(secrets) {
|
|
30
|
+
if (!secrets)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
accessToken: secrets.accessToken,
|
|
34
|
+
evmPrivateKey: secrets.evmPrivateKeyHex,
|
|
35
|
+
accessTokenObtainedAt: secrets.accessTokenObtainedAt,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function listProfiles() {
|
|
39
|
+
ensureMigrated();
|
|
40
|
+
return listConnections().map((c) => ({
|
|
41
|
+
id: c.id,
|
|
42
|
+
label: c.label,
|
|
43
|
+
tokenServiceUrl: c.tokenServiceOrigin,
|
|
44
|
+
email: c.email,
|
|
45
|
+
emailVerified: c.emailVerified,
|
|
46
|
+
evmAddress: c.evmAddress,
|
|
47
|
+
walletId: c.walletId,
|
|
48
|
+
linkId: c.linkId,
|
|
49
|
+
identityKeyId: c.identityKeyId,
|
|
50
|
+
x25519Fingerprint: c.x25519Fingerprint,
|
|
51
|
+
createdAt: c.createdAt,
|
|
52
|
+
updatedAt: c.updatedAt,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export function getActiveProfileId() {
|
|
56
|
+
ensureMigrated();
|
|
57
|
+
return getActiveConnectionId();
|
|
58
|
+
}
|
|
59
|
+
export function getActiveProfile() {
|
|
60
|
+
ensureMigrated();
|
|
61
|
+
return toProfile(getActiveConnection());
|
|
62
|
+
}
|
|
63
|
+
export function selectProfile(profileId) {
|
|
64
|
+
ensureMigrated();
|
|
65
|
+
return toProfile(selectConnection(profileId));
|
|
66
|
+
}
|
|
67
|
+
export function createProfile(input) {
|
|
68
|
+
ensureMigrated();
|
|
69
|
+
return toProfile(createConnection({ label: input.label, tokenServiceUrl: input.tokenServiceUrl }));
|
|
70
|
+
}
|
|
71
|
+
export function updateProfile(profileId, patch) {
|
|
72
|
+
ensureMigrated();
|
|
73
|
+
const mapped = { ...patch };
|
|
74
|
+
if (patch.tokenServiceUrl) {
|
|
75
|
+
mapped.tokenServiceOrigin = patch.tokenServiceUrl;
|
|
76
|
+
delete mapped.tokenServiceUrl;
|
|
77
|
+
}
|
|
78
|
+
return toProfile(updateConnection(profileId, mapped));
|
|
79
|
+
}
|
|
80
|
+
export function removeProfile(_profileId, confirm) {
|
|
81
|
+
if (!confirm) {
|
|
82
|
+
throw new Error("Set confirm: true to delete the profile and its secrets.");
|
|
83
|
+
}
|
|
84
|
+
throw new Error("Profile removal via account_remove is deprecated. Use neozip-connect logout.");
|
|
85
|
+
}
|
|
86
|
+
export function readSecrets(profileId) {
|
|
87
|
+
ensureMigrated();
|
|
88
|
+
return toSecrets(readConnectionSecrets(profileId));
|
|
89
|
+
}
|
|
90
|
+
export function readActiveSecrets() {
|
|
91
|
+
ensureMigrated();
|
|
92
|
+
return toSecrets(readActiveConnectionSecrets());
|
|
93
|
+
}
|
|
94
|
+
export { saveAccessToken, saveEvmWallet, saveWalletLink, saveIdentityProvision, savePhoneVerified, };
|
|
95
|
+
export function logoutProfile(profileId) {
|
|
96
|
+
ensureMigrated();
|
|
97
|
+
const out = logoutConnection(profileId);
|
|
98
|
+
return { ...out, profileId: out.connectionId };
|
|
99
|
+
}
|
|
100
|
+
export function clearProfileCredentialsFromEnv() {
|
|
101
|
+
clearConnectionCredentialsFromEnv();
|
|
102
|
+
}
|
|
103
|
+
export function applyActiveProfileToEnv() {
|
|
104
|
+
ensureMigrated();
|
|
105
|
+
const out = applyActiveConnectionToEnv();
|
|
106
|
+
return { applied: out.applied, profileId: out.connectionId };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=profile-store.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { computeConnectionPhase, nextConnectCommand } from "../connection/phase.js";
|
|
2
|
+
import { getActiveConnection, readActiveConnectionSecrets, } from "../connection/store.js";
|
|
3
|
+
import { formatTokenReauthGuidance, isAccessTokenExpired, } from "../connection/token-auth.js";
|
|
4
|
+
export async function requireAccountReady(context) {
|
|
5
|
+
const phase = computeConnectionPhase();
|
|
6
|
+
const meta = getActiveConnection();
|
|
7
|
+
const secrets = readActiveConnectionSecrets();
|
|
8
|
+
if (secrets?.accessToken?.trim() && isAccessTokenExpired(secrets)) {
|
|
9
|
+
return [
|
|
10
|
+
`${context} requires a valid Token Service access token.`,
|
|
11
|
+
formatTokenReauthGuidance(meta?.email),
|
|
12
|
+
].join("\n\n");
|
|
13
|
+
}
|
|
14
|
+
if (phase !== "ready") {
|
|
15
|
+
const nextCmd = nextConnectCommand(phase, {
|
|
16
|
+
email: meta?.email,
|
|
17
|
+
phoneE164: meta?.phoneE164,
|
|
18
|
+
});
|
|
19
|
+
const parts = [
|
|
20
|
+
`${context} requires a fully initialized NeoZip account (phase: ${phase}).`,
|
|
21
|
+
];
|
|
22
|
+
if (nextCmd)
|
|
23
|
+
parts.push(`Run: ${nextCmd}`);
|
|
24
|
+
parts.push("Reload MCP after setup so credentials load into this session.");
|
|
25
|
+
return parts.join(" ");
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=require-account.js.map
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { getAddress } from "ethers";
|
|
2
|
+
import { resolveTokenServiceBaseUrl } from "../archive/recipient-lookup.js";
|
|
3
|
+
import { formatTokenServiceFetchError } from "../util/token-service-fetch.js";
|
|
4
|
+
import { parseAccessTokenExpiry, tokenServiceAuthErrorMessage, } from "../connection/token-auth.js";
|
|
5
|
+
import { signEvmMessage } from "./wallet-evm.js";
|
|
6
|
+
const DEFAULT_RECIPIENT_SUITE = "ecies-x25519-aes256gcm";
|
|
7
|
+
function challengeBody(bundle) {
|
|
8
|
+
return {
|
|
9
|
+
x25519PublicKeyB64: bundle.x25519PublicKeyB64,
|
|
10
|
+
wrappedPrivateKeyB64: bundle.wrappedPrivateKeyB64,
|
|
11
|
+
wrapFormatVersion: bundle.wrapFormatVersion,
|
|
12
|
+
wrapKdfInfo: bundle.wrapKdfInfo,
|
|
13
|
+
wrapKdfSaltB64: bundle.wrapKdfSaltB64,
|
|
14
|
+
wrapAead: bundle.wrapAead,
|
|
15
|
+
wrapIvB64: bundle.wrapIvB64,
|
|
16
|
+
wrapAadB64: bundle.wrapAadB64,
|
|
17
|
+
recipientSuites: bundle.recipientSuites ?? [DEFAULT_RECIPIENT_SUITE],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function pickString(obj, ...keys) {
|
|
21
|
+
for (const k of keys) {
|
|
22
|
+
const v = obj[k];
|
|
23
|
+
if (typeof v === "string" && v.length > 0)
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function extractError(parsed, fallback, options) {
|
|
29
|
+
const authHint = options?.status != null
|
|
30
|
+
? tokenServiceAuthErrorMessage(options.status, options.email)
|
|
31
|
+
: null;
|
|
32
|
+
if (authHint)
|
|
33
|
+
return authHint;
|
|
34
|
+
if (parsed && typeof parsed === "object") {
|
|
35
|
+
const p = parsed;
|
|
36
|
+
if (typeof p.error === "string")
|
|
37
|
+
return p.error;
|
|
38
|
+
if (typeof p.message === "string")
|
|
39
|
+
return p.message;
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
async function safeJson(response) {
|
|
44
|
+
const ct = response.headers.get("content-type") || "";
|
|
45
|
+
if (!ct.includes("json"))
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
return (await response.json());
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function registerEmailWithDelivery(baseUrl, email, verificationDelivery) {
|
|
55
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
56
|
+
let res;
|
|
57
|
+
try {
|
|
58
|
+
res = await fetch(`${url}/auth/register`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "content-type": "application/json" },
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
email: email.trim().toLowerCase(),
|
|
63
|
+
verificationDelivery,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return { success: false, error: formatTokenServiceFetchError(url, err) };
|
|
69
|
+
}
|
|
70
|
+
const parsed = await safeJson(res);
|
|
71
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: extractError(parsed, `HTTP ${res.status}`),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
message: typeof parsed.message === "string" ? parsed.message : "Verification email sent.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Desktop-style registration (deep link email). */
|
|
83
|
+
export async function registerEmailApp(baseUrl, email) {
|
|
84
|
+
return registerEmailWithDelivery(baseUrl, email, "app");
|
|
85
|
+
}
|
|
86
|
+
/** Terminal / neozip-connect registration (6-digit code only). */
|
|
87
|
+
export async function registerEmailCli(baseUrl, email) {
|
|
88
|
+
return registerEmailWithDelivery(baseUrl, email, "cli");
|
|
89
|
+
}
|
|
90
|
+
export async function verifyEmailAndExtractToken(baseUrl, email, code) {
|
|
91
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
res = await fetch(`${url}/auth/verify-email-code`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "content-type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ email: email.trim().toLowerCase(), code: code.trim() }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
throw new Error(formatTokenServiceFetchError(url, err));
|
|
102
|
+
}
|
|
103
|
+
const parsed = await safeJson(res);
|
|
104
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
105
|
+
throw new Error(extractError(parsed, `Verification failed (HTTP ${res.status})`, {
|
|
106
|
+
status: res.status,
|
|
107
|
+
email,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
const accessToken = pickString(parsed, "accessToken", "token", "bearerToken") ?? null;
|
|
111
|
+
if (!accessToken) {
|
|
112
|
+
throw new Error("Verification succeeded but no accessToken in response. Set NEOZIP_TOKEN_SERVICE_ACCESS_TOKEN manually or update neozip-blockchain.");
|
|
113
|
+
}
|
|
114
|
+
const verifiedEmail = pickString(parsed, "email") ?? email.trim().toLowerCase();
|
|
115
|
+
return {
|
|
116
|
+
accessToken,
|
|
117
|
+
email: verifiedEmail,
|
|
118
|
+
expiresAt: parseAccessTokenExpiry(parsed),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export async function exchangeMagicLinkToken(baseUrl, token) {
|
|
122
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await fetch(`${url}/auth/exchange-token`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "content-type": "application/json" },
|
|
128
|
+
body: JSON.stringify({ token: token.trim() }),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new Error(formatTokenServiceFetchError(url, err));
|
|
133
|
+
}
|
|
134
|
+
const parsed = await safeJson(res);
|
|
135
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
136
|
+
throw new Error(extractError(parsed, `Magic link exchange failed (HTTP ${res.status})`, {
|
|
137
|
+
status: res.status,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
const accessToken = pickString(parsed, "accessToken", "token", "bearerToken") ?? null;
|
|
141
|
+
if (!accessToken) {
|
|
142
|
+
throw new Error("Magic link exchange succeeded but no accessToken in response.");
|
|
143
|
+
}
|
|
144
|
+
const verifiedEmail = pickString(parsed, "email") ?? "";
|
|
145
|
+
if (!verifiedEmail) {
|
|
146
|
+
throw new Error("Magic link exchange succeeded but no email in response.");
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
accessToken,
|
|
150
|
+
email: verifiedEmail,
|
|
151
|
+
expiresAt: parseAccessTokenExpiry(parsed),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export async function requestPhoneOtp(baseUrl, accessToken, phoneE164) {
|
|
155
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
156
|
+
let res;
|
|
157
|
+
try {
|
|
158
|
+
res = await fetch(`${url}/auth/phone/request-otp`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
Authorization: `Bearer ${accessToken}`,
|
|
162
|
+
"content-type": "application/json",
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({ phoneE164: phoneE164.trim() }),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return { success: false, error: formatTokenServiceFetchError(url, err) };
|
|
169
|
+
}
|
|
170
|
+
const parsed = await safeJson(res);
|
|
171
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: extractError(parsed, `Phone OTP request failed (HTTP ${res.status})`),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { success: true };
|
|
178
|
+
}
|
|
179
|
+
export async function verifyPhoneOtp(baseUrl, accessToken, phoneE164, code) {
|
|
180
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
181
|
+
let res;
|
|
182
|
+
try {
|
|
183
|
+
res = await fetch(`${url}/auth/phone/verify`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: `Bearer ${accessToken}`,
|
|
187
|
+
"content-type": "application/json",
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({
|
|
190
|
+
phoneE164: phoneE164.trim(),
|
|
191
|
+
code: code.trim(),
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
return { success: false, error: formatTokenServiceFetchError(url, err) };
|
|
197
|
+
}
|
|
198
|
+
const parsed = await safeJson(res);
|
|
199
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: extractError(parsed, `Phone verification failed (HTTP ${res.status})`),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return { success: true };
|
|
206
|
+
}
|
|
207
|
+
export class IdentityKeyConflictError extends Error {
|
|
208
|
+
constructor(message) {
|
|
209
|
+
super(message);
|
|
210
|
+
this.name = "IdentityKeyConflictError";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function authHeaders(accessToken) {
|
|
214
|
+
return {
|
|
215
|
+
Authorization: `Bearer ${accessToken}`,
|
|
216
|
+
"content-type": "application/json",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export async function fetchCoordinatorConfig(baseUrl) {
|
|
220
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(`${url}/crypto/coordinator-config`, { method: "GET" });
|
|
223
|
+
return res.ok;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export async function requestWalletEnsureChallenge(baseUrl, evmAddress) {
|
|
230
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
231
|
+
const checksummed = getAddress(evmAddress.trim());
|
|
232
|
+
const res = await fetch(`${url}/identity/wallets/challenge`, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: { "content-type": "application/json" },
|
|
235
|
+
body: JSON.stringify({ evmAddress: checksummed }),
|
|
236
|
+
});
|
|
237
|
+
const parsed = await safeJson(res);
|
|
238
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
239
|
+
throw new Error(extractError(parsed, `Wallet challenge failed (HTTP ${res.status})`));
|
|
240
|
+
}
|
|
241
|
+
const challengeId = pickString(parsed, "challengeId");
|
|
242
|
+
const message = pickString(parsed, "message");
|
|
243
|
+
const addr = pickString(parsed, "evmAddress");
|
|
244
|
+
if (!challengeId || !message || !addr) {
|
|
245
|
+
throw new Error("Wallet challenge response missing fields");
|
|
246
|
+
}
|
|
247
|
+
return { challengeId, message, evmAddress: addr };
|
|
248
|
+
}
|
|
249
|
+
export async function completeWalletEnsure(baseUrl, input) {
|
|
250
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
251
|
+
const checksummed = getAddress(input.evmAddress.trim());
|
|
252
|
+
const res = await fetch(`${url}/identity/wallets/ensure`, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: { "content-type": "application/json" },
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
evmAddress: checksummed,
|
|
257
|
+
challengeId: input.challengeId,
|
|
258
|
+
evmSignature: input.evmSignature,
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
const parsed = await safeJson(res);
|
|
262
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
263
|
+
throw new Error(extractError(parsed, `Wallet ensure failed (HTTP ${res.status})`));
|
|
264
|
+
}
|
|
265
|
+
const walletId = typeof parsed.walletId === "number" ? parsed.walletId : Number(parsed.walletId);
|
|
266
|
+
const addr = pickString(parsed, "evmAddress");
|
|
267
|
+
if (!Number.isFinite(walletId) || !addr) {
|
|
268
|
+
throw new Error("Wallet ensure response missing walletId");
|
|
269
|
+
}
|
|
270
|
+
return { walletId, evmAddress: addr };
|
|
271
|
+
}
|
|
272
|
+
export async function requestWalletAttachChallenge(baseUrl, accessToken, walletId) {
|
|
273
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
274
|
+
const res = await fetch(`${url}/identity/users/me/wallets/${walletId}/attach/challenge`, { method: "POST", headers: authHeaders(accessToken) });
|
|
275
|
+
const parsed = await safeJson(res);
|
|
276
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
277
|
+
throw new Error(extractError(parsed, `Attach challenge failed (HTTP ${res.status})`));
|
|
278
|
+
}
|
|
279
|
+
const challengeId = pickString(parsed, "challengeId");
|
|
280
|
+
const message = pickString(parsed, "message");
|
|
281
|
+
const wid = typeof parsed.walletId === "number" ? parsed.walletId : Number(parsed.walletId);
|
|
282
|
+
const addr = pickString(parsed, "evmAddress");
|
|
283
|
+
if (!challengeId || !message || !Number.isFinite(wid) || !addr) {
|
|
284
|
+
throw new Error("Attach challenge response missing fields");
|
|
285
|
+
}
|
|
286
|
+
return { challengeId, message, walletId: wid, evmAddress: addr };
|
|
287
|
+
}
|
|
288
|
+
export async function completeWalletAttach(baseUrl, accessToken, walletId, input) {
|
|
289
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
290
|
+
const res = await fetch(`${url}/identity/users/me/wallets/${walletId}/attach/complete`, {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: authHeaders(accessToken),
|
|
293
|
+
body: JSON.stringify({
|
|
294
|
+
challengeId: input.challengeId,
|
|
295
|
+
evmSignature: input.evmSignature,
|
|
296
|
+
setPrimary: true,
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
const parsed = await safeJson(res);
|
|
300
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
301
|
+
throw new Error(extractError(parsed, `Attach complete failed (HTTP ${res.status})`));
|
|
302
|
+
}
|
|
303
|
+
const linkId = typeof parsed.linkId === "number" ? parsed.linkId : Number(parsed.linkId);
|
|
304
|
+
if (!Number.isFinite(linkId)) {
|
|
305
|
+
throw new Error("Attach complete response missing linkId");
|
|
306
|
+
}
|
|
307
|
+
return { linkId };
|
|
308
|
+
}
|
|
309
|
+
export async function runWalletEnsureAndAttach(baseUrl, accessToken, evmPrivateKey, evmAddress) {
|
|
310
|
+
const ensureCh = await requestWalletEnsureChallenge(baseUrl, evmAddress);
|
|
311
|
+
const ensureSig = await signEvmMessage(evmPrivateKey, ensureCh.message);
|
|
312
|
+
const ensured = await completeWalletEnsure(baseUrl, {
|
|
313
|
+
evmAddress,
|
|
314
|
+
challengeId: ensureCh.challengeId,
|
|
315
|
+
evmSignature: ensureSig,
|
|
316
|
+
});
|
|
317
|
+
const attachCh = await requestWalletAttachChallenge(baseUrl, accessToken, ensured.walletId);
|
|
318
|
+
const attachSig = await signEvmMessage(evmPrivateKey, attachCh.message);
|
|
319
|
+
const { linkId } = await completeWalletAttach(baseUrl, accessToken, ensured.walletId, {
|
|
320
|
+
challengeId: attachCh.challengeId,
|
|
321
|
+
evmSignature: attachSig,
|
|
322
|
+
});
|
|
323
|
+
return { walletId: ensured.walletId, evmAddress: ensured.evmAddress, linkId };
|
|
324
|
+
}
|
|
325
|
+
export async function getIdentityKeyBundle(baseUrl, accessToken, walletId) {
|
|
326
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
327
|
+
const res = await fetch(`${url}/identity/wallets/${walletId}/identity-key`, {
|
|
328
|
+
method: "GET",
|
|
329
|
+
headers: authHeaders(accessToken),
|
|
330
|
+
});
|
|
331
|
+
if (res.status === 404)
|
|
332
|
+
return { found: false };
|
|
333
|
+
const parsed = await safeJson(res);
|
|
334
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
335
|
+
throw new Error(extractError(parsed, `identity-key GET failed (HTTP ${res.status})`));
|
|
336
|
+
}
|
|
337
|
+
const ik = parsed.identityKey;
|
|
338
|
+
if (!ik)
|
|
339
|
+
throw new Error("identity-key response missing identityKey");
|
|
340
|
+
const identityKeyId = typeof ik.identityKeyId === "number" ? ik.identityKeyId : Number(ik.identityKeyId);
|
|
341
|
+
const x25519PublicKey = pickString(ik, "x25519PublicKeyB64", "x25519PublicKey");
|
|
342
|
+
if (!Number.isFinite(identityKeyId) || !x25519PublicKey) {
|
|
343
|
+
throw new Error("identity-key response incomplete");
|
|
344
|
+
}
|
|
345
|
+
return { found: true, identityKeyId, x25519PublicKey };
|
|
346
|
+
}
|
|
347
|
+
export async function requestIdentityKeyInitChallenge(baseUrl, accessToken, walletId, bundle) {
|
|
348
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
349
|
+
const res = await fetch(`${url}/identity/wallets/${walletId}/identity-key/init/challenge`, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: authHeaders(accessToken),
|
|
352
|
+
body: JSON.stringify(challengeBody(bundle)),
|
|
353
|
+
});
|
|
354
|
+
const parsed = await safeJson(res);
|
|
355
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
356
|
+
throw new Error(extractError(parsed, `init challenge failed (HTTP ${res.status})`));
|
|
357
|
+
}
|
|
358
|
+
const challengeId = pickString(parsed, "challengeId");
|
|
359
|
+
const message = pickString(parsed, "message");
|
|
360
|
+
const evmAddress = pickString(parsed, "evmAddress");
|
|
361
|
+
const wrappedSha256Hex = pickString(parsed, "wrappedSha256Hex");
|
|
362
|
+
if (!challengeId || !message || !evmAddress || !wrappedSha256Hex) {
|
|
363
|
+
throw new Error("init challenge response missing fields");
|
|
364
|
+
}
|
|
365
|
+
return { challengeId, message, evmAddress, wrappedSha256Hex };
|
|
366
|
+
}
|
|
367
|
+
export async function completeIdentityKeyInit(baseUrl, accessToken, walletId, bundle, challengeId, evmSignature) {
|
|
368
|
+
const url = resolveTokenServiceBaseUrl({ tokenServiceUrl: baseUrl });
|
|
369
|
+
const res = await fetch(`${url}/identity/wallets/${walletId}/identity-key/init/complete`, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: authHeaders(accessToken),
|
|
372
|
+
body: JSON.stringify({
|
|
373
|
+
...challengeBody(bundle),
|
|
374
|
+
challengeId,
|
|
375
|
+
evmSignature,
|
|
376
|
+
}),
|
|
377
|
+
});
|
|
378
|
+
const parsed = await safeJson(res);
|
|
379
|
+
if (res.status === 409) {
|
|
380
|
+
throw new IdentityKeyConflictError(extractError(parsed, "Identity key already exists for this wallet."));
|
|
381
|
+
}
|
|
382
|
+
if (!res.ok || !parsed || parsed.success !== true) {
|
|
383
|
+
throw new Error(extractError(parsed, `init complete failed (HTTP ${res.status})`));
|
|
384
|
+
}
|
|
385
|
+
const ik = parsed.identityKey;
|
|
386
|
+
if (!ik)
|
|
387
|
+
throw new Error("init complete missing identityKey");
|
|
388
|
+
const identityKeyId = typeof ik.identityKeyId === "number" ? ik.identityKeyId : Number(ik.identityKeyId);
|
|
389
|
+
const x25519PublicKey = pickString(ik, "x25519PublicKeyB64", "x25519PublicKey");
|
|
390
|
+
if (!Number.isFinite(identityKeyId) || !x25519PublicKey) {
|
|
391
|
+
throw new Error("init complete response incomplete");
|
|
392
|
+
}
|
|
393
|
+
return { identityKeyId, x25519PublicKey };
|
|
394
|
+
}
|
|
395
|
+
//# sourceMappingURL=token-service-identity.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Wallet } from "ethers";
|
|
2
|
+
export function generateEvmIdentity() {
|
|
3
|
+
const wallet = Wallet.createRandom();
|
|
4
|
+
return {
|
|
5
|
+
address: wallet.address,
|
|
6
|
+
privateKey: wallet.privateKey,
|
|
7
|
+
mnemonic: wallet.mnemonic?.phrase ?? null,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function evmIdentityFromMnemonic(mnemonic) {
|
|
11
|
+
const phrase = mnemonic.trim().split(/\s+/).join(" ");
|
|
12
|
+
const wallet = Wallet.fromPhrase(phrase);
|
|
13
|
+
return {
|
|
14
|
+
address: wallet.address,
|
|
15
|
+
privateKey: wallet.privateKey,
|
|
16
|
+
mnemonic: wallet.mnemonic?.phrase ?? phrase,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function evmIdentityFromPrivateKey(privateKey) {
|
|
20
|
+
const raw = privateKey.trim();
|
|
21
|
+
const normalized = raw.startsWith("0x") ? raw : `0x${raw}`;
|
|
22
|
+
const wallet = new Wallet(normalized);
|
|
23
|
+
return {
|
|
24
|
+
address: wallet.address,
|
|
25
|
+
privateKey: wallet.privateKey,
|
|
26
|
+
mnemonic: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function signEvmMessage(privateKey, message) {
|
|
30
|
+
const wallet = new Wallet(privateKey);
|
|
31
|
+
return wallet.signMessage(message);
|
|
32
|
+
}
|
|
33
|
+
export function rawEoaBytes(privateKey) {
|
|
34
|
+
const stripped = privateKey.startsWith("0x")
|
|
35
|
+
? privateKey.slice(2)
|
|
36
|
+
: privateKey;
|
|
37
|
+
return Buffer.from(stripped, "hex");
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=wallet-evm.js.map
|