@cartridge/controller 0.13.6 → 0.13.9
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/.turbo/turbo-build$colon$deps.log +25 -19
- package/HEADLESS_MODE.md +28 -7
- package/dist/constants.d.ts +1 -0
- package/dist/controller.d.ts +2 -1
- package/dist/{index-BdTFKueB.js → index-CJNujYxo.js} +40 -30
- package/dist/index-CJNujYxo.js.map +1 -0
- package/dist/index.js +619 -507
- package/dist/index.js.map +1 -1
- package/dist/lookup.d.ts +2 -0
- package/dist/node/index.cjs +3 -2
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.js +3 -2
- package/dist/node/index.js.map +1 -1
- package/dist/session/provider.d.ts +4 -3
- package/dist/session.js +110 -107
- package/dist/session.js.map +1 -1
- package/dist/stats.html +1 -1
- package/dist/types.d.ts +5 -0
- package/package.json +4 -5
- package/src/__tests__/disconnect.test.ts +112 -0
- package/src/__tests__/lookupUsername.test.ts +166 -0
- package/src/constants.ts +4 -0
- package/src/controller.ts +20 -0
- package/src/iframe/keychain.ts +10 -3
- package/src/lookup.ts +170 -2
- package/src/node/server.ts +2 -1
- package/src/session/provider.ts +58 -46
- package/src/types.ts +6 -0
- package/.turbo/turbo-build.log +0 -69
- package/dist/index-BdTFKueB.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -70,6 +70,11 @@ export interface LookupResult {
|
|
|
70
70
|
export interface LookupResponse {
|
|
71
71
|
results: LookupResult[];
|
|
72
72
|
}
|
|
73
|
+
export interface HeadlessUsernameLookupResult {
|
|
74
|
+
username: string;
|
|
75
|
+
exists: boolean;
|
|
76
|
+
signers: AuthOption[];
|
|
77
|
+
}
|
|
73
78
|
export declare enum FeeSource {
|
|
74
79
|
PAYMASTER = "PAYMASTER",
|
|
75
80
|
CREDITS = "CREDITS"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartridge/controller",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
4
4
|
"description": "Cartridge Controller",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@cartridge/controller-wasm": "0.9.
|
|
29
|
+
"@cartridge/controller-wasm": "0.9.4",
|
|
30
30
|
"@cartridge/penpal": "^6.2.4",
|
|
31
31
|
"micro-sol-signer": "^0.5.0",
|
|
32
32
|
"bs58": "^6.0.0",
|
|
@@ -56,14 +56,13 @@
|
|
|
56
56
|
"vite-plugin-node-polyfills": "^0.23.0",
|
|
57
57
|
"vite-plugin-top-level-await": "^1.4.4",
|
|
58
58
|
"vite-plugin-wasm": "^3.4.1",
|
|
59
|
-
"@cartridge/tsconfig": "0.13.
|
|
59
|
+
"@cartridge/tsconfig": "0.13.9"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
-
"build:deps": "pnpm build",
|
|
62
|
+
"build:deps": "pnpm build:browser && pnpm build:node",
|
|
63
63
|
"dev": "vite build --watch",
|
|
64
64
|
"build:browser": "vite build",
|
|
65
65
|
"build:node": "tsup --config tsup.node.config.ts",
|
|
66
|
-
"build": "pnpm build:browser && pnpm build:node",
|
|
67
66
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
68
67
|
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
69
68
|
"test": "jest",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import ControllerProvider from "../controller";
|
|
2
|
+
|
|
3
|
+
type MockStorage = {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const createMockLocalStorage = () => {
|
|
8
|
+
const store: MockStorage = {};
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
get length() {
|
|
12
|
+
return Object.keys(store).length;
|
|
13
|
+
},
|
|
14
|
+
clear: jest.fn(() => {
|
|
15
|
+
Object.keys(store).forEach((key) => {
|
|
16
|
+
delete store[key];
|
|
17
|
+
});
|
|
18
|
+
}),
|
|
19
|
+
getItem: jest.fn((key: string) => (key in store ? store[key] : null)),
|
|
20
|
+
setItem: jest.fn((key: string, value: string) => {
|
|
21
|
+
store[key] = String(value);
|
|
22
|
+
}),
|
|
23
|
+
removeItem: jest.fn((key: string) => {
|
|
24
|
+
delete store[key];
|
|
25
|
+
}),
|
|
26
|
+
key: jest.fn((index: number) => {
|
|
27
|
+
const keys = Object.keys(store);
|
|
28
|
+
return keys[index] || null;
|
|
29
|
+
}),
|
|
30
|
+
} as Storage;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("ControllerProvider.disconnect", () => {
|
|
34
|
+
const originalLocalStorage = (global as any).localStorage;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
(global as any).localStorage = createMockLocalStorage();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
(global as any).localStorage = originalLocalStorage;
|
|
42
|
+
jest.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("cleans persisted connector/session localStorage keys", async () => {
|
|
46
|
+
const controller = new ControllerProvider({});
|
|
47
|
+
const keychainDisconnect = jest.fn().mockResolvedValue(undefined);
|
|
48
|
+
(controller as any).keychain = {
|
|
49
|
+
disconnect: keychainDisconnect,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
localStorage.setItem("lastUsedConnector", "controller");
|
|
53
|
+
localStorage.setItem(
|
|
54
|
+
"@cartridge/account/0x40bda2fcd37963c0b8f951801c63a88132feb399dab0f5318245b2c59a553af/0x534e5f5345504f4c4941",
|
|
55
|
+
JSON.stringify({ Controller: {} }),
|
|
56
|
+
);
|
|
57
|
+
localStorage.setItem("@cartridge/active", JSON.stringify({ Active: {} }));
|
|
58
|
+
localStorage.setItem(
|
|
59
|
+
"@cartridge/https://x.cartridge.gg/active",
|
|
60
|
+
JSON.stringify({ Active: {} }),
|
|
61
|
+
);
|
|
62
|
+
localStorage.setItem(
|
|
63
|
+
"@cartridge/policies/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
|
|
64
|
+
JSON.stringify({ policies: [] }),
|
|
65
|
+
);
|
|
66
|
+
localStorage.setItem("@cartridge/features", JSON.stringify({}));
|
|
67
|
+
localStorage.setItem(
|
|
68
|
+
"@cartridge/session/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
|
|
69
|
+
JSON.stringify({ Session: {} }),
|
|
70
|
+
);
|
|
71
|
+
localStorage.setItem("keepMe", "keep");
|
|
72
|
+
|
|
73
|
+
await controller.disconnect();
|
|
74
|
+
|
|
75
|
+
expect(localStorage.getItem("lastUsedConnector")).toBeNull();
|
|
76
|
+
expect(
|
|
77
|
+
localStorage.getItem(
|
|
78
|
+
"@cartridge/account/0x40bda2fcd37963c0b8f951801c63a88132feb399dab0f5318245b2c59a553af/0x534e5f5345504f4c4941",
|
|
79
|
+
),
|
|
80
|
+
).toBeNull();
|
|
81
|
+
expect(localStorage.getItem("@cartridge/active")).toBeNull();
|
|
82
|
+
expect(
|
|
83
|
+
localStorage.getItem("@cartridge/https://x.cartridge.gg/active"),
|
|
84
|
+
).toBeNull();
|
|
85
|
+
expect(
|
|
86
|
+
localStorage.getItem(
|
|
87
|
+
"@cartridge/policies/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
|
|
88
|
+
),
|
|
89
|
+
).toBeNull();
|
|
90
|
+
expect(localStorage.getItem("@cartridge/features")).toBeNull();
|
|
91
|
+
expect(
|
|
92
|
+
localStorage.getItem(
|
|
93
|
+
"@cartridge/session/0x4fdcb829582d172a6f3858b97c16da38b08da5a1df7101a5d285b868d89921b/0x534e5f4d41494e",
|
|
94
|
+
),
|
|
95
|
+
).toBeNull();
|
|
96
|
+
expect(localStorage.getItem("keepMe")).toBe("keep");
|
|
97
|
+
expect(localStorage.removeItem).toHaveBeenCalledWith("lastUsedConnector");
|
|
98
|
+
expect(keychainDisconnect).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("does not throw when localStorage is unavailable", async () => {
|
|
102
|
+
delete (global as any).localStorage;
|
|
103
|
+
const controller = new ControllerProvider({});
|
|
104
|
+
const keychainDisconnect = jest.fn().mockResolvedValue(undefined);
|
|
105
|
+
(controller as any).keychain = {
|
|
106
|
+
disconnect: keychainDisconnect,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
await expect(controller.disconnect()).resolves.toBeUndefined();
|
|
110
|
+
expect(keychainDisconnect).toHaveBeenCalledTimes(1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { constants } from "starknet";
|
|
2
|
+
import ControllerProvider from "../controller";
|
|
3
|
+
import { IMPLEMENTED_AUTH_OPTIONS } from "../types";
|
|
4
|
+
|
|
5
|
+
describe("lookupUsername", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
(global as any).fetch = jest.fn();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.resetAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("returns normalized signer options in canonical order", async () => {
|
|
15
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
16
|
+
ok: true,
|
|
17
|
+
json: async () => ({
|
|
18
|
+
data: {
|
|
19
|
+
account: {
|
|
20
|
+
username: "alice",
|
|
21
|
+
controllers: {
|
|
22
|
+
edges: [
|
|
23
|
+
{
|
|
24
|
+
node: {
|
|
25
|
+
signers: [
|
|
26
|
+
{
|
|
27
|
+
isOriginal: true,
|
|
28
|
+
isRevoked: false,
|
|
29
|
+
metadata: {
|
|
30
|
+
__typename: "Eip191Credentials",
|
|
31
|
+
eip191: [
|
|
32
|
+
{ provider: "metamask", ethAddress: "0x1" },
|
|
33
|
+
{ provider: "google", ethAddress: "0x2" },
|
|
34
|
+
{ provider: "unknown", ethAddress: "0x3" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
isOriginal: true,
|
|
40
|
+
isRevoked: false,
|
|
41
|
+
metadata: { __typename: "PasswordCredentials" },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
isOriginal: true,
|
|
45
|
+
isRevoked: false,
|
|
46
|
+
metadata: { __typename: "WebauthnCredentials" },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
isOriginal: true,
|
|
50
|
+
isRevoked: true,
|
|
51
|
+
metadata: { __typename: "WebauthnCredentials" },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
isOriginal: true,
|
|
55
|
+
isRevoked: false,
|
|
56
|
+
metadata: {
|
|
57
|
+
__typename: "Eip191Credentials",
|
|
58
|
+
eip191: [{ provider: "discord", ethAddress: "0x4" }],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const provider = new ControllerProvider({ lazyload: true });
|
|
72
|
+
const result = await provider.lookupUsername("alice");
|
|
73
|
+
const expectedOrder = IMPLEMENTED_AUTH_OPTIONS.filter((option) =>
|
|
74
|
+
["google", "webauthn", "discord", "password", "metamask"].includes(
|
|
75
|
+
option,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual({
|
|
80
|
+
username: "alice",
|
|
81
|
+
exists: true,
|
|
82
|
+
signers: expectedOrder,
|
|
83
|
+
});
|
|
84
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
85
|
+
"https://api.cartridge.gg/query",
|
|
86
|
+
expect.any(Object),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("filters non-original signers on non-mainnet chains", async () => {
|
|
91
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
92
|
+
ok: true,
|
|
93
|
+
json: async () => ({
|
|
94
|
+
data: {
|
|
95
|
+
account: {
|
|
96
|
+
username: "alice",
|
|
97
|
+
controllers: {
|
|
98
|
+
edges: [
|
|
99
|
+
{
|
|
100
|
+
node: {
|
|
101
|
+
signers: [
|
|
102
|
+
{
|
|
103
|
+
isOriginal: false,
|
|
104
|
+
isRevoked: false,
|
|
105
|
+
metadata: {
|
|
106
|
+
__typename: "Eip191Credentials",
|
|
107
|
+
eip191: [{ provider: "google", ethAddress: "0x1" }],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
isOriginal: true,
|
|
112
|
+
isRevoked: false,
|
|
113
|
+
metadata: { __typename: "PasswordCredentials" },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const provider = new ControllerProvider({
|
|
126
|
+
lazyload: true,
|
|
127
|
+
defaultChainId: constants.StarknetChainId.SN_SEPOLIA,
|
|
128
|
+
});
|
|
129
|
+
const result = await provider.lookupUsername("alice");
|
|
130
|
+
|
|
131
|
+
expect(result.signers).toEqual(["password"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("returns exists=false for unknown usernames", async () => {
|
|
135
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
136
|
+
ok: true,
|
|
137
|
+
json: async () => ({
|
|
138
|
+
data: {
|
|
139
|
+
account: null,
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const provider = new ControllerProvider({ lazyload: true });
|
|
145
|
+
const result = await provider.lookupUsername("missing-user");
|
|
146
|
+
|
|
147
|
+
expect(result).toEqual({
|
|
148
|
+
username: "missing-user",
|
|
149
|
+
exists: false,
|
|
150
|
+
signers: [],
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("throws on network failures", async () => {
|
|
155
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 503,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const provider = new ControllerProvider({ lazyload: true });
|
|
161
|
+
|
|
162
|
+
await expect(provider.lookupUsername("alice")).rejects.toThrow(
|
|
163
|
+
"HTTP error! status: 503",
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export const KEYCHAIN_URL = "https://x.cartridge.gg";
|
|
2
2
|
export const PROFILE_URL = "https://profile.cartridge.gg";
|
|
3
3
|
export const API_URL = "https://api.cartridge.gg";
|
|
4
|
+
|
|
5
|
+
// Query parameter name used to pass session data via URL redirects.
|
|
6
|
+
// Borrowed from Telegram mini app convention, but the choice is arbitrary.
|
|
7
|
+
export const REDIRECT_QUERY_NAME = "startapp";
|
package/src/controller.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { KEYCHAIN_URL } from "./constants";
|
|
|
15
15
|
import { HeadlessAuthenticationError, NotReadyToConnect } from "./errors";
|
|
16
16
|
import { KeychainIFrame } from "./iframe";
|
|
17
17
|
import BaseProvider from "./provider";
|
|
18
|
+
import { lookupUsername as lookupUsernameApi } from "./lookup";
|
|
18
19
|
import {
|
|
19
20
|
AuthOptions,
|
|
20
21
|
Chain,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
ProfileContextTypeVariant,
|
|
29
30
|
ResponseCodes,
|
|
30
31
|
OpenOptions,
|
|
32
|
+
HeadlessUsernameLookupResult,
|
|
31
33
|
StarterpackOptions,
|
|
32
34
|
} from "./types";
|
|
33
35
|
import { validateRedirectUrl } from "./url-validator";
|
|
@@ -393,6 +395,13 @@ export default class ControllerProvider extends BaseProvider {
|
|
|
393
395
|
try {
|
|
394
396
|
if (typeof localStorage !== "undefined") {
|
|
395
397
|
localStorage.removeItem("lastUsedConnector");
|
|
398
|
+
|
|
399
|
+
for (let i = localStorage.length - 1; i >= 0; i--) {
|
|
400
|
+
const key = localStorage.key(i);
|
|
401
|
+
if (key?.startsWith("@cartridge/")) {
|
|
402
|
+
localStorage.removeItem(key);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
396
405
|
}
|
|
397
406
|
} catch {
|
|
398
407
|
// Ignore environments where localStorage is unavailable.
|
|
@@ -530,6 +539,17 @@ export default class ControllerProvider extends BaseProvider {
|
|
|
530
539
|
return this.keychain.username();
|
|
531
540
|
}
|
|
532
541
|
|
|
542
|
+
async lookupUsername(
|
|
543
|
+
username: string,
|
|
544
|
+
): Promise<HeadlessUsernameLookupResult> {
|
|
545
|
+
const trimmed = username.trim();
|
|
546
|
+
if (!trimmed) {
|
|
547
|
+
throw new Error("Username is required");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return lookupUsernameApi(trimmed, this.selectedChain);
|
|
551
|
+
}
|
|
552
|
+
|
|
533
553
|
openPurchaseCredits() {
|
|
534
554
|
if (!this.iframes) {
|
|
535
555
|
return;
|
package/src/iframe/keychain.ts
CHANGED
|
@@ -93,17 +93,24 @@ export class KeychainIFrame extends IFrame<Keychain> {
|
|
|
93
93
|
_url.searchParams.set("username", encodeURIComponent(username));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
if (preset) {
|
|
97
|
+
_url.searchParams.set("preset", preset);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (shouldOverridePresetPolicies) {
|
|
101
|
+
_url.searchParams.set("should_override_preset_policies", "true");
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
// Policy precedence logic:
|
|
97
105
|
// 1. If shouldOverridePresetPolicies is true and policies are provided, use policies
|
|
98
|
-
// 2. Otherwise, if preset is defined,
|
|
99
|
-
// 3. Otherwise, use provided policies
|
|
106
|
+
// 2. Otherwise, if preset is defined, ignore provided policies
|
|
107
|
+
// 3. Otherwise, use provided policies
|
|
100
108
|
if ((!preset || shouldOverridePresetPolicies) && policies) {
|
|
101
109
|
_url.searchParams.set(
|
|
102
110
|
"policies",
|
|
103
111
|
encodeURIComponent(JSON.stringify(policies)),
|
|
104
112
|
);
|
|
105
113
|
} else if (preset) {
|
|
106
|
-
_url.searchParams.set("preset", preset);
|
|
107
114
|
if (policies) {
|
|
108
115
|
console.warn(
|
|
109
116
|
"[Controller] Both `preset` and `policies` provided to ControllerProvider. " +
|
package/src/lookup.ts
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
AuthOption,
|
|
3
|
+
HeadlessUsernameLookupResult,
|
|
4
|
+
IMPLEMENTED_AUTH_OPTIONS,
|
|
5
|
+
LookupRequest,
|
|
6
|
+
LookupResponse,
|
|
7
|
+
} from "./types";
|
|
8
|
+
import { constants, num } from "starknet";
|
|
3
9
|
import { API_URL } from "./constants";
|
|
4
10
|
|
|
5
11
|
const cache = new Map<string, string>();
|
|
12
|
+
const QUERY_URL = `${API_URL}/query`;
|
|
13
|
+
|
|
14
|
+
type LookupSigner = {
|
|
15
|
+
isOriginal: boolean;
|
|
16
|
+
isRevoked: boolean;
|
|
17
|
+
metadata: {
|
|
18
|
+
__typename: string;
|
|
19
|
+
eip191?: Array<{
|
|
20
|
+
provider: string;
|
|
21
|
+
ethAddress: string;
|
|
22
|
+
}> | null;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type LookupSignersQueryResponse = {
|
|
27
|
+
data?: {
|
|
28
|
+
account?: {
|
|
29
|
+
username: string;
|
|
30
|
+
controllers?: {
|
|
31
|
+
edges?: Array<{
|
|
32
|
+
node?: {
|
|
33
|
+
signers?: LookupSigner[] | null;
|
|
34
|
+
} | null;
|
|
35
|
+
} | null> | null;
|
|
36
|
+
} | null;
|
|
37
|
+
} | null;
|
|
38
|
+
};
|
|
39
|
+
errors?: Array<{ message?: string }>;
|
|
40
|
+
};
|
|
6
41
|
|
|
7
42
|
async function lookup(request: LookupRequest): Promise<LookupResponse> {
|
|
8
43
|
if (!request.addresses?.length && !request.usernames?.length) {
|
|
@@ -24,6 +59,139 @@ async function lookup(request: LookupRequest): Promise<LookupResponse> {
|
|
|
24
59
|
return response.json();
|
|
25
60
|
}
|
|
26
61
|
|
|
62
|
+
async function queryLookupSigners(
|
|
63
|
+
username: string,
|
|
64
|
+
): Promise<LookupSignersQueryResponse> {
|
|
65
|
+
const response = await fetch(QUERY_URL, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
query: `
|
|
72
|
+
query LookupSigners($username: String!) {
|
|
73
|
+
account(username: $username) {
|
|
74
|
+
username
|
|
75
|
+
controllers(first: 1) {
|
|
76
|
+
edges {
|
|
77
|
+
node {
|
|
78
|
+
signers {
|
|
79
|
+
isOriginal
|
|
80
|
+
isRevoked
|
|
81
|
+
metadata {
|
|
82
|
+
__typename
|
|
83
|
+
... on Eip191Credentials {
|
|
84
|
+
eip191 {
|
|
85
|
+
provider
|
|
86
|
+
ethAddress
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
`,
|
|
97
|
+
variables: { username },
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return response.json();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeProvider(provider: string): AuthOption | undefined {
|
|
109
|
+
const normalized = provider.toLowerCase() as AuthOption;
|
|
110
|
+
if (!HEADLESS_AUTH_OPTIONS.includes(normalized)) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return normalized;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const HEADLESS_AUTH_OPTIONS: AuthOption[] = [
|
|
117
|
+
"google",
|
|
118
|
+
"webauthn",
|
|
119
|
+
"discord",
|
|
120
|
+
"walletconnect",
|
|
121
|
+
"password",
|
|
122
|
+
"metamask",
|
|
123
|
+
"rabby",
|
|
124
|
+
"phantom-evm",
|
|
125
|
+
].filter((option) => IMPLEMENTED_AUTH_OPTIONS.includes(option));
|
|
126
|
+
|
|
127
|
+
function normalizeSignerOptions(
|
|
128
|
+
signers: LookupSigner[] | undefined,
|
|
129
|
+
chainId: string,
|
|
130
|
+
): AuthOption[] {
|
|
131
|
+
if (!signers || signers.length === 0) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isMainnet = chainId === constants.StarknetChainId.SN_MAIN;
|
|
136
|
+
const available = signers.filter(
|
|
137
|
+
(signer) => !signer.isRevoked && (isMainnet || signer.isOriginal),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const signerSet = new Set<AuthOption>();
|
|
141
|
+
for (const signer of available) {
|
|
142
|
+
switch (signer.metadata.__typename) {
|
|
143
|
+
case "WebauthnCredentials":
|
|
144
|
+
signerSet.add("webauthn");
|
|
145
|
+
break;
|
|
146
|
+
case "PasswordCredentials":
|
|
147
|
+
signerSet.add("password");
|
|
148
|
+
break;
|
|
149
|
+
case "Eip191Credentials":
|
|
150
|
+
signer.metadata.eip191?.forEach((entry) => {
|
|
151
|
+
const provider = normalizeProvider(entry.provider);
|
|
152
|
+
if (provider) {
|
|
153
|
+
signerSet.add(provider);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
default:
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return HEADLESS_AUTH_OPTIONS.filter((option) => signerSet.has(option));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function lookupUsername(
|
|
166
|
+
username: string,
|
|
167
|
+
chainId: string,
|
|
168
|
+
): Promise<HeadlessUsernameLookupResult> {
|
|
169
|
+
const response = await queryLookupSigners(username);
|
|
170
|
+
if (response.errors?.length) {
|
|
171
|
+
throw new Error(response.errors[0].message || "Lookup query failed");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const account = response.data?.account;
|
|
175
|
+
if (!account) {
|
|
176
|
+
return {
|
|
177
|
+
username,
|
|
178
|
+
exists: false,
|
|
179
|
+
signers: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const controller = account.controllers?.edges?.[0]?.node;
|
|
184
|
+
const signers = normalizeSignerOptions(
|
|
185
|
+
controller?.signers ?? undefined,
|
|
186
|
+
chainId,
|
|
187
|
+
);
|
|
188
|
+
return {
|
|
189
|
+
username: account.username,
|
|
190
|
+
exists: true,
|
|
191
|
+
signers,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
27
195
|
export async function lookupUsernames(
|
|
28
196
|
usernames: string[],
|
|
29
197
|
): Promise<Map<string, string>> {
|
package/src/node/server.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as http from "http";
|
|
2
2
|
import { AddressInfo } from "net";
|
|
3
|
+
import { REDIRECT_QUERY_NAME } from "../constants";
|
|
3
4
|
|
|
4
5
|
type ServerResponse = http.ServerResponse<http.IncomingMessage> & {
|
|
5
6
|
req: http.IncomingMessage;
|
|
@@ -38,7 +39,7 @@ export class CallbackServer {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const params = new URLSearchParams(req.url.split("?")[1]);
|
|
41
|
-
const session = params.get(
|
|
42
|
+
const session = params.get(REDIRECT_QUERY_NAME);
|
|
42
43
|
|
|
43
44
|
if (!session) {
|
|
44
45
|
console.warn("Received callback without session data");
|