@happyvertical/smrt-preferences 0.0.0-stage → 0.55.10

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,127 @@
1
+ /**
2
+ * The overview layer (#3727 phase 3): overview-shaped load, save, reset,
3
+ * page load and widget load over the generic preference store, kind
4
+ * `overview`, surface id = `OverviewDefinition.id`.
5
+ *
6
+ * Tiers, low to high: the page's `definition.defaults`, the tenant default,
7
+ * then the user's override. The tenant tier merges into the defaults first
8
+ * and the user tier applies to that result, so a user override is a delta
9
+ * against the tenant default. Saves are strict (`checkOverviewOverride`, only
10
+ * the canonical override is stored); loads are lenient (a bad entry is
11
+ * dropped and reported, the rest render).
12
+ */
13
+ import { type LoadedOverview, type LoadOverviewOptions, type OverviewDefinition, type OverviewDocument, type OverviewIssue, type OverviewOverride, type WidgetLoadInput, type WidgetRegistry } from '@happyvertical/smrt-svelte/overview/server';
14
+ import type { DatabaseInterface } from '@happyvertical/sql';
15
+ import type { PreferenceScope } from './kinds.js';
16
+ import { type PreferenceStore } from './store.js';
17
+ /** One stored tier, resolved. */
18
+ export interface OverviewTierState {
19
+ /** The document this tier applies to (sanitized). */
20
+ base: OverviewDocument;
21
+ /** The document after this tier (sanitized). */
22
+ document: OverviewDocument;
23
+ /** The stored override, canonical with invalid entries removed, or `null`. */
24
+ override: OverviewOverride | null;
25
+ /** Concurrency token: echo it back on save or reset; `null` = no row. */
26
+ revision: string | null;
27
+ /** Entries of the stored override dropped on load, and why. */
28
+ issues: OverviewIssue[];
29
+ }
30
+ /** What a page load needs to render and edit one overview. */
31
+ export interface OverviewState {
32
+ overviewId: string;
33
+ /** The tenant default over `definition.defaults`. */
34
+ tenant: OverviewTierState;
35
+ /** The user's override over the tenant default; `null` without a user. */
36
+ user: OverviewTierState | null;
37
+ /** What the principal sees: the user tier's document, else the tenant's. */
38
+ document: OverviewDocument;
39
+ /** Every stored-tier issue (tenant first). Page-default issues excluded. */
40
+ issues: OverviewIssue[];
41
+ /** Feed `createOverview({ canCustomize })` for the tier being edited. */
42
+ canCustomize: {
43
+ tenant: boolean;
44
+ user: boolean;
45
+ };
46
+ }
47
+ export type OverviewWriteFailure = {
48
+ ok: false;
49
+ reason: 'not_allowed';
50
+ } | {
51
+ ok: false;
52
+ reason: 'conflict';
53
+ } | {
54
+ ok: false;
55
+ reason: 'invalid';
56
+ issues: OverviewIssue[];
57
+ };
58
+ export type OverviewSaveResult = {
59
+ ok: true;
60
+ override: OverviewOverride | null;
61
+ revision: string | null;
62
+ } | OverviewWriteFailure;
63
+ export type OverviewResetResult = {
64
+ ok: true;
65
+ } | OverviewWriteFailure;
66
+ export interface OverviewSaveInput {
67
+ scope: PreferenceScope;
68
+ /** The client's override (untrusted). `null` resets the tier. */
69
+ override: unknown;
70
+ /**
71
+ * The `revision` the client loaded for this tier (`null` when it loaded no
72
+ * row). A stale revision fails with `conflict`, never overwrites.
73
+ */
74
+ revision: string | null;
75
+ }
76
+ export interface OverviewResetInput {
77
+ scope: PreferenceScope;
78
+ /**
79
+ * The revision the client loaded: a string guards the delete, `null`
80
+ * conflicts when a row exists, and omitting it resets regardless.
81
+ */
82
+ revision?: string | null;
83
+ }
84
+ export interface OverviewPageOptions extends LoadOverviewOptions {
85
+ /** Which tier's document to load: `user` (default) or `tenant` (editing defaults). */
86
+ scope?: PreferenceScope;
87
+ }
88
+ export type OverviewWidgetResult = {
89
+ ok: true;
90
+ data: unknown;
91
+ } | {
92
+ ok: false;
93
+ reason: 'not_allowed';
94
+ } | {
95
+ ok: false;
96
+ reason: 'invalid';
97
+ issues: OverviewIssue[];
98
+ } | {
99
+ ok: false;
100
+ reason: 'load_failed';
101
+ code: 'load_failed' | 'timeout' | 'invalid_data';
102
+ };
103
+ export interface OverviewStoreOptions {
104
+ db: DatabaseInterface;
105
+ /** Share a generic store (default: one over `db`). */
106
+ preferences?: PreferenceStore;
107
+ }
108
+ export interface OverviewStore {
109
+ /** Read both tiers for the principal and resolve the merge. */
110
+ load(definition: OverviewDefinition, registry: WidgetRegistry): Promise<OverviewState>;
111
+ /** Validate and store one tier's override (canonical form only). */
112
+ save(definition: OverviewDefinition, registry: WidgetRegistry, input: OverviewSaveInput): Promise<OverviewSaveResult>;
113
+ /** Delete one tier's row, returning that tier to the tier below. */
114
+ reset(definition: OverviewDefinition, input: OverviewResetInput): Promise<OverviewResetResult>;
115
+ /** `load` plus `loadOverview`, for a page's server load. */
116
+ loadPage(definition: OverviewDefinition, registry: WidgetRegistry, ctx: WidgetLoadInput, options?: OverviewPageOptions): Promise<OverviewState & {
117
+ loaded: LoadedOverview;
118
+ }>;
119
+ /** The data of one widget, for the controller's `loadWidget` remote function. */
120
+ loadWidget(definition: OverviewDefinition, registry: WidgetRegistry, widget: unknown, ctx: WidgetLoadInput, options?: LoadOverviewOptions): Promise<OverviewWidgetResult>;
121
+ }
122
+ /**
123
+ * The persistence and server-load API of overview surfaces. Every call acts
124
+ * as the ambient principal.
125
+ */
126
+ export declare function createOverviewStore(options: OverviewStoreOptions): OverviewStore;
127
+ //# sourceMappingURL=overview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../src/overview.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { type PermissionDefinition } from '@happyvertical/smrt-users';
2
+ /** Authorizes changing and resetting a tenant's default overview layouts. */
3
+ export declare const CUSTOMIZE_OVERVIEW_PERMISSION = "overviews.customize";
4
+ /** Authorizes a principal to keep their own overview layouts. */
5
+ export declare const PERSONALIZE_OVERVIEW_PERMISSION = "overviews.personalize";
6
+ /** Authorizes changing and resetting a tenant's default shell layout. */
7
+ export declare const CUSTOMIZE_SHELL_PERMISSION = "shell.customize";
8
+ /** Authorizes a principal to keep their own shell layout and settings. */
9
+ export declare const PERSONALIZE_SHELL_PERMISSION = "shell.personalize";
10
+ export declare const PREFERENCE_PERMISSION_DEFINITIONS: PermissionDefinition[];
11
+ /** Register the built-in preference permissions once. */
12
+ export declare function ensurePreferencePermissionsRegistered(): void;
13
+ /**
14
+ * `collection.action` parts of a permission slug for the users operation
15
+ * guards: the action is the last segment (`fields.policy.manage` is
16
+ * collection `fields.policy`, action `manage`).
17
+ */
18
+ export declare function splitPermissionSlug(slug: string): {
19
+ collection: string;
20
+ action: string;
21
+ };
22
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,2BAA2B,CAAC;AAEnC,6EAA6E;AAC7E,eAAO,MAAM,6BAA6B,wBAAwB,CAAC;AAEnE,iEAAiE;AACjE,eAAO,MAAM,+BAA+B,0BAA0B,CAAC;AAEvE,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAE5D,0EAA0E;AAC1E,eAAO,MAAM,4BAA4B,sBAAsB,CAAC;AAEhE,eAAO,MAAM,iCAAiC,EAAE,oBAAoB,EA4BnE,CAAC;AAIF,yDAAyD;AACzD,wBAAgB,qCAAqC,IAAI,IAAI,CAM5D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,CAMA"}
@@ -0,0 +1,9 @@
1
+ /** Whether the ambient principal may see `row`. */
2
+ export declare function isPreferenceRowVisible(row: unknown): boolean;
3
+ /**
4
+ * Register the owner-read interceptor (idempotent). Called when the model
5
+ * module loads and again before every store read, so a cleared interceptor
6
+ * registry (tests, `GlobalInterceptors.clear()`) cannot silently drop it.
7
+ */
8
+ export declare function ensurePreferenceReadGuard(): void;
9
+ //# sourceMappingURL=read-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-guard.d.ts","sourceRoot":"","sources":["../src/read-guard.ts"],"names":[],"mappings":"AAoCA,mDAAmD;AACnD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAO5D;AAkDD;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAmEhD"}
@@ -0,0 +1,341 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "sensitiveFieldsExcluded": true,
4
+ "generatedAt": "1970-01-01T00:00:00.000Z",
5
+ "packageName": "@happyvertical/smrt-preferences",
6
+ "packageVersion": "0.55.10",
7
+ "sourceManifestPath": "dist/manifest.json",
8
+ "agentDocPath": "AGENTS.md",
9
+ "sourceHashes": {
10
+ "manifest": "77cf90e3f71e50d105f986834e28d7a4e94c5fd71e14fe78dbc68e0738663e09",
11
+ "packageJson": "a22fe7ae3d0a58bda4cb389ed3f8336d7651a2969a8844ce83a409305f266bed",
12
+ "agents": "ff8ef8173cb499f021a99d03561d2bf50145c010e5a21c989f476caf5698ed30",
13
+ "moduleDoc:agents/preference-kinds.md": "b2e3301b279445a693a95494cd98b0fd791d70fb80e411e0444006c07d259f2a"
14
+ },
15
+ "exports": [
16
+ ".",
17
+ "./manifest",
18
+ "./manifest.json"
19
+ ],
20
+ "dependencies": {
21
+ "@happyvertical/smrt-core": "workspace:*",
22
+ "@happyvertical/smrt-svelte": "workspace:*",
23
+ "@happyvertical/smrt-tenancy": "workspace:*",
24
+ "@happyvertical/smrt-users": "workspace:*",
25
+ "@happyvertical/sql": "catalog:",
26
+ "@happyvertical/smrt-vitest": "workspace:*",
27
+ "@types/node": "24.13.6",
28
+ "svelte": "^5.57.1",
29
+ "typescript": "6.0.3",
30
+ "vite": "8.1.4",
31
+ "vitest": "4.1.10"
32
+ },
33
+ "smrtDependencies": [
34
+ "@happyvertical/smrt-core",
35
+ "@happyvertical/smrt-svelte",
36
+ "@happyvertical/smrt-tenancy",
37
+ "@happyvertical/smrt-users",
38
+ "@happyvertical/smrt-vitest"
39
+ ],
40
+ "sdkDependencies": [
41
+ "@happyvertical/sql"
42
+ ],
43
+ "tags": [
44
+ "cross-package",
45
+ "multi-tenancy",
46
+ "overview",
47
+ "preferences",
48
+ "shell",
49
+ "smrt"
50
+ ],
51
+ "risks": [
52
+ "cross-package-refs:2",
53
+ "sensitive-fields-excluded"
54
+ ],
55
+ "objects": [
56
+ {
57
+ "name": "UiPreferenceRecordCollection",
58
+ "qualifiedName": "@happyvertical/smrt-preferences:UiPreferenceRecordCollection",
59
+ "collection": "uipreferencerecords",
60
+ "description": "Reads of UiPreferenceRecord. Not decorated: the model has no generated surface, so a decorated collection would only add a second manifest schema for the same table.",
61
+ "tableName": "_smrt_ui_preferences",
62
+ "packageName": "@happyvertical/smrt-preferences",
63
+ "extends": "SmrtCollection",
64
+ "fields": [],
65
+ "relationships": [],
66
+ "methods": [
67
+ "findTier"
68
+ ],
69
+ "methodSignatures": [
70
+ {
71
+ "name": "findTier",
72
+ "async": true,
73
+ "params": [
74
+ "tenantId: string",
75
+ "kind: string",
76
+ "surfaceId: string",
77
+ "scope: PreferenceScope",
78
+ "userId?: string"
79
+ ],
80
+ "returns": "Promise<UiPreferenceRecord | null>"
81
+ }
82
+ ],
83
+ "surfaces": [
84
+ {
85
+ "kind": "api",
86
+ "name": "uipreferencerecords.findTier",
87
+ "operation": "findTier",
88
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecordCollection",
89
+ "path": "/uipreferencerecords/findTier",
90
+ "method": "POST"
91
+ },
92
+ {
93
+ "kind": "mcp",
94
+ "name": "uipreferencerecordcollection_findtier",
95
+ "operation": "findTier",
96
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecordCollection"
97
+ }
98
+ ],
99
+ "relationshipFeatures": [
100
+ "uuidColumns"
101
+ ],
102
+ "tags": [],
103
+ "risks": []
104
+ },
105
+ {
106
+ "name": "UiPreferenceRecord",
107
+ "qualifiedName": "@happyvertical/smrt-preferences:UiPreferenceRecord",
108
+ "collection": "uipreferencerecords",
109
+ "description": "One stored user-interface preference (#3727): the canonical payload of a registered preference kind (overview, shell-layout, ...) for one surface at one tier of one tenant.",
110
+ "tableName": "_smrt_ui_preferences",
111
+ "packageName": "@happyvertical/smrt-preferences",
112
+ "extends": "SmrtObject",
113
+ "fields": [
114
+ {
115
+ "name": "tenantId",
116
+ "type": "text",
117
+ "required": true,
118
+ "columnType": "UUID"
119
+ },
120
+ {
121
+ "name": "kind",
122
+ "type": "text",
123
+ "required": true,
124
+ "columnType": "TEXT",
125
+ "default": ""
126
+ },
127
+ {
128
+ "name": "surfaceId",
129
+ "type": "text",
130
+ "required": true,
131
+ "columnType": "TEXT",
132
+ "default": ""
133
+ },
134
+ {
135
+ "name": "scopeType",
136
+ "type": "text",
137
+ "required": true,
138
+ "columnType": "TEXT",
139
+ "default": "tenant"
140
+ },
141
+ {
142
+ "name": "userId",
143
+ "type": "crossPackageRef",
144
+ "required": false,
145
+ "related": "@happyvertical/smrt-users:User",
146
+ "columnType": "UUID"
147
+ },
148
+ {
149
+ "name": "scopeKey",
150
+ "type": "text",
151
+ "required": true,
152
+ "columnType": "TEXT"
153
+ },
154
+ {
155
+ "name": "payloadJson",
156
+ "type": "text",
157
+ "required": true,
158
+ "columnType": "TEXT"
159
+ },
160
+ {
161
+ "name": "formatVersion",
162
+ "type": "integer",
163
+ "required": false,
164
+ "columnType": "INTEGER"
165
+ },
166
+ {
167
+ "name": "updatedBy",
168
+ "type": "crossPackageRef",
169
+ "required": false,
170
+ "related": "@happyvertical/smrt-users:User",
171
+ "columnType": "UUID"
172
+ }
173
+ ],
174
+ "relationships": [
175
+ {
176
+ "name": "userId",
177
+ "type": "crossPackageRef",
178
+ "required": false,
179
+ "related": "@happyvertical/smrt-users:User",
180
+ "columnType": "UUID"
181
+ },
182
+ {
183
+ "name": "updatedBy",
184
+ "type": "crossPackageRef",
185
+ "required": false,
186
+ "related": "@happyvertical/smrt-users:User",
187
+ "columnType": "UUID"
188
+ }
189
+ ],
190
+ "methods": [
191
+ "delete",
192
+ "getPayload",
193
+ "loadFromId",
194
+ "loadFromSlug",
195
+ "save",
196
+ "setPayload"
197
+ ],
198
+ "methodSignatures": [
199
+ {
200
+ "name": "delete",
201
+ "async": true,
202
+ "params": [
203
+ "options?: DeleteOptions"
204
+ ],
205
+ "returns": "Promise<void>"
206
+ },
207
+ {
208
+ "name": "getPayload",
209
+ "returns": "unknown"
210
+ },
211
+ {
212
+ "name": "loadFromId",
213
+ "async": true,
214
+ "returns": "Promise<void>"
215
+ },
216
+ {
217
+ "name": "loadFromSlug",
218
+ "async": true,
219
+ "returns": "Promise<void>"
220
+ },
221
+ {
222
+ "name": "save",
223
+ "async": true,
224
+ "params": [
225
+ "options?: SaveOptions"
226
+ ],
227
+ "returns": "Promise<this>"
228
+ },
229
+ {
230
+ "name": "setPayload",
231
+ "params": [
232
+ "value: unknown"
233
+ ],
234
+ "returns": "void"
235
+ }
236
+ ],
237
+ "tenant": {
238
+ "scoped": true,
239
+ "mode": "required",
240
+ "field": "tenantId"
241
+ },
242
+ "conflictColumns": [
243
+ "tenant_id",
244
+ "kind",
245
+ "surface_id",
246
+ "scope_type",
247
+ "scope_key"
248
+ ],
249
+ "surfaces": [],
250
+ "withheldSurfaces": [
251
+ {
252
+ "kind": "api",
253
+ "operation": "getPayload",
254
+ "code": "not-included",
255
+ "reason": "not listed in api.include",
256
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecord"
257
+ },
258
+ {
259
+ "kind": "api",
260
+ "operation": "loadFromId",
261
+ "code": "lifecycle-method",
262
+ "reason": "`loadFromId` is a framework lifecycle method, not a distinct operation",
263
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecord"
264
+ },
265
+ {
266
+ "kind": "api",
267
+ "operation": "loadFromSlug",
268
+ "code": "lifecycle-method",
269
+ "reason": "`loadFromSlug` is a framework lifecycle method, not a distinct operation",
270
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecord"
271
+ },
272
+ {
273
+ "kind": "api",
274
+ "operation": "save",
275
+ "code": "lifecycle-method",
276
+ "reason": "`save` is a framework lifecycle method, not a distinct operation",
277
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecord"
278
+ },
279
+ {
280
+ "kind": "api",
281
+ "operation": "setPayload",
282
+ "code": "not-included",
283
+ "reason": "not listed in api.include",
284
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecord"
285
+ }
286
+ ],
287
+ "relationshipFeatures": [
288
+ "crossPackageRef",
289
+ "uuidColumns"
290
+ ],
291
+ "tags": [],
292
+ "risks": []
293
+ }
294
+ ],
295
+ "surfaces": [
296
+ {
297
+ "kind": "api",
298
+ "name": "uipreferencerecords.findTier",
299
+ "operation": "findTier",
300
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecordCollection",
301
+ "path": "/uipreferencerecords/findTier",
302
+ "method": "POST"
303
+ },
304
+ {
305
+ "kind": "mcp",
306
+ "name": "uipreferencerecordcollection_findtier",
307
+ "operation": "findTier",
308
+ "objectName": "@happyvertical/smrt-preferences:UiPreferenceRecordCollection"
309
+ }
310
+ ],
311
+ "prompts": [],
312
+ "relationshipsV2": {
313
+ "foreignKeyFields": 0,
314
+ "crossPackageRefFields": 2,
315
+ "junctionCollections": 0,
316
+ "hierarchicalObjects": 0,
317
+ "polymorphicAssociations": 0,
318
+ "uuidColumns": 8
319
+ },
320
+ "agentDoc": "# smrt-preferences\n\nUser-interface preferences (#3727): one `_smrt_ui_preferences` system table\nfor tenant defaults and per-user preferences of any registered **kind**, the\nkind registry, the generic store, and two built-in kinds: `overview`\n(customizable overview pages of `@happyvertical/smrt-svelte/overview`, with an\noverview-shaped layer on top) and `shell-layout` (the AdminShell\n`ShellSettingsDelta`). The kind contract is in\n[agents/preference-kinds.md](agents/preference-kinds.md); the overview\nsurface itself is in\n[smrt-svelte overview-surfaces.md](../smrt-svelte/agents/overview-surfaces.md).\n\n## Why a package of its own\n\nThe repo puts each layered-override table in the package that owns its\nresolution (`smrt-prompts`, `smrt-playbooks`, `smrt-features`,\n`smrt-fields`). `smrt-svelte` has no runtime dependency on core, tenancy or\nusers and holds no tables. This package imports only smrt-svelte's\nSvelte-free entries (`./overview/server`, `./workspace/server`).\n\n## Pieces\n\n| File | Contents |\n| --- | --- |\n| `src/models/UiPreferenceRecord.ts` | The row; owns identity, scope shape and authorization; refuses unregistered kinds |\n| `src/collections/UiPreferenceRecordCollection.ts` | Undecorated reads (`findTier`); internal, not exported; no generated surface |\n| `src/write-capability.ts` | The store-only write capability (internal) |\n| `src/read-guard.ts` | The owner-read interceptor (internal) |\n| `src/kinds.ts` | `registerPreferenceKind`, `requirePreferenceKind`, `UnknownPreferenceKindError`, the validation contract |\n| `src/store.ts` | `createPreferenceStore({ db })`: generic `load` / `save` / `reset(kind, surfaceId, ...)` |\n| `src/kinds/overview.ts`, `src/overview.ts` | The `overview` kind and `createOverviewStore` (`load`, `save`, `reset`, `loadPage`, `loadWidget`, `withTenantDefaults`) |\n| `src/kinds/shell-layout.ts` | The `shell-layout` kind and `createShellSettingsPreferences` (the server half of a `ShellSettingsAdapter`) |\n| `src/kinds/builtin.ts` | Registers the two built-in kinds (imported by the entry and the store) |\n| `src/context.ts` | The ambient principal and `PreferenceAccessError` |\n| `src/permissions.ts` | `overviews.customize` / `overviews.personalize`, `shell.customize` / `shell.personalize` |\n\n## Table\n\n`_smrt_ui_preferences`: `tenant_id` (UUID), `kind`, `surface_id`,\n`scope_type` (`tenant` | `user`), `user_id` (UUID, NULL on tenant rows),\n`scope_key` (`__tenant__` or the user id), `payload_json` (the kind's\ncanonical payload), `format_version`, `updated_by`, plus the SmrtObject\ncolumns. Unique key `(tenant_id, kind, surface_id, scope_type, scope_key)`;\nevery key column is NOT NULL, so the index is total on every engine. It ships\nthrough manifest-driven migrations (`smrt db:migrate`), never runtime DDL;\n`src/__tests__/migration.test.ts` applies it with `migrateSmrtSchemas` on\nSQLite and DuckDB and asserts parity afterwards. On DuckDB only this table is\nmigrated in tests: smrt-users' tables carry ON UPDATE CASCADE foreign keys\nDuckDB cannot create.\n\n## Invariants\n\n- **Kinds gate everything.** The store and the model refuse a kind that is\n not registered (`UnknownPreferenceKindError`), so no row exists that no code\n can validate. A kind's `validate` runs strictly on save (refuse unless `ok`,\n store only `canonical`, `null` deletes the row) and leniently on load (use\n `canonical`, report `issues`, never rewrite the row). A row written at a\n newer `format_version` than the kind's is dropped and reported.\n- **Store-only writes.** Only the store writes rows: it passes a\n module-private symbol capability (`src/write-capability.ts`, not exported;\n the smrt-approvals pattern) bound to the exact `payloadJson` the kind\n validated, single-use, and grants each delete the same way. The model\n refuses any insert, update (also in `validateBeforeSave`, so bulk creates\n are covered) or delete without it, so `new UiPreferenceRecord(...).save()`,\n a collection `create()` or `row.setPayload(); row.save()` never store an\n unvalidated payload. Tests seed corrupt storage with raw SQL\n (`src/__tests__/raw-rows.ts`). This guards in-process callers, not raw SQL.\n- **Owner-safe reads, in the predicate.** A principal never reads another\n user's row. `src/read-guard.ts` (registered when the model loads and again\n before every store read) adds `scope_key IN ('__tenant__', <own user id>)`\n (tenant rows only without a user id) to the read predicate BEFORE SQL\n runs, intersecting any caller `scopeKey in`. Core's read entry points and\n the hook each runs: `list`, `listByIds`, `findAll`, `listWithLatestRelated`\n (`beforeList` + `afterList`); `get`, `findById`, `findOne`, `getOrUpsert`,\n `update` / `delete` lookups, and model hydration `loadFromId` /\n `loadFromSlug` (`beforeGet`; collection `get` also `afterGet`); `count`,\n `counts`, `facets` (`beforeList` only, so ownership must be in the\n predicate); `semanticSearch*`, `findSimilar*` (`beforeList`, some also\n `afterList`); `query` (`beforeQuery` + `afterQuery`; raw SQL cannot carry\n the predicate, so the guard refuses it). Every read path runs at least one\n hook. The after-read hooks re-check hydrated rows as defence in depth (a\n row whose scope cannot be told is dropped), and hydration also re-checks in\n the model. `count()` and `facets()` therefore count only visible rows.\n- **Not an app API.** `UiPreferenceRecord` stays a root export only because\n generated consumer registration imports every public manifest object from\n the root; the collection is not exported. Apps use `createPreferenceStore`,\n `createOverviewStore` and `createShellSettingsPreferences`.\n- **Tiers.** The tenant row is the default; the user row applies on top, and\n `validate` for the user tier receives the tenant tier's canonical payload\n (`ctx.tenant`). User rows are per tenant: preferences name tenant data.\n- **Identity is ambient.** No API takes a tenant or user id. A missing\n principal throws `PreferenceAccessError`; a principal without a user id has\n no user tier and cannot write one. A session-only principal (users\n `createSessionHandler()` without `enterTenantContext`) is entered as a\n tenant context with the same identity before any table access.\n- **Tenancy.** The model is `@TenantScoped({ mode: 'required' })`, and every\n store read also names the tenant explicitly (a super-admin bypass context\n does not filter reads). `save()`/`delete()` refuse a row whose stored\n tenant, kind, surface or tier differs from memory (matched by predicate:\n DuckDB returns raw UUID columns as lossy HUGEINTs), a foreign tenant, and a\n user row that is not the principal's.\n- **Permissions are per kind.** Each kind names `{ tenant, user }` slugs;\n the model asserts them (`assertOperationPermission`, fail closed) and the\n store reports them as `canCustomize` (`checkOperationPermission`). A slug\n must be in the users catalog or it never grants. The users resolver denies\n a principal with no user id even with the slug in its set. The built-in\n `personalize` slugs are in users' self-personalization list (seeded for\n owner/admin/member/viewer once registered).\n- **Permission source.** The principal's permission set is authoritative,\n including an explicit empty set: a tenancy adapter without\n `resolvePermissions` refuses everyone (fails closed). Hosts must configure\n it, or enter identity through users' `enterTenantContext`; never widen it\n here.\n- **Concurrency.** Optimistic: a save passes the `revision` (ISO\n `updated_at`) it loaded, `null` meaning \"no row\". Updates use core's\n revision CAS (`save({ expectedUpdatedAt })`), a first write is a strict\n insert (`_insertOnly`) so a racing first write hits the unique index, and\n a delete is guarded the same way. `reset` treats `revision: null` like\n save does (a row that exists now conflicts); only an omitted revision\n resets unguarded. Mismatch: `{ ok: false, reason: 'conflict' }`. Exception, documented: `createShellSettingsPreferences().write`\n without a revision reads the current one first, i.e. last-writer-wins,\n because the shell's `ShellSettingsAdapter.write(delta)` carries none.\n- **Overview `loadWidget`** lets a principal without either customize\n permission load only a widget on their resolved overview; anything else is\n `not_allowed`. The widget is sanitized against the page definition first.\n- **No generated surface** (`api: { include: [] }`, `cli: false`,\n `mcp: { include: [] }`): a generated route would skip kind validation.\n\n## Tests\n\n`pnpm test` runs the overview suite and the kinds suite on SQLite and DuckDB\nplus the migration suite; `pnpm test:postgres` runs both suites on PostgreSQL\n(`*.optional.test.ts`, skipped without a database).\n",
321
+ "moduleDocs": [
322
+ {
323
+ "path": "agents/preference-kinds.md",
324
+ "module": "preference-kinds",
325
+ "content": "# Preference kinds\n\nThe contract between `_smrt_ui_preferences` and the code that owns a\npreference's meaning (#3727). Package rules are in [../AGENTS.md](../AGENTS.md).\n\n## Registering a kind\n\n```ts\nregisterPreferenceKind({\n kind: 'board-columns', // /^[a-z][a-z0-9-]{0,47}$/\n formatVersion: 1, // positive integer, stored per row\n permissions: { tenant: 'boards.customize', user: 'boards.personalize' },\n validate(payload, ctx) { // never throws\n return { ok, canonical, issues };\n },\n}, { replace? }) // -> dispose()\n```\n\nRegistration is code at module scope, process-wide (one registry per\nprocess). Registering a kind twice throws unless `{ replace: true }`. Both\nslugs must be `collection.action` and registered in the users catalog\n(`registerPermissionDefinitions`), or they never grant; add a `personalize`\nslug to users' built-in self-personalization list only when every built-in\nrole should hold it.\n\nThe store authorizes against the principal's permission set from the\nambient context and never widens it. A tenancy adapter (SvelteKit, Express)\nwithout `resolvePermissions` supplies an explicit empty set, which the store\ntreats as authoritative: every write is `not_allowed` and `canCustomize` is\nfalse for everyone, owners included. Configure `resolvePermissions`, or let\nusers' `createSessionHandler({ enterTenantContext: true })` enter the context.\n\n## `validate(payload, ctx)`\n\n| `ctx` field | Meaning |\n| --- | --- |\n| `kind`, `surfaceId` | What is being validated (`surfaceId` matches `PREFERENCE_SURFACE_PATTERN`) |\n| `scope` | `tenant` or `user` |\n| `phase` | `save` (strict) or `load` (lenient) |\n| `formatVersion` | The version the payload was written at (the kind's own on save); migrate older payloads here |\n| `tenant` | User tier only: the tenant tier's canonical payload, or `null` |\n| `options` | Host-supplied, kind-specific inputs, passed through from `load` / `save` |\n\nReturn `canonical`: the payload with every invalid part removed (`null` means\nno preference), `issues`: what was removed and why (`{ path, code, message,\ndetail? }`), and `ok`: false whenever anything was removed or refused.\n\n- On **save** the store refuses unless `ok`, stores only `canonical` (as\n JSON with the kind's `formatVersion`), and deletes the row when `canonical`\n is `null`.\n- On **load** the store uses `canonical` and reports `issues` per tier; the\n row is not rewritten. Unparseable JSON and rows from a newer\n `formatVersion` are dropped before `validate` runs. A `validate` that\n throws on load drops the tier with a `validator_error` issue; on save the\n error propagates and nothing is stored. Validators should not throw.\n- A payload is data, never code: no query strings, URLs or expressions a\n reader would execute. Authorization happens where the data is read, never\n through payload values.\n\n## Built-in kinds\n\n| Kind | Payload | Validator | Permissions | `options` |\n| --- | --- | --- | --- | --- |\n| `overview` | `OverviewOverride` (sparse, version 1) | save: `checkOverviewOverride`; load: `resolveOverview` + `diffOverview`, against the page definition (user tier: tenant-merged via `withTenantDefaults`) | `overviews.customize` / `overviews.personalize` | `{ definition, registry }`; `definition.id` must equal the surface id |\n| `shell-layout` | `ShellSettingsDelta` | `sanitizeShellSettingsDelta` from `@happyvertical/smrt-svelte/workspace/server` | `shell.customize` / `shell.personalize` | none |\n\n## Store calls\n\n`createPreferenceStore({ db })` returns `load(kind, surfaceId, { options })`\n(both tiers, `canCustomize` from the kind's pair), `save(kind, surfaceId, {\nscope, payload, revision, options })` and `reset(kind, surfaceId, { scope,\nrevision? })` (`null` conflicts when a row exists; omitted is unguarded). Failures are `{ ok: false, reason: 'not_allowed' | 'conflict'\n| 'invalid' }`; an unregistered kind throws `UnknownPreferenceKindError`.\n\nKind-shaped layers sit on top: `createOverviewStore` (the overview page API)\nand `createShellSettingsPreferences` (the server half of the shell's\n`ShellSettingsAdapter`: `read(surfaceId)` merges tenant and user with\n`mergeShellSettingsDelta`; `write(surfaceId, delta, { scope?, revision? })`\nis last-writer-wins only when the revision is omitted).\n"
326
+ }
327
+ ],
328
+ "browser": {
329
+ "status": "server-only",
330
+ "issues": [
331
+ "#3617",
332
+ "#3624",
333
+ "#3626"
334
+ ],
335
+ "via": [
336
+ "@happyvertical/smrt-profiles",
337
+ "@happyvertical/smrt-tenancy",
338
+ "@happyvertical/smrt-users"
339
+ ]
340
+ }
341
+ }
@@ -0,0 +1,86 @@
1
+ import './kinds/builtin.js';
2
+ import type { DatabaseInterface } from '@happyvertical/sql';
3
+ import { type PreferenceIssue, type PreferenceScope } from './kinds.js';
4
+ /** One stored tier, validated for use. */
5
+ export interface PreferenceTier {
6
+ /** The kind's canonical payload, invalid parts removed; `null` = none. */
7
+ payload: unknown | null;
8
+ /** Concurrency token: echo it back on save or reset; `null` = no row. */
9
+ revision: string | null;
10
+ /** What was dropped from the stored payload on load, and why. */
11
+ issues: PreferenceIssue[];
12
+ }
13
+ export interface PreferenceState {
14
+ kind: string;
15
+ surfaceId: string;
16
+ /** The tenant default. */
17
+ tenant: PreferenceTier;
18
+ /** The principal's own preference; `null` without a user. */
19
+ user: PreferenceTier | null;
20
+ /** Every tier's issues, tenant first. */
21
+ issues: PreferenceIssue[];
22
+ /** Whether the principal may write each tier (the kind's permission pair). */
23
+ canCustomize: {
24
+ tenant: boolean;
25
+ user: boolean;
26
+ };
27
+ }
28
+ export type PreferenceWriteFailure = {
29
+ ok: false;
30
+ reason: 'not_allowed';
31
+ } | {
32
+ ok: false;
33
+ reason: 'conflict';
34
+ } | {
35
+ ok: false;
36
+ reason: 'invalid';
37
+ issues: PreferenceIssue[];
38
+ };
39
+ export type PreferenceSaveResult = {
40
+ ok: true;
41
+ payload: unknown | null;
42
+ revision: string | null;
43
+ } | PreferenceWriteFailure;
44
+ export type PreferenceResetResult = {
45
+ ok: true;
46
+ } | PreferenceWriteFailure;
47
+ export interface PreferenceLoadInput {
48
+ /** Kind-specific validation inputs (passed to `validate` as `options`). */
49
+ options?: unknown;
50
+ }
51
+ export interface PreferenceSaveInput extends PreferenceLoadInput {
52
+ scope: PreferenceScope;
53
+ /** The client's payload (untrusted). `null` resets the tier. */
54
+ payload: unknown;
55
+ /**
56
+ * The `revision` the client loaded for this tier (`null` when it loaded no
57
+ * row). A stale revision fails with `conflict`, never overwrites.
58
+ */
59
+ revision: string | null;
60
+ }
61
+ export interface PreferenceResetInput {
62
+ scope: PreferenceScope;
63
+ /**
64
+ * The revision the client loaded: a string guards the delete, `null`
65
+ * ("I loaded no row") conflicts when a row exists, and omitting it resets
66
+ * regardless.
67
+ */
68
+ revision?: string | null;
69
+ }
70
+ export interface PreferenceStoreOptions {
71
+ db: DatabaseInterface;
72
+ }
73
+ export interface PreferenceStore {
74
+ /** Read and validate both tiers of `(kind, surfaceId)` for the principal. */
75
+ load(kind: string, surfaceId: string, input?: PreferenceLoadInput): Promise<PreferenceState>;
76
+ /** Validate and store one tier's payload (canonical form only). */
77
+ save(kind: string, surfaceId: string, input: PreferenceSaveInput): Promise<PreferenceSaveResult>;
78
+ /** Delete one tier's row, returning that tier to the tier below. */
79
+ reset(kind: string, surfaceId: string, input: PreferenceResetInput): Promise<PreferenceResetResult>;
80
+ }
81
+ /**
82
+ * The generic preference store. One per database; every call acts as the
83
+ * ambient principal and names a registered kind.
84
+ */
85
+ export declare function createPreferenceStore(options: PreferenceStoreOptions): PreferenceStore;
86
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAWA,OAAO,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5D,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,eAAe,EAErB,MAAM,YAAY,CAAC;AAUpB,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iEAAiE;IACjE,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,6DAA6D;IAC7D,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,yCAAyC;IACzC,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,8EAA8E;IAC9E,YAAY,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;CAClD;AAED,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GACpC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAAC;AAEhE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC9D,sBAAsB,CAAC;AAE3B,MAAM,MAAM,qBAAqB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,sBAAsB,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D,KAAK,EAAE,eAAe,CAAC;IACvB,gEAAgE;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,eAAe,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,IAAI,CACF,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,mBAAmB,GAC1B,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5B,mEAAmE;IACnE,IAAI,CACF,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACjC,oEAAoE;IACpE,KAAK,CACH,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACnC;AAmHD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CAwNjB"}