@credo-ts/tenants 0.6.1-pr-2091-20241119140918 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/TenantAgent.d.mts +12 -0
- package/build/TenantAgent.d.mts.map +1 -0
- package/build/TenantAgent.mjs +23 -0
- package/build/TenantAgent.mjs.map +1 -0
- package/build/TenantsApi.d.mts +37 -0
- package/build/TenantsApi.d.mts.map +1 -0
- package/build/TenantsApi.mjs +107 -0
- package/build/TenantsApi.mjs.map +1 -0
- package/build/TenantsApiOptions.d.mts +19 -0
- package/build/TenantsApiOptions.d.mts.map +1 -0
- package/build/TenantsModule.d.mts +23 -0
- package/build/TenantsModule.d.mts.map +1 -0
- package/build/TenantsModule.mjs +40 -0
- package/build/TenantsModule.mjs.map +1 -0
- package/build/TenantsModuleConfig.d.mts +35 -0
- package/build/TenantsModuleConfig.d.mts.map +1 -0
- package/build/TenantsModuleConfig.mjs +18 -0
- package/build/TenantsModuleConfig.mjs.map +1 -0
- package/build/_virtual/_@oxc-project_runtime@0.99.0/helpers/decorate.mjs +10 -0
- package/build/_virtual/_@oxc-project_runtime@0.99.0/helpers/decorateMetadata.mjs +7 -0
- package/build/_virtual/_@oxc-project_runtime@0.99.0/helpers/decorateParam.mjs +9 -0
- package/build/context/TenantAgentContextProvider.d.mts +52 -0
- package/build/context/TenantAgentContextProvider.d.mts.map +1 -0
- package/build/context/TenantAgentContextProvider.mjs +149 -0
- package/build/context/TenantAgentContextProvider.mjs.map +1 -0
- package/build/context/TenantSessionCoordinator.d.mts +71 -0
- package/build/context/TenantSessionCoordinator.d.mts.map +1 -0
- package/build/context/TenantSessionCoordinator.mjs +186 -0
- package/build/context/TenantSessionCoordinator.mjs.map +1 -0
- package/build/context/TenantSessionMutex.mjs +66 -0
- package/build/context/TenantSessionMutex.mjs.map +1 -0
- package/build/index.d.mts +10 -0
- package/build/index.mjs +9 -0
- package/build/models/TenantConfig.d.mts +7 -0
- package/build/models/TenantConfig.d.mts.map +1 -0
- package/build/repository/TenantRecord.d.mts +36 -0
- package/build/repository/TenantRecord.d.mts.map +1 -0
- package/build/repository/TenantRecord.mjs +29 -0
- package/build/repository/TenantRecord.mjs.map +1 -0
- package/build/repository/TenantRepository.d.mts +11 -0
- package/build/repository/TenantRepository.d.mts.map +1 -0
- package/build/repository/TenantRepository.mjs +25 -0
- package/build/repository/TenantRepository.mjs.map +1 -0
- package/build/repository/TenantRoutingRecord.d.mts +28 -0
- package/build/repository/TenantRoutingRecord.d.mts.map +1 -0
- package/build/repository/TenantRoutingRecord.mjs +28 -0
- package/build/repository/TenantRoutingRecord.mjs.map +1 -0
- package/build/repository/TenantRoutingRepository.d.mts +11 -0
- package/build/repository/TenantRoutingRepository.d.mts.map +1 -0
- package/build/repository/TenantRoutingRepository.mjs +25 -0
- package/build/repository/TenantRoutingRepository.mjs.map +1 -0
- package/build/repository/index.d.mts +4 -0
- package/build/repository/index.mjs +4 -0
- package/build/services/TenantRecordService.d.mts +26 -0
- package/build/services/TenantRecordService.d.mts.map +1 -0
- package/build/services/TenantRecordService.mjs +63 -0
- package/build/services/TenantRecordService.mjs.map +1 -0
- package/build/services/index.d.mts +1 -0
- package/build/services/index.mjs +1 -0
- package/build/updates/0.4-0.5/index.d.mts +7 -0
- package/build/updates/0.4-0.5/index.d.mts.map +1 -0
- package/build/updates/0.4-0.5/index.mjs +10 -0
- package/build/updates/0.4-0.5/index.mjs.map +1 -0
- package/build/updates/0.4-0.5/tenantRecord.mjs +28 -0
- package/build/updates/0.4-0.5/tenantRecord.mjs.map +1 -0
- package/package.json +15 -14
- package/build/TenantAgent.d.ts +0 -8
- package/build/TenantAgent.js +0 -25
- package/build/TenantAgent.js.map +0 -1
- package/build/TenantsApi.d.ts +0 -25
- package/build/TenantsApi.js +0 -117
- package/build/TenantsApi.js.map +0 -1
- package/build/TenantsApiOptions.d.ts +0 -14
- package/build/TenantsApiOptions.js +0 -3
- package/build/TenantsApiOptions.js.map +0 -1
- package/build/TenantsModule.d.ts +0 -19
- package/build/TenantsModule.js +0 -47
- package/build/TenantsModule.js.map +0 -1
- package/build/TenantsModuleConfig.d.ts +0 -31
- package/build/TenantsModuleConfig.js +0 -20
- package/build/TenantsModuleConfig.js.map +0 -1
- package/build/context/TenantAgentContextProvider.d.ts +0 -40
- package/build/context/TenantAgentContextProvider.js +0 -175
- package/build/context/TenantAgentContextProvider.js.map +0 -1
- package/build/context/TenantSessionCoordinator.d.ts +0 -54
- package/build/context/TenantSessionCoordinator.js +0 -195
- package/build/context/TenantSessionCoordinator.js.map +0 -1
- package/build/context/TenantSessionMutex.d.ts +0 -29
- package/build/context/TenantSessionMutex.js +0 -84
- package/build/context/TenantSessionMutex.js.map +0 -1
- package/build/context/types.d.ts +0 -0
- package/build/context/types.js +0 -2
- package/build/context/types.js.map +0 -1
- package/build/index.d.ts +0 -5
- package/build/index.js +0 -24
- package/build/index.js.map +0 -1
- package/build/models/TenantConfig.d.ts +0 -4
- package/build/models/TenantConfig.js +0 -3
- package/build/models/TenantConfig.js.map +0 -1
- package/build/repository/TenantRecord.d.ts +0 -33
- package/build/repository/TenantRecord.js +0 -32
- package/build/repository/TenantRecord.js.map +0 -1
- package/build/repository/TenantRepository.d.ts +0 -7
- package/build/repository/TenantRepository.js +0 -32
- package/build/repository/TenantRepository.js.map +0 -1
- package/build/repository/TenantRoutingRecord.d.ts +0 -26
- package/build/repository/TenantRoutingRecord.js +0 -24
- package/build/repository/TenantRoutingRecord.js.map +0 -1
- package/build/repository/TenantRoutingRepository.d.ts +0 -7
- package/build/repository/TenantRoutingRepository.js +0 -34
- package/build/repository/TenantRoutingRepository.js.map +0 -1
- package/build/repository/index.d.ts +0 -4
- package/build/repository/index.js +0 -21
- package/build/repository/index.js.map +0 -1
- package/build/services/TenantRecordService.d.ts +0 -17
- package/build/services/TenantRecordService.js +0 -78
- package/build/services/TenantRecordService.js.map +0 -1
- package/build/services/index.d.ts +0 -1
- package/build/services/index.js +0 -18
- package/build/services/index.js.map +0 -1
- package/build/updates/0.4-0.5/index.d.ts +0 -2
- package/build/updates/0.4-0.5/index.js +0 -8
- package/build/updates/0.4-0.5/index.js.map +0 -1
- package/build/updates/0.4-0.5/tenantRecord.d.ts +0 -10
- package/build/updates/0.4-0.5/tenantRecord.js +0 -26
- package/build/updates/0.4-0.5/tenantRecord.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantAgentContextProvider.mjs","names":["TenantAgentContextProvider","logger: Logger","agentContext","recipientKeys: Kms.PublicJwk[]"],"sources":["../../src/context/TenantAgentContextProvider.ts"],"sourcesContent":["import {\n AgentContext,\n type AgentContextProvider,\n CredoError,\n EventEmitter,\n InjectionSymbols,\n inject,\n injectable,\n isJsonObject,\n isStorageUpToDate,\n JsonEncoder,\n Kms,\n type Logger,\n TypedArrayEncoder,\n UpdateAssistant,\n type UpdateAssistantUpdateOptions,\n} from '@credo-ts/core'\nimport type { DidCommEncryptedMessage, DidCommRoutingCreatedEvent } from '@credo-ts/didcomm'\nimport { DidCommRoutingEventTypes, isValidJweStructure } from '@credo-ts/didcomm'\nimport type { TenantRecord } from '../repository'\nimport { TenantRecordService } from '../services'\nimport { TenantAgent } from '../TenantAgent'\n\nimport { TenantSessionCoordinator } from './TenantSessionCoordinator'\n\n@injectable()\nexport class TenantAgentContextProvider implements AgentContextProvider {\n private tenantRecordService: TenantRecordService\n private rootAgentContext: AgentContext\n private eventEmitter: EventEmitter\n private logger: Logger\n private tenantSessionCoordinator: TenantSessionCoordinator\n\n public constructor(\n tenantRecordService: TenantRecordService,\n rootAgentContext: AgentContext,\n eventEmitter: EventEmitter,\n tenantSessionCoordinator: TenantSessionCoordinator,\n @inject(InjectionSymbols.Logger) logger: Logger\n ) {\n this.tenantRecordService = tenantRecordService\n this.rootAgentContext = rootAgentContext\n this.eventEmitter = eventEmitter\n this.tenantSessionCoordinator = tenantSessionCoordinator\n this.logger = logger\n\n // Start listener for newly created routing keys, so we can register a mapping for each new key for the tenant\n this.listenForRoutingKeyCreatedEvents()\n }\n\n public getContextCorrelationIdForTenantId(tenantId: string) {\n return this.tenantSessionCoordinator.getContextCorrelationIdForTenantId(tenantId)\n }\n\n public async getAgentContextForContextCorrelationId(\n contextCorrelationId: string,\n { provisionContext = false }: { provisionContext?: boolean } = {}\n ) {\n // It could be that the root agent context is requested, in that case we return the root agent context\n if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) {\n return this.rootAgentContext\n }\n\n // If not the root agent context, we require it to be a tenant context correlation id\n this.tenantSessionCoordinator.assertTenantContextCorrelationId(contextCorrelationId)\n const tenantId = this.tenantSessionCoordinator.getTenantIdForContextCorrelationId(contextCorrelationId)\n\n // TODO: maybe we can look at not having to retrieve the tenant record if there's already a context available.\n const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId)\n const shouldUpdate = !isStorageUpToDate(tenantRecord.storageVersion)\n\n // If the tenant storage is not up to date, and autoUpdate is disabled we throw an error\n if (shouldUpdate && !this.rootAgentContext.config.autoUpdateStorageOnStartup) {\n throw new CredoError(\n `Current agent storage for tenant ${tenantRecord.id} is not up to date. To prevent the tenant state from getting corrupted the tenant initialization is aborted. Make sure to update the tenant storage (currently at ${tenantRecord.storageVersion}) to the latest version (${UpdateAssistant.frameworkStorageVersion}). You can also downgrade your version of Credo.`\n )\n }\n\n const agentContext = await this.tenantSessionCoordinator.getContextForSession(tenantRecord, {\n provisionContext,\n runInMutex: shouldUpdate ? (agentContext) => this._updateTenantStorage(tenantRecord, agentContext) : undefined,\n })\n\n this.logger.debug(`Created tenant agent context for context correlation id '${contextCorrelationId}'`)\n\n return agentContext\n }\n\n public async getContextForInboundMessage(inboundMessage: unknown, options?: { contextCorrelationId?: string }) {\n this.logger.debug('Getting context for inbound message in tenant agent context provider', {\n contextCorrelationId: options?.contextCorrelationId,\n })\n\n // TODO: what if context is for root agent context?\n let tenantId =\n options?.contextCorrelationId &&\n this.tenantSessionCoordinator.isTenantContextCorrelationId(options.contextCorrelationId)\n ? this.tenantSessionCoordinator.getTenantIdForContextCorrelationId(options.contextCorrelationId)\n : undefined\n let recipientKeys: Kms.PublicJwk[] = []\n\n if (!tenantId && isValidJweStructure(inboundMessage)) {\n this.logger.trace(\"Inbound message is a JWE, extracting tenant id from JWE's protected header\")\n recipientKeys = this.getRecipientKeysFromEncryptedMessage(inboundMessage)\n\n this.logger.trace(`Found ${recipientKeys.length} recipient keys in JWE's protected header`)\n\n // FIXME: what if there are multiple recipients in the same agent? If we receive the messages twice we will process it for\n // the first found recipient multiple times. This is however a case I've never seen before and will add quite some complexity\n // to resolve. I think we're fine to ignore this case for now.\n for (const recipientKey of recipientKeys) {\n const tenantRoutingRecord = await this.tenantRecordService.findTenantRoutingRecordByRecipientKey(\n this.rootAgentContext,\n recipientKey\n )\n\n if (tenantRoutingRecord) {\n this.logger.debug(`Found tenant routing record for recipient key ${recipientKeys[0].fingerprint}`, {\n tenantId: tenantRoutingRecord.tenantId,\n })\n tenantId = tenantRoutingRecord.tenantId\n break\n }\n }\n }\n\n if (!tenantId) {\n this.logger.error(\"Couldn't determine tenant id for inbound message. Unable to create context\", {\n inboundMessage,\n recipientKeys: recipientKeys.map((key) => key.fingerprint),\n })\n throw new CredoError(\"Couldn't determine tenant id for inbound message. Unable to create context\")\n }\n\n const contextCorrelationId = this.tenantSessionCoordinator.getContextCorrelationIdForTenantId(tenantId)\n const agentContext = await this.getAgentContextForContextCorrelationId(contextCorrelationId)\n\n return agentContext\n }\n\n public async endSessionForAgentContext(agentContext: AgentContext) {\n await this.tenantSessionCoordinator.endAgentContextSession(agentContext)\n }\n\n public async deleteAgentContext(agentContext: AgentContext): Promise<void> {\n await this.tenantSessionCoordinator.deleteAgentContext(agentContext)\n }\n\n private getRecipientKeysFromEncryptedMessage(jwe: DidCommEncryptedMessage): Kms.PublicJwk[] {\n const jweProtected = JsonEncoder.fromBase64(jwe.protected)\n if (!Array.isArray(jweProtected.recipients)) return []\n\n const recipientKeys: Kms.PublicJwk[] = []\n\n for (const recipient of jweProtected.recipients) {\n // Check if recipient.header.kid is a string\n if (isJsonObject(recipient) && isJsonObject(recipient.header) && typeof recipient.header.kid === 'string') {\n // This won't work with other key types, we should detect what the encoding is of kid, and based on that\n // determine how we extract the key from the message\n const publicJwk = Kms.PublicJwk.fromPublicKey({\n crv: 'Ed25519',\n kty: 'OKP',\n publicKey: TypedArrayEncoder.fromBase58(recipient.header.kid),\n })\n\n recipientKeys.push(publicJwk)\n }\n }\n\n return recipientKeys\n }\n\n private async registerRecipientKeyForTenant(tenantId: string, recipientKey: Kms.PublicJwk) {\n this.logger.debug(`Registering recipient key ${recipientKey.fingerprint} for tenant ${tenantId}`)\n const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId)\n await this.tenantRecordService.addTenantRoutingRecord(this.rootAgentContext, tenantRecord.id, recipientKey)\n }\n\n private listenForRoutingKeyCreatedEvents() {\n this.logger.debug('Listening for routing key created events in tenant agent context provider')\n this.eventEmitter.on<DidCommRoutingCreatedEvent>(DidCommRoutingEventTypes.RoutingCreatedEvent, async (event) => {\n const contextCorrelationId = event.metadata.contextCorrelationId\n const recipientKey = event.payload.routing.recipientKey\n\n // We don't want to register the key if it's for the root agent context\n if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) return\n\n this.tenantSessionCoordinator.assertTenantContextCorrelationId(contextCorrelationId)\n\n this.logger.debug(\n `Received routing key created event for tenant context ${contextCorrelationId}, registering recipient key ${recipientKey.fingerprint} in base wallet`\n )\n await this.registerRecipientKeyForTenant(\n this.tenantSessionCoordinator.getTenantIdForContextCorrelationId(contextCorrelationId),\n recipientKey\n )\n })\n }\n\n /**\n * Method to allow updating the tenant storage, this method can be called from the TenantsApi\n * to update the storage for a tenant manually\n */\n public async updateTenantStorage(tenantRecord: TenantRecord, updateOptions?: UpdateAssistantUpdateOptions) {\n const agentContext = await this.tenantSessionCoordinator.getContextForSession(tenantRecord, {\n // runInMutex allows us to run the updateTenantStorage method in a mutex lock\n // prevent other sessions from being started while the update is in progress\n runInMutex: (agentContext) => this._updateTenantStorage(tenantRecord, agentContext, updateOptions),\n })\n\n // End sesion afterwards\n await agentContext.endSession()\n }\n\n /**\n * Handle the case where the tenant storage is outdated. If auto-update is disabled we will throw an error\n * and not update the storage. If auto-update is enabled we will update the storage.\n *\n * When this method is called we can be sure that we are in the mutex runExclusive lock and thus other sessions\n * will not be able to open a session for this tenant until we're done.\n *\n * NOTE: We don't support multi-instance locking for now. That means you can only have a single instance open and\n * it will prevent multiple processes from updating the tenant storage at the same time. However if multi-instances\n * are used, we can't prevent multiple instances from updating the tenant storage at the same time.\n * In the future we can make the tenantSessionCoordinator an interface and allowing a instance-tenant-lock as well\n * as an tenant-lock (across all instances)\n */\n private async _updateTenantStorage(\n tenantRecord: TenantRecord,\n agentContext: AgentContext,\n updateOptions?: UpdateAssistantUpdateOptions\n ) {\n try {\n // Update the tenant storage\n const tenantAgent = new TenantAgent(agentContext)\n const updateAssistant = new UpdateAssistant(tenantAgent)\n await updateAssistant.initialize()\n await updateAssistant.update(updateOptions)\n\n // Update the storage version in the tenant record\n tenantRecord.storageVersion = await updateAssistant.getCurrentAgentStorageVersion()\n const tenantRecordService = this.rootAgentContext.dependencyManager.resolve(TenantRecordService)\n await tenantRecordService.updateTenant(this.rootAgentContext, tenantRecord)\n } catch (error) {\n this.logger.error(`Error occurred while updating tenant storage for tenant ${tenantRecord.id}`, error)\n throw error\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TenantRecord } from "../repository/TenantRecord.mjs";
|
|
2
|
+
import "../repository/index.mjs";
|
|
3
|
+
import { TenantsModuleConfig } from "../TenantsModuleConfig.mjs";
|
|
4
|
+
import { AgentContext, Logger } from "@credo-ts/core";
|
|
5
|
+
import "async-mutex";
|
|
6
|
+
|
|
7
|
+
//#region src/context/TenantSessionCoordinator.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Coordinates all agent context instance for tenant sessions.
|
|
11
|
+
*
|
|
12
|
+
* This class keeps a mapping of tenant ids (context correlation ids) to agent context sessions mapping. Each mapping contains the agent context,
|
|
13
|
+
* the current session count and a mutex for making operations against the session mapping (opening / closing an agent context). The mutex ensures
|
|
14
|
+
* we're not susceptible to race conditions where multiple calls to open/close an agent context are made at the same time. Even though JavaScript is
|
|
15
|
+
* single threaded, promises can introduce race conditions as one process can stop and another process can be picked up.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: the implementation doesn't yet cache agent context objects after they aren't being used for any sessions anymore. This means if a wallet is being used
|
|
18
|
+
* often in a short time it will be opened/closed very often. This is an improvement to be made in the near future.
|
|
19
|
+
*/
|
|
20
|
+
declare class TenantSessionCoordinator {
|
|
21
|
+
private rootAgentContext;
|
|
22
|
+
private logger;
|
|
23
|
+
private tenantAgentContextMapping;
|
|
24
|
+
private sessionMutex;
|
|
25
|
+
private tenantsModuleConfig;
|
|
26
|
+
constructor(rootAgentContext: AgentContext, logger: Logger, tenantsModuleConfig: TenantsModuleConfig);
|
|
27
|
+
getSessionCountForTenant(tenantId: string): number;
|
|
28
|
+
/**
|
|
29
|
+
* Get agent context to use for a session. If an agent context for this tenant does not exist yet
|
|
30
|
+
* it will create it and store it for later use. If the agent context does already exist it will
|
|
31
|
+
* be returned.
|
|
32
|
+
*
|
|
33
|
+
* @parm tenantRecord The tenant record for which to get the agent context
|
|
34
|
+
*/
|
|
35
|
+
getContextForSession(tenantRecord: TenantRecord, {
|
|
36
|
+
runInMutex,
|
|
37
|
+
provisionContext
|
|
38
|
+
}?: {
|
|
39
|
+
/** optional callback that will be run inside the mutex lock */
|
|
40
|
+
runInMutex?: (agentContext: AgentContext) => Promise<void>;
|
|
41
|
+
provisionContext?: boolean;
|
|
42
|
+
}): Promise<AgentContext>;
|
|
43
|
+
/**
|
|
44
|
+
* End a session for the provided agent context. It will decrease the session count for the agent context.
|
|
45
|
+
* If the number of sessions is zero after the context for this session has been ended, the agent context will be closed.
|
|
46
|
+
*/
|
|
47
|
+
endAgentContextSession(agentContext: AgentContext): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Delete the provided agent context. All opens sessions will be disposed and not usable anymore
|
|
50
|
+
*/
|
|
51
|
+
deleteAgentContext(agentContext: AgentContext): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* The context correlation id for a tenant is the tenant id prefixed with tenant-
|
|
54
|
+
*/
|
|
55
|
+
getContextCorrelationIdForTenantId(tenantId: string): TenantContextCorrelationId;
|
|
56
|
+
/**
|
|
57
|
+
* The context correlation id for a tenant is the tenant id prefixed with tenant-
|
|
58
|
+
*/
|
|
59
|
+
getTenantIdForContextCorrelationId(contextCorrelationId: TenantContextCorrelationId): string;
|
|
60
|
+
isTenantContextCorrelationId(contextCorrelationId: string): contextCorrelationId is TenantContextCorrelationId;
|
|
61
|
+
assertTenantContextCorrelationId(contextCorrelationId: string): asserts contextCorrelationId is TenantContextCorrelationId;
|
|
62
|
+
private hasTenantSessionMapping;
|
|
63
|
+
private getTenantSessionsMapping;
|
|
64
|
+
private mutexForTenant;
|
|
65
|
+
private createAgentContext;
|
|
66
|
+
private closeAgentContext;
|
|
67
|
+
}
|
|
68
|
+
type TenantContextCorrelationId = `tenant-${string}`;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { TenantSessionCoordinator };
|
|
71
|
+
//# sourceMappingURL=TenantSessionCoordinator.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantSessionCoordinator.d.mts","names":[],"sources":["../../src/context/TenantSessionCoordinator.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AA4BA;;;;;;;;AA4CmD,cA3CtC,wBAAA,CA2CsC;EAGtC,QAAA,gBAAA;EAAR,QAAA,MAAA;EAkE+C,QAAA,yBAAA;EAAe,QAAA,YAAA;EAmDnB,QAAA,mBAAA;EAAe,WAAA,CAAA,gBAAA,EA3JzC,YA2JyC,EAAA,MAAA,EA1JlB,MA0JkB,EAAA,mBAAA,EAzJtC,mBAyJsC;EAmDA,wBAAA,CAAA,QAAA,EAAA,MAAA,CAAA,EAAA,MAAA;EAWG;;;;AA0FlE;;;qCAtRkB;;;;;gCAMgB,iBAAiB;;MAG9C,QAAQ;;;;;uCAkEuC,eAAe;;;;mCAmDnB,eAAe;;;;wDAmDA;;;;2DAWG;sFAYrC;kGAMQ;;;;;;;KAwEzB,0BAAA"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { __decorateMetadata } from "../_virtual/_@oxc-project_runtime@0.99.0/helpers/decorateMetadata.mjs";
|
|
2
|
+
import { __decorateParam } from "../_virtual/_@oxc-project_runtime@0.99.0/helpers/decorateParam.mjs";
|
|
3
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.99.0/helpers/decorate.mjs";
|
|
4
|
+
import { TenantsModuleConfig } from "../TenantsModuleConfig.mjs";
|
|
5
|
+
import { TenantSessionMutex } from "./TenantSessionMutex.mjs";
|
|
6
|
+
import { AgentConfig, AgentContext, CredoError, InjectionSymbols, inject, injectable } from "@credo-ts/core";
|
|
7
|
+
import { Mutex, withTimeout } from "async-mutex";
|
|
8
|
+
|
|
9
|
+
//#region src/context/TenantSessionCoordinator.ts
|
|
10
|
+
var _ref, _ref2;
|
|
11
|
+
let TenantSessionCoordinator = class TenantSessionCoordinator$1 {
|
|
12
|
+
constructor(rootAgentContext, logger, tenantsModuleConfig) {
|
|
13
|
+
this.tenantAgentContextMapping = {};
|
|
14
|
+
this.rootAgentContext = rootAgentContext;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.tenantsModuleConfig = tenantsModuleConfig;
|
|
17
|
+
this.sessionMutex = new TenantSessionMutex(this.logger, this.tenantsModuleConfig.sessionLimit, this.tenantsModuleConfig.sessionAcquireTimeout);
|
|
18
|
+
}
|
|
19
|
+
getSessionCountForTenant(tenantId) {
|
|
20
|
+
const contextCorrelationId = this.getContextCorrelationIdForTenantId(tenantId);
|
|
21
|
+
return this.tenantAgentContextMapping[contextCorrelationId]?.sessionCount ?? 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get agent context to use for a session. If an agent context for this tenant does not exist yet
|
|
25
|
+
* it will create it and store it for later use. If the agent context does already exist it will
|
|
26
|
+
* be returned.
|
|
27
|
+
*
|
|
28
|
+
* @parm tenantRecord The tenant record for which to get the agent context
|
|
29
|
+
*/
|
|
30
|
+
async getContextForSession(tenantRecord, { runInMutex, provisionContext = false } = {}) {
|
|
31
|
+
this.logger.debug(`Getting context for session with tenant '${tenantRecord.id}'`);
|
|
32
|
+
await this.sessionMutex.acquireSession();
|
|
33
|
+
try {
|
|
34
|
+
const contextCorrelationId = this.getContextCorrelationIdForTenantId(tenantRecord.id);
|
|
35
|
+
return await this.mutexForTenant(contextCorrelationId).runExclusive(async () => {
|
|
36
|
+
this.logger.debug(`Acquired lock for tenant '${tenantRecord.id}' to get context`);
|
|
37
|
+
const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId);
|
|
38
|
+
if (!tenantSessions.agentContext) {
|
|
39
|
+
this.logger.debug(`No agent context has been initialized for tenant '${tenantRecord.id}', creating one`);
|
|
40
|
+
tenantSessions.agentContext = await this.createAgentContext(tenantRecord, { provisionContext });
|
|
41
|
+
}
|
|
42
|
+
tenantSessions.sessionCount++;
|
|
43
|
+
this.logger.debug(`Increased agent context session count for tenant '${tenantRecord.id}' to ${tenantSessions.sessionCount}`);
|
|
44
|
+
if (runInMutex) try {
|
|
45
|
+
await runInMutex(tenantSessions.agentContext);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
tenantSessions.sessionCount--;
|
|
48
|
+
this.logger.debug(`Decreased agent context session count for tenant context '${contextCorrelationId}' to ${tenantSessions.sessionCount} due to failure in mutex script`, error);
|
|
49
|
+
if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) {
|
|
50
|
+
await this.closeAgentContext(tenantSessions.agentContext);
|
|
51
|
+
delete this.tenantAgentContextMapping[contextCorrelationId];
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
return tenantSessions.agentContext;
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger.debug(`Releasing session because an error occurred while getting the context for tenant ${tenantRecord.id}`, { errorMessage: error.message });
|
|
59
|
+
this.sessionMutex.releaseSession();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* End a session for the provided agent context. It will decrease the session count for the agent context.
|
|
65
|
+
* If the number of sessions is zero after the context for this session has been ended, the agent context will be closed.
|
|
66
|
+
*/
|
|
67
|
+
async endAgentContextSession(agentContext) {
|
|
68
|
+
this.logger.debug(`Ending session for agent context with contextCorrelationId ${agentContext.contextCorrelationId}'`);
|
|
69
|
+
if (agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) {
|
|
70
|
+
this.logger.debug("Ending session for root agent context. Not disposing dependency manager");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const contextCorrelationId = agentContext.contextCorrelationId;
|
|
74
|
+
this.assertTenantContextCorrelationId(contextCorrelationId);
|
|
75
|
+
if (!this.hasTenantSessionMapping(contextCorrelationId)) {
|
|
76
|
+
this.logger.error(`Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot end session`);
|
|
77
|
+
throw new CredoError(`Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot end session`);
|
|
78
|
+
}
|
|
79
|
+
await this.mutexForTenant(contextCorrelationId).runExclusive(async () => {
|
|
80
|
+
this.logger.debug(`Acquired lock for tenant '${contextCorrelationId}' to end session context`);
|
|
81
|
+
const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId);
|
|
82
|
+
tenantSessions.sessionCount--;
|
|
83
|
+
this.logger.debug(`Decreased agent context session count for tenant '${contextCorrelationId}' to ${tenantSessions.sessionCount}`);
|
|
84
|
+
if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) {
|
|
85
|
+
await this.closeAgentContext(tenantSessions.agentContext);
|
|
86
|
+
delete this.tenantAgentContextMapping[contextCorrelationId];
|
|
87
|
+
}
|
|
88
|
+
}).finally(() => {
|
|
89
|
+
this.sessionMutex.releaseSession();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Delete the provided agent context. All opens sessions will be disposed and not usable anymore
|
|
94
|
+
*/
|
|
95
|
+
async deleteAgentContext(agentContext) {
|
|
96
|
+
this.logger.debug(`Deleting agent context with contextCorrelationId ${agentContext.contextCorrelationId}'`);
|
|
97
|
+
if (agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) {
|
|
98
|
+
this.logger.debug("Deleting agent context for root agent context.");
|
|
99
|
+
await agentContext.dependencyManager.deleteAgentContext(agentContext);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const contextCorrelationId = agentContext.contextCorrelationId;
|
|
103
|
+
this.assertTenantContextCorrelationId(contextCorrelationId);
|
|
104
|
+
if (!this.hasTenantSessionMapping(contextCorrelationId)) {
|
|
105
|
+
this.logger.error(`Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot delete agent context`);
|
|
106
|
+
throw new CredoError(`Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot delete agent context`);
|
|
107
|
+
}
|
|
108
|
+
await this.mutexForTenant(contextCorrelationId).runExclusive(async () => {
|
|
109
|
+
this.logger.debug(`Acquired lock for tenant '${contextCorrelationId}' to delete agent context`);
|
|
110
|
+
const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId);
|
|
111
|
+
this.logger.debug(`Deleting agent context for tenant '${contextCorrelationId}' with ${tenantSessions.sessionCount} active sessions.`);
|
|
112
|
+
if (!tenantSessions.agentContext) throw new CredoError(`Unable to delete agent context for tenant '${contextCorrelationId}' as there are no active sessions.`);
|
|
113
|
+
await agentContext.dependencyManager.deleteAgentContext(tenantSessions.agentContext);
|
|
114
|
+
delete this.tenantAgentContextMapping[contextCorrelationId];
|
|
115
|
+
}).finally(() => {
|
|
116
|
+
this.sessionMutex.releaseSession();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The context correlation id for a tenant is the tenant id prefixed with tenant-
|
|
121
|
+
*/
|
|
122
|
+
getContextCorrelationIdForTenantId(tenantId) {
|
|
123
|
+
if (tenantId.startsWith("tenant-")) throw new CredoError(`Tenant id already starts with 'tenant-'. You are probalby passing a context correlation id`);
|
|
124
|
+
return `tenant-${tenantId}`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The context correlation id for a tenant is the tenant id prefixed with tenant-
|
|
128
|
+
*/
|
|
129
|
+
getTenantIdForContextCorrelationId(contextCorrelationId) {
|
|
130
|
+
if (!contextCorrelationId.startsWith("tenant-")) throw new CredoError(`Could not extract tenant id from context correlation id. Context correlation id should start with 'tenant-'`);
|
|
131
|
+
return contextCorrelationId.replace("tenant-", "");
|
|
132
|
+
}
|
|
133
|
+
isTenantContextCorrelationId(contextCorrelationId) {
|
|
134
|
+
return contextCorrelationId.startsWith("tenant-");
|
|
135
|
+
}
|
|
136
|
+
assertTenantContextCorrelationId(contextCorrelationId) {
|
|
137
|
+
if (!this.isTenantContextCorrelationId(contextCorrelationId)) throw new CredoError(`Expected context correlation id for tenant to start with 'tenant-'`);
|
|
138
|
+
}
|
|
139
|
+
hasTenantSessionMapping(contextCorrelationId) {
|
|
140
|
+
return this.tenantAgentContextMapping[contextCorrelationId] !== void 0;
|
|
141
|
+
}
|
|
142
|
+
getTenantSessionsMapping(contextCorrelationId) {
|
|
143
|
+
let tenantSessionMapping = this.tenantAgentContextMapping[contextCorrelationId];
|
|
144
|
+
if (tenantSessionMapping) return tenantSessionMapping;
|
|
145
|
+
tenantSessionMapping = {
|
|
146
|
+
sessionCount: 0,
|
|
147
|
+
mutex: withTimeout(new Mutex(), this.tenantsModuleConfig.sessionAcquireTimeout, new CredoError(`Error acquiring lock for tenant context ${contextCorrelationId}. Wallet initialization or shutdown took too long.`))
|
|
148
|
+
};
|
|
149
|
+
this.tenantAgentContextMapping[contextCorrelationId] = tenantSessionMapping;
|
|
150
|
+
return tenantSessionMapping;
|
|
151
|
+
}
|
|
152
|
+
mutexForTenant(contextCorrelationId) {
|
|
153
|
+
return this.getTenantSessionsMapping(contextCorrelationId).mutex;
|
|
154
|
+
}
|
|
155
|
+
async createAgentContext(tenantRecord, { provisionContext }) {
|
|
156
|
+
const tenantDependencyManager = this.rootAgentContext.dependencyManager.createChild();
|
|
157
|
+
const tenantConfig = this.rootAgentContext.config.extend({});
|
|
158
|
+
const agentContext = new AgentContext({
|
|
159
|
+
contextCorrelationId: this.getContextCorrelationIdForTenantId(tenantRecord.id),
|
|
160
|
+
dependencyManager: tenantDependencyManager,
|
|
161
|
+
isRootAgentContext: false
|
|
162
|
+
});
|
|
163
|
+
tenantDependencyManager.registerInstance(AgentContext, agentContext);
|
|
164
|
+
tenantDependencyManager.registerInstance(AgentConfig, tenantConfig);
|
|
165
|
+
if (provisionContext) await tenantDependencyManager.provisionAgentContext(agentContext);
|
|
166
|
+
await tenantDependencyManager.initializeAgentContext(agentContext);
|
|
167
|
+
return agentContext;
|
|
168
|
+
}
|
|
169
|
+
async closeAgentContext(agentContext) {
|
|
170
|
+
this.logger.debug(`Closing agent context for tenant '${agentContext.contextCorrelationId}'`);
|
|
171
|
+
await agentContext.dependencyManager.closeAgentContext(agentContext);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
TenantSessionCoordinator = __decorate([
|
|
175
|
+
injectable(),
|
|
176
|
+
__decorateParam(1, inject(InjectionSymbols.Logger)),
|
|
177
|
+
__decorateMetadata("design:paramtypes", [
|
|
178
|
+
typeof (_ref = typeof AgentContext !== "undefined" && AgentContext) === "function" ? _ref : Object,
|
|
179
|
+
Object,
|
|
180
|
+
typeof (_ref2 = typeof TenantsModuleConfig !== "undefined" && TenantsModuleConfig) === "function" ? _ref2 : Object
|
|
181
|
+
])
|
|
182
|
+
], TenantSessionCoordinator);
|
|
183
|
+
|
|
184
|
+
//#endregion
|
|
185
|
+
export { TenantSessionCoordinator };
|
|
186
|
+
//# sourceMappingURL=TenantSessionCoordinator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantSessionCoordinator.mjs","names":["TenantSessionCoordinator","logger: Logger"],"sources":["../../src/context/TenantSessionCoordinator.ts"],"sourcesContent":["import {\n AgentConfig,\n AgentContext,\n CredoError,\n InjectionSymbols,\n inject,\n injectable,\n type Logger,\n} from '@credo-ts/core'\nimport type { MutexInterface } from 'async-mutex'\nimport { Mutex, withTimeout } from 'async-mutex'\nimport type { TenantRecord } from '../repository'\n\nimport { TenantsModuleConfig } from '../TenantsModuleConfig'\n\nimport { TenantSessionMutex } from './TenantSessionMutex'\n\n/**\n * Coordinates all agent context instance for tenant sessions.\n *\n * This class keeps a mapping of tenant ids (context correlation ids) to agent context sessions mapping. Each mapping contains the agent context,\n * the current session count and a mutex for making operations against the session mapping (opening / closing an agent context). The mutex ensures\n * we're not susceptible to race conditions where multiple calls to open/close an agent context are made at the same time. Even though JavaScript is\n * single threaded, promises can introduce race conditions as one process can stop and another process can be picked up.\n *\n * NOTE: the implementation doesn't yet cache agent context objects after they aren't being used for any sessions anymore. This means if a wallet is being used\n * often in a short time it will be opened/closed very often. This is an improvement to be made in the near future.\n */\n@injectable()\nexport class TenantSessionCoordinator {\n private rootAgentContext: AgentContext\n private logger: Logger\n private tenantAgentContextMapping: TenantAgentContextMapping = {}\n private sessionMutex: TenantSessionMutex\n private tenantsModuleConfig: TenantsModuleConfig\n\n public constructor(\n rootAgentContext: AgentContext,\n @inject(InjectionSymbols.Logger) logger: Logger,\n tenantsModuleConfig: TenantsModuleConfig\n ) {\n this.rootAgentContext = rootAgentContext\n this.logger = logger\n this.tenantsModuleConfig = tenantsModuleConfig\n\n this.sessionMutex = new TenantSessionMutex(\n this.logger,\n this.tenantsModuleConfig.sessionLimit,\n // TODO: we should probably allow a higher session acquire timeout if the storage is being updated?\n this.tenantsModuleConfig.sessionAcquireTimeout\n )\n }\n\n public getSessionCountForTenant(tenantId: string) {\n const contextCorrelationId = this.getContextCorrelationIdForTenantId(tenantId)\n return this.tenantAgentContextMapping[contextCorrelationId]?.sessionCount ?? 0\n }\n\n /**\n * Get agent context to use for a session. If an agent context for this tenant does not exist yet\n * it will create it and store it for later use. If the agent context does already exist it will\n * be returned.\n *\n * @parm tenantRecord The tenant record for which to get the agent context\n */\n public async getContextForSession(\n tenantRecord: TenantRecord,\n {\n runInMutex,\n provisionContext = false,\n }: {\n /** optional callback that will be run inside the mutex lock */\n runInMutex?: (agentContext: AgentContext) => Promise<void>\n provisionContext?: boolean\n } = {}\n ): Promise<AgentContext> {\n this.logger.debug(`Getting context for session with tenant '${tenantRecord.id}'`)\n\n // Wait for a session to be available\n await this.sessionMutex.acquireSession()\n\n try {\n const contextCorrelationId = this.getContextCorrelationIdForTenantId(tenantRecord.id)\n return await this.mutexForTenant(contextCorrelationId).runExclusive(async () => {\n this.logger.debug(`Acquired lock for tenant '${tenantRecord.id}' to get context`)\n const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId)\n\n // If we don't have an agent context already, create one and initialize it\n if (!tenantSessions.agentContext) {\n this.logger.debug(`No agent context has been initialized for tenant '${tenantRecord.id}', creating one`)\n tenantSessions.agentContext = await this.createAgentContext(tenantRecord, { provisionContext })\n }\n\n // If we already have a context with sessions in place return the context and increment\n // the session count.\n tenantSessions.sessionCount++\n this.logger.debug(\n `Increased agent context session count for tenant '${tenantRecord.id}' to ${tenantSessions.sessionCount}`\n )\n\n if (runInMutex) {\n try {\n await runInMutex(tenantSessions.agentContext)\n } catch (error) {\n // If the runInMutex failed we should release the session again\n tenantSessions.sessionCount--\n this.logger.debug(\n `Decreased agent context session count for tenant context '${contextCorrelationId}' to ${tenantSessions.sessionCount} due to failure in mutex script`,\n error\n )\n\n if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) {\n await this.closeAgentContext(tenantSessions.agentContext)\n delete this.tenantAgentContextMapping[contextCorrelationId]\n }\n\n throw error\n }\n }\n\n return tenantSessions.agentContext\n })\n } catch (error) {\n this.logger.debug(\n `Releasing session because an error occurred while getting the context for tenant ${tenantRecord.id}`,\n {\n errorMessage: error.message,\n }\n )\n // If there was an error acquiring the session, we MUST release it, otherwise this will lead to deadlocks over time.\n this.sessionMutex.releaseSession()\n\n // Re-throw error\n throw error\n }\n }\n\n /**\n * End a session for the provided agent context. It will decrease the session count for the agent context.\n * If the number of sessions is zero after the context for this session has been ended, the agent context will be closed.\n */\n public async endAgentContextSession(agentContext: AgentContext): Promise<void> {\n this.logger.debug(\n `Ending session for agent context with contextCorrelationId ${agentContext.contextCorrelationId}'`\n )\n\n // Custom handling for the root agent context. We don't keep track of the total number of sessions for the root\n // agent context, and we always keep the dependency manager intact.\n if (agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) {\n this.logger.debug('Ending session for root agent context. Not disposing dependency manager')\n return\n }\n\n const contextCorrelationId = agentContext.contextCorrelationId\n this.assertTenantContextCorrelationId(contextCorrelationId)\n const hasTenantSessionMapping = this.hasTenantSessionMapping(contextCorrelationId)\n\n // This should not happen\n if (!hasTenantSessionMapping) {\n this.logger.error(\n `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot end session`\n )\n throw new CredoError(\n `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot end session`\n )\n }\n\n await this.mutexForTenant(contextCorrelationId)\n .runExclusive(async () => {\n this.logger.debug(`Acquired lock for tenant '${contextCorrelationId}' to end session context`)\n const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId)\n\n // TODO: check if session count is already 0\n tenantSessions.sessionCount--\n this.logger.debug(\n `Decreased agent context session count for tenant '${contextCorrelationId}' to ${tenantSessions.sessionCount}`\n )\n\n if (tenantSessions.sessionCount <= 0 && tenantSessions.agentContext) {\n await this.closeAgentContext(tenantSessions.agentContext)\n delete this.tenantAgentContextMapping[contextCorrelationId]\n }\n })\n .finally(() => {\n // Release a session so new sessions can be acquired\n this.sessionMutex.releaseSession()\n })\n }\n\n /**\n * Delete the provided agent context. All opens sessions will be disposed and not usable anymore\n */\n public async deleteAgentContext(agentContext: AgentContext): Promise<void> {\n this.logger.debug(`Deleting agent context with contextCorrelationId ${agentContext.contextCorrelationId}'`)\n\n // Custom handling for the root agent context. We don't keep track of the total number of sessions for the root\n // agent context, and we always keep the dependency manager intact.\n if (agentContext.contextCorrelationId === this.rootAgentContext.contextCorrelationId) {\n this.logger.debug('Deleting agent context for root agent context.')\n await agentContext.dependencyManager.deleteAgentContext(agentContext)\n return\n }\n\n const contextCorrelationId = agentContext.contextCorrelationId\n this.assertTenantContextCorrelationId(contextCorrelationId)\n const hasTenantSessionMapping = this.hasTenantSessionMapping(contextCorrelationId)\n\n // This should not happen\n if (!hasTenantSessionMapping) {\n this.logger.error(\n `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot delete agent context`\n )\n throw new CredoError(\n `Unknown agent context with contextCorrelationId '${contextCorrelationId}'. Cannot delete agent context`\n )\n }\n\n await this.mutexForTenant(contextCorrelationId)\n .runExclusive(async () => {\n this.logger.debug(`Acquired lock for tenant '${contextCorrelationId}' to delete agent context`)\n const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId)\n\n this.logger.debug(\n `Deleting agent context for tenant '${contextCorrelationId}' with ${tenantSessions.sessionCount} active sessions.`\n )\n if (!tenantSessions.agentContext) {\n throw new CredoError(\n `Unable to delete agent context for tenant '${contextCorrelationId}' as there are no active sessions.`\n )\n }\n\n await agentContext.dependencyManager.deleteAgentContext(tenantSessions.agentContext)\n delete this.tenantAgentContextMapping[contextCorrelationId]\n })\n .finally(() => {\n // Release a session so new sessions can be acquired\n this.sessionMutex.releaseSession()\n })\n }\n\n /**\n * The context correlation id for a tenant is the tenant id prefixed with tenant-\n */\n public getContextCorrelationIdForTenantId(tenantId: string): TenantContextCorrelationId {\n if (tenantId.startsWith('tenant-')) {\n throw new CredoError(`Tenant id already starts with 'tenant-'. You are probalby passing a context correlation id`)\n }\n\n return `tenant-${tenantId}`\n }\n\n /**\n * The context correlation id for a tenant is the tenant id prefixed with tenant-\n */\n public getTenantIdForContextCorrelationId(contextCorrelationId: TenantContextCorrelationId) {\n if (!contextCorrelationId.startsWith('tenant-')) {\n throw new CredoError(\n `Could not extract tenant id from context correlation id. Context correlation id should start with 'tenant-'`\n )\n }\n\n return contextCorrelationId.replace('tenant-', '')\n }\n\n public isTenantContextCorrelationId(\n contextCorrelationId: string\n ): contextCorrelationId is TenantContextCorrelationId {\n return contextCorrelationId.startsWith('tenant-')\n }\n\n public assertTenantContextCorrelationId(\n contextCorrelationId: string\n ): asserts contextCorrelationId is TenantContextCorrelationId {\n if (!this.isTenantContextCorrelationId(contextCorrelationId)) {\n throw new CredoError(`Expected context correlation id for tenant to start with 'tenant-'`)\n }\n }\n\n private hasTenantSessionMapping(contextCorrelationId: TenantContextCorrelationId): boolean {\n return this.tenantAgentContextMapping[contextCorrelationId] !== undefined\n }\n\n private getTenantSessionsMapping(contextCorrelationId: TenantContextCorrelationId): TenantContextSessions {\n let tenantSessionMapping = this.tenantAgentContextMapping[contextCorrelationId]\n if (tenantSessionMapping) return tenantSessionMapping\n\n tenantSessionMapping = {\n sessionCount: 0,\n mutex: withTimeout(\n new Mutex(),\n // NOTE: It can take a while to create an indy wallet. We're using RAW key derivation which should\n // be fast enough to not cause a problem. This wil also only be problem when the wallet is being created\n // for the first time or being acquired while wallet initialization is in progress.\n this.tenantsModuleConfig.sessionAcquireTimeout,\n new CredoError(\n `Error acquiring lock for tenant context ${contextCorrelationId}. Wallet initialization or shutdown took too long.`\n )\n ),\n }\n this.tenantAgentContextMapping[contextCorrelationId] = tenantSessionMapping\n\n return tenantSessionMapping\n }\n\n private mutexForTenant(contextCorrelationId: TenantContextCorrelationId) {\n const tenantSessions = this.getTenantSessionsMapping(contextCorrelationId)\n\n return tenantSessions.mutex\n }\n\n private async createAgentContext(tenantRecord: TenantRecord, { provisionContext }: { provisionContext: boolean }) {\n const tenantDependencyManager = this.rootAgentContext.dependencyManager.createChild()\n const tenantConfig = this.rootAgentContext.config.extend({})\n\n const agentContext = new AgentContext({\n contextCorrelationId: this.getContextCorrelationIdForTenantId(tenantRecord.id),\n dependencyManager: tenantDependencyManager,\n isRootAgentContext: false,\n })\n\n tenantDependencyManager.registerInstance(AgentContext, agentContext)\n tenantDependencyManager.registerInstance(AgentConfig, tenantConfig)\n\n if (provisionContext) {\n await tenantDependencyManager.provisionAgentContext(agentContext)\n }\n\n await tenantDependencyManager.initializeAgentContext(agentContext)\n\n return agentContext\n }\n\n private async closeAgentContext(agentContext: AgentContext) {\n this.logger.debug(`Closing agent context for tenant '${agentContext.contextCorrelationId}'`)\n await agentContext.dependencyManager.closeAgentContext(agentContext)\n }\n}\n\ninterface TenantContextSessions {\n sessionCount: number\n agentContext?: AgentContext\n mutex: MutexInterface\n}\n\nexport type TenantContextCorrelationId = `tenant-${string}`\n\nexport interface TenantAgentContextMapping {\n [contextCorrelationId: TenantContextCorrelationId]: TenantContextSessions | undefined\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CredoError } from "@credo-ts/core";
|
|
2
|
+
import { Mutex, withTimeout } from "async-mutex";
|
|
3
|
+
|
|
4
|
+
//#region src/context/TenantSessionMutex.ts
|
|
5
|
+
/**
|
|
6
|
+
* Keep track of the total number of tenant sessions currently active. This doesn't actually manage the tenant sessions itself, or have anything to do with
|
|
7
|
+
* the agent context. It merely counts the current number of sessions, and provides a mutex to lock new sessions from being created once the maximum number
|
|
8
|
+
* of sessions has been created. Session that can't be required withing the specified sessionsAcquireTimeout will throw an error.
|
|
9
|
+
*/
|
|
10
|
+
var TenantSessionMutex = class {
|
|
11
|
+
constructor(logger, maxSessions, sessionAcquireTimeout) {
|
|
12
|
+
this._currentSessions = 0;
|
|
13
|
+
this.maxSessions = Number.POSITIVE_INFINITY;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
this.maxSessions = maxSessions;
|
|
16
|
+
this.sessionMutex = withTimeout(new Mutex(), sessionAcquireTimeout, new CredoError(`Failed to acquire an agent context session within ${sessionAcquireTimeout}ms`));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Getter to retrieve the total number of current sessions.
|
|
20
|
+
*/
|
|
21
|
+
get currentSessions() {
|
|
22
|
+
return this._currentSessions;
|
|
23
|
+
}
|
|
24
|
+
set currentSessions(value) {
|
|
25
|
+
this._currentSessions = value;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Wait to acquire a session. Will use the session semaphore to keep total number of sessions limited.
|
|
29
|
+
* For each session that is acquired using this method, the sessions MUST be closed by calling `releaseSession`.
|
|
30
|
+
* Failing to do so can lead to deadlocks over time.
|
|
31
|
+
*/
|
|
32
|
+
async acquireSession() {
|
|
33
|
+
this.logger.debug("Acquiring tenant session");
|
|
34
|
+
if (this.sessionMutex.isLocked()) {
|
|
35
|
+
this.logger.debug("Session mutex is locked, waiting for it to unlock");
|
|
36
|
+
await this.sessionMutex.acquire();
|
|
37
|
+
if (this.currentSessions < this.maxSessions) this.sessionMutex.release();
|
|
38
|
+
}
|
|
39
|
+
this.logger.debug(`Increasing current session count to ${this.currentSessions + 1} (max: ${this.maxSessions})`);
|
|
40
|
+
this.currentSessions++;
|
|
41
|
+
if (this.currentSessions >= this.maxSessions) {
|
|
42
|
+
this.logger.debug(`Reached max number of sessions ${this.maxSessions}, locking mutex`);
|
|
43
|
+
await this.sessionMutex.acquire();
|
|
44
|
+
}
|
|
45
|
+
this.logger.debug(`Acquired tenant session (${this.currentSessions} / ${this.maxSessions})`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Release a session from the session mutex. If the total number of current sessions drops below
|
|
49
|
+
* the max number of sessions, the session mutex will be released so new sessions can be started.
|
|
50
|
+
*/
|
|
51
|
+
releaseSession() {
|
|
52
|
+
this.logger.debug("Releasing tenant session");
|
|
53
|
+
if (this.currentSessions > 0) {
|
|
54
|
+
this.logger.debug(`Decreasing current sessions to ${this.currentSessions - 1} (max: ${this.maxSessions})`);
|
|
55
|
+
this.currentSessions--;
|
|
56
|
+
} else this.logger.warn("Total sessions is already at 0, and releasing a session should not happen in this case. Not decrementing current session count.");
|
|
57
|
+
if (this.sessionMutex.isLocked() && this.currentSessions < this.maxSessions) {
|
|
58
|
+
this.logger.debug(`Releasing session mutex as number of current sessions ${this.currentSessions} is below max number of sessions ${this.maxSessions}`);
|
|
59
|
+
this.sessionMutex.release();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
export { TenantSessionMutex };
|
|
66
|
+
//# sourceMappingURL=TenantSessionMutex.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantSessionMutex.mjs","names":[],"sources":["../../src/context/TenantSessionMutex.ts"],"sourcesContent":["import type { Logger } from '@credo-ts/core'\nimport { CredoError } from '@credo-ts/core'\nimport type { MutexInterface } from 'async-mutex'\nimport { Mutex, withTimeout } from 'async-mutex'\n\n/**\n * Keep track of the total number of tenant sessions currently active. This doesn't actually manage the tenant sessions itself, or have anything to do with\n * the agent context. It merely counts the current number of sessions, and provides a mutex to lock new sessions from being created once the maximum number\n * of sessions has been created. Session that can't be required withing the specified sessionsAcquireTimeout will throw an error.\n */\nexport class TenantSessionMutex {\n private _currentSessions = 0\n public readonly maxSessions = Number.POSITIVE_INFINITY\n private sessionMutex: MutexInterface\n private logger: Logger\n\n public constructor(logger: Logger, maxSessions: number, sessionAcquireTimeout: number) {\n this.logger = logger\n\n this.maxSessions = maxSessions\n // Session mutex, it can take at most sessionAcquireTimeout to acquire a session, otherwise it will fail with the error below\n this.sessionMutex = withTimeout(\n new Mutex(),\n sessionAcquireTimeout,\n new CredoError(`Failed to acquire an agent context session within ${sessionAcquireTimeout}ms`)\n )\n }\n\n /**\n * Getter to retrieve the total number of current sessions.\n */\n public get currentSessions() {\n return this._currentSessions\n }\n\n private set currentSessions(value: number) {\n this._currentSessions = value\n }\n\n /**\n * Wait to acquire a session. Will use the session semaphore to keep total number of sessions limited.\n * For each session that is acquired using this method, the sessions MUST be closed by calling `releaseSession`.\n * Failing to do so can lead to deadlocks over time.\n */\n public async acquireSession() {\n // TODO: We should update this to be weighted\n // This will allow to weight sessions for contexts that already exist lower than sessions\n // for contexts that need to be created (new injection container, wallet session etc..)\n // E.g. opening a context could weigh 5, adding sessions to it would be 1 for each\n this.logger.debug('Acquiring tenant session')\n\n // If we're out of sessions, wait for one to be released.\n if (this.sessionMutex.isLocked()) {\n this.logger.debug('Session mutex is locked, waiting for it to unlock')\n // FIXME: waitForUnlock doesn't work with withTimeout but provides a better API (would rather not acquire and lock)\n // await this.sessionMutex.waitForUnlock()\n // Workaround https://github.com/MatrixAI/js-async-locks/pull/3/files#diff-4ee6a7d91cb8428765713bc3045e1dda5d43214030657a9c04804e96d68778bfR46-R61\n await this.sessionMutex.acquire()\n if (this.currentSessions < this.maxSessions) {\n this.sessionMutex.release()\n }\n }\n\n this.logger.debug(`Increasing current session count to ${this.currentSessions + 1} (max: ${this.maxSessions})`)\n // We have waited for the session to unlock,\n this.currentSessions++\n\n // If we reached the limit we should lock the session mutex\n if (this.currentSessions >= this.maxSessions) {\n this.logger.debug(`Reached max number of sessions ${this.maxSessions}, locking mutex`)\n await this.sessionMutex.acquire()\n }\n\n this.logger.debug(`Acquired tenant session (${this.currentSessions} / ${this.maxSessions})`)\n }\n\n /**\n * Release a session from the session mutex. If the total number of current sessions drops below\n * the max number of sessions, the session mutex will be released so new sessions can be started.\n */\n public releaseSession() {\n this.logger.debug('Releasing tenant session')\n\n if (this.currentSessions > 0) {\n this.logger.debug(`Decreasing current sessions to ${this.currentSessions - 1} (max: ${this.maxSessions})`)\n this.currentSessions--\n } else {\n this.logger.warn(\n 'Total sessions is already at 0, and releasing a session should not happen in this case. Not decrementing current session count.'\n )\n }\n\n // If the number of current sessions is lower than the max number of sessions we can release the mutex\n if (this.sessionMutex.isLocked() && this.currentSessions < this.maxSessions) {\n this.logger.debug(\n `Releasing session mutex as number of current sessions ${this.currentSessions} is below max number of sessions ${this.maxSessions}`\n )\n // Even though marked as deprecated, it is not actually deprecated and will be kept\n // https://github.com/DirtyHairy/async-mutex/issues/50#issuecomment-1007785141\n this.sessionMutex.release()\n }\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAa,qBAAb,MAAgC;CAM9B,AAAO,YAAY,QAAgB,aAAqB,uBAA+B;OAL/E,mBAAmB;OACX,cAAc,OAAO;AAKnC,OAAK,SAAS;AAEd,OAAK,cAAc;AAEnB,OAAK,eAAe,YAClB,IAAI,OAAO,EACX,uBACA,IAAI,WAAW,qDAAqD,sBAAsB,IAAI,CAC/F;;;;;CAMH,IAAW,kBAAkB;AAC3B,SAAO,KAAK;;CAGd,IAAY,gBAAgB,OAAe;AACzC,OAAK,mBAAmB;;;;;;;CAQ1B,MAAa,iBAAiB;AAK5B,OAAK,OAAO,MAAM,2BAA2B;AAG7C,MAAI,KAAK,aAAa,UAAU,EAAE;AAChC,QAAK,OAAO,MAAM,oDAAoD;AAItE,SAAM,KAAK,aAAa,SAAS;AACjC,OAAI,KAAK,kBAAkB,KAAK,YAC9B,MAAK,aAAa,SAAS;;AAI/B,OAAK,OAAO,MAAM,uCAAuC,KAAK,kBAAkB,EAAE,SAAS,KAAK,YAAY,GAAG;AAE/G,OAAK;AAGL,MAAI,KAAK,mBAAmB,KAAK,aAAa;AAC5C,QAAK,OAAO,MAAM,kCAAkC,KAAK,YAAY,iBAAiB;AACtF,SAAM,KAAK,aAAa,SAAS;;AAGnC,OAAK,OAAO,MAAM,4BAA4B,KAAK,gBAAgB,KAAK,KAAK,YAAY,GAAG;;;;;;CAO9F,AAAO,iBAAiB;AACtB,OAAK,OAAO,MAAM,2BAA2B;AAE7C,MAAI,KAAK,kBAAkB,GAAG;AAC5B,QAAK,OAAO,MAAM,kCAAkC,KAAK,kBAAkB,EAAE,SAAS,KAAK,YAAY,GAAG;AAC1G,QAAK;QAEL,MAAK,OAAO,KACV,kIACD;AAIH,MAAI,KAAK,aAAa,UAAU,IAAI,KAAK,kBAAkB,KAAK,aAAa;AAC3E,QAAK,OAAO,MACV,yDAAyD,KAAK,gBAAgB,mCAAmC,KAAK,cACvH;AAGD,QAAK,aAAa,SAAS"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TenantConfig } from "./models/TenantConfig.mjs";
|
|
2
|
+
import { TenantRecord, TenantRecordProps } from "./repository/TenantRecord.mjs";
|
|
3
|
+
import { TenantRepository } from "./repository/TenantRepository.mjs";
|
|
4
|
+
import { TenantRoutingRecord } from "./repository/TenantRoutingRecord.mjs";
|
|
5
|
+
import { TenantAgent } from "./TenantAgent.mjs";
|
|
6
|
+
import { TenantsModuleConfig, TenantsModuleConfigOptions } from "./TenantsModuleConfig.mjs";
|
|
7
|
+
import { CreateTenantOptions, GetTenantAgentOptions, UpdateTenantStorageOptions, WithTenantAgentCallback } from "./TenantsApiOptions.mjs";
|
|
8
|
+
import { TenantsApi } from "./TenantsApi.mjs";
|
|
9
|
+
import { TenantsModule } from "./TenantsModule.mjs";
|
|
10
|
+
export { CreateTenantOptions, GetTenantAgentOptions, TenantAgent, type TenantConfig, TenantRecord, type TenantRecordProps, TenantRepository, TenantRoutingRecord, TenantsApi, TenantsModule, TenantsModuleConfig, TenantsModuleConfigOptions, UpdateTenantStorageOptions, WithTenantAgentCallback };
|
package/build/index.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TenantRecord } from "./repository/TenantRecord.mjs";
|
|
2
|
+
import { TenantRepository } from "./repository/TenantRepository.mjs";
|
|
3
|
+
import { TenantRoutingRecord } from "./repository/TenantRoutingRecord.mjs";
|
|
4
|
+
import { TenantAgent } from "./TenantAgent.mjs";
|
|
5
|
+
import { TenantsModuleConfig } from "./TenantsModuleConfig.mjs";
|
|
6
|
+
import { TenantsApi } from "./TenantsApi.mjs";
|
|
7
|
+
import { TenantsModule } from "./TenantsModule.mjs";
|
|
8
|
+
|
|
9
|
+
export { TenantAgent, TenantRecord, TenantRepository, TenantRoutingRecord, TenantsApi, TenantsModule, TenantsModuleConfig };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantConfig.d.mts","names":[],"sources":["../../src/models/TenantConfig.ts"],"sourcesContent":[],"mappings":";KAAY,YAAA;EAAA,KAAA,EAAA,MAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TenantConfig } from "../models/TenantConfig.mjs";
|
|
2
|
+
import { BaseRecord, TagsBase, VersionString } from "@credo-ts/core";
|
|
3
|
+
|
|
4
|
+
//#region src/repository/TenantRecord.d.ts
|
|
5
|
+
interface TenantRecordProps {
|
|
6
|
+
id?: string;
|
|
7
|
+
createdAt?: Date;
|
|
8
|
+
config: TenantConfig;
|
|
9
|
+
tags?: TagsBase;
|
|
10
|
+
storageVersion: VersionString;
|
|
11
|
+
}
|
|
12
|
+
type DefaultTenantRecordTags = {
|
|
13
|
+
label: string;
|
|
14
|
+
storageVersion: VersionString;
|
|
15
|
+
};
|
|
16
|
+
declare class TenantRecord extends BaseRecord<DefaultTenantRecordTags> {
|
|
17
|
+
static readonly type = "TenantRecord";
|
|
18
|
+
readonly type = "TenantRecord";
|
|
19
|
+
config: TenantConfig;
|
|
20
|
+
/**
|
|
21
|
+
* The storage version that is used by this tenant. Can be used to know if the tenant is ready to be used
|
|
22
|
+
* with the current version of the application.
|
|
23
|
+
*
|
|
24
|
+
* @default 0.4 from 0.5 onwards we set the storage version on creation, so if no value
|
|
25
|
+
* is stored, it means the storage version is 0.4 (when multi-tenancy was introduced)
|
|
26
|
+
*/
|
|
27
|
+
storageVersion: VersionString;
|
|
28
|
+
constructor(props: TenantRecordProps);
|
|
29
|
+
getTags(): {
|
|
30
|
+
label: string;
|
|
31
|
+
storageVersion: VersionString;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { DefaultTenantRecordTags, TenantRecord, TenantRecordProps };
|
|
36
|
+
//# sourceMappingURL=TenantRecord.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantRecord.d.mts","names":[],"sources":["../../src/repository/TenantRecord.ts"],"sourcesContent":[],"mappings":";;;;AAMiB,UAAA,iBAAA,CAAiB;EAEpB,EAAA,CAAA,EAAA,MAAA;EACJ,SAAA,CAAA,EADI,IACJ;EACD,MAAA,EADC,YACD;EACS,IAAA,CAAA,EADT,QACS;EAAa,cAAA,EAAb,aAAa;AAG/B;AAKa,KALD,uBAAA,GAKc;EAAmB,KAAA,EAAA,MAAA;EAI3B,cAAA,EAPA,aAOA;CASO;AAEG,cAff,YAAA,SAAqB,UAeN,CAfiB,uBAejB,CAAA,CAAA;;EAfM,SAAA,IAAA,GAAA,cAAA;EAAU,MAAA,EAI1B,YAJ0B;;;;;;;;kBAanB;qBAEG"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseRecord, utils } from "@credo-ts/core";
|
|
2
|
+
|
|
3
|
+
//#region src/repository/TenantRecord.ts
|
|
4
|
+
var TenantRecord = class TenantRecord extends BaseRecord {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
super();
|
|
7
|
+
this.type = TenantRecord.type;
|
|
8
|
+
this.storageVersion = "0.4";
|
|
9
|
+
if (props) {
|
|
10
|
+
this.id = props.id ?? utils.uuid();
|
|
11
|
+
this.createdAt = props.createdAt ?? /* @__PURE__ */ new Date();
|
|
12
|
+
this._tags = props.tags ?? {};
|
|
13
|
+
this.config = props.config;
|
|
14
|
+
this.storageVersion = props.storageVersion;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
getTags() {
|
|
18
|
+
return {
|
|
19
|
+
...this._tags,
|
|
20
|
+
label: this.config.label,
|
|
21
|
+
storageVersion: this.storageVersion
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
TenantRecord.type = "TenantRecord";
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { TenantRecord };
|
|
29
|
+
//# sourceMappingURL=TenantRecord.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantRecord.mjs","names":[],"sources":["../../src/repository/TenantRecord.ts"],"sourcesContent":["import type { RecordTags, TagsBase, VersionString } from '@credo-ts/core'\nimport { BaseRecord, utils } from '@credo-ts/core'\nimport type { TenantConfig } from '../models/TenantConfig'\n\nexport type TenantRecordTags = RecordTags<TenantRecord>\n\nexport interface TenantRecordProps {\n id?: string\n createdAt?: Date\n config: TenantConfig\n tags?: TagsBase\n storageVersion: VersionString\n}\n\nexport type DefaultTenantRecordTags = {\n label: string\n storageVersion: VersionString\n}\n\nexport class TenantRecord extends BaseRecord<DefaultTenantRecordTags> {\n public static readonly type = 'TenantRecord'\n public readonly type = TenantRecord.type\n\n public config!: TenantConfig\n\n /**\n * The storage version that is used by this tenant. Can be used to know if the tenant is ready to be used\n * with the current version of the application.\n *\n * @default 0.4 from 0.5 onwards we set the storage version on creation, so if no value\n * is stored, it means the storage version is 0.4 (when multi-tenancy was introduced)\n */\n public storageVersion: VersionString = '0.4'\n\n public constructor(props: TenantRecordProps) {\n super()\n\n if (props) {\n this.id = props.id ?? utils.uuid()\n this.createdAt = props.createdAt ?? new Date()\n this._tags = props.tags ?? {}\n this.config = props.config\n this.storageVersion = props.storageVersion\n }\n }\n\n public getTags() {\n return {\n ...this._tags,\n label: this.config.label,\n storageVersion: this.storageVersion,\n }\n }\n}\n"],"mappings":";;;AAmBA,IAAa,eAAb,MAAa,qBAAqB,WAAoC;CAepE,AAAO,YAAY,OAA0B;AAC3C,SAAO;OAdO,OAAO,aAAa;OAW7B,iBAAgC;AAKrC,MAAI,OAAO;AACT,QAAK,KAAK,MAAM,MAAM,MAAM,MAAM;AAClC,QAAK,YAAY,MAAM,6BAAa,IAAI,MAAM;AAC9C,QAAK,QAAQ,MAAM,QAAQ,EAAE;AAC7B,QAAK,SAAS,MAAM;AACpB,QAAK,iBAAiB,MAAM;;;CAIhC,AAAO,UAAU;AACf,SAAO;GACL,GAAG,KAAK;GACR,OAAO,KAAK,OAAO;GACnB,gBAAgB,KAAK;GACtB;;;aA/BoB,OAAO"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TenantRecord } from "./TenantRecord.mjs";
|
|
2
|
+
import { AgentContext, EventEmitter, Repository, StorageService } from "@credo-ts/core";
|
|
3
|
+
|
|
4
|
+
//#region src/repository/TenantRepository.d.ts
|
|
5
|
+
declare class TenantRepository extends Repository<TenantRecord> {
|
|
6
|
+
constructor(storageService: StorageService<TenantRecord>, eventEmitter: EventEmitter);
|
|
7
|
+
findByLabel(agentContext: AgentContext, label: string): Promise<TenantRecord[]>;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { TenantRepository };
|
|
11
|
+
//# sourceMappingURL=TenantRepository.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantRepository.d.mts","names":[],"sources":["../../src/repository/TenantRepository.ts"],"sourcesContent":[],"mappings":";;;;cAOa,gBAAA,SAAyB,WAAW;EAApC,WAAA,CAAA,cAAiB,EAE+B,cAF/B,CAE8C,YAF9C,CAAA,EAAA,YAAA,EAGZ,YAHY;EAAmB,WAAA,CAAA,YAAA,EAQR,YARQ,EAAA,KAAA,EAAA,MAAA,CAAA,EAQsB,OARtB,CAQ8B,YAR9B,EAAA,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TenantRecord } from "./TenantRecord.mjs";
|
|
2
|
+
import { __decorateMetadata } from "../_virtual/_@oxc-project_runtime@0.99.0/helpers/decorateMetadata.mjs";
|
|
3
|
+
import { __decorateParam } from "../_virtual/_@oxc-project_runtime@0.99.0/helpers/decorateParam.mjs";
|
|
4
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.99.0/helpers/decorate.mjs";
|
|
5
|
+
import { EventEmitter, InjectionSymbols, Repository, inject, injectable } from "@credo-ts/core";
|
|
6
|
+
|
|
7
|
+
//#region src/repository/TenantRepository.ts
|
|
8
|
+
var _ref;
|
|
9
|
+
let TenantRepository = class TenantRepository$1 extends Repository {
|
|
10
|
+
constructor(storageService, eventEmitter) {
|
|
11
|
+
super(TenantRecord, storageService, eventEmitter);
|
|
12
|
+
}
|
|
13
|
+
async findByLabel(agentContext, label) {
|
|
14
|
+
return this.findByQuery(agentContext, { label });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
TenantRepository = __decorate([
|
|
18
|
+
injectable(),
|
|
19
|
+
__decorateParam(0, inject(InjectionSymbols.StorageService)),
|
|
20
|
+
__decorateMetadata("design:paramtypes", [Object, typeof (_ref = typeof EventEmitter !== "undefined" && EventEmitter) === "function" ? _ref : Object])
|
|
21
|
+
], TenantRepository);
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { TenantRepository };
|
|
25
|
+
//# sourceMappingURL=TenantRepository.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantRepository.mjs","names":["TenantRepository","storageService: StorageService<TenantRecord>"],"sources":["../../src/repository/TenantRepository.ts"],"sourcesContent":["import type { AgentContext } from '@credo-ts/core'\n\nimport { EventEmitter, InjectionSymbols, inject, injectable, Repository, type StorageService } from '@credo-ts/core'\n\nimport { TenantRecord } from './TenantRecord'\n\n@injectable()\nexport class TenantRepository extends Repository<TenantRecord> {\n public constructor(\n @inject(InjectionSymbols.StorageService) storageService: StorageService<TenantRecord>,\n eventEmitter: EventEmitter\n ) {\n super(TenantRecord, storageService, eventEmitter)\n }\n\n public async findByLabel(agentContext: AgentContext, label: string): Promise<TenantRecord[]> {\n return this.findByQuery(agentContext, { label })\n }\n}\n"],"mappings":";;;;;;;;AAOO,6BAAMA,2BAAyB,WAAyB;CAC7D,AAAO,YACL,AAAyCC,gBACzC,cACA;AACA,QAAM,cAAc,gBAAgB,aAAa;;CAGnD,MAAa,YAAY,cAA4B,OAAwC;AAC3F,SAAO,KAAK,YAAY,cAAc,EAAE,OAAO,CAAC;;;;CAVnD,YAAY;oBAGR,OAAO,iBAAiB,eAAe"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseRecord, TagsBase } from "@credo-ts/core";
|
|
2
|
+
|
|
3
|
+
//#region src/repository/TenantRoutingRecord.d.ts
|
|
4
|
+
type DefaultTenantRoutingRecordTags = {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
recipientKeyFingerprint: string;
|
|
7
|
+
};
|
|
8
|
+
interface TenantRoutingRecordProps {
|
|
9
|
+
id?: string;
|
|
10
|
+
createdAt?: Date;
|
|
11
|
+
tags?: TagsBase;
|
|
12
|
+
tenantId: string;
|
|
13
|
+
recipientKeyFingerprint: string;
|
|
14
|
+
}
|
|
15
|
+
declare class TenantRoutingRecord extends BaseRecord<DefaultTenantRoutingRecordTags> {
|
|
16
|
+
static readonly type = "TenantRoutingRecord";
|
|
17
|
+
readonly type = "TenantRoutingRecord";
|
|
18
|
+
tenantId: string;
|
|
19
|
+
recipientKeyFingerprint: string;
|
|
20
|
+
constructor(props: TenantRoutingRecordProps);
|
|
21
|
+
getTags(): {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
recipientKeyFingerprint: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { TenantRoutingRecord, TenantRoutingRecordProps };
|
|
28
|
+
//# sourceMappingURL=TenantRoutingRecord.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantRoutingRecord.d.mts","names":[],"sources":["../../src/repository/TenantRoutingRecord.ts"],"sourcesContent":[],"mappings":";;;AAIqE,KAEhE,8BAAA,GAA8B;EAKlB,QAAA,EAAA,MAAA;EASJ,uBAAoB,EAAA,MAAA;CAAmB;AAOxB,UAhBX,wBAAA,CAgBW;EAPa,EAAA,CAAA,EAAA,MAAA;EAAU,SAAA,CAAA,EAPrC,IAOqC;SAN1C;;;;cAMI,mBAAA,SAA4B,WAAW;;;;;qBAOxB"}
|