@cipherstash/stack 0.18.0 → 0.19.0

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +2 -2
  3. package/dist/{chunk-ZYOZHZMJ.js → chunk-36AA7IBJ.js} +151 -160
  4. package/dist/chunk-36AA7IBJ.js.map +1 -0
  5. package/dist/{chunk-VPOTNP4K.js → chunk-4AVL4VZD.js} +1 -1
  6. package/dist/chunk-4AVL4VZD.js.map +1 -0
  7. package/dist/{chunk-XWPGAHBZ.js → chunk-BNO32ZMV.js} +1 -1
  8. package/dist/chunk-BNO32ZMV.js.map +1 -0
  9. package/dist/{chunk-LLCCNKDL.js → chunk-HQANMV7R.js} +3 -3
  10. package/dist/{chunk-LLCCNKDL.js.map → chunk-HQANMV7R.js.map} +1 -1
  11. package/dist/chunk-IADZCZEA.js +23 -0
  12. package/dist/chunk-IADZCZEA.js.map +1 -0
  13. package/dist/chunk-IBSK6P33.js +209 -0
  14. package/dist/chunk-IBSK6P33.js.map +1 -0
  15. package/dist/chunk-JSG2AMDI.js +17 -0
  16. package/dist/chunk-JSG2AMDI.js.map +1 -0
  17. package/dist/chunk-MP3SSDNN.js +4439 -0
  18. package/dist/chunk-MP3SSDNN.js.map +1 -0
  19. package/dist/chunk-OFQ555AX.js +191 -0
  20. package/dist/chunk-OFQ555AX.js.map +1 -0
  21. package/dist/chunk-PZ5AY32C.js +10 -0
  22. package/dist/chunk-PZ5AY32C.js.map +1 -0
  23. package/dist/chunk-U66S7VIF.js +435 -0
  24. package/dist/chunk-U66S7VIF.js.map +1 -0
  25. package/dist/{client-Dt98Rugc.d.cts → client-DSGHBN-g.d.cts} +200 -58
  26. package/dist/{client-CIsfZ2zg.d.ts → client-DfCrlHXh.d.ts} +200 -58
  27. package/dist/client.cjs +4101 -43
  28. package/dist/client.cjs.map +1 -1
  29. package/dist/client.d.cts +5 -5
  30. package/dist/client.d.ts +5 -5
  31. package/dist/client.js +3 -1
  32. package/dist/drizzle/index.cjs +4133 -74
  33. package/dist/drizzle/index.cjs.map +1 -1
  34. package/dist/drizzle/index.d.cts +33 -33
  35. package/dist/drizzle/index.d.ts +33 -33
  36. package/dist/drizzle/index.js +35 -30
  37. package/dist/drizzle/index.js.map +1 -1
  38. package/dist/dynamodb/index.cjs +19 -12
  39. package/dist/dynamodb/index.cjs.map +1 -1
  40. package/dist/dynamodb/index.d.cts +2 -2
  41. package/dist/dynamodb/index.d.ts +2 -2
  42. package/dist/dynamodb/index.js +8 -10
  43. package/dist/dynamodb/index.js.map +1 -1
  44. package/dist/encryption/index.cjs +4448 -212
  45. package/dist/encryption/index.cjs.map +1 -1
  46. package/dist/encryption/index.d.cts +3 -3
  47. package/dist/encryption/index.d.ts +3 -3
  48. package/dist/encryption/index.js +15 -7
  49. package/dist/encryption/v3.cjs +7791 -0
  50. package/dist/encryption/v3.cjs.map +1 -0
  51. package/dist/encryption/v3.d.cts +100 -0
  52. package/dist/encryption/v3.d.ts +100 -0
  53. package/dist/encryption/v3.js +161 -0
  54. package/dist/encryption/v3.js.map +1 -0
  55. package/dist/eql/v3/index.cjs +612 -0
  56. package/dist/eql/v3/index.cjs.map +1 -0
  57. package/dist/eql/v3/index.d.cts +71 -0
  58. package/dist/eql/v3/index.d.ts +71 -0
  59. package/dist/eql/v3/index.js +89 -0
  60. package/dist/eql/v3/index.js.map +1 -0
  61. package/dist/errors/index.js +1 -0
  62. package/dist/identity/index.cjs +55 -35
  63. package/dist/identity/index.cjs.map +1 -1
  64. package/dist/identity/index.d.cts +54 -30
  65. package/dist/identity/index.d.ts +54 -30
  66. package/dist/identity/index.js +9 -177
  67. package/dist/identity/index.js.map +1 -1
  68. package/dist/index.cjs +4480 -228
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.d.cts +18 -7
  71. package/dist/index.d.ts +18 -7
  72. package/dist/index.js +20 -5
  73. package/dist/index.js.map +1 -1
  74. package/dist/schema/index.cjs +4105 -43
  75. package/dist/schema/index.cjs.map +1 -1
  76. package/dist/schema/index.d.cts +1 -1
  77. package/dist/schema/index.d.ts +1 -1
  78. package/dist/schema/index.js +3 -1
  79. package/dist/supabase/index.cjs +4500 -75
  80. package/dist/supabase/index.cjs.map +1 -1
  81. package/dist/supabase/index.d.cts +113 -34
  82. package/dist/supabase/index.d.ts +113 -34
  83. package/dist/supabase/index.js +350 -32
  84. package/dist/supabase/index.js.map +1 -1
  85. package/dist/table-CIH7jZ2h.d.ts +603 -0
  86. package/dist/table-DihEAlxG.d.cts +603 -0
  87. package/dist/{types-public-BTxqjmtv.d.ts → types-public-CpS5KjwX.d.cts} +250 -92
  88. package/dist/{types-public-BTxqjmtv.d.cts → types-public-CpS5KjwX.d.ts} +250 -92
  89. package/dist/types-public.cjs.map +1 -1
  90. package/dist/types-public.d.cts +2 -2
  91. package/dist/types-public.d.ts +2 -2
  92. package/dist/types-public.js +2 -1
  93. package/dist/wasm-inline.d.ts +1269 -0
  94. package/dist/wasm-inline.js +4594 -0
  95. package/dist/wasm-inline.js.map +1 -0
  96. package/package.json +54 -4
  97. package/dist/chunk-VPOTNP4K.js.map +0 -1
  98. package/dist/chunk-XWPGAHBZ.js.map +0 -1
  99. package/dist/chunk-YWQXTZ5M.js +0 -383
  100. package/dist/chunk-YWQXTZ5M.js.map +0 -1
  101. package/dist/chunk-ZYOZHZMJ.js.map +0 -1
