UNPKG

@sphereon/ssi-sdk.resource-resolver

Version:

201 lines (174 loc) 6.78 kB
import { IKeyValueStore, IValueData, KeyValueStore, ValueStoreType } from '@sphereon/ssi-sdk.kv-store-temp' import { IAgentPlugin } from '@veramo/core' import fetch, { Response, Headers } from 'cross-fetch' import { schema } from '../index' import { deserializeResponse, getResourceIdentifier, isCacheWithinMaxAge, serializeResponse } from '../utils/ResourceResolverUtils' import { ClearAllResourcesArgs, ResolveArgs, GetResourceArgs, IResourceResolver, NamespaceStrArgs, PersistResourceArgs, PrefixArgs, RequiredContext, ResourceResolverOptions, StoreArgs, StoreIdStrArgs, Resource, } from '../types/IResourceResolver' /** * {@inheritDoc IResourceResolver} */ export class ResourceResolver implements IAgentPlugin { readonly schema = schema.IResourceResolver readonly methods: IResourceResolver = { resourceResolve: this.resourceResolve.bind(this), resourceClearAllResources: this.resourceClearAllResources.bind(this), resourceDefaultStoreId: this.resourceDefaultStoreId.bind(this), resourceDefaultNamespace: this.resourceDefaultNamespace.bind(this), resourceDefaultTtl: this.resourceDefaultTtl.bind(this), } private readonly defaultStoreId: string private readonly defaultNamespace: string private readonly defaultTtl: number private readonly detectLocation: boolean private readonly _resourceStores: Map<string, IKeyValueStore<Resource>> constructor(options?: ResourceResolverOptions) { const { defaultStore, defaultNamespace, resourceStores, ttl, detectLocation } = options ?? {} this.defaultStoreId = defaultStore ?? '_default' this.defaultNamespace = defaultNamespace ?? 'resources' this.defaultTtl = ttl ?? 3600 this.detectLocation = detectLocation ?? false if (resourceStores && resourceStores instanceof Map) { this._resourceStores = resourceStores } else if (resourceStores) { this._resourceStores = new Map().set(this.defaultStoreId, resourceStores) } else { this._resourceStores = new Map().set( this.defaultStoreId, new KeyValueStore({ namespace: this.defaultNamespace, store: new Map<string, Resource>(), ttl: this.defaultTtl, }), ) } } /** {@inheritDoc IResourceResolver.resourceResolve} */ private async resourceResolve(args: ResolveArgs, context: RequiredContext): Promise<Response> { const { input, init, resourceType, resolveOpts, partyCorrelationId, storeId, namespace } = args const resourceIdentifier = getResourceIdentifier(input) const cachedResource = await this.getResource({ resourceIdentifier, storeId, namespace }) if (cachedResource.value && isCacheWithinMaxAge(cachedResource.value, resolveOpts)) { return deserializeResponse(cachedResource.value.response) } if (resolveOpts?.onlyCache) { return new Response(JSON.stringify({ error: 'Resource not found' }), { status: 404, statusText: 'Not Found', headers: new Headers({ 'Content-Type': 'application/json' }), }) } let location if (this.detectLocation) { location = await this.retrieveLocation(input, context) } const response = await fetch(input, init) if (!resolveOpts?.skipPersistence && response.status >= 200 && response.status < 300) { const serializedResponse = await serializeResponse(response) const resource: Resource = { location, response: serializedResponse, resourceType, insertedAt: Date.now(), partyCorrelationId, } const cachedResource = await this.persistResource({ resource, resourceIdentifier, namespace, storeId, }) if (!cachedResource.value) { return Promise.reject(Error('Resource not present in persistence result')) } return deserializeResponse(cachedResource.value.response) } return response } private async retrieveLocation(input: RequestInfo | URL, context: RequiredContext) { let url: URL if (input instanceof Request && input.url !== undefined && input.url !== null) { url = new URL(input.url) } else if (input instanceof URL) { url = input } else { throw Error(`input type is required to be RequestInfo | URL`) } return await context.agent.anomalyDetectionLookupLocation({ ipOrHostname: url.hostname, }) } /** {@inheritDoc IResourceResolver.resourceClearAllResources} */ private async resourceClearAllResources(args: ClearAllResourcesArgs, context: RequiredContext): Promise<boolean> { const { storeId } = args return await this.store({ stores: this._resourceStores, storeId }) .clear() .then(() => true) } /** {@inheritDoc IResourceResolver.resourceDefaultStoreId} */ private async resourceDefaultStoreId(context: RequiredContext): Promise<string> { return this.defaultStoreId } /** {@inheritDoc IResourceResolver.resourceDefaultNamespace} */ private async resourceDefaultNamespace(context: RequiredContext): Promise<string> { return this.defaultNamespace } /** {@inheritDoc IResourceResolver.resourceDefaultTtl} */ private async resourceDefaultTtl(context: RequiredContext): Promise<number> { return this.defaultTtl } private async getResource(args: GetResourceArgs): Promise<IValueData<Resource>> { const { resourceIdentifier, storeId, namespace } = args return this.store({ stores: this._resourceStores, storeId }).getAsValueData( this.prefix({ namespace, resourceIdentifier, }), ) } private async persistResource(args: PersistResourceArgs): Promise<IValueData<Resource>> { const { resource, resourceIdentifier, ttl } = args const namespace = this.namespaceStr(args) const storeId = this.storeIdStr(args) return await this.store({ stores: this._resourceStores, storeId }).set( this.prefix({ namespace, resourceIdentifier, }), resource, ttl ?? this.defaultTtl, ) } private store<T extends ValueStoreType>(args: StoreArgs<T>): IKeyValueStore<T> { const storeId = this.storeIdStr({ storeId: args.storeId }) const store = args.stores.get(storeId) if (!store) { throw Error(`Could not get resource store: ${storeId}`) } return store } private storeIdStr(args: StoreIdStrArgs): string { const { storeId } = args return storeId ?? this.defaultStoreId } private namespaceStr(args: NamespaceStrArgs): string { const { namespace } = args return namespace ?? this.defaultNamespace } private prefix(args: PrefixArgs): string { const { namespace, resourceIdentifier } = args return `${this.namespaceStr({ namespace })}:${resourceIdentifier}` } }