@object-ui/data-objectstack 17.3.0 → 17.5.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.
package/dist/index.d.cts CHANGED
@@ -1,8 +1,12 @@
1
1
  import { ObjectStackClient } from '@objectstack/client';
2
2
  import { DroppedFieldsEvent } from '@objectstack/spec/data';
3
3
  export { DroppedFieldsEvent } from '@objectstack/spec/data';
4
- import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, FileUploadResult } from '@object-ui/types';
5
- export { FileUploadResult } from '@object-ui/types';
4
+ import { RuntimeAuthoringIssue } from '@objectstack/spec/api';
5
+ export { RuntimeAuthoringIssue } from '@objectstack/spec/api';
6
+ import { DatasetSelection, AnalyticsResult } from '@objectstack/spec/contracts';
7
+ import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
8
+ export { DeleteViewResult, FileUploadResult, ViewHomeDeleteOutcome } from '@object-ui/types';
9
+ import { DatasetDrillRange } from '@object-ui/core';
6
10
 
7
11
  /**
8
12
  * ObjectUI
@@ -12,975 +16,971 @@ export { FileUploadResult } from '@object-ui/types';
12
16
  * LICENSE file in the root directory of this source tree.
13
17
  */
14
18
  /**
15
- * Base error class for all ObjectStack adapter errors
16
- */
17
- declare class ObjectStackError extends Error {
18
- code: string;
19
- statusCode?: number | undefined;
20
- details?: Record<string, unknown> | undefined;
21
- /**
22
- * Create a new ObjectStackError
23
- *
24
- * @param message - Human-readable error message
25
- * @param code - Unique error code for programmatic handling
26
- * @param statusCode - Optional HTTP status code
27
- * @param details - Optional additional error details for debugging
28
- */
29
- constructor(message: string, code: string, statusCode?: number | undefined, details?: Record<string, unknown> | undefined);
30
- /**
31
- * Convert error to JSON for logging/debugging
32
- */
33
- toJSON(): {
34
- name: string;
35
- message: string;
36
- code: string;
37
- statusCode: number | undefined;
38
- details: Record<string, unknown> | undefined;
39
- stack: string | undefined;
40
- };
41
- }
42
- /**
43
- * Error thrown when requested metadata/schema is not found
19
+ * MetadataClient
20
+ *
21
+ * Thin, framework-agnostic HTTP client for the ObjectStack metadata
22
+ * API (`/api/v1/meta/*`). Used by the platform Setup app surfaces in
23
+ * `@object-ui/plugin-designer` (Object Manager, Field Designer, etc.)
24
+ * so they can read and write protocol metadata without depending on
25
+ * `ObjectStackAdapter` (which is a generic data-source binding and
26
+ * carries a lot of view/dashboard-specific behaviour we don't need
27
+ * here).
28
+ *
29
+ * Endpoints (see `packages/rest/src/rest-server.ts`):
30
+ * GET /api/v1/meta — list all metadata types
31
+ * GET /api/v1/meta/:type — list items of a type
32
+ * GET /api/v1/meta/:type/:name — get one item (returns the
33
+ * unwrapped item content)
34
+ * PUT /api/v1/meta/:type/:name — save (overlay) one item
35
+ * honours `If-Match` for OCC
36
+ * DELETE /api/v1/meta/:type/:name — reset overlay to artifact
37
+ * GET /api/v1/meta/:type/:name/history — durable change log
38
+ *
39
+ * The client deliberately keeps the response shape opaque (typed as
40
+ * `unknown`/generic `T`) so callers explicitly narrow per-metadata-
41
+ * type, mirroring the framework's "single Zod source per type" rule.
44
42
  */
45
- declare class MetadataNotFoundError extends ObjectStackError {
46
- constructor(objectName: string, details?: Record<string, unknown>);
47
- }
43
+
48
44
  /**
49
- * Error thrown when a bulk operation fails
45
+ * Emitted after a {@link MetadataClient.save} whose response carried a
46
+ * non-empty `advisories` array. The save SUCCEEDED — the row persisted and the
47
+ * server returned 200 — so this is advisory, never a failure.
48
+ *
49
+ * Deliberately the same shape of seam as `ObjectStackAdapter.onWriteWarning`
50
+ * (#3431/#3455): a successful write whose response carries something the author
51
+ * needs to be told, surfaced to the shell as an event so the data layer never
52
+ * imports a toaster. The difference is only which door produced it — that one
53
+ * is record CRUD, this one is the metadata save door.
50
54
  */
