Skip to content

Commit c7ec279

Browse files
committed
feat(prisma-next): wire v3 into runtime/control descriptors + exports
Register the v3 codec hooks + v3 baseline migration in the control descriptor, add bulkEncryptV3Middleware to cipherstashFromStack, re-export the v3 surface from runtime/middleware, and advance the head ref to the union of baseline invariants.
1 parent 35e5e27 commit c7ec279

11 files changed

Lines changed: 170 additions & 9 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"from": null,
3+
"to": "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4",
4+
"labels": [],
5+
"providedInvariants": [
6+
"cipherstash:install-eql-v3-bundle-v1"
7+
],
8+
"createdAt": "2026-06-16T23:32:09.463Z",
9+
"fromContract": null,
10+
"toContract": {
11+
"storage": {
12+
"storageHash": "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4"
13+
}
14+
},
15+
"hints": {
16+
"used": [],
17+
"applied": [],
18+
"plannerVersion": "2.0.0"
19+
},
20+
"migrationHash": "sha256:997f0460ad5ad14530144f6c031104966b7cf90052ebf794cc2cf0f7c264e0ee"
21+
}

packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ const INSTALL_LABEL = 'Install EQL v3 bundle (eql_v3 schema, text domains, index
2222

2323
export default class M extends Migration {
2424
override describe() {
25+
// The v3 bundle installs the eql_v3 schema/domains/functions but adds NO
26+
// contract-IR object (no tables modelled), so the resulting storage hash is
27+
// unchanged from the v2 baseline — `to` equals the package's contract
28+
// storageHash. v3 is therefore a parallel install baseline (from: null)
29+
// satisfying its own invariant; the head ref's invariant set is the union of
30+
// both baselines.
2531
return {
2632
from: null,
27-
to: 'cipherstash:eql-v3-baseline',
33+
to: 'sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4',
2834
};
2935
}
3036

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"hash": "sha256:efa685171bebbb8f078f08d12be3578bb5d96b71669dccc6cc9e4be96af8cdb4",
3-
"invariants": ["cipherstash:install-eql-bundle-v1"]
3+
"invariants": ["cipherstash:install-eql-bundle-v1", "cipherstash:install-eql-v3-bundle-v1"]
44
}

packages/prisma-next/src/exports/control.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ import baselineMetadata from '../../migrations/20260601T0000_install_eql_bundle/
3838
import baselineOps from '../../migrations/20260601T0000_install_eql_bundle/ops.json' with {
3939
type: 'json',
4040
};
41+
import baselineV3Metadata from '../../migrations/20260601T0100_install_eql_v3_bundle/migration.json' with {
42+
type: 'json',
43+
};
44+
import baselineV3Ops from '../../migrations/20260601T0100_install_eql_v3_bundle/ops.json' with {
45+
type: 'json',
46+
};
4147
import headRef from '../../migrations/refs/head.json' with { type: 'json' };
4248
import contractJson from '../contract.json' with { type: 'json' };
4349
import {
@@ -48,6 +54,8 @@ import {
4854
CIPHERSTASH_DOUBLE_CODEC_ID,
4955
CIPHERSTASH_JSON_CODEC_ID,
5056
CIPHERSTASH_STRING_CODEC_ID,
57+
CIPHERSTASH_STRING_V3_CODEC_ID,
58+
CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
5159
} from '../extension-metadata/constants';
5260
import { cipherstashPackMeta } from '../extension-metadata/descriptor-meta';
5361
import {
@@ -58,6 +66,7 @@ import {
5866
cipherstashJsonCodecHooks,
5967
cipherstashStringCodecHooks,
6068
} from '../migration/cipherstash-codec';
69+
import { cipherstashStringV3CodecHooks } from '../migration/codec-hooks-v3';
6170

6271
const cipherstashContractSpace = contractSpaceFromJson<Contract<SqlStorage>>({
6372
contractJson,
@@ -67,6 +76,12 @@ const cipherstashContractSpace = contractSpaceFromJson<Contract<SqlStorage>>({
6776
metadata: baselineMetadata,
6877
ops: baselineOps,
6978
},
79+
{
80+
// EQL v3 baseline — installs the eql_v3 bundle. Sorts after the v2 baseline.
81+
dirName: CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
82+
metadata: baselineV3Metadata,
83+
ops: baselineV3Ops,
84+
},
7085
],
7186
headRef,
7287
});
@@ -98,6 +113,8 @@ const cipherstashExtensionDescriptor: SqlControlExtensionDescriptor<'postgres'>
98113
[CIPHERSTASH_DATE_CODEC_ID]: cipherstashDateCodecHooks,
99114
[CIPHERSTASH_BOOLEAN_CODEC_ID]: cipherstashBooleanCodecHooks,
100115
[CIPHERSTASH_JSON_CODEC_ID]: cipherstashJsonCodecHooks,
116+
// v3: expandNativeType → per-index domain; onFieldEvent emits no search-config.
117+
[CIPHERSTASH_STRING_V3_CODEC_ID]: cipherstashStringV3CodecHooks,
101118
},
102119
},
103120
},