@@ -1,13 +1,18 @@
1
+ import {
2
+ EncryptedV3Column
3
+ } from "../chunk-U66S7VIF.js";
1
4
  import {
2
5
  bulkModelsToEncryptedPgComposites,
3
6
  modelToEncryptedPgComposites
4
- } from "../chunk-XWPGAHBZ.js";
7
+ } from "../chunk-BNO32ZMV.js";
5
8
  import {
6
9
  EncryptedColumn
7
- } from "../chunk-YWQXTZ5M.js";
10
+ } from "../chunk-MP3SSDNN.js";
11
+ import "../chunk-IADZCZEA.js";
8
12
  import {
9
13
  logger
10
- } from "../chunk-LLCCNKDL.js";
14
+ } from "../chunk-HQANMV7R.js";
15
+ import "../chunk-PZ5AY32C.js";
11
16
 
12
17
  // src/supabase/helpers.ts
13
18
  function getEncryptedColumnNames(schema) {
@@ -35,6 +40,40 @@ function addJsonbCasts(columns, encryptedColumnNames) {
35
40
  return col;
36
41
  }).join(",");
37
42
  }
43
+ function addJsonbCastsV3(columns, propToDb) {
44
+ const dbNames = new Set(Object.values(propToDb));
45
+ const keyToDb = {};
46
+ const select = columns.split(",").map((col) => {
47
+ const trimmed = col.trim();
48
+ if (!trimmed) return col;
49
+ if (trimmed.includes("::")) return col;
50
+ if (trimmed.includes("(") || trimmed.includes(".")) return col;
51
+ const leadingWhitespace = col.match(/^(\s*)/)?.[1] ?? "";
52
+ const aliasMatch = trimmed.match(
53
+ /^([A-Za-z_][A-Za-z0-9_]*):([A-Za-z_][A-Za-z0-9_]*)$/
54
+ );
55
+ if (aliasMatch) {
56
+ const [, alias, name] = aliasMatch;
57
+ const db2 = propToDb[name] ?? (dbNames.has(name) ? name : void 0);
58
+ if (db2 !== void 0) {
59
+ keyToDb[alias] = db2;
60
+ return `${leadingWhitespace}${alias}:${db2}::jsonb`;
61
+ }
62
+ return col;
63
+ }
64
+ const db = propToDb[trimmed];
65
+ if (db !== void 0) {
66
+ keyToDb[trimmed] = db;
67
+ return db === trimmed ? `${leadingWhitespace}${trimmed}::jsonb` : `${leadingWhitespace}${trimmed}:${db}::jsonb`;
68
+ }
69
+ if (dbNames.has(trimmed)) {
70
+ keyToDb[trimmed] = trimmed;
71
+ return `${leadingWhitespace}${trimmed}::jsonb`;
72
+ }
73
+ return col;
74
+ }).join(",");
75
+ return { select, keyToDb };
76
+ }
38
77
  function mapFilterOpToQueryType(op) {
39
78
  switch (op) {
40
79
  case "eq":
@@ -388,7 +427,7 @@ var EncryptedQueryBuilderImpl = class {
388
427
  result2.failure
389
428
  );
390
429
  }
391
- return bulkModelsToEncryptedPgComposites(result2.data);
430
+ return this.transformEncryptedMutationModels(result2.data);
392
431
  }
