From 0536f0390ae362604f7411c1733a7c4d7a985bb9 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Tue, 19 Nov 2024 15:23:50 -0700 Subject: [PATCH 1/2] feat(jseql): implement eql plaintest schema --- .changeset/lemon-bananas-design.md | 5 ++ bun.lockb | Bin 129560 -> 129560 bytes packages/jseql/cs_plaintext_v1.schema.json | 58 +++++++++++++++ packages/jseql/generateSchema.ts | 17 +++++ packages/jseql/package.json | 2 +- packages/jseql/src/cs_encrypted_v1.ts | 57 --------------- packages/jseql/src/cs_plaintext_v1.ts | 33 +++++++++ packages/jseql/src/index.ts | 61 ++++++++++------ packages/jseql/tests/jseql_test.ts | 81 ++++++++++----------- 9 files changed, 193 insertions(+), 121 deletions(-) create mode 100644 .changeset/lemon-bananas-design.md create mode 100644 packages/jseql/cs_plaintext_v1.schema.json create mode 100644 packages/jseql/generateSchema.ts delete mode 100644 packages/jseql/src/cs_encrypted_v1.ts create mode 100644 packages/jseql/src/cs_plaintext_v1.ts diff --git a/.changeset/lemon-bananas-design.md b/.changeset/lemon-bananas-design.md new file mode 100644 index 0000000..a57461f --- /dev/null +++ b/.changeset/lemon-bananas-design.md @@ -0,0 +1,5 @@ +--- +"@cipherstash/jseql": minor +--- + +Implemented new CsPlaintextV1Schema type and schema. diff --git a/bun.lockb b/bun.lockb index 76e04d6470e51b50be13f084499492ee96b01bbc..8e1ab01301bc7489a9da161b468c5d17298330fe 100755 GIT binary patch delta 40 ycmV+@0N4MR^aq&q2Y|Ezqg@CAUNSB+H!eB1t6c%bnv>KRER*1;HMi0D0Y?E+{Segv delta 36 ocmbR7g?+{s_J%Eti(@&M7$9K#>R863vnC(=k+}Wh4@P%J0Qfi#00000 diff --git a/packages/jseql/cs_plaintext_v1.schema.json b/packages/jseql/cs_plaintext_v1.schema.json new file mode 100644 index 0000000..de16fed --- /dev/null +++ b/packages/jseql/cs_plaintext_v1.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "The EQL plaintext JSON payload sent by a client (such as an application) to CipherStash Proxy.", + "type": "object", + "properties": { + "v": { + "title": "Schema version", + "type": "integer" + }, + "k": { + "title": "kind", + "type": "string", + "const": "pt" + }, + "i": { + "title": "ident", + "type": "object", + "properties": { + "t": { + "title": "table", + "type": "string", + "pattern": "^[a-zA-Z_]{1}[0-9a-zA-Z_]*$" + }, + "c": { + "title": "column", + "type": "string", + "pattern": "^[a-zA-Z_]{1}[0-9a-zA-Z_]*$" + } + }, + "required": [ + "t", + "c" + ] + }, + "p": { + "title": "plaintext", + "type": "string" + }, + "q": { + "title": "for query", + "description": "Specifies that the plaintext should be encrypted for a specific query operation. If null, source encryption and encryption for all indexes will be performed.", + "type": "string", + "enum": [ + "match", + "ore", + "unique", + "ste_vec", + "ejson_path" + ] + } + }, + "required": [ + "v", + "k", + "i", + "p" + ] +} \ No newline at end of file diff --git a/packages/jseql/generateSchema.ts b/packages/jseql/generateSchema.ts new file mode 100644 index 0000000..a3a2768 --- /dev/null +++ b/packages/jseql/generateSchema.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env bun + +// Fetch the latest schemas from https://github.com/cipherstash/encrypt-query-language/tree/main/sql/schemas +// Write file to ./cs_encrypted_v1.schema.json +import { $ } from 'bun' + +const url = + 'https://raw.githubusercontent.com/cipherstash/encrypt-query-language/main/sql/schemas/cs_plaintext_v1.schema.json' +const response = await fetch(url) +const data = await response.json() + +console.log(JSON.stringify(data, null, 2)) +await Bun.write('./cs_plaintext_v1.schema.json', JSON.stringify(data, null, 2)) + +await $`bun run generate-types` + +console.log('Types generated!') diff --git a/packages/jseql/package.json b/packages/jseql/package.json index 54f451f..c8cea96 100644 --- a/packages/jseql/package.json +++ b/packages/jseql/package.json @@ -35,7 +35,7 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "generate-types": "json2ts ../../../cs_encrypted_v1.schema.json --output ./cs_encrypted_v1.ts", + "generate-types": "json2ts ./cs_plaintext_v1.schema.json --output ./src/cs_plaintext_v1.ts", "test": "bun test tests" }, "devDependencies": { diff --git a/packages/jseql/src/cs_encrypted_v1.ts b/packages/jseql/src/cs_encrypted_v1.ts deleted file mode 100644 index 03cc710..0000000 --- a/packages/jseql/src/cs_encrypted_v1.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable */ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. - */ - -export type CsEncryptedV1Schema = ( - | { - k?: 'pt' - [k: string]: unknown - } - | { - k?: 'ct' - [k: string]: unknown - } - | { - k?: 'en' - [k: string]: unknown - } -) & { - s?: SchemaVersion - v: ConfigurationVersion - k: Kind - i?: Ident - p?: Plaintext - c?: Ciphertext - 'u.1'?: UniqueIndex - 'o.1'?: OreIndex - 'm.1'?: MatchIndex - [k: string]: unknown -} -/** - * The schema version of this json document - */ -export type SchemaVersion = 1 -export type ConfigurationVersion = number -export type Kind = 'pt' | 'ct' | 'en' -export type Table = string -export type Column = string -export type Plaintext = string -export type Ciphertext = string -export type UniqueIndex = string -/** - * @minItems 1 - */ -export type OreIndex = [string, ...string[]] -/** - * @minItems 1 - */ -export type MatchIndex = [number, ...number[]] - -export interface Ident { - t: Table - c: Column - [k: string]: unknown -} diff --git a/packages/jseql/src/cs_plaintext_v1.ts b/packages/jseql/src/cs_plaintext_v1.ts new file mode 100644 index 0000000..37c80d5 --- /dev/null +++ b/packages/jseql/src/cs_plaintext_v1.ts @@ -0,0 +1,33 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type SchemaVersion = number +export type Kind = 'pt' +export type Table = string +export type Column = string +export type Plaintext = string +/** + * Specifies that the plaintext should be encrypted for a specific query operation. If null, source encryption and encryption for all indexes will be performed. + */ +export type ForQuery = 'match' | 'ore' | 'unique' | 'ste_vec' | 'ejson_path' + +/** + * The EQL plaintext JSON payload sent by a client (such as an application) to CipherStash Proxy. + */ +export interface CsPlaintextV1Schema { + v: SchemaVersion + k: Kind + i: Ident + p: Plaintext + q?: ForQuery + [k: string]: unknown +} +export interface Ident { + t: Table + c: Column + [k: string]: unknown +} diff --git a/packages/jseql/src/index.ts b/packages/jseql/src/index.ts index 1b578a3..43bb810 100644 --- a/packages/jseql/src/index.ts +++ b/packages/jseql/src/index.ts @@ -1,49 +1,68 @@ -export * from './cs_encrypted_v1' -import type { CsEncryptedV1Schema } from './cs_encrypted_v1' +export * from './cs_plaintext_v1' +import type { + CsPlaintextV1Schema, + ForQuery, + SchemaVersion, + Kind, + Table, + Column, + Plaintext, +} from './cs_plaintext_v1' import { getLogger } from '@logtape/logtape' const logger = getLogger(['jseql']) -type CreateEqlPayload = { - plaintext: string | undefined - table: string - column: string - version?: number - schemaVersion?: number - queryType?: string | null +export type CreateEqlPayload = { + plaintext: Plaintext + table: Table + column: Column + schemaVersion?: SchemaVersion + queryType?: ForQuery | null +} + +export type Result = { + failure?: boolean + error?: Error + plaintext?: Plaintext } export const createEqlPayload = ({ plaintext, table, column, - version = 1, + schemaVersion = 1, queryType = null, -}: CreateEqlPayload): CsEncryptedV1Schema => { - const payload: CsEncryptedV1Schema = { - v: version, - s: 1, +}: CreateEqlPayload): CsPlaintextV1Schema => { + const payload: CsPlaintextV1Schema = { + v: schemaVersion, k: 'pt', p: plaintext ?? '', i: { t: table, c: column, }, - q: queryType, + } + + if (queryType) { + payload.q = queryType } logger.debug('Creating the EQL payload', payload) return payload } -export const getPlaintext = ( - payload: CsEncryptedV1Schema | null, -): string | undefined => { - if (payload?.k === 'pt') { +export const getPlaintext = (payload: CsPlaintextV1Schema): Result => { + if (payload?.p && payload?.k === 'pt') { logger.debug('Returning the plaintext data from the EQL payload', payload) - return payload.p + return { + failure: false, + plaintext: payload.p, + } } logger.error('No plaintext data found in the EQL payload', payload ?? {}) - return undefined + return { + failure: true, + error: new Error('No plaintext data found in the EQL payload'), + } } diff --git a/packages/jseql/tests/jseql_test.ts b/packages/jseql/tests/jseql_test.ts index 6cc4490..af90f85 100644 --- a/packages/jseql/tests/jseql_test.ts +++ b/packages/jseql/tests/jseql_test.ts @@ -1,20 +1,9 @@ -import { configure, getConsoleSink, getFileSink } from '@logtape/logtape' -import { describe, expect, it, beforeEach } from 'bun:test' -import { createEqlPayload, getPlaintext } from '../src/index' -import type { CsEncryptedV1Schema } from '../src/cs_encrypted_v1' - -await configure({ - sinks: { - console: getConsoleSink(), - }, - loggers: [ - { - category: ['jseql'], - level: 'debug', - sinks: ['console'], - }, - ], -}) +import { describe, expect, it } from 'bun:test' +import { createEqlPayload, getPlaintext } from '../src' +import type { CsPlaintextV1Schema } from '../src/cs_plaintext_v1' +import { getLogger } from '@logtape/logtape' + +const logger = getLogger(['jseql']) describe('createEqlPayload', () => { it('should create a payload with the correct default values', () => { @@ -24,40 +13,37 @@ describe('createEqlPayload', () => { column: 'email', }) - const expectedPayload: CsEncryptedV1Schema = { + const expectedPayload: CsPlaintextV1Schema = { v: 1, - s: 1, k: 'pt', p: 'test', i: { t: 'users', c: 'email', }, - q: null, } expect(result).toEqual(expectedPayload) }) - it('should set custom version and queryType values when provided', () => { + it('should set custom schemaVersion and queryType values when provided', () => { const result = createEqlPayload({ plaintext: 'test', table: 'users', column: 'email', - version: 2, - queryType: 'SELECT', + schemaVersion: 2, + queryType: 'match', }) - const expectedPayload: CsEncryptedV1Schema = { + const expectedPayload: CsPlaintextV1Schema = { v: 2, - s: 1, k: 'pt', p: 'test', i: { t: 'users', c: 'email', }, - q: 'SELECT', + q: 'match', } expect(result).toEqual(expectedPayload) @@ -65,7 +51,7 @@ describe('createEqlPayload', () => { it('should set plaintext to an empty string if undefined', () => { const result = createEqlPayload({ - plaintext: undefined, + plaintext: '', table: 'users', column: 'email', }) @@ -75,42 +61,53 @@ describe('createEqlPayload', () => { }) describe('getPlaintext', () => { - it('should return plaintext if payload has "pt" as key', () => { - const payload: CsEncryptedV1Schema = { + it('should return plaintext if payload is valid and key is "pt"', () => { + const payload: CsPlaintextV1Schema = { v: 1, - s: 1, k: 'pt', p: 'test', i: { t: 'users', c: 'email', }, - q: null, } const result = getPlaintext(payload) - expect(result).toBe('test') - }) - it('should return undefined and log error if payload is null', () => { - const result = getPlaintext(null) - expect(result).toBeUndefined() + expect(result).toEqual({ + failure: false, + plaintext: 'test', + }) }) - it('should return undefined and log error if key is not "pt"', () => { - const payload: CsEncryptedV1Schema = { + it('should return an error if payload is missing "p" or key is not "pt"', () => { + const invalidPayload = { v: 1, - s: 1, k: 'ct', c: 'ciphertext', + p: '', i: { t: 'users', c: 'email', }, - q: null, } - const result = getPlaintext(payload) - expect(result).toBeUndefined() + const result = getPlaintext( + invalidPayload as unknown as CsPlaintextV1Schema, + ) + + expect(result).toEqual({ + failure: true, + error: new Error('No plaintext data found in the EQL payload'), + }) + }) + + it('should return an error and log if payload is invalid', () => { + const result = getPlaintext(null as unknown as CsPlaintextV1Schema) + + expect(result).toEqual({ + failure: true, + error: new Error('No plaintext data found in the EQL payload'), + }) }) }) From e02edaf0878211692b4a9bd88a0f3cdf64ded8d5 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 20 Nov 2024 14:46:15 -0700 Subject: [PATCH 2/2] docs: update schema types --- README.md | 4 ++-- packages/jseql/generateSchema.ts | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f5b3b90..40a8955 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ bun add @cipherstash/jseql import { createEqlPayload, getPlaintext, - type CsEncryptedV1Schema, + type CsPlaintextV1Schema, } from '@cipherstash/jseql' ``` @@ -92,7 +92,7 @@ Extracts the plaintext data from an EQL payload. **Parameters:** -- `payload` (`CsEncryptedV1Schema` | null): The EQL payload. +- `payload` (`CsPlaintextV1Schema` | null): The EQL payload. **Returns:** diff --git a/packages/jseql/generateSchema.ts b/packages/jseql/generateSchema.ts index a3a2768..c6c0992 100644 --- a/packages/jseql/generateSchema.ts +++ b/packages/jseql/generateSchema.ts @@ -1,7 +1,4 @@ #!/usr/bin/env bun - -// Fetch the latest schemas from https://github.com/cipherstash/encrypt-query-language/tree/main/sql/schemas -// Write file to ./cs_encrypted_v1.schema.json import { $ } from 'bun' const url =