51
- declare class BulkOperationError extends ObjectStackError {
52
- successCount: number;
53
- failureCount: number;
54
- errors: Array<{
55
- index: number;
56
- error: unknown;
57
- }>;
58
- /**
59
- * Create a new BulkOperationError
60
- *
61
- * @param operation - The bulk operation that failed (create, update, delete)
62
- * @param successCount - Number of successful operations
63
- * @param failureCount - Number of failed operations
64
- * @param errors - Array of individual errors
65
- * @param details - Additional error details
66
- */
67
- constructor(operation: 'create' | 'update' | 'delete', successCount: number, failureCount: number, errors: Array<{
68
- index: number;
69
- error: unknown;
70
- }>, details?: Record<string, unknown>);
55
+ interface MetadataSaveAdvisoryEvent {
56
+ /** Metadata type saved (e.g. `'flow'`). */
57
+ type: string;
58
+ /** Item name saved. */
59
+ name: string;
71
60
  /**
72
- * Get a summary of the bulk operation failure
61
+ * The save mode the call used. `'draft'` writes are **never gated** — the
62
+ * framework returns at `runtime-authoring-gate.ts`'s D1 early-return
63
+ * (`if (args.state !== 'active') return null`), so a draft save produces no
64
+ * findings at all and this event never fires for one. Carried anyway so a
65
+ * consumer reading the event can tell which door it came through.
73
66
  */
74
- getSummary(): {
75
- operation: string;
76
- total: number;
77
- successful: number;
78
- failed: number;
79
- failureRate: number;
80
- errors: {
81
- index: number;
82
- error: unknown;
83
- }[];
84
- };
85
- }
86
- /**
87
- * Error thrown when connection to ObjectStack server fails
88
- */
89
- declare class ConnectionError extends ObjectStackError {
90
- url?: string | undefined;
91
- constructor(message: string, url?: string | undefined, details?: Record<string, unknown>, statusCode?: number);
92
- }
93
- /**
94
- * Error thrown when authentication fails
95
- */
96
- declare class AuthenticationError extends ObjectStackError {
97
- constructor(message?: string, details?: Record<string, unknown>, statusCode?: number);
67
+ mode: 'draft' | 'publish';
68
+ /** The findings. Never empty — the event is not emitted otherwise. */
69
+ advisories: RuntimeAuthoringIssue[];
98
70
  }
71
+ /** Event listener type for save-advisory events. */
72
+ type MetadataSaveAdvisoryListener = (event: MetadataSaveAdvisoryEvent) => void;
99
73
  /**
100
- * Error thrown when the ObjectStack data API rejects a write as invalid.
101
- *
102
- * NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
103
- * class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
104
- * a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
105
- * manifest's validation report — and `@object-ui/types` re-exports it under that
106
- * name. This is a runtime `Error` subclass carrying an HTTP status plus a list
107
- * of such entries, so the two are not even the same KIND of thing.
74
+ * Read the `advisories` array off a save response, defensively.
108
75
  *
109
- * The name follows the convention registered on objectstack#4115 for this
110
- * family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
111
- * took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
112
- * the data API.
76
+ * The server omits the key entirely on a clean save, so `undefined` is the
77
+ * common case and means "nothing to say". Anything that is not an array of
78
+ * objects carrying the six required keys is dropped rather than rendered: a
79
+ * half-shaped finding would print blanks at the author, and this channel must
80
+ * never turn a successful save into noise.
113
81
  */
114
- declare class DataApiValidationError extends ObjectStackError {
115
- field?: string | undefined;
116
- validationErrors?: Array<{
117
- field: string;
118
- message: string;
119
- }> | undefined;
82
+ declare function readSaveAdvisories(body: unknown): RuntimeAuthoringIssue[];
83
+ interface MetadataClientConfig {
84
+ /** Base URL of the ObjectStack server (no trailing slash needed). */
85
+ baseUrl: string;
120
86
  /**
121
- * Create a new DataApiValidationError
122
- *
123
- * @param message - Human-readable error message
124
- * @param field - The field that failed validation (optional)
125
- * @param validationErrors - Array of validation error details
126
- * @param details - Additional error details
87
+ * Optional environment ID to scope reads/writes to a tenant. When
88
+ * provided, requests use the scoped path
89
+ * `/api/v1/environments/:environmentId/meta/...` and overlays are
90
+ * persisted in the environment's own metadata store.
127
91
  */
128
- constructor(message: string, field?: string | undefined, validationErrors?: Array<{
129
- field: string;
130
- message: string;
131
- }> | undefined, details?: Record<string, unknown>);
92
+ environmentId?: string;
93
+ /** Optional custom fetch implementation (defaults to `globalThis.fetch`). */
94
+ fetch?: typeof fetch;
95
+ /** Additional headers (e.g. auth) included on every request. */
96
+ headers?: Record<string, string>;
132
97
  /**
133
- * Get all validation errors as a formatted list
98
+ * ADR-0037 Live Canvas — render the world **as-if-published**. When true,
99
+ * `list()` and `get()` append `?preview=draft`, which the framework
100
+ * dispatcher maps to `getMetaItems/getMetaItem({ previewDrafts: true })`:
101
+ * pending ADR-0033 drafts overlay the active registry (draft wins by name,
102
+ * draft-only items surface). Reads only — writes are unaffected, and
103
+ * nothing the preview shows is live until Publish.
134
104
  */
135
- getValidationErrors(): {
136
- field: string;
137
- message: string;
138
- }[];
105
+ previewDrafts?: boolean;
106
+ /**
107
+ * Called after a {@link MetadataClient.save} whose 2xx response carried a
108
+ * non-empty `advisories` array (objectstack#7435). The save already
109
+ * succeeded; this is how the shell learns there is something to tell the
110
+ * author instead of the findings being discarded client-side.
111
+ *
112
+ * Set on the CONFIG rather than exposed as a `subscribe()` method on purpose:
113
+ * console metadata clients are minted per-component by `useMetadataClient`,
114
+ * so there is no long-lived instance to subscribe to — but every one of them
115
+ * is built by the single `createConsoleMetadataClient` factory, which is
116
+ * where this gets wired exactly once for every save call site in the app.
117
+ *
118
+ * A throw from this callback is swallowed — an advisory channel must never
119
+ * be able to fail a save that the server already committed.
120
+ */
121
+ onSaveAdvisory?: MetadataSaveAdvisoryListener;
139
122
  }
140
- /**
141
- * Helper function to create an error from an HTTP response
142
- *
143
- * @param response - Response object or error from fetch/axios
144
- * @param context - Additional context for debugging
145
- * @returns Appropriate error instance
146
- */
147
- declare function createErrorFromResponse(response: Record<string, unknown>, context?: string): ObjectStackError;
148
- /**
149
- * Type guard to check if an error is an ObjectStackError
150
- */
151
- declare function isObjectStackError(error: unknown): error is ObjectStackError;
152
- /**
153
- * Type guard to check if an error is a specific ObjectStack error type
154
- */
155
- declare function isErrorType<T extends ObjectStackError>(error: unknown, errorClass: new (...args: any[]) => T): error is T;
156
-
157
- /**
158
- * ObjectUI
159
- * Copyright (c) 2024-present ObjectStack Inc.
160
- *
161
- * This source code is licensed under the MIT license found in the
162
- * LICENSE file in the root directory of this source tree.
163
- */
164
- /**
165
- * Statistics reported by {@link MetadataCache.getStats} for monitoring.
166
- *
167
- * NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
168
- * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
169
- * describes the platform's `ICacheService` — a server-side KV cache measured by
170
- * `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
171
- * of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
172
- * concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
173
- * Neither type has a key the other has, so this is a name collision, not a
174
- * dialect — deriving would have replaced every field.
175
- */
176
- interface MetadataCacheStats {
177
- size: number;
178
- maxSize: number;
179
- hits: number;
180
- misses: number;
181
- evictions: number;
182
- /** Number of concurrent fetches that were coalesced onto an in-flight request. */
183
- coalesced: number;
184
- hitRate: number;
123
+ interface MetadataListOptions {
124
+ /** Filter by source package id (matches the `package` query param). */
125
+ packageId?: string;
185
126
  }
186
-
187
- /**
188
- * ObjectUI
189
- * Copyright (c) 2024-present ObjectStack Inc.
190
- *
191
- * This source code is licensed under the MIT license found in the
192
- * LICENSE file in the root directory of this source tree.
193
- */
194
127
  /**
195
- * Cloud namespace integration for @objectstack/spec v3.0.0
196
- * Replaces the legacy Hub namespace for cloud deployment, hosting, and marketplace schemas.
128
+ * A pending DRAFT metadata item (ADR-0033), as returned by {@link MetadataClient.listDrafts}.
129
+ * Light header — no body — carrying the owning package so the console can group
130
+ * pending changes by app package.
197
131
  */
198
- interface CloudDeploymentConfig {
199
- /** Target environment */
200
- environment: 'development' | 'staging' | 'production';
201
- /** Cloud region */
202
- region?: string;
203
- /** Auto-scaling configuration */
204
- scaling?: {
205
- minInstances: number;
206
- maxInstances: number;
207
- targetCPU?: number;
208
- };
209
- /** Environment variables */
210
- envVars?: Record<string, string>;
211
- }
212
- interface CloudHostingConfig {
213
- /** Custom domain */
214
- customDomain?: string;
215
- /** SSL configuration */
216
- ssl?: {
217
- enabled: boolean;
218
- autoRenew: boolean;
219
- };
220
- /** CDN configuration */
221
- cdn?: {
222
- enabled: boolean;
223
- regions?: string[];
224
- };
225
- }
226
- interface CloudMarketplaceEntry {
227
- /** Plugin or app ID */
228
- id: string;
229
- /** Display name */
132
+ interface MetadataDraftHeader {
133
+ type: string;
230
134
  name: string;
231
- /** Description */
232
- description: string;
233
- /** Version */
234
- version: string;
235
- /** Author */
236
- author: string;
237
- /** Category */
238
- category: string;
239
- /** Tags */
240
- tags: string[];
241
- /** Rating (1-5) */
242
- rating?: number;
243
- /** Install count */
244
- installCount?: number;
245
- /** Price (0 = free) */
246
- price?: number;
135
+ packageId: string | null;
136
+ updatedAt: string | null;
137
+ updatedBy: string | null;
247
138
  }
248
139
  /**
249
- * Cloud operations helper for ObjectStack adapter.
250
- * Provides methods for cloud deployment, hosting, and marketplace operations.
140
+ * Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
141
+ * `/api/v1/meta/:type/:name`.
142
+ *
143
+ * NOT the spec's `MetadataSaveOptions` (`@objectstack/spec/system` and
144
+ * `/kernel`), whose name this interface wore until objectui#3160
145
+ * (objectstack#4115 ledger batch 6). Both spec copies describe writing a
146
+ * metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
147
+ * `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
148
+ * exists here, and not one of these exists there: this is the REST client's
149
+ * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), actor
150
+ * attribution, the destructive-change override, the ADR-0033 draft/publish
151
+ * mode, and the owning package. Same words, different layer.
251
152
  */
252
- declare class CloudOperations {
253
- private getClient;
254
- constructor(getClient: () => any);
153
+ interface MetadataClientSaveOptions {
255
154
  /**
256
- * Deploy an application to the cloud.
155
+ * Optimistic concurrency token (the `checksum` returned by the last
156
+ * read). When present, sent as the `If-Match` header so concurrent
157
+ * edits get a 409 instead of overwriting each other.
257
158
  */
258
- deploy(appId: string, config: CloudDeploymentConfig): Promise<{
259
- deploymentId: string;
260
- status: string;
261
- }>;
159
+ ifMatch?: string;
160
+ /** Optional actor id, sent as `X-Actor` for history attribution. */
161
+ actor?: string;
262
162
  /**
263
- * Get deployment status.
264
- */
265
- getDeploymentStatus(deploymentId: string): Promise<{
266
- status: string;
267
- url?: string;
268
- }>;
163
+ * Bypass destructive-change protection (Phase 3a). The server returns
164
+ * `409 destructive_change` with `issues[]` if a write would drop or
165
+ * narrow data; setting `force: true` adds `?force=true` to the request
166
+ * so the operator can confirm and proceed.
167
+ */
168
+ force?: boolean;
269
169
  /**
270
- * Search marketplace entries.
170
+ * Save mode — `'draft'` writes a pending draft row (invisible to the
171
+ * runtime until `publish()` is called); `'publish'` writes directly
172
+ * to the active overlay. Omit (or set `'publish'`) for the legacy
173
+ * "save = live" behaviour.
271
174
  */
272
- searchMarketplace(query?: string, category?: string): Promise<CloudMarketplaceEntry[]>;
175
+ mode?: 'draft' | 'publish';
273
176
  /**
274
- * Install a marketplace plugin.
177
+ * Software-package id to bind the saved row to (sent as the `package`
178
+ * query param → `sys_metadata.package_id`). Set when authoring inside a
179
+ * Studio package workspace. Omit for an env-local overlay.
275
180
  */
276
- installPlugin(pluginId: string): Promise<{
277
- success: boolean;
278
- }>;
181
+ packageId?: string;
182
+ }
183
+ interface MetadataGetOptions {
184
+ /**
185
+ * Read a specific overlay state. `'draft'` returns the pending draft
186
+ * body (no fallback to the published overlay or the registry); omit
187
+ * to read the active (published) value.
188
+ */
189
+ state?: 'active' | 'draft';
190
+ /**
191
+ * Software-package id to scope resolution (sent as the `package` query
192
+ * param → server prefer-local, ADR-0048). Set when reading one item that
193
+ * may collide by name across installed packages (e.g. the Studio editor
194
+ * passes the edited item's owning package). Omit for context-free reads.
195
+ */
196
+ packageId?: string;
197
+ }
198
+ interface MetadataDeleteOptions extends MetadataClientSaveOptions {
199
+ /**
200
+ * Target state. `'draft'` discards the pending draft (keeps the
201
+ * published overlay intact). Omit to reset the active overlay back
202
+ * to the artifact default (the legacy behaviour).
203
+ */
204
+ state?: 'active' | 'draft';
205
+ }
206
+ /** Layered view of a metadata item — Phase 3a `?layers=true`. */
207
+ interface MetadataLayered<T = unknown> {
208
+ /** Code-level (artifact) item; null if the item only exists as an overlay. */
209
+ code: T | null;
210
+ /** Org/environment overlay (just the saved delta or full overlay row). */
211
+ overlay: T | null;
212
+ /** Overlay scope (`organization` | `environment` | `package` | null). */
213
+ overlayScope: string | null;
214
+ /** Merged effective view — what the runtime actually sees. */
215
+ effective: T | null;
216
+ /**
217
+ * Load-time validation result for `effective` (server-computed via
218
+ * the same Zod registry used at save time). Undefined for types
219
+ * without a registered Zod schema. Surfaced by the Studio as a
220
+ * banner + inline field errors so operators can spot bad metadata
221
+ * without having to hit Save.
222
+ */
223
+ _diagnostics?: MetadataDiagnostics;
224
+ /** 4-state lock: `none` / `no-overlay` / `no-delete` / `full`. */
225
+ lock?: 'none' | 'no-overlay' | 'no-delete' | 'full';
226
+ /** Human-readable reason for the lock (tooltip text). */
227
+ lockReason?: string;
228
+ /** Which layer set the lock: artifact / package / overlay / env-forced. */
229
+ lockSource?: 'artifact' | 'package' | 'overlay' | 'env-forced';
230
+ /** Optional docs URL for the lock reason (rendered as "View docs →"). */
231
+ lockDocsUrl?: string;
232
+ /** Origin of the item: `package` (loader) | `org` (tenant) | `env-forced`. */
233
+ provenance?: 'package' | 'org' | 'env-forced';
234
+ /** Owning package id (denormalised from the loader tag). */
235
+ packageId?: string;
236
+ /** Owning package version. */
237
+ packageVersion?: string;
238
+ /** True when the editor should allow Save (PUT). */
239
+ editable?: boolean;
240
+ /** True when the editor should allow Delete. */
241
+ deletable?: boolean;
242
+ /** True when "Reset to package default" applies (has overlay + artifact). */
243
+ resettable?: boolean;
279
244
  }
280
-
281
245
  /**
282
- * ObjectUI
283
- * Copyright (c) 2024-present ObjectStack Inc.
284
- *
285
- * This source code is licensed under the MIT license found in the
286
- * LICENSE file in the root directory of this source tree.
246
+ * One row in the metadata protection-audit trail
247
+ * (ADR-0010 §3.6 / Phase 4.1). Mirrors `sys_metadata_audit` columns
248
+ * the API exposes.
287
249
  */
250
+ interface MetadataAuditEntry {
251
+ /** Stable audit-row id (uuid). */
252
+ id: unknown;
253
+ /** ISO timestamp when the attempt happened. */
254
+ occurredAt: string;
255
+ /** Who attempted the operation (`x-actor` header or `'system'`). */
256
+ actor: string;
257
+ /** Code path that recorded the row (e.g. `protocol.saveMetaItem`). */
258
+ source: string | null;
259
+ /** Which lifecycle op was attempted. */
260
+ operation: 'save' | 'publish' | 'rollback' | 'delete' | 'reset';
261
+ /** Decision: allowed / denied / forced (admin override). */
262
+ outcome: 'allowed' | 'denied' | 'forced';
263
+ /** Machine-readable reason code (`item_locked`, `ok`, …). */
264
+ code: string;
265
+ /** Effective lock at the moment of the attempt. */
266
+ lockState: 'none' | 'no-overlay' | 'no-delete' | 'full' | null;
267
+ /** True when admin forced the write through despite the lock. */
268
+ lockOverridden: boolean;
269
+ /** Request-id for trace correlation (if propagated). */
270
+ requestId: string | null;
271
+ /** Free-text note (often the lock reason). */
272
+ note: string | null;
273
+ }
274
+ /** Response shape for `MetadataClient.audit()`. */
275
+ interface MetadataAuditResponse {
276
+ events: MetadataAuditEntry[];
277
+ }
288
278
  /**
289
- * Contracts module integration for @objectstack/spec v3.0.0
290
- * Provides plugin contract validation and marketplace publishing utilities.
279
+ * Load-time validation envelope attached to metadata items by the
280
+ * framework. Mirrors `MetadataValidationResult` in the kernel spec.
291
281
  */
292
- interface PluginContract {
293
- /** Plugin name */
294
- name: string;
295
- /** Plugin version */
296
- version: string;
297
- /** Required peer dependencies */
298
- peerDependencies?: Record<string, string>;
299
- /** Exported component types */
300
- exports: PluginExport[];
301
- /** Required permissions */
302
- permissions?: string[];
303
- /** API surface contract */
304
- api?: PluginAPIContract;
282
+ interface MetadataDiagnostics {
283
+ valid: boolean;
284
+ errors?: Array<{
285
+ path: string;
286
+ message: string;
287
+ code?: string;
288
+ }>;
289
+ warnings?: Array<{
290
+ path: string;
291
+ message: string;
292
+ }>;
305
293
  }
306
- interface PluginExport {
307
- /** Export name */
294
+ /** Options for the cross-type `/meta/diagnostics` sweep call. */
295
+ interface MetadataDiagnosticsOptions {
296
+ /** Restrict the sweep to a single metadata type (e.g. `'view'`). */
297
+ type?: string;
298
+ /**
299
+ * `'error'` (default) returns only items that fail validation.
300
+ * `'warning'` also includes items whose only diagnostics are warnings.
301
+ */
302
+ severity?: 'error' | 'warning';
303
+ /** Restrict to items owned by this package id. */
304
+ packageId?: string;
305
+ }
306
+ /** One row in the `/meta/diagnostics` response. */
307
+ interface MetadataDiagnosticsEntry {
308
+ type: string;
308
309
  name: string;
309
- /** Export type */
310
- type: 'component' | 'hook' | 'utility' | 'provider';
311
- /** Description */
312
- description?: string;
310
+ diagnostics: MetadataDiagnostics;
313
311
  }
314
- interface PluginAPIContract {
315
- /** Consumed data sources */
316
- dataSources?: string[];
317
- /** Required object schemas */
318
- requiredSchemas?: string[];
319
- /** Event subscriptions */
320
- events?: string[];
312
+ /** Top-level envelope returned by `/meta/diagnostics`. */
313
+ interface MetadataDiagnosticsSummary {
314
+ entries: MetadataDiagnosticsEntry[];
315
+ /** Number of `entries` returned (post-filter). */
316
+ total: number;
317
+ /** How many metadata types the sweep visited. */
318
+ scannedTypes: number;
319
+ /** How many individual items were validated. */
320
+ scannedItems: number;
321
+ /**
322
+ * Per-type aggregate stats — count of items and the list of
323
+ * packages contributing to each type. Computed in the same sweep
324
+ * so a single call serves both the diagnostics governance page and
325
+ * the directory tile counts / package filter.
326
+ *
327
+ * Optional for backward compatibility with older framework
328
+ * versions that do not yet emit it; clients should fall back to
329
+ * an empty record.
330
+ */
331
+ stats?: Record<string, {
332
+ count: number;
333
+ locked?: number;
334
+ packages: string[];
335
+ }>;
321
336
  }
322
- interface ContractValidationResult {
323
- valid: boolean;
324
- errors: ContractValidationError[];
325
- warnings: string[];
337
+ /** Reference back-pointer — Phase 3a `/references`. */
338
+ interface MetadataReference {
339
+ /** Referencing item's metadata type. */
340
+ fromType: string;
341
+ /** Referencing item's name. */
342
+ fromName: string;
343
+ /** JSON path within the referencing item that holds the reference. */
344
+ path: string;
345
+ /** The actual value seen at `path` (the referenced name). */
346
+ value: string;
326
347
  }
327
- interface ContractValidationError {
328
- field: string;
348
+ interface MetadataHistoryOptions {
349
+ /** Only return events after this sequence number. */
350
+ sinceSeq?: number;
351
+ /** Limit the number of events returned. */
352
+ limit?: number;
353
+ }
354
+ /** A single field-anchored spec-validation issue (server `error.details.issues`). */
355
+ interface MetadataValidationIssue {
356
+ /** Dot-path to the offending field, e.g. `fields.amount.type` (`''` = root). */
357
+ path: string;
329
358
  message: string;
330
- code: string;
359
+ code?: string;
360
+ }
361
+ interface MetadataError extends Error {
362
+ status: number;
363
+ code?: string;
364
+ body?: unknown;
365
+ /** Structured spec-validation issues, when the failure was a validation error. */
366
+ issues?: MetadataValidationIssue[];
331
367
  }
332
368
  /**
333
- * Validate a plugin contract against the ObjectStack spec.
334
- */
335
- declare function validatePluginContract(contract: PluginContract): ContractValidationResult;
336
- /**
337
- * Generate a plugin contract manifest for marketplace publishing.
338
- */
339
- declare function generateContractManifest(contract: PluginContract): Record<string, unknown>;
340
-
341
- /**
342
- * ObjectStack-backed `UserDataAdapter` factory.
343
- *
344
- * Persists arbitrary per-user UI state (favorites, recently-accessed items, …)
345
- * as a single JSON blob per `(user_id, key)` row in the **unified per-user
346
- * KV store** — the `sys_user_preference` object shipped by every
347
- * `@objectstack/plugin-auth`-enabled environment.
348
- *
349
- * Using the existing `sys_user_preference` table (rather than a parallel
350
- * `user_app_state` table) keeps things consistent with the platform's
351
- * "one KV store per scope" pattern:
352
- *
353
- * - `sys_setting` ← tenant / env scope
354
- * - `sys_user_preference` ← per-user scope ← we live here
369
+ * MetadataClient — read/write protocol metadata via the framework REST API.
355
370
  *
356
- * Callers are encouraged to namespace their keys (e.g. `ui.favorites`,
357
- * `ui.recent`, `ui.grid.account.state`) so explicit settings (`theme`,
358
- * `locale`) and machine-written UI traces stay easy to tell apart.
359
- *
360
- * ## Schema this adapter expects
361
- *
362
- * The canonical `sys_user_preference` schema (from
363
- * `@objectstack/platform-objects`):
364
- *
365
- * ```yaml
366
- * object: sys_user_preference
367
- * fields:
368
- * user_id: lookup(sys_user) # indexed
369
- * key: string # indexed (e.g. "ui.favorites")
370
- * value: json # the serialised list
371
- * updated_at: datetime # auto-managed
372
- * unique: [user_id, key]
371
+ * @example
372
+ * ```ts
373
+ * const client = new MetadataClient({ baseUrl: 'http://localhost:3000' });
374
+ * const objects = await client.list<{ name: string; label?: string }>('object');
375
+ * const account = await client.get<{ fields: Record<string, unknown> }>('object', 'account');
376
+ * await client.save('object', 'account', { ...account, label: 'Customer' });
373
377
  * ```
374
- *
375
- * ## Failure modes
376
- *
377
- * The adapter is designed to **degrade silently**. Any error — missing
378
- * schema, 4xx/5xx, network — is caught:
379
- *
380
- * - `load()` returns `[]`, so the hosting provider keeps its localStorage state.
381
- * - `save()` resolves without throwing, so UI mutations never surface a toast.
382
- *
383
- * As soon as the backend supports `sys_user_preference`, persistence
384
- * "lights up" with no code change.
385
- *
386
- * @module
387
378
  */
388
-
389
- interface ObjectStackUserStateAdapterOptions {
390
- /** Connected data source (usually the one provided by `<AdapterProvider>`). */
391
- dataSource: DataSource<any>;
392
- /** Authenticated user id. */
393
- userId: string;
379
+ declare class MetadataClient {
380
+ private readonly base;
381
+ private readonly fetchImpl;
382
+ private readonly headers;
383
+ /** ADR-0037: when true, reads render the draft-overlaid world. */
384
+ readonly previewDrafts: boolean;
385
+ /** #4133 — sink for post-save advisory findings; see the config field. */
386
+ private readonly onSaveAdvisory;
387
+ constructor(config: MetadataClientConfig);
388
+ /** Update the client's environment scope at runtime. */
389
+ withEnvironment(environmentId: string | undefined): MetadataClient;
394
390
  /**
395
- * Storage key. Should be a dotted, namespaced string so UI traces
396
- * don't collide with user-facing preferences. Examples:
397
- * `ui.favorites`, `ui.recent`, `ui.grid.account.state`.
391
+ * Derive a client whose reads render the draft-overlaid world (or not).
392
+ * Same base/fetch/headers; used by the app-shell to switch the whole
393
+ * renderer tree into ADR-0037 preview mode off one URL flag.
398
394
  */
399
- key: string;
400
- /** Override the storage object name. Defaults to `"sys_user_preference"`. */
401
- resource?: string;
395
+ withPreviewDrafts(previewDrafts: boolean): MetadataClient;
396
+ /** List all registered metadata types (returns the registry rows). */
397
+ listTypes(): Promise<unknown[]>;
398
+ /** List items of a metadata type (e.g. `object`, `field`, `view`). */
399
+ list<T = unknown>(type: string, options?: MetadataListOptions): Promise<T[]>;
402
400
  /**
403
- * Optional console logger for development diagnostics. Defaults to noop so
404
- * production builds stay quiet.
401
+ * List pending DRAFT items (ADR-0033) — what an AI authored but nobody
402
+ * published yet. `list()` only sees published/active metadata, so a
403
+ * just-built app package looks empty there; this surfaces the drafts so the
404
+ * console can show a "pending changes" view and draft-aware package contents.
405
+ * Optionally narrow by `packageId` and/or `type`. Returns light headers
406
+ * (no body) carrying `packageId` for grouping.
405
407
  */
406
- onError?: (where: 'load' | 'save', error: unknown) => void;
407
- }
408
- interface UserDataAdapter<T> {
409
- load(): Promise<T[]>;
410
- save(items: T[]): Promise<void>;
411
- }
412
- /**
413
- * Build a `UserDataAdapter<T>` backed by ObjectStack.
414
- *
415
- * Each adapter instance is bound to a single `(user, key)` pair; create
416
- * one per slot you want to persist.
417
- */
418
- declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
419
-
420
- /**
421
- * ObjectUI
422
- * Copyright (c) 2024-present ObjectStack Inc.
423
- *
424
- * This source code is licensed under the MIT license found in the
425
- * LICENSE file in the root directory of this source tree.
426
- */
427
- /**
428
- * Integration module for @objectstack/spec v3.0.0
429
- * Provides third-party service connectors for Slack, email, webhooks.
430
- */
431
- type IntegrationProvider = 'slack' | 'email' | 'webhook' | 'teams' | 'discord';
432
- interface IntegrationConfig {
433
- /** Integration provider type */
434
- provider: IntegrationProvider;
435
- /** Whether this integration is enabled */
436
- enabled: boolean;
437
- /** Provider-specific configuration */
438
- config: Record<string, unknown>;
439
- /** Event triggers */
440
- triggers?: IntegrationTrigger[];
441
- }
442
- interface IntegrationTrigger {
443
- /** Event name (e.g. 'record.created', 'record.updated') */
444
- event: string;
445
- /** Filter condition */
446
- filter?: string;
447
- /** Template for the message/payload */
448
- template?: string;
449
- }
450
- interface SlackIntegrationConfig extends IntegrationConfig {
451
- provider: 'slack';
452
- config: {
453
- webhookUrl: string;
454
- channel?: string;
455
- username?: string;
456
- iconEmoji?: string;
457
- };
458
- }
459
- interface EmailIntegrationConfig extends IntegrationConfig {
460
- provider: 'email';
461
- config: {
462
- smtpHost: string;
463
- smtpPort: number;
464
- secure: boolean;
465
- from: string;
466
- to: string[];
467
- subject?: string;
468
- };
469
- }
470
- interface WebhookIntegrationConfig extends IntegrationConfig {
471
- provider: 'webhook';
472
- config: {
473
- url: string;
474
- method: 'GET' | 'POST' | 'PUT' | 'PATCH';
475
- headers?: Record<string, string>;
476
- retryCount?: number;
477
- retryDelay?: number;
478
- };
479
- }
480
- /**
481
- * Integration service manager.
482
- * Manages third-party service connections and event dispatching.
483
- */
484
- declare class IntegrationManager {
485
- private integrations;
408
+ listDrafts(options?: {
409
+ packageId?: string;
410
+ type?: string;
411
+ }): Promise<MetadataDraftHeader[]>;
486
412
  /**
487
- * Register a new integration.
413
+ * Fetch a single metadata item and return the response body EXACTLY as the
414
+ * server sent it — envelope and all.
415
+ *
416
+ * The one transport both {@link get} (which unwraps) and {@link getDraft}
417
+ * (which does not) sit on, so the two can differ in what they hand back
418
+ * without differing in how they ask. Private on purpose: the envelope is a
419
+ * wire detail, and the two published contracts above are the supported ways
420
+ * to read an item.
488
421
  */
489
- register(id: string, config: IntegrationConfig): void;
422
+ private readItemResponse;
490
423
  /**
491
- * Remove an integration.
424
+ * Get a single metadata item. Returns the unwrapped item CONTENT — the
425
+ * metadata document itself, so `obj.fields` / `obj.label` are reachable
426
+ * directly.
427
+ *
428
+ * The framework answers `GET /meta/:type/:name` with the spec-declared
429
+ * envelope `{ type, name, item, …protection fields }`
430
+ * (`GetMetaItemResponseSchema`; objectstack#5563 collapsed this read to that
431
+ * ONE shape), so the envelope is unwrapped here — at the client boundary,
432
+ * once — rather than by each caller.
433
+ *
434
+ * **This method used to return the envelope while promising the body**
435
+ * (objectui#4271). Nothing detected the disagreement, because the test
436
+ * doubles across the repo were written against this docblock: every consumer
437
+ * reading `obj.fields` got `undefined` in production and a field list in
438
+ * unit tests. The visible cost was the entire field half of the permission
439
+ * matrix reporting "No fields registered for this object." for every object,
440
+ * plus dead RLS CEL autocomplete, an inert report drill-down and a designer
441
+ * that saved the envelope back over the object body.
442
+ *
443
+ * A response that is NOT the envelope (an older server answering the bare
444
+ * document, or a body of its own shape) passes through untouched, and `null`
445
+ * still comes back on 404 to keep the call site ergonomic.
492
446
  */
493
- unregister(id: string): void;
447
+ get<T = unknown>(type: string, name: string, options?: MetadataGetOptions): Promise<T | null>;
494
448
  /**
495
- * Get all registered integrations.
449
+ * Read the pending draft body for an item (`?state=draft`). Returns
450
+ * `null` when there is no draft pending. Draft reads do NOT fall
451
+ * back to the published overlay or the artifact registry — a `null`
452
+ * unambiguously means "nothing to publish".
453
+ *
454
+ * Note: this method hands back the `{ type, name, item }` envelope the
455
+ * framework sends, NOT the body — callers read `.item`. That asymmetry with
456
+ * {@link get} is deliberate and long-standing (the draft envelope's identity
457
+ * and protection carriers are part of what a draft reader inspects), so it
458
+ * is preserved by reading the transport directly instead of going through
459
+ * `get()`'s unwrap. `unwrapDraftBody` (app-shell) and `unwrapViewDraft`
460
+ * (this package) are the shared helpers for taking the body out.
496
461
  */
497
- getAll(): Map<string, IntegrationConfig>;
462
+ getDraft<T = unknown>(type: string, name: string, options?: {
463
+ packageId?: string;
464
+ }): Promise<T | null>;
498
465
  /**
499
- * Get integrations that match a specific event.
466
+ * Save (PUT) a metadata item. The framework accepts both the bare
467
+ * item payload and the `{ item: ... }` / `{ metadata: ... }`
468
+ * envelopes; we send bare for consistency. Pass `mode: 'draft'` to
469
+ * stage the change without publishing (Studio's "Save" button).
500
470
  */
501
- getForEvent(event: string): IntegrationConfig[];
471
+ save<T = unknown>(type: string, name: string, item: unknown, options?: MetadataClientSaveOptions): Promise<T>;
502
472
  /**
503
- * Dispatch an event to all matching integrations.
504
- * Returns results for each integration.
473
+ * Publish a single pending draft BY REFERENCE — promotes the draft row for
474
+ * `(type, name)` into the active overlay and drops the draft. Works for ANY
475
+ * draft, including ones with no `packageId` binding (which the package-scoped
476
+ * `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
477
+ * exact set returned by {@link listDrafts} without needing a package.
478
+ *
479
+ * Returns the server result. For `seed` drafts the protocol also materializes
480
+ * the rows and reports under `seedApplied` — a data problem never fails the
481
+ * publish, so callers should check `seedApplied?.success` and warn the user
482
+ * rather than assume the data went live.
505
483
  */
506
- dispatch(event: string, payload: Record<string, unknown>): Promise<Array<{
507
- id: string;
508
- success: boolean;
509
- error?: string;
510
- }>>;
484
+ publishDraft(type: string, name: string): Promise<{
485
+ success?: boolean;
486
+ seedApplied?: {
487
+ success: boolean;
488
+ inserted?: number;
489
+ updated?: number;
490
+ error?: string;
491
+ errors?: unknown[];
492
+ };
493
+ } & Record<string, unknown>>;
511
494
  /**
512
- * Send payload to a specific integration.
495
+ * Get the 3-state layered view of a metadata item (Phase 3a). Returns
496
+ * `code` (the artifact / fallback default), `overlay` (the saved
497
+ * customisation, if any), and `effective` (what the runtime sees).
513
498
  */
514
- private send;
515
- }
516
-
517
- /**
518
- * ObjectUI
519
- * Copyright (c) 2024-present ObjectStack Inc.
520
- *
521
- * This source code is licensed under the MIT license found in the
522
- * LICENSE file in the root directory of this source tree.
523
- */
524
- /**
525
- * MetadataClient
526
- *
527
- * Thin, framework-agnostic HTTP client for the ObjectStack metadata
528
- * API (`/api/v1/meta/*`). Used by the platform Setup app surfaces in
529
- * `@object-ui/plugin-designer` (Object Manager, Field Designer, etc.)
530
- * so they can read and write protocol metadata without depending on
531
- * `ObjectStackAdapter` (which is a generic data-source binding and
532
- * carries a lot of view/dashboard-specific behaviour we don't need
533
- * here).
534
- *
535
- * Endpoints (see `packages/rest/src/rest-server.ts`):
536
- * GET /api/v1/meta — list all metadata types
537
- * GET /api/v1/meta/:type — list items of a type
538
- * GET /api/v1/meta/:type/:name — get one item (returns the
539
- * unwrapped item content)
540
- * PUT /api/v1/meta/:type/:name — save (overlay) one item
541
- * honours `If-Match` for OCC
542
- * DELETE /api/v1/meta/:type/:name — reset overlay to artifact
543
- * GET /api/v1/meta/:type/:name/history — durable change log
544
- *
545
- * The client deliberately keeps the response shape opaque (typed as
546
- * `unknown`/generic `T`) so callers explicitly narrow per-metadata-
547
- * type, mirroring the framework's "single Zod source per type" rule.
548
- */
549
- interface MetadataClientConfig {
550
- /** Base URL of the ObjectStack server (no trailing slash needed). */
551
- baseUrl: string;
499
+ layered<T = unknown>(type: string, name: string, options?: {
500
+ packageId?: string;
501
+ }): Promise<MetadataLayered<T>>;
552
502
  /**
553
- * Optional environment ID to scope reads/writes to a tenant. When
554
- * provided, requests use the scoped path
555
- * `/api/v1/environments/:environmentId/meta/...` and overlays are
556
- * persisted in the environment's own metadata store.
557
- */
558
- environmentId?: string;
559
- /** Optional custom fetch implementation (defaults to `globalThis.fetch`). */
560
- fetch?: typeof fetch;
561
- /** Additional headers (e.g. auth) included on every request. */
562
- headers?: Record<string, string>;
563
- /**
564
- * ADR-0037 Live Canvas — render the world **as-if-published**. When true,
565
- * `list()` and `get()` append `?preview=draft`, which the framework
566
- * dispatcher maps to `getMetaItems/getMetaItem({ previewDrafts: true })`:
567
- * pending ADR-0033 drafts overlay the active registry (draft wins by name,
568
- * draft-only items surface). Reads only — writes are unaffected, and
569
- * nothing the preview shows is live until Publish.
503
+ * Cross-type sweep of load-time validation results — calls
504
+ * `GET /meta/diagnostics`. Returns every entry the framework
505
+ * considers invalid (or, when `severity: 'warning'`, also entries
506
+ * with only warnings).
507
+ *
508
+ * Used by the Studio's governance overview page and by the
509
+ * directory page to show "N invalid" badges per metadata type.
570
510
  */
571
- previewDrafts?: boolean;
572
- }
573
- interface MetadataListOptions {
574
- /** Filter by source package id (matches the `package` query param). */
575
- packageId?: string;
576
- }
577
- /**
578
- * A pending DRAFT metadata item (ADR-0033), as returned by {@link MetadataClient.listDrafts}.
579
- * Light header — no body — carrying the owning package so the console can group
580
- * pending changes by app package.
581
- */
582
- interface MetadataDraftHeader {
583
- type: string;
584
- name: string;
585
- packageId: string | null;
586
- updatedAt: string | null;
587
- updatedBy: string | null;
588
- }
589
- /**
590
- * Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
591
- * `/api/v1/meta/:type/:name`.
592
- *
593
- * NOT the spec's `MetadataSaveOptions` (`@objectstack/spec/system` and
594
- * `/kernel`), whose name this interface wore until objectui#3160
595
- * (objectstack#4115 ledger batch 6). Both spec copies describe writing a
596
- * metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
597
- * `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
598
- * exists here, and not one of these exists there: this is the REST client's
599
- * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), actor
600
- * attribution, the destructive-change override, the ADR-0033 draft/publish
601
- * mode, and the owning package. Same words, different layer.
602
- */
603
- interface MetadataClientSaveOptions {
511
+ diagnostics(options?: MetadataDiagnosticsOptions): Promise<MetadataDiagnosticsSummary>;
604
512
  /**
605
- * Optimistic concurrency token (the `checksum` returned by the last
606
- * read). When present, sent as the `If-Match` header so concurrent
607
- * edits get a 409 instead of overwriting each other.
513
+ * Find every metadata item that references this one (Phase 3a). Useful
514
+ * for pre-delete impact analysis: "Are any views pointing at this
515
+ * object before I drop it?".
608
516
  */
609
- ifMatch?: string;
610
- /** Optional actor id, sent as `X-Actor` for history attribution. */
611
- actor?: string;
517
+ references(type: string, name: string): Promise<MetadataReference[]>;
612
518
  /**
613
- * Bypass destructive-change protection (Phase 3a). The server returns
614
- * `409 destructive_change` with `issues[]` if a write would drop or
615
- * narrow data; setting `force: true` adds `?force=true` to the request
616
- * so the operator can confirm and proceed.
519
+ * Reset a metadata customization overlay back to the artifact default.
520
+ * Idempotent: returns the result even when no overlay row existed.
521
+ * Pass `state: 'draft'` to discard the pending draft only (keeps the
522
+ * published overlay intact) — useful for a Studio "Discard draft" button.
617
523
  */
618
- force?: boolean;
524
+ reset<T = unknown>(type: string, name: string, options?: MetadataDeleteOptions): Promise<T>;
619
525
  /**
620
- * Save mode — `'draft'` writes a pending draft row (invisible to the
621
- * runtime until `publish()` is called); `'publish'` writes directly
622
- * to the active overlay. Omit (or set `'publish'`) for the legacy
623
- * "save = live" behaviour.
526
+ * Promote the pending draft of an item to the active overlay
527
+ * (POST `/meta/:type/:name/publish`). Returns
528
+ * `{ success, version, seq, message }`. Throws a `404 no_draft` if
529
+ * nothing is pending and `409 metadata_conflict` if the published
530
+ * overlay moved while the draft was sitting.
624
531
  */
625
- mode?: 'draft' | 'publish';
532
+ publish<T = unknown>(type: string, name: string, options?: {
533
+ actor?: string;
534
+ message?: string;
535
+ }): Promise<T>;
626
536
  /**
627
- * Software-package id to bind the saved row to (sent as the `package`
628
- * query param → `sys_metadata.package_id`). Set when authoring inside a
629
- * Studio package workspace. Omit for an env-local overlay.
537
+ * Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
538
+ * The server reads the history row at `toVersion`, writes its body back
539
+ * as the active overlay with `operation_type='revert'`. Returns
540
+ * `{ success, version, seq, restoredFromVersion, message }`. Throws
541
+ * `404 version_not_found` for unknown versions and `409 version_not_restorable`
542
+ * when the target version is a delete tombstone.
630
543
  */
631
- packageId?: string;
632
- }
633
- interface MetadataGetOptions {
544
+ rollback<T = unknown>(type: string, name: string, toVersion: number, options?: {
545
+ actor?: string;
546
+ message?: string;
547
+ }): Promise<T>;
634
548
  /**
635
- * Read a specific overlay state. `'draft'` returns the pending draft
636
- * body (no fallback to the published overlay or the registry); omit
637
- * to read the active (published) value.
549
+ * Compute a structured top-level key diff between two history versions
550
+ * (GET `/meta/:type/:name/diff?from=&to=`). Returns
551
+ * `{ added, removed, changed }` where each entry carries a `path` and
552
+ * the relevant value(s). Useful for "what changed in this draft?" and
553
+ * pre-rollback previews.
638
554
  */
639
- state?: 'active' | 'draft';
555
+ diff<T = unknown>(type: string, name: string, fromVersion?: number, toVersion?: number): Promise<T>;
556
+ /** Fetch the durable history (change log) for one metadata item. */
557
+ history<T = unknown>(type: string, name: string, options?: MetadataHistoryOptions): Promise<T>;
640
558
  /**
641
- * Software-package id to scope resolution (sent as the `package` query
642
- * param → server prefer-local, ADR-0048). Set when reading one item that
643
- * may collide by name across installed packages (e.g. the Studio editor
644
- * passes the edited item's owning package). Omit for context-free reads.
559
+ * Fetch the protection-audit trail for one metadata item
560
+ * (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
561
+ * delete/reset attempt — allowed, denied, or forced — so the
562
+ * Studio "审计日志 / Audit log" tab can render who tried what
563
+ * and whether a lock blocked it.
645
564
  */
646
- packageId?: string;
565
+ audit(type: string, name: string, options?: {
566
+ limit?: number;
567
+ }): Promise<MetadataAuditResponse>;
647
568
  }
648
- interface MetadataDeleteOptions extends MetadataClientSaveOptions {
569
+
570
+ /**
571
+ * ObjectUI
572
+ * Copyright (c) 2024-present ObjectStack Inc.
573
+ *
574
+ * This source code is licensed under the MIT license found in the
575
+ * LICENSE file in the root directory of this source tree.
576
+ */
577
+ /**
578
+ * Base error class for all ObjectStack adapter errors
579
+ */
580
+ declare class ObjectStackError extends Error {
581
+ code: string;
582
+ statusCode?: number | undefined;
583
+ details?: Record<string, unknown> | undefined;
649
584
  /**
650
- * Target state. `'draft'` discards the pending draft (keeps the
651
- * published overlay intact). Omit to reset the active overlay back
652
- * to the artifact default (the legacy behaviour).
585
+ * Create a new ObjectStackError
586
+ *
587
+ * @param message - Human-readable error message
588
+ * @param code - Unique error code for programmatic handling
589
+ * @param statusCode - Optional HTTP status code
590
+ * @param details - Optional additional error details for debugging
653
591
  */
654
- state?: 'active' | 'draft';
655
- }
656
- /** Layered view of a metadata item — Phase 3a `?layers=true`. */
657
- interface MetadataLayered<T = unknown> {
658
- /** Code-level (artifact) item; null if the item only exists as an overlay. */
659
- code: T | null;
660
- /** Org/environment overlay (just the saved delta or full overlay row). */
661
- overlay: T | null;
662
- /** Overlay scope (`organization` | `environment` | `package` | null). */
663
- overlayScope: string | null;
664
- /** Merged effective view — what the runtime actually sees. */
665
- effective: T | null;
592
+ constructor(message: string, code: string, statusCode?: number | undefined, details?: Record<string, unknown> | undefined);
666
593
  /**
667
- * Load-time validation result for `effective` (server-computed via
668
- * the same Zod registry used at save time). Undefined for types
669
- * without a registered Zod schema. Surfaced by the Studio as a
670
- * banner + inline field errors so operators can spot bad metadata
671
- * without having to hit Save.
594
+ * Convert error to JSON for logging/debugging
672
595
  */
673
- _diagnostics?: MetadataDiagnostics;
674
- /** 4-state lock: `none` / `no-overlay` / `no-delete` / `full`. */
675
- lock?: 'none' | 'no-overlay' | 'no-delete' | 'full';
676
- /** Human-readable reason for the lock (tooltip text). */
677
- lockReason?: string;
678
- /** Which layer set the lock: artifact / package / overlay / env-forced. */
679
- lockSource?: 'artifact' | 'package' | 'overlay' | 'env-forced';
680
- /** Optional docs URL for the lock reason (rendered as "View docs →"). */
681
- lockDocsUrl?: string;
682
- /** Origin of the item: `package` (loader) | `org` (tenant) | `env-forced`. */
683
- provenance?: 'package' | 'org' | 'env-forced';
684
- /** Owning package id (denormalised from the loader tag). */
685
- packageId?: string;
686
- /** Owning package version. */
687
- packageVersion?: string;
688
- /** True when the editor should allow Save (PUT). */
689
- editable?: boolean;
690
- /** True when the editor should allow Delete. */
691
- deletable?: boolean;
692
- /** True when "Reset to package default" applies (has overlay + artifact). */
693
- resettable?: boolean;
596
+ toJSON(): {
597
+ name: string;
598
+ message: string;
599
+ code: string;
600
+ statusCode: number | undefined;
601
+ details: Record<string, unknown> | undefined;
602
+ stack: string | undefined;
603
+ };
694
604
  }
695
605
  /**
696
- * One row in the metadata protection-audit trail
697
- * (ADR-0010 §3.6 / Phase 4.1). Mirrors `sys_metadata_audit` columns
698
- * the API exposes.
606
+ * Error thrown when requested metadata/schema is not found
699
607
  */
700
- interface MetadataAuditEntry {
701
- /** Stable audit-row id (uuid). */
702
- id: unknown;
703
- /** ISO timestamp when the attempt happened. */
704
- occurredAt: string;
705
- /** Who attempted the operation (`x-actor` header or `'system'`). */
706
- actor: string;
707
- /** Code path that recorded the row (e.g. `protocol.saveMetaItem`). */
708
- source: string | null;
709
- /** Which lifecycle op was attempted. */
710
- operation: 'save' | 'publish' | 'rollback' | 'delete' | 'reset';
711
- /** Decision: allowed / denied / forced (admin override). */
712
- outcome: 'allowed' | 'denied' | 'forced';
713
- /** Machine-readable reason code (`item_locked`, `ok`, …). */
714
- code: string;
715
- /** Effective lock at the moment of the attempt. */
716
- lockState: 'none' | 'no-overlay' | 'no-delete' | 'full' | null;
717
- /** True when admin forced the write through despite the lock. */
718
- lockOverridden: boolean;
719
- /** Request-id for trace correlation (if propagated). */
720
- requestId: string | null;
721
- /** Free-text note (often the lock reason). */
722
- note: string | null;
723
- }
724
- /** Response shape for `MetadataClient.audit()`. */
725
- interface MetadataAuditResponse {
726
- events: MetadataAuditEntry[];
608
+ declare class MetadataNotFoundError extends ObjectStackError {
609
+ constructor(objectName: string, details?: Record<string, unknown>);
727
610
  }
728
611
  /**
729
- * Load-time validation envelope attached to metadata items by the
730
- * framework. Mirrors `MetadataValidationResult` in the kernel spec.
612
+ * Error thrown when a bulk operation fails
731
613
  */
732
- interface MetadataDiagnostics {
733
- valid: boolean;
734
- errors?: Array<{
735
- path: string;
736
- message: string;
737
- code?: string;
614
+ declare class BulkOperationError extends ObjectStackError {
615
+ successCount: number;
616
+ failureCount: number;
617
+ errors: Array<{
618
+ index: number;
619
+ error: unknown;
738
620
  }>;
739
- warnings?: Array<{
740
- path: string;
741
- message: string;
742
- }>;
743
- }
744
- /** Options for the cross-type `/meta/diagnostics` sweep call. */
745
- interface MetadataDiagnosticsOptions {
746
- /** Restrict the sweep to a single metadata type (e.g. `'view'`). */
747
- type?: string;
748
621
  /**
749
- * `'error'` (default) returns only items that fail validation.
750
- * `'warning'` also includes items whose only diagnostics are warnings.
622
+ * Create a new BulkOperationError
623
+ *
624
+ * @param operation - The bulk operation that failed (create, update, delete)
625
+ * @param successCount - Number of successful operations
626
+ * @param failureCount - Number of failed operations
627
+ * @param errors - Array of individual errors
628
+ * @param details - Additional error details
751
629
  */
752
- severity?: 'error' | 'warning';
753
- /** Restrict to items owned by this package id. */
754
- packageId?: string;
630
+ constructor(operation: 'create' | 'update' | 'delete', successCount: number, failureCount: number, errors: Array<{
631
+ index: number;
632
+ error: unknown;
633
+ }>, details?: Record<string, unknown>);
634
+ /**
635
+ * Get a summary of the bulk operation failure
636
+ */
637
+ getSummary(): {
638
+ operation: string;
639
+ total: number;
640
+ successful: number;
641
+ failed: number;
642
+ failureRate: number;
643
+ errors: {
644
+ index: number;
645
+ error: unknown;
646
+ }[];
647
+ };
755
648
  }
756
- /** One row in the `/meta/diagnostics` response. */
757
- interface MetadataDiagnosticsEntry {
758
- type: string;
759
- name: string;
760
- diagnostics: MetadataDiagnostics;
649
+ /**
650
+ * Error thrown when connection to ObjectStack server fails
651
+ */
652
+ declare class ConnectionError extends ObjectStackError {
653
+ url?: string | undefined;
654
+ constructor(message: string, url?: string | undefined, details?: Record<string, unknown>, statusCode?: number);
761
655
  }
762
- /** Top-level envelope returned by `/meta/diagnostics`. */
763
- interface MetadataDiagnosticsSummary {
764
- entries: MetadataDiagnosticsEntry[];
765
- /** Number of `entries` returned (post-filter). */
766
- total: number;
767
- /** How many metadata types the sweep visited. */
768
- scannedTypes: number;
769
- /** How many individual items were validated. */
770
- scannedItems: number;
656
+ /**
657
+ * Error thrown when authentication fails
658
+ */
659
+ declare class AuthenticationError extends ObjectStackError {
660
+ constructor(message?: string, details?: Record<string, unknown>, statusCode?: number);
661
+ }
662
+ /**
663
+ * Error thrown when the ObjectStack data API rejects a write as invalid.
664
+ *
665
+ * NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
666
+ * class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
667
+ * a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
668
+ * manifest's validation report — and `@object-ui/types` re-exports it under that
669
+ * name. This is a runtime `Error` subclass carrying an HTTP status plus a list
670
+ * of such entries, so the two are not even the same KIND of thing.
671
+ *
672
+ * The name follows the convention registered on objectstack#4115 for this
673
+ * family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
674
+ * took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
675
+ * the data API.
676
+ */
677
+ declare class DataApiValidationError extends ObjectStackError {
678
+ field?: string | undefined;
679
+ validationErrors?: Array<{
680
+ field: string;
681
+ message: string;
682
+ }> | undefined;
771
683
  /**
772
- * Per-type aggregate stats — count of items and the list of
773
- * packages contributing to each type. Computed in the same sweep
774
- * so a single call serves both the diagnostics governance page and
775
- * the directory tile counts / package filter.
684
+ * Create a new DataApiValidationError
776
685
  *
777
- * Optional for backward compatibility with older framework
778
- * versions that do not yet emit it; clients should fall back to
779
- * an empty record.
686
+ * @param message - Human-readable error message
687
+ * @param field - The field that failed validation (optional)
688
+ * @param validationErrors - Array of validation error details
689
+ * @param details - Additional error details
780
690
  */
781
- stats?: Record<string, {
782
- count: number;
783
- locked?: number;
784
- packages: string[];
785
- }>;
691
+ constructor(message: string, field?: string | undefined, validationErrors?: Array<{
692
+ field: string;
693
+ message: string;
694
+ }> | undefined, details?: Record<string, unknown>);
695
+ /**
696
+ * Get all validation errors as a formatted list
697
+ */
698
+ getValidationErrors(): {
699
+ field: string;
700
+ message: string;
701
+ }[];
786
702
  }
787
- /** Reference back-pointer — Phase 3a `/references`. */
788
- interface MetadataReference {
789
- /** Referencing item's metadata type. */
790
- fromType: string;
791
- /** Referencing item's name. */
792
- fromName: string;
793
- /** JSON path within the referencing item that holds the reference. */
794
- path: string;
795
- /** The actual value seen at `path` (the referenced name). */
796
- value: string;
703
+ /**
704
+ * Helper function to create an error from an HTTP response
705
+ *
706
+ * @param response - Response object or error from fetch/axios
707
+ * @param context - Additional context for debugging
708
+ * @returns Appropriate error instance
709
+ */
710
+ declare function createErrorFromResponse(response: Record<string, unknown>, context?: string): ObjectStackError;
711
+ /**
712
+ * Type guard to check if an error is an ObjectStackError
713
+ */
714
+ declare function isObjectStackError(error: unknown): error is ObjectStackError;
715
+ /**
716
+ * Type guard to check if an error is a specific ObjectStack error type
717
+ */
718
+ declare function isErrorType<T extends ObjectStackError>(error: unknown, errorClass: new (...args: any[]) => T): error is T;
719
+
720
+ /**
721
+ * ObjectUI
722
+ * Copyright (c) 2024-present ObjectStack Inc.
723
+ *
724
+ * This source code is licensed under the MIT license found in the
725
+ * LICENSE file in the root directory of this source tree.
726
+ */
727
+ /**
728
+ * Statistics reported by {@link MetadataCache.getStats} for monitoring.
729
+ *
730
+ * NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
731
+ * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
732
+ * describes the platform's `ICacheService` — a server-side KV cache measured by
733
+ * `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
734
+ * of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
735
+ * concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
736
+ * Neither type has a key the other has, so this is a name collision, not a
737
+ * dialect — deriving would have replaced every field.
738
+ */
739
+ interface MetadataCacheStats {
740
+ size: number;
741
+ maxSize: number;
742
+ hits: number;
743
+ misses: number;
744
+ evictions: number;
745
+ /** Number of concurrent fetches that were coalesced onto an in-flight request. */
746
+ coalesced: number;
747
+ hitRate: number;
797
748
  }
798
- interface MetadataHistoryOptions {
799
- /** Only return events after this sequence number. */
800
- sinceSeq?: number;
801
- /** Limit the number of events returned. */
802
- limit?: number;
749
+
750
+ /**
751
+ * ObjectUI
752
+ * Copyright (c) 2024-present ObjectStack Inc.
753
+ *
754
+ * This source code is licensed under the MIT license found in the
755
+ * LICENSE file in the root directory of this source tree.
756
+ */
757
+ /**
758
+ * Contracts module integration for @objectstack/spec v3.0.0
759
+ * Provides plugin contract validation and marketplace publishing utilities.
760
+ */
761
+ interface PluginContract {
762
+ /** Plugin name */
763
+ name: string;
764
+ /** Plugin version */
765
+ version: string;
766
+ /** Required peer dependencies */
767
+ peerDependencies?: Record<string, string>;
768
+ /** Exported component types */
769
+ exports: PluginExport[];
770
+ /** Required permissions */
771
+ permissions?: string[];
772
+ /** API surface contract */
773
+ api?: PluginAPIContract;
803
774
  }
804
- /** A single field-anchored spec-validation issue (server `error.details.issues`). */
805
- interface MetadataValidationIssue {
806
- /** Dot-path to the offending field, e.g. `fields.amount.type` (`''` = root). */
807
- path: string;
808
- message: string;
809
- code?: string;
775
+ interface PluginExport {
776
+ /** Export name */
777
+ name: string;
778
+ /** Export type */
779
+ type: 'component' | 'hook' | 'utility' | 'provider';
780
+ /** Description */
781
+ description?: string;
810
782
  }
811
- interface MetadataError extends Error {
812
- status: number;
813
- code?: string;
814
- body?: unknown;
815
- /** Structured spec-validation issues, when the failure was a validation error. */
816
- issues?: MetadataValidationIssue[];
783
+ interface PluginAPIContract {
784
+ /** Consumed data sources */
785
+ dataSources?: string[];
786
+ /** Required object schemas */
787
+ requiredSchemas?: string[];
788
+ /** Event subscriptions */
789
+ events?: string[];
790
+ }
791
+ interface ContractValidationResult {
792
+ valid: boolean;
793
+ errors: ContractValidationError[];
794
+ warnings: string[];
795
+ }
796
+ interface ContractValidationError {
797
+ field: string;
798
+ message: string;
799
+ code: string;
817
800
  }
818
801
  /**
819
- * MetadataClient — read/write protocol metadata via the framework REST API.
802
+ * Validate a plugin contract against the ObjectStack spec.
803
+ */
804
+ declare function validatePluginContract(contract: PluginContract): ContractValidationResult;
805
+ /**
806
+ * Generate a plugin contract manifest for marketplace publishing.
807
+ */
808
+ declare function generateContractManifest(contract: PluginContract): Record<string, unknown>;
809
+
810
+ /**
811
+ * ObjectStack-backed `UserDataAdapter` factory.
820
812
  *
821
- * @example
822
- * ```ts
823
- * const client = new MetadataClient({ baseUrl: 'http://localhost:3000' });
824
- * const objects = await client.list<{ name: string; label?: string }>('object');
825
- * const account = await client.get<{ fields: Record<string, unknown> }>('object', 'account');
826
- * await client.save('object', 'account', { ...account, label: 'Customer' });
813
+ * Persists arbitrary per-user UI state (favorites, recently-accessed items, …)
814
+ * as a single JSON blob per `(user_id, key)` row in the **unified per-user
815
+ * KV store** — the `sys_user_preference` object shipped by every
816
+ * `@objectstack/plugin-auth`-enabled environment.
817
+ *
818
+ * Using the existing `sys_user_preference` table (rather than a parallel
819
+ * `user_app_state` table) keeps things consistent with the platform's
820
+ * "one KV store per scope" pattern:
821
+ *
822
+ * - `sys_setting` ← tenant / env scope
823
+ * - `sys_user_preference` ← per-user scope ← we live here
824
+ *
825
+ * Callers are encouraged to namespace their keys (e.g. `ui.favorites`,
826
+ * `ui.recent`, `ui.grid.account.state`) so explicit settings (`theme`,
827
+ * `locale`) and machine-written UI traces stay easy to tell apart.
828
+ *
829
+ * ## Schema this adapter expects
830
+ *
831
+ * The canonical `sys_user_preference` schema (from
832
+ * `@objectstack/platform-objects`):
833
+ *
834
+ * ```yaml
835
+ * object: sys_user_preference
836
+ * fields:
837
+ * user_id: lookup(sys_user) # indexed
838
+ * key: string # indexed (e.g. "ui.favorites")
839
+ * value: json # the serialised list
840
+ * updated_at: datetime # auto-managed
841
+ * unique: [user_id, key]
827
842
  * ```
843
+ *
844
+ * ## Failure modes
845
+ *
846
+ * The adapter is designed to **degrade silently**. Any error — missing
847
+ * schema, 4xx/5xx, network — is caught:
848
+ *
849
+ * - `load()` returns `[]`, so the hosting provider keeps its localStorage state.
850
+ * - `save()` resolves without throwing, so UI mutations never surface a toast.
851
+ *
852
+ * As soon as the backend supports `sys_user_preference`, persistence
853
+ * "lights up" with no code change.
854
+ *
855
+ * @module
828
856
  */
829
- declare class MetadataClient {
830
- private readonly base;
831
- private readonly fetchImpl;
832
- private readonly headers;
833
- /** ADR-0037: when true, reads render the draft-overlaid world. */
834
- readonly previewDrafts: boolean;
835
- constructor(config: MetadataClientConfig);
836
- /** Update the client's environment scope at runtime. */
837
- withEnvironment(environmentId: string | undefined): MetadataClient;
838
- /**
839
- * Derive a client whose reads render the draft-overlaid world (or not).
840
- * Same base/fetch/headers; used by the app-shell to switch the whole
841
- * renderer tree into ADR-0037 preview mode off one URL flag.
842
- */
843
- withPreviewDrafts(previewDrafts: boolean): MetadataClient;
844
- /** List all registered metadata types (returns the registry rows). */
845
- listTypes(): Promise<unknown[]>;
846
- /** List items of a metadata type (e.g. `object`, `field`, `view`). */
847
- list<T = unknown>(type: string, options?: MetadataListOptions): Promise<T[]>;
848
- /**
849
- * List pending DRAFT items (ADR-0033) — what an AI authored but nobody
850
- * published yet. `list()` only sees published/active metadata, so a
851
- * just-built app package looks empty there; this surfaces the drafts so the
852
- * console can show a "pending changes" view and draft-aware package contents.
853
- * Optionally narrow by `packageId` and/or `type`. Returns light headers
854
- * (no body) carrying `packageId` for grouping.
855
- */
856
- listDrafts(options?: {
857
- packageId?: string;
858
- type?: string;
859
- }): Promise<MetadataDraftHeader[]>;
860
- /**
861
- * Get a single metadata item. Returns the unwrapped item content
862
- * (matching the framework REST handler which calls `res.json(item)`).
863
- * Returns `null` on 404 to keep the call-site ergonomic.
864
- */
865
- get<T = unknown>(type: string, name: string, options?: MetadataGetOptions): Promise<T | null>;
866
- /**
867
- * Read the pending draft body for an item (`?state=draft`). Returns
868
- * `null` when there is no draft pending. Draft reads do NOT fall
869
- * back to the published overlay or the artifact registry — a `null`
870
- * unambiguously means "nothing to publish".
871
- *
872
- * Note: the framework wraps draft responses in an envelope
873
- * `{ type, name, item }` (matching `getMetaItem`); callers should
874
- * read `.item` to get the body. The legacy `get()` returns the
875
- * unwrapped body, so this method preserves that asymmetry by
876
- * returning whatever the server sent.
877
- */
878
- getDraft<T = unknown>(type: string, name: string, options?: {
879
- packageId?: string;
880
- }): Promise<T | null>;
881
- /**
882
- * Save (PUT) a metadata item. The framework accepts both the bare
883
- * item payload and the `{ item: ... }` / `{ metadata: ... }`
884
- * envelopes; we send bare for consistency. Pass `mode: 'draft'` to
885
- * stage the change without publishing (Studio's "Save" button).
886
- */
887
- save<T = unknown>(type: string, name: string, item: unknown, options?: MetadataClientSaveOptions): Promise<T>;
888
- /**
889
- * Publish a single pending draft BY REFERENCE — promotes the draft row for
890
- * `(type, name)` into the active overlay and drops the draft. Works for ANY
891
- * draft, including ones with no `packageId` binding (which the package-scoped
892
- * `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
893
- * exact set returned by {@link listDrafts} without needing a package.
894
- *
895
- * Returns the server result. For `seed` drafts the protocol also materializes
896
- * the rows and reports under `seedApplied` — a data problem never fails the
897
- * publish, so callers should check `seedApplied?.success` and warn the user
898
- * rather than assume the data went live.
899
- */
900
- publishDraft(type: string, name: string): Promise<{
901
- success?: boolean;
902
- seedApplied?: {
903
- success: boolean;
904
- inserted?: number;
905
- updated?: number;
906
- error?: string;
907
- errors?: unknown[];
908
- };
909
- } & Record<string, unknown>>;
857
+
858
+ interface ObjectStackUserStateAdapterOptions {
859
+ /** Connected data source (usually the one provided by `<AdapterProvider>`). */
860
+ dataSource: DataSource<any>;
861
+ /** Authenticated user id. */
862
+ userId: string;
910
863
  /**
911
- * Get the 3-state layered view of a metadata item (Phase 3a). Returns
912
- * `code` (the artifact / fallback default), `overlay` (the saved
913
- * customisation, if any), and `effective` (what the runtime sees).
864
+ * Storage key. Should be a dotted, namespaced string so UI traces
865
+ * don't collide with user-facing preferences. Examples:
866
+ * `ui.favorites`, `ui.recent`, `ui.grid.account.state`.
914
867
  */
915
- layered<T = unknown>(type: string, name: string, options?: {
916
- packageId?: string;
917
- }): Promise<MetadataLayered<T>>;
868
+ key: string;
869
+ /** Override the storage object name. Defaults to `"sys_user_preference"`. */
870
+ resource?: string;
918
871
  /**
919
- * Cross-type sweep of load-time validation results — calls
920
- * `GET /meta/diagnostics`. Returns every entry the framework
921
- * considers invalid (or, when `severity: 'warning'`, also entries
922
- * with only warnings).
923
- *
924
- * Used by the Studio's governance overview page and by the
925
- * directory page to show "N invalid" badges per metadata type.
872
+ * Optional console logger for development diagnostics. Defaults to noop so
873
+ * production builds stay quiet.
926
874
  */
927
- diagnostics(options?: MetadataDiagnosticsOptions): Promise<MetadataDiagnosticsSummary>;
875
+ onError?: (where: 'load' | 'save', error: unknown) => void;
876
+ }
877
+ interface UserDataAdapter<T> {
878
+ load(): Promise<T[]>;
879
+ save(items: T[]): Promise<void>;
880
+ }
881
+ /**
882
+ * Build a `UserDataAdapter<T>` backed by ObjectStack.
883
+ *
884
+ * Each adapter instance is bound to a single `(user, key)` pair; create
885
+ * one per slot you want to persist.
886
+ */
887
+ declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
888
+
889
+ /**
890
+ * ObjectUI
891
+ * Copyright (c) 2024-present ObjectStack Inc.
892
+ *
893
+ * This source code is licensed under the MIT license found in the
894
+ * LICENSE file in the root directory of this source tree.
895
+ */
896
+ /**
897
+ * Integration module for @objectstack/spec v3.0.0
898
+ * Provides third-party service connectors for Slack, email, webhooks.
899
+ */
900
+ type IntegrationProvider = 'slack' | 'email' | 'webhook' | 'teams' | 'discord';
901
+ interface IntegrationConfig {
902
+ /** Integration provider type */
903
+ provider: IntegrationProvider;
904
+ /** Whether this integration is enabled */
905
+ enabled: boolean;
906
+ /** Provider-specific configuration */
907
+ config: Record<string, unknown>;
908
+ /** Event triggers */
909
+ triggers?: IntegrationTrigger[];
910
+ }
911
+ interface IntegrationTrigger {
912
+ /** Event name (e.g. 'record.created', 'record.updated') */
913
+ event: string;
914
+ /** Filter condition */
915
+ filter?: string;
916
+ /** Template for the message/payload */
917
+ template?: string;
918
+ }
919
+ interface SlackIntegrationConfig extends IntegrationConfig {
920
+ provider: 'slack';
921
+ config: {
922
+ webhookUrl: string;
923
+ channel?: string;
924
+ username?: string;
925
+ iconEmoji?: string;
926
+ };
927
+ }
928
+ interface EmailIntegrationConfig extends IntegrationConfig {
929
+ provider: 'email';
930
+ config: {
931
+ smtpHost: string;
932
+ smtpPort: number;
933
+ secure: boolean;
934
+ from: string;
935
+ to: string[];
936
+ subject?: string;
937
+ };
938
+ }
939
+ interface WebhookIntegrationConfig extends IntegrationConfig {
940
+ provider: 'webhook';
941
+ config: {
942
+ url: string;
943
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH';
944
+ headers?: Record<string, string>;
945
+ retryCount?: number;
946
+ retryDelay?: number;
947
+ };
948
+ }
949
+ /**
950
+ * Integration service manager.
951
+ * Manages third-party service connections and event dispatching.
952
+ */
953
+ declare class IntegrationManager {
954
+ private integrations;
928
955
  /**
929
- * Find every metadata item that references this one (Phase 3a). Useful
930
- * for pre-delete impact analysis: "Are any views pointing at this
931
- * object before I drop it?".
956
+ * Register a new integration.
932
957
  */
933
- references(type: string, name: string): Promise<MetadataReference[]>;
958
+ register(id: string, config: IntegrationConfig): void;
934
959
  /**
935
- * Reset a metadata customization overlay back to the artifact default.
936
- * Idempotent: returns the result even when no overlay row existed.
937
- * Pass `state: 'draft'` to discard the pending draft only (keeps the
938
- * published overlay intact) — useful for a Studio "Discard draft" button.
960
+ * Remove an integration.
939
961
  */
940
- reset<T = unknown>(type: string, name: string, options?: MetadataDeleteOptions): Promise<T>;
962
+ unregister(id: string): void;
941
963
  /**
942
- * Promote the pending draft of an item to the active overlay
943
- * (POST `/meta/:type/:name/publish`). Returns
944
- * `{ success, version, seq, message }`. Throws a `404 no_draft` if
945
- * nothing is pending and `409 metadata_conflict` if the published
946
- * overlay moved while the draft was sitting.
964
+ * Get all registered integrations.
947
965
  */
948
- publish<T = unknown>(type: string, name: string, options?: {
949
- actor?: string;
950
- message?: string;
951
- }): Promise<T>;
966
+ getAll(): Map<string, IntegrationConfig>;
952
967
  /**
953
- * Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
954
- * The server reads the history row at `toVersion`, writes its body back
955
- * as the active overlay with `operation_type='revert'`. Returns
956
- * `{ success, version, seq, restoredFromVersion, message }`. Throws
957
- * `404 version_not_found` for unknown versions and `409 version_not_restorable`
958
- * when the target version is a delete tombstone.
968
+ * Get integrations that match a specific event.
959
969
  */
960
- rollback<T = unknown>(type: string, name: string, toVersion: number, options?: {
961
- actor?: string;
962
- message?: string;
963
- }): Promise<T>;
970
+ getForEvent(event: string): IntegrationConfig[];
964
971
  /**
965
- * Compute a structured top-level key diff between two history versions
966
- * (GET `/meta/:type/:name/diff?from=&to=`). Returns
967
- * `{ added, removed, changed }` where each entry carries a `path` and
968
- * the relevant value(s). Useful for "what changed in this draft?" and
969
- * pre-rollback previews.
972
+ * Dispatch an event to all matching integrations.
973
+ * Returns results for each integration.
970
974
  */
971
- diff<T = unknown>(type: string, name: string, fromVersion?: number, toVersion?: number): Promise<T>;
972
- /** Fetch the durable history (change log) for one metadata item. */
973
- history<T = unknown>(type: string, name: string, options?: MetadataHistoryOptions): Promise<T>;
975
+ dispatch(event: string, payload: Record<string, unknown>): Promise<Array<{
976
+ id: string;
977
+ success: boolean;
978
+ error?: string;
979
+ }>>;
974
980
  /**
975
- * Fetch the protection-audit trail for one metadata item
976
- * (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
977
- * delete/reset attempt — allowed, denied, or forced — so the
978
- * Studio "审计日志 / Audit log" tab can render who tried what
979
- * and whether a lock blocked it.
981
+ * Send payload to a specific integration.
980
982
  */
981
- audit(type: string, name: string, options?: {
982
- limit?: number;
983
- }): Promise<MetadataAuditResponse>;
983
+ private send;
984
984
  }
985
985
 
986
986
  /**
@@ -1320,6 +1320,83 @@ declare function readTransactionalBatchCapability(discovery: unknown): boolean |
1320
1320
  * tripping on the property-name mismatch.
1321
1321
  */
1322
1322
  declare function is404Error(error: unknown): boolean;
1323
+ /**
1324
+ * The two denials the server derives from an object's `enable` block —
1325
+ * `apiAccessDenialFromEnable` (objectstack `packages/rest/src/rest-server.ts`).
1326
+ *
1327
+ * - `OBJECT_API_DISABLED` (404) — `enable.apiEnabled: false`; the object is
1328
+ * not exposed over the data API at all.
1329
+ * - `OBJECT_API_METHOD_NOT_ALLOWED` (405) — the operation is absent from the
1330
+ * `enable.apiMethods` whitelist.
1331
+ *
1332
+ * Both are **pure functions of the object's metadata**: no user, no permission,
1333
+ * no context, no request body. So neither is transient and neither is
1334
+ * per-user — when one happens it is a permanent property of that object, and
1335
+ * every retry of every persona gets the identical answer.
1336
+ *
1337
+ * That is exactly why they must not be degraded into "no data". A 404 from a
1338
+ * missing collection means *this backend doesn't have that table*, which the
1339
+ * optional-collection probes below legitimately read as "feature unavailable";
1340
+ * a 404 from `OBJECT_API_DISABLED` means *this page can never work*. Answering
1341
+ * the second with an empty result set renders "you have no records" over a
1342
+ * surface that is not allowed to have any (objectui#4408 — it also hid the
1343
+ * upstream defect objectstack#7544 for its entire life, because a merely
1344
+ * unpopulated page invites nobody to click through).
1345
+ */
1346
+ declare const API_ACCESS_DENIED_CODES: readonly ["OBJECT_API_DISABLED", "OBJECT_API_METHOD_NOT_ALLOWED"];
1347
+ /**
1348
+ * True when `error` is an `enable`-block API denial (see
1349
+ * {@link API_ACCESS_DENIED_CODES}).
1350
+ *
1351
+ * Discriminates on the ADR-0112 `code`, never on the status: 404 alone cannot
1352
+ * separate a disabled object from a missing collection, and 405 alone cannot
1353
+ * separate a withheld method from any other method rejection. The code survives
1354
+ * the transport — `@objectstack/client`'s fetch wrapper stamps `error.code`
1355
+ * from the response envelope, and both spellings are declared members of the
1356
+ * spec's `StandardErrorCode` — so no heuristic on status is needed or wanted.
1357
+ */
1358
+ declare function isApiAccessDeniedError(error: unknown): boolean;
1359
+ /**
1360
+ * What the by-name meta app route said about THIS session's access to an app
1361
+ * (objectui#4252 / objectstack#8013).
1362
+ *
1363
+ * - `granted` — the route served the app document.
1364
+ * - `denied` — the app EXISTS and the session lacks its `requiredPermissions`.
1365
+ * The only verdict a caller may render as an authorization refusal.
1366
+ * - `unknown` — anything else: an absent app, an unpublished one, an app
1367
+ * withheld by an absent optional service, an unreachable server, an adapter
1368
+ * that cannot ask. All of these are cases where the server declined to say
1369
+ * that a permission of the caller's is missing, so no caller may claim it.
1370
+ *
1371
+ * Three values rather than a boolean because the third is not a shade of the
1372
+ * other two: "the app is missing" and "I could not find out" both have to leave
1373
+ * the caller's existing copy alone, and collapsing them into `false` invites a
1374
+ * consumer to read a failed probe as a positive absence.
1375
+ */
1376
+ type AppAccessVerdict = 'granted' | 'denied' | 'unknown';
1377
+ /**
1378
+ * The ADR-0112 standard catalog code the by-name meta app route answers with
1379
+ * when an app exists and the session lacks its `requiredPermissions`
1380
+ * (objectstack#8013, `sendError(res, 403, 'PERMISSION_DENIED', …)` in
1381
+ * `packages/rest/src/rest-server.ts`).
1382
+ *
1383
+ * Deliberately NOT a member of {@link API_ACCESS_DENIED_CODES}: those two are
1384
+ * pure functions of an object's `enable` metadata — permanent, identical for
1385
+ * every persona — whereas this one is a statement about the CALLER, and the same
1386
+ * request by a different session succeeds. Same word "denied", different
1387
+ * question, so a consumer that wants one must never match the other.
1388
+ */
1389
+ declare const APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED";
1390
+ /**
1391
+ * True when `error` is the by-name app route's permission denial.
1392
+ *
1393
+ * Discriminates on the ADR-0112 `code`, never on the status (objectui#4408): the
1394
+ * route answers 403 for this and 404 for absence today, but a status is a
1395
+ * transport fact many conditions share, while the code is the contract. The
1396
+ * console's whole reason to call this is to tell two REFUSALS apart, and both
1397
+ * are errors.
1398
+ */
1399
+ declare function isAppPermissionDeniedError(error: unknown): boolean;
1323
1400
  /**
1324
1401
  * Thrown when the deployment has no analytics capability installed
1325
1402
  * (framework#3891 / #4019).
@@ -1434,6 +1511,50 @@ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpd
1434
1511
  * instead of showing one undirected toast.
1435
1512
  */
1436
1513
  declare function normaliseClientError(error: unknown): unknown;
1514
+ /**
1515
+ * Fold an @objectstack/client HTTP-failure `meta` bag into the log MESSAGE.
1516
+ *
1517
+ * The client already hands us everything worth knowing —
1518
+ * `logger.error("HTTP request failed", undefined, { method, url, status, error })`
1519
+ * — but it hands it as the THIRD argument. Everything that flattens a console
1520
+ * record to text (a headless/CDP console capture, a log shipper, a copied
1521
+ * DevTools line) keeps only the message and renders the rest as `[object
1522
+ * Object]` / `Object`, so a wall of failures carried no method, no URL and no
1523
+ * status: the reporter of objectui#4042 had to diff the network panel by hand
1524
+ * to find out that 30 red lines were all one benign pre-login burst.
1525
+ *
1526
+ * So the identifying fields go into the string itself, and the structured bag
1527
+ * is STILL passed alongside for DevTools to expand — text for the flatteners,
1528
+ * object for the inspectors, neither at the other's expense.
1529
+ *
1530
+ * Exported for tests. Returns `null` when `meta` carries none of the three
1531
+ * fields, so callers keep the original message rather than printing a husk.
1532
+ */
1533
+ declare function formatHttpFailureMessage(message: string, meta?: Record<string, any>): string | null;
1534
+ /**
1535
+ * Build a Logger compatible with @objectstack/client that (a) spells every
1536
+ * request failure out in the message — see {@link formatHttpFailureMessage} —
1537
+ * and (b) demotes expected 404 noise to console.debug. The client logs every
1538
+ * non-2xx response with
1539
+ * `logger.error("HTTP request failed", undefined, { method, url, status, error })`,
1540
+ * but 404s on optional collections (sys_presence, sys_activity, …) are part of
1541
+ * normal degraded operation when those plugins aren't installed on the
1542
+ * server — they should not surface as errors in the browser DevTools.
1543
+ *
1544
+ * NOTE the asymmetry, and keep it: 404-on-an-optional-collection is demoted
1545
+ * because it is an EXPECTED outcome of a request we still mean to make. No
1546
+ * other status is demoted — a 401 that survives the console's session gate
1547
+ * (objectui#4042: a mid-session expiry, say) is a real event and must stay a
1548
+ * visible, fully-identified error. The cure for doomed requests is not issuing
1549
+ * them, never hiding them once issued.
1550
+ *
1551
+ * Returned object is loosely typed because the spec's Logger interface lives
1552
+ * in a transitive package; using `any` keeps us decoupled.
1553
+ *
1554
+ * Exported so the console's log contract is testable, and so an app wiring its
1555
+ * own `ObjectStackClient` gets the same identified failures.
1556
+ */
1557
+ declare function createQuietHttpLogger(): any;
1437
1558
  /**
1438
1559
  * Connection state for monitoring
1439
1560
  */
@@ -1480,6 +1601,36 @@ interface WriteWarningEvent {
1480
1601
  /** Event listener type for write-warning (dropped-fields) events. */
1481
1602
  type WriteWarningListener = (event: WriteWarningEvent) => void;
1482
1603
 
1604
+ /**
1605
+ * Resolve which object a `type='view'` metadata item belongs to.
1606
+ *
1607
+ * The metadata index is name-only, not field-typed: `GET /api/v1/meta/view`
1608
+ * accepts `?package=` and `?preview=draft` and nothing else (measured on
1609
+ * framework `packages/rest/src/rest-server.ts` — the `GET /meta/:type`
1610
+ * handler — and on `client.meta.getItems(type, { packageId })`). So every
1611
+ * reader of the view namespace enumerates `type='view'` once and narrows to
1612
+ * one object HERE, client-side.
1613
+ *
1614
+ * ONE spelling, one place, deliberately: {@link ObjectStackAdapter.listViews}
1615
+ * and {@link ObjectStackAdapter.listViewOverrides} read the same rows out of
1616
+ * the same namespace, and two private copies of "which object is this?" is a
1617
+ * drift waiting to happen — the switcher showing a view whose override the
1618
+ * grid cannot find, or the reverse.
1619
+ *
1620
+ * `object` is the identity field the write path stamps (and that the
1621
+ * framework's overlay heals onto identity-less personalization rows —
1622
+ * objectstack#2555); `data.object` is the config's data-provider target and
1623
+ * `objectName` the legacy artifact spelling.
1624
+ *
1625
+ * **Exported** since objectui#4373, for the same one-spelling reason: a writer
1626
+ * outside this module that holds a view BODY but not its object name (app-shell's
1627
+ * `MetadataService`) needs the object to name the keys
1628
+ * {@link ObjectStackAdapter.invalidateViewKeys} drops, and a fourth private copy
1629
+ * of "which object is this?" is exactly the drift this accessor exists to
1630
+ * prevent. Identity only — it names no cache key.
1631
+ */
1632
+ declare function viewItemObjectName(item: any): string | undefined;
1633
+
1483
1634
  /**
1484
1635
  * ObjectStack Data Source Adapter
1485
1636
  *
@@ -1532,6 +1683,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1532
1683
  private atomicBatchCapability;
1533
1684
  private mutationListeners;
1534
1685
  private writeWarningListeners;
1686
+ private saveAdvisoryListeners;
1535
1687
  constructor(config: {
1536
1688
  baseUrl: string;
1537
1689
  token?: string;
@@ -1652,6 +1804,82 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1652
1804
  * this to toast the user; the write itself already succeeded.
1653
1805
  */
1654
1806
  onWriteWarning(callback: WriteWarningListener): () => void;
1807
+ /**
1808
+ * Subscribe to metadata save-advisory events — the runtime authoring gate's
1809
+ * advisory findings on a save that SUCCEEDED (#4237; backend
1810
+ * objectstack#7435). Returns an unsubscribe function.
1811
+ *
1812
+ * Deliberately the same seam as {@link onWriteWarning} (#3431/#3455), which
1813
+ * is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it
1814
+ * was modelled on. It is a SIBLING of that channel rather than a second
1815
+ * payload pushed down it: `WriteWarningEvent` is a closed shape whose
1816
+ * `droppedFields` is required and means "fields the write legally stripped",
1817
+ * so carrying advisories on it would either force every existing
1818
+ * `onWriteWarning` consumer to grow a branch or make the event lie about what
1819
+ * happened. The seam's SHAPE is what is reused here — a long-lived instance
1820
+ * with a `subscribe → unsubscribe` registration that `AdapterProvider` wires
1821
+ * once — not its event type.
1822
+ *
1823
+ * Why here and not on the config, which is how the other client class does it
1824
+ * (#4133/#4236): `MetadataClient` is minted per component by
1825
+ * `useMetadataClient`, so it has no instance to subscribe to and its sink
1826
+ * rides the factory. `ObjectStackAdapter` is the opposite — one long-lived
1827
+ * instance per app, already carrying this exact subscription pattern.
1828
+ */
1829
+ onSaveAdvisory(callback: MetadataSaveAdvisoryListener): () => void;
1830
+ /**
1831
+ * Notify all save-advisory subscribers. Isolated exactly like
1832
+ * {@link emitWriteWarning}: a throwing listener must neither break the save
1833
+ * nor starve the others.
1834
+ */
1835
+ private emitSaveAdvisory;
1836
+ /**
1837
+ * Install the ONE emitter for the metadata save door (#4237).
1838
+ *
1839
+ * ## Why this seam, and what it covers
1840
+ *
1841
+ * `ObjectStackClient.meta.saveItem` is the second client class that writes
1842
+ * through `PUT /api/v1/meta/:type/:name`, and every one of its callers reaches
1843
+ * it through an adapter this class constructed — the four inside this file
1844
+ * (`updateViewConfig`, the two view paths, `updateDashboard`) via
1845
+ * `this.client`, and every caller outside it via {@link getClient}, which
1846
+ * hands back this same instance: `MetadataService` (app-shell, five saves),
1847
+ * `useNavigationSync`, and plugin-designer's Create/EditAppPage. Wrapping the
1848
+ * method once here therefore covers all of them WITHOUT a per-site edit, which
1849
+ * is the whole point — a toast copied into a dozen call sites is the shape
1850
+ * #4133 rejected for the other client class and it is no better here.
1851
+ *
1852
+ * `meta` is an own, writable property assigned per instance in the SDK's
1853
+ * constructor (`this.meta = { … }`), and the client this adapter builds is
1854
+ * never shared, so the wrap is bounded to an object this adapter owns for its
1855
+ * whole lifetime. It is not a prototype or global patch.
1856
+ *
1857
+ * ## Response shape — measured, not assumed
1858
+ *
1859
+ * The two client classes' envelopes coincide at the top level, which is what
1860
+ * makes `readSaveAdvisories` reusable unchanged across both. `SaveMetaItem-
1861
+ * ResponseSchema` puts `advisories` at the body's top level next to
1862
+ * `success` / `version` / `seq` / `state`, and the SDK's `unwrapResponse`
1863
+ * strips its `{ success, data }` envelope only when the body actually HAS a
1864
+ * `data` key — this body does not, so it is returned verbatim. So the same
1865
+ * reader that `MetadataClient.save` uses reads this response correctly, and
1866
+ * the pins in `onSaveAdvisory.test.ts` drive a real SDK client through a fake
1867
+ * `fetch` rather than stubbing `meta`, so that continues to be measured.
1868
+ *
1869
+ * ## Draft-door honesty (D1)
1870
+ *
1871
+ * Drafts are NEVER gated: the framework returns at its D1 early-return
1872
+ * (`if (args.state !== 'active') return null`) before running a rule, so a
1873
+ * draft save produces no findings to withhold. This client class has no draft
1874
+ * door at all to worry about — the SDK's `saveItem(type, name, item)` takes no
1875
+ * mode and always writes the active door, which is exactly why the gate DOES
1876
+ * run for its callers. `mode` on the emitted event is therefore derived from
1877
+ * the response's own `state` rather than from a request-side flag that does
1878
+ * not exist here: `'draft'` when the server says the row landed as a draft,
1879
+ * `'publish'` otherwise. That keeps the event truthful about which door it
1880
+ * came through instead of hard-coding one.
1881
+ */
1882
+ private installSaveAdvisoryInterceptor;
1655
1883
  create(resource: string, data: Partial<T>): Promise<T>;
1656
1884
  /**
1657
1885
  * Update an existing record.
@@ -1887,23 +2115,90 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1887
2115
  * @returns Promise resolving to discovery data, or null if not connected
1888
2116
  */
1889
2117
  getDiscovery(): Promise<unknown | null>;
2118
+ /**
2119
+ * **The one place that knows which cache keys a view-row write invalidates.**
2120
+ *
2121
+ * Two reads are cached under view-shaped keys, and both go stale when any
2122
+ * writer touches a view row for `objectName`:
2123
+ *
2124
+ * | reader | key |
2125
+ * |----------------------------|----------------------------|
2126
+ * | {@link getView} | `view:{object}:{viewName}` |
2127
+ * | {@link listViewOverrides} | `view-overrides:{object}` |
2128
+ *
2129
+ * Every writer routes here rather than restating that pair. The repo has now
2130
+ * paid twice for restatement: objectui#3778 removed five copies of a key
2131
+ * (`views:{object}`) that no reader had ever populated, and objectui#4363
2132
+ * fixed four copies that named only half the live set — and objectui#4373 is
2133
+ * the measured proof that the failure recurs by default, because the console's
2134
+ * real create/publish flow writes these same rows through the ADR-0034
2135
+ * metadata seam and never learned the key list at all. A pin suite can only
2136
+ * guard the writers that exist; a mandatory seam makes the next writer
2137
+ * structurally unable to forget.
2138
+ *
2139
+ * The rule is uniform per WRITE, not per branch: a write to a view row drops
2140
+ * both keys, in that order. Draft-addressed writes over-invalidate on purpose
2141
+ * — both readers enumerate PUBLISHED rows, so a draft write stales neither —
2142
+ * because the costs are not symmetric. An unnecessary invalidation costs one
2143
+ * refetch; a missed one costs up to the cache's 5-minute TTL of stale
2144
+ * overrides, and "which half am I in?" is not a question a future writer
2145
+ * should have to re-answer to stay correct.
2146
+ *
2147
+ * Public because the writers are not all in this class: app-shell's ADR-0034
2148
+ * seam (`runtime-metadata-persistence.ts`) and `MetadataService` write the
2149
+ * same rows through `client.meta` / the `/meta` draft-publish API, and they
2150
+ * reach this adapter as the `dataSource` they were already handed.
2151
+ *
2152
+ * @param objectName - Object the view belongs to (the `{object}` in both keys)
2153
+ * @param viewName - The view's canonical `name` — the identity
2154
+ * {@link getView} reads back by and {@link listViewOverrides} keys its map
2155
+ * with. Callers that hold a qualified `<object>.<key>` name pass it as-is:
2156
+ * it is the row key, not a path to be split.
2157
+ */
2158
+ invalidateViewKeys(objectName: string, viewName: string): void;
1890
2159
  /**
1891
2160
  * Batch-fetch all persisted view overrides for an object.
1892
2161
  *
1893
- * Per-view runtime overrides (density, column widths, sort, …) are
1894
- * stored in the metadata registry under key `<objectName>/<viewName>`.
1895
- * Loading them per-view fires N HTTP GETs that return 404 for views
1896
- * the user has never customized — generates console noise on every
1897
- * page load. This batch method performs a single
1898
- * `GET /api/v1/meta/<objectName>` (returns `{type, items}`) and
1899
- * returns a `{viewName: override}` map. Returns an empty map if the
1900
- * server doesn't expose the listing or returns no items.
2162
+ * Per-view runtime overrides (density, column widths, sort, hidden
2163
+ * columns, inlineEdit …) live in the SAME metadata namespace the
2164
+ * write path uses: `type='view'`, `name=<viewId>` (see
2165
+ * {@link updateViewConfig}). Loading them per-view fires N HTTP GETs
2166
+ * that 404 for every view the user never customized — console noise on
2167
+ * every page load. This batch method performs a single
2168
+ * `GET /api/v1/meta/view` (returns `{type, items}`) and narrows the
2169
+ * result to `objectName` client-side, exactly as {@link listViews}
2170
+ * does over the same rows (shared accessor: {@link viewItemObjectName}).
2171
+ *
2172
+ * objectui#3774 — this used to enumerate `GET /api/v1/meta/<objectName>`,
2173
+ * putting the OBJECT name in the metadata TYPE slot. That key space is
2174
+ * disjoint from the one the write path lands in, so the batch map came
2175
+ * back empty for every object, forever, and every saved personalization
2176
+ * read back as "setting didn't save".
1901
2177
  *
1902
- * Result is cached identically to {@link getView}; saving a view via
1903
- * {@link updateViewConfig} invalidates the cache.
2178
+ * FAILURES REJECT — they are not answered as `{}`. An empty map is an
2179
+ * authoritative "this object has no overrides" (callers may trust it and
2180
+ * skip the per-view reads); a transport/permission failure is "I could
2181
+ * not tell", and reporting the two as the same value is what made
2182
+ * ObjectView's per-view {@link getView} fallback unreachable code. When
2183
+ * we cannot tell, we do not pretend we can. Rejections are not cached
2184
+ * (the cache stores on success only), so a transient failure does not
2185
+ * pin an empty answer for the TTL.
2186
+ *
2187
+ * Result is cached identically to {@link getView}, and EVERY view write —
2188
+ * on this adapter and above it — invalidates it through the one seam,
2189
+ * {@link invalidateViewKeys}. For a long time only {@link updateViewConfig}
2190
+ * did (objectui#4363), which left the other three adapter paths stale
2191
+ * for the cache's 5-minute TTL. That gap does not self-heal: the consumer
2192
+ * (`loadViewOverrides`, app-shell `ObjectView`) treats a RESOLVED map as
2193
+ * authoritative and deliberately does not re-probe per view — objectui#3774,
2194
+ * and correct, since re-probing reinstates the 404 flurry the batch read
2195
+ * exists to remove. So a stale map here is served in full, and the per-view
2196
+ * {@link getView} fallback that would have masked it never runs.
1904
2197
  *
1905
2198
  * @param objectName - Object name (e.g. 'lead')
1906
2199
  * @returns Map keyed by view name with the persisted override config
2200
+ * @throws whatever the metadata transport throws — callers that have a
2201
+ * per-view fallback should catch and use it.
1907
2202
  */
1908
2203
  listViewOverrides(objectName: string): Promise<Record<string, any>>;
1909
2204
  /**
@@ -1942,8 +2237,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1942
2237
  * match the view spec shape.
1943
2238
  *
1944
2239
  * Returns view spec objects with their canonical `name` as identifier.
1945
- * Filters by `data.object === objectName` (or top-level `object`)
1946
- * client-side because the metadata index is name-only, not field-typed.
2240
+ * Narrows to one object client-side via {@link viewItemObjectName} —
2241
+ * the metadata index is name-only, not field-typed, so the route has no
2242
+ * `?object=` to push the filter down into. {@link listViewOverrides}
2243
+ * reads the same rows through the same accessor.
1947
2244
  */
1948
2245
  listViews(objectName: string, options?: {
1949
2246
  previewDrafts?: boolean;
@@ -1971,23 +2268,127 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1971
2268
  *
1972
2269
  * Generates a snake_case name if `spec.name` is not provided by appending
1973
2270
  * a short timestamp suffix to the source-name hint.
2271
+ *
2272
+ * Invalidates through {@link invalidateViewKeys}, like every other writer —
2273
+ * see {@link listViewOverrides} for why the batch map is the one that cannot
2274
+ * heal itself (objectui#4363).
1974
2275
  */
1975
2276
  createView(objectName: string, spec: Record<string, any>): Promise<Record<string, any> | void>;
1976
2277
  /**
1977
2278
  * Apply a partial update to an existing overlay view. Reads the current
1978
- * overlay (or seeds from artifact), merges, and writes back. ADR-0005
1979
- * overlay rows store the *full* view document, so partial updates require
1980
- * a read-merge-write cycle.
2279
+ * document, merges, and writes it back. ADR-0005 overlay rows store the
2280
+ * *full* view document, so partial updates require a read-merge-write cycle.
2281
+ *
2282
+ * **Both halves address the same row (#4139).** A view has two possible
2283
+ * homes and the read must resolve the one the write will target:
2284
+ *
2285
+ * - a pending per-item **draft** (`?state=draft` / `?mode=draft`) — where
2286
+ * ADR-0034 stages every runtime-created view, so a view made from the `+`
2287
+ * tab lives ONLY here until an explicit Publish;
2288
+ * - the **published** overlay (`client.meta.getItem` / `saveItem`).
2289
+ *
2290
+ * The draft is probed FIRST, and a hit is merged and written straight back
2291
+ * as a draft. Two things that ordering buys, both load-bearing:
2292
+ *
2293
+ * 1. A draft-only view is no longer invisible to the read. It used to 404,
2294
+ * and a `catch {}` labelled "treat missing as create-equivalent"
2295
+ * substituted `current = {}` — so a rename merged onto nothing and went
2296
+ * out as a `{label, name, object}` partial the server rejects (422),
2297
+ * while the draft row the UI reads back through `?preview=draft` kept the
2298
+ * old label. The edit was lost with no error surfaced to the user.
2299
+ * 2. A draft is never bypassed. Writing the published row while a draft is
2300
+ * pending would put the edit somewhere the draft shadows — and Publish
2301
+ * would then overwrite it with the pre-edit body, losing the change a
2302
+ * second time, later, where nothing connects it to this call.
2303
+ *
2304
+ * A draft edit stays a draft: `mode: 'draft'` keeps ADR-0037's guarantee
2305
+ * that nothing the preview shows goes live until Publish. Renaming a
2306
+ * *published* view (no draft pending) is unchanged — it writes the
2307
+ * published overlay, as before.
2308
+ *
2309
+ * **Both halves invalidate through {@link invalidateViewKeys}** (objectui#4363,
2310
+ * #4373) — same call, same key set, so the draft half's deliberate
2311
+ * over-invalidation is the seam's uniform rule rather than a per-branch
2312
+ * decision this method has to keep re-making.
2313
+ *
2314
+ * @throws when the view resolves in neither home, or when either read fails
2315
+ * for any other reason (network, permission). Both used to be swallowed
2316
+ * and converted into the bad partial write above; a caller that wants a
2317
+ * view created should call {@link createView}, which is the operation that
2318
+ * actually means "create".
1981
2319
  */
1982
2320
  updateView(objectName: string, viewName: string, partial: Record<string, any>): Promise<Record<string, any> | void>;
1983
2321
  /**
1984
- * Delete an overlay view (reset to artifact default if one exists, or
1985
- * remove entirely if it was a user-created view). Routes to
1986
- * `DELETE /api/v1/meta/view/:name`.
2322
+ * Delete an overlay view — from **every home it has**.
2323
+ *
2324
+ * A view has two possible homes, the same two {@link updateView} addresses:
2325
+ * the pending per-item **draft** (`DELETE /meta/view/:name?state=draft`) and
2326
+ * the **published** overlay (`DELETE /meta/view/:name`). This method used to
2327
+ * issue only the second, and the three cases came out like this (#4479):
2328
+ *
2329
+ * | case | before |
2330
+ * |--------------------|-----------------------------------------------------|
2331
+ * | draft-only view | BUG — the published delete answered `reset:false` / |
2332
+ * | | "nothing to delete", the draft survived, and the tab |
2333
+ * | | was still there after reload |
2334
+ * | published-only | correct — `reset:true`, tab gone |
2335
+ * | published + draft | ACCIDENTALLY correct — the published row went, so |
2336
+ * | | the tab went; the orphan draft stayed behind |
2337
+ *
2338
+ * **Why this is not the mechanical mirror of #4139.** `updateView` probes
2339
+ * the draft first and writes back to whichever home the read resolved, and
2340
+ * that is right for an update in all three cases. Copying it here would be
2341
+ * wrong in the third: `persistRuntimeMetadata` (app-shell) stages EVERY
2342
+ * runtime edit as a draft, so "publish a view, then edit it" routinely
2343
+ * produces a pair — and a draft-first-only delete on a pair discards the
2344
+ * draft and leaves the published row serving the view. That is not Delete
2345
+ * view, it is **Discard draft**, a deliberately different operation that
2346
+ * already exists (`discardRuntimeDraft`, documented as "the published
2347
+ * overlay is untouched"). The clean statement of the asymmetry: for an
2348
+ * update, one home is the right home; for a delete, "remove this view" is
2349
+ * satisfied only when NO home is left serving it.
2350
+ *
2351
+ * So both homes are deleted, **draft first**. The order is load-bearing on
2352
+ * the failure path: a fault between the two calls leaves the PUBLISHED
2353
+ * overlay intact, so the view is still served and the operation is cleanly
2354
+ * retryable. The reverse order would strand a draft-only view — which is
2355
+ * precisely the bug shape above.
2356
+ *
2357
+ * **Two blind calls, no probe.** The framework's `deleteMetaItem` answers a
2358
+ * missing home with a **200** carrying `reset:false` (`"No pending draft
2359
+ * for view/x."` / `"No view 'x' found — nothing to delete."`), never a 404,
2360
+ * so there is nothing for a probe to protect against. `updateView` needs its
2361
+ * probe for a different reason — its read must resolve the row the merge
2362
+ * writes back to — and that reason has no counterpart for a delete.
2363
+ *
2364
+ * **One transport, one error contract.** Both halves go through
2365
+ * {@link MetadataClient} (`reset`), the transport that can express the
2366
+ * `?state=` qualifier and the one `updateView`'s draft half already uses.
2367
+ * The published half used to go through `client.meta.deleteItem`; measured,
2368
+ * that issues the byte-identical request (`DELETE
2369
+ * {baseUrl}/api/v1/meta/view/:name`, no environment scoping is configured on
2370
+ * this adapter), so routing it here costs no addressing change and buys a
2371
+ * single `MetadataError` shape across both calls instead of two.
2372
+ *
2373
+ * @returns `deleted` is true only when no home is left serving the view AND
2374
+ * at least one actually held a row. A view that existed in neither home
2375
+ * still answers `false`, unchanged. The per-home outcomes are additive:
2376
+ * a partial result is observable rather than rounded up to `true`.
2377
+ * @throws when either delete fails, matching {@link updateView}'s
2378
+ * convention of surfacing the fault rather than degrading. A failure of
2379
+ * the PUBLISHED half after the draft was discarded carries the partial
2380
+ * state on the error's `outcome` — "draft gone, overlay left" is exactly
2381
+ * what the old `{ deleted: boolean }` could not express.
2382
+ *
2383
+ * Invalidates through {@link invalidateViewKeys}: the deleted row leaves the
2384
+ * batch override map too, and a ghost entry there is what the object page
2385
+ * would keep applying (objectui#4363). Fired in a `finally`, so it happens
2386
+ * once per call on EVERY outcome including the throw — after a half-failure
2387
+ * the draft row really is gone, and #4363's asymmetry decides it: an
2388
+ * unnecessary invalidation costs one refetch, a missed one costs the cache's
2389
+ * full 5-minute TTL of stale overrides.
1987
2390
  */
1988
- deleteView(objectName: string, viewName: string): Promise<{
1989
- deleted: boolean;
1990
- }>;
2391
+ deleteView(objectName: string, viewName: string): Promise<DeleteViewResult>;
1991
2392
  /**
1992
2393
  * Get an application definition by name or ID.
1993
2394
  * Attempts to fetch from the server metadata API.
@@ -1998,6 +2399,43 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1998
2399
  * @returns Promise resolving to the app definition or null
1999
2400
  */
2000
2401
  getApp(appId: string): Promise<unknown | null>;
2402
+ /**
2403
+ * Ask the by-name meta app route WHY an app is not in this session's app list
2404
+ * (objectui#4252).
2405
+ *
2406
+ * `GET /api/v1/meta/apps` is filtered per session server-side
2407
+ * (`filterAppForUser`), so an app withheld by `requiredPermissions` and an app
2408
+ * that does not exist are byte-identical there: both are simply absent. A
2409
+ * console reading only that list has one fact and two conditions, and it
2410
+ * renders its copy for the wrong one — "it may still be publishing" over a
2411
+ * permanent authorization decision, which cost a downstream acceptance round
2412
+ * two test batches spent chasing a platform defect that was a missing
2413
+ * permission-set binding.
2414
+ *
2415
+ * The maintainer ruling (2026-08-12) put the answer on the BY-NAME route
2416
+ * rather than in the list, so the enumeration surface is not widened past what
2417
+ * a by-name probe already implies (objectstack#8013 / PR #8135): an app that
2418
+ * exists and whose `requiredPermissions` the session lacks answers `403` with
2419
+ * `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent
2420
+ * name, an unpublished app, an app gated by an absent optional service —
2421
+ * keeps answering `404 RESOURCE_NOT_FOUND`.
2422
+ *
2423
+ * ## Why this is a separate method and not a flavour of {@link getApp}
2424
+ *
2425
+ * - `getApp` degrades EVERY failure to `null`, which is exactly the
2426
+ * conflation this exists to undo; changing it would silently re-point its
2427
+ * own callers' fallback-to-static-config path.
2428
+ * - `getApp` memoises in `metadataCache`. A verdict about the CALLER must not
2429
+ * be cached beside a document about the APP — one grant, and a cached
2430
+ * denial outlives the session it described.
2431
+ *
2432
+ * Nothing here throws: a probe that cannot reach an answer returns `unknown`
2433
+ * and the caller keeps whatever it was already showing. Only the measured
2434
+ * `code` produces `denied` — never a status, never a message (objectui#4408).
2435
+ *
2436
+ * @param appName - the app name as it appears in the URL segment
2437
+ */
2438
+ probeAppAccess(appName: string): Promise<AppAccessVerdict>;
2001
2439
  /**
2002
2440
  * Get a page definition from ObjectStack.
2003
2441
  * Uses the metadata API to fetch page layouts.
@@ -2057,44 +2495,67 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2057
2495
  * silently returning wrong numbers.
2058
2496
  *
2059
2497
  * @param dataset - An inline dataset definition (draft) OR a saved dataset name.
2060
- * @param selection - Dimension/measure names to project + runtime directives.
2061
- */
2062
- queryDataset(dataset: Record<string, unknown> | string, selection: {
2063
- dimensions?: string[];
2064
- measures: string[];
2065
- runtimeFilter?: Record<string, unknown>;
2066
- timeDimensions?: unknown[];
2067
- compareTo?: {
2068
- kind: 'previousPeriod' | 'previousYear';
2069
- dimension: string;
2070
- };
2071
- order?: Record<string, 'asc' | 'desc'>;
2072
- limit?: number;
2073
- offset?: number;
2074
- timezone?: string;
2075
- /** Marginal-aggregate groupings (e.g. `[rows, [colDim], []]`) — server
2076
- * computes each subtotal with the measure's TRUE aggregate (never client
2077
- * re-derived). `[]` is the grand total. */
2078
- totals?: {
2079
- groupings: string[][];
2080
- };
2081
- }): Promise<{
2498
+ * @param selection - The spec's {@link DatasetSelection} — dimension/measure
2499
+ * names to project plus runtime directives. This parameter IS the spec type
2500
+ * by reference, never a local restatement of it (objectui#3613): a hand
2501
+ * copy of a contract is a second dialect of it, and the copy this replaced
2502
+ * had already drifted three ways from `@objectstack/spec` — it required
2503
+ * `compareTo.dimension` (optional since objectstack#5011, and resolved by
2504
+ * the EXECUTOR, so requiring it pushed callers into exactly the
2505
+ * consumer-side dimension guess AGENTS.md #0.1 forbids), it widened
2506
+ * `timeDimensions` to `unknown[]` and `runtimeFilter` to
2507
+ * `Record<string, unknown>`, and it had never grown `dateGranularity` at
2508
+ * all. Pinned in `queryDataset.test.ts`.
2509
+ */
2510
+ queryDataset(dataset: Record<string, unknown> | string, selection: DatasetSelection): Promise<{
2082
2511
  rows: Array<Record<string, unknown>>;
2083
- /** Column metadata: a display `label` (dimensions and measures), and a
2084
- * measure's numeral `format` + declared `currency` for value formatting. */
2085
- fields: Array<{
2086
- name: string;
2087
- type: string;
2088
- label?: string;
2089
- format?: string;
2090
- currency?: string;
2091
- }>;
2512
+ /**
2513
+ * Column metadata — the spec's `AnalyticsResult.fields[]` element BY
2514
+ * REFERENCE, never a local restatement of it (objectui#3752). Read
2515
+ * `@objectstack/spec` for what a column carries; this comment deliberately
2516
+ * does not re-list the keys, because the enumeration it replaced was the
2517
+ * bug: it named five (`name`/`type`/`label`/`format`/`currency`) and stopped
2518
+ * at the contract of the day it was written, so it never grew
2519
+ * `percentScale` — the server's answer to whether a percentage column is a
2520
+ * 0–1 fraction or already percentage points. The spec says a renderer that
2521
+ * receives it "must scale by it instead of guessing from the value"
2522
+ * (objectui#3136), so a declaration that hides the key steers a typed
2523
+ * consumer into exactly the guess-by-magnitude the issue banned. Pinned in
2524
+ * `queryDataset.test.ts`.
2525
+ *
2526
+ * Only this element is spec-owned: the envelope around it (`object` /
2527
+ * `dimensionFields` / `drillRawRows`) is ADR-0021 D2 drill metadata the REST
2528
+ * route adds on top of `AnalyticsResult`, and this method never returns the
2529
+ * result's `sql`, so the whole envelope is NOT an `AnalyticsResult`.
2530
+ */
2531
+ fields: Array<AnalyticsResult['fields'][number]>;
2092
2532
  /** ADR-0021 D2 drill-through: the dataset's base object (records to drill into). */
2093
2533
  object?: string;
2094
2534
  /** Drillable dimension NAME → underlying object FIELD name. */
2095
2535
  dimensionFields?: Record<string, string>;
2096
2536
  /** Raw grouped values per row (aligned to `rows` by index) for drill filters. */
2097
2537
  drillRawRows?: Array<Record<string, unknown>>;
2538
+ /**
2539
+ * Half-open date-range drill scope per row (framework#1752), aligned to
2540
+ * `rows` by index: dimension NAME → the field and `[gte, lt)` bounds of that
2541
+ * row's time bucket. The RANGE companion to `drillRawRows`, which handles
2542
+ * equality dims only — a `dateGranularity` dimension groups a SPAN of
2543
+ * records into one bucket, so the server excludes date dims from
2544
+ * `dimensionFields`/`drillRawRows` and sends this sidecar instead.
2545
+ *
2546
+ * The entry type is `@object-ui/core`'s `DatasetDrillRange` BY REFERENCE,
2547
+ * not a local restatement of it (objectui#3613/#3752 discipline): the same
2548
+ * declaration is what `buildDatasetDrillFilter` — the single consumer that
2549
+ * turns these bounds into an ObjectQL `{ $gte, $lt }` — accepts, and what
2550
+ * `DatasetWidget` / `DatasetReportRenderer` type their state with. Nothing
2551
+ * in `@objectstack/spec` owns this shape yet (the server's own
2552
+ * `AnalyticsResultWithDrill` is local to `service-analytics`), so the shared
2553
+ * in-repo interface is the one contract available; restating it here would
2554
+ * make a third dialect of it. Like `drillRawRows`, only the ARRAY is
2555
+ * validated below — the bounds are unvalidated payload, which is exactly why
2556
+ * `DatasetDrillRange` declares them `unknown`.
2557
+ */
2558
+ drillRanges?: Array<Record<string, DatasetDrillRange>>;
2098
2559
  /** Server-computed marginal aggregates, one entry per requested grouping. */
2099
2560
  totals?: Array<{
2100
2561
  dimensions: string[];
@@ -2211,4 +2672,4 @@ declare function createObjectStackAdapter<T = unknown>(config: {
2211
2672
  reconnectDelay?: number;
2212
2673
  }): DataSource<T>;
2213
2674
 
2214
- export { AnalyticsNotInstalledError, AnalyticsQueryRejectedError, type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, DataApiValidationError, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, FILTER_OPERATOR_ALIASES, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, MalformedFilterError, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, MetadataNotFoundError, type MetadataReference, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityManagerPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, type WebhookIntegrationConfig, type WriteWarningEvent, type WriteWarningListener, calculateAutoLayout, classifyAnalyticsFailure, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, generateContractManifest, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, normaliseClientError, readTransactionalBatchCapability, serializeOrderBy, snapToGrid, validatePluginContract };
2675
+ export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, type AppAccessVerdict, type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, DataApiValidationError, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, FILTER_OPERATOR_ALIASES, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, MalformedFilterError, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, MetadataNotFoundError, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityManagerPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, type WebhookIntegrationConfig, type WriteWarningEvent, type WriteWarningListener, calculateAutoLayout, classifyAnalyticsFailure, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, createQuietHttpLogger, formatHttpFailureMessage, generateContractManifest, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, normaliseClientError, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, snapToGrid, validatePluginContract, viewItemObjectName };