@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.
- 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/Attempt.d.ts +14 -1
- package/dist/internal/Attempt.d.ts.map +1 -1
- package/dist/internal/Attempt.js +18 -1
- package/dist/internal/Attempt.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 +151 -1
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +731 -214
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/KernelQuery.d.ts +8 -3
- package/dist/internal/KernelQuery.d.ts.map +1 -1
- package/dist/internal/KernelQuery.js +10 -3
- package/dist/internal/KernelQuery.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/Metering.d.ts +186 -0
- package/dist/internal/Metering.d.ts.map +1 -0
- package/dist/internal/Metering.js +401 -0
- package/dist/internal/Metering.js.map +1 -0
- 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/Attempt.ts +24 -0
- package/src/internal/ConvexTest.ts +135 -38
- package/src/internal/Execution.ts +1141 -324
- package/src/internal/KernelQuery.ts +17 -6
- package/src/internal/KernelRefusal.ts +160 -0
- package/src/internal/Metering.ts +520 -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,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
|