@cipherstash/stack 0.19.0 → 1.0.0-rc.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/CHANGELOG.md +596 -0
- package/README.md +376 -276
- package/dist/adapter-kit.cjs +1002 -0
- package/dist/adapter-kit.cjs.map +1 -0
- package/dist/adapter-kit.d.cts +120 -0
- package/dist/adapter-kit.d.ts +120 -0
- package/dist/adapter-kit.js +122 -0
- package/dist/adapter-kit.js.map +1 -0
- package/dist/base-operation-AOAIvsSB.d.cts +32 -0
- package/dist/base-operation-FXEzUXIq.d.ts +32 -0
- package/dist/{chunk-4AVL4VZD.js → chunk-3B5ZX3IS.js} +3 -1
- package/dist/chunk-3B5ZX3IS.js.map +1 -0
- package/dist/{chunk-U66S7VIF.js → chunk-6SGN52W6.js} +166 -107
- package/dist/chunk-6SGN52W6.js.map +1 -0
- package/dist/chunk-7333ZC6L.js +48 -0
- package/dist/chunk-7333ZC6L.js.map +1 -0
- package/dist/{chunk-MP3SSDNN.js → chunk-CLM7E4I6.js} +15 -15
- package/dist/{chunk-MP3SSDNN.js.map → chunk-CLM7E4I6.js.map} +1 -1
- package/dist/{chunk-OFQ555AX.js → chunk-IDKP6ABU.js} +2 -2
- package/dist/{chunk-36AA7IBJ.js → chunk-L7ISHSG7.js} +53 -20
- package/dist/chunk-L7ISHSG7.js.map +1 -0
- package/dist/{chunk-LBMC4D6D.js → chunk-NVKK7UDN.js} +1 -1
- package/dist/chunk-NVKK7UDN.js.map +1 -0
- package/dist/chunk-X3JRXEIB.js +98 -0
- package/dist/chunk-X3JRXEIB.js.map +1 -0
- package/dist/client.cjs +29 -12
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +3 -2
- package/dist/client.d.ts +3 -2
- package/dist/client.js +2 -2
- package/dist/{table-CIH7jZ2h.d.ts → columns-0lbT9stl.d.ts} +157 -138
- package/dist/{table-DihEAlxG.d.cts → columns-Bxv7Oo9o.d.cts} +157 -138
- package/dist/dynamodb/index.d.cts +3 -2
- package/dist/dynamodb/index.d.ts +3 -2
- package/dist/encryption/index.cjs +54 -16
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +807 -6
- package/dist/encryption/index.d.ts +807 -6
- package/dist/encryption/index.js +6 -6
- package/dist/encryption/v3.cjs +1078 -973
- package/dist/encryption/v3.cjs.map +1 -1
- package/dist/encryption/v3.d.cts +15 -12
- package/dist/encryption/v3.d.ts +15 -12
- package/dist/encryption/v3.js +50 -22
- package/dist/encryption/v3.js.map +1 -1
- package/dist/eql/v3/index.cjs +130 -79
- package/dist/eql/v3/index.cjs.map +1 -1
- package/dist/eql/v3/index.d.cts +115 -13
- package/dist/eql/v3/index.d.ts +115 -13
- package/dist/eql/v3/index.js +16 -6
- package/dist/errors/index.cjs.map +1 -1
- package/dist/errors/index.d.cts +5 -5
- package/dist/errors/index.d.ts +5 -5
- package/dist/errors/index.js +1 -1
- package/dist/identity/index.cjs.map +1 -1
- package/dist/identity/index.js +2 -2
- package/dist/index-BquA71_Y.d.ts +24 -0
- package/dist/index-fhWTOV0K.d.cts +24 -0
- package/dist/index.cjs +79 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -17
- package/dist/index.d.ts +5 -17
- package/dist/index.js +6 -6
- package/dist/schema/index.cjs +29 -12
- 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 +2 -2
- package/dist/{types-public-CpS5KjwX.d.ts → types-public-QMjYNfQO.d.cts} +765 -726
- package/dist/{types-public-CpS5KjwX.d.cts → types-public-QMjYNfQO.d.ts} +765 -726
- package/dist/types-public.cjs.map +1 -1
- package/dist/types-public.d.cts +1 -1
- package/dist/types-public.d.ts +1 -1
- package/dist/types-public.js +1 -1
- package/dist/wasm-inline.d.ts +779 -405
- package/dist/wasm-inline.js +606 -299
- package/dist/wasm-inline.js.map +1 -1
- package/package.json +25 -53
- package/dist/chunk-36AA7IBJ.js.map +0 -1
- package/dist/chunk-4AVL4VZD.js.map +0 -1
- package/dist/chunk-IADZCZEA.js +0 -23
- package/dist/chunk-IADZCZEA.js.map +0 -1
- package/dist/chunk-IBSK6P33.js +0 -209
- package/dist/chunk-IBSK6P33.js.map +0 -1
- package/dist/chunk-LBMC4D6D.js.map +0 -1
- package/dist/chunk-U66S7VIF.js.map +0 -1
- package/dist/client-DSGHBN-g.d.cts +0 -834
- package/dist/client-DfCrlHXh.d.ts +0 -834
- package/dist/drizzle/index.cjs +0 -5617
- package/dist/drizzle/index.cjs.map +0 -1
- package/dist/drizzle/index.d.cts +0 -358
- package/dist/drizzle/index.d.ts +0 -358
- package/dist/drizzle/index.js +0 -1220
- package/dist/drizzle/index.js.map +0 -1
- package/dist/supabase/index.cjs +0 -5951
- package/dist/supabase/index.cjs.map +0 -1
- package/dist/supabase/index.d.cts +0 -223
- package/dist/supabase/index.d.ts +0 -223
- package/dist/supabase/index.js +0 -1217
- package/dist/supabase/index.js.map +0 -1
- /package/dist/{chunk-OFQ555AX.js.map → chunk-IDKP6ABU.js.map} +0 -0
package/dist/supabase/index.js
DELETED
|
@@ -1,1217 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
EncryptedV3Column
|
|
3
|
-
} from "../chunk-U66S7VIF.js";
|
|
4
|
-
import {
|
|
5
|
-
bulkModelsToEncryptedPgComposites,
|
|
6
|
-
modelToEncryptedPgComposites
|
|
7
|
-
} from "../chunk-BNO32ZMV.js";
|
|
8
|
-
import {
|
|
9
|
-
EncryptedColumn
|
|
10
|
-
} from "../chunk-MP3SSDNN.js";
|
|
11
|
-
import "../chunk-IADZCZEA.js";
|
|
12
|
-
import {
|
|
13
|
-
logger
|
|
14
|
-
} from "../chunk-HQANMV7R.js";
|
|
15
|
-
import "../chunk-PZ5AY32C.js";
|
|
16
|
-
|
|
17
|
-
// src/supabase/helpers.ts
|
|
18
|
-
function getEncryptedColumnNames(schema) {
|
|
19
|
-
const built = schema.build();
|
|
20
|
-
return Object.keys(built.columns);
|
|
21
|
-
}
|
|
22
|
-
function isEncryptedColumn(columnName, encryptedColumnNames) {
|
|
23
|
-
return encryptedColumnNames.includes(columnName);
|
|
24
|
-
}
|
|
25
|
-
function addJsonbCasts(columns, encryptedColumnNames) {
|
|
26
|
-
return columns.split(",").map((col) => {
|
|
27
|
-
const trimmed = col.trim();
|
|
28
|
-
if (!trimmed) return col;
|
|
29
|
-
if (trimmed.includes("::")) return col;
|
|
30
|
-
if (trimmed.includes("(") || trimmed.includes(".")) return col;
|
|
31
|
-
const parts = trimmed.split(/\s+/);
|
|
32
|
-
const colName = parts[0];
|
|
33
|
-
if (isEncryptedColumn(colName, encryptedColumnNames)) {
|
|
34
|
-
const leadingWhitespace = col.match(/^(\s*)/)?.[1] ?? "";
|
|
35
|
-
if (parts.length > 1) {
|
|
36
|
-
return `${leadingWhitespace}${colName}::jsonb ${parts.slice(1).join(" ")}`;
|
|
37
|
-
}
|
|
38
|
-
return `${leadingWhitespace}${colName}::jsonb`;
|
|
39
|
-
}
|
|
40
|
-
return col;
|
|
41
|
-
}).join(",");
|
|
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
|
-
}
|
|
77
|
-
function mapFilterOpToQueryType(op) {
|
|
78
|
-
switch (op) {
|
|
79
|
-
case "eq":
|
|
80
|
-
case "neq":
|
|
81
|
-
case "in":
|
|
82
|
-
case "is":
|
|
83
|
-
return "equality";
|
|
84
|
-
case "like":
|
|
85
|
-
case "ilike":
|
|
86
|
-
return "freeTextSearch";
|
|
87
|
-
case "gt":
|
|
88
|
-
case "gte":
|
|
89
|
-
case "lt":
|
|
90
|
-
case "lte":
|
|
91
|
-
return "orderAndRange";
|
|
92
|
-
default:
|
|
93
|
-
return "equality";
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function parseOrString(orString) {
|
|
97
|
-
const conditions = [];
|
|
98
|
-
const parts = splitOrString(orString);
|
|
99
|
-
for (const part of parts) {
|
|
100
|
-
const trimmed = part.trim();
|
|
101
|
-
if (!trimmed) continue;
|
|
102
|
-
const firstDot = trimmed.indexOf(".");
|
|
103
|
-
if (firstDot === -1) continue;
|
|
104
|
-
const column = trimmed.slice(0, firstDot);
|
|
105
|
-
const rest = trimmed.slice(firstDot + 1);
|
|
106
|
-
const secondDot = rest.indexOf(".");
|
|
107
|
-
if (secondDot === -1) continue;
|
|
108
|
-
const op = rest.slice(0, secondDot);
|
|
109
|
-
const value = rest.slice(secondDot + 1);
|
|
110
|
-
const parsedValue = parseOrValue(value);
|
|
111
|
-
conditions.push({ column, op, value: parsedValue });
|
|
112
|
-
}
|
|
113
|
-
return conditions;
|
|
114
|
-
}
|
|
115
|
-
function rebuildOrString(conditions) {
|
|
116
|
-
return conditions.map((c) => {
|
|
117
|
-
const value = formatOrValue(c.value);
|
|
118
|
-
return `${c.column}.${c.op}.${value}`;
|
|
119
|
-
}).join(",");
|
|
120
|
-
}
|
|
121
|
-
function splitOrString(input) {
|
|
122
|
-
const parts = [];
|
|
123
|
-
let current = "";
|
|
124
|
-
let depth = 0;
|
|
125
|
-
let inQuotes = false;
|
|
126
|
-
for (const char of input) {
|
|
127
|
-
if (char === '"' && depth === 0) {
|
|
128
|
-
inQuotes = !inQuotes;
|
|
129
|
-
current += char;
|
|
130
|
-
} else if (char === "(" && !inQuotes) {
|
|
131
|
-
depth++;
|
|
132
|
-
current += char;
|
|
133
|
-
} else if (char === ")" && !inQuotes) {
|
|
134
|
-
depth--;
|
|
135
|
-
current += char;
|
|
136
|
-
} else if (char === "," && depth === 0 && !inQuotes) {
|
|
137
|
-
parts.push(current);
|
|
138
|
-
current = "";
|
|
139
|
-
} else {
|
|
140
|
-
current += char;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (current) {
|
|
144
|
-
parts.push(current);
|
|
145
|
-
}
|
|
146
|
-
return parts;
|
|
147
|
-
}
|
|
148
|
-
function parseOrValue(value) {
|
|
149
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
150
|
-
return value.slice(1, -1);
|
|
151
|
-
}
|
|
152
|
-
if (value.startsWith("(") && value.endsWith(")")) {
|
|
153
|
-
return value.slice(1, -1).split(",").map((v) => v.trim());
|
|
154
|
-
}
|
|
155
|
-
if (value === "true") return true;
|
|
156
|
-
if (value === "false") return false;
|
|
157
|
-
if (value === "null") return null;
|
|
158
|
-
return value;
|
|
159
|
-
}
|
|
160
|
-
var POSTGREST_RESERVED = /[,().]/;
|
|
161
|
-
function formatOrValue(value) {
|
|
162
|
-
if (Array.isArray(value)) {
|
|
163
|
-
return `(${value.join(",")})`;
|
|
164
|
-
}
|
|
165
|
-
if (value === null) return "null";
|
|
166
|
-
if (value === true) return "true";
|
|
167
|
-
if (value === false) return "false";
|
|
168
|
-
const str = String(value);
|
|
169
|
-
if (POSTGREST_RESERVED.test(str)) {
|
|
170
|
-
return `"${str}"`;
|
|
171
|
-
}
|
|
172
|
-
return str;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// src/supabase/query-builder.ts
|
|
176
|
-
var EncryptedQueryBuilderImpl = class {
|
|
177
|
-
tableName;
|
|
178
|
-
schema;
|
|
179
|
-
encryptionClient;
|
|
180
|
-
supabaseClient;
|
|
181
|
-
encryptedColumnNames;
|
|
182
|
-
// Recorded operations
|
|
183
|
-
mutation = null;
|
|
184
|
-
selectColumns = null;
|
|
185
|
-
selectOptions = void 0;
|
|
186
|
-
filters = [];
|
|
187
|
-
orFilters = [];
|
|
188
|
-
matchFilters = [];
|
|
189
|
-
notFilters = [];
|
|
190
|
-
rawFilters = [];
|
|
191
|
-
transforms = [];
|
|
192
|
-
resultMode = "array";
|
|
193
|
-
shouldThrowOnError = false;
|
|
194
|
-
// Encryption-specific state
|
|
195
|
-
lockContext = null;
|
|
196
|
-
auditConfig = null;
|
|
197
|
-
constructor(tableName, schema, encryptionClient, supabaseClient) {
|
|
198
|
-
this.tableName = tableName;
|
|
199
|
-
this.schema = schema;
|
|
200
|
-
this.encryptionClient = encryptionClient;
|
|
201
|
-
this.supabaseClient = supabaseClient;
|
|
202
|
-
this.encryptedColumnNames = getEncryptedColumnNames(schema);
|
|
203
|
-
}
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// Mutation methods
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
select(columns, options) {
|
|
208
|
-
if (columns === "*") {
|
|
209
|
-
throw new Error(
|
|
210
|
-
"encryptedSupabase does not support select('*'). Please list columns explicitly so that encrypted columns can be cast with ::jsonb."
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
this.selectColumns = columns;
|
|
214
|
-
this.selectOptions = options;
|
|
215
|
-
return this;
|
|
216
|
-
}
|
|
217
|
-
insert(data, options) {
|
|
218
|
-
this.mutation = {
|
|
219
|
-
kind: "insert",
|
|
220
|
-
data,
|
|
221
|
-
options
|
|
222
|
-
};
|
|
223
|
-
return this;
|
|
224
|
-
}
|
|
225
|
-
update(data, options) {
|
|
226
|
-
this.mutation = {
|
|
227
|
-
kind: "update",
|
|
228
|
-
data,
|
|
229
|
-
options
|
|
230
|
-
};
|
|
231
|
-
return this;
|
|
232
|
-
}
|
|
233
|
-
upsert(data, options) {
|
|
234
|
-
this.mutation = {
|
|
235
|
-
kind: "upsert",
|
|
236
|
-
data,
|
|
237
|
-
options
|
|
238
|
-
};
|
|
239
|
-
return this;
|
|
240
|
-
}
|
|
241
|
-
delete(options) {
|
|
242
|
-
this.mutation = { kind: "delete", options };
|
|
243
|
-
return this;
|
|
244
|
-
}
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
// Filter methods
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
eq(column, value) {
|
|
249
|
-
this.filters.push({ op: "eq", column, value });
|
|
250
|
-
return this;
|
|
251
|
-
}
|
|
252
|
-
neq(column, value) {
|
|
253
|
-
this.filters.push({ op: "neq", column, value });
|
|
254
|
-
return this;
|
|
255
|
-
}
|
|
256
|
-
gt(column, value) {
|
|
257
|
-
this.filters.push({ op: "gt", column, value });
|
|
258
|
-
return this;
|
|
259
|
-
}
|
|
260
|
-
gte(column, value) {
|
|
261
|
-
this.filters.push({ op: "gte", column, value });
|
|
262
|
-
return this;
|
|
263
|
-
}
|
|
264
|
-
lt(column, value) {
|
|
265
|
-
this.filters.push({ op: "lt", column, value });
|
|
266
|
-
return this;
|
|
267
|
-
}
|
|
268
|
-
lte(column, value) {
|
|
269
|
-
this.filters.push({ op: "lte", column, value });
|
|
270
|
-
return this;
|
|
271
|
-
}
|
|
272
|
-
like(column, pattern) {
|
|
273
|
-
this.filters.push({ op: "like", column, value: pattern });
|
|
274
|
-
return this;
|
|
275
|
-
}
|
|
276
|
-
ilike(column, pattern) {
|
|
277
|
-
this.filters.push({ op: "ilike", column, value: pattern });
|
|
278
|
-
return this;
|
|
279
|
-
}
|
|
280
|
-
is(column, value) {
|
|
281
|
-
this.filters.push({ op: "is", column, value });
|
|
282
|
-
return this;
|
|
283
|
-
}
|
|
284
|
-
in(column, values) {
|
|
285
|
-
this.filters.push({ op: "in", column, value: values });
|
|
286
|
-
return this;
|
|
287
|
-
}
|
|
288
|
-
filter(column, operator, value) {
|
|
289
|
-
this.rawFilters.push({ column, operator, value });
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
not(column, operator, value) {
|
|
293
|
-
this.notFilters.push({ column, op: operator, value });
|
|
294
|
-
return this;
|
|
295
|
-
}
|
|
296
|
-
or(filtersOrConditions, options) {
|
|
297
|
-
if (typeof filtersOrConditions === "string") {
|
|
298
|
-
this.orFilters.push({
|
|
299
|
-
kind: "string",
|
|
300
|
-
value: filtersOrConditions,
|
|
301
|
-
referencedTable: options?.referencedTable ?? options?.foreignTable
|
|
302
|
-
});
|
|
303
|
-
} else {
|
|
304
|
-
this.orFilters.push({
|
|
305
|
-
kind: "structured",
|
|
306
|
-
conditions: filtersOrConditions
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
return this;
|
|
310
|
-
}
|
|
311
|
-
match(query) {
|
|
312
|
-
this.matchFilters.push({ query });
|
|
313
|
-
return this;
|
|
314
|
-
}
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
// Transform methods (passthrough)
|
|
317
|
-
// ---------------------------------------------------------------------------
|
|
318
|
-
order(column, options) {
|
|
319
|
-
this.transforms.push({ kind: "order", column, options });
|
|
320
|
-
return this;
|
|
321
|
-
}
|
|
322
|
-
limit(count, options) {
|
|
323
|
-
this.transforms.push({ kind: "limit", count, options });
|
|
324
|
-
return this;
|
|
325
|
-
}
|
|
326
|
-
range(from, to, options) {
|
|
327
|
-
this.transforms.push({ kind: "range", from, to, options });
|
|
328
|
-
return this;
|
|
329
|
-
}
|
|
330
|
-
single() {
|
|
331
|
-
this.resultMode = "single";
|
|
332
|
-
this.transforms.push({ kind: "single" });
|
|
333
|
-
return this;
|
|
334
|
-
}
|
|
335
|
-
maybeSingle() {
|
|
336
|
-
this.resultMode = "maybeSingle";
|
|
337
|
-
this.transforms.push({ kind: "maybeSingle" });
|
|
338
|
-
return this;
|
|
339
|
-
}
|
|
340
|
-
csv() {
|
|
341
|
-
this.transforms.push({ kind: "csv" });
|
|
342
|
-
return this;
|
|
343
|
-
}
|
|
344
|
-
abortSignal(signal) {
|
|
345
|
-
this.transforms.push({ kind: "abortSignal", signal });
|
|
346
|
-
return this;
|
|
347
|
-
}
|
|
348
|
-
throwOnError() {
|
|
349
|
-
this.shouldThrowOnError = true;
|
|
350
|
-
this.transforms.push({ kind: "throwOnError" });
|
|
351
|
-
return this;
|
|
352
|
-
}
|
|
353
|
-
returns() {
|
|
354
|
-
return this;
|
|
355
|
-
}
|
|
356
|
-
// ---------------------------------------------------------------------------
|
|
357
|
-
// Encryption-specific methods
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
withLockContext(lockContext) {
|
|
360
|
-
this.lockContext = lockContext;
|
|
361
|
-
return this;
|
|
362
|
-
}
|
|
363
|
-
audit(config) {
|
|
364
|
-
this.auditConfig = config;
|
|
365
|
-
return this;
|
|
366
|
-
}
|
|
367
|
-
// ---------------------------------------------------------------------------
|
|
368
|
-
// PromiseLike implementation (deferred execution)
|
|
369
|
-
// ---------------------------------------------------------------------------
|
|
370
|
-
then(onfulfilled, onrejected) {
|
|
371
|
-
return this.execute().then(onfulfilled, onrejected);
|
|
372
|
-
}
|
|
373
|
-
// ---------------------------------------------------------------------------
|
|
374
|
-
// Core execution
|
|
375
|
-
// ---------------------------------------------------------------------------
|
|
376
|
-
async execute() {
|
|
377
|
-
try {
|
|
378
|
-
logger.debug(`Supabase encrypted query on table "${this.tableName}".`);
|
|
379
|
-
const encryptedMutation = await this.encryptMutationData();
|
|
380
|
-
const selectString = this.buildSelectString();
|
|
381
|
-
const encryptedFilters = await this.encryptFilterValues();
|
|
382
|
-
const result = await this.buildAndExecuteQuery(
|
|
383
|
-
encryptedMutation,
|
|
384
|
-
selectString,
|
|
385
|
-
encryptedFilters
|
|
386
|
-
);
|
|
387
|
-
return await this.decryptResults(result);
|
|
388
|
-
} catch (err) {
|
|
389
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
390
|
-
logger.error(
|
|
391
|
-
`Supabase encrypted query failed on table "${this.tableName}": ${message}`
|
|
392
|
-
);
|
|
393
|
-
const error = {
|
|
394
|
-
message,
|
|
395
|
-
encryptionError: void 0
|
|
396
|
-
};
|
|
397
|
-
if (this.shouldThrowOnError) {
|
|
398
|
-
throw err;
|
|
399
|
-
}
|
|
400
|
-
return {
|
|
401
|
-
data: null,
|
|
402
|
-
error,
|
|
403
|
-
count: null,
|
|
404
|
-
status: 500,
|
|
405
|
-
statusText: "Encryption Error"
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
// ---------------------------------------------------------------------------
|
|
410
|
-
// Step 1: Encrypt mutation data
|
|
411
|
-
// ---------------------------------------------------------------------------
|
|
412
|
-
async encryptMutationData() {
|
|
413
|
-
if (!this.mutation) return null;
|
|
414
|
-
if (this.mutation.kind === "delete") return null;
|
|
415
|
-
const data = this.mutation.data;
|
|
416
|
-
if (Array.isArray(data)) {
|
|
417
|
-
const baseOp2 = this.encryptionClient.bulkEncryptModels(data, this.schema);
|
|
418
|
-
const op2 = this.lockContext ? baseOp2.withLockContext(this.lockContext) : baseOp2;
|
|
419
|
-
if (this.auditConfig) op2.audit(this.auditConfig);
|
|
420
|
-
const result2 = await op2;
|
|
421
|
-
if (result2.failure) {
|
|
422
|
-
logger.error(
|
|
423
|
-
`Supabase: failed to encrypt models for table "${this.tableName}"`
|
|
424
|
-
);
|
|
425
|
-
throw new EncryptionFailedError(
|
|
426
|
-
`Failed to encrypt models: ${result2.failure.message}`,
|
|
427
|
-
result2.failure
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
return this.transformEncryptedMutationModels(result2.data);
|
|
431
|
-
}
|
|
432
|
-
const baseOp = this.encryptionClient.encryptModel(data, this.schema);
|
|
433
|
-
const op = this.lockContext ? baseOp.withLockContext(this.lockContext) : baseOp;
|
|
434
|
-
if (this.auditConfig) op.audit(this.auditConfig);
|
|
435
|
-
const result = await op;
|
|
436
|
-
if (result.failure) {
|
|
437
|
-
logger.error(
|
|
438
|
-
`Supabase: failed to encrypt model for table "${this.tableName}"`
|
|
439
|
-
);
|
|
440
|
-
throw new EncryptionFailedError(
|
|
441
|
-
`Failed to encrypt model: ${result.failure.message}`,
|
|
442
|
-
result.failure
|
|
443
|
-
);
|
|
444
|
-
}
|
|
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);
|
|
459
|
-
}
|
|
460
|
-
// ---------------------------------------------------------------------------
|
|
461
|
-
// Step 2: Build select string with casts
|
|
462
|
-
// ---------------------------------------------------------------------------
|
|
463
|
-
buildSelectString() {
|
|
464
|
-
if (this.selectColumns === null) return null;
|
|
465
|
-
return addJsonbCasts(this.selectColumns, this.encryptedColumnNames);
|
|
466
|
-
}
|
|
467
|
-
// ---------------------------------------------------------------------------
|
|
468
|
-
// Step 3: Encrypt filter values
|
|
469
|
-
// ---------------------------------------------------------------------------
|
|
470
|
-
async encryptFilterValues() {
|
|
471
|
-
const terms = [];
|
|
472
|
-
const termMap = [];
|
|
473
|
-
const tableColumns = this.getColumnMap();
|
|
474
|
-
for (let i = 0; i < this.filters.length; i++) {
|
|
475
|
-
const f = this.filters[i];
|
|
476
|
-
if (!isEncryptedColumn(f.column, this.encryptedColumnNames)) continue;
|
|
477
|
-
const column = tableColumns[f.column];
|
|
478
|
-
if (!column) continue;
|
|
479
|
-
if (f.op === "in" && Array.isArray(f.value)) {
|
|
480
|
-
for (let j = 0; j < f.value.length; j++) {
|
|
481
|
-
terms.push({
|
|
482
|
-
value: f.value[j],
|
|
483
|
-
column,
|
|
484
|
-
table: this.schema,
|
|
485
|
-
queryType: mapFilterOpToQueryType(f.op),
|
|
486
|
-
returnType: "composite-literal"
|
|
487
|
-
});
|
|
488
|
-
termMap.push({ source: "filter", filterIndex: i, inIndex: j });
|
|
489
|
-
}
|
|
490
|
-
} else if (f.op === "is") {
|
|
491
|
-
} else {
|
|
492
|
-
terms.push({
|
|
493
|
-
value: f.value,
|
|
494
|
-
column,
|
|
495
|
-
table: this.schema,
|
|
496
|
-
queryType: mapFilterOpToQueryType(f.op),
|
|
497
|
-
returnType: "composite-literal"
|
|
498
|
-
});
|
|
499
|
-
termMap.push({ source: "filter", filterIndex: i });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
for (let i = 0; i < this.matchFilters.length; i++) {
|
|
503
|
-
const mf = this.matchFilters[i];
|
|
504
|
-
for (const [colName, value] of Object.entries(mf.query)) {
|
|
505
|
-
if (!isEncryptedColumn(colName, this.encryptedColumnNames)) continue;
|
|
506
|
-
const column = tableColumns[colName];
|
|
507
|
-
if (!column) continue;
|
|
508
|
-
terms.push({
|
|
509
|
-
value,
|
|
510
|
-
column,
|
|
511
|
-
table: this.schema,
|
|
512
|
-
queryType: "equality",
|
|
513
|
-
returnType: "composite-literal"
|
|
514
|
-
});
|
|
515
|
-
termMap.push({ source: "match", matchIndex: i, column: colName });
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
for (let i = 0; i < this.notFilters.length; i++) {
|
|
519
|
-
const nf = this.notFilters[i];
|
|
520
|
-
if (!isEncryptedColumn(nf.column, this.encryptedColumnNames)) continue;
|
|
521
|
-
const column = tableColumns[nf.column];
|
|
522
|
-
if (!column) continue;
|
|
523
|
-
terms.push({
|
|
524
|
-
value: nf.value,
|
|
525
|
-
column,
|
|
526
|
-
table: this.schema,
|
|
527
|
-
queryType: mapFilterOpToQueryType(nf.op),
|
|
528
|
-
returnType: "composite-literal"
|
|
529
|
-
});
|
|
530
|
-
termMap.push({ source: "not", notIndex: i });
|
|
531
|
-
}
|
|
532
|
-
for (let i = 0; i < this.orFilters.length; i++) {
|
|
533
|
-
const of_ = this.orFilters[i];
|
|
534
|
-
if (of_.kind === "string") {
|
|
535
|
-
const parsed = parseOrString(of_.value);
|
|
536
|
-
for (let j = 0; j < parsed.length; j++) {
|
|
537
|
-
const cond = parsed[j];
|
|
538
|
-
if (!isEncryptedColumn(cond.column, this.encryptedColumnNames))
|
|
539
|
-
continue;
|
|
540
|
-
const column = tableColumns[cond.column];
|
|
541
|
-
if (!column) continue;
|
|
542
|
-
terms.push({
|
|
543
|
-
value: cond.value,
|
|
544
|
-
column,
|
|
545
|
-
table: this.schema,
|
|
546
|
-
queryType: mapFilterOpToQueryType(cond.op),
|
|
547
|
-
returnType: "composite-literal"
|
|
548
|
-
});
|
|
549
|
-
termMap.push({ source: "or-string", orIndex: i, conditionIndex: j });
|
|
550
|
-
}
|
|
551
|
-
} else {
|
|
552
|
-
for (let j = 0; j < of_.conditions.length; j++) {
|
|
553
|
-
const cond = of_.conditions[j];
|
|
554
|
-
if (!isEncryptedColumn(cond.column, this.encryptedColumnNames))
|
|
555
|
-
continue;
|
|
556
|
-
const column = tableColumns[cond.column];
|
|
557
|
-
if (!column) continue;
|
|
558
|
-
terms.push({
|
|
559
|
-
value: cond.value,
|
|
560
|
-
column,
|
|
561
|
-
table: this.schema,
|
|
562
|
-
queryType: mapFilterOpToQueryType(cond.op),
|
|
563
|
-
returnType: "composite-literal"
|
|
564
|
-
});
|
|
565
|
-
termMap.push({
|
|
566
|
-
source: "or-structured",
|
|
567
|
-
orIndex: i,
|
|
568
|
-
conditionIndex: j
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
for (let i = 0; i < this.rawFilters.length; i++) {
|
|
574
|
-
const rf = this.rawFilters[i];
|
|
575
|
-
if (!isEncryptedColumn(rf.column, this.encryptedColumnNames)) continue;
|
|
576
|
-
const column = tableColumns[rf.column];
|
|
577
|
-
if (!column) continue;
|
|
578
|
-
terms.push({
|
|
579
|
-
value: rf.value,
|
|
580
|
-
column,
|
|
581
|
-
table: this.schema,
|
|
582
|
-
queryType: "equality",
|
|
583
|
-
returnType: "composite-literal"
|
|
584
|
-
});
|
|
585
|
-
termMap.push({ source: "raw", rawIndex: i });
|
|
586
|
-
}
|
|
587
|
-
if (terms.length === 0) {
|
|
588
|
-
return { encryptedValues: [], termMap: [] };
|
|
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) {
|
|
601
|
-
const baseOp = this.encryptionClient.encryptQuery(terms);
|
|
602
|
-
const op = this.lockContext ? baseOp.withLockContext(this.lockContext) : baseOp;
|
|
603
|
-
if (this.auditConfig) op.audit(this.auditConfig);
|
|
604
|
-
const result = await op;
|
|
605
|
-
if (result.failure) {
|
|
606
|
-
logger.error(
|
|
607
|
-
`Supabase: failed to encrypt query terms for table "${this.tableName}"`
|
|
608
|
-
);
|
|
609
|
-
throw new EncryptionFailedError(
|
|
610
|
-
`Failed to encrypt query terms: ${result.failure.message}`,
|
|
611
|
-
result.failure
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
return result.data;
|
|
615
|
-
}
|
|
616
|
-
// ---------------------------------------------------------------------------
|
|
617
|
-
// Step 4: Build and execute real Supabase query
|
|
618
|
-
// ---------------------------------------------------------------------------
|
|
619
|
-
async buildAndExecuteQuery(encryptedMutation, selectString, encryptedFilters) {
|
|
620
|
-
let query = this.supabaseClient.from(this.tableName);
|
|
621
|
-
if (this.mutation) {
|
|
622
|
-
switch (this.mutation.kind) {
|
|
623
|
-
case "insert":
|
|
624
|
-
query = query.insert(encryptedMutation, this.mutation.options);
|
|
625
|
-
break;
|
|
626
|
-
case "update":
|
|
627
|
-
query = query.update(encryptedMutation, this.mutation.options);
|
|
628
|
-
break;
|
|
629
|
-
case "upsert":
|
|
630
|
-
query = query.upsert(encryptedMutation, this.mutation.options);
|
|
631
|
-
break;
|
|
632
|
-
case "delete":
|
|
633
|
-
query = query.delete(this.mutation.options);
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
if (selectString !== null) {
|
|
638
|
-
query = query.select(selectString, this.selectOptions);
|
|
639
|
-
} else if (!this.mutation) {
|
|
640
|
-
query = query.select("*", this.selectOptions);
|
|
641
|
-
}
|
|
642
|
-
query = this.applyFilters(query, encryptedFilters);
|
|
643
|
-
for (const t of this.transforms) {
|
|
644
|
-
switch (t.kind) {
|
|
645
|
-
case "order":
|
|
646
|
-
query = query.order(t.column, t.options);
|
|
647
|
-
break;
|
|
648
|
-
case "limit":
|
|
649
|
-
query = query.limit(t.count, t.options);
|
|
650
|
-
break;
|
|
651
|
-
case "range":
|
|
652
|
-
query = query.range(t.from, t.to, t.options);
|
|
653
|
-
break;
|
|
654
|
-
case "single":
|
|
655
|
-
query = query.single();
|
|
656
|
-
break;
|
|
657
|
-
case "maybeSingle":
|
|
658
|
-
query = query.maybeSingle();
|
|
659
|
-
break;
|
|
660
|
-
case "csv":
|
|
661
|
-
query = query.csv();
|
|
662
|
-
break;
|
|
663
|
-
case "abortSignal":
|
|
664
|
-
query = query.abortSignal(t.signal);
|
|
665
|
-
break;
|
|
666
|
-
case "throwOnError":
|
|
667
|
-
query = query.throwOnError();
|
|
668
|
-
break;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
const result = await query;
|
|
672
|
-
return result;
|
|
673
|
-
}
|
|
674
|
-
// ---------------------------------------------------------------------------
|
|
675
|
-
// Apply filters with encrypted values substituted
|
|
676
|
-
// ---------------------------------------------------------------------------
|
|
677
|
-
applyFilters(query, encryptedFilters) {
|
|
678
|
-
let q = query;
|
|
679
|
-
const filterValueMap = /* @__PURE__ */ new Map();
|
|
680
|
-
const filterInMap = /* @__PURE__ */ new Map();
|
|
681
|
-
const matchValueMap = /* @__PURE__ */ new Map();
|
|
682
|
-
const notValueMap = /* @__PURE__ */ new Map();
|
|
683
|
-
const rawValueMap = /* @__PURE__ */ new Map();
|
|
684
|
-
const orStringConditionMap = /* @__PURE__ */ new Map();
|
|
685
|
-
const orStructuredConditionMap = /* @__PURE__ */ new Map();
|
|
686
|
-
for (let i = 0; i < encryptedFilters.termMap.length; i++) {
|
|
687
|
-
const mapping = encryptedFilters.termMap[i];
|
|
688
|
-
const encValue = encryptedFilters.encryptedValues[i];
|
|
689
|
-
switch (mapping.source) {
|
|
690
|
-
case "filter":
|
|
691
|
-
if (mapping.inIndex !== void 0) {
|
|
692
|
-
filterInMap.set(
|
|
693
|
-
`${mapping.filterIndex}:${mapping.inIndex}`,
|
|
694
|
-
encValue
|
|
695
|
-
);
|
|
696
|
-
} else {
|
|
697
|
-
filterValueMap.set(mapping.filterIndex, encValue);
|
|
698
|
-
}
|
|
699
|
-
break;
|
|
700
|
-
case "match":
|
|
701
|
-
matchValueMap.set(`${mapping.matchIndex}:${mapping.column}`, encValue);
|
|
702
|
-
break;
|
|
703
|
-
case "not":
|
|
704
|
-
notValueMap.set(mapping.notIndex, encValue);
|
|
705
|
-
break;
|
|
706
|
-
case "raw":
|
|
707
|
-
rawValueMap.set(mapping.rawIndex, encValue);
|
|
708
|
-
break;
|
|
709
|
-
case "or-string":
|
|
710
|
-
orStringConditionMap.set(
|
|
711
|
-
`${mapping.orIndex}:${mapping.conditionIndex}`,
|
|
712
|
-
encValue
|
|
713
|
-
);
|
|
714
|
-
break;
|
|
715
|
-
case "or-structured":
|
|
716
|
-
orStructuredConditionMap.set(
|
|
717
|
-
`${mapping.orIndex}:${mapping.conditionIndex}`,
|
|
718
|
-
encValue
|
|
719
|
-
);
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
for (let i = 0; i < this.filters.length; i++) {
|
|
724
|
-
const f = this.filters[i];
|
|
725
|
-
let value = f.value;
|
|
726
|
-
if (filterValueMap.has(i)) {
|
|
727
|
-
value = filterValueMap.get(i);
|
|
728
|
-
} else if (f.op === "in" && Array.isArray(f.value)) {
|
|
729
|
-
value = f.value.map((v, j) => {
|
|
730
|
-
const key = `${i}:${j}`;
|
|
731
|
-
return filterInMap.has(key) ? filterInMap.get(key) : v;
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
const column = this.filterColumnName(f.column);
|
|
735
|
-
const wasEncrypted = filterValueMap.has(i);
|
|
736
|
-
switch (f.op) {
|
|
737
|
-
case "eq":
|
|
738
|
-
q = q.eq(column, value);
|
|
739
|
-
break;
|
|
740
|
-
case "neq":
|
|
741
|
-
q = q.neq(column, value);
|
|
742
|
-
break;
|
|
743
|
-
case "gt":
|
|
744
|
-
q = q.gt(column, value);
|
|
745
|
-
break;
|
|
746
|
-
case "gte":
|
|
747
|
-
q = q.gte(column, value);
|
|
748
|
-
break;
|
|
749
|
-
case "lt":
|
|
750
|
-
q = q.lt(column, value);
|
|
751
|
-
break;
|
|
752
|
-
case "lte":
|
|
753
|
-
q = q.lte(column, value);
|
|
754
|
-
break;
|
|
755
|
-
case "like":
|
|
756
|
-
case "ilike":
|
|
757
|
-
q = this.applyPatternFilter(q, column, f.op, value, wasEncrypted);
|
|
758
|
-
break;
|
|
759
|
-
case "is":
|
|
760
|
-
q = q.is(column, value);
|
|
761
|
-
break;
|
|
762
|
-
case "in":
|
|
763
|
-
q = q.in(column, value);
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
for (let i = 0; i < this.matchFilters.length; i++) {
|
|
768
|
-
const mf = this.matchFilters[i];
|
|
769
|
-
const resolvedQuery = {};
|
|
770
|
-
for (const [colName, originalValue] of Object.entries(mf.query)) {
|
|
771
|
-
const key = `${i}:${colName}`;
|
|
772
|
-
resolvedQuery[this.filterColumnName(colName)] = matchValueMap.has(key) ? matchValueMap.get(key) : originalValue;
|
|
773
|
-
}
|
|
774
|
-
q = q.match(resolvedQuery);
|
|
775
|
-
}
|
|
776
|
-
for (let i = 0; i < this.notFilters.length; i++) {
|
|
777
|
-
const nf = this.notFilters[i];
|
|
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
|
-
);
|
|
785
|
-
}
|
|
786
|
-
for (let i = 0; i < this.orFilters.length; i++) {
|
|
787
|
-
const of_ = this.orFilters[i];
|
|
788
|
-
if (of_.kind === "string") {
|
|
789
|
-
const parsed = parseOrString(of_.value);
|
|
790
|
-
const encryptedIndexes = /* @__PURE__ */ new Set();
|
|
791
|
-
for (let j = 0; j < parsed.length; j++) {
|
|
792
|
-
const key = `${i}:${j}`;
|
|
793
|
-
if (orStringConditionMap.has(key)) {
|
|
794
|
-
parsed[j] = { ...parsed[j], value: orStringConditionMap.get(key) };
|
|
795
|
-
encryptedIndexes.add(j);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (encryptedIndexes.size > 0) {
|
|
799
|
-
q = q.or(
|
|
800
|
-
rebuildOrString(
|
|
801
|
-
this.transformOrConditions(parsed, encryptedIndexes)
|
|
802
|
-
),
|
|
803
|
-
{
|
|
804
|
-
referencedTable: of_.referencedTable
|
|
805
|
-
}
|
|
806
|
-
);
|
|
807
|
-
} else {
|
|
808
|
-
q = q.or(of_.value, { referencedTable: of_.referencedTable });
|
|
809
|
-
}
|
|
810
|
-
} else {
|
|
811
|
-
const encryptedIndexes = /* @__PURE__ */ new Set();
|
|
812
|
-
const conditions = of_.conditions.map((cond, j) => {
|
|
813
|
-
const key = `${i}:${j}`;
|
|
814
|
-
if (orStructuredConditionMap.has(key)) {
|
|
815
|
-
encryptedIndexes.add(j);
|
|
816
|
-
return { ...cond, value: orStructuredConditionMap.get(key) };
|
|
817
|
-
}
|
|
818
|
-
return cond;
|
|
819
|
-
});
|
|
820
|
-
q = q.or(
|
|
821
|
-
rebuildOrString(
|
|
822
|
-
this.transformOrConditions(conditions, encryptedIndexes)
|
|
823
|
-
)
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
for (let i = 0; i < this.rawFilters.length; i++) {
|
|
828
|
-
const rf = this.rawFilters[i];
|
|
829
|
-
const value = rawValueMap.has(i) ? rawValueMap.get(i) : rf.value;
|
|
830
|
-
q = q.filter(this.filterColumnName(rf.column), rf.operator, value);
|
|
831
|
-
}
|
|
832
|
-
return q;
|
|
833
|
-
}
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
879
|
-
// Step 5: Decrypt results
|
|
880
|
-
// ---------------------------------------------------------------------------
|
|
881
|
-
async decryptResults(result) {
|
|
882
|
-
if (result.error) {
|
|
883
|
-
return {
|
|
884
|
-
data: null,
|
|
885
|
-
error: {
|
|
886
|
-
message: result.error.message,
|
|
887
|
-
details: result.error.details,
|
|
888
|
-
hint: result.error.hint,
|
|
889
|
-
code: result.error.code
|
|
890
|
-
},
|
|
891
|
-
count: result.count ?? null,
|
|
892
|
-
status: result.status,
|
|
893
|
-
statusText: result.statusText
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
if (result.data === null || result.data === void 0) {
|
|
897
|
-
return {
|
|
898
|
-
data: null,
|
|
899
|
-
error: null,
|
|
900
|
-
count: result.count ?? null,
|
|
901
|
-
status: result.status,
|
|
902
|
-
statusText: result.statusText
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
const hasSelect = this.selectColumns !== null;
|
|
906
|
-
const hasMutationWithReturning = this.mutation !== null && hasSelect;
|
|
907
|
-
if (!hasSelect && !hasMutationWithReturning) {
|
|
908
|
-
return {
|
|
909
|
-
data: result.data,
|
|
910
|
-
error: null,
|
|
911
|
-
count: result.count ?? null,
|
|
912
|
-
status: result.status,
|
|
913
|
-
statusText: result.statusText
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
if (this.resultMode === "single" || this.resultMode === "maybeSingle") {
|
|
917
|
-
if (result.data === null) {
|
|
918
|
-
return {
|
|
919
|
-
data: null,
|
|
920
|
-
error: null,
|
|
921
|
-
count: result.count ?? null,
|
|
922
|
-
status: result.status,
|
|
923
|
-
statusText: result.statusText
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
const baseDecryptOp = this.encryptionClient.decryptModel(
|
|
927
|
-
result.data
|
|
928
|
-
);
|
|
929
|
-
const decryptOp = this.lockContext ? baseDecryptOp.withLockContext(this.lockContext) : baseDecryptOp;
|
|
930
|
-
if (this.auditConfig) decryptOp.audit(this.auditConfig);
|
|
931
|
-
const decrypted2 = await decryptOp;
|
|
932
|
-
if (decrypted2.failure) {
|
|
933
|
-
logger.error(
|
|
934
|
-
`Supabase: failed to decrypt model for table "${this.tableName}"`
|
|
935
|
-
);
|
|
936
|
-
throw new EncryptionFailedError(
|
|
937
|
-
`Failed to decrypt model: ${decrypted2.failure.message}`,
|
|
938
|
-
decrypted2.failure
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
return {
|
|
942
|
-
data: this.postprocessDecryptedRow(
|
|
943
|
-
decrypted2.data
|
|
944
|
-
),
|
|
945
|
-
error: null,
|
|
946
|
-
count: result.count ?? null,
|
|
947
|
-
status: result.status,
|
|
948
|
-
statusText: result.statusText
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
const dataArray = result.data;
|
|
952
|
-
if (dataArray.length === 0) {
|
|
953
|
-
return {
|
|
954
|
-
data: [],
|
|
955
|
-
error: null,
|
|
956
|
-
count: result.count ?? null,
|
|
957
|
-
status: result.status,
|
|
958
|
-
statusText: result.statusText
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
const baseBulkDecryptOp = this.encryptionClient.bulkDecryptModels(dataArray);
|
|
962
|
-
const bulkDecryptOp = this.lockContext ? baseBulkDecryptOp.withLockContext(this.lockContext) : baseBulkDecryptOp;
|
|
963
|
-
if (this.auditConfig) bulkDecryptOp.audit(this.auditConfig);
|
|
964
|
-
const decrypted = await bulkDecryptOp;
|
|
965
|
-
if (decrypted.failure) {
|
|
966
|
-
logger.error(
|
|
967
|
-
`Supabase: failed to decrypt models for table "${this.tableName}"`
|
|
968
|
-
);
|
|
969
|
-
throw new EncryptionFailedError(
|
|
970
|
-
`Failed to decrypt models: ${decrypted.failure.message}`,
|
|
971
|
-
decrypted.failure
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
return {
|
|
975
|
-
data: decrypted.data.map(
|
|
976
|
-
(row) => this.postprocessDecryptedRow(row)
|
|
977
|
-
),
|
|
978
|
-
error: null,
|
|
979
|
-
count: result.count ?? null,
|
|
980
|
-
status: result.status,
|
|
981
|
-
statusText: result.statusText
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
// ---------------------------------------------------------------------------
|
|
985
|
-
// Helpers
|
|
986
|
-
// ---------------------------------------------------------------------------
|
|
987
|
-
getColumnMap() {
|
|
988
|
-
const map = {};
|
|
989
|
-
const schema = this.schema;
|
|
990
|
-
for (const colName of this.encryptedColumnNames) {
|
|
991
|
-
const col = schema[colName];
|
|
992
|
-
if (col instanceof EncryptedColumn) {
|
|
993
|
-
map[colName] = col;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return map;
|
|
997
|
-
}
|
|
998
|
-
};
|
|
999
|
-
var EncryptionFailedError = class extends Error {
|
|
1000
|
-
encryptionError;
|
|
1001
|
-
constructor(message, encryptionError) {
|
|
1002
|
-
super(message);
|
|
1003
|
-
this.name = "EncryptionFailedError";
|
|
1004
|
-
this.encryptionError = encryptionError;
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
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
|
-
|
|
1186
|
-
// src/supabase/index.ts
|
|
1187
|
-
function encryptedSupabase(config) {
|
|
1188
|
-
const { encryptionClient, supabaseClient } = config;
|
|
1189
|
-
return {
|
|
1190
|
-
from(tableName, schema) {
|
|
1191
|
-
return new EncryptedQueryBuilderImpl(
|
|
1192
|
-
tableName,
|
|
1193
|
-
schema,
|
|
1194
|
-
encryptionClient,
|
|
1195
|
-
supabaseClient
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
};
|
|
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
|
-
}
|
|
1213
|
-
export {
|
|
1214
|
-
encryptedSupabase,
|
|
1215
|
-
encryptedSupabaseV3
|
|
1216
|
-
};
|
|
1217
|
-
//# sourceMappingURL=index.js.map
|