Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@wharfkit/session",
"description": "Create account-based sessions, perform transactions, and allow users to login using Antelope-based blockchains.",
"version": "1.6.1",
"version": "1.7.0",
"homepage": "https://github.com/wharfkit/session",
"license": "BSD-3-Clause",
"main": "lib/session.js",
Expand Down
35 changes: 35 additions & 0 deletions src/encoded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Checksum256, Name, Struct} from '@wharfkit/antelope'
import {RestoreArgs, SerializedSession, Session} from './index-module'

/**
* The metadata of an [[AccountCreationPlugin]].
*/
@Struct.type('url_encoded_session')
export class URLEncodedSession extends Struct {
@Struct.field(Checksum256) declare chain: Checksum256
@Struct.field(Name) declare actor: Name
@Struct.field(Name) declare permission: Name
@Struct.field('string') declare walletPlugin: string
@Struct.field('string', {optional: true}) declare data?: string

static fromSession(data: Session | SerializedSession): URLEncodedSession {
Copy link
Copy Markdown
Contributor

@dafuga dafuga Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need some better error handling for situations when bad data is passed to this method?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializer should catch any bad data and report on it.

const session = data instanceof Session ? data.serialize() : data
return new URLEncodedSession({
chain: session.chain,
actor: session.actor,
permission: session.permission,
walletPlugin: JSON.stringify(session.walletPlugin),
data: JSON.stringify(session.data),
})
}

get args(): RestoreArgs {
return {
chain: this.chain,
actor: this.actor,
permission: this.permission,
walletPlugin: JSON.parse(this.walletPlugin),
data: this.data ? JSON.parse(this.data) : undefined,
}
}
}
1 change: 1 addition & 0 deletions src/index-module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './encoded'
export * from './kit'
export * from './login'
export * from './session'
Expand Down
67 changes: 53 additions & 14 deletions src/kit.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {ChainDefinition, type ChainDefinitionType, type Fetch} from '@wharfkit/common'
import type {Contract} from '@wharfkit/contract'
import {
Bytes,
Checksum256,
Checksum256Type,
Name,
NameType,
PermissionLevel,
PermissionLevelType,
Serializer,
} from '@wharfkit/antelope'

