@gaganref/confect-test 10.3.1-fork.92 → 10.3.1-fork.93
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/dist/TestConfect.d.ts +26 -0
- package/dist/TestConfect.d.ts.map +1 -1
- package/dist/TestConfect.js +9 -1
- package/dist/TestConfect.js.map +1 -1
- package/dist/internal/ConvexTest.d.ts +26 -1
- package/dist/internal/ConvexTest.d.ts.map +1 -1
- package/dist/internal/ConvexTest.js +55 -27
- package/dist/internal/ConvexTest.js.map +1 -1
- package/dist/internal/Execution.d.ts +138 -1
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +595 -182
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/KernelRefusal.d.ts +134 -0
- package/dist/internal/KernelRefusal.d.ts.map +1 -1
- package/dist/internal/KernelRefusal.js +134 -1
- package/dist/internal/KernelRefusal.js.map +1 -1
- package/dist/internal/Redaction.d.ts +142 -0
- package/dist/internal/Redaction.d.ts.map +1 -0
- package/dist/internal/Redaction.js +448 -0
- package/dist/internal/Redaction.js.map +1 -0
- package/dist/internal/Snapshot.d.ts +298 -0
- package/dist/internal/Snapshot.d.ts.map +1 -0
- package/dist/internal/Snapshot.js +532 -0
- package/dist/internal/Snapshot.js.map +1 -0
- package/dist/internal/Subscriptions.d.ts +13 -0
- package/dist/internal/Subscriptions.d.ts.map +1 -1
- package/dist/internal/Subscriptions.js +27 -3
- package/dist/internal/Subscriptions.js.map +1 -1
- package/package.json +3 -3
- package/src/TestConfect.ts +63 -4
- package/src/internal/ConvexTest.ts +135 -38
- package/src/internal/Execution.ts +886 -287
- package/src/internal/KernelRefusal.ts +160 -0
- package/src/internal/Redaction.ts +730 -0
- package/src/internal/Snapshot.ts +945 -0
- package/src/internal/Subscriptions.ts +34 -4
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { SnapshotSecretMissing } from "./KernelRefusal.js";
|
|
2
|
+
import { convexToJson, jsonToConvex } from "convex/values";
|
|
3
|
+
import * as Result from "effect/Result";
|
|
4
|
+
import * as DatabaseSchema from "@confect/server/DatabaseSchema";
|
|
5
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
6
|
+
//#region src/internal/Redaction.ts
|
|
7
|
+
/**
|
|
8
|
+
* What a marker writes in place of a secret in text and in values: a failure
|
|
9
|
+
* report, a diff, a read record.
|
|
10
|
+
*/
|
|
11
|
+
const marker = "[redacted]";
|
|
12
|
+
/**
|
|
13
|
+
* The key of a snapshot's marker object, `{ "$redacted": <ref> }`. Convex's
|
|
14
|
+
* encoded JSON allows no object key that starts with `$`, so no stored value
|
|
15
|
+
* reads as a marker.
|
|
16
|
+
*/
|
|
17
|
+
const markerKey = "$redacted";
|
|
18
|
+
/**
|
|
19
|
+
* Free text has no schema, so a failure report's message and stack are searched
|
|
20
|
+
* for every kept secret string. A string shorter than this is not searched for,
|
|
21
|
+
* or text that merely shares a few letters would be marked. Values are marked
|
|
22
|
+
* by their schema at any length.
|
|
23
|
+
*/
|
|
24
|
+
const searchableLength = 4;
|
|
25
|
+
const isSecretNode = (ast) => ast._tag === "Declaration" && (SchemaAST.resolve(ast)?.["representation"])?.id === "effect/schema/Redacted";
|
|
26
|
+
/**
|
|
27
|
+
* Whether a secret node is reachable from `ast`. A suspension is followed once
|
|
28
|
+
* per ancestry, so a recursive schema ends, and a suspension reused at another
|
|
29
|
+
* path is followed again there.
|
|
30
|
+
*/
|
|
31
|
+
const reachesSecret = (ast, ancestry) => {
|
|
32
|
+
if (isSecretNode(ast)) return true;
|
|
33
|
+
switch (ast._tag) {
|
|
34
|
+
case "Objects": return ast.propertySignatures.some((p) => reachesSecret(p.type, ancestry)) || ast.indexSignatures.some((i) => reachesSecret(i.type, ancestry));
|
|
35
|
+
case "Arrays": return [...ast.elements, ...ast.rest].some((e) => reachesSecret(e, ancestry));
|
|
36
|
+
case "Union": return ast.types.some((t) => reachesSecret(t, ancestry));
|
|
37
|
+
case "Declaration": return ast.typeParameters.some((t) => reachesSecret(t, ancestry));
|
|
38
|
+
case "Suspend": {
|
|
39
|
+
if (ancestry.has(ast)) return false;
|
|
40
|
+
ancestry.add(ast);
|
|
41
|
+
const found = reachesSecret(ast.thunk(), ancestry);
|
|
42
|
+
ancestry.delete(ast);
|
|
43
|
+
return found;
|
|
44
|
+
}
|
|
45
|
+
default: return false;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const reaches = /* @__PURE__ */ new WeakMap();
|
|
49
|
+
/**
|
|
50
|
+
* Whether `shape` holds a secret anywhere beneath it.
|
|
51
|
+
*/
|
|
52
|
+
const hasSecret = (shape) => {
|
|
53
|
+
let found = reaches.get(shape);
|
|
54
|
+
if (found === void 0) {
|
|
55
|
+
found = reachesSecret(shape, /* @__PURE__ */ new Set());
|
|
56
|
+
reaches.set(shape, found);
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
};
|
|
60
|
+
const isObject = (json) => typeof json === "object" && json !== null && !Array.isArray(json);
|
|
61
|
+
/**
|
|
62
|
+
* A Convex encoded leaf, such as `{"$integer": …}` or `{"$bytes": …}`, or a
|
|
63
|
+
* marker: one key that starts with `$`.
|
|
64
|
+
*/
|
|
65
|
+
const isWrapped = (json) => {
|
|
66
|
+
if (!isObject(json)) return false;
|
|
67
|
+
const keys = Object.keys(json);
|
|
68
|
+
return keys.length === 1 && keys[0].startsWith("$");
|
|
69
|
+
};
|
|
70
|
+
const isMarker = (json) => isObject(json) && Object.keys(json).length === 1 && "$redacted" in json;
|
|
71
|
+
const isRecord = (json) => isObject(json) && !isWrapped(json);
|
|
72
|
+
/**
|
|
73
|
+
* Whether `json` can be a value of `ast`, for choosing a union's members. Nodes
|
|
74
|
+
* the walk does not look into fit anything.
|
|
75
|
+
*/
|
|
76
|
+
const fits = (ast, json, ancestry = /* @__PURE__ */ new Set()) => {
|
|
77
|
+
switch (ast._tag) {
|
|
78
|
+
case "Objects": return isRecord(json);
|
|
79
|
+
case "Arrays": return Array.isArray(json);
|
|
80
|
+
case "String":
|
|
81
|
+
case "TemplateLiteral": return typeof json === "string";
|
|
82
|
+
case "Number": return typeof json === "number" || isObject(json) && "$float" in json;
|
|
83
|
+
case "BigInt": return isObject(json) && "$integer" in json;
|
|
84
|
+
case "Boolean": return typeof json === "boolean";
|
|
85
|
+
case "Null": return json === null;
|
|
86
|
+
case "Literal": return typeof ast.literal === "bigint" ? isObject(json) && "$integer" in json : json === ast.literal;
|
|
87
|
+
case "Union": return ast.types.some((t) => fits(t, json, ancestry));
|
|
88
|
+
case "Suspend": {
|
|
89
|
+
if (ancestry.has(ast)) return true;
|
|
90
|
+
ancestry.add(ast);
|
|
91
|
+
const fit = fits(ast.thunk(), json, ancestry);
|
|
92
|
+
ancestry.delete(ast);
|
|
93
|
+
return fit;
|
|
94
|
+
}
|
|
95
|
+
case "Declaration": return !isSecretNode(ast) || ast.typeParameters.some((t) => fits(t, json, ancestry));
|
|
96
|
+
default: return true;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* `json` with `onSecret` applied at every position where `shape` puts a secret.
|
|
101
|
+
* A value whose structure the shape cannot follow, beneath a node that holds a
|
|
102
|
+
* secret, is treated as secret whole: the walk fails closed. A marker is never
|
|
103
|
+
* walked again, so marking is idempotent and overlapping members of a union
|
|
104
|
+
* mark a shared field once.
|
|
105
|
+
*/
|
|
106
|
+
const walk = (shape, json, onSecret, unconsumed = /* @__PURE__ */ new Set()) => {
|
|
107
|
+
if (isMarker(json)) return json;
|
|
108
|
+
if (isSecretNode(shape)) return onSecret(json);
|
|
109
|
+
if (!hasSecret(shape)) return json;
|
|
110
|
+
switch (shape._tag) {
|
|
111
|
+
case "Objects": {
|
|
112
|
+
if (!isRecord(json)) return onSecret(json);
|
|
113
|
+
const named = new Map(shape.propertySignatures.map((p) => [String(p.name), p.type]));
|
|
114
|
+
return Object.fromEntries(Object.entries(json).map(([key, item]) => {
|
|
115
|
+
const type = named.get(key);
|
|
116
|
+
return [key, type !== void 0 ? walk(type, item, onSecret) : shape.indexSignatures.reduce((value, index) => walk(index.type, value, onSecret), item)];
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
case "Arrays": {
|
|
120
|
+
if (!Array.isArray(json)) return onSecret(json);
|
|
121
|
+
const [rest, ...post] = shape.rest;
|
|
122
|
+
return json.map((item, i) => {
|
|
123
|
+
if (i < shape.elements.length) return walk(shape.elements[i], item, onSecret);
|
|
124
|
+
const fromEnd = json.length - i;
|
|
125
|
+
if (fromEnd <= post.length) return walk(post[post.length - fromEnd], item, onSecret);
|
|
126
|
+
return rest === void 0 ? item : walk(rest, item, onSecret);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
case "Union": {
|
|
130
|
+
const members = shape.types.filter((t) => fits(t, json));
|
|
131
|
+
if (members.length === 0) return onSecret(json);
|
|
132
|
+
return members.reduce((value, member) => walk(member, value, onSecret, unconsumed), json);
|
|
133
|
+
}
|
|
134
|
+
case "Suspend": {
|
|
135
|
+
if (unconsumed.has(shape)) return json;
|
|
136
|
+
unconsumed.add(shape);
|
|
137
|
+
const walked = walk(shape.thunk(), json, onSecret, unconsumed);
|
|
138
|
+
unconsumed.delete(shape);
|
|
139
|
+
return walked;
|
|
140
|
+
}
|
|
141
|
+
case "Declaration": return shape.typeParameters.reduce((value, parameter) => walk(parameter, value, onSecret, unconsumed), json);
|
|
142
|
+
default: return onSecret(json);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const replaceAt = (json, path, replace) => {
|
|
146
|
+
if (path.length === 0) return replace(json);
|
|
147
|
+
const [head, ...rest] = path;
|
|
148
|
+
if (Array.isArray(json)) return json.map((item, i) => `${i}` === head ? replaceAt(item, rest, replace) : item);
|
|
149
|
+
if (isRecord(json)) return Object.fromEntries(Object.entries(json).map(([key, item]) => [key, key === head ? replaceAt(item, rest, replace) : item]));
|
|
150
|
+
return json;
|
|
151
|
+
};
|
|
152
|
+
const walkPlaced = (json, placements, onSecret) => placements.reduce((value, { at, shape }) => replaceAt(value, at, (inner) => walk(shape, inner, onSecret)), json);
|
|
153
|
+
/**
|
|
154
|
+
* Whether the field at `fieldPath` of a document of `shape` holds a secret, or
|
|
155
|
+
* sits under one.
|
|
156
|
+
*/
|
|
157
|
+
const sensitiveAt = (shape, fieldPath) => {
|
|
158
|
+
const at = (ast, path, ancestry) => {
|
|
159
|
+
if (isSecretNode(ast)) return true;
|
|
160
|
+
if (path.length === 0) return hasSecret(ast);
|
|
161
|
+
const [head, ...rest] = path;
|
|
162
|
+
switch (ast._tag) {
|
|
163
|
+
case "Objects": {
|
|
164
|
+
const property = ast.propertySignatures.find((p) => String(p.name) === head);
|
|
165
|
+
return property !== void 0 ? at(property.type, rest, /* @__PURE__ */ new Set()) : ast.indexSignatures.some((i) => at(i.type, rest, /* @__PURE__ */ new Set()));
|
|
166
|
+
}
|
|
167
|
+
case "Union": return ast.types.some((t) => at(t, path, ancestry));
|
|
168
|
+
case "Declaration": return ast.typeParameters.some((t) => at(t, path, ancestry));
|
|
169
|
+
case "Suspend": {
|
|
170
|
+
if (ancestry.has(ast)) return false;
|
|
171
|
+
ancestry.add(ast);
|
|
172
|
+
const found = at(ast.thunk(), path, ancestry);
|
|
173
|
+
ancestry.delete(ast);
|
|
174
|
+
return found;
|
|
175
|
+
}
|
|
176
|
+
default: return hasSecret(ast);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
return at(shape, fieldPath, /* @__PURE__ */ new Set());
|
|
180
|
+
};
|
|
181
|
+
const memo = (compute) => {
|
|
182
|
+
const cache = /* @__PURE__ */ new Map();
|
|
183
|
+
return (key) => {
|
|
184
|
+
if (!cache.has(key)) cache.set(key, compute(key));
|
|
185
|
+
return cache.get(key);
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* A policy from `declared`'s schemas. Each schema is built the first time its
|
|
190
|
+
* shape is asked for.
|
|
191
|
+
*/
|
|
192
|
+
const policy = (declared) => {
|
|
193
|
+
const split = (key) => key.split("\0");
|
|
194
|
+
const table = memo((key) => {
|
|
195
|
+
const [componentPath, name] = split(key);
|
|
196
|
+
return declared.tables?.[componentPath]?.[name]?.().ast;
|
|
197
|
+
});
|
|
198
|
+
const fn = (part) => memo((key) => {
|
|
199
|
+
const [componentPath, udfPath] = split(key);
|
|
200
|
+
return declared.functions?.[componentPath]?.[udfPath]?.[part]?.().ast;
|
|
201
|
+
});
|
|
202
|
+
const args = fn("args");
|
|
203
|
+
const returns = fn("returns");
|
|
204
|
+
return {
|
|
205
|
+
table: (componentPath, name) => table(`${componentPath}\u0000${name}`),
|
|
206
|
+
args: ({ componentPath, udfPath }) => args(`${componentPath}\u0000${udfPath}`),
|
|
207
|
+
returns: ({ componentPath, udfPath }) => returns(`${componentPath}\u0000${udfPath}`)
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
const none = policy({});
|
|
211
|
+
/**
|
|
212
|
+
* Two declarations as one: `second`'s entries win where both name a table or
|
|
213
|
+
* function.
|
|
214
|
+
*/
|
|
215
|
+
const merge = (first, second) => {
|
|
216
|
+
const each = (a, b) => Object.fromEntries([.../* @__PURE__ */ new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].map((componentPath) => [componentPath, {
|
|
217
|
+
...a?.[componentPath],
|
|
218
|
+
...b?.[componentPath]
|
|
219
|
+
}]));
|
|
220
|
+
return {
|
|
221
|
+
tables: each(first.tables, second.tables),
|
|
222
|
+
functions: each(first.functions, second.functions)
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* A Confect database schema's tables, as a policy's `tables` entry for one
|
|
227
|
+
* component. No table's schema is built until the kernel asks for its shape.
|
|
228
|
+
*/
|
|
229
|
+
const tablesOf = (databaseSchema) => Object.fromEntries(Object.values(DatabaseSchema.tables(databaseSchema)).map((table) => [table.tableName, () => table.Fields]));
|
|
230
|
+
/**
|
|
231
|
+
* The text a secret leaf can show in free text: a string itself, an int64 as
|
|
232
|
+
* its decimal digits.
|
|
233
|
+
*/
|
|
234
|
+
const leafTexts = (json) => {
|
|
235
|
+
if (typeof json === "string") return [json];
|
|
236
|
+
if (Array.isArray(json)) return json.flatMap(leafTexts);
|
|
237
|
+
if (isObject(json)) {
|
|
238
|
+
if ("$integer" in json) try {
|
|
239
|
+
const integer = jsonToConvex(json);
|
|
240
|
+
return typeof integer === "bigint" ? [integer.toString()] : [];
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
if (isWrapped(json)) return [];
|
|
245
|
+
return Object.values(json).flatMap(leafTexts);
|
|
246
|
+
}
|
|
247
|
+
return [];
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* The raw secrets the kernel has seen under a secret node, in the order it
|
|
251
|
+
* first saw each, with every string a compound secret holds beneath it. This
|
|
252
|
+
* store is replay state: it lives in memory beside the world and is never
|
|
253
|
+
* written to an artifact. A snapshot's markers name entries here by position.
|
|
254
|
+
*/
|
|
255
|
+
var Secrets = class Secrets {
|
|
256
|
+
values = [];
|
|
257
|
+
refs = /* @__PURE__ */ new Map();
|
|
258
|
+
texts = /* @__PURE__ */ new Set();
|
|
259
|
+
/**
|
|
260
|
+
* Keep `value` and answer its position. A value kept before answers its first
|
|
261
|
+
* position.
|
|
262
|
+
*/
|
|
263
|
+
keep(value) {
|
|
264
|
+
const key = JSON.stringify(value);
|
|
265
|
+
const known = this.refs.get(key);
|
|
266
|
+
if (known !== void 0) return known;
|
|
267
|
+
this.values.push(value);
|
|
268
|
+
this.refs.set(key, this.values.length - 1);
|
|
269
|
+
for (const text of leafTexts(value)) this.texts.add(text);
|
|
270
|
+
return this.values.length - 1;
|
|
271
|
+
}
|
|
272
|
+
raw(ref) {
|
|
273
|
+
return this.values[ref];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Every kept secret's text long enough to search free text for, longest
|
|
277
|
+
* first, so a secret inside another is never marked half.
|
|
278
|
+
*/
|
|
279
|
+
searchable() {
|
|
280
|
+
return [...this.texts].filter((text) => text.length >= searchableLength).sort((a, b) => b.length - a.length);
|
|
281
|
+
}
|
|
282
|
+
copy() {
|
|
283
|
+
const copy = new Secrets();
|
|
284
|
+
for (const value of this.values) copy.keep(value);
|
|
285
|
+
return copy;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Keep every secret of `value` that `placements` put there, before it is
|
|
290
|
+
* validated, so a validation error can already mark it.
|
|
291
|
+
*/
|
|
292
|
+
const keep = (value, placements, secrets) => {
|
|
293
|
+
if (placements.length === 0) return;
|
|
294
|
+
let json;
|
|
295
|
+
try {
|
|
296
|
+
json = convexToJson(value);
|
|
297
|
+
} catch {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
walkPlaced(json, placements, (secret) => {
|
|
301
|
+
secrets.keep(secret);
|
|
302
|
+
return secret;
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
const containsSecret = (text, searchable) => searchable.some((secret) => text.includes(secret));
|
|
306
|
+
/**
|
|
307
|
+
* The backstop beneath every schema walk: each string, outside a marker, that
|
|
308
|
+
* holds a kept secret, replaced by `replace`.
|
|
309
|
+
*/
|
|
310
|
+
const backstop = (json, searchable, replace) => {
|
|
311
|
+
if (searchable.length === 0) return json;
|
|
312
|
+
const go = (value) => {
|
|
313
|
+
if (typeof value === "string") return containsSecret(value, searchable) ? replace(value) : value;
|
|
314
|
+
if (Array.isArray(value)) return value.map(go);
|
|
315
|
+
if (isObject(value)) {
|
|
316
|
+
if (isWrapped(value)) return value;
|
|
317
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, systemFields.has(key) ? item : go(item)]));
|
|
318
|
+
}
|
|
319
|
+
return value;
|
|
320
|
+
};
|
|
321
|
+
return go(json);
|
|
322
|
+
};
|
|
323
|
+
const systemFields = /* @__PURE__ */ new Set(["_id", "_creationTime"]);
|
|
324
|
+
/**
|
|
325
|
+
* `json`, a document bound for a snapshot, with every secret its placements put
|
|
326
|
+
* there, and every string holding a kept secret, replaced by a marker that
|
|
327
|
+
* names the raw value's position in `secrets`. The walk never enters a marker,
|
|
328
|
+
* so marking a marked value changes nothing.
|
|
329
|
+
*/
|
|
330
|
+
const mark = (json, placements, secrets) => {
|
|
331
|
+
const toMarker = (value) => ({ [markerKey]: secrets.keep(value) });
|
|
332
|
+
return backstop(walkPlaced(json, placements, toMarker), secrets.searchable(), toMarker);
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* `value` as an artifact other than a snapshot writes it: every secret its
|
|
336
|
+
* placements put there, and every string holding a kept secret, replaced by the
|
|
337
|
+
* marker text.
|
|
338
|
+
*/
|
|
339
|
+
const markValue = (value, placements, secrets) => {
|
|
340
|
+
let json;
|
|
341
|
+
try {
|
|
342
|
+
json = convexToJson(value);
|
|
343
|
+
} catch {
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
const marked = backstop(walkPlaced(json, placements, () => marker), secrets.searchable(), () => marker);
|
|
347
|
+
return marked === json ? value : jsonToConvex(marked);
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* `json` with each marker replaced by the raw value it names in `secrets`, or
|
|
351
|
+
* `SnapshotSecretMissing` for the first marker the store cannot answer.
|
|
352
|
+
*/
|
|
353
|
+
const unmark = (json, secrets) => {
|
|
354
|
+
let missing = void 0;
|
|
355
|
+
const go = (value) => {
|
|
356
|
+
if (Array.isArray(value)) return value.map(go);
|
|
357
|
+
if (!isObject(value)) return value;
|
|
358
|
+
if (isMarker(value)) {
|
|
359
|
+
const ref = value[markerKey];
|
|
360
|
+
const raw = typeof ref === "number" ? secrets.raw(ref) : void 0;
|
|
361
|
+
if (raw === void 0) missing ??= ref;
|
|
362
|
+
return raw ?? null;
|
|
363
|
+
}
|
|
364
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, go(item)]));
|
|
365
|
+
};
|
|
366
|
+
const unmarked = go(json);
|
|
367
|
+
return missing === void 0 ? Result.succeed(unmarked) : Result.fail(new SnapshotSecretMissing({
|
|
368
|
+
ref: JSON.stringify(missing),
|
|
369
|
+
message: `The snapshot marks a secret at ${JSON.stringify(missing)}, which the secrets passed to restore do not hold. Restore with the secrets of the backend that exported it.`
|
|
370
|
+
}));
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* `text` with every kept secret, raw or as it appears inside a JSON string,
|
|
374
|
+
* replaced by the marker.
|
|
375
|
+
*/
|
|
376
|
+
const scrubText = (text, secrets) => secrets.searchable().reduce((scrubbed, secret) => {
|
|
377
|
+
const escaped = JSON.stringify(secret).slice(1, -1);
|
|
378
|
+
return scrubbed.split(secret).join(marker).split(escaped).join(marker);
|
|
379
|
+
}, text);
|
|
380
|
+
const isPlainObject = (value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
|
|
381
|
+
/**
|
|
382
|
+
* A failure report for `thrown`, whatever was thrown: every kept secret in its
|
|
383
|
+
* text, its data and its causes marked. When nothing needs marking it is
|
|
384
|
+
* `thrown` itself. Otherwise it is a copy with the same prototype and fields,
|
|
385
|
+
* so its class and tag still read the same. `thrown` is never changed, so a
|
|
386
|
+
* subscription that keeps the raw error compares it as it was.
|
|
387
|
+
*/
|
|
388
|
+
const failure = (thrown, secrets) => {
|
|
389
|
+
if (secrets.searchable().length === 0) return thrown;
|
|
390
|
+
const copyable = (value) => Array.isArray(value) || value instanceof Error || isPlainObject(value);
|
|
391
|
+
const field = (value, key) => {
|
|
392
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
393
|
+
return {
|
|
394
|
+
raw: "value" in descriptor ? descriptor.value : Reflect.get(value, key),
|
|
395
|
+
enumerable: descriptor.enumerable ?? false
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
const visited = /* @__PURE__ */ new Set();
|
|
399
|
+
const holdsSecret = (value) => {
|
|
400
|
+
if (typeof value === "string") return scrubText(value, secrets) !== value;
|
|
401
|
+
if (typeof value !== "object" || value === null) return false;
|
|
402
|
+
if (visited.has(value) || !copyable(value)) return false;
|
|
403
|
+
visited.add(value);
|
|
404
|
+
return Reflect.ownKeys(value).some((key) => holdsSecret(field(value, key).raw));
|
|
405
|
+
};
|
|
406
|
+
if (!holdsSecret(thrown)) return thrown;
|
|
407
|
+
const copies = /* @__PURE__ */ new Map();
|
|
408
|
+
const copy = (value) => {
|
|
409
|
+
if (typeof value === "string") return scrubText(value, secrets);
|
|
410
|
+
if (typeof value !== "object" || value === null) return value;
|
|
411
|
+
if (copies.has(value)) return copies.get(value);
|
|
412
|
+
if (!copyable(value)) return value;
|
|
413
|
+
if (Array.isArray(value)) {
|
|
414
|
+
const items = [];
|
|
415
|
+
copies.set(value, items);
|
|
416
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
417
|
+
if (key === "length") continue;
|
|
418
|
+
const { raw, enumerable } = field(value, key);
|
|
419
|
+
if (typeof key === "string" && /^(0|[1-9][0-9]*)$/.test(key)) items[Number(key)] = copy(raw);
|
|
420
|
+
else Object.defineProperty(items, key, {
|
|
421
|
+
value: copy(raw),
|
|
422
|
+
writable: true,
|
|
423
|
+
enumerable,
|
|
424
|
+
configurable: true
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
items.length = value.length;
|
|
428
|
+
return items;
|
|
429
|
+
}
|
|
430
|
+
const target = Object.create(Object.getPrototypeOf(value));
|
|
431
|
+
copies.set(value, target);
|
|
432
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
433
|
+
const { raw, enumerable } = field(value, key);
|
|
434
|
+
Object.defineProperty(target, key, {
|
|
435
|
+
value: copy(raw),
|
|
436
|
+
writable: true,
|
|
437
|
+
enumerable,
|
|
438
|
+
configurable: true
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return target;
|
|
442
|
+
};
|
|
443
|
+
return copy(thrown);
|
|
444
|
+
};
|
|
445
|
+
//#endregion
|
|
446
|
+
export { Secrets, failure, hasSecret, keep, mark, markValue, marker, markerKey, merge, none, policy, scrubText, sensitiveAt, tablesOf, unmark };
|
|
447
|
+
|
|
448
|
+
//# sourceMappingURL=Redaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Redaction.js","names":["KernelRefusal.SnapshotSecretMissing"],"sources":["../../src/internal/Redaction.ts"],"sourcesContent":["import * as DatabaseSchema from \"@confect/server/DatabaseSchema\";\nimport {\n convexToJson,\n jsonToConvex,\n type JSONValue,\n type Value,\n} from \"convex/values\";\nimport * as Result from \"effect/Result\";\nimport type * as Schema from \"effect/Schema\";\nimport * as SchemaAST from \"effect/SchemaAST\";\nimport * as KernelRefusal from \"./KernelRefusal.js\";\nimport type { FunctionPath } from \"./KernelValues.js\";\n\n/**\n * What a marker writes in place of a secret in text and in values: a failure\n * report, a diff, a read record.\n */\nexport const marker = \"[redacted]\";\n\n/**\n * The key of a snapshot's marker object, `{ \"$redacted\": <ref> }`. Convex's\n * encoded JSON allows no object key that starts with `$`, so no stored value\n * reads as a marker.\n */\nexport const markerKey = \"$redacted\";\n\n/**\n * Free text has no schema, so a failure report's message and stack are searched\n * for every kept secret string. A string shorter than this is not searched for,\n * or text that merely shares a few letters would be marked. Values are marked\n * by their schema at any length.\n */\nconst searchableLength = 4;\n\n/**\n * Where a value may hold secrets: the type side of a schema's AST. A walk\n * follows it through the encoded value, so the value bounds the walk.\n */\nexport type Shape = SchemaAST.AST;\n\n/**\n * A shape placed at a path inside a value: a table's schema at a document's\n * root, a job's target arguments at its row's `args.0`.\n */\nexport interface Placement {\n readonly at: ReadonlyArray<string>;\n readonly shape: Shape;\n}\n\nconst isSecretNode = (ast: SchemaAST.AST): boolean =>\n ast._tag === \"Declaration\" &&\n (SchemaAST.resolve(ast)?.[\"representation\"] as { id?: unknown } | undefined)\n ?.id === \"effect/schema/Redacted\";\n\n/**\n * Whether a secret node is reachable from `ast`. A suspension is followed once\n * per ancestry, so a recursive schema ends, and a suspension reused at another\n * path is followed again there.\n */\nconst reachesSecret = (\n ast: SchemaAST.AST,\n ancestry: Set<SchemaAST.AST>,\n): boolean => {\n if (isSecretNode(ast)) return true;\n switch (ast._tag) {\n case \"Objects\":\n return (\n ast.propertySignatures.some((p) => reachesSecret(p.type, ancestry)) ||\n ast.indexSignatures.some((i) => reachesSecret(i.type, ancestry))\n );\n case \"Arrays\":\n return [...ast.elements, ...ast.rest].some((e) =>\n reachesSecret(e, ancestry),\n );\n case \"Union\":\n return ast.types.some((t) => reachesSecret(t, ancestry));\n case \"Declaration\":\n return ast.typeParameters.some((t) => reachesSecret(t, ancestry));\n case \"Suspend\": {\n if (ancestry.has(ast)) return false;\n ancestry.add(ast);\n const found = reachesSecret(ast.thunk(), ancestry);\n ancestry.delete(ast);\n return found;\n }\n default:\n return false;\n }\n};\n\nconst reaches = new WeakMap<SchemaAST.AST, boolean>();\n\n/**\n * Whether `shape` holds a secret anywhere beneath it.\n */\nexport const hasSecret = (shape: Shape): boolean => {\n let found = reaches.get(shape);\n if (found === undefined) {\n found = reachesSecret(shape, new Set());\n reaches.set(shape, found);\n }\n return found;\n};\n\nconst isObject = (json: JSONValue): json is { [key: string]: JSONValue } =>\n typeof json === \"object\" && json !== null && !Array.isArray(json);\n\n/**\n * A Convex encoded leaf, such as `{\"$integer\": …}` or `{\"$bytes\": …}`, or a\n * marker: one key that starts with `$`.\n */\nconst isWrapped = (json: JSONValue): boolean => {\n if (!isObject(json)) return false;\n const keys = Object.keys(json);\n return keys.length === 1 && keys[0]!.startsWith(\"$\");\n};\n\nconst isMarker = (json: JSONValue): boolean =>\n isObject(json) && Object.keys(json).length === 1 && markerKey in json;\n\nconst isRecord = (json: JSONValue): json is { [key: string]: JSONValue } =>\n isObject(json) && !isWrapped(json);\n\n/**\n * Whether `json` can be a value of `ast`, for choosing a union's members. Nodes\n * the walk does not look into fit anything.\n */\nconst fits = (\n ast: SchemaAST.AST,\n json: JSONValue,\n ancestry: Set<SchemaAST.AST> = new Set(),\n): boolean => {\n switch (ast._tag) {\n case \"Objects\":\n return isRecord(json);\n case \"Arrays\":\n return Array.isArray(json);\n case \"String\":\n case \"TemplateLiteral\":\n return typeof json === \"string\";\n case \"Number\":\n return typeof json === \"number\" || (isObject(json) && \"$float\" in json);\n case \"BigInt\":\n return isObject(json) && \"$integer\" in json;\n case \"Boolean\":\n return typeof json === \"boolean\";\n case \"Null\":\n return json === null;\n case \"Literal\":\n return typeof ast.literal === \"bigint\"\n ? isObject(json) && \"$integer\" in json\n : json === ast.literal;\n case \"Union\":\n return ast.types.some((t) => fits(t, json, ancestry));\n case \"Suspend\": {\n if (ancestry.has(ast)) return true;\n ancestry.add(ast);\n const fit = fits(ast.thunk(), json, ancestry);\n ancestry.delete(ast);\n return fit;\n }\n case \"Declaration\":\n // A secret is stored as what its inner type encodes to, so a union\n // member that is a secret fits only those values.\n return (\n !isSecretNode(ast) ||\n ast.typeParameters.some((t) => fits(t, json, ancestry))\n );\n default:\n return true;\n }\n};\n\n/**\n * `json` with `onSecret` applied at every position where `shape` puts a secret.\n * A value whose structure the shape cannot follow, beneath a node that holds a\n * secret, is treated as secret whole: the walk fails closed. A marker is never\n * walked again, so marking is idempotent and overlapping members of a union\n * mark a shared field once.\n */\nconst walk = (\n shape: SchemaAST.AST,\n json: JSONValue,\n onSecret: (value: JSONValue) => JSONValue,\n unconsumed: Set<SchemaAST.AST> = new Set(),\n): JSONValue => {\n if (isMarker(json)) return json;\n if (isSecretNode(shape)) return onSecret(json);\n if (!hasSecret(shape)) return json;\n switch (shape._tag) {\n case \"Objects\": {\n if (!isRecord(json)) return onSecret(json);\n const named = new Map(\n shape.propertySignatures.map((p) => [String(p.name), p.type]),\n );\n return Object.fromEntries(\n Object.entries(json).map(([key, item]) => {\n const type = named.get(key);\n return [\n key,\n type !== undefined\n ? walk(type, item, onSecret)\n : shape.indexSignatures.reduce(\n (value, index) => walk(index.type, value, onSecret),\n item,\n ),\n ];\n }),\n );\n }\n case \"Arrays\": {\n if (!Array.isArray(json)) return onSecret(json);\n const [rest, ...post] = shape.rest;\n return json.map((item, i) => {\n if (i < shape.elements.length)\n return walk(shape.elements[i]!, item, onSecret);\n const fromEnd = json.length - i;\n if (fromEnd <= post.length)\n return walk(post[post.length - fromEnd]!, item, onSecret);\n return rest === undefined ? item : walk(rest, item, onSecret);\n });\n }\n case \"Union\": {\n const members = shape.types.filter((t) => fits(t, json));\n if (members.length === 0) return onSecret(json);\n return members.reduce(\n (value, member) => walk(member, value, onSecret, unconsumed),\n json,\n );\n }\n case \"Suspend\": {\n if (unconsumed.has(shape)) return json;\n unconsumed.add(shape);\n const walked = walk(shape.thunk(), json, onSecret, unconsumed);\n unconsumed.delete(shape);\n return walked;\n }\n case \"Declaration\":\n return shape.typeParameters.reduce(\n (value, parameter) => walk(parameter, value, onSecret, unconsumed),\n json,\n );\n default:\n return onSecret(json);\n }\n};\n\nconst replaceAt = (\n json: JSONValue,\n path: ReadonlyArray<string>,\n replace: (value: JSONValue) => JSONValue,\n): JSONValue => {\n if (path.length === 0) return replace(json);\n const [head, ...rest] = path as [string, ...Array<string>];\n if (Array.isArray(json)) {\n return json.map((item, i) =>\n `${i}` === head ? replaceAt(item, rest, replace) : item,\n );\n }\n if (isRecord(json)) {\n return Object.fromEntries(\n Object.entries(json).map(([key, item]) => [\n key,\n key === head ? replaceAt(item, rest, replace) : item,\n ]),\n );\n }\n return json;\n};\n\nconst walkPlaced = (\n json: JSONValue,\n placements: ReadonlyArray<Placement>,\n onSecret: (value: JSONValue) => JSONValue,\n): JSONValue =>\n placements.reduce(\n (value, { at, shape }) =>\n replaceAt(value, at, (inner) => walk(shape, inner, onSecret)),\n json,\n );\n\n/**\n * Whether the field at `fieldPath` of a document of `shape` holds a secret, or\n * sits under one.\n */\nexport const sensitiveAt = (\n shape: Shape,\n fieldPath: ReadonlyArray<string>,\n): boolean => {\n const at = (\n ast: SchemaAST.AST,\n path: ReadonlyArray<string>,\n ancestry: Set<SchemaAST.AST>,\n ): boolean => {\n if (isSecretNode(ast)) return true;\n if (path.length === 0) return hasSecret(ast);\n const [head, ...rest] = path as [string, ...Array<string>];\n switch (ast._tag) {\n case \"Objects\": {\n const property = ast.propertySignatures.find(\n (p) => String(p.name) === head,\n );\n // A segment is consumed here, so a suspension met again further down\n // the path is a new descent, not a cycle.\n return property !== undefined\n ? at(property.type, rest, new Set())\n : ast.indexSignatures.some((i) => at(i.type, rest, new Set()));\n }\n case \"Union\":\n return ast.types.some((t) => at(t, path, ancestry));\n case \"Declaration\":\n return ast.typeParameters.some((t) => at(t, path, ancestry));\n case \"Suspend\": {\n if (ancestry.has(ast)) return false;\n ancestry.add(ast);\n const found = at(ast.thunk(), path, ancestry);\n ancestry.delete(ast);\n return found;\n }\n default:\n return hasSecret(ast);\n }\n };\n return at(shape, fieldPath, new Set());\n};\n\n/**\n * Which values the kernel treats as secret: the shapes of a table's documents,\n * of a function's arguments and of its return value.\n */\nexport interface Policy {\n readonly table: (componentPath: string, table: string) => Shape | undefined;\n readonly args: (functionPath: FunctionPath) => Shape | undefined;\n readonly returns: (functionPath: FunctionPath) => Shape | undefined;\n}\n\n/**\n * A schema the policy builds the first time it needs it, as Confect's specs and\n * tables keep theirs.\n */\nexport type Lazy = () => Schema.Top;\n\n/**\n * The schemas a policy reads its secret nodes from, by component path, then\n * table name or `module:export`. Row 8's codegen fills this from the Confect\n * specs. A test that declares it by hand writes the same record.\n */\nexport interface Declared {\n readonly tables?: Readonly<Record<string, Readonly<Record<string, Lazy>>>>;\n readonly functions?: Readonly<\n Record<\n string,\n Readonly<\n Record<string, { readonly args?: Lazy; readonly returns?: Lazy }>\n >\n >\n >;\n}\n\nconst memo = <A>(compute: (key: string) => A) => {\n const cache = new Map<string, A>();\n return (key: string): A => {\n if (!cache.has(key)) cache.set(key, compute(key));\n return cache.get(key)!;\n };\n};\n\n/**\n * A policy from `declared`'s schemas. Each schema is built the first time its\n * shape is asked for.\n */\nexport const policy = (declared: Declared): Policy => {\n const split = (key: string) => key.split(\"\\u0000\") as [string, string];\n const table = memo((key) => {\n const [componentPath, name] = split(key);\n return declared.tables?.[componentPath]?.[name]?.().ast;\n });\n const fn = (part: \"args\" | \"returns\") =>\n memo((key) => {\n const [componentPath, udfPath] = split(key);\n return declared.functions?.[componentPath]?.[udfPath]?.[part]?.().ast;\n });\n const args = fn(\"args\");\n const returns = fn(\"returns\");\n return {\n table: (componentPath, name) => table(`${componentPath}\\u0000${name}`),\n args: ({ componentPath, udfPath }) =>\n args(`${componentPath}\\u0000${udfPath}`),\n returns: ({ componentPath, udfPath }) =>\n returns(`${componentPath}\\u0000${udfPath}`),\n };\n};\n\nexport const none: Policy = policy({});\n\n/**\n * Two declarations as one: `second`'s entries win where both name a table or\n * function.\n */\nexport const merge = (first: Declared, second: Declared): Declared => {\n const each = <A>(\n a: Readonly<Record<string, Readonly<Record<string, A>>>> | undefined,\n b: Readonly<Record<string, Readonly<Record<string, A>>>> | undefined,\n ) =>\n Object.fromEntries(\n [...new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].map(\n (componentPath) => [\n componentPath,\n { ...a?.[componentPath], ...b?.[componentPath] },\n ],\n ),\n );\n return {\n tables: each(first.tables, second.tables),\n functions: each(first.functions, second.functions),\n };\n};\n\n/**\n * A Confect database schema's tables, as a policy's `tables` entry for one\n * component. No table's schema is built until the kernel asks for its shape.\n */\nexport const tablesOf = (\n databaseSchema: DatabaseSchema.AnyWithProps,\n): Readonly<Record<string, Lazy>> =>\n Object.fromEntries(\n Object.values(DatabaseSchema.tables(databaseSchema)).map(\n (table: { readonly tableName: string; readonly Fields: Schema.Top }) => [\n table.tableName,\n () => table.Fields,\n ],\n ),\n );\n\n/**\n * The text a secret leaf can show in free text: a string itself, an int64 as\n * its decimal digits.\n */\nconst leafTexts = (json: JSONValue): Array<string> => {\n if (typeof json === \"string\") return [json];\n if (Array.isArray(json)) return json.flatMap(leafTexts);\n if (isObject(json)) {\n if (\"$integer\" in json) {\n try {\n const integer = jsonToConvex(json);\n return typeof integer === \"bigint\" ? [integer.toString()] : [];\n } catch {\n return [];\n }\n }\n if (isWrapped(json)) return [];\n return Object.values(json).flatMap(leafTexts);\n }\n return [];\n};\n\n/**\n * The raw secrets the kernel has seen under a secret node, in the order it\n * first saw each, with every string a compound secret holds beneath it. This\n * store is replay state: it lives in memory beside the world and is never\n * written to an artifact. A snapshot's markers name entries here by position.\n */\nexport class Secrets {\n private readonly values: Array<JSONValue> = [];\n private readonly refs = new Map<string, number>();\n private readonly texts = new Set<string>();\n\n /**\n * Keep `value` and answer its position. A value kept before answers its first\n * position.\n */\n keep(value: JSONValue): number {\n const key = JSON.stringify(value);\n const known = this.refs.get(key);\n if (known !== undefined) return known;\n this.values.push(value);\n this.refs.set(key, this.values.length - 1);\n for (const text of leafTexts(value)) this.texts.add(text);\n return this.values.length - 1;\n }\n\n raw(ref: number): JSONValue | undefined {\n return this.values[ref];\n }\n\n /**\n * Every kept secret's text long enough to search free text for, longest\n * first, so a secret inside another is never marked half.\n */\n searchable(): ReadonlyArray<string> {\n return [...this.texts]\n .filter((text) => text.length >= searchableLength)\n .sort((a, b) => b.length - a.length);\n }\n\n copy(): Secrets {\n const copy = new Secrets();\n for (const value of this.values) copy.keep(value);\n return copy;\n }\n}\n\n/**\n * Keep every secret of `value` that `placements` put there, before it is\n * validated, so a validation error can already mark it.\n */\nexport const keep = (\n value: unknown,\n placements: ReadonlyArray<Placement>,\n secrets: Secrets,\n): void => {\n if (placements.length === 0) return;\n let json: JSONValue;\n try {\n json = convexToJson(value as Value);\n } catch {\n // A value Convex cannot encode is never stored or passed, so it reaches no\n // artifact as a value.\n return;\n }\n walkPlaced(json, placements, (secret) => {\n secrets.keep(secret);\n return secret;\n });\n};\n\nconst containsSecret = (text: string, searchable: ReadonlyArray<string>) =>\n searchable.some((secret) => text.includes(secret));\n\n/**\n * The backstop beneath every schema walk: each string, outside a marker, that\n * holds a kept secret, replaced by `replace`.\n */\nconst backstop = (\n json: JSONValue,\n searchable: ReadonlyArray<string>,\n replace: (value: JSONValue) => JSONValue,\n): JSONValue => {\n if (searchable.length === 0) return json;\n const go = (value: JSONValue): JSONValue => {\n if (typeof value === \"string\")\n return containsSecret(value, searchable) ? replace(value) : value;\n if (Array.isArray(value)) return value.map(go);\n if (isObject(value)) {\n if (isWrapped(value)) return value;\n // A document's ID and creation time are its identity, never a secret\n // value, even when a secret field holds the same ID. Convex reserves\n // these names for system fields, so no user value is skipped.\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [\n key,\n systemFields.has(key) ? item : go(item),\n ]),\n );\n }\n return value;\n };\n return go(json);\n};\n\nconst systemFields = new Set([\"_id\", \"_creationTime\"]);\n\n/**\n * `json`, a document bound for a snapshot, with every secret its placements put\n * there, and every string holding a kept secret, replaced by a marker that\n * names the raw value's position in `secrets`. The walk never enters a marker,\n * so marking a marked value changes nothing.\n */\nexport const mark = (\n json: JSONValue,\n placements: ReadonlyArray<Placement>,\n secrets: Secrets,\n): JSONValue => {\n const toMarker = (value: JSONValue): JSONValue => ({\n [markerKey]: secrets.keep(value),\n });\n return backstop(\n walkPlaced(json, placements, toMarker),\n secrets.searchable(),\n toMarker,\n );\n};\n\n/**\n * `value` as an artifact other than a snapshot writes it: every secret its\n * placements put there, and every string holding a kept secret, replaced by the\n * marker text.\n */\nexport const markValue = (\n value: Value,\n placements: ReadonlyArray<Placement>,\n secrets: Secrets,\n): Value => {\n let json: JSONValue;\n try {\n json = convexToJson(value);\n } catch {\n return value;\n }\n const marked = backstop(\n walkPlaced(json, placements, () => marker),\n secrets.searchable(),\n () => marker,\n );\n return marked === json ? value : jsonToConvex(marked);\n};\n\n/**\n * `json` with each marker replaced by the raw value it names in `secrets`, or\n * `SnapshotSecretMissing` for the first marker the store cannot answer.\n */\nexport const unmark = (\n json: JSONValue,\n secrets: Secrets,\n): Result.Result<JSONValue, KernelRefusal.SnapshotSecretMissing> => {\n let missing: unknown = undefined;\n const go = (value: JSONValue): JSONValue => {\n if (Array.isArray(value)) return value.map(go);\n if (!isObject(value)) return value;\n if (isMarker(value)) {\n const ref = value[markerKey];\n const raw = typeof ref === \"number\" ? secrets.raw(ref) : undefined;\n if (raw === undefined) missing ??= ref;\n return raw ?? null;\n }\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, go(item)]),\n );\n };\n const unmarked = go(json);\n return missing === undefined\n ? Result.succeed(unmarked)\n : Result.fail(\n new KernelRefusal.SnapshotSecretMissing({\n ref: JSON.stringify(missing),\n message: `The snapshot marks a secret at ${JSON.stringify(missing)}, which the secrets passed to restore do not hold. Restore with the secrets of the backend that exported it.`,\n }),\n );\n};\n\n/**\n * `text` with every kept secret, raw or as it appears inside a JSON string,\n * replaced by the marker.\n */\nexport const scrubText = (text: string, secrets: Secrets): string =>\n secrets.searchable().reduce((scrubbed, secret) => {\n const escaped = JSON.stringify(secret).slice(1, -1);\n return scrubbed.split(secret).join(marker).split(escaped).join(marker);\n }, text);\n\nconst isPlainObject = (value: object): boolean =>\n Object.getPrototypeOf(value) === Object.prototype ||\n Object.getPrototypeOf(value) === null;\n\n/**\n * A failure report for `thrown`, whatever was thrown: every kept secret in its\n * text, its data and its causes marked. When nothing needs marking it is\n * `thrown` itself. Otherwise it is a copy with the same prototype and fields,\n * so its class and tag still read the same. `thrown` is never changed, so a\n * subscription that keeps the raw error compares it as it was.\n */\nexport const failure = (thrown: unknown, secrets: Secrets): unknown => {\n if (secrets.searchable().length === 0) return thrown;\n const copyable = (value: object) =>\n Array.isArray(value) || value instanceof Error || isPlainObject(value);\n const field = (value: object, key: string | symbol) => {\n const descriptor = Object.getOwnPropertyDescriptor(value, key)!;\n return {\n raw: \"value\" in descriptor ? descriptor.value : Reflect.get(value, key),\n enumerable: descriptor.enumerable ?? false,\n };\n };\n // First find whether anything reachable holds a kept secret. Every object is\n // visited once, so a cycle ends.\n const visited = new Set<object>();\n const holdsSecret = (value: unknown): boolean => {\n if (typeof value === \"string\") return scrubText(value, secrets) !== value;\n if (typeof value !== \"object\" || value === null) return false;\n if (visited.has(value) || !copyable(value)) return false;\n visited.add(value);\n return Reflect.ownKeys(value).some((key) =>\n holdsSecret(field(value, key).raw),\n );\n };\n if (!holdsSecret(thrown)) return thrown;\n // Then copy every reachable error, array and plain object. Each copy is\n // registered before its fields are walked, so a cycle that leads back to it\n // reaches the copy, never the raw value.\n const copies = new Map<object, unknown>();\n const copy = (value: unknown): unknown => {\n if (typeof value === \"string\") return scrubText(value, secrets);\n if (typeof value !== \"object\" || value === null) return value;\n if (copies.has(value)) return copies.get(value);\n if (!copyable(value)) return value;\n if (Array.isArray(value)) {\n // The copy is registered first, so a cycle through it reaches the copy.\n // Its entries and its own other properties, such as a `code`, are copied.\n const items: Array<unknown> = [];\n copies.set(value, items);\n for (const key of Reflect.ownKeys(value)) {\n if (key === \"length\") continue;\n const { raw, enumerable } = field(value, key);\n if (typeof key === \"string\" && /^(0|[1-9][0-9]*)$/.test(key))\n items[Number(key)] = copy(raw);\n else\n Object.defineProperty(items, key, {\n value: copy(raw),\n writable: true,\n enumerable,\n configurable: true,\n });\n }\n items.length = value.length;\n return items;\n }\n const target: object = Object.create(Object.getPrototypeOf(value));\n copies.set(value, target);\n for (const key of Reflect.ownKeys(value)) {\n const { raw, enumerable } = field(value, key);\n Object.defineProperty(target, key, {\n value: copy(raw),\n writable: true,\n enumerable,\n configurable: true,\n });\n }\n return target;\n };\n return copy(thrown);\n};\n"],"mappings":"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"}
|