@cartridge/controller 0.5.8 → 0.6.0

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.
Files changed (56) hide show
  1. package/.turbo/turbo-build$colon$deps.log +77 -110
  2. package/.turbo/turbo-build.log +78 -111
  3. package/dist/__tests__/parseChainId.test.d.ts +2 -0
  4. package/dist/__tests__/parseChainId.test.js +89 -0
  5. package/dist/__tests__/parseChainId.test.js.map +1 -0
  6. package/dist/account.js +6 -0
  7. package/dist/account.js.map +1 -1
  8. package/dist/controller.d.ts +1 -1
  9. package/dist/controller.js +218 -136
  10. package/dist/controller.js.map +1 -1
  11. package/dist/iframe/base.js +4 -0
  12. package/dist/iframe/base.js.map +1 -1
  13. package/dist/iframe/index.js +4 -0
  14. package/dist/iframe/index.js.map +1 -1
  15. package/dist/iframe/keychain.js +4 -0
  16. package/dist/iframe/keychain.js.map +1 -1
  17. package/dist/iframe/profile.js +4 -0
  18. package/dist/iframe/profile.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +222 -138
  21. package/dist/index.js.map +1 -1
  22. package/dist/mutex.d.ts +14 -0
  23. package/dist/mutex.js +22 -0
  24. package/dist/mutex.js.map +1 -0
  25. package/dist/policies.d.ts +19 -0
  26. package/dist/policies.js +26 -0
  27. package/dist/policies.js.map +1 -0
  28. package/dist/provider.d.ts +2 -0
  29. package/dist/provider.js +167 -109
  30. package/dist/provider.js.map +1 -1
  31. package/dist/session/account.js +4 -0
  32. package/dist/session/account.js.map +1 -1
  33. package/dist/session/index.d.ts +1 -0
  34. package/dist/session/index.js +292 -121
  35. package/dist/session/index.js.map +1 -1
  36. package/dist/session/provider.d.ts +7 -2
  37. package/dist/session/provider.js +292 -121
  38. package/dist/session/provider.js.map +1 -1
  39. package/dist/telegram/provider.d.ts +2 -1
  40. package/dist/telegram/provider.js +204 -112
  41. package/dist/telegram/provider.js.map +1 -1
  42. package/dist/utils.d.ts +5 -3
  43. package/dist/utils.js +29 -2
  44. package/dist/utils.js.map +1 -1
  45. package/jest.config.ts +13 -0
  46. package/package.json +26 -8
  47. package/src/__tests__/parseChainId.test.ts +60 -0
  48. package/src/controller.ts +25 -29
  49. package/src/mutex.ts +22 -0
  50. package/src/policies.ts +49 -0
  51. package/src/provider.ts +33 -2
  52. package/src/session/account.ts +1 -0
  53. package/src/session/provider.ts +139 -10
  54. package/src/telegram/provider.ts +3 -2
  55. package/src/utils.ts +32 -1
  56. package/tsconfig.json +1 -2