393
432
  const baseOp = this.encryptionClient.encryptModel(data, this.schema);
394
433
  const op = this.lockContext ? baseOp.withLockContext(this.lockContext) : baseOp;
@@ -403,7 +442,20 @@ var EncryptedQueryBuilderImpl = class {
403
442
  result.failure
404
443
  );
405
444
  }
406
- return modelToEncryptedPgComposites(result.data);
445
+ return this.transformEncryptedMutationModel(result.data);
446
+ }
447
+ /**
448
+ * Encode an encrypted model for the Supabase request body. v2 wraps each
449
+ * encrypted value in the `{ data: ... }` object expected by the
450
+ * `eql_v2_encrypted` composite type. The v3 dialect overrides this — native
451
+ * `eql_v3.*` domains are plain jsonb, so the raw payload is sent instead
452
+ * (keyed by DB column name).
453
+ */
454
+ transformEncryptedMutationModel(model) {
455
+ return modelToEncryptedPgComposites(model);
456
+ }
457
+ transformEncryptedMutationModels(models) {
458
+ return bulkModelsToEncryptedPgComposites(models);
407
459
  }
408
460
  // ---------------------------------------------------------------------------
409
461
  // Step 2: Build select string with casts
@@ -436,7 +488,6 @@ var EncryptedQueryBuilderImpl = class {
436
488
  termMap.push({ source: "filter", filterIndex: i, inIndex: j });
437
489
  }
