@cipherstash/stack 0.16.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.
- package/CHANGELOG.md +225 -0
- package/README.md +2 -2
- package/dist/{chunk-CY5B4LIF.js → chunk-36AA7IBJ.js} +303 -204
- package/dist/chunk-36AA7IBJ.js.map +1 -0
- package/dist/{chunk-MD6742R6.js → chunk-4AVL4VZD.js} +1 -1
- package/dist/chunk-4AVL4VZD.js.map +1 -0
- package/dist/{chunk-4RNBI3UH.js → chunk-BNO32ZMV.js} +1 -1
- package/dist/chunk-BNO32ZMV.js.map +1 -0
- package/dist/{chunk-6FD7OBGM.js → chunk-HQANMV7R.js} +3 -3
- package/dist/chunk-HQANMV7R.js.map +1 -0
- package/dist/chunk-IADZCZEA.js +23 -0
- package/dist/chunk-IADZCZEA.js.map +1 -0
- package/dist/chunk-IBSK6P33.js +209 -0
- package/dist/chunk-IBSK6P33.js.map +1 -0
- package/dist/chunk-JSG2AMDI.js +17 -0
- package/dist/chunk-JSG2AMDI.js.map +1 -0
- package/dist/chunk-MP3SSDNN.js +4439 -0
- package/dist/chunk-MP3SSDNN.js.map +1 -0
- package/dist/chunk-OFQ555AX.js +191 -0
- package/dist/chunk-OFQ555AX.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-U66S7VIF.js +435 -0
- package/dist/chunk-U66S7VIF.js.map +1 -0
- package/dist/{client-C_7TF8EL.d.cts → client-DSGHBN-g.d.cts} +209 -72
- package/dist/{client-BLbA6aFD.d.ts → client-DfCrlHXh.d.ts} +209 -72
- package/dist/client.cjs +4103 -43
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +5 -5
- package/dist/client.d.ts +5 -5
- package/dist/client.js +3 -1
- package/dist/drizzle/index.cjs +4139 -77
- package/dist/drizzle/index.cjs.map +1 -1
- package/dist/drizzle/index.d.cts +33 -33
- package/dist/drizzle/index.d.ts +33 -33
- package/dist/drizzle/index.js +39 -33
- package/dist/drizzle/index.js.map +1 -1
- package/dist/dynamodb/index.cjs +31 -14
- package/dist/dynamodb/index.cjs.map +1 -1
- package/dist/dynamodb/index.d.cts +12 -12
- package/dist/dynamodb/index.d.ts +12 -12
- package/dist/dynamodb/index.js +16 -12
- package/dist/dynamodb/index.js.map +1 -1
- package/dist/encryption/index.cjs +4611 -257
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +3 -3
- package/dist/encryption/index.d.ts +3 -3
- package/dist/encryption/index.js +35 -7
- package/dist/encryption/v3.cjs +7791 -0
- package/dist/encryption/v3.cjs.map +1 -0
- package/dist/encryption/v3.d.cts +100 -0
- package/dist/encryption/v3.d.ts +100 -0
- package/dist/encryption/v3.js +161 -0
- package/dist/encryption/v3.js.map +1 -0
- package/dist/eql/v3/index.cjs +612 -0
- package/dist/eql/v3/index.cjs.map +1 -0
- package/dist/eql/v3/index.d.cts +71 -0
- package/dist/eql/v3/index.d.ts +71 -0
- package/dist/eql/v3/index.js +89 -0
- package/dist/eql/v3/index.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/identity/index.cjs +55 -35
- package/dist/identity/index.cjs.map +1 -1
- package/dist/identity/index.d.cts +54 -30
- package/dist/identity/index.d.ts +54 -30
- package/dist/identity/index.js +9 -177
- package/dist/identity/index.js.map +1 -1
- package/dist/index.cjs +4625 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/schema/index.cjs +4108 -44
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +3 -1
- package/dist/supabase/index.cjs +4500 -75
- package/dist/supabase/index.cjs.map +1 -1
- package/dist/supabase/index.d.cts +113 -34
- package/dist/supabase/index.d.ts +113 -34
- package/dist/supabase/index.js +350 -32
- package/dist/supabase/index.js.map +1 -1
- package/dist/table-CIH7jZ2h.d.ts +603 -0
- package/dist/table-DihEAlxG.d.cts +603 -0
- package/dist/{types-public-Dv-r6Ik0.d.ts → types-public-CpS5KjwX.d.cts} +209 -36
- package/dist/{types-public-Dv-r6Ik0.d.cts → types-public-CpS5KjwX.d.ts} +209 -36
- package/dist/types-public.cjs.map +1 -1
- package/dist/types-public.d.cts +2 -2
- package/dist/types-public.d.ts +2 -2
- package/dist/types-public.js +2 -1
- package/dist/wasm-inline.d.ts +1269 -0
- package/dist/wasm-inline.js +4594 -0
- package/dist/wasm-inline.js.map +1 -0
- package/package.json +63 -13
- package/dist/chunk-4RNBI3UH.js.map +0 -1
- package/dist/chunk-6FD7OBGM.js.map +0 -1
- package/dist/chunk-CY5B4LIF.js.map +0 -1
- package/dist/chunk-JDZ2QUUF.js +0 -381
- package/dist/chunk-JDZ2QUUF.js.map +0 -1
- package/dist/chunk-MD6742R6.js.map +0 -1
package/dist/supabase/index.js
CHANGED
|
@@ -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-
|
|
7
|
+
} from "../chunk-BNO32ZMV.js";
|
|
5
8
|
import {
|
|
6
9
|
EncryptedColumn
|
|
7
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-MP3SSDNN.js";
|
|
11
|
+
import "../chunk-IADZCZEA.js";
|
|
8
12
|
import {
|
|
9
13
|
logger
|
|
10
|
-
} from "../chunk-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
738
|
+
q = q.eq(column, value);
|
|
675
739
|
break;
|
|
676
740
|
case "neq":
|
|
677
|
-
q = q.neq(
|
|
741
|
+
q = q.neq(column, value);
|
|
678
742
|
break;
|
|
679
743
|
case "gt":
|
|
680
|
-
q = q.gt(
|
|
744
|
+
q = q.gt(column, value);
|
|
681
745
|
break;
|
|
682
746
|
case "gte":
|
|
683
|
-
q = q.gte(
|
|
747
|
+
q = q.gte(column, value);
|
|
684
748
|
break;
|
|
685
749
|
case "lt":
|
|
686
|
-
q = q.lt(
|
|
750
|
+
q = q.lt(column, value);
|
|
687
751
|
break;
|
|
688
752
|
case "lte":
|
|
689
|
-
q = q.lte(
|
|
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 =
|
|
757
|
+
q = this.applyPatternFilter(q, column, f.op, value, wasEncrypted);
|
|
696
758
|
break;
|
|
697
759
|
case "is":
|
|
698
|
-
q = q.is(
|
|
760
|
+
q = q.is(column, value);
|
|
699
761
|
break;
|
|
700
762
|
case "in":
|
|
701
|
-
q = q.in(
|
|
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
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
795
|
+
encryptedIndexes.add(j);
|
|
729
796
|
}
|
|
730
797
|
}
|
|
731
|
-
if (
|
|
732
|
-
q = q.or(
|
|
733
|
-
|
|
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(
|
|
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:
|
|
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
|