@cartridge/controller
Advanced tools
Comparing version 0.5.9 to 0.6.0
@@ -23,2 +23,6 @@ // src/account.ts | ||
var ControllerAccount = class extends WalletAccount { | ||
address; | ||
keychain; | ||
modal; | ||
options; | ||
constructor(provider, rpcUrl, address, keychain, options, modal) { | ||
@@ -25,0 +29,0 @@ super({ nodeUrl: rpcUrl }, provider); |
@@ -45,2 +45,6 @@ // src/account.ts | ||
var ControllerAccount = class extends WalletAccount { | ||
address; | ||
keychain; | ||
modal; | ||
options; | ||
constructor(provider, rpcUrl, address, keychain, options, modal) { | ||
@@ -130,2 +134,6 @@ super({ nodeUrl: rpcUrl }, provider); | ||
var IFrame = class { | ||
url; | ||
iframe; | ||
container; | ||
onClose; | ||
constructor({ | ||
@@ -323,3 +331,3 @@ id, | ||
name: "@cartridge/controller", | ||
version: "0.5.9", | ||
version: "0.6.0", | ||
description: "Cartridge Controller", | ||
@@ -338,6 +346,18 @@ module: "dist/index.js", | ||
exports: { | ||
".": "./dist/index.js", | ||
"./session": "./dist/session/index.js", | ||
"./provider": "./dist/provider/index.js", | ||
"./types": "./dist/types/index.js" | ||
".": { | ||
types: "./dist/index.d.ts", | ||
default: "./dist/index.js" | ||
}, | ||
"./session": { | ||
types: "./dist/session/index.d.ts", | ||
default: "./dist/session/index.js" | ||
}, | ||
"./provider": { | ||
types: "./dist/provider/index.d.ts", | ||
default: "./dist/provider/index.js" | ||
}, | ||
"./types": { | ||
types: "./dist/types/index.d.ts", | ||
default: "./dist/types/index.js" | ||
} | ||
}, | ||
@@ -355,2 +375,5 @@ tsup: { | ||
}, | ||
peerDependencies: { | ||
starknet: "^6.21.0" | ||
}, | ||
dependencies: { | ||
@@ -364,4 +387,3 @@ "@cartridge/account-wasm": "workspace:*", | ||
"fast-deep-equal": "^3.1.3", | ||
"query-string": "^7.1.1", | ||
starknet: "^6.11.0" | ||
"query-string": "^7.1.1" | ||
}, | ||
@@ -381,123 +403,163 @@ devDependencies: { | ||
// src/mutex.ts | ||
function releaseStub() { | ||
} | ||
var Mutex = class { | ||
m_lastPromise = Promise.resolve(); | ||
/** | ||
* Acquire lock | ||
* @param [bypass=false] option to skip lock acquisition | ||
*/ | ||
async obtain(bypass = false) { | ||
let release = releaseStub; | ||
if (bypass) return release; | ||
const lastPromise = this.m_lastPromise; | ||
this.m_lastPromise = new Promise((resolve) => release = resolve); | ||
await lastPromise; | ||
return release; | ||
} | ||
}; | ||
// src/provider.ts | ||
var mutex = new Mutex(); | ||
var BaseProvider = class { | ||
constructor() { | ||
this.id = "controller"; | ||
this.name = "Controller"; | ||
this.version = package_default.version; | ||
this.icon = icon; | ||
this.subscriptions = []; | ||
this.request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.probe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.probe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
id = "controller"; | ||
name = "Controller"; | ||
version = package_default.version; | ||
icon = icon; | ||
account; | ||
subscriptions = []; | ||
_probePromise = null; | ||
async safeProbe() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
if (this._probePromise) { | ||
return this._probePromise; | ||
} | ||
const release = await mutex.obtain(); | ||
return await new Promise(async (resolve) => { | ||
try { | ||
this._probePromise = this.probe(); | ||
const result = await this._probePromise; | ||
resolve(result); | ||
} finally { | ||
this._probePromise = null; | ||
} | ||
}).finally(() => { | ||
release(); | ||
}); | ||
} | ||
request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.safeProbe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.safeProbe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
}; | ||
this.on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
this.off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
}; | ||
} | ||
}; | ||
on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
emitNetworkChanged(chainId) { | ||
@@ -517,2 +579,8 @@ this.subscriptions.filter((sub) => sub.type === "networkChanged").forEach((sub) => { | ||
var ControllerProvider = class extends BaseProvider { | ||
keychain; | ||
profile; | ||
options; | ||
iframes; | ||
selectedChain; | ||
chains; | ||
constructor(options) { | ||
@@ -519,0 +587,0 @@ super(); |
// src/iframe/base.ts | ||
import { connectToChild } from "@cartridge/penpal"; | ||
var IFrame = class { | ||
url; | ||
iframe; | ||
container; | ||
onClose; | ||
constructor({ | ||
@@ -5,0 +9,0 @@ id, |
// src/iframe/base.ts | ||
import { connectToChild } from "@cartridge/penpal"; | ||
var IFrame = class { | ||
url; | ||
iframe; | ||
container; | ||
onClose; | ||
constructor({ | ||
@@ -5,0 +9,0 @@ id, |
@@ -7,2 +7,6 @@ // src/constants.ts | ||
var IFrame = class { | ||
url; | ||
iframe; | ||
container; | ||
onClose; | ||
constructor({ | ||
@@ -9,0 +13,0 @@ id, |
@@ -7,2 +7,6 @@ // src/constants.ts | ||
var IFrame = class { | ||
url; | ||
iframe; | ||
container; | ||
onClose; | ||
constructor({ | ||
@@ -9,0 +13,0 @@ id, |
@@ -12,1 +12,2 @@ export { default } from './controller.js'; | ||
import '@cartridge/account-wasm/controller'; | ||
import './policies.js'; |
@@ -11,2 +11,4 @@ import { WalletAccount } from 'starknet'; | ||
subscriptions: WalletEvents[]; | ||
private _probePromise; | ||
protected safeProbe(): Promise<WalletAccount | undefined>; | ||
request: RequestFn; | ||
@@ -13,0 +15,0 @@ on: WalletEventListener; |
@@ -9,3 +9,3 @@ // src/provider.ts | ||
name: "@cartridge/controller", | ||
version: "0.5.9", | ||
version: "0.6.0", | ||
description: "Cartridge Controller", | ||
@@ -24,6 +24,18 @@ module: "dist/index.js", | ||
exports: { | ||
".": "./dist/index.js", | ||
"./session": "./dist/session/index.js", | ||
"./provider": "./dist/provider/index.js", | ||
"./types": "./dist/types/index.js" | ||
".": { | ||
types: "./dist/index.d.ts", | ||
default: "./dist/index.js" | ||
}, | ||
"./session": { | ||
types: "./dist/session/index.d.ts", | ||
default: "./dist/session/index.js" | ||
}, | ||
"./provider": { | ||
types: "./dist/provider/index.d.ts", | ||
default: "./dist/provider/index.js" | ||
}, | ||
"./types": { | ||
types: "./dist/types/index.d.ts", | ||
default: "./dist/types/index.js" | ||
} | ||
}, | ||
@@ -41,2 +53,5 @@ tsup: { | ||
}, | ||
peerDependencies: { | ||
starknet: "^6.21.0" | ||
}, | ||
dependencies: { | ||
@@ -50,4 +65,3 @@ "@cartridge/account-wasm": "workspace:*", | ||
"fast-deep-equal": "^3.1.3", | ||
"query-string": "^7.1.1", | ||
starknet: "^6.11.0" | ||
"query-string": "^7.1.1" | ||
}, | ||
@@ -67,123 +81,163 @@ devDependencies: { | ||
// src/mutex.ts | ||
function releaseStub() { | ||
} | ||
var Mutex = class { | ||
m_lastPromise = Promise.resolve(); | ||
/** | ||
* Acquire lock | ||
* @param [bypass=false] option to skip lock acquisition | ||
*/ | ||
async obtain(bypass = false) { | ||
let release = releaseStub; | ||
if (bypass) return release; | ||
const lastPromise = this.m_lastPromise; | ||
this.m_lastPromise = new Promise((resolve) => release = resolve); | ||
await lastPromise; | ||
return release; | ||
} | ||
}; | ||
// src/provider.ts | ||
var mutex = new Mutex(); | ||
var BaseProvider = class { | ||
constructor() { | ||
this.id = "controller"; | ||
this.name = "Controller"; | ||
this.version = package_default.version; | ||
this.icon = icon; | ||
this.subscriptions = []; | ||
this.request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.probe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.probe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
id = "controller"; | ||
name = "Controller"; | ||
version = package_default.version; | ||
icon = icon; | ||
account; | ||
subscriptions = []; | ||
_probePromise = null; | ||
async safeProbe() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
if (this._probePromise) { | ||
return this._probePromise; | ||
} | ||
const release = await mutex.obtain(); | ||
return await new Promise(async (resolve) => { | ||
try { | ||
this._probePromise = this.probe(); | ||
const result = await this._probePromise; | ||
resolve(result); | ||
} finally { | ||
this._probePromise = null; | ||
} | ||
}).finally(() => { | ||
release(); | ||
}); | ||
} | ||
request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.safeProbe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.safeProbe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
}; | ||
this.on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
this.off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
}; | ||
} | ||
}; | ||
on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
emitNetworkChanged(chainId) { | ||
@@ -190,0 +244,0 @@ this.subscriptions.filter((sub) => sub.type === "networkChanged").forEach((sub) => { |
@@ -49,2 +49,3 @@ // src/session/account.ts | ||
var SessionAccount = class extends WalletAccount { | ||
controller; | ||
constructor(provider, { | ||
@@ -60,2 +61,3 @@ rpcUrl, | ||
super({ nodeUrl: rpcUrl }, provider); | ||
this.address = address; | ||
this.controller = CartridgeSessionAccount.new_as_registered( | ||
@@ -62,0 +64,0 @@ rpcUrl, |
@@ -8,2 +8,3 @@ export { SessionOptions, default } from './provider.js'; | ||
import '../index.d-BbTUPBeO.js'; | ||
import '../policies.js'; | ||
import '@cartridge/penpal'; |
@@ -33,3 +33,4 @@ // src/session/provider.ts | ||
target, | ||
method: m.entrypoint | ||
method: m.entrypoint, | ||
authorized: m.authorized | ||
})) | ||
@@ -50,3 +51,4 @@ ), | ||
return { | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash) | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash), | ||
authorized: p.authorized | ||
}; | ||
@@ -80,2 +82,3 @@ }) | ||
var SessionAccount = class extends WalletAccount { | ||
controller; | ||
constructor(provider, { | ||
@@ -91,2 +94,3 @@ rpcUrl, | ||
super({ nodeUrl: rpcUrl }, provider); | ||
this.address = address; | ||
this.controller = CartridgeSessionAccount.new_as_registered( | ||
@@ -132,3 +136,3 @@ rpcUrl, | ||
name: "@cartridge/controller", | ||
version: "0.5.9", | ||
version: "0.6.0", | ||
description: "Cartridge Controller", | ||
@@ -147,6 +151,18 @@ module: "dist/index.js", | ||
exports: { | ||
".": "./dist/index.js", | ||
"./session": "./dist/session/index.js", | ||
"./provider": "./dist/provider/index.js", | ||
"./types": "./dist/types/index.js" | ||
".": { | ||
types: "./dist/index.d.ts", | ||
default: "./dist/index.js" | ||
}, | ||
"./session": { | ||
types: "./dist/session/index.d.ts", | ||
default: "./dist/session/index.js" | ||
}, | ||
"./provider": { | ||
types: "./dist/provider/index.d.ts", | ||
default: "./dist/provider/index.js" | ||
}, | ||
"./types": { | ||
types: "./dist/types/index.d.ts", | ||
default: "./dist/types/index.js" | ||
} | ||
}, | ||
@@ -164,2 +180,5 @@ tsup: { | ||
}, | ||
peerDependencies: { | ||
starknet: "^6.21.0" | ||
}, | ||
dependencies: { | ||
@@ -173,4 +192,3 @@ "@cartridge/account-wasm": "workspace:*", | ||
"fast-deep-equal": "^3.1.3", | ||
"query-string": "^7.1.1", | ||
starknet: "^6.11.0" | ||
"query-string": "^7.1.1" | ||
}, | ||
@@ -190,123 +208,163 @@ devDependencies: { | ||
// src/mutex.ts | ||
function releaseStub() { | ||
} | ||
var Mutex = class { | ||
m_lastPromise = Promise.resolve(); | ||
/** | ||
* Acquire lock | ||
* @param [bypass=false] option to skip lock acquisition | ||
*/ | ||
async obtain(bypass = false) { | ||
let release = releaseStub; | ||
if (bypass) return release; | ||
const lastPromise = this.m_lastPromise; | ||
this.m_lastPromise = new Promise((resolve) => release = resolve); | ||
await lastPromise; | ||
return release; | ||
} | ||
}; | ||
// src/provider.ts | ||
var mutex = new Mutex(); | ||
var BaseProvider = class { | ||
constructor() { | ||
this.id = "controller"; | ||
this.name = "Controller"; | ||
this.version = package_default.version; | ||
this.icon = icon; | ||
this.subscriptions = []; | ||
this.request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.probe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.probe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
id = "controller"; | ||
name = "Controller"; | ||
version = package_default.version; | ||
icon = icon; | ||
account; | ||
subscriptions = []; | ||
_probePromise = null; | ||
async safeProbe() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
if (this._probePromise) { | ||
return this._probePromise; | ||
} | ||
const release = await mutex.obtain(); | ||
return await new Promise(async (resolve) => { | ||
try { | ||
this._probePromise = this.probe(); | ||
const result = await this._probePromise; | ||
resolve(result); | ||
} finally { | ||
this._probePromise = null; | ||
} | ||
}).finally(() => { | ||
release(); | ||
}); | ||
} | ||
request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.safeProbe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.safeProbe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
}; | ||
this.on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
this.off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
}; | ||
} | ||
}; | ||
on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
emitNetworkChanged(chainId) { | ||
@@ -326,10 +384,41 @@ this.subscriptions.filter((sub) => sub.type === "networkChanged").forEach((sub) => { | ||
var SessionProvider = class extends BaseProvider { | ||
constructor({ rpc, chainId, policies, redirectUrl }) { | ||
id = "controller_session"; | ||
name = "Controller Session"; | ||
_chainId; | ||
_rpcUrl; | ||
_username; | ||
_redirectUrl; | ||
_policies; | ||
_keychainUrl; | ||
constructor({ | ||
rpc, | ||
chainId, | ||
policies, | ||
redirectUrl, | ||
keychainUrl | ||
}) { | ||
super(); | ||
this.id = "controller_session"; | ||
this.name = "Controller Session"; | ||
this._policies = { | ||
verified: false, | ||
contracts: policies.contracts ? Object.fromEntries( | ||
Object.entries(policies.contracts).map(([address, contract]) => [ | ||
address, | ||
{ | ||
...contract, | ||
methods: contract.methods.map((method) => ({ | ||
...method, | ||
authorized: true | ||
})) | ||
} | ||
]) | ||
) : void 0, | ||
messages: policies.messages?.map((message) => ({ | ||
...message, | ||
authorized: true | ||
})) | ||
}; | ||
this._rpcUrl = rpc; | ||
this._chainId = chainId; | ||
this._redirectUrl = redirectUrl; | ||
this._policies = policies; | ||
this._keychainUrl = keychainUrl || KEYCHAIN_URL; | ||
if (typeof window !== "undefined") { | ||
@@ -339,2 +428,27 @@ window.starknet_controller_session = this; | ||
} | ||
validatePoliciesSubset(newPolicies, existingPolicies) { | ||
if (newPolicies.contracts) { | ||
if (!existingPolicies.contracts) return false; | ||
for (const [address, contract] of Object.entries(newPolicies.contracts)) { | ||
const existingContract = existingPolicies.contracts[address]; | ||
if (!existingContract) return false; | ||
for (const method of contract.methods) { | ||
const existingMethod = existingContract.methods.find( | ||
(m) => m.entrypoint === method.entrypoint | ||
); | ||
if (!existingMethod || !existingMethod.authorized) return false; | ||
} | ||
} | ||
} | ||
if (newPolicies.messages) { | ||
if (!existingPolicies.messages) return false; | ||
for (const message of newPolicies.messages) { | ||
const existingMessage = existingPolicies.messages.find( | ||
(m) => JSON.stringify(m.domain) === JSON.stringify(message.domain) && JSON.stringify(m.types) === JSON.stringify(message.types) | ||
); | ||
if (!existingMessage || !existingMessage.authorized) return false; | ||
} | ||
} | ||
return true; | ||
} | ||
async username() { | ||
@@ -345,10 +459,16 @@ await this.tryRetrieveFromQueryOrStorage(); | ||
async probe() { | ||
await this.tryRetrieveFromQueryOrStorage(); | ||
return; | ||
if (this.account) { | ||
return this.account; | ||
} | ||
this.account = await this.tryRetrieveFromQueryOrStorage(); | ||
return this.account; | ||
} | ||
async connect() { | ||
await this.tryRetrieveFromQueryOrStorage(); | ||
if (this.account) { | ||
return; | ||
return this.account; | ||
} | ||
this.account = await this.tryRetrieveFromQueryOrStorage(); | ||
if (this.account) { | ||
return this.account; | ||
} | ||
const pk = stark.randomAddress(); | ||
@@ -363,3 +483,4 @@ const publicKey = ec.starkCurve.getStarkKey(pk); | ||
); | ||
const url = `${KEYCHAIN_URL}/session?public_key=${publicKey}&redirect_uri=${this._redirectUrl}&redirect_query_name=startapp&policies=${JSON.stringify( | ||
localStorage.setItem("sessionPolicies", JSON.stringify(this._policies)); | ||
const url = `${this._keychainUrl}/session?public_key=${publicKey}&redirect_uri=${this._redirectUrl}&redirect_query_name=startapp&policies=${JSON.stringify( | ||
this._policies | ||
@@ -369,3 +490,3 @@ )}&rpc_url=${this._rpcUrl}`; | ||
window.open(url, "_blank"); | ||
return; | ||
return this.account; | ||
} | ||
@@ -381,2 +502,3 @@ switchStarknetChain(_chainId) { | ||
localStorage.removeItem("session"); | ||
localStorage.removeItem("sessionPolicies"); | ||
this.account = void 0; | ||
@@ -387,2 +509,5 @@ this._username = void 0; | ||
async tryRetrieveFromQueryOrStorage() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
const signerString = localStorage.getItem("sessionSigner"); | ||
@@ -411,2 +536,37 @@ const signer = signerString ? JSON.parse(signerString) : null; | ||
} | ||
const expirationTime = parseInt(sessionRegistration.expiresAt) * 1e3; | ||
console.log("Session expiration check:", { | ||
expirationTime, | ||
currentTime: Date.now(), | ||
expired: Date.now() >= expirationTime | ||
}); | ||
if (Date.now() >= expirationTime) { | ||
console.log("Session expired, clearing stored session"); | ||
this.clearStoredSession(); | ||
return; | ||
} | ||
const storedPoliciesStr = localStorage.getItem("sessionPolicies"); | ||
console.log("Checking stored policies:", { | ||
storedPoliciesStr, | ||
currentPolicies: this._policies | ||
}); | ||
if (storedPoliciesStr) { | ||
const storedPolicies = JSON.parse( | ||
storedPoliciesStr | ||
); | ||
const isValid = this.validatePoliciesSubset( | ||
this._policies, | ||
storedPolicies | ||
); | ||
console.log("Policy validation result:", { | ||
isValid, | ||
storedPolicies, | ||
requestedPolicies: this._policies | ||
}); | ||
if (!isValid) { | ||
console.log("Policy validation failed, clearing stored session"); | ||
this.clearStoredSession(); | ||
return; | ||
} | ||
} | ||
this._username = sessionRegistration.username; | ||
@@ -424,2 +584,7 @@ this.account = new SessionAccount(this, { | ||
} | ||
clearStoredSession() { | ||
localStorage.removeItem("sessionSigner"); | ||
localStorage.removeItem("session"); | ||
localStorage.removeItem("sessionPolicies"); | ||
} | ||
}; | ||
@@ -426,0 +591,0 @@ export { |
@@ -5,2 +5,3 @@ import { WalletAccount } from 'starknet'; | ||
import { AddStarknetChainParameters } from '@starknet-io/types-js'; | ||
import { ParsedSessionPolicies } from '../policies.js'; | ||
@@ -12,2 +13,3 @@ type SessionOptions = { | ||
redirectUrl: string; | ||
keychainUrl?: string; | ||
}; | ||
@@ -21,4 +23,6 @@ declare class SessionProvider extends BaseProvider { | ||
protected _redirectUrl: string; | ||
protected _policies: SessionPolicies; | ||
constructor({ rpc, chainId, policies, redirectUrl }: SessionOptions); | ||
protected _policies: ParsedSessionPolicies; | ||
protected _keychainUrl: string; | ||
constructor({ rpc, chainId, policies, redirectUrl, keychainUrl, }: SessionOptions); | ||
private validatePoliciesSubset; | ||
username(): Promise<string | undefined>; | ||
@@ -31,4 +35,5 @@ probe(): Promise<WalletAccount | undefined>; | ||
tryRetrieveFromQueryOrStorage(): Promise<WalletAccount | undefined>; | ||
private clearStoredSession; | ||
} | ||
export { type SessionOptions, SessionProvider as default }; |
@@ -33,3 +33,4 @@ // src/session/provider.ts | ||
target, | ||
method: m.entrypoint | ||
method: m.entrypoint, | ||
authorized: m.authorized | ||
})) | ||
@@ -50,3 +51,4 @@ ), | ||
return { | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash) | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash), | ||
authorized: p.authorized | ||
}; | ||
@@ -62,2 +64,3 @@ }) | ||
var SessionAccount = class extends WalletAccount { | ||
controller; | ||
constructor(provider, { | ||
@@ -73,2 +76,3 @@ rpcUrl, | ||
super({ nodeUrl: rpcUrl }, provider); | ||
this.address = address; | ||
this.controller = CartridgeSessionAccount.new_as_registered( | ||
@@ -114,3 +118,3 @@ rpcUrl, | ||
name: "@cartridge/controller", | ||
version: "0.5.9", | ||
version: "0.6.0", | ||
description: "Cartridge Controller", | ||
@@ -129,6 +133,18 @@ module: "dist/index.js", | ||
exports: { | ||
".": "./dist/index.js", | ||
"./session": "./dist/session/index.js", | ||
"./provider": "./dist/provider/index.js", | ||
"./types": "./dist/types/index.js" | ||
".": { | ||
types: "./dist/index.d.ts", | ||
default: "./dist/index.js" | ||
}, | ||
"./session": { | ||
types: "./dist/session/index.d.ts", | ||
default: "./dist/session/index.js" | ||
}, | ||
"./provider": { | ||
types: "./dist/provider/index.d.ts", | ||
default: "./dist/provider/index.js" | ||
}, | ||
"./types": { | ||
types: "./dist/types/index.d.ts", | ||
default: "./dist/types/index.js" | ||
} | ||
}, | ||
@@ -146,2 +162,5 @@ tsup: { | ||
}, | ||
peerDependencies: { | ||
starknet: "^6.21.0" | ||
}, | ||
dependencies: { | ||
@@ -155,4 +174,3 @@ "@cartridge/account-wasm": "workspace:*", | ||
"fast-deep-equal": "^3.1.3", | ||
"query-string": "^7.1.1", | ||
starknet: "^6.11.0" | ||
"query-string": "^7.1.1" | ||
}, | ||
@@ -172,123 +190,163 @@ devDependencies: { | ||
// src/mutex.ts | ||
function releaseStub() { | ||
} | ||
var Mutex = class { | ||
m_lastPromise = Promise.resolve(); | ||
/** | ||
* Acquire lock | ||
* @param [bypass=false] option to skip lock acquisition | ||
*/ | ||
async obtain(bypass = false) { | ||
let release = releaseStub; | ||
if (bypass) return release; | ||
const lastPromise = this.m_lastPromise; | ||
this.m_lastPromise = new Promise((resolve) => release = resolve); | ||
await lastPromise; | ||
return release; | ||
} | ||
}; | ||
// src/provider.ts | ||
var mutex = new Mutex(); | ||
var BaseProvider = class { | ||
constructor() { | ||
this.id = "controller"; | ||
this.name = "Controller"; | ||
this.version = package_default.version; | ||
this.icon = icon; | ||
this.subscriptions = []; | ||
this.request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.probe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.probe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
id = "controller"; | ||
name = "Controller"; | ||
version = package_default.version; | ||
icon = icon; | ||
account; | ||
subscriptions = []; | ||
_probePromise = null; | ||
async safeProbe() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
if (this._probePromise) { | ||
return this._probePromise; | ||
} | ||
const release = await mutex.obtain(); | ||
return await new Promise(async (resolve) => { | ||
try { | ||
this._probePromise = this.probe(); | ||
const result = await this._probePromise; | ||
resolve(result); | ||
} finally { | ||
this._probePromise = null; | ||
} | ||
}).finally(() => { | ||
release(); | ||
}); | ||
} | ||
request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.safeProbe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.safeProbe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
}; | ||
this.on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
this.off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
}; | ||
} | ||
}; | ||
on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
emitNetworkChanged(chainId) { | ||
@@ -308,10 +366,41 @@ this.subscriptions.filter((sub) => sub.type === "networkChanged").forEach((sub) => { | ||
var SessionProvider = class extends BaseProvider { | ||
constructor({ rpc, chainId, policies, redirectUrl }) { | ||
id = "controller_session"; | ||
name = "Controller Session"; | ||
_chainId; | ||
_rpcUrl; | ||
_username; | ||
_redirectUrl; | ||
_policies; | ||
_keychainUrl; | ||
constructor({ | ||
rpc, | ||
chainId, | ||
policies, | ||
redirectUrl, | ||
keychainUrl | ||
}) { | ||
super(); | ||
this.id = "controller_session"; | ||
this.name = "Controller Session"; | ||
this._policies = { | ||
verified: false, | ||
contracts: policies.contracts ? Object.fromEntries( | ||
Object.entries(policies.contracts).map(([address, contract]) => [ | ||
address, | ||
{ | ||
...contract, | ||
methods: contract.methods.map((method) => ({ | ||
...method, | ||
authorized: true | ||
})) | ||
} | ||
]) | ||
) : void 0, | ||
messages: policies.messages?.map((message) => ({ | ||
...message, | ||
authorized: true | ||
})) | ||
}; | ||
this._rpcUrl = rpc; | ||
this._chainId = chainId; | ||
this._redirectUrl = redirectUrl; | ||
this._policies = policies; | ||
this._keychainUrl = keychainUrl || KEYCHAIN_URL; | ||
if (typeof window !== "undefined") { | ||
@@ -321,2 +410,27 @@ window.starknet_controller_session = this; | ||
} | ||
validatePoliciesSubset(newPolicies, existingPolicies) { | ||
if (newPolicies.contracts) { | ||
if (!existingPolicies.contracts) return false; | ||
for (const [address, contract] of Object.entries(newPolicies.contracts)) { | ||
const existingContract = existingPolicies.contracts[address]; | ||
if (!existingContract) return false; | ||
for (const method of contract.methods) { | ||
const existingMethod = existingContract.methods.find( | ||
(m) => m.entrypoint === method.entrypoint | ||
); | ||
if (!existingMethod || !existingMethod.authorized) return false; | ||
} | ||
} | ||
} | ||
if (newPolicies.messages) { | ||
if (!existingPolicies.messages) return false; | ||
for (const message of newPolicies.messages) { | ||
const existingMessage = existingPolicies.messages.find( | ||
(m) => JSON.stringify(m.domain) === JSON.stringify(message.domain) && JSON.stringify(m.types) === JSON.stringify(message.types) | ||
); | ||
if (!existingMessage || !existingMessage.authorized) return false; | ||
} | ||
} | ||
return true; | ||
} | ||
async username() { | ||
@@ -327,10 +441,16 @@ await this.tryRetrieveFromQueryOrStorage(); | ||
async probe() { | ||
await this.tryRetrieveFromQueryOrStorage(); | ||
return; | ||
if (this.account) { | ||
return this.account; | ||
} | ||
this.account = await this.tryRetrieveFromQueryOrStorage(); | ||
return this.account; | ||
} | ||
async connect() { | ||
await this.tryRetrieveFromQueryOrStorage(); | ||
if (this.account) { | ||
return; | ||
return this.account; | ||
} | ||
this.account = await this.tryRetrieveFromQueryOrStorage(); | ||
if (this.account) { | ||
return this.account; | ||
} | ||
const pk = stark.randomAddress(); | ||
@@ -345,3 +465,4 @@ const publicKey = ec.starkCurve.getStarkKey(pk); | ||
); | ||
const url = `${KEYCHAIN_URL}/session?public_key=${publicKey}&redirect_uri=${this._redirectUrl}&redirect_query_name=startapp&policies=${JSON.stringify( | ||
localStorage.setItem("sessionPolicies", JSON.stringify(this._policies)); | ||
const url = `${this._keychainUrl}/session?public_key=${publicKey}&redirect_uri=${this._redirectUrl}&redirect_query_name=startapp&policies=${JSON.stringify( | ||
this._policies | ||
@@ -351,3 +472,3 @@ )}&rpc_url=${this._rpcUrl}`; | ||
window.open(url, "_blank"); | ||
return; | ||
return this.account; | ||
} | ||
@@ -363,2 +484,3 @@ switchStarknetChain(_chainId) { | ||
localStorage.removeItem("session"); | ||
localStorage.removeItem("sessionPolicies"); | ||
this.account = void 0; | ||
@@ -369,2 +491,5 @@ this._username = void 0; | ||
async tryRetrieveFromQueryOrStorage() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
const signerString = localStorage.getItem("sessionSigner"); | ||
@@ -393,2 +518,37 @@ const signer = signerString ? JSON.parse(signerString) : null; | ||
} | ||
const expirationTime = parseInt(sessionRegistration.expiresAt) * 1e3; | ||
console.log("Session expiration check:", { | ||
expirationTime, | ||
currentTime: Date.now(), | ||
expired: Date.now() >= expirationTime | ||
}); | ||
if (Date.now() >= expirationTime) { | ||
console.log("Session expired, clearing stored session"); | ||
this.clearStoredSession(); | ||
return; | ||
} | ||
const storedPoliciesStr = localStorage.getItem("sessionPolicies"); | ||
console.log("Checking stored policies:", { | ||
storedPoliciesStr, | ||
currentPolicies: this._policies | ||
}); | ||
if (storedPoliciesStr) { | ||
const storedPolicies = JSON.parse( | ||
storedPoliciesStr | ||
); | ||
const isValid = this.validatePoliciesSubset( | ||
this._policies, | ||
storedPolicies | ||
); | ||
console.log("Policy validation result:", { | ||
isValid, | ||
storedPolicies, | ||
requestedPolicies: this._policies | ||
}); | ||
if (!isValid) { | ||
console.log("Policy validation failed, clearing stored session"); | ||
this.clearStoredSession(); | ||
return; | ||
} | ||
} | ||
this._username = sessionRegistration.username; | ||
@@ -406,2 +566,7 @@ this.account = new SessionAccount(this, { | ||
} | ||
clearStoredSession() { | ||
localStorage.removeItem("sessionSigner"); | ||
localStorage.removeItem("session"); | ||
localStorage.removeItem("sessionPolicies"); | ||
} | ||
}; | ||
@@ -408,0 +573,0 @@ export { |
@@ -5,2 +5,3 @@ import { WalletAccount } from 'starknet'; | ||
import { AddStarknetChainParameters } from '@starknet-io/types-js'; | ||
import { ParsedSessionPolicies } from '../policies.js'; | ||
@@ -11,3 +12,3 @@ declare class TelegramProvider extends BaseProvider { | ||
protected _username?: string; | ||
protected _policies: SessionPolicies; | ||
protected _policies: ParsedSessionPolicies; | ||
private _rpcUrl; | ||
@@ -14,0 +15,0 @@ constructor({ rpc, chainId, policies, tmaUrl, }: { |
@@ -42,3 +42,4 @@ // src/telegram/provider.ts | ||
target, | ||
method: m.entrypoint | ||
method: m.entrypoint, | ||
authorized: m.authorized | ||
})) | ||
@@ -59,3 +60,4 @@ ), | ||
return { | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash) | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash), | ||
authorized: p.authorized | ||
}; | ||
@@ -71,2 +73,3 @@ }) | ||
var SessionAccount = class extends WalletAccount { | ||
controller; | ||
constructor(provider, { | ||
@@ -82,2 +85,3 @@ rpcUrl, | ||
super({ nodeUrl: rpcUrl }, provider); | ||
this.address = address; | ||
this.controller = CartridgeSessionAccount.new_as_registered( | ||
@@ -120,3 +124,3 @@ rpcUrl, | ||
name: "@cartridge/controller", | ||
version: "0.5.9", | ||
version: "0.6.0", | ||
description: "Cartridge Controller", | ||
@@ -135,6 +139,18 @@ module: "dist/index.js", | ||
exports: { | ||
".": "./dist/index.js", | ||
"./session": "./dist/session/index.js", | ||
"./provider": "./dist/provider/index.js", | ||
"./types": "./dist/types/index.js" | ||
".": { | ||
types: "./dist/index.d.ts", | ||
default: "./dist/index.js" | ||
}, | ||
"./session": { | ||
types: "./dist/session/index.d.ts", | ||
default: "./dist/session/index.js" | ||
}, | ||
"./provider": { | ||
types: "./dist/provider/index.d.ts", | ||
default: "./dist/provider/index.js" | ||
}, | ||
"./types": { | ||
types: "./dist/types/index.d.ts", | ||
default: "./dist/types/index.js" | ||
} | ||
}, | ||
@@ -152,2 +168,5 @@ tsup: { | ||
}, | ||
peerDependencies: { | ||
starknet: "^6.21.0" | ||
}, | ||
dependencies: { | ||
@@ -161,4 +180,3 @@ "@cartridge/account-wasm": "workspace:*", | ||
"fast-deep-equal": "^3.1.3", | ||
"query-string": "^7.1.1", | ||
starknet: "^6.11.0" | ||
"query-string": "^7.1.1" | ||
}, | ||
@@ -178,123 +196,163 @@ devDependencies: { | ||
// src/mutex.ts | ||
function releaseStub() { | ||
} | ||
var Mutex = class { | ||
m_lastPromise = Promise.resolve(); | ||
/** | ||
* Acquire lock | ||
* @param [bypass=false] option to skip lock acquisition | ||
*/ | ||
async obtain(bypass = false) { | ||
let release = releaseStub; | ||
if (bypass) return release; | ||
const lastPromise = this.m_lastPromise; | ||
this.m_lastPromise = new Promise((resolve) => release = resolve); | ||
await lastPromise; | ||
return release; | ||
} | ||
}; | ||
// src/provider.ts | ||
var mutex = new Mutex(); | ||
var BaseProvider = class { | ||
constructor() { | ||
this.id = "controller"; | ||
this.name = "Controller"; | ||
this.version = package_default.version; | ||
this.icon = icon; | ||
this.subscriptions = []; | ||
this.request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.probe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.probe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
id = "controller"; | ||
name = "Controller"; | ||
version = package_default.version; | ||
icon = icon; | ||
account; | ||
subscriptions = []; | ||
_probePromise = null; | ||
async safeProbe() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
if (this._probePromise) { | ||
return this._probePromise; | ||
} | ||
const release = await mutex.obtain(); | ||
return await new Promise(async (resolve) => { | ||
try { | ||
this._probePromise = this.probe(); | ||
const result = await this._probePromise; | ||
resolve(result); | ||
} finally { | ||
this._probePromise = null; | ||
} | ||
}).finally(() => { | ||
release(); | ||
}); | ||
} | ||
request = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.safeProbe(); | ||
if (this.account) { | ||
return [Permission.ACCOUNTS]; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
return []; | ||
case "wallet_requestAccounts": { | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
const silentMode = call.params && call.params.silent_mode; | ||
this.account = await this.safeProbe(); | ||
if (!this.account && !silentMode) { | ||
this.account = await this.connect(); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
if (this.account) { | ||
return [this.account.address]; | ||
} | ||
return []; | ||
} | ||
case "wallet_watchAsset": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_watchAsset not implemented" | ||
}; | ||
case "wallet_addStarknetChain": { | ||
let params2 = call.params; | ||
return this.addStarknetChain(params2); | ||
} | ||
case "wallet_switchStarknetChain": { | ||
let params2 = call.params; | ||
return this.switchStarknetChain(params2.chainId); | ||
} | ||
case "wallet_requestChainId": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
} | ||
return await this.account.getChainId(); | ||
case "wallet_deploymentData": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_deploymentData not implemented" | ||
}; | ||
case "wallet_addInvokeTransaction": | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
data: "Account not initialized" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
let params = call.params; | ||
return await this.account.execute( | ||
params.calls.map((call2) => ({ | ||
contractAddress: call2.contract_address, | ||
entrypoint: call2.entry_point, | ||
calldata: call2.calldata | ||
})) | ||
); | ||
case "wallet_addDeclareTransaction": | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: "wallet_addDeclareTransaction not implemented" | ||
}; | ||
case "wallet_signTypedData": { | ||
if (!this.account) { | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
data: "Account not initialized" | ||
}; | ||
} | ||
return await this.account.signMessage(call.params); | ||
} | ||
}; | ||
this.on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
this.off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
} | ||
case "wallet_supportedSpecs": | ||
return []; | ||
case "wallet_supportedWalletApi": | ||
return []; | ||
default: | ||
throw { | ||
code: 63, | ||
message: "An unexpected error occurred", | ||
data: `Unknown RPC call type: ${call.type}` | ||
}; | ||
} | ||
}; | ||
on = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
this.subscriptions.push({ type: event, handler }); | ||
}; | ||
off = (event, handler) => { | ||
if (event !== "accountsChanged" && event !== "networkChanged") { | ||
throw new Error(`Unknown event: ${event}`); | ||
} | ||
const idx = this.subscriptions.findIndex( | ||
(sub) => sub.type === event && sub.handler === handler | ||
); | ||
if (idx >= 0) { | ||
this.subscriptions.splice(idx, 1); | ||
} | ||
}; | ||
emitNetworkChanged(chainId) { | ||
@@ -312,4 +370,32 @@ this.subscriptions.filter((sub) => sub.type === "networkChanged").forEach((sub) => { | ||
// src/policies.ts | ||
function parsePolicies(policies) { | ||
return { | ||
verified: false, | ||
contracts: policies.contracts ? Object.fromEntries( | ||
Object.entries(policies.contracts).map(([address, contract]) => [ | ||
address, | ||
{ | ||
...contract, | ||
methods: contract.methods.map((method) => ({ | ||
...method, | ||
authorized: true | ||
})) | ||
} | ||
]) | ||
) : void 0, | ||
messages: policies.messages?.map((message) => ({ | ||
...message, | ||
authorized: true | ||
})) | ||
}; | ||
} | ||
// src/telegram/provider.ts | ||
var TelegramProvider = class extends BaseProvider { | ||
_tmaUrl; | ||
_chainId; | ||
_username; | ||
_policies; | ||
_rpcUrl; | ||
constructor({ | ||
@@ -325,3 +411,3 @@ rpc, | ||
this._chainId = chainId; | ||
this._policies = policies; | ||
this._policies = parsePolicies(policies); | ||
if (typeof window !== "undefined") { | ||
@@ -328,0 +414,0 @@ window.starknet_controller = this; |
@@ -6,2 +6,3 @@ import * as starknet from 'starknet'; | ||
import { ChainId } from '@starknet-io/types-js'; | ||
import { ParsedSessionPolicies } from './policies.js'; | ||
@@ -14,3 +15,3 @@ declare function normalizeCalls(calls: Call | Call[]): { | ||
declare function toSessionPolicies(policies: Policies): SessionPolicies; | ||
declare function toWasmPolicies(policies: SessionPolicies): wasm.Policy[]; | ||
declare function toWasmPolicies(policies: ParsedSessionPolicies): wasm.Policy[]; | ||
declare function toArray<T>(val: T | T[]): T[]; | ||
@@ -17,0 +18,0 @@ declare function humanizeString(str: string): string; |
@@ -82,3 +82,4 @@ // src/utils.ts | ||
target, | ||
method: m.entrypoint | ||
method: m.entrypoint, | ||
authorized: m.authorized | ||
})) | ||
@@ -99,3 +100,4 @@ ), | ||
return { | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash) | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash), | ||
authorized: p.authorized | ||
}; | ||
@@ -102,0 +104,0 @@ }) |
{ | ||
"name": "@cartridge/controller", | ||
"version": "0.5.9", | ||
"version": "0.6.0", | ||
"description": "Cartridge Controller", | ||
@@ -9,6 +9,18 @@ "module": "dist/index.js", | ||
"exports": { | ||
".": "./dist/index.js", | ||
"./session": "./dist/session/index.js", | ||
"./provider": "./dist/provider/index.js", | ||
"./types": "./dist/types/index.js" | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"default": "./dist/index.js" | ||
}, | ||
"./session": { | ||
"types": "./dist/session/index.d.ts", | ||
"default": "./dist/session/index.js" | ||
}, | ||
"./provider": { | ||
"types": "./dist/provider/index.d.ts", | ||
"default": "./dist/provider/index.js" | ||
}, | ||
"./types": { | ||
"types": "./dist/types/index.d.ts", | ||
"default": "./dist/types/index.js" | ||
} | ||
}, | ||
@@ -26,2 +38,5 @@ "tsup": { | ||
}, | ||
"peerDependencies": { | ||
"starknet": "^6.21.0" | ||
}, | ||
"dependencies": { | ||
@@ -35,4 +50,3 @@ "@cartridge/penpal": "^6.2.3", | ||
"query-string": "^7.1.1", | ||
"starknet": "^6.11.0", | ||
"@cartridge/account-wasm": "0.5.9" | ||
"@cartridge/account-wasm": "0.6.0" | ||
}, | ||
@@ -45,3 +59,3 @@ "devDependencies": { | ||
"typescript": "^5.4.5", | ||
"@cartridge/tsconfig": "0.5.9" | ||
"@cartridge/tsconfig": "0.6.0" | ||
}, | ||
@@ -48,0 +62,0 @@ "scripts": { |
@@ -350,2 +350,3 @@ import { AsyncMethodReturns } from "@cartridge/penpal"; | ||
} | ||
if (!this.keychain) return; | ||
@@ -352,0 +353,0 @@ |
@@ -19,3 +19,6 @@ import { WalletAccount } from "starknet"; | ||
import { icon } from "./icon"; | ||
import { Mutex } from "./mutex"; | ||
const mutex = new Mutex(); | ||
export default abstract class BaseProvider implements StarknetWindowObject { | ||
@@ -30,6 +33,33 @@ public id = "controller"; | ||
private _probePromise: Promise<WalletAccount | undefined> | null = null; | ||
protected async safeProbe(): Promise<WalletAccount | undefined> { | ||
// If we already have an account, return it | ||
if (this.account) { | ||
return this.account; | ||
} | ||
// If we're already probing, wait for the existing probe | ||
if (this._probePromise) { | ||
return this._probePromise; | ||
} | ||
const release = await mutex.obtain(); | ||
return await new Promise<WalletAccount | undefined>(async (resolve) => { | ||
try { | ||
this._probePromise = this.probe(); | ||
const result = await this._probePromise; | ||
resolve(result); | ||
} finally { | ||
this._probePromise = null; | ||
} | ||
}).finally(() => { | ||
release(); | ||
}); | ||
} | ||
request: RequestFn = async (call) => { | ||
switch (call.type) { | ||
case "wallet_getPermissions": | ||
await this.probe(); | ||
await this.safeProbe(); | ||
@@ -50,3 +80,4 @@ if (this.account) { | ||
this.account = await this.probe(); | ||
this.account = await this.safeProbe(); | ||
if (!this.account && !silentMode) { | ||
@@ -53,0 +84,0 @@ this.account = await this.connect(); |
@@ -36,2 +36,3 @@ import { Policy } from "@cartridge/account-wasm"; | ||
this.address = address; | ||
this.controller = CartridgeSessionAccount.new_as_registered( | ||
@@ -38,0 +39,0 @@ rpcUrl, |
@@ -9,2 +9,3 @@ import { ec, stark, WalletAccount } from "starknet"; | ||
import { AddStarknetChainParameters } from "@starknet-io/types-js"; | ||
import { ParsedSessionPolicies } from "../policies"; | ||
@@ -24,2 +25,3 @@ interface SessionRegistration { | ||
redirectUrl: string; | ||
keychainUrl?: string; | ||
}; | ||
@@ -35,11 +37,40 @@ | ||
protected _redirectUrl: string; | ||
protected _policies: SessionPolicies; | ||
protected _policies: ParsedSessionPolicies; | ||
protected _keychainUrl: string; | ||
constructor({ rpc, chainId, policies, redirectUrl }: SessionOptions) { | ||
constructor({ | ||
rpc, | ||
chainId, | ||
policies, | ||
redirectUrl, | ||
keychainUrl, | ||
}: SessionOptions) { | ||
super(); | ||
this._policies = { | ||
verified: false, | ||
contracts: policies.contracts | ||
? Object.fromEntries( | ||
Object.entries(policies.contracts).map(([address, contract]) => [ | ||
address, | ||
{ | ||
...contract, | ||
methods: contract.methods.map((method) => ({ | ||
...method, | ||
authorized: true, | ||
})), | ||
}, | ||
]), | ||
) | ||
: undefined, | ||
messages: policies.messages?.map((message) => ({ | ||
...message, | ||
authorized: true, | ||
})), | ||
}; | ||
this._rpcUrl = rpc; | ||
this._chainId = chainId; | ||
this._redirectUrl = redirectUrl; | ||
this._policies = policies; | ||
this._keychainUrl = keychainUrl || KEYCHAIN_URL; | ||
@@ -51,2 +82,38 @@ if (typeof window !== "undefined") { | ||
private validatePoliciesSubset( | ||
newPolicies: ParsedSessionPolicies, | ||
existingPolicies: ParsedSessionPolicies, | ||
): boolean { | ||
if (newPolicies.contracts) { | ||
if (!existingPolicies.contracts) return false; | ||
for (const [address, contract] of Object.entries(newPolicies.contracts)) { | ||
const existingContract = existingPolicies.contracts[address]; | ||
if (!existingContract) return false; | ||
for (const method of contract.methods) { | ||
const existingMethod = existingContract.methods.find( | ||
(m) => m.entrypoint === method.entrypoint, | ||
); | ||
if (!existingMethod || !existingMethod.authorized) return false; | ||
} | ||
} | ||
} | ||
if (newPolicies.messages) { | ||
if (!existingPolicies.messages) return false; | ||
for (const message of newPolicies.messages) { | ||
const existingMessage = existingPolicies.messages.find( | ||
(m) => | ||
JSON.stringify(m.domain) === JSON.stringify(message.domain) && | ||
JSON.stringify(m.types) === JSON.stringify(message.types), | ||
); | ||
if (!existingMessage || !existingMessage.authorized) return false; | ||
} | ||
} | ||
return true; | ||
} | ||
async username() { | ||
@@ -58,14 +125,20 @@ await this.tryRetrieveFromQueryOrStorage(); | ||
async probe(): Promise<WalletAccount | undefined> { | ||
await this.tryRetrieveFromQueryOrStorage(); | ||
return; | ||
if (this.account) { | ||
return this.account; | ||
} | ||
this.account = await this.tryRetrieveFromQueryOrStorage(); | ||
return this.account; | ||
} | ||
async connect(): Promise<WalletAccount | undefined> { | ||
await this.tryRetrieveFromQueryOrStorage(); | ||
if (this.account) { | ||
return this.account; | ||
} | ||
this.account = await this.tryRetrieveFromQueryOrStorage(); | ||
if (this.account) { | ||
return; | ||
return this.account; | ||
} | ||
// Generate a random local key pair | ||
const pk = stark.randomAddress(); | ||
@@ -82,3 +155,7 @@ const publicKey = ec.starkCurve.getStarkKey(pk); | ||
const url = `${KEYCHAIN_URL}/session?public_key=${publicKey}&redirect_uri=${ | ||
localStorage.setItem("sessionPolicies", JSON.stringify(this._policies)); | ||
const url = `${ | ||
this._keychainUrl | ||
}/session?public_key=${publicKey}&redirect_uri=${ | ||
this._redirectUrl | ||
@@ -92,3 +169,3 @@ }&redirect_query_name=startapp&policies=${JSON.stringify( | ||
return; | ||
return this.account; | ||
} | ||
@@ -107,2 +184,3 @@ | ||
localStorage.removeItem("session"); | ||
localStorage.removeItem("sessionPolicies"); | ||
this.account = undefined; | ||
@@ -114,2 +192,6 @@ this._username = undefined; | ||
async tryRetrieveFromQueryOrStorage() { | ||
if (this.account) { | ||
return this.account; | ||
} | ||
const signerString = localStorage.getItem("sessionSigner"); | ||
@@ -147,2 +229,43 @@ const signer = signerString ? JSON.parse(signerString) : null; | ||
// Check expiration | ||
const expirationTime = parseInt(sessionRegistration.expiresAt) * 1000; | ||
console.log("Session expiration check:", { | ||
expirationTime, | ||
currentTime: Date.now(), | ||
expired: Date.now() >= expirationTime, | ||
}); | ||
if (Date.now() >= expirationTime) { | ||
console.log("Session expired, clearing stored session"); | ||
this.clearStoredSession(); | ||
return; | ||
} | ||
// Check stored policies | ||
const storedPoliciesStr = localStorage.getItem("sessionPolicies"); | ||
console.log("Checking stored policies:", { | ||
storedPoliciesStr, | ||
currentPolicies: this._policies, | ||
}); | ||
if (storedPoliciesStr) { | ||
const storedPolicies = JSON.parse( | ||
storedPoliciesStr, | ||
) as ParsedSessionPolicies; | ||
const isValid = this.validatePoliciesSubset( | ||
this._policies, | ||
storedPolicies, | ||
); | ||
console.log("Policy validation result:", { | ||
isValid, | ||
storedPolicies, | ||
requestedPolicies: this._policies, | ||
}); | ||
if (!isValid) { | ||
console.log("Policy validation failed, clearing stored session"); | ||
this.clearStoredSession(); | ||
return; | ||
} | ||
} | ||
this._username = sessionRegistration.username; | ||
@@ -161,2 +284,8 @@ this.account = new SessionAccount(this, { | ||
} | ||
private clearStoredSession(): void { | ||
localStorage.removeItem("sessionSigner"); | ||
localStorage.removeItem("session"); | ||
localStorage.removeItem("sessionPolicies"); | ||
} | ||
} |
@@ -15,2 +15,3 @@ import { | ||
import { AddStarknetChainParameters } from "@starknet-io/types-js"; | ||
import { ParsedSessionPolicies, parsePolicies } from "../policies"; | ||
@@ -29,3 +30,3 @@ interface SessionRegistration { | ||
protected _username?: string; | ||
protected _policies: SessionPolicies; | ||
protected _policies: ParsedSessionPolicies; | ||
private _rpcUrl: string; | ||
@@ -49,3 +50,3 @@ | ||
this._chainId = chainId; | ||
this._policies = policies; | ||
this._policies = parsePolicies(policies); | ||
@@ -52,0 +53,0 @@ if (typeof window !== "undefined") { |
@@ -15,2 +15,3 @@ import { | ||
import { ChainId } from "@starknet-io/types-js"; | ||
import { ParsedSessionPolicies } from "./policies"; | ||
@@ -92,3 +93,3 @@ // Whitelist of allowed property names to prevent prototype pollution | ||
export function toWasmPolicies(policies: SessionPolicies): wasm.Policy[] { | ||
export function toWasmPolicies(policies: ParsedSessionPolicies): wasm.Policy[] { | ||
return [ | ||
@@ -100,2 +101,3 @@ ...Object.entries(policies.contracts ?? {}).flatMap( | ||
method: m.entrypoint, | ||
authorized: m.authorized, | ||
})), | ||
@@ -118,2 +120,3 @@ ), | ||
scope_hash: hash.computePoseidonHash(domainHash, typeHash), | ||
authorized: p.authorized, | ||
}; | ||
@@ -120,0 +123,0 @@ }), |
@@ -8,6 +8,5 @@ { | ||
"composite": false, | ||
"incremental": false, | ||
"moduleResolution": "node" | ||
"incremental": false | ||
}, | ||
"include": ["src/**/*", "package.json"] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1362879
100
16575
+ Added@cartridge/account-wasm@0.6.0(transitive)
- Removedstarknet@^6.11.0
- Removed@cartridge/account-wasm@0.5.9(transitive)