438
490
  } else if (f.op === "is") {
439
- continue;
440
491
  } else {
441
492
  terms.push({
442
493
  value: f.value,
@@ -536,6 +587,17 @@ var EncryptedQueryBuilderImpl = class {
536
587
  if (terms.length === 0) {
537
588
  return { encryptedValues: [], termMap: [] };
538
589
  }
590
+ const encryptedValues = await this.encryptCollectedTerms(terms);
591
+ return { encryptedValues, termMap };
592
+ }
593
+ /**
594
+ * Encrypt the collected filter terms, returning one encoded value per term
595
+ * (in order). v2 batch-encrypts via `encryptQuery` with the
596
+ * `composite-literal` return type — the `("json")` string the
597
+ * `eql_v2_encrypted` composite operators compare. The v3 dialect overrides
598
+ * this to produce full-envelope jsonb operands instead.
599
+ */
600
+ async encryptCollectedTerms(terms) {
539
601
  const baseOp = this.encryptionClient.encryptQuery(terms);
540
602
  const op = this.lockContext ? baseOp.withLockContext(this.lockContext) : baseOp;
541
603
  if (this.auditConfig) op.audit(this.auditConfig);
@@ -549,7 +611,7 @@ var EncryptedQueryBuilderImpl = class {
549
611
  result.failure
550
612
  );
551
613
  }
552
- return { encryptedValues: result.data, termMap };
614
+ return result.data;
553
615
  }
554
616
  // ---------------------------------------------------------------------------
555
617
  // Step 4: Build and execute real Supabase query
@@ -669,36 +731,36 @@ var EncryptedQueryBuilderImpl = class {
669
731
  return filterInMap.has(key) ? filterInMap.get(key) : v;
670
732
  });
671
733
  }
734
+ const column = this.filterColumnName(f.column);
735
+ const wasEncrypted = filterValueMap.has(i);
672
736
  switch (f.op) {
673
737
  case "eq":
674
- q = q.eq(f.column, value);
738
+ q = q.eq(column, value);
675
739
  break;
676
740
  case "neq":
677
- q = q.neq(f.column, value);
741
+ q = q.neq(column, value);
678
742
  break;
679
743
  case "gt":
680
- q = q.gt(f.column, value);
744
+ q = q.gt(column, value);
681
745
  break;
682
746
  case "gte":
683
- q = q.gte(f.column, value);
747
+ q = q.gte(column, value);
684
748
  break;
685
749
  case "lt":
686
- q = q.lt(f.column, value);
750
+ q = q.lt(column, value);
687
751
  break;
688
752
  case "lte":
689
- q = q.lte(f.column, value);
753
+ q = q.lte(column, value);
690
754
  break;
691
755
  case "like":
692
- q = q.like(f.column, value);
693
- break;
694
756
  case "ilike":
695
- q = q.ilike(f.column, value);
757
+ q = this.applyPatternFilter(q, column, f.op, value, wasEncrypted);
696
758
  break;
697
759
  case "is":
698
- q = q.is(f.column, value);
760
+ q = q.is(column, value);
699
761
  break;
700
762
  case "in":
701
- q = q.in(f.column, value);
763
+ q = q.in(column, value);
702
764
  break;
703
765
  }
704
766
  }
@@ -707,53 +769,113 @@ var EncryptedQueryBuilderImpl = class {
707
769
  const resolvedQuery = {};
708
770
  for (const [colName, originalValue] of Object.entries(mf.query)) {
709
771
  const key = `${i}:${colName}`;
710
- resolvedQuery[colName] = matchValueMap.has(key) ? matchValueMap.get(key) : originalValue;
772
+ resolvedQuery[this.filterColumnName(colName)] = matchValueMap.has(key) ? matchValueMap.get(key) : originalValue;
711
773
  }
712
774
  q = q.match(resolvedQuery);
713
775
  }
714
776
  for (let i = 0; i < this.notFilters.length; i++) {
715
777
  const nf = this.notFilters[i];
716
- const value = notValueMap.has(i) ? notValueMap.get(i) : nf.value;
717
- q = q.not(nf.column, nf.op, value);
778
+ const wasEncrypted = notValueMap.has(i);
779
+ const value = wasEncrypted ? notValueMap.get(i) : nf.value;
780
+ q = q.not(
781
+ this.filterColumnName(nf.column),
782
+ this.notFilterOperator(nf.op, wasEncrypted),
783
+ value
784
+ );
718
785
  }
