import * as plugins from './plugins.js'; type TGetIdentityCredential = () => plugins.servezoneInterfaces.data.IIdentityCredential; type TFireRequest = ( methodArg: T['method'], requestArg: T['request'], ) => Promise; const isExactRecord = ( valueArg: unknown, keysArg: string[], ): valueArg is Record => { try { return valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg) && JSON.stringify(Object.keys(valueArg).sort()) === JSON.stringify([...keysArg].sort()); } catch { return false; } }; export type TSecretClientValueInput = | plugins.servezoneInterfaces.data.TSecretValueInput | { mode: 'provided-bytes'; bytes: Uint8Array; }; export type TCreateSecretOptions = Omit< plugins.servezoneInterfaces.requests.secret.IReq_CreateSecret['request'], 'identity' | 'valueInput' > & { valueInput: TSecretClientValueInput; }; export type TRotateSecretOptions = Omit< plugins.servezoneInterfaces.requests.secret.IReq_RotateSecret['request'], 'identity' | 'valueInput' > & { valueInput: TSecretClientValueInput; }; const cloneSecretTarget = ( targetArg: plugins.servezoneInterfaces.data.TSecretMutationTarget, ): plugins.servezoneInterfaces.data.TSecretMutationTarget => { switch (targetArg.kind) { case 'service': return { kind: 'service', serviceId: targetArg.serviceId }; case 'secret-set': return { kind: 'secret-set', secretSetId: targetArg.secretSetId }; case 'platform-provider': return { kind: 'platform-provider', providerConfigId: targetArg.providerConfigId }; case 'system': return { kind: 'system', systemId: targetArg.systemId }; } }; const cloneSecretDelivery = ( deliveryArg: plugins.servezoneInterfaces.data.TSecretDelivery, ): plugins.servezoneInterfaces.data.TSecretDelivery => { if (deliveryArg.type === 'launcher-environment') { return { type: 'launcher-environment', variableName: deliveryArg.variableName, uid: deliveryArg.uid, gid: deliveryArg.gid, mode: deliveryArg.mode, }; } return { type: 'file', targetPath: deliveryArg.targetPath, uid: deliveryArg.uid, gid: deliveryArg.gid, mode: deliveryArg.mode, }; }; const cloneSecretTags = ( tagsArg: Array<{ key: string; value: string }>, ): Array<{ key: string; value: string }> => tagsArg.map((tagArg) => ({ key: tagArg.key, value: tagArg.value, })); const cloneSecretSetAttachment = ( attachmentArg: plugins.servezoneInterfaces.data.ISecretSetAttachment, ): plugins.servezoneInterfaces.data.ISecretSetAttachment => { const attachment: plugins.servezoneInterfaces.data.ISecretSetAttachment = { id: attachmentArg.id, secretSetId: attachmentArg.secretSetId, environment: attachmentArg.environment, }; if (attachmentArg.mappings !== undefined) { attachment.mappings = attachmentArg.mappings.map((mappingArg) => { const mapping: plugins.servezoneInterfaces.data.ISecretSetAttachmentMapping = { sourceKey: mappingArg.sourceKey, }; if (mappingArg.targetKey !== undefined) { mapping.targetKey = mappingArg.targetKey; } if (mappingArg.delivery !== undefined) { mapping.delivery = cloneSecretDelivery(mappingArg.delivery); } return mapping; }); } return attachment; }; const encodeCanonicalBase64Url = (bytesArg: Uint8Array): string => { let binary = ''; for (const byte of bytesArg) binary += String.fromCharCode(byte); return globalThis.btoa(binary) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); }; const decodeCanonicalRawX25519PublicKey = (valueArg: unknown): Uint8Array => { if (typeof valueArg !== 'string') { throw new Error('secret ingress recipient is invalid'); } let decoded: string; try { const base64 = valueArg.replace(/-/g, '+').replace(/_/g, '/'); decoded = globalThis.atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')); } catch { throw new Error('secret ingress recipient is invalid'); } const publicKey = Uint8Array.from(decoded, (characterArg) => characterArg.charCodeAt(0)); if (publicKey.byteLength !== plugins.smartcrypto.SMARTCRYPTO_X25519_KEY_BYTES || encodeCanonicalBase64Url(publicKey) !== valueArg) { publicKey.fill(0); throw new Error('secret ingress recipient is invalid'); } return publicKey; }; const cloneEnvelope = ( envelopeArg: plugins.smartcrypto.IX25519EnvelopeV1, ): plugins.smartcrypto.IX25519EnvelopeV1 => ({ schemaVersion: envelopeArg.schemaVersion, profile: envelopeArg.profile, recipientKeyId: envelopeArg.recipientKeyId, ephemeralPublicKey: envelopeArg.ephemeralPublicKey, nonce: envelopeArg.nonce, ciphertext: envelopeArg.ciphertext, tag: envelopeArg.tag, contextDigest: envelopeArg.contextDigest, }); export class SecretClient { constructor( private readonly getIdentityCredential: TGetIdentityCredential, private readonly fireRequest: TFireRequest, ) {} private normalizeWireValueInput( valueInputArg: Exclude, ): plugins.servezoneInterfaces.data.TSecretValueInput { let valueInput: plugins.servezoneInterfaces.data.TSecretValueInput; if (valueInputArg.mode === 'generated') { valueInput = { mode: 'generated', encoding: valueInputArg.encoding, bytes: valueInputArg.bytes, }; } else { const envelope = plugins.smartcrypto.parseX25519Envelope(valueInputArg.envelope); valueInput = { mode: 'sealed', envelope: cloneEnvelope(envelope), }; } if (plugins.servezoneInterfaces.data.validateSecretValueInput(valueInput).length > 0) { throw new Error('secret value input is invalid'); } return valueInput; } private async getActiveIngressRecipient(): Promise<{ recipient: plugins.servezoneInterfaces.data.IActiveSecretRecipientMetadata; publicKey: Uint8Array; }> { const response = await this.getSecretIngressRecipient(); return { recipient: response.recipient, publicKey: decodeCanonicalRawX25519PublicKey(response.recipient.publicKey), }; } private buildCreateRequest( optionsArg: TCreateSecretOptions, identityArg: plugins.servezoneInterfaces.data.IIdentityCredential, valueInputArg: plugins.servezoneInterfaces.data.TSecretValueInput, ): plugins.servezoneInterfaces.requests.secret.IReq_CreateSecret['request'] { const request: plugins.servezoneInterfaces.requests.secret.IReq_CreateSecret['request'] = { identity: identityArg, mutationId: optionsArg.mutationId, target: cloneSecretTarget(optionsArg.target), key: optionsArg.key, environment: optionsArg.environment, name: optionsArg.name, delivery: cloneSecretDelivery(optionsArg.delivery), valueInput: valueInputArg, expectedTargetSecretsRevision: optionsArg.expectedTargetSecretsRevision, }; if (optionsArg.description !== undefined) { request.description = optionsArg.description; } if (optionsArg.tags !== undefined) { request.tags = cloneSecretTags(optionsArg.tags); } return request; } private buildRotateRequest( optionsArg: TRotateSecretOptions, identityArg: plugins.servezoneInterfaces.data.IIdentityCredential, valueInputArg: plugins.servezoneInterfaces.data.TSecretValueInput, ): plugins.servezoneInterfaces.requests.secret.IReq_RotateSecret['request'] { return { identity: identityArg, mutationId: optionsArg.mutationId, secretId: optionsArg.secretId, valueInput: valueInputArg, expectedSecretRevision: optionsArg.expectedSecretRevision, expectedActiveVersionId: optionsArg.expectedActiveVersionId, expectedTargetSecretsRevision: optionsArg.expectedTargetSecretsRevision, }; } public async getSecretIngressRecipient(): Promise< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretIngressRecipient['response'] > { const response = await this.fireRequest< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretIngressRecipient >( 'getSecretIngressRecipient', { identity: this.getIdentityCredential() }, ); if (!isExactRecord(response, ['recipient']) || plugins.servezoneInterfaces.data.validateSecretRecipientMetadata( response.recipient, ).length > 0 || response.recipient.lifecycleState !== 'active') { throw new Error('secret ingress recipient response is invalid'); } return response; } public async listSecrets( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_ListSecrets['request'], 'identity' >, ): Promise { const request: plugins.servezoneInterfaces.requests.secret.IReq_ListSecrets['request'] = { identity: this.getIdentityCredential(), mutationId: optionsArg.mutationId, target: cloneSecretTarget(optionsArg.target), }; if (optionsArg.environment !== undefined) { request.environment = optionsArg.environment; } return this.fireRequest( 'listSecrets', request, ); } public async getSecretMetadata( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretMetadata['request'], 'identity' >, ): Promise { return this.fireRequest( 'getSecretMetadata', { identity: this.getIdentityCredential(), mutationId: optionsArg.mutationId, secretId: optionsArg.secretId, }, ); } public async createSecret( optionsArg: TCreateSecretOptions, ): Promise { const identity = this.getIdentityCredential(); if (optionsArg.valueInput.mode !== 'provided-bytes') { const valueInput = this.normalizeWireValueInput(optionsArg.valueInput); return this.fireRequest( 'createSecret', this.buildCreateRequest(optionsArg, identity, valueInput), ); } if (!(optionsArg.valueInput.bytes instanceof Uint8Array)) { throw new Error('secret value input is invalid'); } const valueBytes = new Uint8Array(optionsArg.valueInput.bytes); let recipientPublicKey: Uint8Array | undefined; let context: Uint8Array | undefined; try { if (plugins.servezoneInterfaces.data.validateSecretValueBytes( valueBytes, cloneSecretDelivery(optionsArg.delivery), ).length > 0) { throw new Error('secret value input is invalid'); } const { recipient, publicKey } = await this.getActiveIngressRecipient(); recipientPublicKey = publicKey; context = plugins.servezoneInterfaces.data.createSecretCreateEnvelopeContext({ mutationId: optionsArg.mutationId, target: cloneSecretTarget(optionsArg.target), key: optionsArg.key, environment: optionsArg.environment, delivery: cloneSecretDelivery(optionsArg.delivery), expectedTargetSecretsRevision: optionsArg.expectedTargetSecretsRevision, }); const envelope = await plugins.smartcrypto.sealX25519Envelope({ plaintext: valueBytes, recipientPublicKey, recipientKeyId: recipient.recipientKeyId, context, }); const request = this.buildCreateRequest(optionsArg, identity, { mode: 'sealed', envelope: cloneEnvelope(envelope), }); if ((await plugins.servezoneInterfaces.requests.secret.validateCreateSecretRequest( request, recipient, )).length > 0) { throw new Error('secret create request is invalid'); } return await this.fireRequest( 'createSecret', request, ); } finally { valueBytes.fill(0); recipientPublicKey?.fill(0); context?.fill(0); } } public async rotateSecret( optionsArg: TRotateSecretOptions, ): Promise { const identity = this.getIdentityCredential(); if (optionsArg.valueInput.mode !== 'provided-bytes') { const valueInput = this.normalizeWireValueInput(optionsArg.valueInput); return this.fireRequest( 'rotateSecret', this.buildRotateRequest(optionsArg, identity, valueInput), ); } if (!(optionsArg.valueInput.bytes instanceof Uint8Array) || optionsArg.valueInput.bytes.byteLength > plugins.servezoneInterfaces.data.secretValueLimits.maximumBytes) { throw new Error('secret value input is invalid'); } const valueBytes = new Uint8Array(optionsArg.valueInput.bytes); let recipientPublicKey: Uint8Array | undefined; let context: Uint8Array | undefined; try { const { recipient, publicKey } = await this.getActiveIngressRecipient(); recipientPublicKey = publicKey; context = plugins.servezoneInterfaces.data.createSecretRotateEnvelopeContext({ mutationId: optionsArg.mutationId, secretId: optionsArg.secretId, expectedSecretRevision: optionsArg.expectedSecretRevision, expectedActiveVersionId: optionsArg.expectedActiveVersionId, expectedTargetSecretsRevision: optionsArg.expectedTargetSecretsRevision, }); const envelope = await plugins.smartcrypto.sealX25519Envelope({ plaintext: valueBytes, recipientPublicKey, recipientKeyId: recipient.recipientKeyId, context, }); const request = this.buildRotateRequest(optionsArg, identity, { mode: 'sealed', envelope: cloneEnvelope(envelope), }); if ((await plugins.servezoneInterfaces.requests.secret.validateRotateSecretRequest( request, recipient, )).length > 0) { throw new Error('secret rotate request is invalid'); } return await this.fireRequest( 'rotateSecret', request, ); } finally { valueBytes.fill(0); recipientPublicKey?.fill(0); context?.fill(0); } } public async changeSecretLifecycle( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_ChangeSecretLifecycle['request'], 'identity' >, ): Promise { return this.fireRequest( 'changeSecretLifecycle', { identity: this.getIdentityCredential(), secretId: optionsArg.secretId, action: optionsArg.action, expectedSecretRevision: optionsArg.expectedSecretRevision, expectedTargetSecretsRevision: optionsArg.expectedTargetSecretsRevision, }, ); } public async getSecretVersionPurgePreflight( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretVersionPurgePreflight['request'], 'identity' >, ): Promise< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretVersionPurgePreflight['response'] > { const request: plugins.servezoneInterfaces.requests.secret .IReq_GetSecretVersionPurgePreflight['request'] = { identity: this.getIdentityCredential(), secretId: optionsArg.secretId, secretVersionId: optionsArg.secretVersionId, }; if (optionsArg.cursor !== undefined) request.cursor = optionsArg.cursor; if (optionsArg.limit !== undefined) request.limit = optionsArg.limit; if (plugins.servezoneInterfaces.requests.secret .validateGetSecretVersionPurgePreflightRequest(request).length > 0) { throw new Error('secret version purge preflight request is invalid'); } const response = await this.fireRequest< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretVersionPurgePreflight >('getSecretVersionPurgePreflight', request); if ((await plugins.servezoneInterfaces.requests.secret .validateSecretVersionPurgePreflightResponse(response)).length > 0) { throw new Error('secret version purge preflight response is invalid'); } return response; } public async purgeSecretVersion( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_PurgeSecretVersion['request'], 'identity' >, ): Promise { const request: plugins.servezoneInterfaces.requests.secret.IReq_PurgeSecretVersion['request'] = { identity: this.getIdentityCredential(), mutationId: optionsArg.mutationId, secretId: optionsArg.secretId, secretVersionId: optionsArg.secretVersionId, expectedSecretRevision: optionsArg.expectedSecretRevision, expectedSecretVersionRevision: optionsArg.expectedSecretVersionRevision, expectedTargetSecretsRevision: optionsArg.expectedTargetSecretsRevision, }; if (plugins.servezoneInterfaces.requests.secret .validatePurgeSecretVersionRequest(request).length > 0) { throw new Error('purge secret version request is invalid'); } return this.fireRequest( 'purgeSecretVersion', request, ); } public async listSecretSets( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_ListSecretSets['request'], 'identity' >, ): Promise { return this.fireRequest( 'listSecretSets', { identity: this.getIdentityCredential(), organizationId: optionsArg.organizationId, }, ); } public async createSecretSet( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_CreateSecretSet['request'], 'identity' >, ): Promise { const request: plugins.servezoneInterfaces.requests.secret.IReq_CreateSecretSet['request'] = { identity: this.getIdentityCredential(), organizationId: optionsArg.organizationId, name: optionsArg.name, }; if (optionsArg.description !== undefined) { request.description = optionsArg.description; } return this.fireRequest( 'createSecretSet', request, ); } public async updateSecretSet( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_UpdateSecretSet['request'], 'identity' >, ): Promise { const request: plugins.servezoneInterfaces.requests.secret.IReq_UpdateSecretSet['request'] = { identity: this.getIdentityCredential(), secretSetId: optionsArg.secretSetId, name: optionsArg.name, expectedRevision: optionsArg.expectedRevision, }; if (optionsArg.description !== undefined) { request.description = optionsArg.description; } return this.fireRequest( 'updateSecretSet', request, ); } public async changeSecretSetLifecycle( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_ChangeSecretSetLifecycle['request'], 'identity' >, ): Promise { return this.fireRequest< plugins.servezoneInterfaces.requests.secret.IReq_ChangeSecretSetLifecycle >( 'changeSecretSetLifecycle', { identity: this.getIdentityCredential(), secretSetId: optionsArg.secretSetId, action: optionsArg.action, expectedRevision: optionsArg.expectedRevision, expectedSecretsRevision: optionsArg.expectedSecretsRevision, }, ); } public async getSecretSetConsumerRollout( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretSetConsumerRollout['request'], 'identity' >, ): Promise< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretSetConsumerRollout['response'] > { const request: plugins.servezoneInterfaces.requests.secret.IReq_GetSecretSetConsumerRollout['request'] = { identity: this.getIdentityCredential(), secretSetId: optionsArg.secretSetId, rolloutId: optionsArg.rolloutId, }; if (optionsArg.cursor !== undefined) { request.cursor = optionsArg.cursor; } if (optionsArg.limit !== undefined) { request.limit = optionsArg.limit; } return this.fireRequest< plugins.servezoneInterfaces.requests.secret.IReq_GetSecretSetConsumerRollout >('getSecretSetConsumerRollout', request); } public async setServiceSecretSetAttachments( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_SetServiceSecretSetAttachments['request'], 'identity' >, ): Promise< plugins.servezoneInterfaces.requests.secret.IReq_SetServiceSecretSetAttachments['response'] > { return this.fireRequest< plugins.servezoneInterfaces.requests.secret.IReq_SetServiceSecretSetAttachments >( 'setServiceSecretSetAttachments', { identity: this.getIdentityCredential(), serviceId: optionsArg.serviceId, attachments: optionsArg.attachments.map(cloneSecretSetAttachment), expectedSecretConfigurationRevision: optionsArg.expectedSecretConfigurationRevision, }, ); } public async previewServiceSecretResolution( optionsArg: Omit< plugins.servezoneInterfaces.requests.secret.IReq_PreviewServiceSecretResolution['request'], 'identity' >, ): Promise< plugins.servezoneInterfaces.requests.secret.IReq_PreviewServiceSecretResolution['response'] > { const request: plugins.servezoneInterfaces.requests.secret.IReq_PreviewServiceSecretResolution['request'] = { identity: this.getIdentityCredential(), serviceId: optionsArg.serviceId, }; if (optionsArg.attachments !== undefined) { request.attachments = optionsArg.attachments.map(cloneSecretSetAttachment); } return this.fireRequest< plugins.servezoneInterfaces.requests.secret.IReq_PreviewServiceSecretResolution >('previewServiceSecretResolution', request); } }