@prisma-next/extension-cipherstash 0.0.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/README.md +153 -0
- package/dist/call-classes-CSvD7w8U.mjs +206 -0
- package/dist/call-classes-CSvD7w8U.mjs.map +1 -0
- package/dist/column-types.d.mts +33 -0
- package/dist/column-types.d.mts.map +1 -0
- package/dist/column-types.mjs +42 -0
- package/dist/column-types.mjs.map +1 -0
- package/dist/constants-BDxL9Pe3.d.mts +22 -0
- package/dist/constants-BDxL9Pe3.d.mts.map +1 -0
- package/dist/constants-B_2TNvUi.mjs +46 -0
- package/dist/constants-B_2TNvUi.mjs.map +1 -0
- package/dist/control.d.mts +7 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +430 -0
- package/dist/control.mjs.map +1 -0
- package/dist/descriptor-meta-BgQfZTAF.mjs +129 -0
- package/dist/descriptor-meta-BgQfZTAF.mjs.map +1 -0
- package/dist/envelope-P9BxfJNr.mjs +271 -0
- package/dist/envelope-P9BxfJNr.mjs.map +1 -0
- package/dist/middleware.d.mts +13 -0
- package/dist/middleware.d.mts.map +1 -0
- package/dist/middleware.mjs +129 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/migration.d.mts +141 -0
- package/dist/migration.d.mts.map +1 -0
- package/dist/migration.mjs +2 -0
- package/dist/operation-types.d.mts +49 -0
- package/dist/operation-types.d.mts.map +1 -0
- package/dist/operation-types.mjs +1 -0
- package/dist/pack.d.mts +86 -0
- package/dist/pack.d.mts.map +1 -0
- package/dist/pack.mjs +2 -0
- package/dist/runtime.d.mts +207 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +429 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/sdk-D5FTGyzp.d.mts +67 -0
- package/dist/sdk-D5FTGyzp.d.mts.map +1 -0
- package/package.json +69 -0
- package/src/contract/authoring.ts +62 -0
- package/src/contract/contract.d.ts +149 -0
- package/src/contract/contract.json +104 -0
- package/src/contract/contract.prisma +46 -0
- package/src/execution/abort.ts +143 -0
- package/src/execution/codec-runtime.ts +209 -0
- package/src/execution/decrypt-all.ts +217 -0
- package/src/execution/envelope.ts +263 -0
- package/src/execution/operators.ts +211 -0
- package/src/execution/parameterized.ts +71 -0
- package/src/execution/routing.ts +93 -0
- package/src/execution/sdk.ts +68 -0
- package/src/exports/column-types.ts +62 -0
- package/src/exports/contract-space-typing.ts +86 -0
- package/src/exports/control.ts +120 -0
- package/src/exports/middleware.ts +24 -0
- package/src/exports/migration.ts +43 -0
- package/src/exports/operation-types.ts +16 -0
- package/src/exports/pack.ts +13 -0
- package/src/exports/runtime.ts +110 -0
- package/src/extension-metadata/codec-metadata.ts +81 -0
- package/src/extension-metadata/constants.ts +70 -0
- package/src/extension-metadata/descriptor-meta.ts +76 -0
- package/src/middleware/bulk-encrypt.ts +192 -0
- package/src/migration/call-classes.ts +350 -0
- package/src/migration/cipherstash-codec.ts +157 -0
- package/src/migration/eql-bundle.ts +29 -0
- package/src/migration/eql-install.generated.ts +5751 -0
- package/src/types/operation-types.ts +81 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.mjs","names":["baselineMetadata","baselineOps","contractJson"],"sources":["../migrations/cipherstash/20260601T0000_install_eql_bundle/migration.json","../migrations/cipherstash/20260601T0000_install_eql_bundle/ops.json","../migrations/cipherstash/refs/head.json","../src/contract/contract.json","../src/migration/cipherstash-codec.ts","../src/exports/contract-space-typing.ts","../src/exports/control.ts"],"sourcesContent":["","","","","/**\n * Control hooks for the `cipherstash:string@1` codec.\n *\n * Implements `CodecControlHooks.onFieldEvent`. Reacts to per-field\n * added / dropped / altered events as the *application* emitter diffs\n * the prior contract against the new contract; the returned Calls flow\n * through the SQL planner's IR alongside structural DDL and render as\n * `cipherstashAddSearchConfig({...})` / `cipherstashRemoveSearchConfig({...})`\n * calls in the user's `migration.ts` (ADR 195 two-renderer pattern).\n *\n * Trigger: a field uses the `cipherstash:string@1` codec. The planner\n * already dispatches per `(table, field)` based on the field's\n * `codecId` (new field for `'added'` / `'altered'`, prior field for\n * `'dropped'`), so this hook only fires when a cipherstash field is\n * involved. Per field the hook emits **one\n * `cipherstashAddSearchConfig` Call per enabled flag** in `typeParams`\n * (and one `cipherstashRemoveSearchConfig` Call per previously-enabled\n * flag on drop / altered-off).\n *\n * Flag → EQL index mapping:\n *\n * - `equality: true` → `'unique'` index\n * - `freeTextSearch: true` → `'match'` index\n *\n * One Call per flag (rather than a single multi-statement Call per\n * field) keeps each Call independently invertible by a paired\n * `cipherstashRemoveSearchConfig` Call carrying the same index name,\n * and the op-graph stays per-flag granular for diffing.\n *\n * `'altered'` events decompose into per-flag deltas:\n * - flag flipped on → emit `cipherstashAddSearchConfig({...})`.\n * - flag flipped off → emit `cipherstashRemoveSearchConfig({...})`.\n * - flag unchanged → no Call.\n *\n * `invariantId` template (carried on the Call's `toOp()` output):\n * `cipherstash-codec:<table>.<field>:<action>:<index>@v1`\n * `<action>` ∈ `'add-search-config' | 'remove-search-config'`,\n * `<index>` ∈ `'unique' | 'match'`.\n * Stable across regenerations because every input is deterministic.\n */\n\nimport type { CodecControlHooks, FieldEventContext } from '@prisma-next/family-sql/control';\nimport type { OpFactoryCall } from '@prisma-next/framework-components/control';\nimport { CIPHERSTASH_STRING_CODEC_ID } from '../extension-metadata/constants';\nimport {\n type CipherstashSearchIndex,\n cipherstashAddSearchConfig,\n cipherstashRemoveSearchConfig,\n} from './call-classes';\n\ntype FlagName = 'equality' | 'freeTextSearch';\n\nconst FLAG_TO_INDEX: Readonly<Record<FlagName, CipherstashSearchIndex>> = {\n equality: 'unique',\n freeTextSearch: 'match',\n};\n\nconst ALL_FLAGS: ReadonlyArray<FlagName> = ['equality', 'freeTextSearch'];\n\nfunction isEnabled(\n typeParams: Readonly<Record<string, unknown>> | undefined,\n flag: FlagName,\n): boolean {\n return typeParams !== undefined && typeParams[flag] === true;\n}\n\n/**\n * Hook entry point. Called by `planFieldEventOperations` for every per-\n * field delta dispatched to `cipherstash:string@1`. Pure and\n * synchronous; callers replay it deterministically when re-emitting.\n */\nfunction onFieldEvent(\n event: 'added' | 'dropped' | 'altered',\n ctx: FieldEventContext,\n): readonly OpFactoryCall[] {\n const { tableName, fieldName, priorField, newField } = ctx;\n\n if (event === 'added') {\n if (newField === undefined) return [];\n const calls: OpFactoryCall[] = [];\n for (const flag of ALL_FLAGS) {\n if (isEnabled(newField.typeParams, flag)) {\n calls.push(\n cipherstashAddSearchConfig({\n table: tableName,\n column: fieldName,\n index: FLAG_TO_INDEX[flag],\n }),\n );\n }\n }\n return calls;\n }\n\n if (event === 'dropped') {\n if (priorField === undefined) return [];\n const calls: OpFactoryCall[] = [];\n for (const flag of ALL_FLAGS) {\n if (isEnabled(priorField.typeParams, flag)) {\n calls.push(\n cipherstashRemoveSearchConfig({\n table: tableName,\n column: fieldName,\n index: FLAG_TO_INDEX[flag],\n }),\n );\n }\n }\n return calls;\n }\n\n if (priorField === undefined || newField === undefined) return [];\n const calls: OpFactoryCall[] = [];\n for (const flag of ALL_FLAGS) {\n const before = isEnabled(priorField.typeParams, flag);\n const after = isEnabled(newField.typeParams, flag);\n if (after && !before) {\n calls.push(\n cipherstashAddSearchConfig({\n table: tableName,\n column: fieldName,\n index: FLAG_TO_INDEX[flag],\n }),\n );\n } else if (before && !after) {\n calls.push(\n cipherstashRemoveSearchConfig({\n table: tableName,\n column: fieldName,\n index: FLAG_TO_INDEX[flag],\n }),\n );\n }\n }\n return calls;\n}\n\n/**\n * The DDL type for an `Encrypted<string>` column is always\n * `eql_v2_encrypted` regardless of any `typeParams` flags: the\n * search-config wiring is delivered by the codec hook's\n * `cipherstashAddSearchConfig` Calls (separate rows in\n * `eql_v2_configuration`), not by the column type itself. Returning\n * `nativeType` unchanged tells the planner \"no expansion required\" —\n * see `expandParameterizedTypeSql` in\n * `packages/3-targets/3-targets/postgres/src/core/migrations/planner-ddl-builders.ts`,\n * which only requires this hook to *exist* for any column carrying\n * `typeParams`. Without it, the planner refuses to render the column\n * (the existing arktype-json extension wires the same identity hook).\n */\nconst expandNativeType: NonNullable<CodecControlHooks['expandNativeType']> = ({ nativeType }) =>\n nativeType;\n\nexport const cipherstashStringCodecHooks: CodecControlHooks = { onFieldEvent, expandNativeType };\n\n/** Re-export the codec id alongside the hooks so wiring sites import them together. */\nexport { CIPHERSTASH_STRING_CODEC_ID };\n","/**\n * Typed-narrowing helpers for the on-disk contract-space JSON artefacts\n * the cipherstash control descriptor wires into its\n * `SqlControlExtensionDescriptor`.\n *\n * JSON-imported values come back as widened, structurally-typed\n * objects: branded fields (`storageHash: StorageHashBase<string>`) and\n * discriminated unions (`MigrationPlanOperation['operationClass']`)\n * fall back to plain strings, so a direct assignment into the\n * descriptor surfaces is a type error. The cipherstash MVP previously\n * suppressed that error with `as unknown as X` triple-casts, which\n * silently masks any future shape drift between the emitted JSON and\n * the in-package descriptor.\n *\n * This module replaces the blind casts with thin runtime assertions\n * that fail fast on drift and narrow the JSON inputs to the framework\n * types in a single, auditable place. The assertions are intentionally\n * minimal — they check the canonical discriminator fields (`storageHash`,\n * `space`, `dirName`, `operationClass`, …) rather than re-validating\n * the whole emitter contract — which is enough to surface schema-level\n * drift while keeping the descriptor module light.\n */\n\nimport type { Contract } from '@prisma-next/contract/types';\nimport type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\n\nfunction fail(field: string, value: unknown): never {\n throw new Error(\n `cipherstash contract-space JSON is missing or malformed at \"${field}\" (saw ${typeof value}). The on-disk JSON drifted from the framework's expected shape — re-run \\`prisma-next contract emit\\` and \\`prisma-next migration plan\\` for the cipherstash space.`,\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * Narrow a JSON-imported `contract.json` value to `Contract<SqlStorage>`.\n * Checks the discriminators the framework relies on at descriptor\n * registration time; everything else is consumed downstream by the\n * runner / verifier, which performs its own validation.\n */\nexport function asCipherstashContract(value: unknown): Contract<SqlStorage> {\n if (!isRecord(value)) fail('<root>', value);\n if (typeof value['target'] !== 'string') fail('target', value['target']);\n if (typeof value['targetFamily'] !== 'string') fail('targetFamily', value['targetFamily']);\n const storage = value['storage'];\n if (!isRecord(storage)) fail('storage', storage);\n if (typeof storage['storageHash'] !== 'string')\n fail('storage.storageHash', storage['storageHash']);\n return value as unknown as Contract<SqlStorage>;\n}\n\n/**\n * Narrow a JSON-imported `migration.json` value to `MigrationMetadata`.\n * The framework's runner consumes the metadata for ordering /\n * provenance; missing `to` or a non-string `migrationHash` here means\n * a non-emitted artefact slipped into the import path.\n */\nexport function asCipherstashMigrationMetadata(value: unknown): MigrationMetadata {\n if (!isRecord(value)) fail('<root>', value);\n if (typeof value['to'] !== 'string') fail('to', value['to']);\n if (typeof value['migrationHash'] !== 'string') fail('migrationHash', value['migrationHash']);\n return value as unknown as MigrationMetadata;\n}\n\n/**\n * Narrow a JSON-imported `ops.json` value to\n * `readonly MigrationPlanOperation[]`. Checks each entry carries the\n * canonical `id` / `operationClass` discriminator so a malformed entry\n * doesn't reach the planner.\n */\nexport function asCipherstashMigrationOps(value: unknown): readonly MigrationPlanOperation[] {\n if (!Array.isArray(value)) fail('<root>', value);\n for (let index = 0; index < value.length; index += 1) {\n const entry = value[index];\n if (!isRecord(entry)) fail(`[${index}]`, entry);\n if (typeof entry['id'] !== 'string') fail(`[${index}].id`, entry['id']);\n if (typeof entry['operationClass'] !== 'string') {\n fail(`[${index}].operationClass`, entry['operationClass']);\n }\n }\n return value as unknown as readonly MigrationPlanOperation[];\n}\n","/**\n * Control-plane descriptor for the CipherStash extension.\n *\n * **On-disk-in-package authoring.** The extension's contract +\n * migrations are emitted by the same pipeline application authors use:\n *\n * `prisma-next contract emit` → `<package>/src/contract/contract.{json,d.ts}`\n * `prisma-next migration plan` → `<package>/migrations/cipherstash/<dir>/...`\n *\n * The descriptor wires those JSON artefacts via JSON-import declarations\n * so they flow through the consuming application's module resolver\n * without filesystem assumptions, and synthesises the canonical\n * {@link import('@prisma-next/migration-tools/package').MigrationPackage}\n * shape (gaining `dirPath` from `import.meta.url`) for the framework's\n * runner / verifier to consume.\n *\n * Wired surfaces:\n *\n * - `contractSpace.{contractJson,migrations,headRef}` — sourced from\n * the on-disk artefacts emitted by `build:contract-space`.\n * - `types.codecTypes.controlPlaneHooks[CIPHERSTASH_STRING_CODEC_ID]`\n * — the lifecycle hook the SQL planner extracts via\n * `extractCodecControlHooks` and inlines into the application's\n * migration via `planFieldEventOperations`. Implements\n * `add_search_config` / `remove_search_config` / rotate behaviour\n * for `searchable: true` `Encrypted<string>` columns.\n *\n * @see docs/architecture docs/adrs/ADR 211 - Contract spaces.md\n * (on-disk-in-package authoring convention).\n * @see packages/3-extensions/test-contract-space/src/exports/control.ts\n * (reference model).\n */\n\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type { SqlControlExtensionDescriptor } from '@prisma-next/family-sql/control';\nimport type { ContractSpace } from '@prisma-next/framework-components/control';\nimport type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport baselineMetadata from '../../migrations/cipherstash/20260601T0000_install_eql_bundle/migration.json' with {\n type: 'json',\n};\nimport baselineOps from '../../migrations/cipherstash/20260601T0000_install_eql_bundle/ops.json' with {\n type: 'json',\n};\nimport headRef from '../../migrations/cipherstash/refs/head.json' with { type: 'json' };\nimport contractJson from '../contract/contract.json' with { type: 'json' };\nimport {\n CIPHERSTASH_BASELINE_MIGRATION_NAME,\n CIPHERSTASH_SPACE_ID,\n CIPHERSTASH_STRING_CODEC_ID,\n} from '../extension-metadata/constants';\nimport { cipherstashPackMeta } from '../extension-metadata/descriptor-meta';\nimport { cipherstashStringCodecHooks } from '../migration/cipherstash-codec';\nimport {\n asCipherstashContract,\n asCipherstashMigrationMetadata,\n asCipherstashMigrationOps,\n} from './contract-space-typing';\n\n/**\n * Resolve a migration package's on-disk path from this descriptor module's\n * URL. The framework's runner uses `dirPath` for diagnostic messages and\n * to locate sibling files (e.g. `start-contract.json` for non-baseline\n * migrations); pinning it from `import.meta.url` keeps the value correct\n * regardless of where the consuming application installs the package\n * (workspace, node_modules, bundled, etc.).\n */\nfunction resolveMigrationDirPath(dirName: string): string {\n return fileURLToPath(\n new URL(`../../migrations/${CIPHERSTASH_SPACE_ID}/${dirName}/`, import.meta.url),\n );\n}\n\nconst baselinePackage: OnDiskMigrationPackage = {\n dirName: CIPHERSTASH_BASELINE_MIGRATION_NAME,\n dirPath: resolveMigrationDirPath(CIPHERSTASH_BASELINE_MIGRATION_NAME),\n metadata: asCipherstashMigrationMetadata(baselineMetadata),\n ops: asCipherstashMigrationOps(baselineOps),\n};\n\nconst cipherstashContractSpace: ContractSpace<Contract<SqlStorage>> = {\n contractJson: asCipherstashContract(contractJson),\n migrations: [baselinePackage],\n headRef,\n};\n\nconst cipherstashExtensionDescriptor: SqlControlExtensionDescriptor<'postgres'> = {\n // Spread pack-meta first so it contributes `kind` / `id` / `familyId`\n // / `targetId` / `version` / `authoring` / `types.{codecTypes,storage}`\n // — then overlay the contract-space block and the codec lifecycle\n // hook on top. The two `types.codecTypes` slots (`codecInstances`\n // from pack-meta, `controlPlaneHooks` from this descriptor) coexist\n // on the same path and are merged below.\n ...cipherstashPackMeta,\n contractSpace: cipherstashContractSpace,\n /**\n * Free-form `types.codecTypes.controlPlaneHooks` block — the SQL\n * family's `extractCodecControlHooks` (in `@prisma-next/family-sql/\n * control`) finds hooks via duck-typing on this exact path. Mirrors\n * pgvector's wiring at `packages/3-extensions/pgvector/src/exports/\n * control.ts`.\n */\n types: {\n ...cipherstashPackMeta.types,\n codecTypes: {\n ...cipherstashPackMeta.types.codecTypes,\n controlPlaneHooks: {\n [CIPHERSTASH_STRING_CODEC_ID]: cipherstashStringCodecHooks,\n },\n },\n },\n create: () => ({\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n }),\n};\n\nexport { cipherstashExtensionDescriptor };\nexport default cipherstashExtensionDescriptor;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { a as EQL_V2_ENCRYPTED_TYPE, i as CIPHERSTASH_STRING_CODEC_ID, n as CIPHERSTASH_EXTENSION_VERSION, r as CIPHERSTASH_SPACE_ID } from "./constants-B_2TNvUi.mjs";
|
|
2
|
+
import { CodecImpl } from "@prisma-next/framework-components/codec";
|
|
3
|
+
//#region src/contract/authoring.ts
|
|
4
|
+
const cipherstashAuthoringTypes = { cipherstash: { EncryptedString: {
|
|
5
|
+
kind: "typeConstructor",
|
|
6
|
+
args: [{
|
|
7
|
+
kind: "object",
|
|
8
|
+
name: "options",
|
|
9
|
+
optional: true,
|
|
10
|
+
properties: {
|
|
11
|
+
equality: {
|
|
12
|
+
kind: "boolean",
|
|
13
|
+
optional: true
|
|
14
|
+
},
|
|
15
|
+
freeTextSearch: {
|
|
16
|
+
kind: "boolean",
|
|
17
|
+
optional: true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}],
|
|
21
|
+
output: {
|
|
22
|
+
codecId: CIPHERSTASH_STRING_CODEC_ID,
|
|
23
|
+
nativeType: EQL_V2_ENCRYPTED_TYPE,
|
|
24
|
+
typeParams: {
|
|
25
|
+
equality: {
|
|
26
|
+
kind: "arg",
|
|
27
|
+
index: 0,
|
|
28
|
+
path: ["equality"],
|
|
29
|
+
default: true
|
|
30
|
+
},
|
|
31
|
+
freeTextSearch: {
|
|
32
|
+
kind: "arg",
|
|
33
|
+
index: 0,
|
|
34
|
+
path: ["freeTextSearch"],
|
|
35
|
+
default: true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} } };
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/extension-metadata/codec-metadata.ts
|
|
42
|
+
const METADATA_DESCRIPTOR = {
|
|
43
|
+
codecId: CIPHERSTASH_STRING_CODEC_ID,
|
|
44
|
+
traits: [],
|
|
45
|
+
targetTypes: [EQL_V2_ENCRYPTED_TYPE],
|
|
46
|
+
meta: { db: { sql: { postgres: { nativeType: EQL_V2_ENCRYPTED_TYPE } } } },
|
|
47
|
+
paramsSchema: { "~standard": {
|
|
48
|
+
version: 1,
|
|
49
|
+
vendor: "cipherstash",
|
|
50
|
+
validate: (value) => ({ value })
|
|
51
|
+
} },
|
|
52
|
+
isParameterized: false,
|
|
53
|
+
renderOutputType: () => "EncryptedString",
|
|
54
|
+
factory: () => () => {
|
|
55
|
+
throw new Error("cipherstash codec: metadata descriptor factory is not callable");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var CipherstashStringCodecMetadata = class extends CodecImpl {
|
|
59
|
+
async encode() {
|
|
60
|
+
throw new Error("cipherstash codec: encode called on the pack-meta metadata codec. Construct a runtime descriptor via `createCipherstashRuntimeDescriptor({ sdk })` and use that instead.");
|
|
61
|
+
}
|
|
62
|
+
async decode() {
|
|
63
|
+
throw new Error("cipherstash codec: decode called on the pack-meta metadata codec. Construct a runtime descriptor via `createCipherstashRuntimeDescriptor({ sdk })` and use that instead.");
|
|
64
|
+
}
|
|
65
|
+
encodeJson() {
|
|
66
|
+
return { $encryptedString: "<opaque>" };
|
|
67
|
+
}
|
|
68
|
+
decodeJson() {
|
|
69
|
+
throw new Error("cipherstash codec: decodeJson is not supported; envelopes do not round-trip through JSON.");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const cipherstashStringCodecMetadata = new CipherstashStringCodecMetadata(METADATA_DESCRIPTOR);
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/extension-metadata/descriptor-meta.ts
|
|
75
|
+
/**
|
|
76
|
+
* Pack metadata for the cipherstash extension.
|
|
77
|
+
*
|
|
78
|
+
* Mirrors `packages/3-extensions/pgvector/src/extension-metadata/descriptor-meta.ts` —
|
|
79
|
+
* the metadata block that gets serialized into `contract.json`'s
|
|
80
|
+
* `extensionPacks.cipherstash` slot at emit time.
|
|
81
|
+
*
|
|
82
|
+
* SDK-free: the runtime descriptor layers SDK-bound codec instances on
|
|
83
|
+
* top at execution time. The `codecInstances` slot here uses the
|
|
84
|
+
* metadata-only
|
|
85
|
+
* codec from `./codec-metadata` because pack-meta consumers only read
|
|
86
|
+
* codec metadata (typeId, targetTypes, traits, renderOutputType);
|
|
87
|
+
* runtime encode/decode always go through the SDK-bound codec produced
|
|
88
|
+
* by `RuntimeParameterizedCodecDescriptor.factory` (see
|
|
89
|
+
* `./parameterized`).
|
|
90
|
+
*
|
|
91
|
+
* The control descriptor in `../exports/control.ts` spreads this pack
|
|
92
|
+
* meta so the framework's contract emitter sees `authoring`,
|
|
93
|
+
* `types.codecTypes.codecInstances`, and `types.storage` alongside
|
|
94
|
+
* the contract-space and codec-lifecycle-hooks blocks already wired
|
|
95
|
+
* by the codec lifecycle hook block.
|
|
96
|
+
*/
|
|
97
|
+
const cipherstashPackMeta = {
|
|
98
|
+
kind: "extension",
|
|
99
|
+
id: CIPHERSTASH_SPACE_ID,
|
|
100
|
+
familyId: "sql",
|
|
101
|
+
targetId: "postgres",
|
|
102
|
+
version: CIPHERSTASH_EXTENSION_VERSION,
|
|
103
|
+
authoring: { type: cipherstashAuthoringTypes },
|
|
104
|
+
types: {
|
|
105
|
+
codecTypes: {
|
|
106
|
+
codecInstances: [cipherstashStringCodecMetadata],
|
|
107
|
+
typeImports: [{
|
|
108
|
+
package: "@prisma-next/extension-cipherstash/runtime",
|
|
109
|
+
named: "EncryptedString",
|
|
110
|
+
alias: "EncryptedString"
|
|
111
|
+
}]
|
|
112
|
+
},
|
|
113
|
+
queryOperationTypes: { import: {
|
|
114
|
+
package: "@prisma-next/extension-cipherstash/operation-types",
|
|
115
|
+
named: "QueryOperationTypes",
|
|
116
|
+
alias: "CipherstashQueryOperationTypes"
|
|
117
|
+
} },
|
|
118
|
+
storage: [{
|
|
119
|
+
typeId: CIPHERSTASH_STRING_CODEC_ID,
|
|
120
|
+
familyId: "sql",
|
|
121
|
+
targetId: "postgres",
|
|
122
|
+
nativeType: EQL_V2_ENCRYPTED_TYPE
|
|
123
|
+
}]
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
export { cipherstashPackMeta as t };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=descriptor-meta-BgQfZTAF.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptor-meta-BgQfZTAF.mjs","names":[],"sources":["../src/contract/authoring.ts","../src/extension-metadata/codec-metadata.ts","../src/extension-metadata/descriptor-meta.ts"],"sourcesContent":["/**\n * Authoring contributions for the cipherstash extension.\n *\n * Registers `cipherstash.EncryptedString({ equality?, freeTextSearch? })`\n * as a namespaced PSL type constructor. The same descriptor lowers a\n * PSL field-type expression like `cipherstash.EncryptedString({ equality:\n * true })` and a TS factory call like `encryptedString({ equality: true })`\n * (see `../exports/column-types`) to an identical `ColumnTypeDescriptor`\n * so PSL- and TS-authored contracts emit byte-identical `contract.json`.\n *\n * Mirrors `packages/3-extensions/pgvector/src/contract/authoring.ts`. The\n * cipherstash variant differs in three respects:\n * (a) `cipherstash` is the namespace,\n * (b) the constructor takes a single OPTIONAL object argument with two\n * optional booleans (so `cipherstash.EncryptedString()`,\n * `cipherstash.EncryptedString({})`, and the fully-spelled\n * `cipherstash.EncryptedString({ equality: true, freeTextSearch: true })`\n * all parse), and\n * (c) both flags default to `true` — searchable encryption is the\n * legitimate default for an extension whose entire reason for\n * existing is to make encrypted columns queryable. Users who want\n * storage-only encryption opt out explicitly:\n * `cipherstash.EncryptedString({ equality: false, freeTextSearch: false })`.\n */\n\nimport type { AuthoringTypeNamespace } from '@prisma-next/framework-components/authoring';\nimport {\n CIPHERSTASH_STRING_CODEC_ID,\n EQL_V2_ENCRYPTED_TYPE,\n} from '../extension-metadata/constants';\n\nexport const cipherstashAuthoringTypes = {\n cipherstash: {\n EncryptedString: {\n kind: 'typeConstructor',\n args: [\n {\n kind: 'object',\n name: 'options',\n optional: true,\n properties: {\n equality: { kind: 'boolean', optional: true },\n freeTextSearch: { kind: 'boolean', optional: true },\n },\n },\n ],\n output: {\n codecId: CIPHERSTASH_STRING_CODEC_ID,\n nativeType: EQL_V2_ENCRYPTED_TYPE,\n typeParams: {\n equality: { kind: 'arg', index: 0, path: ['equality'], default: true },\n freeTextSearch: {\n kind: 'arg',\n index: 0,\n path: ['freeTextSearch'],\n default: true,\n },\n },\n },\n },\n },\n} as const satisfies AuthoringTypeNamespace;\n","/**\n * SDK-free codec used in pack-meta (`cipherstashPackMeta.types.codecTypes\n * .codecInstances`). Pack-meta consumers only read codec *metadata*\n * (`typeId`, `targetTypes`, `traits`, `renderOutputType`) at contract\n * emit time — they never call `encode`/`decode`.\n *\n * The SDK-bound runtime codec for actual `encode`/`decode` lives in\n * `../execution/codec-runtime`; it is resolved through\n * `RuntimeParameterizedCodecDescriptor.factory` at runtime instead of\n * through pack-meta's `codecInstances`.\n *\n * Keeping the SDK-free metadata in its own module — and *not* importing\n * the runtime `CipherstashStringCodec` class — preserves the control\n * vs runtime split. Control-plane consumers (`exports/control.ts`,\n * `exports/pack.ts`) pull this file but never touch the envelope, the\n * SDK interface, or the bulk-encrypt middleware. The bundling-isolation\n * test pins this property by snapshotting that the control entry's\n * chunk graph does not transitively load `envelope-*.mjs`.\n *\n * `encode`/`decode` throw with a clear hint in the misuse case so\n * accidental wiring of the metadata codec into a real runtime path\n * surfaces immediately instead of silently no-op'ing.\n */\n\nimport type { JsonValue } from '@prisma-next/contract/types';\nimport { type AnyCodecDescriptor, CodecImpl } from '@prisma-next/framework-components/codec';\nimport { CIPHERSTASH_STRING_CODEC_ID, EQL_V2_ENCRYPTED_TYPE } from './constants';\n\nconst METADATA_DESCRIPTOR: AnyCodecDescriptor = {\n codecId: CIPHERSTASH_STRING_CODEC_ID,\n traits: [],\n targetTypes: [EQL_V2_ENCRYPTED_TYPE],\n meta: { db: { sql: { postgres: { nativeType: EQL_V2_ENCRYPTED_TYPE } } } },\n paramsSchema: {\n '~standard': {\n version: 1,\n vendor: 'cipherstash',\n validate: (value: unknown) => ({ value }),\n },\n },\n isParameterized: false,\n renderOutputType: () => 'EncryptedString',\n factory: () => () => {\n throw new Error('cipherstash codec: metadata descriptor factory is not callable');\n },\n};\n\nclass CipherstashStringCodecMetadata extends CodecImpl<\n typeof CIPHERSTASH_STRING_CODEC_ID,\n readonly [],\n unknown,\n unknown\n> {\n async encode(): Promise<unknown> {\n throw new Error(\n 'cipherstash codec: encode called on the pack-meta metadata codec. ' +\n 'Construct a runtime descriptor via `createCipherstashRuntimeDescriptor({ sdk })` and use that instead.',\n );\n }\n\n async decode(): Promise<unknown> {\n throw new Error(\n 'cipherstash codec: decode called on the pack-meta metadata codec. ' +\n 'Construct a runtime descriptor via `createCipherstashRuntimeDescriptor({ sdk })` and use that instead.',\n );\n }\n\n encodeJson(): JsonValue {\n return { $encryptedString: '<opaque>' };\n }\n\n decodeJson(): unknown {\n throw new Error(\n 'cipherstash codec: decodeJson is not supported; envelopes do not round-trip through JSON.',\n );\n }\n}\n\nexport const cipherstashStringCodecMetadata = new CipherstashStringCodecMetadata(\n METADATA_DESCRIPTOR,\n);\n","/**\n * Pack metadata for the cipherstash extension.\n *\n * Mirrors `packages/3-extensions/pgvector/src/extension-metadata/descriptor-meta.ts` —\n * the metadata block that gets serialized into `contract.json`'s\n * `extensionPacks.cipherstash` slot at emit time.\n *\n * SDK-free: the runtime descriptor layers SDK-bound codec instances on\n * top at execution time. The `codecInstances` slot here uses the\n * metadata-only\n * codec from `./codec-metadata` because pack-meta consumers only read\n * codec metadata (typeId, targetTypes, traits, renderOutputType);\n * runtime encode/decode always go through the SDK-bound codec produced\n * by `RuntimeParameterizedCodecDescriptor.factory` (see\n * `./parameterized`).\n *\n * The control descriptor in `../exports/control.ts` spreads this pack\n * meta so the framework's contract emitter sees `authoring`,\n * `types.codecTypes.codecInstances`, and `types.storage` alongside\n * the contract-space and codec-lifecycle-hooks blocks already wired\n * by the codec lifecycle hook block.\n */\n\nimport { cipherstashAuthoringTypes } from '../contract/authoring';\nimport { cipherstashStringCodecMetadata } from './codec-metadata';\nimport {\n CIPHERSTASH_EXTENSION_VERSION,\n CIPHERSTASH_SPACE_ID,\n CIPHERSTASH_STRING_CODEC_ID,\n EQL_V2_ENCRYPTED_TYPE,\n} from './constants';\n\nexport { CIPHERSTASH_EXTENSION_VERSION };\n\nexport const cipherstashPackMeta = {\n kind: 'extension',\n id: CIPHERSTASH_SPACE_ID,\n familyId: 'sql',\n targetId: 'postgres',\n version: CIPHERSTASH_EXTENSION_VERSION,\n authoring: {\n type: cipherstashAuthoringTypes,\n },\n types: {\n codecTypes: {\n codecInstances: [cipherstashStringCodecMetadata],\n // `renderOutputType` returns the bare type name `EncryptedString`\n // for parameterized cipherstash columns; the contract emitter\n // needs to import the type alongside that occurrence so the\n // generated `.d.ts` typechecks cleanly. Mirrors pgvector's\n // `Vector` typeImports declaration.\n typeImports: [\n {\n package: '@prisma-next/extension-cipherstash/runtime',\n named: 'EncryptedString',\n alias: 'EncryptedString',\n },\n ],\n },\n queryOperationTypes: {\n import: {\n package: '@prisma-next/extension-cipherstash/operation-types',\n named: 'QueryOperationTypes',\n alias: 'CipherstashQueryOperationTypes',\n },\n },\n storage: [\n {\n typeId: CIPHERSTASH_STRING_CODEC_ID,\n familyId: 'sql',\n targetId: 'postgres',\n nativeType: EQL_V2_ENCRYPTED_TYPE,\n },\n ],\n },\n} as const;\n"],"mappings":";;;AA+BA,MAAa,4BAA4B,EACvC,aAAa,EACX,iBAAiB;CACf,MAAM;CACN,MAAM,CACJ;EACE,MAAM;EACN,MAAM;EACN,UAAU;EACV,YAAY;GACV,UAAU;IAAE,MAAM;IAAW,UAAU;IAAM;GAC7C,gBAAgB;IAAE,MAAM;IAAW,UAAU;IAAM;GACpD;EACF,CACF;CACD,QAAQ;EACN,SAAS;EACT,YAAY;EACZ,YAAY;GACV,UAAU;IAAE,MAAM;IAAO,OAAO;IAAG,MAAM,CAAC,WAAW;IAAE,SAAS;IAAM;GACtE,gBAAgB;IACd,MAAM;IACN,OAAO;IACP,MAAM,CAAC,iBAAiB;IACxB,SAAS;IACV;GACF;EACF;CACF,EACF,EACF;;;ACjCD,MAAM,sBAA0C;CAC9C,SAAS;CACT,QAAQ,EAAE;CACV,aAAa,CAAC,sBAAsB;CACpC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,uBAAuB,EAAE,EAAE,EAAE;CAC1E,cAAc,EACZ,aAAa;EACX,SAAS;EACT,QAAQ;EACR,WAAW,WAAoB,EAAE,OAAO;EACzC,EACF;CACD,iBAAiB;CACjB,wBAAwB;CACxB,qBAAqB;EACnB,MAAM,IAAI,MAAM,iEAAiE;;CAEpF;AAED,IAAM,iCAAN,cAA6C,UAK3C;CACA,MAAM,SAA2B;EAC/B,MAAM,IAAI,MACR,2KAED;;CAGH,MAAM,SAA2B;EAC/B,MAAM,IAAI,MACR,2KAED;;CAGH,aAAwB;EACtB,OAAO,EAAE,kBAAkB,YAAY;;CAGzC,aAAsB;EACpB,MAAM,IAAI,MACR,4FACD;;;AAIL,MAAa,iCAAiC,IAAI,+BAChD,oBACD;;;;;;;;;;;;;;;;;;;;;;;;;AC9CD,MAAa,sBAAsB;CACjC,MAAM;CACN,IAAI;CACJ,UAAU;CACV,UAAU;CACV,SAAS;CACT,WAAW,EACT,MAAM,2BACP;CACD,OAAO;EACL,YAAY;GACV,gBAAgB,CAAC,+BAA+B;GAMhD,aAAa,CACX;IACE,SAAS;IACT,OAAO;IACP,OAAO;IACR,CACF;GACF;EACD,qBAAqB,EACnB,QAAQ;GACN,SAAS;GACT,OAAO;GACP,OAAO;GACR,EACF;EACD,SAAS,CACP;GACE,QAAQ;GACR,UAAU;GACV,UAAU;GACV,YAAY;GACb,CACF;EACF;CACF"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
2
|
+
import { RUNTIME_ABORTED, runtimeError } from "@prisma-next/framework-components/runtime";
|
|
3
|
+
//#region src/execution/abort.ts
|
|
4
|
+
/**
|
|
5
|
+
* Construct a `RUNTIME.ABORTED` envelope tagged with a cipherstash
|
|
6
|
+
* phase. Reuses the framework`s `runtimeError(RUNTIME_ABORTED, ...)`
|
|
7
|
+
* envelope builder so the structural shape (`code`, `category`,
|
|
8
|
+
* `severity`, `message`, `details.phase`, `cause`) matches everything
|
|
9
|
+
* else the framework emits. Only the `phase` string set is
|
|
10
|
+
* cipherstash-specific.
|
|
11
|
+
*/
|
|
12
|
+
function cipherstashAborted(phase, cause) {
|
|
13
|
+
const envelope = runtimeError(RUNTIME_ABORTED, `Operation aborted during ${phase}`, { phase });
|
|
14
|
+
return Object.assign(envelope, { cause });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Pre-check helper: throw a cipherstash-tagged `RUNTIME.ABORTED`
|
|
18
|
+
* envelope if the supplied signal is already aborted at the call
|
|
19
|
+
* site. Mirrors framework `checkAborted` (which is typed against the
|
|
20
|
+
* framework`s phase union) — used to short-circuit the bulk-encrypt
|
|
21
|
+
* middleware`s pre-flight, the single-cell `decrypt()` pre-flight,
|
|
22
|
+
* and the `decryptAll` walker`s pre-flight before any SDK round-trip
|
|
23
|
+
* is scheduled.
|
|
24
|
+
*/
|
|
25
|
+
function checkCipherstashAborted(signal, phase) {
|
|
26
|
+
if (signal?.aborted) throw cipherstashAborted(phase, signal.reason);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Race a cipherstash SDK promise against the supplied `AbortSignal`
|
|
30
|
+
* so the awaiting caller is rejected promptly with a
|
|
31
|
+
* `RUNTIME.ABORTED` envelope as soon as the signal aborts — even
|
|
32
|
+
* when the SDK body itself ignores the signal. Cooperative
|
|
33
|
+
* cancellation: in-flight SDK calls that ignore the signal continue
|
|
34
|
+
* running in the background and complete; the abort-attributed
|
|
35
|
+
* rejection is what the cipherstash caller sees (the SDK`s eventual
|
|
36
|
+
* resolution is silently abandoned per ADR 207`s "cooperative
|
|
37
|
+
* cancellation, not termination" contract).
|
|
38
|
+
*
|
|
39
|
+
* Mirrors framework `raceAgainstAbort` line-for-line aside from the
|
|
40
|
+
* cipherstash-typed phase parameter and the cipherstash-tagged
|
|
41
|
+
* envelope construction. The sentinel-identity attribution is
|
|
42
|
+
* load-bearing for the same reason ADR 207 spells out: a codec /
|
|
43
|
+
* SDK that itself throws a `RUNTIME.ENCODE_FAILED` /
|
|
44
|
+
* `RUNTIME.DECODE_FAILED` (or any other named envelope) must pass
|
|
45
|
+
* through unchanged — only the cipherstash-installed listener ever
|
|
46
|
+
* rejects with the local sentinel reference, so an `error ===
|
|
47
|
+
* sentinel` identity check after the race is unambiguous.
|
|
48
|
+
*/
|
|
49
|
+
async function raceCipherstashAbort(work, signal, phase) {
|
|
50
|
+
if (signal === void 0) return await work;
|
|
51
|
+
const sentinel = { reason: void 0 };
|
|
52
|
+
let onAbort;
|
|
53
|
+
const abortPromise = new Promise((_, reject) => {
|
|
54
|
+
if (signal.aborted) {
|
|
55
|
+
sentinel.reason = signal.reason;
|
|
56
|
+
reject(sentinel);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
onAbort = () => {
|
|
60
|
+
sentinel.reason = signal.reason;
|
|
61
|
+
reject(sentinel);
|
|
62
|
+
};
|
|
63
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
return await Promise.race([work, abortPromise]);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error === sentinel) throw cipherstashAborted(phase, sentinel.reason);
|
|
69
|
+
throw error;
|
|
70
|
+
} finally {
|
|
71
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/execution/envelope.ts
|
|
76
|
+
/**
|
|
77
|
+
* `EncryptedString` envelope and its package-internal handle helpers.
|
|
78
|
+
*
|
|
79
|
+
* The envelope is the user-facing input/output type for cipherstash-
|
|
80
|
+
* backed columns. It wraps an `EncryptedStringHandle` (plaintext slot,
|
|
81
|
+
* ciphertext slot, routing key, SDK reference) holding the per-cell
|
|
82
|
+
* lifecycle state.
|
|
83
|
+
*
|
|
84
|
+
* ## Encapsulation pattern (Rust `secrecy` style)
|
|
85
|
+
*
|
|
86
|
+
* Storage is a `#private` instance field. The blessed read path is the
|
|
87
|
+
* explicit `expose()` method — same shape as the Rust `secrecy` crate's
|
|
88
|
+
* `SecretBox<T>::expose_secret`. Calling `expose()` is a deliberate
|
|
89
|
+
* opt-in: the caller is announcing "I want the wrapped state". The
|
|
90
|
+
* envelope does not — and is not meant to — make that *impossible*; it
|
|
91
|
+
* is meant to make accidental exposure (logger output, error envelopes,
|
|
92
|
+
* stringification, JSON serialization, primitive coercion) impossible
|
|
93
|
+
* unless the caller goes through `expose()`.
|
|
94
|
+
*
|
|
95
|
+
* Concretely the class overrides every coercion / serialization vector
|
|
96
|
+
* that would otherwise reveal the handle:
|
|
97
|
+
*
|
|
98
|
+
* - `toJSON()` — `JSON.stringify`
|
|
99
|
+
* - `toString()` — `String(envelope)`
|
|
100
|
+
* - `valueOf()` — legacy primitive coercion
|
|
101
|
+
* - `[Symbol.toPrimitive]()` — template literals, `+`
|
|
102
|
+
* - `[Symbol.for('nodejs.util.inspect.custom')]()` — `console.log`,
|
|
103
|
+
* Node REPL, debuggers
|
|
104
|
+
*
|
|
105
|
+
* All five return the same `[REDACTED]` placeholder. Without these
|
|
106
|
+
* overrides, modern Node runtimes surface `#private` fields in
|
|
107
|
+
* `util.inspect` output by default, which would silently re-expose
|
|
108
|
+
* the handle through `console.log(envelope)` (and any error message
|
|
109
|
+
* that interpolates an envelope).
|
|
110
|
+
*
|
|
111
|
+
* ## Lifecycle
|
|
112
|
+
*
|
|
113
|
+
* The handle has two flavours:
|
|
114
|
+
* - **Write side** — `EncryptedString.from(plaintext)` populates the
|
|
115
|
+
* `plaintext` slot and leaves `ciphertext` empty. The bulk-encrypt
|
|
116
|
+
* middleware populates `ciphertext` post-SDK and intentionally
|
|
117
|
+
* leaves the plaintext slot in place (zeroing JS strings is
|
|
118
|
+
* best-effort and GC-driven lifecycle is sufficient here). As a
|
|
119
|
+
* side effect a write-side envelope's `decrypt()` returns the
|
|
120
|
+
* original plaintext synchronously without an SDK round-trip.
|
|
121
|
+
* - **Read side** — `EncryptedString.fromInternal({...})` (called from
|
|
122
|
+
* the codec `decode` body) populates `ciphertext`, `(table, column)`
|
|
123
|
+
* from `SqlCodecCallContext.column`, and an `sdk` reference so
|
|
124
|
+
* `decrypt({signal?})` can issue the SDK's single-cell decrypt.
|
|
125
|
+
*/
|
|
126
|
+
const REDACTED = "[REDACTED]";
|
|
127
|
+
var EncryptedString = class EncryptedString {
|
|
128
|
+
#handle;
|
|
129
|
+
constructor(handle) {
|
|
130
|
+
this.#handle = handle;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Construct a write-side envelope from plaintext. Bulk-encrypt
|
|
134
|
+
* middleware populates the handle's ciphertext slot before the codec
|
|
135
|
+
* encodes the envelope to wire format.
|
|
136
|
+
*/
|
|
137
|
+
static from(plaintext) {
|
|
138
|
+
return new EncryptedString({
|
|
139
|
+
plaintext,
|
|
140
|
+
ciphertext: void 0,
|
|
141
|
+
table: void 0,
|
|
142
|
+
column: void 0,
|
|
143
|
+
sdk: void 0
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Construct a read-side envelope from a wire ciphertext + the column
|
|
148
|
+
* identity + the SDK used to decrypt the cell. Called from the codec
|
|
149
|
+
* `decode` body.
|
|
150
|
+
*/
|
|
151
|
+
static fromInternal(args) {
|
|
152
|
+
return new EncryptedString({
|
|
153
|
+
plaintext: void 0,
|
|
154
|
+
ciphertext: args.ciphertext,
|
|
155
|
+
table: args.table,
|
|
156
|
+
column: args.column,
|
|
157
|
+
sdk: args.sdk
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Explicitly retrieve the wrapped handle. Modelled on Rust `secrecy`'s
|
|
162
|
+
* `SecretBox<T>::expose_secret`: the handle is reachable, but you have
|
|
163
|
+
* to ask for it by name. Callers reach for `expose()` when they need
|
|
164
|
+
* to inspect or transport the ciphertext envelope, debug lifecycle
|
|
165
|
+
* state, or wire ad-hoc tooling around the SDK reference.
|
|
166
|
+
*
|
|
167
|
+
* Mutating the returned handle is supported but unusual — the
|
|
168
|
+
* framework's lifecycle mutators (`setHandleCiphertext`,
|
|
169
|
+
* `setHandleRoutingKey`, etc.) are the conventional path during
|
|
170
|
+
* encrypt / decrypt flow.
|
|
171
|
+
*/
|
|
172
|
+
expose() {
|
|
173
|
+
return this.#handle;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Decrypt and return the plaintext.
|
|
177
|
+
*
|
|
178
|
+
* - If the handle's `plaintext` slot is already populated (write-side
|
|
179
|
+
* envelopes from `from(plaintext)`, or read-side envelopes already
|
|
180
|
+
* materialized by `decryptAll(...)` or a prior `decrypt()`), returns
|
|
181
|
+
* the cached plaintext synchronously without consulting the SDK.
|
|
182
|
+
* - Otherwise (read-side handle without a cached plaintext), invokes
|
|
183
|
+
* the SDK's single-cell `decrypt` with the handle's routing context.
|
|
184
|
+
* The caller-supplied `signal` is forwarded to the SDK by identity
|
|
185
|
+
* per the umbrella cancellation contract; the SDK promise is also
|
|
186
|
+
* raced against the signal so an abort surfaces a `RUNTIME.ABORTED
|
|
187
|
+
* { phase: 'decrypt' }` envelope promptly even if the SDK body
|
|
188
|
+
* ignores the signal. The cached-plaintext fast path returns
|
|
189
|
+
* synchronously without consulting the signal — no IO, no abort
|
|
190
|
+
* observation point.
|
|
191
|
+
*/
|
|
192
|
+
async decrypt(opts) {
|
|
193
|
+
if (this.#handle.plaintext !== void 0) return this.#handle.plaintext;
|
|
194
|
+
if (!this.#handle.sdk || this.#handle.table === void 0 || this.#handle.column === void 0) throw new Error("EncryptedString.decrypt(): envelope has no cached plaintext and no SDK binding. This typically means the bulk-encrypt middleware did not run before the encode site.");
|
|
195
|
+
checkCipherstashAborted(opts?.signal, "decrypt");
|
|
196
|
+
const plaintext = await raceCipherstashAbort(this.#handle.sdk.decrypt({
|
|
197
|
+
ciphertext: this.#handle.ciphertext,
|
|
198
|
+
table: this.#handle.table,
|
|
199
|
+
column: this.#handle.column,
|
|
200
|
+
...ifDefined("signal", opts?.signal)
|
|
201
|
+
}), opts?.signal, "decrypt");
|
|
202
|
+
this.#handle.plaintext = plaintext;
|
|
203
|
+
return plaintext;
|
|
204
|
+
}
|
|
205
|
+
toJSON() {
|
|
206
|
+
return REDACTED;
|
|
207
|
+
}
|
|
208
|
+
toString() {
|
|
209
|
+
return REDACTED;
|
|
210
|
+
}
|
|
211
|
+
valueOf() {
|
|
212
|
+
return REDACTED;
|
|
213
|
+
}
|
|
214
|
+
[Symbol.toPrimitive]() {
|
|
215
|
+
return REDACTED;
|
|
216
|
+
}
|
|
217
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
218
|
+
return REDACTED;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Populate the handle's ciphertext slot. Called by the bulk-encrypt
|
|
223
|
+
* middleware after the SDK returns the encrypted batch.
|
|
224
|
+
*
|
|
225
|
+
* The plaintext slot is intentionally retained — zeroing in JS is
|
|
226
|
+
* best-effort (strings are immutable) and the GC-driven lifecycle is
|
|
227
|
+
* sufficient.
|
|
228
|
+
*/
|
|
229
|
+
function setHandleCiphertext(envelope, ciphertext) {
|
|
230
|
+
envelope.expose().ciphertext = ciphertext;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Populate the handle's plaintext slot with a freshly-decrypted value
|
|
234
|
+
* (read-side caching path used by `decryptAll` and by `decrypt()`'s own
|
|
235
|
+
* memoization).
|
|
236
|
+
*/
|
|
237
|
+
function setHandlePlaintextCache(envelope, plaintext) {
|
|
238
|
+
envelope.expose().plaintext = plaintext;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Stamp the encrypt-side `(table, column)` routing context onto a
|
|
242
|
+
* write-side envelope's handle. Called by the bulk-encrypt middleware
|
|
243
|
+
* before grouping envelopes into per-routing-key bulk-encrypt batches.
|
|
244
|
+
*
|
|
245
|
+
* Idempotent for matching reassignments (re-stamping the same
|
|
246
|
+
* `(table, column)` is a no-op, which covers envelopes reconstructed
|
|
247
|
+
* via `fromInternal` on the read side and re-stamped on the way back
|
|
248
|
+
* in). Conflicting reassignments throw a descriptive error: an
|
|
249
|
+
* envelope reused across plans with a different routing context is a
|
|
250
|
+
* programming error — silently keeping the stale binding would lower
|
|
251
|
+
* to the wrong bulk-encrypt batch.
|
|
252
|
+
*/
|
|
253
|
+
function setHandleRoutingKey(envelope, table, column) {
|
|
254
|
+
const handle = envelope.expose();
|
|
255
|
+
if (handle.table === void 0) handle.table = table;
|
|
256
|
+
else if (handle.table !== table) throw new Error(`cipherstash envelope: routing-key table conflict — handle already bound to "${handle.table}", refusing to rebind to "${table}". Re-encode the value or construct a fresh envelope for the new routing target.`);
|
|
257
|
+
if (handle.column === void 0) handle.column = column;
|
|
258
|
+
else if (handle.column !== column) throw new Error(`cipherstash envelope: routing-key column conflict on table "${handle.table}" — handle already bound to "${handle.column}", refusing to rebind to "${column}". Re-encode the value or construct a fresh envelope for the new routing target.`);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* `true` when the handle already carries a usable plaintext (write-side
|
|
262
|
+
* construction or post-`decrypt` caching). Used by `decryptAll` to skip
|
|
263
|
+
* envelopes that don't need a round-trip.
|
|
264
|
+
*/
|
|
265
|
+
function isHandleDecrypted(envelope) {
|
|
266
|
+
return envelope.expose().plaintext !== void 0;
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
export { setHandleRoutingKey as a, setHandlePlaintextCache as i, isHandleDecrypted as n, checkCipherstashAborted as o, setHandleCiphertext as r, raceCipherstashAbort as s, EncryptedString as t };
|
|
270
|
+
|
|
271
|
+
//# sourceMappingURL=envelope-P9BxfJNr.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope-P9BxfJNr.mjs","names":["#handle"],"sources":["../src/execution/abort.ts","../src/execution/envelope.ts"],"sourcesContent":["/**\n * Cipherstash-internal `RUNTIME.ABORTED` phase wrapping.\n *\n * The framework`s `runtimeAborted(phase)` (`@prisma-next/framework-\n * components/runtime`) constructs the canonical `RUNTIME.ABORTED`\n * envelope (`code === 'RUNTIME.ABORTED'`, `category === 'RUNTIME'`,\n * `details.phase`, `cause`) but its `phase` parameter is typed as\n * the framework`s closed `RuntimeAbortedPhase` union — `encode`,\n * `decode`, `stream`, `beforeExecute`, `afterExecute`, `onRow`. Those\n * tags describe phases of `runtime.execute()` itself (see ADR 207`s\n * \"Where the runtime observes abort\" table); cipherstash`s async\n * observation points sit one layer outside the framework runtime:\n *\n * - `bulk-encrypt` — the bulk-encrypt middleware`s SDK round-trip\n * inside `beforeExecute`. Conceptually a sub-phase of the\n * framework`s `beforeExecute`, but tag-wise distinct so callers\n * can attribute the abort to the cipherstash SDK call rather\n * than to a generic middleware step.\n * - `decrypt` — the single-cell `EncryptedString#decrypt()`\n * SDK call, invoked by the application after the framework\n * returns the row. Not inside any framework phase.\n * - `decrypt-all` — the `decryptAll` walker`s `bulkDecrypt` calls,\n * invoked by the application after the framework returns the\n * row set. Not inside any framework phase.\n *\n * Rather than widen the framework union (which would conflate\n * extension-specific tags with the framework`s own attribution\n * sites), this module reuses the framework`s `runtimeError(...)`\n * envelope builder directly — the *envelope shape* (the\n * `RuntimeErrorEnvelope` interface, the `code` slot, the `category`\n * slot, the `details.phase` slot, the `cause` field) is unchanged;\n * only the set of legal `phase` string values grows. ADR 027`s\n * envelope contract is preserved bit-for-bit.\n *\n * The `raceCipherstashAbort` helper mirrors framework\n * `raceAgainstAbort` so cipherstash`s SDK-call sites get the same\n * \"return promptly even when the SDK ignores the signal\" behaviour\n * (the cooperative-cancellation model from ADR 207). Identity-\n * checked sentinel rejection distinguishes abort-source from a\n * codec-thrown envelope, matching the framework`s pattern. We\n * duplicate the logic (rather than passing a cast tag to the\n * framework helper) to keep the cipherstash `phase` strings\n * cipherstash-internal — no widening of the framework union.\n */\n\nimport type { RuntimeErrorEnvelope } from '@prisma-next/framework-components/runtime';\nimport { RUNTIME_ABORTED, runtimeError } from '@prisma-next/framework-components/runtime';\n\n/** Discriminator placed in `details.phase` of cipherstash-issued aborts. */\nexport type CipherstashAbortPhase = 'bulk-encrypt' | 'decrypt' | 'decrypt-all';\n\n/**\n * Construct a `RUNTIME.ABORTED` envelope tagged with a cipherstash\n * phase. Reuses the framework`s `runtimeError(RUNTIME_ABORTED, ...)`\n * envelope builder so the structural shape (`code`, `category`,\n * `severity`, `message`, `details.phase`, `cause`) matches everything\n * else the framework emits. Only the `phase` string set is\n * cipherstash-specific.\n */\nexport function cipherstashAborted(\n phase: CipherstashAbortPhase,\n cause?: unknown,\n): RuntimeErrorEnvelope {\n const envelope = runtimeError(RUNTIME_ABORTED, `Operation aborted during ${phase}`, { phase });\n return Object.assign(envelope, { cause });\n}\n\n/**\n * Pre-check helper: throw a cipherstash-tagged `RUNTIME.ABORTED`\n * envelope if the supplied signal is already aborted at the call\n * site. Mirrors framework `checkAborted` (which is typed against the\n * framework`s phase union) — used to short-circuit the bulk-encrypt\n * middleware`s pre-flight, the single-cell `decrypt()` pre-flight,\n * and the `decryptAll` walker`s pre-flight before any SDK round-trip\n * is scheduled.\n */\nexport function checkCipherstashAborted(\n signal: AbortSignal | undefined,\n phase: CipherstashAbortPhase,\n): void {\n if (signal?.aborted) {\n throw cipherstashAborted(phase, signal.reason);\n }\n}\n\n/**\n * Race a cipherstash SDK promise against the supplied `AbortSignal`\n * so the awaiting caller is rejected promptly with a\n * `RUNTIME.ABORTED` envelope as soon as the signal aborts — even\n * when the SDK body itself ignores the signal. Cooperative\n * cancellation: in-flight SDK calls that ignore the signal continue\n * running in the background and complete; the abort-attributed\n * rejection is what the cipherstash caller sees (the SDK`s eventual\n * resolution is silently abandoned per ADR 207`s \"cooperative\n * cancellation, not termination\" contract).\n *\n * Mirrors framework `raceAgainstAbort` line-for-line aside from the\n * cipherstash-typed phase parameter and the cipherstash-tagged\n * envelope construction. The sentinel-identity attribution is\n * load-bearing for the same reason ADR 207 spells out: a codec /\n * SDK that itself throws a `RUNTIME.ENCODE_FAILED` /\n * `RUNTIME.DECODE_FAILED` (or any other named envelope) must pass\n * through unchanged — only the cipherstash-installed listener ever\n * rejects with the local sentinel reference, so an `error ===\n * sentinel` identity check after the race is unambiguous.\n */\nexport async function raceCipherstashAbort<T>(\n work: Promise<T>,\n signal: AbortSignal | undefined,\n phase: CipherstashAbortPhase,\n): Promise<T> {\n if (signal === undefined) {\n return await work;\n }\n const sentinel: { reason: unknown } = { reason: undefined };\n let onAbort: (() => void) | undefined;\n\n const abortPromise = new Promise<never>((_, reject) => {\n if (signal.aborted) {\n sentinel.reason = signal.reason;\n reject(sentinel);\n return;\n }\n onAbort = () => {\n sentinel.reason = signal.reason;\n reject(sentinel);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n\n try {\n return await Promise.race([work, abortPromise]);\n } catch (error) {\n if (error === sentinel) {\n throw cipherstashAborted(phase, sentinel.reason);\n }\n throw error;\n } finally {\n if (onAbort) {\n signal.removeEventListener('abort', onAbort);\n }\n }\n}\n","/**\n * `EncryptedString` envelope and its package-internal handle helpers.\n *\n * The envelope is the user-facing input/output type for cipherstash-\n * backed columns. It wraps an `EncryptedStringHandle` (plaintext slot,\n * ciphertext slot, routing key, SDK reference) holding the per-cell\n * lifecycle state.\n *\n * ## Encapsulation pattern (Rust `secrecy` style)\n *\n * Storage is a `#private` instance field. The blessed read path is the\n * explicit `expose()` method — same shape as the Rust `secrecy` crate's\n * `SecretBox<T>::expose_secret`. Calling `expose()` is a deliberate\n * opt-in: the caller is announcing \"I want the wrapped state\". The\n * envelope does not — and is not meant to — make that *impossible*; it\n * is meant to make accidental exposure (logger output, error envelopes,\n * stringification, JSON serialization, primitive coercion) impossible\n * unless the caller goes through `expose()`.\n *\n * Concretely the class overrides every coercion / serialization vector\n * that would otherwise reveal the handle:\n *\n * - `toJSON()` — `JSON.stringify`\n * - `toString()` — `String(envelope)`\n * - `valueOf()` — legacy primitive coercion\n * - `[Symbol.toPrimitive]()` — template literals, `+`\n * - `[Symbol.for('nodejs.util.inspect.custom')]()` — `console.log`,\n * Node REPL, debuggers\n *\n * All five return the same `[REDACTED]` placeholder. Without these\n * overrides, modern Node runtimes surface `#private` fields in\n * `util.inspect` output by default, which would silently re-expose\n * the handle through `console.log(envelope)` (and any error message\n * that interpolates an envelope).\n *\n * ## Lifecycle\n *\n * The handle has two flavours:\n * - **Write side** — `EncryptedString.from(plaintext)` populates the\n * `plaintext` slot and leaves `ciphertext` empty. The bulk-encrypt\n * middleware populates `ciphertext` post-SDK and intentionally\n * leaves the plaintext slot in place (zeroing JS strings is\n * best-effort and GC-driven lifecycle is sufficient here). As a\n * side effect a write-side envelope's `decrypt()` returns the\n * original plaintext synchronously without an SDK round-trip.\n * - **Read side** — `EncryptedString.fromInternal({...})` (called from\n * the codec `decode` body) populates `ciphertext`, `(table, column)`\n * from `SqlCodecCallContext.column`, and an `sdk` reference so\n * `decrypt({signal?})` can issue the SDK's single-cell decrypt.\n */\n\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { checkCipherstashAborted, raceCipherstashAbort } from './abort';\nimport type { CipherstashSdk } from './sdk';\n\n/**\n * The mutable state of an `EncryptedString` — exposed by `expose()` for\n * callers that explicitly opt in. Mutating these slots from outside the\n * package is supported (we don't stop you) but unusual; the framework's\n * own lifecycle mutators (`setHandleCiphertext`, `setHandleRoutingKey`,\n * etc.) are the conventional path.\n */\nexport interface EncryptedStringHandle {\n plaintext: string | undefined;\n ciphertext: unknown;\n table: string | undefined;\n column: string | undefined;\n sdk: CipherstashSdk | undefined;\n}\n\nconst REDACTED = '[REDACTED]';\n\nexport interface EncryptedStringFromInternalArgs {\n readonly ciphertext: unknown;\n readonly table: string;\n readonly column: string;\n readonly sdk: CipherstashSdk;\n}\n\nexport class EncryptedString {\n readonly #handle: EncryptedStringHandle;\n\n private constructor(handle: EncryptedStringHandle) {\n this.#handle = handle;\n }\n\n /**\n * Construct a write-side envelope from plaintext. Bulk-encrypt\n * middleware populates the handle's ciphertext slot before the codec\n * encodes the envelope to wire format.\n */\n static from(plaintext: string): EncryptedString {\n return new EncryptedString({\n plaintext,\n ciphertext: undefined,\n table: undefined,\n column: undefined,\n sdk: undefined,\n });\n }\n\n /**\n * Construct a read-side envelope from a wire ciphertext + the column\n * identity + the SDK used to decrypt the cell. Called from the codec\n * `decode` body.\n */\n static fromInternal(args: EncryptedStringFromInternalArgs): EncryptedString {\n return new EncryptedString({\n plaintext: undefined,\n ciphertext: args.ciphertext,\n table: args.table,\n column: args.column,\n sdk: args.sdk,\n });\n }\n\n /**\n * Explicitly retrieve the wrapped handle. Modelled on Rust `secrecy`'s\n * `SecretBox<T>::expose_secret`: the handle is reachable, but you have\n * to ask for it by name. Callers reach for `expose()` when they need\n * to inspect or transport the ciphertext envelope, debug lifecycle\n * state, or wire ad-hoc tooling around the SDK reference.\n *\n * Mutating the returned handle is supported but unusual — the\n * framework's lifecycle mutators (`setHandleCiphertext`,\n * `setHandleRoutingKey`, etc.) are the conventional path during\n * encrypt / decrypt flow.\n */\n expose(): EncryptedStringHandle {\n return this.#handle;\n }\n\n /**\n * Decrypt and return the plaintext.\n *\n * - If the handle's `plaintext` slot is already populated (write-side\n * envelopes from `from(plaintext)`, or read-side envelopes already\n * materialized by `decryptAll(...)` or a prior `decrypt()`), returns\n * the cached plaintext synchronously without consulting the SDK.\n * - Otherwise (read-side handle without a cached plaintext), invokes\n * the SDK's single-cell `decrypt` with the handle's routing context.\n * The caller-supplied `signal` is forwarded to the SDK by identity\n * per the umbrella cancellation contract; the SDK promise is also\n * raced against the signal so an abort surfaces a `RUNTIME.ABORTED\n * { phase: 'decrypt' }` envelope promptly even if the SDK body\n * ignores the signal. The cached-plaintext fast path returns\n * synchronously without consulting the signal — no IO, no abort\n * observation point.\n */\n async decrypt(opts?: { signal?: AbortSignal }): Promise<string> {\n if (this.#handle.plaintext !== undefined) {\n return this.#handle.plaintext;\n }\n if (\n !this.#handle.sdk ||\n this.#handle.table === undefined ||\n this.#handle.column === undefined\n ) {\n throw new Error(\n 'EncryptedString.decrypt(): envelope has no cached plaintext and no SDK binding. ' +\n 'This typically means the bulk-encrypt middleware did not run before the encode site.',\n );\n }\n checkCipherstashAborted(opts?.signal, 'decrypt');\n const plaintext = await raceCipherstashAbort(\n this.#handle.sdk.decrypt({\n ciphertext: this.#handle.ciphertext,\n table: this.#handle.table,\n column: this.#handle.column,\n ...ifDefined('signal', opts?.signal),\n }),\n opts?.signal,\n 'decrypt',\n );\n this.#handle.plaintext = plaintext;\n return plaintext;\n }\n\n toJSON(): string {\n return REDACTED;\n }\n\n toString(): string {\n return REDACTED;\n }\n\n valueOf(): string {\n return REDACTED;\n }\n\n [Symbol.toPrimitive](): string {\n return REDACTED;\n }\n\n [Symbol.for('nodejs.util.inspect.custom')](): string {\n return REDACTED;\n }\n}\n\n/**\n * Populate the handle's ciphertext slot. Called by the bulk-encrypt\n * middleware after the SDK returns the encrypted batch.\n *\n * The plaintext slot is intentionally retained — zeroing in JS is\n * best-effort (strings are immutable) and the GC-driven lifecycle is\n * sufficient.\n */\nexport function setHandleCiphertext(envelope: EncryptedString, ciphertext: unknown): void {\n envelope.expose().ciphertext = ciphertext;\n}\n\n/**\n * Populate the handle's plaintext slot with a freshly-decrypted value\n * (read-side caching path used by `decryptAll` and by `decrypt()`'s own\n * memoization).\n */\nexport function setHandlePlaintextCache(envelope: EncryptedString, plaintext: string): void {\n envelope.expose().plaintext = plaintext;\n}\n\n/**\n * Stamp the encrypt-side `(table, column)` routing context onto a\n * write-side envelope's handle. Called by the bulk-encrypt middleware\n * before grouping envelopes into per-routing-key bulk-encrypt batches.\n *\n * Idempotent for matching reassignments (re-stamping the same\n * `(table, column)` is a no-op, which covers envelopes reconstructed\n * via `fromInternal` on the read side and re-stamped on the way back\n * in). Conflicting reassignments throw a descriptive error: an\n * envelope reused across plans with a different routing context is a\n * programming error — silently keeping the stale binding would lower\n * to the wrong bulk-encrypt batch.\n */\nexport function setHandleRoutingKey(\n envelope: EncryptedString,\n table: string,\n column: string,\n): void {\n const handle = envelope.expose();\n if (handle.table === undefined) {\n handle.table = table;\n } else if (handle.table !== table) {\n throw new Error(\n `cipherstash envelope: routing-key table conflict — handle already bound to \"${handle.table}\", refusing to rebind to \"${table}\". Re-encode the value or construct a fresh envelope for the new routing target.`,\n );\n }\n if (handle.column === undefined) {\n handle.column = column;\n } else if (handle.column !== column) {\n throw new Error(\n `cipherstash envelope: routing-key column conflict on table \"${handle.table}\" — handle already bound to \"${handle.column}\", refusing to rebind to \"${column}\". Re-encode the value or construct a fresh envelope for the new routing target.`,\n );\n }\n}\n\n/**\n * `true` when the handle already carries a usable plaintext (write-side\n * construction or post-`decrypt` caching). Used by `decryptAll` to skip\n * envelopes that don't need a round-trip.\n */\nexport function isHandleDecrypted(envelope: EncryptedString): boolean {\n return envelope.expose().plaintext !== undefined;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { i as CipherstashSdk } from "./sdk-D5FTGyzp.mjs";
|
|
2
|
+
import { SqlMiddleware } from "@prisma-next/sql-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/middleware/bulk-encrypt.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Construct the bulk-encrypt middleware. The returned middleware is
|
|
7
|
+
* stateless aside from the captured `sdk` reference; one instance per
|
|
8
|
+
* runtime extension is the expected pattern.
|
|
9
|
+
*/
|
|
10
|
+
declare function bulkEncryptMiddleware(sdk: CipherstashSdk): SqlMiddleware;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { bulkEncryptMiddleware };
|
|
13
|
+
//# sourceMappingURL=middleware.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../src/middleware/bulk-encrypt.ts"],"mappings":";;;;;;;;;iBA6DgB,qBAAA,CAAsB,GAAA,EAAK,cAAA,GAAiB,aAAA"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import "./constants-B_2TNvUi.mjs";
|
|
2
|
+
import { a as setHandleRoutingKey, o as checkCipherstashAborted, r as setHandleCiphertext, s as raceCipherstashAbort, t as EncryptedString } from "./envelope-P9BxfJNr.mjs";
|
|
3
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
|
+
//#region src/execution/routing.ts
|
|
5
|
+
/**
|
|
6
|
+
* Stable string key used to group targets by their `(table, column)`
|
|
7
|
+
* routing key. Exported for tests; not part of the package's public
|
|
8
|
+
* surface. Uses a NUL byte as the separator so the id never collides
|
|
9
|
+
* across pairs whose names happen to share a literal concatenation
|
|
10
|
+
* (e.g. `(a, bc)` vs `(ab, c)`).
|
|
11
|
+
*/
|
|
12
|
+
function routingKeyId(routingKey) {
|
|
13
|
+
return `${routingKey.table}\u0000${routingKey.column}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Group bulk-encrypt targets by `(table, column)` routing key. Each
|
|
17
|
+
* `Map` entry yields one homogeneous batch suitable for a single
|
|
18
|
+
* `sdk.bulkEncrypt({ routingKey, values, signal })` call.
|
|
19
|
+
*
|
|
20
|
+
* Order preservation: within each group, targets keep the order they
|
|
21
|
+
* were collected from `params.entries()` — which is the canonical
|
|
22
|
+
* ParamRef order the renderer's `$N` index map and the encode-side walk
|
|
23
|
+
* both consume. Iteration order across groups follows the order each
|
|
24
|
+
* routing key was first observed in the input.
|
|
25
|
+
*/
|
|
26
|
+
function groupByRoutingKey(targets) {
|
|
27
|
+
const groups = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const target of targets) {
|
|
29
|
+
const id = routingKeyId(target.routingKey);
|
|
30
|
+
let group = groups.get(id);
|
|
31
|
+
if (!group) {
|
|
32
|
+
group = [];
|
|
33
|
+
groups.set(id, group);
|
|
34
|
+
}
|
|
35
|
+
group.push(target);
|
|
36
|
+
}
|
|
37
|
+
return groups;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/middleware/bulk-encrypt.ts
|
|
41
|
+
/**
|
|
42
|
+
* Construct the bulk-encrypt middleware. The returned middleware is
|
|
43
|
+
* stateless aside from the captured `sdk` reference; one instance per
|
|
44
|
+
* runtime extension is the expected pattern.
|
|
45
|
+
*/
|
|
46
|
+
function bulkEncryptMiddleware(sdk) {
|
|
47
|
+
return {
|
|
48
|
+
name: "cipherstash.bulk-encrypt",
|
|
49
|
+
familyId: "sql",
|
|
50
|
+
async beforeExecute(plan, ctx, params) {
|
|
51
|
+
if (!params) return;
|
|
52
|
+
stampRoutingKeysFromAst(plan.ast);
|
|
53
|
+
const targets = collectTargets(params);
|
|
54
|
+
if (targets.length === 0) return;
|
|
55
|
+
const groups = groupByRoutingKey(targets);
|
|
56
|
+
for (const [groupKey, group] of groups) {
|
|
57
|
+
const first = group[0];
|
|
58
|
+
if (!first) continue;
|
|
59
|
+
const routingKey = first.routingKey;
|
|
60
|
+
checkCipherstashAborted(ctx.signal, "bulk-encrypt");
|
|
61
|
+
const ciphertexts = await raceCipherstashAbort(sdk.bulkEncrypt({
|
|
62
|
+
routingKey,
|
|
63
|
+
values: group.map((t) => t.plaintext),
|
|
64
|
+
...ifDefined("signal", ctx.signal)
|
|
65
|
+
}), ctx.signal, "bulk-encrypt");
|
|
66
|
+
if (ciphertexts.length !== group.length) throw new Error(`cipherstash bulk-encrypt: SDK returned ${ciphertexts.length} ciphertexts for routing key ${groupKey} but ${group.length} were requested.`);
|
|
67
|
+
params.replaceValues(group.map((t, i) => {
|
|
68
|
+
const ciphertext = ciphertexts[i];
|
|
69
|
+
setHandleCiphertext(t.envelope, ciphertext);
|
|
70
|
+
return {
|
|
71
|
+
ref: t.ref,
|
|
72
|
+
newValue: t.envelope
|
|
73
|
+
};
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function collectTargets(params) {
|
|
80
|
+
const targets = [];
|
|
81
|
+
for (const entry of params.entries()) {
|
|
82
|
+
if (entry.codecId !== "cipherstash/string@1") continue;
|
|
83
|
+
const value = entry.value;
|
|
84
|
+
if (!(value instanceof EncryptedString)) continue;
|
|
85
|
+
const handle = value.expose();
|
|
86
|
+
if (handle.plaintext === void 0) throw new Error("cipherstash bulk-encrypt: encountered an envelope with no plaintext on the write path. Use `EncryptedString.from(plaintext)` to construct write-side envelopes.");
|
|
87
|
+
if (handle.table === void 0 || handle.column === void 0) throw new Error("cipherstash bulk-encrypt: envelope reached the bulk-encrypt phase without a (table, column) routing context. The middleware's AST walk only handles `InsertAst` and `UpdateAst`; cipherstash envelopes embedded in other plan shapes (e.g. raw SQL) must stamp routing context explicitly via `setHandleRoutingKey` before execute.");
|
|
88
|
+
targets.push({
|
|
89
|
+
ref: entry.ref,
|
|
90
|
+
plaintext: handle.plaintext,
|
|
91
|
+
envelope: value,
|
|
92
|
+
routingKey: {
|
|
93
|
+
table: handle.table,
|
|
94
|
+
column: handle.column
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return targets;
|
|
99
|
+
}
|
|
100
|
+
function stampRoutingKeysFromAst(ast) {
|
|
101
|
+
if (!ast) return;
|
|
102
|
+
switch (ast.kind) {
|
|
103
|
+
case "insert":
|
|
104
|
+
stampInsert(ast);
|
|
105
|
+
return;
|
|
106
|
+
case "update":
|
|
107
|
+
stampUpdate(ast);
|
|
108
|
+
return;
|
|
109
|
+
default: return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function stampInsert(ast) {
|
|
113
|
+
const tableName = ast.table.name;
|
|
114
|
+
for (const row of ast.rows) for (const [column, value] of Object.entries(row)) stampParamRefIfEnvelope(value, tableName, column);
|
|
115
|
+
if (ast.onConflict?.action.kind === "do-update-set") for (const [column, value] of Object.entries(ast.onConflict.action.set)) stampParamRefIfEnvelope(value, tableName, column);
|
|
116
|
+
}
|
|
117
|
+
function stampUpdate(ast) {
|
|
118
|
+
const tableName = ast.table.name;
|
|
119
|
+
for (const [column, value] of Object.entries(ast.set)) stampParamRefIfEnvelope(value, tableName, column);
|
|
120
|
+
}
|
|
121
|
+
function stampParamRefIfEnvelope(value, table, column) {
|
|
122
|
+
if (value.kind !== "param-ref") return;
|
|
123
|
+
const inner = value.value;
|
|
124
|
+
if (inner instanceof EncryptedString) setHandleRoutingKey(inner, table, column);
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
export { bulkEncryptMiddleware };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=middleware.mjs.map
|