719
786
  for (let i = 0; i < this.orFilters.length; i++) {
720
787
  const of_ = this.orFilters[i];
721
788
  if (of_.kind === "string") {
722
789
  const parsed = parseOrString(of_.value);
723
- let hasEncrypted = false;
790
+ const encryptedIndexes = /* @__PURE__ */ new Set();
724
791
  for (let j = 0; j < parsed.length; j++) {
725
792
  const key = `${i}:${j}`;
726
793
  if (orStringConditionMap.has(key)) {
727
794
  parsed[j] = { ...parsed[j], value: orStringConditionMap.get(key) };
728
- hasEncrypted = true;
795
+ encryptedIndexes.add(j);
729
796
  }
730
797
  }
731
- if (hasEncrypted) {
732
- q = q.or(rebuildOrString(parsed), {
733
- referencedTable: of_.referencedTable
734
- });
798
+ if (encryptedIndexes.size > 0) {
799
+ q = q.or(
800
+ rebuildOrString(
801
+ this.transformOrConditions(parsed, encryptedIndexes)
802
+ ),
803
+ {
804
+ referencedTable: of_.referencedTable
805
+ }
806
+ );
735
807
  } else {
736
808
  q = q.or(of_.value, { referencedTable: of_.referencedTable });
737
809
  }
738
810
  } else {
811
+ const encryptedIndexes = /* @__PURE__ */ new Set();
739
812
  const conditions = of_.conditions.map((cond, j) => {
740
813
  const key = `${i}:${j}`;
741
814
  if (orStructuredConditionMap.has(key)) {
815
+ encryptedIndexes.add(j);
742
816
  return { ...cond, value: orStructuredConditionMap.get(key) };
743
817
  }
744
818
  return cond;
745
819
  });
746
- q = q.or(rebuildOrString(conditions));
820
+ q = q.or(
821
+ rebuildOrString(
822
+ this.transformOrConditions(conditions, encryptedIndexes)
823
+ )
824
+ );
747
825
  }
748
826
  }
749
827
  for (let i = 0; i < this.rawFilters.length; i++) {
750
828
  const rf = this.rawFilters[i];
751
829
  const value = rawValueMap.has(i) ? rawValueMap.get(i) : rf.value;
752
- q = q.filter(rf.column, rf.operator, value);
830
+ q = q.filter(this.filterColumnName(rf.column), rf.operator, value);
753
831
  }
754
832
  return q;
755
833
  }
756
834
  // ---------------------------------------------------------------------------
835
+ // Dialect seams — every default preserves the v2 behaviour byte-for-byte.
836
+ // The v3 builder (see ./query-builder-v3) overrides these for native
837
+ // `eql_v3.*` domain columns.
838
+ // ---------------------------------------------------------------------------
839
+ /**
840
+ * Map a filter's column name to the DB column name PostgREST must see.
841
+ * v2 schemas key columns by their DB name already, so this is the identity;
842
+ * the v3 dialect resolves a JS property name to its DB name.
843
+ */
844
+ filterColumnName(column) {
845
+ return column;
846
+ }
847
+ /**
848
+ * Apply a `like`/`ilike` filter. v2 relies on the `~~` operator defined on
849
+ * `eql_v2_encrypted`; the v3 dialect overrides this for encrypted columns
850
+ * because the `eql_v3.*` domains expose free-text match via `@>`
851
+ * (PostgREST `cs`) rather than a LIKE operator.
852
+ */
853
+ applyPatternFilter(q, column, op, value, _wasEncrypted) {
854
+ return op === "like" ? q.like(column, value) : q.ilike(column, value);
855
+ }
856
+ /**
857
+ * The PostgREST operator to use for a `.not()` filter. The v3 dialect maps
858
+ * `like`/`ilike` on encrypted columns to `cs` (see applyPatternFilter).
859
+ */
860
+ notFilterOperator(op, _wasEncrypted) {
861
+ return op;
862
+ }
863
+ /**
864
+ * Transform `.or()` conditions before the or-string is rebuilt. The v3
865
+ * dialect maps property names to DB names and `like`/`ilike` on encrypted
866
+ * conditions to `cs`.
867
+ */
868
+ transformOrConditions(conditions, _encryptedIndexes) {
869
+ return conditions;
870
+ }
871
+ /**
872
+ * Post-process a decrypted result row. The v3 dialect reconstructs `Date`
873
+ * values from the encrypt-config `cast_as`; v2 returns rows unchanged.
874
+ */
875
+ postprocessDecryptedRow(row) {
876
+ return row;
877
+ }
878
+ // ---------------------------------------------------------------------------
757
879
  // Step 5: Decrypt results
