|
| 1 | +/** |
| 2 | + * v3 codec ids, traits, and migration invariants. Parallel to the v2 |
| 3 | + * `constants.ts`; imports NO v2 codec/wire code. Codec ids are the generated |
| 4 | + * closed union derived from the imported catalog: `(codec id) ⟺ (public.* |
| 5 | + * domain)` is bijective, so the id set IS the catalog. |
| 6 | + */ |
| 7 | +import type { AnyEncryptedV3Column, QueryCapabilities } from '@cipherstash/stack/eql/v3' |
| 8 | +import { |
| 9 | + CIPHERSTASH_TRAIT_EQUALITY, |
| 10 | + CIPHERSTASH_TRAIT_FREE_TEXT_SEARCH, |
| 11 | + CIPHERSTASH_TRAIT_ORDER_AND_RANGE, |
| 12 | + CIPHERSTASH_TRAIT_SEARCHABLE_JSON, |
| 13 | +} from './constants' |
| 14 | + |
| 15 | +export { |
| 16 | + CIPHERSTASH_TRAIT_EQUALITY, |
| 17 | + CIPHERSTASH_TRAIT_FREE_TEXT_SEARCH, |
| 18 | + CIPHERSTASH_TRAIT_ORDER_AND_RANGE, |
| 19 | + CIPHERSTASH_TRAIT_SEARCHABLE_JSON, |
| 20 | +} |
| 21 | + |
| 22 | +/** Strip the `public.` schema qualifier at the type level. */ |
| 23 | +type StripSchema<T> = T extends `public.${infer D}` ? D : never |
| 24 | + |
| 25 | +/** |
| 26 | + * Per-column literal `eqlType`, inferred from the `getEqlType()` return type |
| 27 | + * rather than the stack's `EqlTypeForColumn<C>`. NOT a style choice: the |
| 28 | + * stack's bundled `.d.ts` emits the base class's private `definition` field |
| 29 | + * without its type, so `C extends EncryptedV3Column<infer D>` (which |
| 30 | + * `EqlTypeForColumn` uses) has nothing to infer `D` from and falls back to the |
| 31 | + * constraint — widening every id to `` `public.${string}` `` and silently |
| 32 | + * disabling the closed-union drift guards below. The getter's declared return |
| 33 | + * type survives d.ts bundling because each concrete subclass binds `D` |
| 34 | + * explicitly in its `extends` clause. |
| 35 | + */ |
| 36 | +type EqlTypeFromGetter<C> = C extends { getEqlType(): infer T } ? T : never |
| 37 | + |
| 38 | +/** Compile-time closed union: one concrete codec id per catalog domain. A |
| 39 | + * catalog change that adds/drops a domain surfaces as a type error at the |
| 40 | + * `satisfies` line below. The domain segment is the GA registry key VERBATIM |
| 41 | + * (`eql_v3_*`); the leading `eql-v3` path token is the logical version tag. */ |
| 42 | +export type CipherstashV3CodecId = |
| 43 | + `cipherstash/eql-v3/${StripSchema<EqlTypeFromGetter<AnyEncryptedV3Column>>}@1` |
| 44 | + |
| 45 | +/** |
| 46 | + * PINNED literal tuple — the compile-time source of truth for the v3 codec-id |
| 47 | + * set. Hand-listed, one entry per catalog domain (INCLUDING the `*_ord_ore` |
| 48 | + * variants that authoring does not expose but the codec/derivation layer still |
| 49 | + * decodes). This is deliberately NOT `V3_CODEC_IDS as readonly CipherstashV3CodecId[]` |
| 50 | + * — that is a masking cast that asserts the derived array IS the union and so |
| 51 | + * catches NO drift. Here, catalog drift fails to COMPILE in both directions: |
| 52 | + * - `satisfies readonly CipherstashV3CodecId[]` fails if a listed id is no |
| 53 | + * longer a catalog domain (a domain removed / renamed). |
| 54 | + * - `_NoUnpinnedDomain` (below) fails if the catalog gained a domain not |
| 55 | + * pinned here. |
| 56 | + * The runtime drift test additionally asserts this equals the registry-derived |
| 57 | + * `V3_CODEC_IDS` (guarding the derivation itself). |
| 58 | + */ |
| 59 | +export const CIPHERSTASH_V3_CODEC_IDS = [ |
| 60 | + 'cipherstash/eql-v3/eql_v3_integer@1', |
| 61 | + 'cipherstash/eql-v3/eql_v3_integer_eq@1', |
| 62 | + 'cipherstash/eql-v3/eql_v3_integer_ord_ore@1', |
| 63 | + 'cipherstash/eql-v3/eql_v3_integer_ord@1', |
| 64 | + 'cipherstash/eql-v3/eql_v3_smallint@1', |
| 65 | + 'cipherstash/eql-v3/eql_v3_smallint_eq@1', |
| 66 | + 'cipherstash/eql-v3/eql_v3_smallint_ord_ore@1', |
| 67 | + 'cipherstash/eql-v3/eql_v3_smallint_ord@1', |
| 68 | + 'cipherstash/eql-v3/eql_v3_bigint@1', |
| 69 | + 'cipherstash/eql-v3/eql_v3_bigint_eq@1', |
| 70 | + 'cipherstash/eql-v3/eql_v3_bigint_ord_ore@1', |
| 71 | + 'cipherstash/eql-v3/eql_v3_bigint_ord@1', |
| 72 | + 'cipherstash/eql-v3/eql_v3_date@1', |
| 73 | + 'cipherstash/eql-v3/eql_v3_date_eq@1', |
| 74 | + 'cipherstash/eql-v3/eql_v3_date_ord_ore@1', |
| 75 | + 'cipherstash/eql-v3/eql_v3_date_ord@1', |
| 76 | + 'cipherstash/eql-v3/eql_v3_timestamp@1', |
| 77 | + 'cipherstash/eql-v3/eql_v3_timestamp_eq@1', |
| 78 | + 'cipherstash/eql-v3/eql_v3_timestamp_ord_ore@1', |
| 79 | + 'cipherstash/eql-v3/eql_v3_timestamp_ord@1', |
| 80 | + 'cipherstash/eql-v3/eql_v3_numeric@1', |
| 81 | + 'cipherstash/eql-v3/eql_v3_numeric_eq@1', |
| 82 | + 'cipherstash/eql-v3/eql_v3_numeric_ord_ore@1', |
| 83 | + 'cipherstash/eql-v3/eql_v3_numeric_ord@1', |
| 84 | + 'cipherstash/eql-v3/eql_v3_text@1', |
| 85 | + 'cipherstash/eql-v3/eql_v3_text_eq@1', |
| 86 | + 'cipherstash/eql-v3/eql_v3_text_match@1', |
| 87 | + 'cipherstash/eql-v3/eql_v3_text_ord_ore@1', |
| 88 | + 'cipherstash/eql-v3/eql_v3_text_ord@1', |
| 89 | + 'cipherstash/eql-v3/eql_v3_text_search@1', |
| 90 | + 'cipherstash/eql-v3/eql_v3_boolean@1', |
| 91 | + 'cipherstash/eql-v3/eql_v3_real@1', |
| 92 | + 'cipherstash/eql-v3/eql_v3_real_eq@1', |
| 93 | + 'cipherstash/eql-v3/eql_v3_real_ord_ore@1', |
| 94 | + 'cipherstash/eql-v3/eql_v3_real_ord@1', |
| 95 | + 'cipherstash/eql-v3/eql_v3_double@1', |
| 96 | + 'cipherstash/eql-v3/eql_v3_double_eq@1', |
| 97 | + 'cipherstash/eql-v3/eql_v3_double_ord_ore@1', |
| 98 | + 'cipherstash/eql-v3/eql_v3_double_ord@1', |
| 99 | + 'cipherstash/eql-v3/eql_v3_json@1', |
| 100 | +] as const satisfies readonly CipherstashV3CodecId[] |
| 101 | + |
| 102 | +// Bidirectional drift guard — the direction `satisfies` cannot catch. If the |
| 103 | +// catalog gains a domain that is not pinned above, `Exclude<...>` is non-`never` |
| 104 | +// and this assignment fails to compile with the offending id in the error. |
| 105 | +type _PinnedId = (typeof CIPHERSTASH_V3_CODEC_IDS)[number] |
| 106 | +type _NoUnpinnedDomain = [Exclude<CipherstashV3CodecId, _PinnedId>] extends [never] |
| 107 | + ? true |
| 108 | + : ['catalog gained an unpinned codec id', Exclude<CipherstashV3CodecId, _PinnedId>] |
| 109 | +const _driftGuard: _NoUnpinnedDomain = true |
| 110 | +void _driftGuard |
| 111 | + |
| 112 | +export const CIPHERSTASH_V3_CODEC_ID_SET: ReadonlySet<string> = new Set(CIPHERSTASH_V3_CODEC_IDS) |
| 113 | + |
| 114 | +export function isCipherstashV3CodecId(id: string): id is CipherstashV3CodecId { |
| 115 | + return CIPHERSTASH_V3_CODEC_ID_SET.has(id) |
| 116 | +} |
| 117 | + |
| 118 | +export function v3TraitsForCapabilities(caps: QueryCapabilities): readonly string[] { |
| 119 | + const traits: string[] = [] |
| 120 | + if (caps.equality) traits.push(CIPHERSTASH_TRAIT_EQUALITY) |
| 121 | + if (caps.orderAndRange) traits.push(CIPHERSTASH_TRAIT_ORDER_AND_RANGE) |
| 122 | + if (caps.freeTextSearch) traits.push(CIPHERSTASH_TRAIT_FREE_TEXT_SEARCH) |
| 123 | + if (caps.searchableJson) traits.push(CIPHERSTASH_TRAIT_SEARCHABLE_JSON) |
| 124 | + return traits |
| 125 | +} |
| 126 | + |
| 127 | +export const CIPHERSTASH_V3_BASELINE_MIGRATION_NAME = '20260601T0100_install_eql_v3_bundle' |
| 128 | + |
| 129 | +export const CIPHERSTASH_V3_INVARIANTS = { |
| 130 | + installBundle: 'cipherstash:install-eql-v3-bundle-v1', |
| 131 | +} as const |
| 132 | + |
| 133 | +// v3's OWN extension identity (decision 1b) — DISTINCT from the v2 |
| 134 | +// CIPHERSTASH_SPACE_ID / CIPHERSTASH_EXTENSION_VERSION. v2 and v3 are separate |
| 135 | +// entry points, never co-registered in one client; the distinct id keeps the |
| 136 | +// two extension descriptors cleanly separate. The shape mirrors the v2 id |
| 137 | +// (a bare, path-safe token — it may be used as a directory segment) rather |
| 138 | +// than a slashed namespace. |
| 139 | +export const CIPHERSTASH_V3_SPACE_ID = 'cipherstash-eql-v3' as const |
| 140 | +export const CIPHERSTASH_V3_EXTENSION_VERSION = '1.0.0' as const |
0 commit comments