diff --git a/package.json b/package.json index 76fade158..d9d8ceb6b 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "prebuild": "yarn typechain" }, "dependencies": { - "@nucypher/nucypher-core": "^0.12.0", + "@nucypher/nucypher-core": "^0.13.0", "axios": "^1.5.0", "deep-equal": "^2.2.1", "ethers": "^5.7.2", diff --git a/src/characters/cbd-recipient.ts b/src/characters/cbd-recipient.ts index b7cba3aaa..ad973616a 100644 --- a/src/characters/cbd-recipient.ts +++ b/src/characters/cbd-recipient.ts @@ -1,26 +1,22 @@ import { - AccessControlPolicy, - AuthenticatedData, - Ciphertext, combineDecryptionSharesSimple, Context, DecryptionShareSimple, - decryptWithSharedSecret, EncryptedThresholdDecryptionRequest, EncryptedThresholdDecryptionResponse, FerveoVariant, SessionSharedSecret, SessionStaticSecret, ThresholdDecryptionRequest, + ThresholdMessageKit, } from '@nucypher/nucypher-core'; import { ethers } from 'ethers'; -import { keccak256 } from 'ethers/lib/utils'; import { DkgCoordinatorAgent, DkgParticipant } from '../agents/coordinator'; -import { ConditionExpression } from '../conditions'; -import { DkgClient, DkgRitual } from '../dkg'; +import { ConditionContext } from '../conditions'; +import { DkgRitual } from '../dkg'; import { PorterClient } from '../porter'; -import { fromJSON, toBytes, toJSON } from '../utils'; +import { fromJSON, objectEquals, toJSON } from '../utils'; export type ThresholdDecrypterJSON = { porterUri: string; @@ -46,71 +42,40 @@ export class ThresholdDecrypter { // Retrieve and decrypt ciphertext using provider and condition expression public async retrieveAndDecrypt( provider: ethers.providers.Provider, - signer: ethers.Signer, - conditionExpr: ConditionExpression, - ciphertext: Ciphertext + thresholdMessageKit: ThresholdMessageKit, + signer?: ethers.Signer ): Promise { - const acp = await this.makeAcp(provider, signer, conditionExpr, ciphertext); - const decryptionShares = await this.retrieve( provider, - conditionExpr, - ciphertext, - acp, + thresholdMessageKit, signer ); - const sharedSecret = combineDecryptionSharesSimple(decryptionShares); - return decryptWithSharedSecret( - ciphertext, - conditionExpr.asAad(), - sharedSecret - ); - } - - private async makeAcp( - provider: ethers.providers.Provider, - signer: ethers.Signer, - conditionExpr: ConditionExpression, - ciphertext: Ciphertext - ) { - const dkgRitual = await DkgClient.getExistingRitual( - provider, - this.ritualId - ); - const authData = new AuthenticatedData( - dkgRitual.dkgPublicKey, - conditionExpr.toWASMConditions() - ); - - const headerHash = keccak256(ciphertext.header.toBytes()); - const authorization = await signer.signMessage(headerHash); - - return new AccessControlPolicy(authData, toBytes(authorization)); + return thresholdMessageKit.decryptWithSharedSecret(sharedSecret); } // Retrieve decryption shares public async retrieve( provider: ethers.providers.Provider, - conditionExpr: ConditionExpression, - ciphertext: Ciphertext, - acp: AccessControlPolicy, + thresholdMessageKit: ThresholdMessageKit, signer?: ethers.Signer ): Promise { const dkgParticipants = await DkgCoordinatorAgent.getParticipants( provider, this.ritualId ); - const contextStr = await conditionExpr - .buildContext(provider, {}, signer) - .toJson(); - const { sharedSecrets, encryptedRequests } = this.makeDecryptionRequests( - this.ritualId, - ciphertext, - contextStr, - dkgParticipants, - acp - ); + const wasmContext = await ConditionContext.fromConditions( + provider, + thresholdMessageKit.acp.conditions, + signer + ).toWASMContext(); + const { sharedSecrets, encryptedRequests } = + await this.makeDecryptionRequests( + this.ritualId, + wasmContext, + dkgParticipants, + thresholdMessageKit + ); const { encryptedResponses, errors } = await this.porter.cbdDecrypt( encryptedRequests, @@ -152,22 +117,21 @@ export class ThresholdDecrypter { ); } - private makeDecryptionRequests( + private async makeDecryptionRequests( ritualId: number, - ciphertext: Ciphertext, - contextStr: string, + wasmContext: Context, dkgParticipants: Array, - acp: AccessControlPolicy - ): { + thresholdMessageKit: ThresholdMessageKit + ): Promise<{ sharedSecrets: Record; encryptedRequests: Record; - } { + }> { const decryptionRequest = new ThresholdDecryptionRequest( ritualId, FerveoVariant.simple, - ciphertext.header, - acp, - new Context(contextStr) + thresholdMessageKit.ciphertextHeader, + thresholdMessageKit.acp, + wasmContext ); const ephemeralSessionKey = this.makeSessionKey(); @@ -234,8 +198,6 @@ export class ThresholdDecrypter { } public equals(other: ThresholdDecrypter): boolean { - return ( - this.porter.porterUrl.toString() === other.porter.porterUrl.toString() - ); + return objectEquals(this.toObj(), other.toObj()); } } diff --git a/src/characters/enrico.ts b/src/characters/enrico.ts index 17c6ff358..3185ca050 100644 --- a/src/characters/enrico.ts +++ b/src/characters/enrico.ts @@ -1,11 +1,13 @@ import { - Ciphertext, + AccessControlPolicy, DkgPublicKey, - ferveoEncrypt, + encryptForDkg, MessageKit, PublicKey, SecretKey, + ThresholdMessageKit, } from '@nucypher/nucypher-core'; +import { arrayify, keccak256 } from 'ethers/lib/utils'; import { ConditionExpression } from '../conditions'; import { Keyring } from '../keyring'; @@ -51,13 +53,13 @@ export class Enrico { public encryptMessageCbd( plaintext: Uint8Array | string, - withConditions?: ConditionExpression - ): { ciphertext: Ciphertext; aad: Uint8Array } { - if (!withConditions) { - withConditions = this.conditions; + conditions?: ConditionExpression + ): ThresholdMessageKit { + if (!conditions) { + conditions = this.conditions; } - if (!withConditions) { + if (!conditions) { throw new Error('Conditions are required for CBD encryption.'); } @@ -65,12 +67,19 @@ export class Enrico { throw new Error('Wrong key type. Use encryptMessagePre instead.'); } - const aad = withConditions.asAad(); - const ciphertext = ferveoEncrypt( + const [ciphertext, authenticatedData] = encryptForDkg( plaintext instanceof Uint8Array ? plaintext : toBytes(plaintext), - aad, - this.encryptingKey + this.encryptingKey, + conditions.toWASMConditions() + ); + + const headerHash = keccak256(ciphertext.header.toBytes()); + const authorization = this.keyring.signer.sign(arrayify(headerHash)); + const acp = new AccessControlPolicy( + authenticatedData, + authorization.toBEBytes() ); - return { ciphertext, aad }; + + return new ThresholdMessageKit(ciphertext, acp); } } diff --git a/src/conditions/condition-expr.ts b/src/conditions/condition-expr.ts index 697687088..ee12d7ccb 100644 --- a/src/conditions/condition-expr.ts +++ b/src/conditions/condition-expr.ts @@ -2,7 +2,7 @@ import { Conditions as WASMConditions } from '@nucypher/nucypher-core'; import { ethers } from 'ethers'; import { SemVer } from 'semver'; -import { toBytes, toJSON } from '../utils'; +import { toJSON } from '../utils'; import { Condition } from './condition'; import { ConditionContext, CustomContextParam } from './context'; @@ -13,7 +13,7 @@ export type ConditionExpressionJSON = { }; export class ConditionExpression { - static VERSION = '1.0.0'; + public static VERSION = '1.0.0'; constructor( public readonly condition: Condition, @@ -61,6 +61,10 @@ export class ConditionExpression { return new WASMConditions(toJSON(this.toObj())); } + public static fromWASMConditions(conditions: WASMConditions) { + return ConditionExpression.fromJSON(conditions.toString()); + } + public buildContext( provider: ethers.providers.Provider, customParameters: Record = {}, @@ -78,10 +82,6 @@ export class ConditionExpression { return this.condition.requiresSigner(); } - public asAad(): Uint8Array { - return toBytes(this.toJson()); - } - public equals(other: ConditionExpression): boolean { return [ this.version === other.version, diff --git a/src/conditions/condition.ts b/src/conditions/condition.ts index b16444ef1..26f08618d 100644 --- a/src/conditions/condition.ts +++ b/src/conditions/condition.ts @@ -75,6 +75,6 @@ export class Condition { } public equals(other: Condition) { - return objectEquals(this, other); + return objectEquals(this.toObj(), other.toObj()); } } diff --git a/src/conditions/context/context.ts b/src/conditions/context/context.ts index b32e5a4e1..c1d8426df 100644 --- a/src/conditions/context/context.ts +++ b/src/conditions/context/context.ts @@ -1,8 +1,9 @@ -import { Conditions as WASMConditions } from '@nucypher/nucypher-core'; +import { Context, Conditions as WASMConditions } from '@nucypher/nucypher-core'; import { ethers } from 'ethers'; import { fromJSON, toJSON } from '../../utils'; import { Condition } from '../condition'; +import { ConditionExpression } from '../condition-expr'; import { USER_ADDRESS_PARAM } from '../const'; import { TypedSignature, WalletAuthenticationProvider } from './providers'; @@ -31,10 +32,6 @@ export class ConditionContext { this.validate(); } - public requiresSigner(): boolean { - return this.conditions.some((cond) => cond.requiresSigner()); - } - private validate() { Object.keys(this.customParameters).forEach((key) => { if (RESERVED_CONTEXT_PARAMS.includes(key)) { @@ -49,9 +46,12 @@ export class ConditionContext { } }); - if (this.requiresSigner() && !this.signer) { + const conditionRequiresSigner = this.conditions.some((c) => + c.requiresSigner() + ); + if (conditionRequiresSigner && !this.signer) { throw new Error( - `Cannot use ${USER_ADDRESS_PARAM} as custom parameter without a signer` + `Condition contains ${USER_ADDRESS_PARAM} context variable and requires a signer to populate` ); } @@ -92,7 +92,7 @@ export class ConditionContext { if (requestedParameters.has(USER_ADDRESS_PARAM)) { if (!this.walletAuthProvider) { throw new Error( - `Cannot use ${USER_ADDRESS_PARAM} as custom parameter without a signer` + `Condition contains ${USER_ADDRESS_PARAM} context variable and requires a signer to populate` ); } parameters[USER_ADDRESS_PARAM] = @@ -120,19 +120,35 @@ export class ConditionContext { return parameters; }; - public toJson = async (): Promise => { + public async toJson(): Promise { const parameters = await this.toObj(); return toJSON(parameters); - }; + } - public withCustomParams = ( + public withCustomParams( params: Record - ): ConditionContext => { + ): ConditionContext { return new ConditionContext( this.provider, this.conditions, params, this.signer ); - }; + } + + public async toWASMContext(): Promise { + const asJson = await this.toJson(); + return new Context(asJson); + } + + public static fromConditions( + provider: ethers.providers.Provider, + conditions: WASMConditions, + signer?: ethers.Signer + ): ConditionContext { + const innerConditions = [ + ConditionExpression.fromWASMConditions(conditions).condition, + ]; + return new ConditionContext(provider, innerConditions, {}, signer); + } } diff --git a/test/integration/dkg-client.test.ts b/test/integration/dkg-client.test.ts index c3eed8e8a..b2c1a6cc6 100644 --- a/test/integration/dkg-client.test.ts +++ b/test/integration/dkg-client.test.ts @@ -29,7 +29,7 @@ describe('DkgCoordinatorAgent', () => { it('fetches participants from the coordinator', async () => { const provider = fakeProvider(SecretKey.random().toBEBytes()); - const fakeParticipants = fakeDkgParticipants(fakeRitualId); + const fakeParticipants = await fakeDkgParticipants(fakeRitualId); const getParticipantsSpy = mockGetParticipants( fakeParticipants.participants ); diff --git a/test/unit/cbd-strategy.test.ts b/test/unit/cbd-strategy.test.ts index 093dba61c..7251262b1 100644 --- a/test/unit/cbd-strategy.test.ts +++ b/test/unit/cbd-strategy.test.ts @@ -30,8 +30,8 @@ const { // Shared test variables const aliceSecretKey = SecretKey.fromBEBytes(aliceSecretKeyBytes); -const aliceSigner = fakeSigner(aliceSecretKey.toBEBytes()); const aliceProvider = fakeProvider(aliceSecretKey.toBEBytes()); +const aliceSigner = fakeSigner(aliceSecretKey.toBEBytes()); const ownsNFT = new ERC721Ownership({ contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', parameters: [3591], @@ -54,10 +54,9 @@ async function makeDeployedCbdStrategy() { const mockedDkg = fakeDkgFlow(variant, 0, 4, 4); const mockedDkgRitual = fakeDkgRitual(mockedDkg); - const web3Provider = fakeProvider(aliceSecretKey.toBEBytes()); const getUrsulasSpy = mockGetUrsulas(ursulas); const getExistingRitualSpy = mockGetExistingRitual(mockedDkgRitual); - const deployedStrategy = await strategy.deploy(web3Provider, ritualId); + const deployedStrategy = await strategy.deploy(aliceProvider, ritualId); expect(getUrsulasSpy).toHaveBeenCalled(); expect(getExistingRitualSpy).toHaveBeenCalled(); @@ -104,20 +103,19 @@ describe('CbdDeployedStrategy', () => { const { mockedDkg, deployedStrategy } = await makeDeployedCbdStrategy(); const message = 'this is a secret'; - const { ciphertext, aad } = deployedStrategy + const thresholdMessageKit = deployedStrategy .makeEncrypter(conditionExpr) .encryptMessageCbd(message); // Setup mocks for `retrieveAndDecrypt` - const { decryptionShares } = fakeTDecFlow({ + const { decryptionShares } = await fakeTDecFlow({ ...mockedDkg, message: toBytes(message), - aad, - ciphertext, + dkgPublicKey: mockedDkg.dkg.publicKey(), + thresholdMessageKit, }); - const { participantSecrets, participants } = fakeDkgParticipants( - mockedDkg.ritualId, - variant + const { participantSecrets, participants } = await fakeDkgParticipants( + mockedDkg.ritualId ); const requesterSessionKey = SessionStaticSecret.random(); const decryptSpy = mockCbdDecrypt( @@ -133,9 +131,8 @@ describe('CbdDeployedStrategy', () => { const decryptedMessage = await deployedStrategy.decrypter.retrieveAndDecrypt( aliceProvider, - aliceSigner, - conditionExpr, - ciphertext + thresholdMessageKit, + aliceSigner ); expect(getUrsulasSpy).toHaveBeenCalled(); expect(getParticipantsSpy).toHaveBeenCalled(); diff --git a/test/unit/conditions/condition-expr.test.ts b/test/unit/conditions/condition-expr.test.ts index ff53730e1..f7d11fe2c 100644 --- a/test/unit/conditions/condition-expr.test.ts +++ b/test/unit/conditions/condition-expr.test.ts @@ -188,6 +188,13 @@ describe('condition set', () => { expect(conditionExprFromJson.equals(conditionExprFromJson)).toBeTruthy(); }); + it('serializes to and from WASM conditions', () => { + const conditionExpr = new ConditionExpression(erc721BalanceCondition); + const wasmConditions = conditionExpr.toWASMConditions(); + const fromWasm = ConditionExpression.fromWASMConditions(wasmConditions); + expect(conditionExpr.equals(fromWasm)).toBeTruthy(); + }); + it('incompatible version', () => { const currentVersion = new SemVer(ConditionExpression.VERSION); const invalidVersion = currentVersion.inc('major'); diff --git a/test/unit/conditions/context.test.ts b/test/unit/conditions/context.test.ts index 3f0f9e501..4a0815912 100644 --- a/test/unit/conditions/context.test.ts +++ b/test/unit/conditions/context.test.ts @@ -90,20 +90,32 @@ describe('context parameters', () => { }; const condition = new ContractCondition(conditionObj); const conditionExpr = new ConditionExpression(condition); - const conditionContext = conditionExpr.buildContext(provider, {}, signer); expect(conditionExpr.contextRequiresSigner()).toBe(true); - expect(conditionContext.requiresSigner()).toBe(true); }); it('detects if a signer is not required', () => { const condition = new RpcCondition(testRpcConditionObj); const conditionExpr = new ConditionExpression(condition); - const conditionContext = conditionExpr.buildContext(provider, {}, signer); expect(JSON.stringify(condition.toObj()).includes(USER_ADDRESS_PARAM)).toBe( false ); expect(conditionExpr.contextRequiresSigner()).toBe(false); - expect(conditionContext.requiresSigner()).toBe(false); + }); + + it('rejects on a missing signer', () => { + const conditionObj = { + ...testContractConditionObj, + returnValueTest: { + ...testReturnValueTest, + value: USER_ADDRESS_PARAM, + }, + }; + const condition = new ContractCondition(conditionObj); + const conditionExpr = new ConditionExpression(condition); + expect(conditionExpr.contextRequiresSigner()).toBe(true); + expect(() => conditionExpr.buildContext(provider, {}, undefined)).toThrow( + `Condition contains ${USER_ADDRESS_PARAM} context variable and requires a signer to populate` + ); }); describe('custom method parameters', () => { diff --git a/test/utils.ts b/test/utils.ts index dad688591..11f8b6714 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -7,16 +7,14 @@ import { AggregatedTranscript, Capsule, CapsuleFrag, - Ciphertext, combineDecryptionSharesSimple, DecryptionSharePrecomputed, DecryptionShareSimple, - decryptWithSharedSecret, Dkg, + DkgPublicKey, EncryptedThresholdDecryptionResponse, EncryptedTreasureMap, EthereumAddress, - ferveoEncrypt, FerveoVariant, Keypair, PublicKey, @@ -26,6 +24,7 @@ import { SessionStaticKey, SessionStaticSecret, ThresholdDecryptionResponse, + ThresholdMessageKit, Transcript, Validator, ValidatorMessage, @@ -36,13 +35,15 @@ import axios from 'axios'; import { ethers, providers, Wallet } from 'ethers'; import { keccak256 } from 'ethers/lib/utils'; -import { Alice, Bob, Cohort, RemoteBob } from '../src'; +import { Alice, Bob, Cohort, Enrico, RemoteBob } from '../src'; import { DkgCoordinatorAgent, DkgParticipant, DkgRitualState, } from '../src/agents/coordinator'; import { ThresholdDecrypter } from '../src/characters/cbd-recipient'; +import { ConditionExpression } from '../src/conditions'; +import { ERC721Balance } from '../src/conditions/predefined'; import { DkgClient, DkgRitual } from '../src/dkg'; import { BlockchainPolicy, PreEnactedPolicy } from '../src/policies/policy'; import { @@ -55,6 +56,8 @@ import { import { ChecksumAddress } from '../src/types'; import { toBytes, toHexString, zip } from '../src/utils'; +import { TEST_CHAIN_ID, TEST_CONTRACT_ADDR } from './unit/testVariables'; + export const bytesEqual = (first: Uint8Array, second: Uint8Array): boolean => first.length === second.length && first.every((value, index) => value === second[index]); @@ -304,22 +307,21 @@ interface FakeDkgRitualFlow { sharesNum: number; threshold: number; receivedMessages: ValidatorMessage[]; - ciphertext: Ciphertext; - aad: Uint8Array; dkg: Dkg; message: Uint8Array; + dkgPublicKey: DkgPublicKey; + thresholdMessageKit: ThresholdMessageKit; } -export const fakeTDecFlow = ({ +export const fakeTDecFlow = async ({ validators, validatorKeypairs, ritualId, sharesNum, threshold, receivedMessages, - ciphertext, - aad, message, + thresholdMessageKit, }: FakeDkgRitualFlow) => { // Having aggregated the transcripts, the validators can now create decryption shares const decryptionShares: ( @@ -336,56 +338,68 @@ export const fakeTDecFlow = ({ const decryptionShare = aggregate.createDecryptionShareSimple( dkg, - ciphertext.header, - aad, + thresholdMessageKit.ciphertextHeader, + thresholdMessageKit.acp.aad(), keypair ); decryptionShares.push(decryptionShare); }); - // Now, the decryption share can be used to decrypt the ciphertext - // This part is in the client API const sharedSecret = combineDecryptionSharesSimple(decryptionShares); - // The client should have access to the public parameters of the DKG - const plaintext = decryptWithSharedSecret(ciphertext, aad, sharedSecret); + const plaintext = thresholdMessageKit.decryptWithSharedSecret(sharedSecret); if (!bytesEqual(plaintext, message)) { throw new Error('Decryption failed'); } - return { decryptionShares, sharedSecret, plaintext }; + return { + decryptionShares, + plaintext, + sharedSecret, + thresholdMessageKit, + }; }; -export const fakeDkgTDecFlowE2e = ( - variant: FerveoVariant, - message = toBytes('fake-message'), - aad = toBytes('fake-aad'), +const fakeConditionExpr = () => { + const erc721Balance = new ERC721Balance({ + chain: TEST_CHAIN_ID, + contractAddress: TEST_CONTRACT_ADDR, + }); + return new ConditionExpression(erc721Balance); +}; + +export const fakeDkgTDecFlowE2E = async ( ritualId = 0, + variant: FerveoVariant = FerveoVariant.precomputed, + conditionExpr: ConditionExpression = fakeConditionExpr(), + message = toBytes('fake-message'), sharesNum = 4, threshold = 4 ) => { const ritual = fakeDkgFlow(variant, ritualId, sharesNum, threshold); + const dkgPublicKey = ritual.dkg.publicKey(); + const thresholdMessageKit = new Enrico(dkgPublicKey).encryptMessageCbd( + message, + conditionExpr + ); - // In the meantime, the client creates a ciphertext and decryption request - const ciphertext = ferveoEncrypt(message, aad, ritual.dkg.publicKey()); - const { decryptionShares } = fakeTDecFlow({ + const { decryptionShares } = await fakeTDecFlow({ ...ritual, - ciphertext, - aad, message, + dkgPublicKey, + thresholdMessageKit, }); return { ...ritual, message, - aad, - ciphertext, decryptionShares, + thresholdMessageKit, }; }; -export const fakeCoordinatorRitual = ( +export const fakeCoordinatorRitual = async ( ritualId: number -): { +): Promise<{ aggregationMismatch: boolean; initTimestamp: number; aggregatedTranscriptHash: string; @@ -397,8 +411,8 @@ export const fakeCoordinatorRitual = ( aggregatedTranscript: string; publicKeyHash: string; totalAggregations: number; -} => { - const ritual = fakeDkgTDecFlowE2e(FerveoVariant.precomputed); +}> => { + const ritual = await fakeDkgTDecFlowE2E(); const dkgPkBytes = ritual.dkg.publicKey().toBytes(); return { id: ritualId, @@ -418,14 +432,13 @@ export const fakeCoordinatorRitual = ( }; }; -export const fakeDkgParticipants = ( - ritualId: number, - variant = FerveoVariant.precomputed -): { +export const fakeDkgParticipants = async ( + ritualId: number +): Promise<{ participants: DkgParticipant[]; participantSecrets: Record; -} => { - const ritual = fakeDkgTDecFlowE2e(variant); +}> => { + const ritual = await fakeDkgTDecFlowE2E(ritualId); const label = toBytes(`${ritualId}`); const participantSecrets: Record = diff --git a/yarn.lock b/yarn.lock index 7a7ef1720..c495cb6b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1818,10 +1818,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@nucypher/nucypher-core@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@nucypher/nucypher-core/-/nucypher-core-0.12.0.tgz#0274026f3996601994e9639c06ab79c62c8f5d7c" - integrity sha512-hEjjnTSLNqHUiIF6U02j+M8jUOBCOt5mGG78lMHoathbak15AmYX4gkdkHTg2Ssvt5o77Cwzsn5YgGM4Haf7AQ== +"@nucypher/nucypher-core@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@nucypher/nucypher-core/-/nucypher-core-0.13.0.tgz#071263931d4e9604b428ea738ebe8ee5e1f302f3" + integrity sha512-HfEbrQvngOHIn0bMAdqPIF7WzkLdb5+sMhmX7bQl2SINABQ6FGGN8G+Arb+pbkYgru5qeQ+RTTbCBwBNaxFKEg== "@sinclair/typebox@^0.27.8": version "0.27.8"