758
880
  // ---------------------------------------------------------------------------
759
881
  async decryptResults(result) {
@@ -817,7 +939,9 @@ var EncryptedQueryBuilderImpl = class {
817
939
  );
818
940
  }
819
941
  return {
820
- data: decrypted2.data,
942
+ data: this.postprocessDecryptedRow(
943
+ decrypted2.data
944
+ ),
821
945
  error: null,
822
946
  count: result.count ?? null,
823
947
  status: result.status,
@@ -848,7 +972,9 @@ var EncryptedQueryBuilderImpl = class {
848
972
  );
849
973
  }
850
974
  return {
851
- data: decrypted.data,
975
+ data: decrypted.data.map(
976
+ (row) => this.postprocessDecryptedRow(row)
977
+ ),
852
978
  error: null,
853
979
  count: result.count ?? null,
854
980
  status: result.status,
@@ -879,6 +1005,184 @@ var EncryptionFailedError = class extends Error {
879
1005
  }
880
1006
  };
881
1007
 
1008
+ // src/supabase/query-builder-v3.ts
1009
+ var EncryptedQueryBuilderV3Impl = class extends EncryptedQueryBuilderImpl {
1010
+ v3Table;
1011
+ /** JS property name → DB column name, for every encrypted column. */
1012
+ propToDb;
1013
+ /** Built column schemas keyed by DB column name (for `cast_as`). */
1014
+ columnSchemas;
1015
+ /** Column builders keyed by BOTH property name and DB name. */
1016
+ v3Columns;
1017
+ /**
1018
+ * Result-row key → DB column name for the columns the current select
1019
+ * produces, including user-chosen PostgREST aliases (`ts:created_at` keys
1020
+ * rows by `ts`). Populated by {@link buildSelectString}; consumed by
1021
+ * {@link postprocessDecryptedRow} so aliased date columns still get `Date`
1022
+ * reconstruction.
1023
+ */
1024
+ selectKeyToDb = {};
1025
+ constructor(tableName, table, encryptionClient, supabaseClient) {
1026
+ super(
1027
+ tableName,
1028
+ // The base class only ever calls BuildableTable members on the schema
1029
+ // (build / encryptModel plumbing); every v2-specific behaviour is
1030
+ // overridden below.
1031
+ table,
1032
+ encryptionClient,
1033
+ supabaseClient
1034
+ );
1035
+ this.v3Table = table;
1036
+ this.propToDb = table.buildColumnKeyMap();
1037
+ this.columnSchemas = table.build().columns;
1038
+ this.v3Columns = {};
1039
+ for (const [property, builder] of Object.entries(table.columnBuilders)) {
1040
+ if (builder instanceof EncryptedV3Column) {
1041
+ const col = builder;
1042
+ this.v3Columns[property] = col;
1043
+ this.v3Columns[col.getName()] = col;
1044
+ }
1045
+ }
1046
+ this.encryptedColumnNames = Object.keys(this.v3Columns);
1047
+ }
1048
+ // ---------------------------------------------------------------------------
1049
+ // Dialect overrides
1050
+ // ---------------------------------------------------------------------------
1051
+ getColumnMap() {
1052
+ return this.v3Columns;
1053
+ }
1054
+ filterColumnName(column) {
1055
+ return this.propToDb[column] ?? column;
1056
+ }
1057
+ buildSelectString() {
1058
+ if (this.selectColumns === null) return null;
1059
+ const { select, keyToDb } = addJsonbCastsV3(
1060
+ this.selectColumns,
1061
+ this.propToDb
1062
+ );
1063
+ this.selectKeyToDb = keyToDb;
1064
+ return select;
1065
+ }
1066
+ /** v3 domains are plain jsonb — send the raw payload, keyed by DB name. */
1067
+ transformEncryptedMutationModel(model) {
1068
+ const out = {};
1069
+ for (const [key, value] of Object.entries(model)) {
1070
+ out[this.propToDb[key] ?? key] = value;
1071
+ }
1072
+ return out;
1073
+ }
1074
+ transformEncryptedMutationModels(models) {
1075
+ return models.map((model) => this.transformEncryptedMutationModel(model));
1076
+ }
1077
+ /**
1078
+ * Encrypt every filter operand as a full storage envelope (see the class
1079
+ * doc for why `encryptQuery` terms cannot be used), serialized to jsonb
1080
+ * text for the PostgREST filter value.
1081
+ *
1082
+ * INTERIM + single swap point. The full-envelope operand is a tracked
1083
+ * workaround (Linear CIP-3402), not the design: it carries a real
1084
+ * decryptable ciphertext `c` plus ALL of the column's index terms, and
1085
+ * PostgREST filters travel in GET query strings — so operands can land in
1086
+ * URL logs, proxies, and Supabase request logs (query terms are meant to
1087
+ * be index-terms-only). When the EQL term-only scalar query envelope (the
1088
+ * scalar analog of `eql_v3.jsonb_query`) ships, swapping the encryption
1089
+ * call inside THIS method — and its `JSON.stringify` wire encoding — must
1090
+ * be the ONLY change needed: every consuming seam (filter application,
1091
+ * `or`/`not` transforms, the wire-operator remap) treats the returned
1092
+ * operand as an opaque, already-encoded string and is encoding-agnostic.
1093
+ * Do not let operand-encoding knowledge leak out of this method.
1094
+ */
1095
+ async encryptCollectedTerms(terms) {
1096
+ return Promise.all(
1097
+ terms.map(async (term) => {
1098
+ const column = term.column;
1099
+ const queryType = term.queryType ?? "equality";
1100
+ const capabilities = column.getQueryCapabilities();
1101
+ if (term.value == null) {
1102
+ throw new Error(
1103
+ `[supabase v3]: cannot encrypt a null filter value for column "${column.getName()}" \u2014 use .is('${column.getName()}', null) for NULL checks`
1104
+ );
1105
+ }
1106
+ if (queryType !== "equality" && queryType !== "orderAndRange" && queryType !== "freeTextSearch") {
1107
+ throw new Error(
1108
+ `[supabase v3]: query type "${queryType}" is not supported on scalar eql_v3 columns`
1109
+ );
1110
+ }
1111
+ if (!capabilities[queryType]) {
1112
+ throw new Error(
1113
+ `[supabase v3]: column "${column.getName()}" (${column.getEqlType()}) does not support ${queryType} queries \u2014 declare the column with a domain that carries that capability`
1114
+ );
1115
+ }
1116
+ const baseOp = this.encryptionClient.encrypt(term.value, {
1117
+ column,
1118
+ table: this.v3Table
1119
+ });
1120
+ const op = this.lockContext ? baseOp.withLockContext(this.lockContext) : baseOp;
1121
+ if (this.auditConfig) op.audit(this.auditConfig);
1122
+ const result = await op;
1123
+ if (result.failure) {
1124
+ logger.error(
1125
+ `Supabase: failed to encrypt query terms for table "${this.tableName}"`
1126
+ );
1127
+ throw new EncryptionFailedError(
1128
+ `Failed to encrypt query terms: ${result.failure.message}`,
1129
+ result.failure
1130
+ );
1131
+ }
1132
+ return JSON.stringify(result.data);
1133
+ })
1134
+ );
1135
+ }
1136
+ /**
1137
+ * The single source of the encrypted wire-operator remap: `like`/`ilike`
1138
+ * on an encrypted column become PostgREST `cs` (bloom-filter `@>`) — the
1139
+ * `eql_v3.*` domains define no LIKE operator. Every pattern-bearing seam
1140
+ * (`applyPatternFilter`, `notFilterOperator`, `transformOrConditions`)
1141
+ * derives its operator here so they cannot drift apart.
1142
+ */
1143
+ encryptedFilterOp(op, wasEncrypted) {
1144
+ return wasEncrypted && (op === "like" || op === "ilike") ? "cs" : op;
1145
+ }
1146
+ /** Encrypted pattern filters go through the bloom-filter `@>` (`cs`). */
1147
+ applyPatternFilter(q, column, op, value, wasEncrypted) {
1148
+ const wireOp = this.encryptedFilterOp(op, wasEncrypted);
1149
+ if (wireOp !== op) {
1150
+ return q.filter(column, wireOp, value);
1151
+ }
1152
+ return super.applyPatternFilter(q, column, op, value, wasEncrypted);
1153
+ }
1154
+ notFilterOperator(op, wasEncrypted) {
1155
+ return this.encryptedFilterOp(op, wasEncrypted);
1156
+ }
1157
+ transformOrConditions(conditions, encryptedIndexes) {
1158
+ return conditions.map((cond, j) => ({
1159
+ ...cond,
1160
+ column: this.filterColumnName(cond.column),
1161
+ op: this.encryptedFilterOp(cond.op, encryptedIndexes.has(j))
1162
+ }));
1163
+ }
1164
+ /** Rebuild `Date` values from the encrypt-config `cast_as`, mirroring the
1165
+ * typed v3 client's decrypt-model path. */
1166
+ postprocessDecryptedRow(row) {
1167
+ const keyToDb = { ...this.selectKeyToDb };
1168
+ for (const [property, dbName] of Object.entries(this.propToDb)) {
1169
+ keyToDb[property] ??= dbName;
1170
+ keyToDb[dbName] ??= dbName;
1171
+ }
1172
+ const out = { ...row };
1173
+ for (const [key, dbName] of Object.entries(keyToDb)) {
1174
+ const castAs = this.columnSchemas[dbName]?.cast_as;
1175
+ if (castAs !== "date" && castAs !== "timestamp") continue;
1176
+ const value = out[key];
1177
+ if (value == null || value instanceof Date) continue;
1178
+ if (typeof value === "string" || typeof value === "number") {
1179
+ out[key] = new Date(value);
1180
+ }
1181
+ }
1182
+ return out;
1183
+ }
1184
+ };
1185
+
882
1186
  // src/supabase/index.ts
883
1187
  function encryptedSupabase(config) {
884
1188
  const { encryptionClient, supabaseClient } = config;
@@ -893,7 +1197,21 @@ function encryptedSupabase(config) {
893
1197
  }
894
1198
  };
895
1199
  }
1200
+ function encryptedSupabaseV3(config) {
1201
+ const { encryptionClient, supabaseClient } = config;
1202
+ return {
1203
+ from(tableName, table) {
1204
+ return new EncryptedQueryBuilderV3Impl(
1205
+ tableName,
1206
+ table,
1207
+ encryptionClient,
1208
+ supabaseClient
1209
+ );
1210
+ }
1211
+ };
1212
+ }
896
1213
  export {
897
- encryptedSupabase
1214
+ encryptedSupabase,
1215
+ encryptedSupabaseV3
898
1216
  };
899
1217
  //# sourceMappingURL=index.js.map