@@ -0,0 +1,49 @@
1
+ import {
2
+ ContractPolicy,
3
+ Method,
4
+ SessionPolicies,
5
+ SignMessagePolicy,
6
+ } from "@cartridge/presets";
7
+
8
+ export type ParsedSessionPolicies = {
9
+ verified: boolean;
10
+ contracts?: SessionContracts;
11
+ messages?: SessionMessages;
12
+ };
13
+
14
+ export type SessionContracts = Record<
15
+ string,
16
+ Omit<ContractPolicy, "methods"> & {
17
+ methods: (Method & { authorized?: boolean })[];
18
+ }
19
+ >;
20
+
21
+ export type SessionMessages = (SignMessagePolicy & {
22
+ authorized?: boolean;
23
+ })[];
24
+
25
+ export function parsePolicies(
26
+ policies: SessionPolicies,
27
+ ): ParsedSessionPolicies {
28
+ return {
29
+ verified: false,
30
+ contracts: policies.contracts
31
+ ? Object.fromEntries(
32
+ Object.entries(policies.contracts).map(([address, contract]) => [
33
+ address,
34
+ {
35
+ ...contract,
36
+ methods: contract.methods.map((method) => ({
37
+ ...method,
38
+ authorized: true,
39
+ })),
40
+ },
41
+ ]),
42
+ )
43
+ : undefined,
44
+ messages: policies.messages?.map((message) => ({
45
+ ...message,
46
+ authorized: true,
47
+ })),
48
+ };
49
+ }
package/src/provider.ts CHANGED
@@ -16,6 +16,9 @@ import {
16
16
  import manifest from "../package.json";
17
17
 
18
18
  import { icon } from "./icon";
19
+ import { Mutex } from "./mutex";
20
+
21
+ const mutex = new Mutex();
19
22
 
20
23
  export default abstract class BaseProvider implements StarknetWindowObject {
21
24
  public id = "controller";
@@ -26,10 +29,37 @@ export default abstract class BaseProvider implements StarknetWindowObject {
26
29
  public account?: WalletAccount;
27
30
  public subscriptions: WalletEvents[] = [];
28
31
 
32
+ private _probePromise: Promise<WalletAccount | undefined> | null = null;
33
+
34
+ protected async safeProbe(): Promise<WalletAccount | undefined> {
35
+ // If we already have an account, return it
36
+ if (this.account) {
37
+ return this.account;
38
+ }
39
+
40
+ // If we're already probing, wait for the existing probe
41
+ if (this._probePromise) {
42
+ return this._probePromise;
43
+ }
44
+
45
+ const release = await mutex.obtain();
46
+ return await new Promise<WalletAccount | undefined>(async (resolve) => {
47
+ try {
48
+ this._probePromise = this.probe();
49
+ const result = await this._probePromise;
50
+ resolve(result);
51
+ } finally {
52
+ this._probePromise = null;
53
+ }
54
+ }).finally(() => {
55
+ release();
56
+ });
57
+ }
58
+
29
59
  request: RequestFn = async (call) => {
30
60
  switch (call.type) {
31
61
  case "wallet_getPermissions":
32
- await this.probe();
62
+ await this.safeProbe();
33
63
 
34
64
  if (this.account) {
35
65
  return [Permission.ACCOUNTS];
@@ -45,7 +75,8 @@ export default abstract class BaseProvider implements StarknetWindowObject {
45
75
  const silentMode =
46
76
  call.params && (call.params as RequestAccountsParameters).silent_mode;
47
77
 
48
- this.account = await this.probe();
78
+ this.account = await this.safeProbe();
79
+
49
80
  if (!this.account && !silentMode) {
50
81
  this.account = await this.connect();
51
82
  }
@@ -33,6 +33,7 @@ export default class SessionAccount extends WalletAccount {
33
33
  ) {
34
34
  super({ nodeUrl: rpcUrl }, provider);
35
35
 
36
+ this.address = address;
36
37
  this.controller = CartridgeSessionAccount.new_as_registered(
37
38
  rpcUrl,
38
39
  privateKey,
@@ -6,6 +6,7 @@ import BaseProvider from "../provider";
6
6
  import { toWasmPolicies } from "../utils";
7
7
  import { SessionPolicies } from "@cartridge/presets";
8
8
  import { AddStarknetChainParameters } from "@starknet-io/types-js";
9
+ import { ParsedSessionPolicies } from "../policies";
9
10
 
10
11
  interface SessionRegistration {
11
12
  username: string;
@@ -20,6 +21,7 @@ export type SessionOptions = {
20
21
  chainId: string;
21
22
  policies: SessionPolicies;
22
23
  redirectUrl: string;
24
+ keychainUrl?: string;
23
25
  };
24
26
 
25
27
  export default class SessionProvider extends BaseProvider {
@@ -30,39 +32,110 @@ export default class SessionProvider extends BaseProvider {
30
32
  protected _rpcUrl: string;
31
33
  protected _username?: string;
32
34
  protected _redirectUrl: string;
33
- protected _policies: SessionPolicies;
35
+ protected _policies: ParsedSessionPolicies;
36
+ protected _keychainUrl: string;
34
37
 
35
- constructor({ rpc, chainId, policies, redirectUrl }: SessionOptions) {
38
+ constructor({
39
+ rpc,
40
+ chainId,
41
+ policies,
42
+ redirectUrl,
43
+ keychainUrl,
44
+ }: SessionOptions) {
36
45
  super();
37
46
 
47
+ this._policies = {
48
+ verified: false,
49
+ contracts: policies.contracts
50
+ ? Object.fromEntries(
51
+ Object.entries(policies.contracts).map(([address, contract]) => [
52
+ address,
53
+ {
54
+ ...contract,
55
+ methods: contract.methods.map((method) => ({
56
+ ...method,
57
+ authorized: true,
58
+ })),
59
+ },
60
+ ]),
61
+ )
62
+ : undefined,
63
+ messages: policies.messages?.map((message) => ({
64
+ ...message,
65
+ authorized: true,
66
+ })),
67
+ };
68
+
38
69
  this._rpcUrl = rpc;
39
70
  this._chainId = chainId;
40
71
  this._redirectUrl = redirectUrl;
41
- this._policies = policies;
72
+ this._keychainUrl = keychainUrl || KEYCHAIN_URL;
42
73
 
43
74
  if (typeof window !== "undefined") {
44
75
  (window as any).starknet_controller_session = this;
45
76
  }
46
77
  }
47
78
 
79
+ private validatePoliciesSubset(
80
+ newPolicies: ParsedSessionPolicies,
81
+ existingPolicies: ParsedSessionPolicies,
82
+ ): boolean {
83
+ if (newPolicies.contracts) {
84
+ if (!existingPolicies.contracts) return false;
85
+
86
+ for (const [address, contract] of Object.entries(newPolicies.contracts)) {
87
+ const existingContract = existingPolicies.contracts[address];
88
+ if (!existingContract) return false;
89
+
90
+ for (const method of contract.methods) {
91
+ const existingMethod = existingContract.methods.find(
92
+ (m) => m.entrypoint === method.entrypoint,
93
+ );
94
+ if (!existingMethod || !existingMethod.authorized) return false;
95
+ }
96
+ }
97
+ }
98
+
99
+ if (newPolicies.messages) {
100
+ if (!existingPolicies.messages) return false;
101
+
102
+ for (const message of newPolicies.messages) {
103
+ const existingMessage = existingPolicies.messages.find(
104
+ (m) =>
105
+ JSON.stringify(m.domain) === JSON.stringify(message.domain) &&
106
+ JSON.stringify(m.types) === JSON.stringify(message.types),
107
+ );
108
+ if (!existingMessage || !existingMessage.authorized) return false;
109
+ }
110
+ }
111
+
112
+ return true;
113
+ }
114
+
48
115
  async username() {
49
116
  await this.tryRetrieveFromQueryOrStorage();
50
117
  return this._username;
51
118
  }
52
119
 
53
120
  async probe(): Promise<WalletAccount | undefined> {
54
- await this.tryRetrieveFromQueryOrStorage();
55
- return;
121
+ if (this.account) {
122
+ return this.account;
123
+ }
124
+
125
+ this.account = await this.tryRetrieveFromQueryOrStorage();
126
+ return this.account;
56
127
  }
57
128
 
58
129
  async connect(): Promise<WalletAccount | undefined> {
59
- await this.tryRetrieveFromQueryOrStorage();
130
+ if (this.account) {
131
+ return this.account;
132
+ }
60
133
 
134
+ this.account = await this.tryRetrieveFromQueryOrStorage();
61
135
  if (this.account) {
62
- return;
136
+ return this.account;
63
137
  }
64
138
 
65
- // Generate a random local key pair
66
139
  const pk = stark.randomAddress();
67
140
  const publicKey = ec.starkCurve.getStarkKey(pk);
68
141
 
@@ -74,7 +147,11 @@ export default class SessionProvider extends BaseProvider {
74
147
  }),
75
148
  );
76
149
 
77
- const url = `${KEYCHAIN_URL}/session?public_key=${publicKey}&redirect_uri=${
150
+ localStorage.setItem("sessionPolicies", JSON.stringify(this._policies));
151
+
152
+ const url = `${
153
+ this._keychainUrl
154
+ }/session?public_key=${publicKey}&redirect_uri=${
78
155
  this._redirectUrl
79
156
  }&redirect_query_name=startapp&policies=${JSON.stringify(
80
157
  this._policies,
@@ -83,7 +160,7 @@ export default class SessionProvider extends BaseProvider {
83
160
  localStorage.setItem("lastUsedConnector", this.id);
84
161
  window.open(url, "_blank");
85
162
 
86
- return;
163
+ return this.account;
87
164
  }
88
165
 
89
166
  switchStarknetChain(_chainId: string): Promise<boolean> {
@@ -97,12 +174,17 @@ export default class SessionProvider extends BaseProvider {
97
174
  disconnect(): Promise<void> {
98
175
  localStorage.removeItem("sessionSigner");
99
176
  localStorage.removeItem("session");
177
+ localStorage.removeItem("sessionPolicies");
100
178
  this.account = undefined;
101
179
  this._username = undefined;
102
180
  return Promise.resolve();
103
181
  }
104
182
 
105
183
  async tryRetrieveFromQueryOrStorage() {
184
+ if (this.account) {
185
+ return this.account;
186
+ }
187
+
106
188
  const signerString = localStorage.getItem("sessionSigner");
107
189
  const signer = signerString ? JSON.parse(signerString) : null;
108
190
  let sessionRegistration: SessionRegistration | null = null;
@@ -135,6 +217,47 @@ export default class SessionProvider extends BaseProvider {
135
217
  return;
136
218
  }
137
219
 
220
+ // Check expiration
221
+ const expirationTime = parseInt(sessionRegistration.expiresAt) * 1000;
222
+ console.log("Session expiration check:", {
223
+ expirationTime,
224
+ currentTime: Date.now(),
225
+ expired: Date.now() >= expirationTime,
226
+ });
227
+ if (Date.now() >= expirationTime) {
228
+ console.log("Session expired, clearing stored session");
229
+ this.clearStoredSession();
230
+ return;
231
+ }
232
+
233
+ // Check stored policies
234
+ const storedPoliciesStr = localStorage.getItem("sessionPolicies");
235
+ console.log("Checking stored policies:", {
236
+ storedPoliciesStr,
237
+ currentPolicies: this._policies,
238
+ });
239
+ if (storedPoliciesStr) {
240
+ const storedPolicies = JSON.parse(
241
+ storedPoliciesStr,
242
+ ) as ParsedSessionPolicies;
243
+
244
+ const isValid = this.validatePoliciesSubset(
245
+ this._policies,
246
+ storedPolicies,
247
+ );
248
+ console.log("Policy validation result:", {
249
+ isValid,
250
+ storedPolicies,
251
+ requestedPolicies: this._policies,
252
+ });
253
+
254
+ if (!isValid) {
255
+ console.log("Policy validation failed, clearing stored session");
256
+ this.clearStoredSession();
257
+ return;
258
+ }
259
+ }
260
+
138
261
  this._username = sessionRegistration.username;
139
262
  this.account = new SessionAccount(this, {
140
263
  rpcUrl: this._rpcUrl,
@@ -148,4 +271,10 @@ export default class SessionProvider extends BaseProvider {
148
271
 
149
272
  return this.account;
150
273
  }
274
+
275
+ private clearStoredSession(): void {
276
+ localStorage.removeItem("sessionSigner");
277
+ localStorage.removeItem("session");
278
+ localStorage.removeItem("sessionPolicies");
279
+ }
151
280
  }
@@ -12,6 +12,7 @@ import BaseProvider from "../provider";
12
12
  import { toWasmPolicies } from "../utils";
13
13
  import { SessionPolicies } from "@cartridge/presets";
14
14
  import { AddStarknetChainParameters } from "@starknet-io/types-js";
15
+ import { ParsedSessionPolicies, parsePolicies } from "../policies";
15
16
 
16
17
  interface SessionRegistration {
17
18
  username: string;
@@ -25,7 +26,7 @@ export default class TelegramProvider extends BaseProvider {
25
26
  private _tmaUrl: string;
26
27
  protected _chainId: string;
27
28
  protected _username?: string;
28
- protected _policies: SessionPolicies;
29
+ protected _policies: ParsedSessionPolicies;
29
30
  private _rpcUrl: string;
30
31
 
31
32
  constructor({
@@ -44,7 +45,7 @@ export default class TelegramProvider extends BaseProvider {
44
45
  this._rpcUrl = rpc;
45
46
  this._tmaUrl = tmaUrl;
46
47
  this._chainId = chainId;
47
- this._policies = policies;
48
+ this._policies = parsePolicies(policies);
48
49
 
49
50
  if (typeof window !== "undefined") {
50
51
  (window as any).starknet_controller = this;
package/src/utils.ts CHANGED
@@ -2,13 +2,17 @@ import {
2
2
  addAddressPadding,
3
3
  Call,
4
4
  CallData,
5
+ constants,
5
6
  getChecksumAddress,
6
7
  hash,
8
+ shortString,
7
9
  typedData,
8
10
  TypedDataRevision,
9
11
  } from "starknet";
10
12
  import wasm from "@cartridge/account-wasm/controller";
11
13
  import { Policies, SessionPolicies } from "@cartridge/presets";
14
+ import { ChainId } from "@starknet-io/types-js";
15
+ import { ParsedSessionPolicies } from "./policies";
12
16
 
13
17
  // Whitelist of allowed property names to prevent prototype pollution
14
18
  const ALLOWED_PROPERTIES = new Set([
@@ -85,13 +89,14 @@ export function toSessionPolicies(policies: Policies): SessionPolicies {
85
89
  : policies;
86
90
  }
87
91
 
88
- export function toWasmPolicies(policies: SessionPolicies): wasm.Policy[] {
92
+ export function toWasmPolicies(policies: ParsedSessionPolicies): wasm.Policy[] {
89
93
  return [
90
94
  ...Object.entries(policies.contracts ?? {}).flatMap(
91
95
  ([target, { methods }]) =>
92
96
  toArray(methods).map((m) => ({
93
97
  target,
94
98
  method: m.entrypoint,
99
+ authorized: m.authorized,
95
100
  })),
96
101
  ),
97
102
  ...(policies.messages ?? []).map((p) => {
@@ -109,6 +114,7 @@ export function toWasmPolicies(policies: SessionPolicies): wasm.Policy[] {
109
114
 
110
115
  return {
111
116
  scope_hash: hash.computePoseidonHash(domainHash, typeHash),
117
+ authorized: p.authorized,
112
118
  };
113
119
  }),
114
120
  ];
@@ -129,3 +135,28 @@ export function humanizeString(str: string): string {
129
135
  .replace(/^\w/, (c) => c.toUpperCase())
130
136
  );
131
137
  }
138
+
139
+ export function parseChainId(url: URL): ChainId {
140
+ const parts = url.pathname.split("/");
141
+
142
+ if (parts.includes("starknet")) {
143
+ if (parts.includes("mainnet")) {
144
+ return constants.StarknetChainId.SN_MAIN;
145
+ } else if (parts.includes("sepolia")) {
146
+ return constants.StarknetChainId.SN_SEPOLIA;
147
+ }
148
+ } else if (parts.length >= 3) {
149
+ const projectName = parts[2];
150
+ if (parts.includes("katana")) {
151
+ return shortString.encodeShortString(
152
+ `WP_${projectName.toUpperCase().replace(/-/g, "_")}`,
153
+ ) as ChainId;
154
+ } else if (parts.includes("mainnet")) {
155
+ return shortString.encodeShortString(
156
+ `GG_${projectName.toUpperCase().replace(/-/g, "_")}`,
157
+ ) as ChainId;
158
+ }
159
+ }
160
+
161
+ throw new Error(`Chain ${url.toString()} not supported`);
162
+ }
package/tsconfig.json CHANGED
@@ -5,8 +5,7 @@
5
5
  "rootDir": ".",
6
6
  "outDir": "./dist",
7
7
  "composite": false,
8
- "incremental": false,
9
- "moduleResolution": "node"
8
+ "incremental": false
10
9
  },
11
10
  "include": ["src/**/*", "package.json"]
12
11
  }