@gaganref/confect-test 10.3.1-fork.91 → 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.
Files changed (51) hide show
  1. package/dist/TestConfect.d.ts +26 -0
  2. package/dist/TestConfect.d.ts.map +1 -1
  3. package/dist/TestConfect.js +9 -1
  4. package/dist/TestConfect.js.map +1 -1
  5. package/dist/internal/Attempt.d.ts +14 -1
  6. package/dist/internal/Attempt.d.ts.map +1 -1
  7. package/dist/internal/Attempt.js +18 -1
  8. package/dist/internal/Attempt.js.map +1 -1
  9. package/dist/internal/ConvexTest.d.ts +26 -1
  10. package/dist/internal/ConvexTest.d.ts.map +1 -1
  11. package/dist/internal/ConvexTest.js +55 -27
  12. package/dist/internal/ConvexTest.js.map +1 -1
  13. package/dist/internal/Execution.d.ts +151 -1
  14. package/dist/internal/Execution.d.ts.map +1 -1
  15. package/dist/internal/Execution.js +731 -214
  16. package/dist/internal/Execution.js.map +1 -1
  17. package/dist/internal/KernelQuery.d.ts +8 -3
  18. package/dist/internal/KernelQuery.d.ts.map +1 -1
  19. package/dist/internal/KernelQuery.js +10 -3
  20. package/dist/internal/KernelQuery.js.map +1 -1
  21. package/dist/internal/KernelRefusal.d.ts +134 -0
  22. package/dist/internal/KernelRefusal.d.ts.map +1 -1
  23. package/dist/internal/KernelRefusal.js +134 -1
  24. package/dist/internal/KernelRefusal.js.map +1 -1
  25. package/dist/internal/Metering.d.ts +186 -0
  26. package/dist/internal/Metering.d.ts.map +1 -0
  27. package/dist/internal/Metering.js +401 -0
  28. package/dist/internal/Metering.js.map +1 -0
  29. package/dist/internal/Redaction.d.ts +142 -0
  30. package/dist/internal/Redaction.d.ts.map +1 -0
  31. package/dist/internal/Redaction.js +448 -0
  32. package/dist/internal/Redaction.js.map +1 -0
  33. package/dist/internal/Snapshot.d.ts +298 -0
  34. package/dist/internal/Snapshot.d.ts.map +1 -0
  35. package/dist/internal/Snapshot.js +532 -0
  36. package/dist/internal/Snapshot.js.map +1 -0
  37. package/dist/internal/Subscriptions.d.ts +13 -0
  38. package/dist/internal/Subscriptions.d.ts.map +1 -1
  39. package/dist/internal/Subscriptions.js +27 -3
  40. package/dist/internal/Subscriptions.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/TestConfect.ts +63 -4
  43. package/src/internal/Attempt.ts +24 -0
  44. package/src/internal/ConvexTest.ts +135 -38
  45. package/src/internal/Execution.ts +1141 -324
  46. package/src/internal/KernelQuery.ts +17 -6
  47. package/src/internal/KernelRefusal.ts +160 -0
  48. package/src/internal/Metering.ts +520 -0
  49. package/src/internal/Redaction.ts +730 -0
  50. package/src/internal/Snapshot.ts +945 -0
  51. package/src/internal/Subscriptions.ts +34 -4
