Reduce diff
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import {describe, expect, it} from '@jest/globals'
|
||||
|
||||
import * as persisted from '../index'
|
||||
import {defaults} from '../schema'
|
||||
|
||||
describe('generic persisted API', () => {
|
||||
it('rejects session writes', () => {
|
||||
expect(() => persisted.write('session', defaults.session)).toThrow(
|
||||
"Session state must be written through '#/state/persisted/session'",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
applySessionUpdate,
|
||||
getCredentialState,
|
||||
type SessionCredentialMutation,
|
||||
} from '../session'
|
||||
} from '../session-merge'
|
||||
|
||||
const DID = 'did:plc:example123'
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
tryStringify,
|
||||
} from '#/state/persisted/schema'
|
||||
import {device} from '#/storage'
|
||||
import {applySessionUpdate} from './session'
|
||||
import {runWithSessionCredentialLock} from './session-lock'
|
||||
import {
|
||||
applySessionUpdate,
|
||||
type SessionCredentialMutation,
|
||||
} from './session-merge'
|
||||
import {type PersistedApi} from './types'
|
||||
import {normalizeData} from './util'
|
||||
|
||||
export type {SessionCredentialMutation} from './session'
|
||||
export {runWithSessionCredentialLock} from './session-lock'
|
||||
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
|
||||
export {defaults} from '#/state/persisted/schema'
|
||||
|
||||
@@ -52,6 +52,11 @@ export function write<K extends keyof Schema>(
|
||||
key: K,
|
||||
value: Schema[K],
|
||||
): Promise<void> {
|
||||
if (key === 'session') {
|
||||
throw new Error(
|
||||
"Session state must be written through '#/state/persisted/session'",
|
||||
)
|
||||
}
|
||||
return enqueueWrite(async () => {
|
||||
const next = normalizeData({
|
||||
..._state,
|
||||
@@ -63,12 +68,13 @@ export function write<K extends keyof Schema>(
|
||||
}
|
||||
write satisfies PersistedApi['write']
|
||||
|
||||
export function updateSession({
|
||||
/** @internal Use `#/state/persisted/session` instead. */
|
||||
export function writeSessionInternal({
|
||||
nextSession,
|
||||
credentialMutations,
|
||||
}: {
|
||||
nextSession: Schema['session']
|
||||
credentialMutations: import('./session').SessionCredentialMutation[]
|
||||
credentialMutations: SessionCredentialMutation[]
|
||||
}): Promise<Schema['session']> {
|
||||
return enqueueWrite(async () => {
|
||||
const session = applySessionUpdate({
|
||||
@@ -82,9 +88,6 @@ export function updateSession({
|
||||
return session
|
||||
})
|
||||
}
|
||||
updateSession satisfies PersistedApi['updateSession']
|
||||
runWithSessionCredentialLock satisfies PersistedApi['runWithSessionCredentialLock']
|
||||
|
||||
export function onUpdate<K extends keyof Schema>(
|
||||
_key: K,
|
||||
_cb: (v: Schema[K]) => void,
|
||||
|
||||
@@ -8,17 +8,15 @@ import {
|
||||
tryParse,
|
||||
tryStringify,
|
||||
} from '#/state/persisted/schema'
|
||||
import {runWithSessionCredentialLock} from './session-lock'
|
||||
import {
|
||||
applySessionUpdate,
|
||||
getCredentialState,
|
||||
type SessionCredentialMutation,
|
||||
} from './session'
|
||||
import {runWithSessionCredentialLock} from './session-lock'
|
||||
} from './session-merge'
|
||||
import {type PersistedApi} from './types'
|
||||
import {normalizeData} from './util'
|
||||
|
||||
export type {SessionCredentialMutation} from './session'
|
||||
export {runWithSessionCredentialLock} from './session-lock'
|
||||
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
|
||||
export {defaults} from '#/state/persisted/schema'
|
||||
|
||||
@@ -74,6 +72,11 @@ export function write<K extends keyof Schema>(
|
||||
key: K,
|
||||
value: Schema[K],
|
||||
): Promise<void> {
|
||||
if (key === 'session') {
|
||||
throw new Error(
|
||||
"Session state must be written through '#/state/persisted/session'",
|
||||
)
|
||||
}
|
||||
return runWithSessionCredentialLock({
|
||||
accountDids: [],
|
||||
operation: () => {
|
||||
@@ -106,8 +109,9 @@ export function write<K extends keyof Schema>(
|
||||
}
|
||||
write satisfies PersistedApi['write']
|
||||
|
||||
/** @internal Use `#/state/persisted/session` instead. */
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export async function updateSession({
|
||||
export async function writeSessionInternal({
|
||||
nextSession,
|
||||
credentialMutations,
|
||||
}: {
|
||||
@@ -134,9 +138,6 @@ export async function updateSession({
|
||||
broadcastUpdate({key: 'session', invalidations})
|
||||
return session
|
||||
}
|
||||
updateSession satisfies PersistedApi['updateSession']
|
||||
runWithSessionCredentialLock satisfies PersistedApi['runWithSessionCredentialLock']
|
||||
|
||||
export function onUpdate<K extends keyof Schema>(
|
||||
key: K,
|
||||
cb: (v: Schema[K]) => void,
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import {jwtDecode} from 'jwt-decode'
|
||||
|
||||
import {
|
||||
type PersistedAccount,
|
||||
type PersistedCredentialState,
|
||||
type Schema,
|
||||
} from './schema'
|
||||
|
||||
export type SessionCredentialMutation =
|
||||
| {
|
||||
type: 'refresh'
|
||||
accountDid: string
|
||||
baseRefreshJwt: string | undefined
|
||||
resultRefreshJwt: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'expire'
|
||||
accountDid: string
|
||||
baseRefreshJwt: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'login'
|
||||
accountDid: string
|
||||
resultRefreshJwt: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'logout'
|
||||
accountDid: string
|
||||
}
|
||||
| {
|
||||
type: 'remove'
|
||||
accountDid: string
|
||||
}
|
||||
|
||||
const CREDENTIAL_FIELDS = new Set<keyof PersistedAccount>([
|
||||
'accessJwt',
|
||||
'refreshJwt',
|
||||
])
|
||||
|
||||
/** Read the server-side refresh-token generation, tolerating legacy bad data. */
|
||||
export function getRefreshJti({
|
||||
refreshJwt,
|
||||
}: {
|
||||
refreshJwt: string | undefined
|
||||
}): string | undefined {
|
||||
if (!refreshJwt) return undefined
|
||||
try {
|
||||
const decoded = jwtDecode(refreshJwt)
|
||||
if (typeof decoded.jti === 'string') return decoded.jti
|
||||
} catch {}
|
||||
/* A malformed legacy token still needs stable local identity. */
|
||||
return refreshJwt
|
||||
}
|
||||
|
||||
export function getCredentialState({
|
||||
session,
|
||||
accountDid,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
accountDid: string
|
||||
}): PersistedCredentialState {
|
||||
const stored = session.credentialStates?.[accountDid]
|
||||
if (stored) return stored
|
||||
const account = session.accounts.find(
|
||||
candidate => candidate.did === accountDid,
|
||||
)
|
||||
return {
|
||||
credentialVersion: 0,
|
||||
refreshJti: getRefreshJti({refreshJwt: account?.refreshJwt}),
|
||||
status: account?.refreshJwt ? 'active' : 'logged-out',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a session snapshot to the latest persisted session without allowing its
|
||||
* credential fields to overwrite newer credential generations.
|
||||
*/
|
||||
export function applySessionUpdate({
|
||||
storedSession,
|
||||
nextSession,
|
||||
credentialMutations,
|
||||
}: {
|
||||
storedSession: Schema['session']
|
||||
nextSession: Schema['session']
|
||||
credentialMutations: SessionCredentialMutation[]
|
||||
}): Schema['session'] {
|
||||
const incomingByDid = new Map(
|
||||
nextSession.accounts.map(account => [account.did, account]),
|
||||
)
|
||||
const storedByDid = new Map(
|
||||
storedSession.accounts.map(account => [account.did, account]),
|
||||
)
|
||||
|
||||
/* Incoming metadata wins, but stored credentials remain authoritative. */
|
||||
const accounts = nextSession.accounts.map(incoming => {
|
||||
const stored = storedByDid.get(incoming.did)
|
||||
return stored
|
||||
? mergeAccountMetadata({storedAccount: stored, incomingAccount: incoming})
|
||||
: incoming
|
||||
})
|
||||
for (const stored of storedSession.accounts) {
|
||||
if (!incomingByDid.has(stored.did)) accounts.push(stored)
|
||||
}
|
||||
|
||||
const credentialStates: Record<string, PersistedCredentialState> = {
|
||||
...storedSession.credentialStates,
|
||||
}
|
||||
for (const account of storedSession.accounts) {
|
||||
credentialStates[account.did] = getCredentialState({
|
||||
session: storedSession,
|
||||
accountDid: account.did,
|
||||
})
|
||||
}
|
||||
|
||||
const result: Schema['session'] = {
|
||||
accounts,
|
||||
currentAccount: nextSession.currentAccount,
|
||||
credentialStates,
|
||||
}
|
||||
|
||||
for (const mutation of credentialMutations) {
|
||||
applyCredentialMutation({session: result, nextSession, mutation})
|
||||
}
|
||||
|
||||
result.accounts = result.accounts
|
||||
.filter(
|
||||
account =>
|
||||
getCredentialState({session: result, accountDid: account.did})
|
||||
.status !== 'removed',
|
||||
)
|
||||
.map(account => {
|
||||
const credentialState = getCredentialState({
|
||||
session: result,
|
||||
accountDid: account.did,
|
||||
})
|
||||
return credentialState.status === 'logged-out'
|
||||
? {...account, accessJwt: undefined, refreshJwt: undefined}
|
||||
: account
|
||||
})
|
||||
|
||||
const currentDid = result.currentAccount?.did
|
||||
const currentAccount = currentDid
|
||||
? result.accounts.find(account => account.did === currentDid)
|
||||
: undefined
|
||||
const currentCredentialState = currentDid
|
||||
? getCredentialState({session: result, accountDid: currentDid})
|
||||
: undefined
|
||||
result.currentAccount =
|
||||
currentAccount && currentCredentialState?.status === 'active'
|
||||
? currentAccount
|
||||
: undefined
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function applyCredentialMutation({
|
||||
session,
|
||||
nextSession,
|
||||
mutation,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
nextSession: Schema['session']
|
||||
mutation: SessionCredentialMutation
|
||||
}) {
|
||||
const previousState = getCredentialState({
|
||||
session,
|
||||
accountDid: mutation.accountDid,
|
||||
})
|
||||
const nextAccount = nextSession.accounts.find(
|
||||
account => account.did === mutation.accountDid,
|
||||
)
|
||||
|
||||
switch (mutation.type) {
|
||||
case 'refresh': {
|
||||
const baseRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.baseRefreshJwt,
|
||||
})
|
||||
const resultRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.resultRefreshJwt,
|
||||
})
|
||||
if (
|
||||
previousState.status !== 'active' ||
|
||||
previousState.refreshJti !== baseRefreshJti
|
||||
) {
|
||||
/* The stored generation already advanced or became a tombstone. */
|
||||
return
|
||||
}
|
||||
if (!nextAccount || !resultRefreshJti) return
|
||||
replaceAccount({session, account: nextAccount})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion:
|
||||
resultRefreshJti === previousState.refreshJti
|
||||
? previousState.credentialVersion
|
||||
: previousState.credentialVersion + 1,
|
||||
refreshJti: resultRefreshJti,
|
||||
status: 'active',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'expire': {
|
||||
const baseRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.baseRefreshJwt,
|
||||
})
|
||||
if (
|
||||
previousState.status !== 'active' ||
|
||||
previousState.refreshJti !== baseRefreshJti
|
||||
) {
|
||||
return
|
||||
}
|
||||
clearAccountCredentials({session, accountDid: mutation.accountDid})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
status: 'logged-out',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'login': {
|
||||
const resultRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.resultRefreshJwt,
|
||||
})
|
||||
if (!nextAccount || !resultRefreshJti) return
|
||||
replaceAccount({session, account: nextAccount})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
refreshJti: resultRefreshJti,
|
||||
status: 'active',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'logout': {
|
||||
clearAccountCredentials({session, accountDid: mutation.accountDid})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
status: 'logged-out',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'remove': {
|
||||
session.accounts = session.accounts.filter(
|
||||
account => account.did !== mutation.accountDid,
|
||||
)
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
status: 'removed',
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeAccountMetadata({
|
||||
storedAccount,
|
||||
incomingAccount,
|
||||
}: {
|
||||
storedAccount: PersistedAccount
|
||||
incomingAccount: PersistedAccount
|
||||
}): PersistedAccount {
|
||||
const merged = {...storedAccount}
|
||||
for (const key of Object.keys(
|
||||
incomingAccount,
|
||||
) as (keyof PersistedAccount)[]) {
|
||||
if (!CREDENTIAL_FIELDS.has(key)) {
|
||||
Object.assign(merged, {[key]: incomingAccount[key]})
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
function replaceAccount({
|
||||
session,
|
||||
account,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
account: PersistedAccount
|
||||
}) {
|
||||
session.accounts = [
|
||||
account,
|
||||
...session.accounts.filter(candidate => candidate.did !== account.did),
|
||||
]
|
||||
}
|
||||
|
||||
function clearAccountCredentials({
|
||||
session,
|
||||
accountDid,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
accountDid: string
|
||||
}) {
|
||||
session.accounts = session.accounts.map(account =>
|
||||
account.did === accountDid
|
||||
? {...account, accessJwt: undefined, refreshJwt: undefined}
|
||||
: account,
|
||||
)
|
||||
}
|
||||
+27
-276
@@ -1,294 +1,45 @@
|
||||
import {jwtDecode} from 'jwt-decode'
|
||||
import * as persisted from './index'
|
||||
import {type Schema} from './schema'
|
||||
import {runWithSessionCredentialLock} from './session-lock'
|
||||
import {type SessionCredentialMutation} from './session-merge'
|
||||
|
||||
import {
|
||||
type PersistedAccount,
|
||||
type PersistedCredentialState,
|
||||
type Schema,
|
||||
} from './schema'
|
||||
export type {SessionCredentialMutation} from './session-merge'
|
||||
|
||||
export type SessionCredentialMutation =
|
||||
| {
|
||||
type: 'refresh'
|
||||
accountDid: string
|
||||
baseRefreshJwt: string | undefined
|
||||
resultRefreshJwt: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'expire'
|
||||
accountDid: string
|
||||
baseRefreshJwt: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'login'
|
||||
accountDid: string
|
||||
resultRefreshJwt: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'logout'
|
||||
accountDid: string
|
||||
}
|
||||
| {
|
||||
type: 'remove'
|
||||
accountDid: string
|
||||
}
|
||||
|
||||
const CREDENTIAL_FIELDS = new Set<keyof PersistedAccount>([
|
||||
'accessJwt',
|
||||
'refreshJwt',
|
||||
])
|
||||
|
||||
/** Read the server-side refresh-token generation, tolerating legacy bad data. */
|
||||
export function getRefreshJti({
|
||||
refreshJwt,
|
||||
}: {
|
||||
refreshJwt: string | undefined
|
||||
}): string | undefined {
|
||||
if (!refreshJwt) return undefined
|
||||
try {
|
||||
const decoded = jwtDecode(refreshJwt)
|
||||
if (typeof decoded.jti === 'string') return decoded.jti
|
||||
} catch {}
|
||||
/* A malformed legacy token still needs stable local identity. */
|
||||
return refreshJwt
|
||||
export function read(): Schema['session'] {
|
||||
return persisted.get('session')
|
||||
}
|
||||
|
||||
export function getCredentialState({
|
||||
session,
|
||||
accountDid,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
accountDid: string
|
||||
}): PersistedCredentialState {
|
||||
const stored = session.credentialStates?.[accountDid]
|
||||
if (stored) return stored
|
||||
const account = session.accounts.find(
|
||||
candidate => candidate.did === accountDid,
|
||||
)
|
||||
return {
|
||||
credentialVersion: 0,
|
||||
refreshJti: getRefreshJti({refreshJwt: account?.refreshJwt}),
|
||||
status: account?.refreshJwt ? 'active' : 'logged-out',
|
||||
}
|
||||
/** On web, synchronously read the authoritative localStorage session. */
|
||||
export function readLatest(): Schema['session'] {
|
||||
return persisted.readLatest('session')
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a session snapshot to the latest persisted session without allowing its
|
||||
* credential fields to overwrite newer credential generations.
|
||||
*/
|
||||
export function applySessionUpdate({
|
||||
storedSession,
|
||||
/** Conditionally commit a session update inside {@link runWithCredentialLock}. */
|
||||
export function write({
|
||||
nextSession,
|
||||
credentialMutations,
|
||||
}: {
|
||||
storedSession: Schema['session']
|
||||
nextSession: Schema['session']
|
||||
credentialMutations: SessionCredentialMutation[]
|
||||
}): Schema['session'] {
|
||||
const incomingByDid = new Map(
|
||||
nextSession.accounts.map(account => [account.did, account]),
|
||||
)
|
||||
const storedByDid = new Map(
|
||||
storedSession.accounts.map(account => [account.did, account]),
|
||||
)
|
||||
|
||||
/* Incoming metadata wins, but stored credentials remain authoritative. */
|
||||
const accounts = nextSession.accounts.map(incoming => {
|
||||
const stored = storedByDid.get(incoming.did)
|
||||
return stored
|
||||
? mergeAccountMetadata({storedAccount: stored, incomingAccount: incoming})
|
||||
: incoming
|
||||
}): Promise<Schema['session']> {
|
||||
return persisted.writeSessionInternal({
|
||||
nextSession,
|
||||
credentialMutations,
|
||||
})
|
||||
for (const stored of storedSession.accounts) {
|
||||
if (!incomingByDid.has(stored.did)) accounts.push(stored)
|
||||
}
|
||||
|
||||
const credentialStates: Record<string, PersistedCredentialState> = {
|
||||
...storedSession.credentialStates,
|
||||
}
|
||||
for (const account of storedSession.accounts) {
|
||||
credentialStates[account.did] = getCredentialState({
|
||||
session: storedSession,
|
||||
accountDid: account.did,
|
||||
})
|
||||
}
|
||||
|
||||
const result: Schema['session'] = {
|
||||
accounts,
|
||||
currentAccount: nextSession.currentAccount,
|
||||
credentialStates,
|
||||
}
|
||||
|
||||
for (const mutation of credentialMutations) {
|
||||
applyCredentialMutation({session: result, nextSession, mutation})
|
||||
}
|
||||
|
||||
result.accounts = result.accounts
|
||||
.filter(
|
||||
account =>
|
||||
getCredentialState({session: result, accountDid: account.did})
|
||||
.status !== 'removed',
|
||||
)
|
||||
.map(account => {
|
||||
const credentialState = getCredentialState({
|
||||
session: result,
|
||||
accountDid: account.did,
|
||||
})
|
||||
return credentialState.status === 'logged-out'
|
||||
? {...account, accessJwt: undefined, refreshJwt: undefined}
|
||||
: account
|
||||
})
|
||||
|
||||
const currentDid = result.currentAccount?.did
|
||||
const currentAccount = currentDid
|
||||
? result.accounts.find(account => account.did === currentDid)
|
||||
: undefined
|
||||
const currentCredentialState = currentDid
|
||||
? getCredentialState({session: result, accountDid: currentDid})
|
||||
: undefined
|
||||
result.currentAccount =
|
||||
currentAccount && currentCredentialState?.status === 'active'
|
||||
? currentAccount
|
||||
: undefined
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function applyCredentialMutation({
|
||||
session,
|
||||
nextSession,
|
||||
mutation,
|
||||
export function runWithCredentialLock<T>({
|
||||
accountDids,
|
||||
operation,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
nextSession: Schema['session']
|
||||
mutation: SessionCredentialMutation
|
||||
}) {
|
||||
const previousState = getCredentialState({
|
||||
session,
|
||||
accountDid: mutation.accountDid,
|
||||
})
|
||||
const nextAccount = nextSession.accounts.find(
|
||||
account => account.did === mutation.accountDid,
|
||||
)
|
||||
|
||||
switch (mutation.type) {
|
||||
case 'refresh': {
|
||||
const baseRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.baseRefreshJwt,
|
||||
})
|
||||
const resultRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.resultRefreshJwt,
|
||||
})
|
||||
if (
|
||||
previousState.status !== 'active' ||
|
||||
previousState.refreshJti !== baseRefreshJti
|
||||
) {
|
||||
/* The stored generation already advanced or became a tombstone. */
|
||||
return
|
||||
}
|
||||
if (!nextAccount || !resultRefreshJti) return
|
||||
replaceAccount({session, account: nextAccount})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion:
|
||||
resultRefreshJti === previousState.refreshJti
|
||||
? previousState.credentialVersion
|
||||
: previousState.credentialVersion + 1,
|
||||
refreshJti: resultRefreshJti,
|
||||
status: 'active',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'expire': {
|
||||
const baseRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.baseRefreshJwt,
|
||||
})
|
||||
if (
|
||||
previousState.status !== 'active' ||
|
||||
previousState.refreshJti !== baseRefreshJti
|
||||
) {
|
||||
return
|
||||
}
|
||||
clearAccountCredentials({session, accountDid: mutation.accountDid})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
status: 'logged-out',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'login': {
|
||||
const resultRefreshJti = getRefreshJti({
|
||||
refreshJwt: mutation.resultRefreshJwt,
|
||||
})
|
||||
if (!nextAccount || !resultRefreshJti) return
|
||||
replaceAccount({session, account: nextAccount})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
refreshJti: resultRefreshJti,
|
||||
status: 'active',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'logout': {
|
||||
clearAccountCredentials({session, accountDid: mutation.accountDid})
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
status: 'logged-out',
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'remove': {
|
||||
session.accounts = session.accounts.filter(
|
||||
account => account.did !== mutation.accountDid,
|
||||
)
|
||||
session.credentialStates![mutation.accountDid] = {
|
||||
credentialVersion: previousState.credentialVersion + 1,
|
||||
status: 'removed',
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
accountDids: string[]
|
||||
operation: () => T | Promise<T>
|
||||
}): Promise<T> {
|
||||
return runWithSessionCredentialLock({accountDids, operation})
|
||||
}
|
||||
|
||||
function mergeAccountMetadata({
|
||||
storedAccount,
|
||||
incomingAccount,
|
||||
}: {
|
||||
storedAccount: PersistedAccount
|
||||
incomingAccount: PersistedAccount
|
||||
}): PersistedAccount {
|
||||
const merged = {...storedAccount}
|
||||
for (const key of Object.keys(
|
||||
incomingAccount,
|
||||
) as (keyof PersistedAccount)[]) {
|
||||
if (!CREDENTIAL_FIELDS.has(key)) {
|
||||
Object.assign(merged, {[key]: incomingAccount[key]})
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
function replaceAccount({
|
||||
session,
|
||||
account,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
account: PersistedAccount
|
||||
}) {
|
||||
session.accounts = [
|
||||
account,
|
||||
...session.accounts.filter(candidate => candidate.did !== account.did),
|
||||
]
|
||||
}
|
||||
|
||||
function clearAccountCredentials({
|
||||
session,
|
||||
accountDid,
|
||||
}: {
|
||||
session: Schema['session']
|
||||
accountDid: string
|
||||
}) {
|
||||
session.accounts = session.accounts.map(account =>
|
||||
account.did === accountDid
|
||||
? {...account, accessJwt: undefined, refreshJwt: undefined}
|
||||
: account,
|
||||
)
|
||||
export function onUpdate(
|
||||
callback: (session: Schema['session']) => void,
|
||||
): () => void {
|
||||
return persisted.onUpdate('session', callback)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {type Schema} from './schema'
|
||||
import {type SessionCredentialMutation} from './session'
|
||||
|
||||
export type PersistedApi = {
|
||||
init(): Promise<void>
|
||||
@@ -14,14 +13,6 @@ export type PersistedApi = {
|
||||
*/
|
||||
readLatest<K extends keyof Schema>(key: K): Schema[K]
|
||||
write<K extends keyof Schema>(key: K, value: Schema[K]): Promise<void>
|
||||
updateSession(params: {
|
||||
nextSession: Schema['session']
|
||||
credentialMutations: SessionCredentialMutation[]
|
||||
}): Promise<Schema['session']>
|
||||
runWithSessionCredentialLock<T>(params: {
|
||||
accountDids: string[]
|
||||
operation: () => T | Promise<T>
|
||||
}): Promise<T>
|
||||
onUpdate<K extends keyof Schema>(
|
||||
key: K,
|
||||
cb: (v: Schema[K]) => void,
|
||||
|
||||
Reference in New Issue
Block a user