Skip to content

Commit

Permalink
feat: Allow to use a customInstanceId as well as an existingInstanceI…
Browse files Browse the repository at this point in the history
…d, so we can differentiate between re-using an existing machine and using a custom id
  • Loading branch information
nklomp committed Mar 8, 2024
1 parent 02c5e12 commit 3aeb93d
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type StoreFindMachineStatesArgs = {
filter: FindMachineStatesFilterArgs
}

export type StoreMachineStateGetArgs = Pick<StoreMachineStateInfo, 'instanceId'>
export type StoreMachineStateGetArgs = Pick<StoreMachineStateInfo, 'instanceId' | 'tenantId'>

export type StoreMachineStateDeleteArgs = StoreMachineStateGetArgs
export type StoreMachineStateDeleteExpiredArgs = { machineName?: string; tenantId?: string; deleteDoneStates?: boolean }
Expand Down
26 changes: 14 additions & 12 deletions packages/oid4vci-holder/src/agent/OID4VCIHolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@ import { IAgentPlugin, VerifiableCredential } from '@veramo/core'
import { computeEntryHash } from '@veramo/utils'
import { v4 as uuidv4 } from 'uuid'
import { OID4VCIMachine } from '../machine/oid4vciMachine'
import {
getCredentialBranding,
getSupportedCredentials,
mapCredentialToAccept,
selectCredentialLocaleBranding,
verifyCredentialToAccept,
} from './OID4VCIHolderService'
import {
AddContactIdentityArgs,
AssertValidCredentialsArgs,
Expand All @@ -27,23 +20,29 @@ import {
CredentialTypeSelection,
GetContactArgs,
GetCredentialsArgs,
OnGetCredentialsArgs,
IOID4VCIHolder,
InitiateOID4VCIArgs,
InitiationData,
IOID4VCIHolder,
MappedCredentialToAccept,
OID4VCIHolderEvent,
OID4VCIHolderOptions,
OID4VCIMachine as OID4VCImachineId,
OID4VCIMachineInstanceOpts,
OID4VCIMachineInterpreter,
OnContactIdentityCreatedArgs,
OnCredentialStoredArgs,
OnGetCredentialsArgs,
RequestType,
RequiredContext,
StoreCredentialBrandingArgs,
StoreCredentialsArgs,
} from '../types/IOID4VCIHolder'
import {
getCredentialBranding,
getSupportedCredentials,
mapCredentialToAccept,
selectCredentialLocaleBranding,
verifyCredentialToAccept,
} from './OID4VCIHolderService'

/**
* {@inheritDoc IOID4VCIHolder}
Expand Down Expand Up @@ -93,6 +92,9 @@ export class OID4VCIHolder implements IAgentPlugin {
}
}

/**
* FIXME: This method can only be used locally. Creating the interpreter should be local to where the agent is running
*/
private async oid4vciHolderGetMachineInterpreter(args: OID4VCIMachineInstanceOpts, context: RequiredContext): Promise<OID4VCImachineId> {
const services = {
initiateOID4VCI: (args: InitiateOID4VCIArgs) => this.oid4vciHolderGetCredentialOfferData(args, context),
Expand All @@ -113,10 +115,10 @@ export class OID4VCIHolder implements IAgentPlugin {
},
}

const interpreter: OID4VCIMachineInterpreter = await OID4VCIMachine.newInstance(oid4vciMachineInstanceArgs, context)
const { machineStateInit, interpreter } = await OID4VCIMachine.newInstance(oid4vciMachineInstanceArgs, context)

return {
id: interpreter.id,
machineStateInit,
interpreter,
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const verifyCredentialToAccept = async (args: VerifyCredentialToAcceptArg
wrappedVC.decoded?.iss?.includes('did:ebsi:') ||
(typeof wrappedVC.decoded?.vc?.issuer === 'string'
? wrappedVC.decoded?.vc?.issuer?.includes('did:ebsi:')
: wrappedVC.decoded?.vc?.issuer?.instanceId?.includes('did:ebsi:'))
: wrappedVC.decoded?.vc?.issuer?.existingInstanceId?.includes('did:ebsi:'))
) {
// TODO: Skipping VC validation for EBSI conformance issued credential, as their Issuer is not present in the ledger (sigh)
if (JSON.stringify(wrappedVC.decoded).includes('vc:ebsi:conformance')) {
Expand Down
32 changes: 23 additions & 9 deletions packages/oid4vci-holder/src/machine/oid4vciMachine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AuthzFlowType, toAuthorizationResponsePayload } from '@sphereon/oid4vci-common'
import { Identity, Party } from '@sphereon/ssi-sdk.data-store'
import { machineStatePersistRegistration } from '@sphereon/ssi-sdk.xstate-machine-persistence'
import { MachineStateInit, machineStatePersistRegistration } from '@sphereon/ssi-sdk.xstate-machine-persistence'
import { assign, createMachine, DoneInvokeEvent, interpret } from 'xstate'
import { translate } from '../localization/Localization'
import {
Expand Down Expand Up @@ -105,7 +105,7 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach
}

return createMachine<OID4VCIMachineContext, OID4VCIMachineEventTypes>({
id: opts?.machineId ?? 'OID4VCIHolder',
id: opts?.machineName ?? 'OID4VCIHolder',
predictableActionArguments: true,
initial: OID4VCIMachineStates.initiateOID4VCI,
schema: {
Expand Down Expand Up @@ -531,8 +531,13 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach
}

export class OID4VCIMachine {
static async newInstance(opts: OID4VCIMachineInstanceOpts, context: RequiredContext): Promise<OID4VCIMachineInterpreter> {
const instance: OID4VCIMachineInterpreter = interpret(
static async newInstance(
opts: OID4VCIMachineInstanceOpts,
context: RequiredContext
): Promise<{ interpreter: OID4VCIMachineInterpreter; machineStateInit?: MachineStateInit }> {
const { machineName, statePersistence } = opts
const { expireInMS, disablePersistence, expiresAt, customInstanceId, existingInstanceId } = statePersistence ?? {}
const interpreter: OID4VCIMachineInterpreter = interpret(
createOID4VCIMachine(opts).withConfig({
services: {
...opts?.services,
Expand All @@ -552,17 +557,26 @@ export class OID4VCIMachine {
})
)

await machineStatePersistRegistration({ instance, context, machineName: 'OID4VCIHolder' })
const machineStateInit = await machineStatePersistRegistration({
interpreter,
context,
machineName,
expiresAt,
expireInMS,
disablePersistence,
customInstanceId,
existingInstanceId /* TODO:, tenantId*/,
})

if (typeof opts?.subscription === 'function') {
instance.onTransition(opts.subscription)
interpreter.onTransition(opts.subscription)
}
if (opts?.requireCustomNavigationHook !== true) {
instance.onTransition((snapshot: OID4VCIMachineState): void => {
opts?.stateNavigationListener(instance, snapshot)
interpreter.onTransition((snapshot: OID4VCIMachineState): void => {
opts?.stateNavigationListener(interpreter, snapshot)
})
}

return instance
return { interpreter, machineStateInit }
}
}
6 changes: 4 additions & 2 deletions packages/oid4vci-holder/src/types/IOID4VCIHolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AuthorizationResponse, CredentialResponse, CredentialSupported, Endpoin
import { IContactManager } from '@sphereon/ssi-sdk.contact-manager'
import { IBasicCredentialLocaleBranding, IBasicIssuerLocaleBranding, Identity, Party } from '@sphereon/ssi-sdk.data-store'
import { IIssuanceBranding } from '@sphereon/ssi-sdk.issuance-branding'
import { MachineStateInit, MachineStatePersistenceOpts } from '@sphereon/ssi-sdk.xstate-machine-persistence'
import { IVerifiableCredential, WrappedVerifiableCredential, WrappedVerifiablePresentation } from '@sphereon/ssi-types'
import { IAgentContext, ICredentialPlugin, IPluginMethodMap, TKeyType, VerifiableCredential } from '@veramo/core'
import { IDataStore, IDataStoreORM } from '@veramo/data-store'
Expand Down Expand Up @@ -162,9 +163,10 @@ export type OID4VCIStateMachine = StateMachine<

export type CreateOID4VCIMachineOpts = {
requestData: RequestData
machineId?: string
machineName?: string
locale?: string
stateDefinition?: OID4VCIMachineState
statePersistence?: MachineStatePersistenceOpts
}

export type OID4VCIMachineInstanceOpts = {
Expand Down Expand Up @@ -274,7 +276,7 @@ export type CredentialTypeSelection = {
}

export type OID4VCIMachine = {
id: string
machineStateInit?: MachineStateInit
interpreter: OID4VCIMachineInterpreter
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro
})

it('should automatically store xstate state changes', async (): Promise<void> => {
const init = await machineStatePersistRegistration({ context, instance, machineName: instance.machine.id })
const init = await machineStatePersistRegistration({ context, interpreter: instance, machineName: instance.machine.id })
console.log(JSON.stringify(init, null, 2))
if (!init) {
return Promise.reject(new Error('No init'))
Expand Down
35 changes: 26 additions & 9 deletions packages/xstate-persistence/src/agent/MachineStatePersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IAgentPlugin } from '@veramo/core'
import * as console from 'console'
import Debug from 'debug'
import { v4 as uuidv4 } from 'uuid'
import { deserializeMachineState, machineStateToStoreInfo } from '../functions'
import { deserializeMachineState, machineStateToStoreInfo, storeInfoToMachineInit } from '../functions'

import {
DeleteExpiredStatesArgs,
Expand Down Expand Up @@ -91,14 +91,31 @@ export class MachineStatePersistence implements IAgentPlugin {
}

private async machineStateInit(args: InitMachineStateArgs): Promise<MachineStateInit> {
const { tenantId, machineName, expiresAt } = args
debug(`machineStateInit for machine name ${machineName} and tenant ${tenantId}`)
const machineInit: MachineStateInit = {
machineName,
tenantId,
expiresAt,
instanceId: args.instanceId ?? uuidv4(),
createdAt: args.createdAt ?? new Date(),
const { tenantId, machineName, expiresAt, customInstanceId, existingInstanceId } = args
debug(
`machineStateInit for machine name ${machineName}, tenant ${tenantId}, custom instance ${customInstanceId}, existing id ${existingInstanceId}`
)
let machineInit: MachineStateInit | undefined = undefined
if (customInstanceId && existingInstanceId) {
return Promise.reject(new Error(`Cannot have both a custom and existing instance id at the same time`))
} else if (existingInstanceId) {
// An instanceId is provided. First lookup whether this id is persisted, if not an error is thrown
debug(`machineStateInit is using a previously persisted instance id (${existingInstanceId})`)
const state = await this.store.getMachineState({ tenantId, instanceId: existingInstanceId })
machineInit = storeInfoToMachineInit({ ...state, stateType: 'existing' })
} else if (customInstanceId) {
// An custom instanceId is provided.
debug(`machineStateInit is using a custom instance id (${existingInstanceId})`)
}
if (!machineInit) {
machineInit = {
machineName,
tenantId,
expiresAt,
instanceId: customInstanceId ?? uuidv4(),
createdAt: args.createdAt ?? new Date(),
stateType: 'new',
}
}
debug(`machineStateInit result: ${JSON.stringify(machineInit)}`)
return machineInit
Expand Down
34 changes: 22 additions & 12 deletions packages/xstate-persistence/src/functions/machineRegistration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IAgentContext } from '@veramo/core'
import { DefaultContext, EventObject, Interpreter, StateSchema, TypegenDisabled, Typestate } from 'xstate'
import { IMachineStatePersistence, InitMachineStateArgs, MachineStateInit, MachineStatePersistEventType } from '../types'
import { IMachineStatePersistence, InitMachineStateArgs, MachineStateInit, MachineStatePersistenceOpts, MachineStatePersistEventType } from '../types'
import { emitMachineStatePersistEvent } from './stateEventEmitter'

/**
Expand All @@ -12,10 +12,12 @@ import { emitMachineStatePersistEvent } from './stateEventEmitter'
* @returns {Promise<MachineStateInit | undefined>} - A promise that resolves to the initialized machine state, or undefined if the agent isn't using the Xstate plugin.
*/
export const machineStatePersistInit = async (
opts: InitMachineStateArgs & {
context: IAgentContext<any> // We use any as this method could be called from an agent with access to, but not exposing this plugin
}
opts: InitMachineStateArgs &
Pick<MachineStatePersistenceOpts, 'existingInstanceId' | 'customInstanceId'> & {
context: IAgentContext<any> // We use any as this method could be called from an agent with access to, but not exposing this plugin
}
): Promise<MachineStateInit | undefined> => {
// make sure the machine context does not end up in the machine state init args
const { context, ...args } = opts
if (!(context.agent.availableMethods().includes('machineStateInit') && 'machineStateInit' in context.agent)) {
console.log(`IMachineStatePersistence was not exposed in the current agent. Not initializing new persistence object`)
Expand Down Expand Up @@ -44,11 +46,11 @@ export const machineStatePersistOnTransition = async <
},
TResolvedTypesMeta = TypegenDisabled
>(opts: {
instance: Interpreter<TContext, TStateSchema, TEvent, TTypestate, TResolvedTypesMeta>
interpreter: Interpreter<TContext, TStateSchema, TEvent, TTypestate, TResolvedTypesMeta>
context: IAgentContext<any> // We use any as this method could be called from an agent with access to, but not exposing this plugin
init: MachineStateInit
}): Promise<void> => {
const { context, init, instance } = opts
const { context, init, interpreter } = opts
if (!(context.agent.availableMethods().includes('machineStatePersist') && 'machineStatePersist' in context.agent)) {
console.log(`IMachineStatePersistence was not exposed in the current agent. Disabling machine state persistence events`)
return
Expand All @@ -57,7 +59,7 @@ export const machineStatePersistOnTransition = async <
let _eventCounter = 0

// XState persistence plugin is available. So let's emit events on every transition, so it can persist the state
instance.subscribe((state) => {
interpreter.subscribe((state) => {
emitMachineStatePersistEvent(
{
type: MachineStatePersistEventType.EVERY,
Expand Down Expand Up @@ -90,14 +92,22 @@ export const machineStatePersistRegistration = async <
},
TResolvedTypesMeta = TypegenDisabled
>(
args: InitMachineStateArgs &
Partial<Pick<InitMachineStateArgs, 'machineName'>> & {
instance: Interpreter<TContext, TStateSchema, TEvent, TTypestate, TResolvedTypesMeta>
args: Omit<InitMachineStateArgs, 'machineName'> &
Partial<Pick<InitMachineStateArgs, 'machineName'>> &
MachineStatePersistenceOpts & {
interpreter: Interpreter<TContext, TStateSchema, TEvent, TTypestate, TResolvedTypesMeta>
context: IAgentContext<any> // We use any as this method could be called from an agent with access to, but not exposing this plugin
}
): Promise<MachineStateInit | undefined> => {
const machineName = args.machineName ?? args.instance.machine.id ?? args.instance.id
const init = await machineStatePersistInit({ ...args, machineName })
const { disablePersistence } = args
if (disablePersistence === true) {
return
}

// We use expires in MS first. If not provided, look at expires at. If not provided, the persistence will not expire
const expiresAt = args.expireInMS ? new Date(Date.now() + args.expireInMS) : args.expiresAt
const machineName = args.machineName ?? args.interpreter.machine.id ?? args.interpreter.id
const init = await machineStatePersistInit({ ...args, machineName, expiresAt })
if (init) {
await machineStatePersistOnTransition({ ...args, init })
}
Expand Down
13 changes: 12 additions & 1 deletion packages/xstate-persistence/src/functions/stateMapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StoreMachineStateInfo } from '@sphereon/ssi-sdk.data-store'
import { State } from 'xstate'
import { EventObject } from 'xstate/lib/types'
import { MachineStatePersistArgs, SerializableState } from '../types'
import { MachineStateInit, MachineStateInitType, MachineStatePersistArgs, SerializableState } from '../types'

/**
* Create a machine state info object useful for the store, based on the provided machine info and existing state.
Expand Down Expand Up @@ -40,6 +40,17 @@ export const machineStateToStoreInfo = (
completedAt: existing.completedAt ?? (stateInstance.done ? new Date() : undefined),
}
}
export const storeInfoToMachineInit = (machineState: StoreMachineStateInfo & { stateType: MachineStateInitType }): MachineStateInit => {
const { instanceId, machineName, tenantId, expiresAt, createdAt, stateType } = machineState
return {
stateType,
machineName,
tenantId,
expiresAt,
instanceId,
createdAt,
}
}

/**
* Serializes a machine state to a string representation.
Expand Down
Loading

0 comments on commit 3aeb93d

Please sign in to comment.