@cartridge/controller 0.13.5 → 0.13.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/.turbo/turbo-build$colon$deps.log +27 -17
- package/.turbo/turbo-build.log +25 -15
- package/HEADLESS_MODE.md +28 -7
- package/dist/controller.d.ts +2 -1
- package/dist/index-CYAUAqql.js +1072 -0
- package/dist/index-CYAUAqql.js.map +1 -0
- package/dist/index.js +1925 -2488
- package/dist/index.js.map +1 -1
- package/dist/lookup.d.ts +2 -0
- package/dist/node/index.cjs +3 -3
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.js +4 -4
- package/dist/node/index.js.map +1 -1
- package/dist/session/provider.d.ts +7 -2
- package/dist/session.js +141 -142
- package/dist/session.js.map +1 -1
- package/dist/stats.html +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/utils.d.ts +5 -3
- package/package.json +2 -2
- package/src/__tests__/lookupUsername.test.ts +166 -0
- package/src/__tests__/toWasmPolicies.test.ts +89 -40
- package/src/controller.ts +13 -0
- package/src/iframe/keychain.ts +7 -0
- package/src/lookup.ts +170 -2
- package/src/session/provider.ts +75 -48
- package/src/types.ts +6 -0
- package/src/utils.ts +19 -5
- package/dist/provider-NKp7_oNj.js +0 -387
- package/dist/provider-NKp7_oNj.js.map +0 -1
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/session/provider.ts
CHANGED
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
signerToGuid,
|
|
5
5
|
subscribeCreateSession,
|
|
6
6
|
} from "@cartridge/controller-wasm";
|
|
7
|
-
import { SessionPolicies } from "@cartridge/presets";
|
|
7
|
+
import { loadConfig, SessionPolicies } from "@cartridge/presets";
|
|
8
8
|
import { AddStarknetChainParameters } from "@starknet-io/types-js";
|
|
9
9
|
import { encode } from "starknet";
|
|
10
10
|
import { API_URL, KEYCHAIN_URL } from "../constants";
|
|
11
|
-
import { ParsedSessionPolicies } from "../policies";
|
|
11
|
+
import { parsePolicies, ParsedSessionPolicies } from "../policies";
|
|
12
12
|
import BaseProvider from "../provider";
|
|
13
13
|
import { AuthOptions } from "../types";
|
|
14
|
-
import { toWasmPolicies } from "../utils";
|
|
14
|
+
import { getPresetSessionPolicies, toWasmPolicies } from "../utils";
|
|
15
15
|
import SessionAccount from "./account";
|
|
16
16
|
|
|
17
17
|
interface SessionRegistration {
|
|
@@ -29,7 +29,9 @@ interface SessionRegistration {
|
|
|
29
29
|
export type SessionOptions = {
|
|
30
30
|
rpc: string;
|
|
31
31
|
chainId: string;
|
|
32
|
-
policies
|
|
32
|
+
policies?: SessionPolicies;
|
|
33
|
+
preset?: string;
|
|
34
|
+
shouldOverridePresetPolicies?: boolean;
|
|
33
35
|
redirectUrl: string;
|
|
34
36
|
disconnectRedirectUrl?: string;
|
|
35
37
|
keychainUrl?: string;
|
|
@@ -47,10 +49,12 @@ export default class SessionProvider extends BaseProvider {
|
|
|
47
49
|
protected _redirectUrl: string;
|
|
48
50
|
protected _disconnectRedirectUrl?: string;
|
|
49
51
|
protected _policies: ParsedSessionPolicies;
|
|
52
|
+
protected _preset?: string;
|
|
53
|
+
private _ready: Promise<void>;
|
|
50
54
|
protected _keychainUrl: string;
|
|
51
55
|
protected _apiUrl: string;
|
|
52
|
-
protected _publicKey
|
|
53
|
-
protected _sessionKeyGuid
|
|
56
|
+
protected _publicKey!: string;
|
|
57
|
+
protected _sessionKeyGuid!: string;
|
|
54
58
|
protected _signupOptions?: AuthOptions;
|
|
55
59
|
public reopenBrowser: boolean = true;
|
|
56
60
|
|
|
@@ -58,6 +62,8 @@ export default class SessionProvider extends BaseProvider {
|
|
|
58
62
|
rpc,
|
|
59
63
|
chainId,
|
|
60
64
|
policies,
|
|
65
|
+
preset,
|
|
66
|
+
shouldOverridePresetPolicies,
|
|
61
67
|
redirectUrl,
|
|
62
68
|
disconnectRedirectUrl,
|
|
63
69
|
keychainUrl,
|
|
@@ -66,27 +72,27 @@ export default class SessionProvider extends BaseProvider {
|
|
|
66
72
|
}: SessionOptions) {
|
|
67
73
|
super();
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
75
|
+
if (!policies && !preset) {
|
|
76
|
+
throw new Error("Either `policies` or `preset` must be provided");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Policy precedence logic (matching ControllerProvider):
|
|
80
|
+
// 1. If shouldOverridePresetPolicies is true and policies are provided, use policies
|
|
81
|
+
// 2. Otherwise, if preset is defined, resolve policies from preset
|
|
82
|
+
// 3. Otherwise, use provided policies
|
|
83
|
+
if ((!preset || shouldOverridePresetPolicies) && policies) {
|
|
84
|
+
this._policies = parsePolicies(policies);
|
|
85
|
+
} else {
|
|
86
|
+
this._preset = preset;
|
|
87
|
+
if (policies) {
|
|
88
|
+
console.warn(
|
|
89
|
+
"[Controller] Both `preset` and `policies` provided to SessionProvider. " +
|
|
90
|
+
"Policies are ignored when preset is set. " +
|
|
91
|
+
"Use `shouldOverridePresetPolicies: true` to override.",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
this._policies = { verified: false };
|
|
95
|
+
}
|
|
90
96
|
|
|
91
97
|
this._rpcUrl = rpc;
|
|
92
98
|
this._chainId = chainId;
|
|
@@ -96,6 +102,33 @@ export default class SessionProvider extends BaseProvider {
|
|
|
96
102
|
this._apiUrl = apiUrl ?? API_URL;
|
|
97
103
|
this._signupOptions = signupOptions;
|
|
98
104
|
|
|
105
|
+
// Eagerly start async init: resolve preset policies (if any),
|
|
106
|
+
// then try to restore an existing session from storage.
|
|
107
|
+
// All public async methods await this before proceeding.
|
|
108
|
+
this._ready = this._init();
|
|
109
|
+
|
|
110
|
+
if (typeof window !== "undefined") {
|
|
111
|
+
(window as any).starknet_controller_session = this;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async _init(): Promise<void> {
|
|
116
|
+
if (this._preset) {
|
|
117
|
+
const config = await loadConfig(this._preset);
|
|
118
|
+
if (!config) {
|
|
119
|
+
throw new Error(`Failed to load preset: ${this._preset}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sessionPolicies = getPresetSessionPolicies(config, this._chainId);
|
|
123
|
+
if (!sessionPolicies) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`No policies found for chain ${this._chainId} in preset ${this._preset}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this._policies = parsePolicies(sessionPolicies);
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
const account = this.tryRetrieveFromQueryOrStorage();
|
|
100
133
|
if (!account) {
|
|
101
134
|
const pk = stark.randomAddress();
|
|
@@ -125,10 +158,6 @@ export default class SessionProvider extends BaseProvider {
|
|
|
125
158
|
starknet: { privateKey: encode.addHexPrefix(jsonPk.privKey) },
|
|
126
159
|
});
|
|
127
160
|
}
|
|
128
|
-
|
|
129
|
-
if (typeof window !== "undefined") {
|
|
130
|
-
(window as any).starknet_controller_session = this;
|
|
131
|
-
}
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
private validatePoliciesSubset(
|
|
@@ -218,25 +247,18 @@ export default class SessionProvider extends BaseProvider {
|
|
|
218
247
|
}
|
|
219
248
|
|
|
220
249
|
async username() {
|
|
221
|
-
await this.
|
|
250
|
+
await this._ready;
|
|
222
251
|
return this._username;
|
|
223
252
|
}
|
|
224
253
|
|
|
225
254
|
async probe(): Promise<WalletAccount | undefined> {
|
|
226
|
-
|
|
227
|
-
return this.account;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
this.account = this.tryRetrieveFromQueryOrStorage();
|
|
255
|
+
await this._ready;
|
|
231
256
|
return this.account;
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
async connect(): Promise<WalletAccount | undefined> {
|
|
235
|
-
|
|
236
|
-
return this.account;
|
|
237
|
-
}
|
|
260
|
+
await this._ready;
|
|
238
261
|
|
|
239
|
-
this.account = this.tryRetrieveFromQueryOrStorage();
|
|
240
262
|
if (this.account) {
|
|
241
263
|
return this.account;
|
|
242
264
|
}
|
|
@@ -259,13 +281,18 @@ export default class SessionProvider extends BaseProvider {
|
|
|
259
281
|
this._sessionKeyGuid = signerToGuid({
|
|
260
282
|
starknet: { privateKey: encode.addHexPrefix(pk) },
|
|
261
283
|
});
|
|
262
|
-
let url =
|
|
263
|
-
this._keychainUrl
|
|
264
|
-
|
|
265
|
-
this._redirectUrl
|
|
266
|
-
|
|
267
|
-
this.
|
|
268
|
-
|
|
284
|
+
let url =
|
|
285
|
+
`${this._keychainUrl}` +
|
|
286
|
+
`/session?public_key=${this._publicKey}` +
|
|
287
|
+
`&redirect_uri=${this._redirectUrl}` +
|
|
288
|
+
`&redirect_query_name=startapp` +
|
|
289
|
+
`&rpc_url=${this._rpcUrl}`;
|
|
290
|
+
|
|
291
|
+
if (this._preset) {
|
|
292
|
+
url += `&preset=${encodeURIComponent(this._preset)}`;
|
|
293
|
+
} else {
|
|
294
|
+
url += `&policies=${encodeURIComponent(JSON.stringify(this._policies))}`;
|
|
295
|
+
}
|
|
269
296
|
|
|
270
297
|
if (this._signupOptions) {
|
|
271
298
|
url += `&signers=${encodeURIComponent(JSON.stringify(this._signupOptions))}`;
|
package/src/types.ts
CHANGED
|
@@ -120,6 +120,12 @@ export interface LookupResponse {
|
|
|
120
120
|
results: LookupResult[];
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export interface HeadlessUsernameLookupResult {
|
|
124
|
+
username: string;
|
|
125
|
+
exists: boolean;
|
|
126
|
+
signers: AuthOption[];
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
export enum FeeSource {
|
|
124
130
|
PAYMASTER = "PAYMASTER",
|
|
125
131
|
CREDITS = "CREDITS",
|
package/src/utils.ts
CHANGED
|
@@ -48,6 +48,19 @@ export function normalizeCalls(calls: Call | Call[]) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export function getPresetSessionPolicies(
|
|
52
|
+
config: Record<string, unknown>,
|
|
53
|
+
chainId: string,
|
|
54
|
+
): SessionPolicies | undefined {
|
|
55
|
+
const decodedChainId = shortString.decodeShortString(chainId);
|
|
56
|
+
const chains = config.chains as
|
|
57
|
+
| Record<string, Record<string, unknown>>
|
|
58
|
+
| undefined;
|
|
59
|
+
const chainConfig = chains?.[decodedChainId];
|
|
60
|
+
if (!chainConfig?.policies) return undefined;
|
|
61
|
+
return toSessionPolicies(chainConfig.policies as Policies);
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
export function toSessionPolicies(policies: Policies): SessionPolicies {
|
|
52
65
|
return Array.isArray(policies)
|
|
53
66
|
? policies.reduce<SessionPolicies>(
|
|
@@ -92,9 +105,10 @@ export function toSessionPolicies(policies: Policies): SessionPolicies {
|
|
|
92
105
|
/**
|
|
93
106
|
* Converts parsed session policies to WASM-compatible Policy objects.
|
|
94
107
|
*
|
|
95
|
-
* IMPORTANT: Policies are sorted canonically
|
|
96
|
-
*
|
|
97
|
-
*
|
|
108
|
+
* IMPORTANT: Policies are sorted canonically and addresses are normalized
|
|
109
|
+
* via getChecksumAddress before hashing. Without this, Object.keys/entries
|
|
110
|
+
* reordering or inconsistent address casing can cause identical policies to
|
|
111
|
+
* produce different merkle roots, leading to "session/not-registered" errors.
|
|
98
112
|
* See: https://github.com/cartridge-gg/controller/issues/2357
|
|
99
113
|
*/
|
|
100
114
|
export function toWasmPolicies(policies: ParsedSessionPolicies): Policy[] {
|
|
@@ -110,7 +124,7 @@ export function toWasmPolicies(policies: ParsedSessionPolicies): Policy[] {
|
|
|
110
124
|
if (m.entrypoint === "approve") {
|
|
111
125
|
if ("spender" in m && "amount" in m && m.spender && m.amount) {
|
|
112
126
|
const approvalPolicy: ApprovalPolicy = {
|
|
113
|
-
target,
|
|
127
|
+
target: getChecksumAddress(target),
|
|
114
128
|
spender: m.spender,
|
|
115
129
|
amount: String(m.amount),
|
|
116
130
|
};
|
|
@@ -127,7 +141,7 @@ export function toWasmPolicies(policies: ParsedSessionPolicies): Policy[] {
|
|
|
127
141
|
|
|
128
142
|
// For non-approve methods and legacy approve, create a regular CallPolicy
|
|
129
143
|
return {
|
|
130
|
-
target,
|
|
144
|
+
target: getChecksumAddress(target),
|
|
131
145
|
method: hash.getSelectorFromName(m.entrypoint),
|
|
132
146
|
authorized: !!m.authorized,
|
|
133
147
|
};
|