packages/prisma-next/src/exports/middleware.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@
2222
*/
2323

2424
export { bulkEncryptMiddleware } from '../middleware/bulk-encrypt';
25+
// EQL v3 storage-vs-search-term split middleware. Register ALONGSIDE
26+
// bulkEncryptMiddleware (disjoint codec-id sets — each ignores the other's params).
27+
export { bulkEncryptV3Middleware } from '../middleware/bulk-encrypt-v3';

packages/prisma-next/src/exports/runtime.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export {
4343
createCipherstashDoubleCodec,
4444
createCipherstashJsonCodec,
4545
createCipherstashStringCodec,
46+
createCipherstashStringV3Codec,
4647
} from '../execution/codec-runtime';
4748
export type { DecryptAllOptions } from '../execution/decrypt-all';
4849
export { decryptAll } from '../execution/decrypt-all';
@@ -82,13 +83,15 @@ export {
8283
cipherstashJsonbGet,
8384
cipherstashJsonbPathQueryFirst,
8485
} from '../execution/helpers';
86+
export { queryTypeForIndex } from '../execution/operators';
8587
export type {
8688
CipherstashAnyParams,
8789
CipherstashBooleanParams,
8890
CipherstashDateParams,
8991
CipherstashJsonParams,
9092
CipherstashNumericParams,
9193
CipherstashStringParams,
94+
CipherstashStringV3Params,
9295
} from '../execution/parameterized';
9396
export {
9497
createParameterizedCodecDescriptors,
@@ -98,26 +101,31 @@ export {
98101
encryptedDoubleParamsSchema,
99102
encryptedJsonParamsSchema,
100103
encryptedStringParamsSchema,
104+
encryptedStringV3ParamsSchema,
101105
renderEncryptedBigIntOutputType,
102106
renderEncryptedBooleanOutputType,
103107
renderEncryptedDateOutputType,
104108
renderEncryptedDoubleOutputType,
105109
renderEncryptedJsonOutputType,
106110
renderEncryptedStringOutputType,
111+
renderEncryptedStringV3OutputType,
107112
} from '../execution/parameterized';
108113
export type {
109114
CipherstashBulkDecryptArgs,
110115
CipherstashBulkEncryptArgs,
116+
CipherstashBulkEncryptQueryArgs,
111117
CipherstashRoutingKey,
112118
CipherstashSdk,
113119
CipherstashSingleDecryptArgs,
114120
} from '../execution/sdk';
121+
export type { V3DataType, V3Index } from '../v3/domain-map';
115122
export {
116123
CIPHERSTASH_BIGINT_CODEC_ID,
117124
CIPHERSTASH_BOOLEAN_CODEC_ID,
118125
CIPHERSTASH_DATE_CODEC_ID,
119126
CIPHERSTASH_DOUBLE_CODEC_ID,
120127
CIPHERSTASH_JSON_CODEC_ID,
128+
CIPHERSTASH_STRING_V3_CODEC_ID,
121129
} from '../extension-metadata/constants';
122130

123131
export { CIPHERSTASH_EXTENSION_VERSION };

packages/prisma-next/src/stack/from-stack.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type { SqlMiddleware, SqlRuntimeExtensionDescriptor } from '@prisma-next/
3636

3737
import { createCipherstashRuntimeDescriptor } from '../exports/runtime'
3838
import { bulkEncryptMiddleware } from '../middleware/bulk-encrypt'
39+
import { bulkEncryptV3Middleware } from '../middleware/bulk-encrypt-v3'
3940
import {
4041
type ContractStorageView,
4142
deriveStackSchemas,
@@ -91,7 +92,10 @@ export async function cipherstashFromStack(
9192

9293
return {
9394
extensions: [createCipherstashRuntimeDescriptor({ sdk })],
94-
middleware: [bulkEncryptMiddleware(sdk)],
95+
// Two middlewares over one sdk: v2 filters CIPHERSTASH_CODEC_ID_SET, v3 filters
96+
// CIPHERSTASH_V3_CODEC_ID_SET — disjoint, so order is irrelevant and each ignores
97+
// the other's params.
98+
middleware: [bulkEncryptMiddleware(sdk), bulkEncryptV3Middleware(sdk)],
9599
encryptionClient,
96100
}
97101
}

packages/prisma-next/test/descriptor.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
CIPHERSTASH_BASELINE_MIGRATION_NAME,
3030
CIPHERSTASH_INVARIANTS,
3131
CIPHERSTASH_SPACE_ID,
32+
CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
3233
EQL_V2_CONFIGURATION_TABLE,
3334
} from '../src/extension-metadata/constants';
3435
import { EQL_BUNDLE_SQL } from '../src/migration/eql-bundle';
@@ -49,13 +50,19 @@ describe('cipherstash extension descriptor (contract-space package layout)', ()
4950
expect(Object.keys(space!.contractJson.storage.tables)).toEqual([EQL_V2_CONFIGURATION_TABLE]);
5051
});
5152