import {
Expand Down Expand Up @@ -34,6 +36,7 @@ import {
CreateAccountOptions,
CreateAccountResponse,
} from './account-creation'
import {URLEncodedSession} from './encoded'

export interface LoginOptions {
arbitrary?: Record<string, any> // Arbitrary data that will be passed via context to wallet plugin
Expand Down Expand Up @@ -75,6 +78,9 @@ export interface SessionKitArgs {

export interface SessionKitOptions {
abis?: TransactABIDef[]
acceptUrlSession?: boolean
acceptUrlSessionParam?: string
accountCreationPlugins?: AccountCreationPlugin[]
allowModify?: boolean
contracts?: Contract[]
expireSeconds?: number
Expand All @@ -83,14 +89,16 @@ export interface SessionKitOptions {
storage?: SessionStorage
transactPlugins?: TransactPlugin[]
transactPluginsOptions?: TransactPluginsOptions
accountCreationPlugins?: AccountCreationPlugin[]
}

/**
* Request a session from an account.
*/
export class SessionKit {
readonly abis: TransactABIDef[] = []
readonly acceptUrlSession: boolean = false
readonly acceptUrlSessionParam: string = 'incomingWharfSession'
readonly accountCreationPlugins: AccountCreationPlugin[] = []
readonly allowModify: boolean = true
readonly appName: string
readonly expireSeconds: number = 120
Expand All @@ -101,7 +109,6 @@ export class SessionKit {
readonly transactPluginsOptions: TransactPluginsOptions = {}
readonly ui: UserInterface
readonly walletPlugins: WalletPlugin[]
readonly accountCreationPlugins: AccountCreationPlugin[] = []
public chains: ChainDefinition[]

constructor(args: SessionKitArgs, options: SessionKitOptions = {}) {
Expand All @@ -123,6 +130,14 @@ export class SessionKit {
if (options.abis) {
this.abis = [...options.abis]
}
// Determine if URL sessions should be accepted
if (options.acceptUrlSession) {
this.acceptUrlSession = options.acceptUrlSession
}
// Determine if URL session param name was overridden
if (options.acceptUrlSessionParam) {
this.acceptUrlSessionParam = options.acceptUrlSessionParam
}
// Extract any ABIs from the Contract instances provided
if (options.contracts) {
this.abis.push(...options.contracts.map((c) => ({account: c.account, abi: c.abi})))
Expand Down Expand Up @@ -567,13 +582,36 @@ export class SessionKit {
}

async restore(args?: RestoreArgs, options?: LoginOptions): Promise<Session | undefined> {
// If no args were provided, attempt to default restore the session from storage.
if (!args) {
const data = await this.storage.read('session')
if (data) {
args = JSON.parse(data)
} else {
return
if (this.acceptUrlSession && typeof window !== 'undefined') {
// Attempt to retrieve session from current URL params
const url = new URL(window.location.href)
const incoming = url.searchParams.get(this.acceptUrlSessionParam)
if (incoming) {
try {
const encodedSession = Serializer.decode({
data: Bytes.from(incoming, 'hex'),
type: URLEncodedSession,
})
args = encodedSession.args
// Remove the session from the URL to prevent reuse
url.searchParams.delete(this.acceptUrlSessionParam)
window.history.replaceState(null, '', url)
} catch (e) {
// eslint-disable-next-line no-console -- warn the developer since this may be unintentional
console.warn('Failed to decode session from URL: ' + incoming)
}
}
}

// If no args were provided or retrieved from the URL, attempt to default restore the session from storage.
if (!args) {
const data = await this.storage.read('session')
if (!args && data) {
args = JSON.parse(data)
} else {
return
}
}
}

Expand All @@ -585,14 +623,13 @@ export class SessionKit {
args.chain instanceof ChainDefinition ? args.chain.id : args.chain
)

let serializedSession: SerializedSession
let serializedSession: SerializedSession | undefined

// Retrieve all sessions from storage
const data = await this.storage.read('sessions')

if (data) {
// If sessions exist, restore the session that matches the provided args
const sessions = JSON.parse(data)
const sessions = JSON.parse(data) as SerializedSession[]
if (args.actor && args.permission) {
// If all args are provided, return exact match
serializedSession = sessions.find((s: SerializedSession) => {
Expand All @@ -609,8 +646,10 @@ export class SessionKit {
return args && chainId.equals(s.chain) && s.default
})
}
} else {
// If no sessions were found, but the args contains all the data for a serialized session, use args
}

// If no sessions were found, but the args contains all the data for a serialized session, use args
if (!serializedSession) {
if (args.actor && args.permission && args.walletPlugin) {
serializedSession = {
chain: String(chainId),
Expand Down Expand Up @@ -638,7 +677,7 @@ export class SessionKit {
if (!args) {
return false
}
return p.id === serializedSession.walletPlugin.id
return p.id === serializedSession?.walletPlugin.id
})

if (!walletPlugin) {
Expand Down
33 changes: 33 additions & 0 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ import {SessionStorage} from './storage'
import {getFetch, getPluginTranslations} from './utils'
import {SerializedWalletPlugin, WalletPlugin, WalletPluginSignResponse} from './wallet'
import {UserInterface} from './ui'
import {URLEncodedSession} from './encoded'

/**
* Arguments required to create a new [[Session]].
*/
export interface SessionArgs {
actor?: NameType
chain: ChainDefinitionType
data?: Record<string, any>
permission?: NameType
permissionLevel?: PermissionLevelType | string
walletPlugin: WalletPlugin
Expand Down Expand Up @@ -83,6 +85,8 @@ export interface SerializedSession {
data?: Record<string, any>
}

export type SessionEncodingTypes = 'encoded' | 'json' | 'serialized' | 'url'

/**
* A representation of a session to interact with a specific blockchain account.
*/
Expand Down Expand Up @@ -140,6 +144,11 @@ export class Session {
// Set the WalletPlugin for this session
this.walletPlugin = args.walletPlugin

// Initialize any arbitrary data provided to the constructor
if (args.data) {
this.data = args.data
}

// Handle all the optional values provided
if (options.appName) {
this.appName = String(options.appName)
Expand Down Expand Up @@ -682,6 +691,30 @@ export class Session {

return abiCache
}

encode(encoding: 'encoded'): URLEncodedSession
encode(encoding: 'json'): string
encode(encoding: 'serialized'): SerializedSession
encode(encoding: 'url'): string
encode(
encoding: SessionEncodingTypes = 'serialized'
): string | SerializedSession | URLEncodedSession {
const serialized = this.serialize()
switch (encoding) {
case 'encoded':
return URLEncodedSession.fromSession(serialized)
case 'json':
return JSON.stringify(serialized)
case 'serialized':
return serialized
case 'url':
return Serializer.encode({
object: URLEncodedSession.fromSession(serialized),
}).toString('hex')
default:
throw new Error(`Unsupported encoding: ${encoding}`)
}
}
}

async function processReturnValues(
Expand Down
58 changes: 58 additions & 0 deletions test/tests/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,64 @@ suite('kit', function () {
assert.isTrue(restoredJUNGLE.chain.id.equals(Chains.Jungle4.id))
}
})
test('session from URL', async function () {
const sessionKit = new SessionKit(mockSessionKitArgs, {
...mockSessionKitOptions,
acceptUrlSession: true,
storage: new MockStorage(),
})

// Ensure no sessions
const sessions = await sessionKit.restoreAll()
assert.lengthOf(sessions, 0)

// Mock window object for Node.js environment
if (typeof globalThis.window === 'undefined') {
;(globalThis as any).window = {}
}

// Mock window.location with a writable href property
if (typeof (globalThis as any).window.location === 'undefined') {
;(globalThis as any).window.location = {href: ''}
} else {
try {
;(globalThis as any).window.location.href =
(globalThis as any).window.location.href || ''
} catch {
;(globalThis as any).window.location = {href: ''}
}
}

// Set the href to include an incomingWharfSession parameter
window.location.href =
'https://somewhere.com?incomingWharfSession=73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d104208d9c1754de3000000000090b1ca737b226964223a2277616c6c65742d706c7567696e2d707269766174656b6579222c2264617461223a7b22707269766174654b6579223a225056545f4b315f32355850314c7431527438376879796d6f755369654262676e554541657253317951486939777148433255656b326d677a48227d7d010f7b226669656c64223a22666f6f227d'

// Attempt to restore the session from the URL
const session = await sessionKit.restore()
if (!session) {
throw new Error('Failed to restore session from URL')
}

// Ensure session is correct
assert.isDefined(session)
assert.isTrue(session.chain.id.equals(mockChainDefinition.id), 'Incorrect chain')
assert.isTrue(session.actor.equals('wharfkit1111'), 'Incorrect actor')
assert.isTrue(session.permission.equals('test'), 'Incorrect permission')
assert.isTrue(
session.walletPlugin instanceof WalletPluginPrivateKey,
'Incorrect walletPlugin type'
)
assert.equal(session.data.field, 'foo', 'Incorrect session data')
assert.equal(session.walletPlugin.id, 'wallet-plugin-privatekey')
assert.equal(
session.walletPlugin.data.privateKey,
'PVT_K1_25XP1Lt1Rt87hyymouSieBbgnUEAerS1yQHi9wqHC2Uek2mgzH'
)

// Ensure session was persisted to storage
const sessionsAfter = await sessionKit.restoreAll()
assert.lengthOf(sessionsAfter, 1)
})
test('no session returns undefined', async function () {
const sessionKit = new SessionKit(mockSessionKitArgs, {
...mockSessionKitOptions,
Expand Down
Loading