@@ -0,0 +1,142 @@
1
+ import * as DatabaseSchema from "@confect/server/DatabaseSchema";
2
+ import { type JSONValue, type Value } from "convex/values";
3
+ import * as Result from "effect/Result";
4
+ import type * as Schema from "effect/Schema";
5
+ import * as SchemaAST from "effect/SchemaAST";
6
+ import * as KernelRefusal from "./KernelRefusal.js";
7
+ import type { FunctionPath } from "./KernelValues.js";
8
+ /**
9
+ * What a marker writes in place of a secret in text and in values: a failure
10
+ * report, a diff, a read record.
11
+ */
12
+ export declare const marker = "[redacted]";
13
+ /**
14
+ * The key of a snapshot's marker object, `{ "$redacted": <ref> }`. Convex's
15
+ * encoded JSON allows no object key that starts with `$`, so no stored value
16
+ * reads as a marker.
17
+ */
18
+ export declare const markerKey = "$redacted";
19
+ /**
20
+ * Where a value may hold secrets: the type side of a schema's AST. A walk
21
+ * follows it through the encoded value, so the value bounds the walk.
22
+ */
23
+ export type Shape = SchemaAST.AST;
24
+ /**
25
+ * A shape placed at a path inside a value: a table's schema at a document's
26
+ * root, a job's target arguments at its row's `args.0`.
27
+ */
28
+ export interface Placement {
29
+ readonly at: ReadonlyArray<string>;
30
+ readonly shape: Shape;
31
+ }
32
+ /**
33
+ * Whether `shape` holds a secret anywhere beneath it.
34
+ */
35
+ export declare const hasSecret: (shape: Shape) => boolean;
36
+ /**
37
+ * Whether the field at `fieldPath` of a document of `shape` holds a secret, or
38
+ * sits under one.
39
+ */
40
+ export declare const sensitiveAt: (shape: Shape, fieldPath: ReadonlyArray<string>) => boolean;
41
+ /**
42
+ * Which values the kernel treats as secret: the shapes of a table's documents,
43
+ * of a function's arguments and of its return value.
44
+ */
45
+ export interface Policy {
46
+ readonly table: (componentPath: string, table: string) => Shape | undefined;
47
+ readonly args: (functionPath: FunctionPath) => Shape | undefined;
48
+ readonly returns: (functionPath: FunctionPath) => Shape | undefined;
49
+ }
50
+ /**
51
+ * A schema the policy builds the first time it needs it, as Confect's specs and
52
+ * tables keep theirs.
53
+ */
54
+ export type Lazy = () => Schema.Top;
55
+ /**
56
+ * The schemas a policy reads its secret nodes from, by component path, then
57
+ * table name or `module:export`. Row 8's codegen fills this from the Confect
58
+ * specs. A test that declares it by hand writes the same record.
59
+ */
60
+ export interface Declared {
61
+ readonly tables?: Readonly<Record<string, Readonly<Record<string, Lazy>>>>;
62
+ readonly functions?: Readonly<Record<string, Readonly<Record<string, {
63
+ readonly args?: Lazy;
64
+ readonly returns?: Lazy;
65
+ }>>>>;
66
+ }
67
+ /**
68
+ * A policy from `declared`'s schemas. Each schema is built the first time its
69
+ * shape is asked for.
70
+ */
71
+ export declare const policy: (declared: Declared) => Policy;
72
+ export declare const none: Policy;
73
+ /**
74
+ * Two declarations as one: `second`'s entries win where both name a table or
75
+ * function.
76
+ */
77
+ export declare const merge: (first: Declared, second: Declared) => Declared;
78
+ /**
79
+ * A Confect database schema's tables, as a policy's `tables` entry for one
80
+ * component. No table's schema is built until the kernel asks for its shape.
81
+ */
82
+ export declare const tablesOf: (databaseSchema: DatabaseSchema.AnyWithProps) => Readonly<Record<string, Lazy>>;
83
+ /**
84
+ * The raw secrets the kernel has seen under a secret node, in the order it
85
+ * first saw each, with every string a compound secret holds beneath it. This
86
+ * store is replay state: it lives in memory beside the world and is never
87
+ * written to an artifact. A snapshot's markers name entries here by position.
88
+ */
89
+ export declare class Secrets {
90
+ private readonly values;
91
+ private readonly refs;
92
+ private readonly texts;
93
+ /**
94
+ * Keep `value` and answer its position. A value kept before answers its first
95
+ * position.
96
+ */
97
+ keep(value: JSONValue): number;
98
+ raw(ref: number): JSONValue | undefined;
99
+ /**
100
+ * Every kept secret's text long enough to search free text for, longest
101
+ * first, so a secret inside another is never marked half.
102
+ */
103
+ searchable(): ReadonlyArray<string>;
104
+ copy(): Secrets;
105
+ }
106
+ /**
107
+ * Keep every secret of `value` that `placements` put there, before it is
108
+ * validated, so a validation error can already mark it.
109
+ */
110
+ export declare const keep: (value: unknown, placements: ReadonlyArray<Placement>, secrets: Secrets) => void;
111
+ /**
112
+ * `json`, a document bound for a snapshot, with every secret its placements put
113
+ * there, and every string holding a kept secret, replaced by a marker that
114
+ * names the raw value's position in `secrets`. The walk never enters a marker,
115
+ * so marking a marked value changes nothing.
116
+ */
117
+ export declare const mark: (json: JSONValue, placements: ReadonlyArray<Placement>, secrets: Secrets) => JSONValue;
118
+ /**
119
+ * `value` as an artifact other than a snapshot writes it: every secret its
120
+ * placements put there, and every string holding a kept secret, replaced by the
121
+ * marker text.
122
+ */
123
+ export declare const markValue: (value: Value, placements: ReadonlyArray<Placement>, secrets: Secrets) => Value;
124
+ /**
125
+ * `json` with each marker replaced by the raw value it names in `secrets`, or
126
+ * `SnapshotSecretMissing` for the first marker the store cannot answer.
127
+ */
128
+ export declare const unmark: (json: JSONValue, secrets: Secrets) => Result.Result<JSONValue, KernelRefusal.SnapshotSecretMissing>;
129
+ /**
130
+ * `text` with every kept secret, raw or as it appears inside a JSON string,
131
+ * replaced by the marker.
132
+ */
133
+ export declare const scrubText: (text: string, secrets: Secrets) => string;
134
+ /**
135
+ * A failure report for `thrown`, whatever was thrown: every kept secret in its
136
+ * text, its data and its causes marked. When nothing needs marking it is
137
+ * `thrown` itself. Otherwise it is a copy with the same prototype and fields,
138
+ * so its class and tag still read the same. `thrown` is never changed, so a
139
+ * subscription that keeps the raw error compares it as it was.
140
+ */
141
+ export declare const failure: (thrown: unknown, secrets: Secrets) => unknown;
142
+ //# sourceMappingURL=Redaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Redaction.d.ts","sourceRoot":"","sources":["../../src/internal/Redaction.ts"],"names":[],"mappings":"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"}
@@ -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