diff --git a/packages/data-store/src/types/machineState/IAbstractMachineStateStore.ts b/packages/data-store/src/types/machineState/IAbstractMachineStateStore.ts index 5678ff233..ba97c3570 100644 --- a/packages/data-store/src/types/machineState/IAbstractMachineStateStore.ts +++ b/packages/data-store/src/types/machineState/IAbstractMachineStateStore.ts @@ -8,7 +8,7 @@ export type StoreFindMachineStatesArgs = { filter: FindMachineStatesFilterArgs } -export type StoreMachineStateGetArgs = Pick +export type StoreMachineStateGetArgs = Pick export type StoreMachineStateDeleteArgs = StoreMachineStateGetArgs export type StoreMachineStateDeleteExpiredArgs = { machineName?: string; tenantId?: string; deleteDoneStates?: boolean } diff --git a/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts b/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts index 058adc258..fc519e8fc 100644 --- a/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts +++ b/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts @@ -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, @@ -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} @@ -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 { const services = { initiateOID4VCI: (args: InitiateOID4VCIArgs) => this.oid4vciHolderGetCredentialOfferData(args, context), @@ -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, } } diff --git a/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts b/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts index 571fd18da..2471f9fec 100644 --- a/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts +++ b/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts @@ -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')) { diff --git a/packages/oid4vci-holder/src/machine/oid4vciMachine.ts b/packages/oid4vci-holder/src/machine/oid4vciMachine.ts index 713ac1adb..757bcbccb 100644 --- a/packages/oid4vci-holder/src/machine/oid4vciMachine.ts +++ b/packages/oid4vci-holder/src/machine/oid4vciMachine.ts @@ -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 { @@ -105,7 +105,7 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach } return createMachine({ - id: opts?.machineId ?? 'OID4VCIHolder', + id: opts?.machineName ?? 'OID4VCIHolder', predictableActionArguments: true, initial: OID4VCIMachineStates.initiateOID4VCI, schema: { @@ -531,8 +531,13 @@ const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMach } export class OID4VCIMachine { - static async newInstance(opts: OID4VCIMachineInstanceOpts, context: RequiredContext): Promise { - 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, @@ -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 } } } diff --git a/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts b/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts index 727185d56..4326bf53b 100644 --- a/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts +++ b/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts @@ -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' @@ -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 = { @@ -274,7 +276,7 @@ export type CredentialTypeSelection = { } export type OID4VCIMachine = { - id: string + machineStateInit?: MachineStateInit interpreter: OID4VCIMachineInterpreter } diff --git a/packages/xstate-persistence/src/__tests__/shared/MachineStatePersistenceAgentLogic.ts b/packages/xstate-persistence/src/__tests__/shared/MachineStatePersistenceAgentLogic.ts index e15b671b7..1f7259a4b 100644 --- a/packages/xstate-persistence/src/__tests__/shared/MachineStatePersistenceAgentLogic.ts +++ b/packages/xstate-persistence/src/__tests__/shared/MachineStatePersistenceAgentLogic.ts @@ -133,7 +133,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro }) it('should automatically store xstate state changes', async (): Promise => { - 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')) diff --git a/packages/xstate-persistence/src/agent/MachineStatePersistence.ts b/packages/xstate-persistence/src/agent/MachineStatePersistence.ts index a08a53726..78760d170 100644 --- a/packages/xstate-persistence/src/agent/MachineStatePersistence.ts +++ b/packages/xstate-persistence/src/agent/MachineStatePersistence.ts @@ -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, @@ -91,14 +91,31 @@ export class MachineStatePersistence implements IAgentPlugin { } private async machineStateInit(args: InitMachineStateArgs): Promise { - 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 diff --git a/packages/xstate-persistence/src/functions/machineRegistration.ts b/packages/xstate-persistence/src/functions/machineRegistration.ts index ef12002e0..81585eb71 100644 --- a/packages/xstate-persistence/src/functions/machineRegistration.ts +++ b/packages/xstate-persistence/src/functions/machineRegistration.ts @@ -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' /** @@ -12,10 +12,12 @@ import { emitMachineStatePersistEvent } from './stateEventEmitter' * @returns {Promise} - 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 // We use any as this method could be called from an agent with access to, but not exposing this plugin - } + opts: InitMachineStateArgs & + Pick & { + context: IAgentContext // We use any as this method could be called from an agent with access to, but not exposing this plugin + } ): Promise => { + // 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`) @@ -44,11 +46,11 @@ export const machineStatePersistOnTransition = async < }, TResolvedTypesMeta = TypegenDisabled >(opts: { - instance: Interpreter + interpreter: Interpreter context: IAgentContext // We use any as this method could be called from an agent with access to, but not exposing this plugin init: MachineStateInit }): Promise => { - 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 @@ -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, @@ -90,14 +92,22 @@ export const machineStatePersistRegistration = async < }, TResolvedTypesMeta = TypegenDisabled >( - args: InitMachineStateArgs & - Partial> & { - instance: Interpreter + args: Omit & + Partial> & + MachineStatePersistenceOpts & { + interpreter: Interpreter context: IAgentContext // We use any as this method could be called from an agent with access to, but not exposing this plugin } ): Promise => { - 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 }) } diff --git a/packages/xstate-persistence/src/functions/stateMapper.ts b/packages/xstate-persistence/src/functions/stateMapper.ts index 8fe65c0c8..4aabe1acd 100644 --- a/packages/xstate-persistence/src/functions/stateMapper.ts +++ b/packages/xstate-persistence/src/functions/stateMapper.ts @@ -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. @@ -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. diff --git a/packages/xstate-persistence/src/types/types.ts b/packages/xstate-persistence/src/types/types.ts index 609a73518..0d85bbf19 100644 --- a/packages/xstate-persistence/src/types/types.ts +++ b/packages/xstate-persistence/src/types/types.ts @@ -77,17 +77,29 @@ export type MachineStateInfo = Omit & { state: SerializableState } +export type MachineStatePersistenceOpts = { + disablePersistence?: boolean // Disable persistence altogether + customInstanceId?: string // Used when creating a machine using a custom new instance id + existingInstanceId?: string // Used when creating a machine using an existing instance (rehydrating a persisted machine) + expireInMS?: number // Expire in an amount of MS (takes precedence over date if both are provided!) + expiresAt?: Date // Expire at a specific date +} + +export type MachineStateInitType = 'new' | 'existing' + /** * Represents the initial state for a machine. * * @typedef {Object} MachineStateInit - * @property {string} instanceId - The unique identifier for the machine instance. + * @property {string} existingInstanceId - The unique identifier for the machine instance. * @property {string} machineName - The name of the machine. * @property {string} tenantId - The identifier for the tenant associated with the machine. * @property {Date} createdAt - The date and time when the machine was created. * @property {Date} expiresAt - The date and time when the machine's state expires. */ -export type MachineStateInit = Pick +export type MachineStateInit = Pick & { + stateType: MachineStateInitType +} /** * Represents the arguments required to initialize the machine state. @@ -95,7 +107,9 @@ export type MachineStateInit = Pick} [additionalArgs] - Additional initialization arguments for the machine state. */ -export type InitMachineStateArgs = Partial & Pick +export type InitMachineStateArgs = Omit, 'instanceId'> & + Pick & + Pick /** * Represents the arguments required to persist the machine state. @@ -107,7 +121,7 @@ export type MachineStatePersistArgs = Omit & /** * Represents the arguments required to get machine state. * @typedef {Object} MachineStateGetArgs - * @property {string} instanceId - The ID of the machine instance. + * @property {string} existingInstanceId - The ID of the machine instance. * @property {string} tenantId - The ID of the tenant the machine belongs to. */ export type MachineStateGetArgs = Pick @@ -116,7 +130,7 @@ export type MachineStateGetArgs = Pick