@starkeep/protocol-primitives 0.1.0

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.
@@ -0,0 +1,316 @@
1
+ import * as v from 'valibot';
2
+
3
+ type StarkeepId = string & {
4
+ readonly __brand: unique symbol;
5
+ };
6
+ declare function createStarkeepId(value: string): StarkeepId;
7
+ declare function isStarkeepId(value: unknown): value is StarkeepId;
8
+
9
+ declare function generateId(): StarkeepId;
10
+ declare function generateIdAt(timestamp: number): StarkeepId;
11
+
12
+ interface HLCTimestamp {
13
+ readonly wallTime: number;
14
+ readonly counter: number;
15
+ readonly nodeId: string;
16
+ }
17
+ interface HLCClock {
18
+ now(): HLCTimestamp;
19
+ send(): HLCTimestamp;
20
+ receive(remote: HLCTimestamp): HLCTimestamp;
21
+ }
22
+
23
+ interface ClockState {
24
+ wallTime: number;
25
+ counter: number;
26
+ }
27
+ interface ClockOptions {
28
+ nodeId: string;
29
+ wallClockFunction?: () => number;
30
+ /**
31
+ * Pre-seed the clock from persisted state so a post-restart HLC never
32
+ * emits a timestamp earlier than one the node already sent.
33
+ */
34
+ initialState?: ClockState;
35
+ /**
36
+ * Invoked on every state change. Callers typically debounce and persist
37
+ * to a SyncStateStore.
38
+ */
39
+ onTick?: (state: ClockState) => void;
40
+ }
41
+ declare function createHLCClock(options: ClockOptions): HLCClock;
42
+
43
+ /** Identity element for HLC ordering. Useful as a default watermark / "never seen". */
44
+ declare const ZERO_HLC: HLCTimestamp;
45
+ declare function compareHLC(a: HLCTimestamp, b: HLCTimestamp): -1 | 0 | 1;
46
+ declare function maxHLC(a: HLCTimestamp, b: HLCTimestamp): HLCTimestamp;
47
+
48
+ declare function serializeHLC(timestamp: HLCTimestamp): string;
49
+ declare function deserializeHLC(serializedString: string): HLCTimestamp;
50
+
51
+ interface BaseRecord {
52
+ readonly id: StarkeepId;
53
+ /**
54
+ * The record's lowercase file extension, verbatim (e.g. "jpg", "md", "xyz");
55
+ * "" for extension-less files. This is the identification key. The derived
56
+ * category (`categoryOf(type)`) determines the metadata table and storage
57
+ * prefix; unmapped/empty extensions derive category "other".
58
+ */
59
+ readonly type: string;
60
+ readonly createdAt: HLCTimestamp;
61
+ updatedAt: HLCTimestamp;
62
+ readonly ownerId: string;
63
+ deletedAt: HLCTimestamp | null;
64
+ version: number;
65
+ }
66
+ /**
67
+ * A row in the shared records table. Every DataRecord is backed by a file in
68
+ * object storage (`objectStorageKey` + `contentHash`); metadata derived from
69
+ * the file lives in the per-category `record_<category>_metadata` table
70
+ * (category = `categoryOf(type)`; `other` records have no metadata table).
71
+ *
72
+ * App-level / user-authored fields that cannot be deterministically derived
73
+ * from the file (titles, captions, edit provenance, etc.) live in app-private
74
+ * storage, not on this row.
75
+ */
76
+ interface DataRecord extends BaseRecord {
77
+ readonly kind: "data";
78
+ contentHash: string;
79
+ objectStorageKey: string;
80
+ mimeType: string;
81
+ sizeBytes: number;
82
+ originalFilename: string | null;
83
+ /**
84
+ * App identity that produced this record. Set by the data-server at write
85
+ * time from the authenticated subject. Required on every write.
86
+ */
87
+ originAppId: string;
88
+ /**
89
+ * Optional parent record id linking this record to another shared record
90
+ * (e.g. a thumbnail's parent is its original). The parent may be of any
91
+ * type; cross-type parent links are permitted.
92
+ */
93
+ parentId: StarkeepId | null;
94
+ }
95
+ /**
96
+ * One row in a per-category metadata table (`shared_record_<category>_metadata`
97
+ * / `shared.record_<category>_metadata`). Columns are declared by the
98
+ * category's entry in `CATEGORIES`. Every column other than `recordId` must be
99
+ * deterministically derivable from the record's file bytes.
100
+ */
101
+ interface MetadataRow {
102
+ recordId: StarkeepId;
103
+ [column: string]: unknown;
104
+ }
105
+ type AnyRecord = DataRecord;
106
+
107
+ interface CreateDataRecordInput {
108
+ type: string;
109
+ ownerId: string;
110
+ originAppId: string;
111
+ contentHash: string;
112
+ objectStorageKey: string;
113
+ mimeType: string;
114
+ sizeBytes: number;
115
+ originalFilename?: string | null;
116
+ parentId?: StarkeepId | null;
117
+ }
118
+ declare function createDataRecord(input: CreateDataRecordInput, clock: HLCClock): DataRecord;
119
+
120
+ interface TypeDefinition {
121
+ name: string;
122
+ namespace: string;
123
+ schema: v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>;
124
+ }
125
+ interface TypeRegistry {
126
+ register(definition: TypeDefinition): void;
127
+ get(namespace: string, name: string): TypeDefinition | undefined;
128
+ getByKey(key: string): TypeDefinition | undefined;
129
+ has(namespace: string, name: string): boolean;
130
+ list(): TypeDefinition[];
131
+ }
132
+ declare function createTypeRegistry(): TypeRegistry;
133
+
134
+ declare const dataRecordSchema: v.ObjectSchema<{
135
+ readonly kind: v.LiteralSchema<"data", undefined>;
136
+ readonly contentHash: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
137
+ readonly objectStorageKey: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
138
+ readonly mimeType: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
139
+ readonly sizeBytes: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
140
+ readonly originalFilename: v.NullableSchema<v.StringSchema<undefined>, undefined>;
141
+ readonly originAppId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
142
+ readonly parentId: v.NullableSchema<v.StringSchema<undefined>, undefined>;
143
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.LengthAction<string, 26, undefined>]>;
144
+ readonly type: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
145
+ readonly createdAt: v.ObjectSchema<{
146
+ readonly wallTime: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
147
+ readonly counter: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
148
+ readonly nodeId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
149
+ }, undefined>;
150
+ readonly updatedAt: v.ObjectSchema<{
151
+ readonly wallTime: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
152
+ readonly counter: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
153
+ readonly nodeId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
154
+ }, undefined>;
155
+ readonly ownerId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
156
+ readonly deletedAt: v.NullableSchema<v.ObjectSchema<{
157
+ readonly wallTime: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
158
+ readonly counter: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
159
+ readonly nodeId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
160
+ }, undefined>, undefined>;
161
+ readonly version: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
162
+ }, undefined>;
163
+ declare function validateDataRecord(data: unknown): v.SafeParseResult<v.ObjectSchema<{
164
+ readonly kind: v.LiteralSchema<"data", undefined>;
165
+ readonly contentHash: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
166
+ readonly objectStorageKey: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
167
+ readonly mimeType: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
168
+ readonly sizeBytes: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
169
+ readonly originalFilename: v.NullableSchema<v.StringSchema<undefined>, undefined>;
170
+ readonly originAppId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
171
+ readonly parentId: v.NullableSchema<v.StringSchema<undefined>, undefined>;
172
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.LengthAction<string, 26, undefined>]>;
173
+ readonly type: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
174
+ readonly createdAt: v.ObjectSchema<{
175
+ readonly wallTime: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
176
+ readonly counter: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
177
+ readonly nodeId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
178
+ }, undefined>;
179
+ readonly updatedAt: v.ObjectSchema<{
180
+ readonly wallTime: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
181
+ readonly counter: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
182
+ readonly nodeId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
183
+ }, undefined>;
184
+ readonly ownerId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
185
+ readonly deletedAt: v.NullableSchema<v.ObjectSchema<{
186
+ readonly wallTime: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
187
+ readonly counter: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
188
+ readonly nodeId: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
189
+ }, undefined>, undefined>;
190
+ readonly version: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
191
+ }, undefined>>;
192
+
193
+ declare class StarkeepError extends Error {
194
+ readonly code: string;
195
+ readonly cause?: unknown | undefined;
196
+ constructor(message: string, code: string, cause?: unknown | undefined);
197
+ }
198
+ declare class ValidationError extends StarkeepError {
199
+ constructor(message: string, cause?: unknown);
200
+ }
201
+ declare class NotFoundError extends StarkeepError {
202
+ constructor(entity: string, id: string);
203
+ }
204
+ declare class ConflictError extends StarkeepError {
205
+ constructor(message: string);
206
+ }
207
+
208
+ type Result<T, E = Error> = {
209
+ ok: true;
210
+ value: T;
211
+ } | {
212
+ ok: false;
213
+ error: E;
214
+ };
215
+ declare function ok<T>(value: T): Result<T, never>;
216
+ declare function err<E>(error: E): Result<never, E>;
217
+ interface PaginationOptions {
218
+ limit: number;
219
+ cursor?: string;
220
+ }
221
+ interface PaginatedResult<T> {
222
+ items: T[];
223
+ nextCursor: string | null;
224
+ hasMore: boolean;
225
+ }
226
+
227
+ /**
228
+ * Single source of truth for Starkeep's shared core type system.
229
+ *
230
+ * Two registries derived from one place:
231
+ * - EXTENSIONS: lowercase file extension → mapped Category. Identification is
232
+ * by extension; MIME is never authoritative.
233
+ * - CATEGORIES: the user-facing organizational layer (mobile-style: Images,
234
+ * Videos, Documents…). Each mapped category owns one metadata table holding
235
+ * cross-format properties derivable from the file bytes.
236
+ *
237
+ * A record's `type` is the lowercase extension verbatim (e.g. "jpg", "md",
238
+ * "xyz"), even when the extension is unmapped. Its category is derived:
239
+ * `category = EXTENSIONS[ext] ?? "other"`. `other` is the terminal catch-all
240
+ * for unmapped / extension-less files — Drive-only, no metadata table, and no
241
+ * installable app can ever be granted it (apps may only declare extensions that
242
+ * are present in EXTENSIONS, and the unmapped set IS the `other` set).
243
+ *
244
+ * Every relevant system — manifest validation, IAM emission, DSQL schema-init,
245
+ * SQLite bootstrap, object-key construction, and the local data-server's access
246
+ * path — derives its view from the registries below. Adding an extension or a
247
+ * metadata column is a one-file edit here. There is no runtime registration
248
+ * path — apps cannot register new types or extend metadata columns.
249
+ */
250
+ type LogicalColumnType = "integer" | "bigint" | "real" | "text" | "timestamp" | "boolean";
251
+ interface CoreTypeMetadataColumn {
252
+ name: string;
253
+ type: LogicalColumnType;
254
+ /** Defaults to true. Set explicitly when the column must be NOT NULL. */
255
+ nullable?: boolean;
256
+ }
257
+ /** The fixed set of categories. `other` is the terminal catch-all (last). */
258
+ type Category = "image" | "video" | "audio" | "document" | "text" | "code" | "font" | "archive" | "data" | "model3d" | "other";
259
+ interface CategoryDef {
260
+ id: Category;
261
+ description: string;
262
+ /** Cross-format metadata columns. Empty for `other` (no metadata table). */
263
+ metadataColumns: CoreTypeMetadataColumn[];
264
+ }
265
+ declare const CATEGORIES: readonly CategoryDef[];
266
+ /**
267
+ * Extension (lowercase, no dot) → mapped category. `other` is NEVER a value
268
+ * here — it is exclusively the `?? "other"` fallback in `categoryOf`, which is
269
+ * why no app can ever declare an `other` extension.
270
+ */
271
+ declare const EXTENSIONS: Readonly<Record<string, Exclude<Category, "other">>>;
272
+ declare const CATEGORY_IDS: readonly Category[];
273
+ /**
274
+ * Categories an installable app may be granted — every category a real
275
+ * extension can map to, i.e. all categories EXCEPT `other`. Drive's all-access
276
+ * (`fileAccessAll`) covers `other` as well, via its `shared/*` IAM ceiling.
277
+ */
278
+ declare const APP_GRANTABLE_CATEGORIES: readonly Category[];
279
+ /** The set of known (mapped) extensions. */
280
+ declare const KNOWN_EXTENSIONS: ReadonlySet<string>;
281
+ /**
282
+ * Derived category for a record's extension/type. Unmapped or empty → "other".
283
+ * Accepts the extension with or without a leading dot, any case.
284
+ */
285
+ declare function categoryOf(ext: string): Category;
286
+ declare function getCategory(id: string): CategoryDef | undefined;
287
+ declare function isCategoryId(id: string): id is Category;
288
+ /**
289
+ * Emits a `CREATE TABLE IF NOT EXISTS shared.record_<category>_metadata`
290
+ * statement for DSQL. Single non-PL/pgSQL statement, no FK constraints — see
291
+ * `dsql-schema-init.ts` for the DSQL surface caveats. Callers must skip the
292
+ * `other` category (no metadata table).
293
+ */
294
+ declare function pgMetadataDdl(c: CategoryDef): string;
295
+ /**
296
+ * Emits a `CREATE TABLE IF NOT EXISTS shared_record_<category>_metadata`
297
+ * statement for the local SQLite bootstrap. Callers must skip the `other`
298
+ * category (no metadata table).
299
+ */
300
+ declare function sqliteMetadataDdl(c: CategoryDef): string;
301
+ /**
302
+ * Returns the SQLite metadata table name for a record's type/extension or a
303
+ * category id. The category is derived when an extension is passed, so storage
304
+ * adapters that hold only `record.type` route to the correct per-category
305
+ * table. Passing the literal `"other"` (or an unmapped extension) yields the
306
+ * `other` table name, which is never created — callers must not write metadata
307
+ * for `other` records.
308
+ */
309
+ declare function sqliteMetadataTableName(typeOrCategory: string): string;
310
+ /** DSQL/Postgres counterpart of {@link sqliteMetadataTableName}. */
311
+ declare function pgMetadataTableName(typeOrCategory: string): string;
312
+
313
+ declare function dataRecordObjectKey(typeOrExt: string, contentHash: string): string;
314
+ declare function appSyncableObjectKey(appId: string, subKey: string): string;
315
+
316
+ export { APP_GRANTABLE_CATEGORIES, type AnyRecord, type BaseRecord, CATEGORIES, CATEGORY_IDS, type Category, type CategoryDef, type ClockOptions, ConflictError, type CoreTypeMetadataColumn, type CreateDataRecordInput, type DataRecord, EXTENSIONS, type HLCClock, type HLCTimestamp, KNOWN_EXTENSIONS, type LogicalColumnType, type MetadataRow, NotFoundError, type PaginatedResult, type PaginationOptions, type Result, StarkeepError, type StarkeepId, type TypeDefinition, type TypeRegistry, ValidationError, ZERO_HLC, appSyncableObjectKey, categoryOf, compareHLC, createDataRecord, createHLCClock, createStarkeepId, createTypeRegistry, dataRecordObjectKey, dataRecordSchema, deserializeHLC, err, generateId, generateIdAt, getCategory, isCategoryId, isStarkeepId, maxHLC, ok, pgMetadataDdl, pgMetadataTableName, serializeHLC, sqliteMetadataDdl, sqliteMetadataTableName, validateDataRecord };