@docstack/client 0.1.5 → 0.1.8
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/LICENSE +10 -0
- package/README.md +1 -1
- package/lib/core/attribute.d.ts +11 -2
- package/lib/core/class.d.ts +29 -6
- package/lib/core/content-transfer.d.ts +178 -0
- package/lib/core/crypto-engine/index.d.ts +82 -1
- package/lib/core/crypto-engine/utils.d.ts +36 -1
- package/lib/core/datamodel/index.d.ts +16 -0
- package/lib/core/domain.d.ts +1 -1
- package/lib/core/guarded-db.d.ts +53 -0
- package/lib/core/index.d.ts +138 -2
- package/lib/core/job-engine/schedule.d.ts +90 -0
- package/lib/core/job-engine/scheduler.d.ts +206 -0
- package/lib/core/policy-engine/index.d.ts +35 -0
- package/lib/core/query-engine/classes.d.ts +24 -0
- package/lib/core/query-engine/executor.d.ts +11 -0
- package/lib/core/query-engine/index.d.ts +2 -1
- package/lib/core/query-engine/planner.d.ts +19 -0
- package/lib/core/stack.d.ts +690 -7
- package/lib/core/sync/class-filter.d.ts +106 -0
- package/lib/core/sync/filter-identity.d.ts +53 -0
- package/lib/core/sync/index.d.ts +352 -0
- package/lib/core/sync/internal-docs.d.ts +159 -0
- package/lib/core/sync/tenants.d.ts +67 -0
- package/lib/index.d.ts +38 -1
- package/lib/index.js +5865 -664
- package/lib/index.umd.js +5903 -681
- package/lib/plugins/pouchdb.d.ts +43 -3
- package/lib/utils/logger/index.d.ts +28 -4
- package/lib/utils/logger/transport.d.ts +52 -11
- package/package.json +18 -10
- package/lib/core/attribute.js +0 -406
- package/lib/core/attribute.js.map +0 -1
- package/lib/core/class.js +0 -774
- package/lib/core/class.js.map +0 -1
- package/lib/core/crypto-engine/index.js +0 -229
- package/lib/core/crypto-engine/index.js.map +0 -1
- package/lib/core/crypto-engine/utils.js +0 -88
- package/lib/core/crypto-engine/utils.js.map +0 -1
- package/lib/core/datamodel/index.js +0 -1308
- package/lib/core/datamodel/index.js.map +0 -1
- package/lib/core/domain.js +0 -423
- package/lib/core/domain.js.map +0 -1
- package/lib/core/index.js +0 -532
- package/lib/core/index.js.map +0 -1
- package/lib/core/job-engine/index.js +0 -220
- package/lib/core/job-engine/index.js.map +0 -1
- package/lib/core/policy-engine/index.js +0 -232
- package/lib/core/policy-engine/index.js.map +0 -1
- package/lib/core/query-engine/accumulators.js +0 -258
- package/lib/core/query-engine/accumulators.js.map +0 -1
- package/lib/core/query-engine/evaluator.js +0 -179
- package/lib/core/query-engine/evaluator.js.map +0 -1
- package/lib/core/query-engine/executor.js +0 -405
- package/lib/core/query-engine/executor.js.map +0 -1
- package/lib/core/query-engine/index.js +0 -4
- package/lib/core/query-engine/index.js.map +0 -1
- package/lib/core/query-engine/parser.js +0 -515
- package/lib/core/query-engine/parser.js.map +0 -1
- package/lib/core/query-engine/planner.js +0 -330
- package/lib/core/query-engine/planner.js.map +0 -1
- package/lib/core/stack.js +0 -1827
- package/lib/core/stack.js.map +0 -1
- package/lib/core/test-utils/docstack.js +0 -222
- package/lib/core/test-utils/docstack.js.map +0 -1
- package/lib/core/trigger/index.js +0 -81
- package/lib/core/trigger/index.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/plugins/pouchdb.js +0 -412
- package/lib/plugins/pouchdb.js.map +0 -1
- package/lib/utils/crypto/index.js +0 -34
- package/lib/utils/crypto/index.js.map +0 -1
- package/lib/utils/index.js +0 -58
- package/lib/utils/index.js.map +0 -1
- package/lib/utils/logger/index.js +0 -20
- package/lib/utils/logger/index.js.map +0 -1
- package/lib/utils/logger/transport.js +0 -28
- package/lib/utils/logger/transport.js.map +0 -1
- package/lib/workers/dataModel.js +0 -48
- package/lib/workers/dataModel.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Onyx <hello@onyx.ac> (https://onyx.ac)
|
|
4
|
+
|
|
5
|
+
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
|
|
6
|
+
International License. You are free to share and adapt this work, including
|
|
7
|
+
commercially, provided you give appropriate attribution and distribute any
|
|
8
|
+
derivative works under the same license.
|
|
9
|
+
|
|
10
|
+
Full legal text: https://creativecommons.org/licenses/by-sa/4.0/legalcode
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @docstack/client
|
|
5
5
|
|
|
6
|
-
**
|
|
6
|
+
**One does not simply stack documents.**
|
|
7
7
|
|
|
8
8
|
DocStack Client is a standalone, browser-based datastore built on PouchDB. It bridges the gap between simple client-side storage and full-fledged backend databases by bringing schemas, relationships, SQL-like querying, and background processing directly to the browser.
|
|
9
9
|
|
package/lib/core/attribute.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ZodSafeParseResult } from "zod";
|
|
2
2
|
import Class from "./class.js";
|
|
3
3
|
import { Attribute as Attribute_, AttributeModel, AttributeType, AttributeTypeConfig } from "@docstack/shared";
|
|
4
4
|
/**
|
|
@@ -57,6 +57,15 @@ declare class Attribute extends Attribute_ {
|
|
|
57
57
|
* @throws Error if the reference configuration is invalid
|
|
58
58
|
*/
|
|
59
59
|
ensureReferenceConfigIsValid: () => Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Built Zod fields, shared across attributes with the same type and config.
|
|
62
|
+
*
|
|
63
|
+
* An attribute's validator is a pure function of `(type, config)`, and the same
|
|
64
|
+
* handful of shapes recur across every class build - which happens per snapshot,
|
|
65
|
+
* per schema propagation, and per cold class fetch. `foreign_key` is the one type
|
|
66
|
+
* excluded: its refinement closes over the owning instance's class and stack.
|
|
67
|
+
*/
|
|
68
|
+
private static fieldCache;
|
|
60
69
|
/**
|
|
61
70
|
* Builds the Zod validation schema based on attribute type and configuration.
|
|
62
71
|
* Called automatically during construction.
|
|
@@ -120,7 +129,7 @@ declare class Attribute extends Attribute_ {
|
|
|
120
129
|
* }
|
|
121
130
|
* ```
|
|
122
131
|
*/
|
|
123
|
-
validate: (data: any) => Promise<
|
|
132
|
+
validate: (data: any) => Promise<ZodSafeParseResult<any>>;
|
|
124
133
|
/**
|
|
125
134
|
* Adds an attribute to its parent class and persists to the database.
|
|
126
135
|
* Used internally by {@link Attribute.create}.
|
package/lib/core/class.d.ts
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { Class as Class_, TriggerModel } from "@docstack/shared";
|
|
2
2
|
import { Stack, ClassModel, Attribute as Attribute_, AttributeModel, Document } from "@docstack/shared";
|
|
3
3
|
import Attribute from "./attribute.js";
|
|
4
|
-
import { Logger } from '
|
|
4
|
+
import type { Logger } from '../utils/logger/index.js';
|
|
5
5
|
import { Trigger } from "./trigger/index.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
/**
|
|
8
|
+
* How a Class instance should be built.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Just the schema, for validating one write - no live subscription to release.
|
|
13
|
+
* const schemaOnly = await Class.fetch(stack, 'Task', { subscribe: false });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export type ClassBuildOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Whether the instance watches its documents and emits `doc` events. Defaults to
|
|
19
|
+
* `true`.
|
|
20
|
+
*
|
|
21
|
+
* Set it to `false` for an instance that is only being read for its schema.
|
|
22
|
+
* Subscribing is not free - the handle lives until it is closed - so a caller that
|
|
23
|
+
* builds a Class per write or per read row must not subscribe, or the database
|
|
24
|
+
* accumulates live feeds and PouchDB warns about its `destroyed` listeners.
|
|
25
|
+
*/
|
|
26
|
+
subscribe?: boolean;
|
|
27
|
+
};
|
|
7
28
|
/**
|
|
8
29
|
* Represents a data class (schema definition) in the DocStack database.
|
|
9
30
|
*
|
|
@@ -44,7 +65,7 @@ declare class Class extends Class_ {
|
|
|
44
65
|
/** The raw schema definition from the ClassModel. */
|
|
45
66
|
schema: ClassModel["schema"];
|
|
46
67
|
/** Zod schema for runtime validation of document data. */
|
|
47
|
-
schemaZOD: z.ZodObject<any
|
|
68
|
+
schemaZOD: z.ZodObject<any>;
|
|
48
69
|
/** The unique identifier for this class (e.g., 'Task', 'Class-123'). */
|
|
49
70
|
id?: string;
|
|
50
71
|
/** The underlying ClassModel document. */
|
|
@@ -71,7 +92,7 @@ declare class Class extends Class_ {
|
|
|
71
92
|
* @param schema - Initial schema definition
|
|
72
93
|
* @returns A new Class instance (not persisted)
|
|
73
94
|
*/
|
|
74
|
-
static get: (stack: Stack, id: string, name: string, type: ClassModel["~class"], description?: string, schema?: ClassModel["schema"]) => Class;
|
|
95
|
+
static get: (stack: Stack, id: string, name: string, type: ClassModel["~class"], description?: string, schema?: ClassModel["schema"], options?: ClassBuildOptions) => Class;
|
|
75
96
|
/**
|
|
76
97
|
* Creates a new class and persists it to the database.
|
|
77
98
|
* This is the primary factory method for creating new classes.
|
|
@@ -97,7 +118,7 @@ declare class Class extends Class_ {
|
|
|
97
118
|
* @param classModel - The ClassModel document from the database
|
|
98
119
|
* @returns The hydrated Class instance
|
|
99
120
|
*/
|
|
100
|
-
static buildFromModel: (stack: Stack, classModel: ClassModel) => Promise<Class>;
|
|
121
|
+
static buildFromModel: (stack: Stack, classModel: ClassModel, options?: ClassBuildOptions) => Promise<Class>;
|
|
101
122
|
/**
|
|
102
123
|
* Fetches a class by its document ID.
|
|
103
124
|
*
|
|
@@ -123,7 +144,7 @@ declare class Class extends Class_ {
|
|
|
123
144
|
* }
|
|
124
145
|
* ```
|
|
125
146
|
*/
|
|
126
|
-
static fetch: (stack: Stack, className: string) => Promise<Class>;
|
|
147
|
+
static fetch: (stack: Stack, className: string, options?: ClassBuildOptions) => Promise<Class>;
|
|
127
148
|
uniqueCheck: (doc: Document) => Promise<boolean>;
|
|
128
149
|
bulkUniqueCheck: (pKs: string[]) => Promise<boolean>;
|
|
129
150
|
/**
|
|
@@ -304,7 +325,9 @@ declare class Class extends Class_ {
|
|
|
304
325
|
*/
|
|
305
326
|
getCards: (selector?: {
|
|
306
327
|
[key: string]: any;
|
|
307
|
-
}, fields?: string[], skip?: number, limit?: number
|
|
328
|
+
}, fields?: string[], skip?: number, limit?: number, sort?: {
|
|
329
|
+
[field: string]: "asc" | "desc";
|
|
330
|
+
}[]) => Promise<Document[]>;
|
|
308
331
|
/**
|
|
309
332
|
* Retrieves one or more documents by their IDs.
|
|
310
333
|
*
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Document, RelationDocument } from "@docstack/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Moving a stack's *content* between databases, without its datamodel.
|
|
4
|
+
*
|
|
5
|
+
* `stack.dump()` is the other kind of export: every document exactly as stored, which
|
|
6
|
+
* means class models, patches, users, sessions, policies, design documents - and
|
|
7
|
+
* encrypted attributes as unreadable {@link EncryptedPayload} blobs. That is a debugging
|
|
8
|
+
* tool and a backup of one database.
|
|
9
|
+
*
|
|
10
|
+
* This is the portable one. It carries the documents an application put in, in the clear,
|
|
11
|
+
* so that {@link ClientStack.importContent} can place them into a *different* stack -
|
|
12
|
+
* different device, different key, schema built by that stack's own patches.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
/** The envelope format identifier, so an importer can refuse what it does not understand. */
|
|
17
|
+
export declare const CONTENT_EXPORT_FORMAT = "docstack/content-export@1";
|
|
18
|
+
/**
|
|
19
|
+
* `~class` values that describe the stack rather than hold its content.
|
|
20
|
+
*
|
|
21
|
+
* `~self` is the bootstrap class-of-classes; `class` and `superclass` are class models;
|
|
22
|
+
* `domain` is a relation definition; `patch` is the local ledger of applied patches.
|
|
23
|
+
*/
|
|
24
|
+
export declare const META_CLASSES: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Reports whether a class name belongs to application content.
|
|
27
|
+
*
|
|
28
|
+
* DocStack names everything it owns with a leading `~` - `~User`, `~Group`, `~Policy`,
|
|
29
|
+
* `~Job`, `~JobRun`, `~UserSession`, `~AuthModule`, `~lock` - and reserves the handful of
|
|
30
|
+
* unprefixed names in {@link META_CLASSES} for the datamodel itself. Everything else was
|
|
31
|
+
* created by an application.
|
|
32
|
+
*
|
|
33
|
+
* @param className - A `~class` value or class-model id.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* isContentClassName("Task"); // true
|
|
38
|
+
* isContentClassName("~User"); // false - DocStack's own
|
|
39
|
+
* isContentClassName("class"); // false - a class model
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare const isContentClassName: (className: unknown) => className is string;
|
|
43
|
+
/** The shape the predicates need. */
|
|
44
|
+
type MaybeDoc = {
|
|
45
|
+
_id?: unknown;
|
|
46
|
+
"~class"?: unknown;
|
|
47
|
+
"~domain"?: unknown;
|
|
48
|
+
active?: unknown;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Reports whether a document is application content rather than part of the stack.
|
|
52
|
+
*
|
|
53
|
+
* @param doc - Any stored document.
|
|
54
|
+
*/
|
|
55
|
+
export declare const isContentDocument: (doc: MaybeDoc | null | undefined) => boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Reports whether a document is an application relation.
|
|
58
|
+
*
|
|
59
|
+
* Relations carry `~domain` and no `~class` (see `isRelation` in `@docstack/shared`), so
|
|
60
|
+
* they need their own test rather than falling out of {@link isContentDocument}.
|
|
61
|
+
*
|
|
62
|
+
* @param doc - Any stored document.
|
|
63
|
+
*/
|
|
64
|
+
export declare const isContentRelation: (doc: MaybeDoc | null | undefined) => boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Strips the fields that belong to the source database rather than to the document.
|
|
67
|
+
*
|
|
68
|
+
* `_rev` above all: a revision from one database means nothing in another, and carrying
|
|
69
|
+
* it into an import turns every write into a conflict.
|
|
70
|
+
*
|
|
71
|
+
* @param doc - The document to clean.
|
|
72
|
+
* @returns A copy without the transient fields.
|
|
73
|
+
*/
|
|
74
|
+
export declare const stripTransientFields: <T extends object>(doc: T) => T;
|
|
75
|
+
/**
|
|
76
|
+
* A stack's content, portable to another stack.
|
|
77
|
+
*
|
|
78
|
+
* **The documents are plaintext.** Encrypted attributes are decrypted on the way out so
|
|
79
|
+
* that a stack holding a different document key can read them; the file is therefore as
|
|
80
|
+
* sensitive as the data it holds, and protecting it is the caller's job.
|
|
81
|
+
*/
|
|
82
|
+
export interface ContentExport {
|
|
83
|
+
/** Always {@link CONTENT_EXPORT_FORMAT}. */
|
|
84
|
+
format: string;
|
|
85
|
+
/** ISO-8601, when the export was taken. */
|
|
86
|
+
exportedAt: string;
|
|
87
|
+
/** Where it came from. Advisory - an import does not require a match. */
|
|
88
|
+
source: {
|
|
89
|
+
stack: string;
|
|
90
|
+
appVersion: string;
|
|
91
|
+
schemaVersion?: string;
|
|
92
|
+
};
|
|
93
|
+
/** Class names present in {@link documents}, so an import can report gaps up front. */
|
|
94
|
+
classes: string[];
|
|
95
|
+
/** Domain names present in {@link relations}. */
|
|
96
|
+
domains: string[];
|
|
97
|
+
/** Content documents, decrypted, without `_rev`. */
|
|
98
|
+
documents: Document[];
|
|
99
|
+
/** Relation documents, ordered after the documents they connect. */
|
|
100
|
+
relations: RelationDocument[];
|
|
101
|
+
}
|
|
102
|
+
/** Controls what {@link ClientStack.exportContent} collects. */
|
|
103
|
+
export interface ContentExportOptions {
|
|
104
|
+
/** Restrict to these class names. Defaults to every content class in the stack. */
|
|
105
|
+
classes?: string[];
|
|
106
|
+
/** Restrict to these domain names. Defaults to every content domain. */
|
|
107
|
+
domains?: string[];
|
|
108
|
+
/** Include relation documents. Defaults to `true`. */
|
|
109
|
+
includeRelations?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Include soft-deleted documents (`active: false`). Defaults to `false` - an export
|
|
112
|
+
* is the live content, not the tombstones.
|
|
113
|
+
*/
|
|
114
|
+
includeInactive?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Export encrypted attributes as `null` instead of refusing when the stack is locked.
|
|
117
|
+
* Defaults to `false`.
|
|
118
|
+
*
|
|
119
|
+
* A locked stack cannot decrypt, so those attributes read back as `null` and an export
|
|
120
|
+
* taken that way is lossy in a way nothing downstream can detect. Refusing is the safe
|
|
121
|
+
* default; this is the escape hatch for a caller that genuinely wants the rest.
|
|
122
|
+
*/
|
|
123
|
+
allowLossyWhenLocked?: boolean;
|
|
124
|
+
}
|
|
125
|
+
/** What an import did, per category. */
|
|
126
|
+
export interface ContentImportReport {
|
|
127
|
+
documents: {
|
|
128
|
+
written: number;
|
|
129
|
+
skipped: number;
|
|
130
|
+
};
|
|
131
|
+
relations: {
|
|
132
|
+
written: number;
|
|
133
|
+
skipped: number;
|
|
134
|
+
};
|
|
135
|
+
/** Everything that did not go in cleanly, with the reason. */
|
|
136
|
+
issues: ContentImportIssue[];
|
|
137
|
+
}
|
|
138
|
+
/** One document that could not be imported, or that was changed on the way in. */
|
|
139
|
+
export interface ContentImportIssue {
|
|
140
|
+
docId: string;
|
|
141
|
+
kind: "missing-class" | "missing-domain" | "unknown-attribute" | "conflict" | "rejected";
|
|
142
|
+
detail: string;
|
|
143
|
+
}
|
|
144
|
+
/** Controls how {@link ClientStack.importContent} reconciles against the datamodel. */
|
|
145
|
+
export interface ContentImportOptions {
|
|
146
|
+
/**
|
|
147
|
+
* What to do with a document whose class this stack does not have.
|
|
148
|
+
* `"skip"` (default) records an issue and carries on; `"fail"` throws.
|
|
149
|
+
*
|
|
150
|
+
* Creating the class is deliberately not offered: the export carries no schema, so
|
|
151
|
+
* DocStack would have to infer one from the data and would get it wrong. Apply the
|
|
152
|
+
* patch that defines the class first.
|
|
153
|
+
*/
|
|
154
|
+
onMissingClass?: "skip" | "fail";
|
|
155
|
+
/**
|
|
156
|
+
* What to do with an attribute the target class does not define.
|
|
157
|
+
* `"strip"` (default) drops it and records an issue; `"fail"` throws; `"keep"` passes
|
|
158
|
+
* it through to schema validation, which will reject it.
|
|
159
|
+
*/
|
|
160
|
+
onUnknownAttribute?: "strip" | "fail" | "keep";
|
|
161
|
+
/**
|
|
162
|
+
* Replace a document that already exists. Defaults to `false`, which records a
|
|
163
|
+
* conflict and leaves the stored document alone.
|
|
164
|
+
*/
|
|
165
|
+
overwrite?: boolean;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Validates an import payload's envelope before anything is written.
|
|
169
|
+
*
|
|
170
|
+
* A plain validator rather than an `asserts` signature: the caller already types its
|
|
171
|
+
* parameter, so there is nothing to narrow, and an assertion function reached through an
|
|
172
|
+
* import needs a declaration TypeScript can see (TS2775).
|
|
173
|
+
*
|
|
174
|
+
* @param payload - The value handed to {@link ClientStack.importContent}.
|
|
175
|
+
* @throws Error when it is not a content export this version understands.
|
|
176
|
+
*/
|
|
177
|
+
export declare const assertContentExport: (payload: unknown) => void;
|
|
178
|
+
export {};
|
|
@@ -30,6 +30,18 @@ export declare class CryptoEngine {
|
|
|
30
30
|
private documentKey?;
|
|
31
31
|
/** The imported CryptoKey for Web Crypto API operations. */
|
|
32
32
|
private cryptoKey?;
|
|
33
|
+
/** Identifier of {@link documentKey}, stamped onto everything it encrypts. */
|
|
34
|
+
private documentKeyId?;
|
|
35
|
+
/**
|
|
36
|
+
* Keys this engine can still read but will never write with, by identifier.
|
|
37
|
+
*
|
|
38
|
+
* Re-keying a database is not instantaneous: between the moment a new key becomes the
|
|
39
|
+
* one that encrypts and the moment the last field has been rewritten, both keys are
|
|
40
|
+
* needed at once. Retiring a key rather than discarding it is what lets that interval
|
|
41
|
+
* be arbitrarily long - and therefore what lets a re-key be done in the background,
|
|
42
|
+
* interrupted, and resumed.
|
|
43
|
+
*/
|
|
44
|
+
private retiredKeys;
|
|
33
45
|
private readonly logger;
|
|
34
46
|
/** Reference to the parent stack. */
|
|
35
47
|
private readonly stack;
|
|
@@ -53,6 +65,47 @@ export declare class CryptoEngine {
|
|
|
53
65
|
* @returns The hex-encoded document key, or undefined if not set
|
|
54
66
|
*/
|
|
55
67
|
getDocumentKey(): string;
|
|
68
|
+
/**
|
|
69
|
+
* Returns the identifier of the key this engine currently encrypts with.
|
|
70
|
+
*
|
|
71
|
+
* Every payload it writes carries this value, so comparing it against a stored
|
|
72
|
+
* payload's `kid` says whether that field is current or still under an older key -
|
|
73
|
+
* which is what makes an incremental re-key possible.
|
|
74
|
+
*
|
|
75
|
+
* @returns The key identifier, or `undefined` when no key is held.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const current = stack.cryptoEngine.getKeyId();
|
|
80
|
+
* const stale = doc.ssn.kid !== current; // needs rewriting
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
getKeyId(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Keeps an old document key available for reading after it has stopped being used.
|
|
86
|
+
*
|
|
87
|
+
* Call this with the previous key before installing a replacement, and fields written
|
|
88
|
+
* under either will open while the database is rewritten a piece at a time. Without
|
|
89
|
+
* it, swapping the key makes every not-yet-rewritten field unreadable, which is what
|
|
90
|
+
* forces a re-key to be a single offline pass that cannot be resumed if it stops
|
|
91
|
+
* halfway.
|
|
92
|
+
*
|
|
93
|
+
* @param documentKey - The hex-encoded key to retire.
|
|
94
|
+
* @returns The retired key's identifier.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* await stack.cryptoEngine.retireDocumentKey(oldKey); // still readable
|
|
99
|
+
* await stack.unlock(newKey); // now writes under newKey
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
retireDocumentKey(documentKey: string): Promise<string>;
|
|
103
|
+
/**
|
|
104
|
+
* Identifiers of every key this engine can read with, current one first.
|
|
105
|
+
*
|
|
106
|
+
* @returns Key identifiers; empty when no key is held.
|
|
107
|
+
*/
|
|
108
|
+
getReadableKeyIds(): string[];
|
|
56
109
|
/**
|
|
57
110
|
* Generates a cryptographically secure random string.
|
|
58
111
|
* Useful for generating salts or nonces.
|
|
@@ -62,6 +115,25 @@ export declare class CryptoEngine {
|
|
|
62
115
|
*/
|
|
63
116
|
generateRandomString(length?: number): string;
|
|
64
117
|
encryptValueForMarker(value: unknown): Promise<EncryptedPayload>;
|
|
118
|
+
/**
|
|
119
|
+
* Tests whether the current document key is the one a stack was written with.
|
|
120
|
+
*
|
|
121
|
+
* AES-GCM authenticates its ciphertext, so a wrong key fails to decrypt rather than
|
|
122
|
+
* yielding plausible garbage - which makes the stored marker a reliable canary. This
|
|
123
|
+
* is deliberately separate from {@link decryptValue}, which swallows failures to keep
|
|
124
|
+
* reads non-fatal: here the failure *is* the answer.
|
|
125
|
+
*
|
|
126
|
+
* @param marker - The stored `encryptedMarker` payload.
|
|
127
|
+
* @returns `true` if the held key decrypts the marker.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* if (!await stack.cryptoEngine.verifyMarker(config.encryptedMarker)) {
|
|
132
|
+
* throw new Error("wrong document key");
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
verifyMarker(marker: unknown): Promise<boolean>;
|
|
65
137
|
/**
|
|
66
138
|
* Checks if the crypto engine is enabled.
|
|
67
139
|
* @returns `true` if encryption is enabled
|
|
@@ -87,6 +159,15 @@ export declare class CryptoEngine {
|
|
|
87
159
|
*/
|
|
88
160
|
unwrapAndStoreDocumentKey(wrappedDocumentKey?: string | null, derivedKey?: string | null): Promise<string>;
|
|
89
161
|
private getCryptoKey;
|
|
162
|
+
/**
|
|
163
|
+
* Chooses the key that can open a payload.
|
|
164
|
+
*
|
|
165
|
+
* A payload names its key, so an old field found mid-re-key is decrypted with the key
|
|
166
|
+
* it was actually written under instead of failing against the current one. Payloads
|
|
167
|
+
* from before identifiers existed name nothing, and are tried against the current key
|
|
168
|
+
* - which is what they meant when only one key could exist.
|
|
169
|
+
*/
|
|
170
|
+
private resolveKeyFor;
|
|
90
171
|
private encryptValue;
|
|
91
172
|
private decryptValue;
|
|
92
173
|
/**
|
|
@@ -118,4 +199,4 @@ export declare class CryptoEngine {
|
|
|
118
199
|
decryptDocument(document: Document, classObj?: Class, encryptedKeys?: string[]): Promise<void>;
|
|
119
200
|
}
|
|
120
201
|
export type { EncryptedPayload } from "./utils.js";
|
|
121
|
-
export { wrapDocumentKey, unwrapDocumentKey } from "./utils.js";
|
|
202
|
+
export { wrapDocumentKey, unwrapDocumentKey, deriveKeyId, isEncryptedPayload } from "./utils.js";
|
|
@@ -3,13 +3,48 @@ export type EncryptedPayload = {
|
|
|
3
3
|
iv: string;
|
|
4
4
|
data: string;
|
|
5
5
|
alg: "AES-GCM";
|
|
6
|
+
/**
|
|
7
|
+
* Identifies the document key this value was encrypted under.
|
|
8
|
+
*
|
|
9
|
+
* Without it a database that holds two keys is illegible: a field either opens or it
|
|
10
|
+
* does not, and nothing says which key it wanted, so re-keying has to be
|
|
11
|
+
* all-or-nothing and cannot be resumed after an interruption. With it, the fields
|
|
12
|
+
* still under an old key can be found, so re-keying becomes incremental and
|
|
13
|
+
* restartable.
|
|
14
|
+
*
|
|
15
|
+
* Optional because payloads written before it existed do not carry one. A payload
|
|
16
|
+
* without a `kid` is assumed to belong to the primary key - which is what it meant
|
|
17
|
+
* when there could only be one.
|
|
18
|
+
*
|
|
19
|
+
* @see deriveKeyId
|
|
20
|
+
*/
|
|
21
|
+
kid?: string;
|
|
6
22
|
};
|
|
7
23
|
export declare const hexToBytes: (hex: string) => Uint8Array;
|
|
8
24
|
export declare const toBase64: (data: ArrayBuffer | Uint8Array) => string;
|
|
9
25
|
export declare const fromBase64: (value: string) => Uint8Array;
|
|
10
26
|
export declare const importAesKeyFromHex: (hexKey: string, usages?: KeyUsage[]) => Promise<CryptoKey>;
|
|
11
27
|
export declare const isEncryptedPayload: (value: unknown) => value is EncryptedPayload;
|
|
12
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Derives the stable identifier of a document key.
|
|
30
|
+
*
|
|
31
|
+
* A truncated SHA-256 of the key's bytes: deterministic, so two devices handed the same
|
|
32
|
+
* key arrive at the same identifier without talking to each other, and one-way, so
|
|
33
|
+
* publishing it in every stored payload discloses nothing about the key itself. Eight
|
|
34
|
+
* bytes is far more than needed to tell apart the handful of keys a database sees across
|
|
35
|
+
* its re-keyings, and short enough to sit on every encrypted field without weighing it
|
|
36
|
+
* down.
|
|
37
|
+
*
|
|
38
|
+
* @param hexKey - The hex-encoded document key.
|
|
39
|
+
* @returns A 16-character hex identifier.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const kid = await deriveKeyId(documentKey); // e.g. "9f2c1ab30e77d541"
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare const deriveKeyId: (hexKey: string) => Promise<string>;
|
|
47
|
+
export declare const encryptWithAesGcm: (plaintext: string, key: CryptoKey, kid?: string) => Promise<EncryptedPayload>;
|
|
13
48
|
export declare const decryptWithAesGcm: (payload: EncryptedPayload, key: CryptoKey) => Promise<string>;
|
|
14
49
|
export declare const wrapDocumentKey: (documentKey: string, derivedKeyHex: string) => Promise<string>;
|
|
15
50
|
export declare const unwrapDocumentKey: (wrappedDocumentKey: string, cryptoKey: CryptoKey, derivedKeyHex: string) => Promise<string>;
|
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
import { Patch } from "@docstack/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Every document id the system patches seed.
|
|
4
|
+
*
|
|
5
|
+
* Derived from the patches rather than hand-listed, so it cannot drift as the datamodel
|
|
6
|
+
* grows. Load-bearing for replication: each client applies these patches for itself, so
|
|
7
|
+
* every one of these documents already exists on every client. Sending them costs quota
|
|
8
|
+
* and invites conflicts - two devices writing `Policy-Admin` independently - while
|
|
9
|
+
* telling a peer nothing it did not already know. See ADR-0023.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* SYSTEM_SEEDED_DOC_IDS.includes("Group-Admin"); // true - patch-seeded everywhere
|
|
14
|
+
* SYSTEM_SEEDED_DOC_IDS.includes("user-alice"); // false - created by an application
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const SYSTEM_SEEDED_DOC_IDS: readonly string[];
|
|
2
18
|
export declare function getSystemPatches(currentVersion: string): Promise<Patch[]>;
|
|
3
19
|
export declare function getAllSystemPatches(): Promise<Patch[]>;
|
package/lib/core/domain.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Document, Domain as Domain_, DomainModel, Stack, DomainRelationValidation, DomainRelationParams, Class, RelationDocument } from "@docstack/shared";
|
|
2
|
-
import { Logger } from "
|
|
2
|
+
import type { Logger } from "../utils/logger/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Represents a relationship definition between two Classes in the DocStack database.
|
|
5
5
|
*
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when application code tries to write around a stack's authoring path.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* try {
|
|
7
|
+
* await stack.db.bulkDocs({ docs: incoming, new_edits: false });
|
|
8
|
+
* } catch (error) {
|
|
9
|
+
* error instanceof StackWriteGuardError; // true
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare class StackWriteGuardError extends Error {
|
|
14
|
+
name: string;
|
|
15
|
+
/** The method the caller reached for. */
|
|
16
|
+
readonly method: string;
|
|
17
|
+
constructor(method: string, reason: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wraps a PouchDB instance so the validation-skipping paths are unreachable.
|
|
21
|
+
*
|
|
22
|
+
* @param db - The stack's PouchDB instance, already carrying {@link StackPlugin}'s
|
|
23
|
+
* replaced `bulkDocs`/`bulkGet`.
|
|
24
|
+
* @returns A handle with the same surface, minus the escape hatches.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const guarded = createGuardedDb(rawDb);
|
|
29
|
+
* await guarded.put({ _id: "Task-1", "~class": "Task" }); // validated as usual
|
|
30
|
+
* await guarded.bulkDocs({ docs, new_edits: false }); // rejects with StackWriteGuardError
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const createGuardedDb: <T extends {}>(db: PouchDB.Database<T>) => PouchDB.Database<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Restores the pristine PouchDB methods on a stack's database.
|
|
36
|
+
*
|
|
37
|
+
* Replication has to write documents verbatim (`new_edits: false`) and has to read them
|
|
38
|
+
* exactly as they are stored - {@link StackPlugin}'s `bulkGet` decrypts on read, which
|
|
39
|
+
* would push plaintext to a remote that is supposed to hold ciphertext. This handle
|
|
40
|
+
* skips the plugin on both methods while leaving every other method, and the database's
|
|
41
|
+
* own identity, untouched.
|
|
42
|
+
*
|
|
43
|
+
* @param db - The stack's PouchDB instance.
|
|
44
|
+
* @param pristine - The `bulkDocs`/`bulkGet` captured before the plugin replaced them.
|
|
45
|
+
* @returns A handle suitable for `PouchDB.replicate`/`PouchDB.sync`.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export declare const createReplicationDb: <T extends {}>(db: PouchDB.Database<T>, pristine: {
|
|
50
|
+
bulkDocs: Function;
|
|
51
|
+
bulkGet: Function;
|
|
52
|
+
get: Function;
|
|
53
|
+
}) => PouchDB.Database<T>;
|