52-
it('publishes one baseline migration sourced from the on-disk emit pipeline', () => {
53+
it('publishes the v2 + v3 baseline migrations sourced from the on-disk emit pipeline', () => {
5354
const space = cipherstashExtensionDescriptor.contractSpace!;
54-
expect(space.migrations).toHaveLength(1);
55+
expect(space.migrations).toHaveLength(2);
5556
const baseline = space.migrations[0]!;
5657
expect(baseline.dirName).toBe(CIPHERSTASH_BASELINE_MIGRATION_NAME);
5758
expect(baseline.metadata.from).toBeNull();
5859
expect(baseline.metadata.to).toBe(space.contractJson.storage.storageHash);
60+
// The v3 baseline installs the eql_v3 bundle; it adds no contract-IR object,
61+
// so it resolves to the SAME storage hash (a parallel install baseline).
62+
const v3 = space.migrations[1]!;
63+
expect(v3.dirName).toBe(CIPHERSTASH_V3_BASELINE_MIGRATION_NAME);
64+
expect(v3.metadata.from).toBeNull();
65+
expect(v3.metadata.to).toBe(space.contractJson.storage.storageHash);
5966
});
6067

6168
it('baseline ops carry the installEqlBundle op + structural create-* ops', () => {
@@ -83,12 +90,13 @@ describe('cipherstash extension descriptor (contract-space package layout)', ()
8390
expect(installOp?.execute?.[0]?.sql).toBe(EQL_BUNDLE_SQL);
8491
});
8592

86-
it("points the head ref at the latest migration's destination hash", () => {
93+
it("points the head ref at the baseline destination hash + the union of every baseline's invariants", () => {
8794
const space = cipherstashExtensionDescriptor.contractSpace!;
95+
// Both baselines resolve to the same storage hash (v3 adds no IR object).
8896
expect(space.headRef.hash).toBe(space.migrations[0]!.metadata.to);
89-
expect([...space.headRef.invariants].sort()).toEqual(
90-
[...space.migrations[0]!.metadata.providedInvariants].sort(),
91-
);
97+
// The head invariant set is the union of all applied baselines' invariants.
98+
const union = space.migrations.flatMap((m) => [...m.metadata.providedInvariants]).sort();
99+
expect([...space.headRef.invariants].sort()).toEqual(union);
92100
});
93101

94102
it('self-consistency check passes — headRef.hash matches re-derived storage hash', () => {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from 'vitest'
2+
import cipherstashExtensionDescriptor from '../../src/exports/control'
3+
import {
4+
CIPHERSTASH_STRING_V3_CODEC_ID,
5+
CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
6+
} from '../../src/extension-metadata/constants'
7+
import { cipherstashStringV3CodecHooks } from '../../src/migration/codec-hooks-v3'
8+
9+
describe('cipherstash control descriptor (v3)', () => {
10+
it('registers cipherstashStringV3CodecHooks keyed by the v3 codec id', () => {
11+
const hooks = (
12+
cipherstashExtensionDescriptor as unknown as {
13+
types: { codecTypes: { controlPlaneHooks: Record<string, unknown> } }
14+
}
15+
).types.codecTypes.controlPlaneHooks
16+
expect(hooks[CIPHERSTASH_STRING_V3_CODEC_ID]).toBe(cipherstashStringV3CodecHooks)
17+
})
18+
19+
it('includes the v3 baseline migration in the contract space', () => {
20+
const cs = (
21+
cipherstashExtensionDescriptor as unknown as {
22+
contractSpace: { migrations: ReadonlyArray<{ dirName: string }> }
23+
}
24+
).contractSpace
25+
expect(cs.migrations.map((m) => m.dirName)).toContain(CIPHERSTASH_V3_BASELINE_MIGRATION_NAME)
26+
})
27+
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { createCipherstashRuntimeDescriptor } from '../../src/exports/runtime'
3+
import { makeFakeSdk } from './helpers/fake-sdk'
4+
5+
describe('v3 runtime descriptor', () => {
6+
it('advertises the v3 string codec and the v3-capable operators', () => {
7+
const desc = createCipherstashRuntimeDescriptor({ sdk: makeFakeSdk() })
8+
expect(desc.codecs?.().map((c: { codecId: string }) => c.codecId)).toContain('cipherstash/string-v3@1')
9+
// cipherstashEq attaches to the v3 codec via the shared cipherstash:string trait.
10+
expect(Object.keys(desc.queryOperations?.() ?? {})).toContain('cipherstashEq')
11+
})
12+
})
13+
14+
describe('v3 public re-exports', () => {
15+
it('exposes the v3 surface from @cipherstash/prisma-next/runtime', async () => {
16+
const runtime = await import('../../src/exports/runtime')
17+
expect(typeof runtime.createCipherstashStringV3Codec).toBe('function')
18+
expect(typeof runtime.queryTypeForIndex).toBe('function')
19+
expect(runtime.CIPHERSTASH_STRING_V3_CODEC_ID).toBe('cipherstash/string-v3@1')
20+
expect(typeof runtime.encryptedStringV3ParamsSchema).toBe('function')
21+
expect(typeof runtime.renderEncryptedStringV3OutputType).toBe('function')
22+
})
23+
24+
it('exposes encryptedStringV3 from @cipherstash/prisma-next/column-types', async () => {
25+
const columnTypes = await import('../../src/exports/column-types')
26+
expect(typeof columnTypes.encryptedStringV3).toBe('function')
27+
})
28+
29+
it('exposes bulkEncryptV3Middleware from @cipherstash/prisma-next/middleware', async () => {
30+
const mw = await import('../../src/exports/middleware')
31+
expect(typeof mw.bulkEncryptV3Middleware).toBe('function')
32+
})
33+
})

0 commit comments

Comments
 (0)