@docstack/client 0.1.4 → 0.1.6

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 (77) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +1 -1
  3. package/lib/core/attribute.d.ts +11 -2
  4. package/lib/core/class.d.ts +40 -6
  5. package/lib/core/content-transfer.d.ts +178 -0
  6. package/lib/core/crypto-engine/index.d.ts +82 -1
  7. package/lib/core/crypto-engine/utils.d.ts +36 -1
  8. package/lib/core/datamodel/index.d.ts +16 -0
  9. package/lib/core/domain.d.ts +1 -1
  10. package/lib/core/guarded-db.d.ts +52 -0
  11. package/lib/core/index.d.ts +106 -1
  12. package/lib/core/policy-engine/index.d.ts +35 -0
  13. package/lib/core/query-engine/classes.d.ts +24 -0
  14. package/lib/core/query-engine/executor.d.ts +11 -0
  15. package/lib/core/query-engine/index.d.ts +2 -1
  16. package/lib/core/query-engine/planner.d.ts +19 -0
  17. package/lib/core/stack.d.ts +676 -7
  18. package/lib/core/sync/class-filter.d.ts +106 -0
  19. package/lib/core/sync/filter-identity.d.ts +53 -0
  20. package/lib/core/sync/index.d.ts +334 -0
  21. package/lib/core/sync/internal-docs.d.ts +159 -0
  22. package/lib/index.d.ts +27 -1
  23. package/lib/index.js +12686 -4
  24. package/lib/index.umd.js +5017 -620
  25. package/lib/plugins/pouchdb.d.ts +43 -3
  26. package/lib/utils/logger/index.d.ts +28 -4
  27. package/lib/utils/logger/transport.d.ts +52 -11
  28. package/package.json +18 -10
  29. package/lib/core/attribute.js +0 -406
  30. package/lib/core/attribute.js.map +0 -1
  31. package/lib/core/class.js +0 -761
  32. package/lib/core/class.js.map +0 -1
  33. package/lib/core/crypto-engine/index.js +0 -229
  34. package/lib/core/crypto-engine/index.js.map +0 -1
  35. package/lib/core/crypto-engine/utils.js +0 -88
  36. package/lib/core/crypto-engine/utils.js.map +0 -1
  37. package/lib/core/datamodel/index.js +0 -1308
  38. package/lib/core/datamodel/index.js.map +0 -1
  39. package/lib/core/domain.js +0 -423
  40. package/lib/core/domain.js.map +0 -1
  41. package/lib/core/index.js +0 -532
  42. package/lib/core/index.js.map +0 -1
  43. package/lib/core/job-engine/index.js +0 -220
  44. package/lib/core/job-engine/index.js.map +0 -1
  45. package/lib/core/policy-engine/index.js +0 -232
  46. package/lib/core/policy-engine/index.js.map +0 -1
  47. package/lib/core/query-engine/accumulators.js +0 -258
  48. package/lib/core/query-engine/accumulators.js.map +0 -1
  49. package/lib/core/query-engine/evaluator.js +0 -179
  50. package/lib/core/query-engine/evaluator.js.map +0 -1
  51. package/lib/core/query-engine/executor.js +0 -405
  52. package/lib/core/query-engine/executor.js.map +0 -1
  53. package/lib/core/query-engine/index.js +0 -4
  54. package/lib/core/query-engine/index.js.map +0 -1
  55. package/lib/core/query-engine/parser.js +0 -515
  56. package/lib/core/query-engine/parser.js.map +0 -1
  57. package/lib/core/query-engine/planner.js +0 -330
  58. package/lib/core/query-engine/planner.js.map +0 -1
  59. package/lib/core/stack.js +0 -1826
  60. package/lib/core/stack.js.map +0 -1
  61. package/lib/core/test-utils/docstack.js +0 -222
  62. package/lib/core/test-utils/docstack.js.map +0 -1
  63. package/lib/core/trigger/index.js +0 -81
  64. package/lib/core/trigger/index.js.map +0 -1
  65. package/lib/index.js.map +0 -1
  66. package/lib/plugins/pouchdb.js +0 -412
  67. package/lib/plugins/pouchdb.js.map +0 -1
  68. package/lib/utils/crypto/index.js +0 -34
  69. package/lib/utils/crypto/index.js.map +0 -1
  70. package/lib/utils/index.js +0 -58
  71. package/lib/utils/index.js.map +0 -1
  72. package/lib/utils/logger/index.js +0 -20
  73. package/lib/utils/logger/index.js.map +0 -1
  74. package/lib/utils/logger/transport.js +0 -28
  75. package/lib/utils/logger/transport.js.map +0 -1
  76. package/lib/workers/dataModel.js +0 -48
  77. 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
- **The Intelligent, Offline-First Database Engine for Modern Web Apps.**
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
 
@@ -1,4 +1,4 @@
1
- import { SafeParseReturnType } from "zod";
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<SafeParseReturnType<any, any>>;
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}.
@@ -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 'winston';
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, any, 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
  /**
@@ -256,6 +277,17 @@ declare class Class extends Class_ {
256
277
  addOrUpdateCard: (params: {
257
278
  [key: string]: any;
258
279
  }, cardId?: string) => Promise<Document | null>;
280
+ /**
281
+ * Pushes a document to the database.
282
+ * This is an alias for {@alias addOrUpdateCard}.
283
+ *
284
+ * @param params - The document data
285
+ * @param docId - Optional document ID. If provided, performs an update.
286
+ * @returns The created or updated document
287
+ */
288
+ push: (params: {
289
+ [key: string]: any;
290
+ }, docId?: string) => Promise<Document | null>;
259
291
  /**
260
292
  * Updates an existing document (card) of this class.
261
293
  *
@@ -293,7 +325,9 @@ declare class Class extends Class_ {
293
325
  */
294
326
  getCards: (selector?: {
295
327
  [key: string]: any;
296
- }, fields?: string[], skip?: number, limit?: number) => Promise<Document[]>;
328
+ }, fields?: string[], skip?: number, limit?: number, sort?: {
329
+ [field: string]: "asc" | "desc";
330
+ }[]) => Promise<Document[]>;
297
331
  /**
298
332
  * Retrieves one or more documents by their IDs.
299
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
- export declare const encryptWithAesGcm: (plaintext: string, key: CryptoKey) => Promise<EncryptedPayload>;
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[]>;
@@ -1,5 +1,5 @@
1
1
  import { Document, Domain as Domain_, DomainModel, Stack, DomainRelationValidation, DomainRelationParams, Class, RelationDocument } from "@docstack/shared";
2
- import { Logger } from "winston";
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,52 @@
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
+ }) => PouchDB.Database<T>;