@proveanything/smartlinks 2.0.21 → 2.0.24

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/openapi.yaml CHANGED
@@ -18255,6 +18255,11 @@ components:
18255
18255
  type: array
18256
18256
  items:
18257
18257
  type: string
18258
+ dataScope:
18259
+ type: string
18260
+ enum:
18261
+ - collection
18262
+ - global
18258
18263
  apiVersion:
18259
18264
  type: string
18260
18265
  handler:
@@ -18274,6 +18279,41 @@ components:
18274
18279
  required:
18275
18280
  - files
18276
18281
  - definitions
18282
+ AppDataOpts:
18283
+ type: object
18284
+ properties:
18285
+ scope:
18286
+ type: string
18287
+ enum:
18288
+ - collection
18289
+ - global
18290
+ productId:
18291
+ type: string
18292
+ variantId:
18293
+ type: string
18294
+ batchId:
18295
+ type: string
18296
+ dataId:
18297
+ type: string
18298
+ queries:
18299
+ type: object
18300
+ additionalProperties: true
18301
+ AppDataHandle:
18302
+ type: object
18303
+ properties: {}
18304
+ ServerFunctionSl:
18305
+ type: object
18306
+ properties:
18307
+ appRecords: {}
18308
+ products: {}
18309
+ attestations: {}
18310
+ appData:
18311
+ $ref: "#/components/schemas/AppDataHandle"
18312
+ required:
18313
+ - appRecords
18314
+ - products
18315
+ - attestations
18316
+ - appData
18277
18317
  ServerFunctionCaller:
18278
18318
  type: object
18279
18319
  properties:
@@ -18298,7 +18338,8 @@ components:
18298
18338
  type: string
18299
18339
  appId:
18300
18340
  type: string
18301
- sl: {}
18341
+ sl:
18342
+ $ref: "#/components/schemas/ServerFunctionSl"
18302
18343
  secrets:
18303
18344
  type: object
18304
18345
  additionalProperties: true
@@ -18314,6 +18355,30 @@ components:
18314
18355
  - secrets
18315
18356
  - caller
18316
18357
  - fetch
18358
+ ServerFunctionHttpEvent:
18359
+ type: object
18360
+ properties:
18361
+ method:
18362
+ type: string
18363
+ body:
18364
+ type: object
18365
+ additionalProperties: true
18366
+ query:
18367
+ type: object
18368
+ additionalProperties: true
18369
+ headers:
18370
+ type: object
18371
+ additionalProperties: true
18372
+ rawBody:
18373
+ type: object
18374
+ additionalProperties: true
18375
+ contentType:
18376
+ type: string
18377
+ required:
18378
+ - method
18379
+ - body
18380
+ - query
18381
+ - headers
18317
18382
  AppAdminConfig:
18318
18383
  type: object
18319
18384
  properties:
@@ -18397,6 +18462,40 @@ components:
18397
18462
  - scope
18398
18463
  - name
18399
18464
  - type
18465
+ PublicViewParams:
18466
+ type: object
18467
+ properties:
18468
+ required:
18469
+ type: array
18470
+ items:
18471
+ type: string
18472
+ optional:
18473
+ type: array
18474
+ items:
18475
+ type: string
18476
+ PublicView:
18477
+ type: object
18478
+ properties:
18479
+ id:
18480
+ type: string
18481
+ title:
18482
+ type: string
18483
+ kind:
18484
+ $ref: "#/components/schemas/PublicViewKind"
18485
+ route:
18486
+ type: string
18487
+ set:
18488
+ type: object
18489
+ additionalProperties:
18490
+ type: string
18491
+ params:
18492
+ $ref: "#/components/schemas/PublicViewParams"
18493
+ default:
18494
+ type: boolean
18495
+ required:
18496
+ - id
18497
+ - title
18498
+ - kind
18400
18499
  AppManifest:
18401
18500
  type: object
18402
18501
  properties:
@@ -18472,6 +18571,10 @@ components:
18472
18571
  type: array
18473
18572
  items:
18474
18573
  $ref: "#/components/schemas/DeepLinkEntry"
18574
+ publicViews:
18575
+ type: array
18576
+ items:
18577
+ $ref: "#/components/schemas/PublicView"
18475
18578
  executor:
18476
18579
  $ref: "#/components/schemas/AppManifestExecutor"
18477
18580
  functions:
@@ -18532,6 +18635,11 @@ components:
18532
18635
  enum:
18533
18636
  - caller
18534
18637
  - collection
18638
+ PublicViewKind:
18639
+ type: string
18640
+ enum:
18641
+ - contextual
18642
+ - standalone
18535
18643
  PaginatedResponse:
18536
18644
  type: object
18537
18645
  properties:
@@ -29290,7 +29398,8 @@ components:
29290
29398
  type: object
29291
29399
  properties:
29292
29400
  appId:
29293
- type: string
29401
+ type: object
29402
+ additionalProperties: true
29294
29403
  AllocateSequenceInput:
29295
29404
  type: object
29296
29405
  properties:
@@ -24,6 +24,25 @@ export interface TestSlImpl {
24
24
  attestations?: {
25
25
  create?(fields: any): any;
26
26
  };
27
+ /**
28
+ * THIS app's own data. Provide impls to assert calls, or omit to use a built-in in-memory store
29
+ * (so a counter test actually persists across calls within the test). `.global`/`.collection` and
30
+ * per-call `{ scope }` share the same store in the harness.
31
+ */
32
+ appData?: {
33
+ get?(opts?: any): any;
34
+ set?(data: any, opts?: any): any;
35
+ getData?(opts?: any): any;
36
+ setData?(data: any, opts?: any): any;
37
+ delete?(opts?: any): any;
38
+ };
39
+ /** Cross-app reads: return another app's data as the caller would see it. Keyed by appId. */
40
+ app?: (appId: string) => {
41
+ data?: {
42
+ get?(opts?: any): any;
43
+ getData?(opts?: any): any;
44
+ };
45
+ };
27
46
  }
28
47
  export interface TestCaller {
29
48
  userId?: string | null;
@@ -102,6 +102,36 @@ export function createFunctionTestContext(opts) {
102
102
  create: gated('attestations', 'write', at.create && at.create.bind(at), 'create'),
103
103
  },
104
104
  };
105
+ // appData — own-data (capability-gated only, no role gate). Delegates to a provided impl, else a
106
+ // built-in in-memory store so read-modify-write (e.g. a counter) works across calls in one test.
107
+ const ad = impl.appData || {};
108
+ const memConfig = {};
109
+ const memData = {};
110
+ const deepMerge = (t, s) => { for (const k of Object.keys(s || {}))
111
+ t[k] = (s[k] && typeof s[k] === 'object' && !Array.isArray(s[k])) ? deepMerge(t[k] || {}, s[k]) : s[k]; return t; };
112
+ const makeAppData = () => ({
113
+ get: gated('data', 'read', ad.get ? ad.get.bind(ad) : (async () => (Object.assign({}, memConfig))), 'get'),
114
+ set: gated('data', 'write', ad.set ? ad.set.bind(ad) : (async (data) => deepMerge(memConfig, data)), 'set'),
115
+ getData: gated('data', 'read', ad.getData ? ad.getData.bind(ad) : (async (opts = {}) => (opts.dataId ? memData[opts.dataId] : Object.values(memData))), 'getData'),
116
+ setData: gated('data', 'write', ad.setData ? ad.setData.bind(ad) : (async (data, opts = {}) => { if (opts.dataId)
117
+ memData[opts.dataId] = data; return data; }), 'setData'),
118
+ delete: gated('data', 'write', ad.delete ? ad.delete.bind(ad) : (async (opts = {}) => { if (opts.dataId)
119
+ delete memData[opts.dataId];
120
+ else
121
+ for (const k of Object.keys(memConfig))
122
+ delete memConfig[k]; }), 'delete'),
123
+ });
124
+ sl.appData = Object.assign(makeAppData(), { global: makeAppData(), collection: makeAppData() });
125
+ sl.app = (appId) => {
126
+ const other = (impl.app && impl.app(appId)) || {};
127
+ const od = other.data || {};
128
+ return {
129
+ data: {
130
+ get: gated('data', 'read', od.get ? od.get.bind(od) : (async () => null), 'app.get'),
131
+ getData: gated('data', 'read', od.getData ? od.getData.bind(od) : (async () => []), 'app.getData'),
132
+ },
133
+ };
134
+ };
105
135
  const secretsMap = opts.secrets || {};
106
136
  const baseFetch = opts.fetch || (typeof fetch !== 'undefined' ? fetch : undefined);
107
137
  const via = (def.trigger && def.trigger.type) || 'http';
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
1
3
  /**
2
4
  * A bundle (widget or container) as returned by the collection widgets endpoint.
3
5
  *
@@ -258,6 +260,13 @@ export interface AppFunctionDef {
258
260
  * Grammar: `sl:<resource>:<read|write>`, `network` (or `network:<host>`), `secrets:<ref>`.
259
261
  */
260
262
  capabilities?: string[];
263
+ /**
264
+ * Default scope for `ctx.sl.appData.*` — `'collection'` (this collection; the default) or
265
+ * `'global'` (the app's app-wide bucket, shared by every collection that installed it). A function
266
+ * whose data is global-by-intent declares `'global'` and then just uses `appData.get()/set()`.
267
+ * Per-call `{ scope }` and the `appData.global` / `appData.collection` handles override this.
268
+ */
269
+ dataScope?: 'collection' | 'global';
261
270
  /** SDK/manifest API version this function targets (pinned for runtime compatibility). */
262
271
  apiVersion?: string;
263
272
  /** Exported handler name in the functions bundle. Defaults to `name`. */
@@ -268,6 +277,57 @@ export interface AppManifestFunctions {
268
277
  files: AppManifestFiles;
269
278
  definitions: AppFunctionDef[];
270
279
  }
280
+ /** Scope + selector options for `ctx.sl.appData.*`. */
281
+ export interface AppDataOpts {
282
+ /** `'collection'` (this collection) or `'global'` (the app's app-wide bucket). Defaults to the function's `dataScope`. */
283
+ scope?: 'collection' | 'global';
284
+ productId?: string;
285
+ variantId?: string;
286
+ batchId?: string;
287
+ /** For `getData`/`setData`/`delete`: the keyed data item id. */
288
+ dataId?: string;
289
+ queries?: Record<string, any>;
290
+ }
291
+ /** Read/write handle for an app's own durable data. `set` deep-merges the singleton config blob. */
292
+ export interface AppDataHandle {
293
+ get(opts?: AppDataOpts): Promise<any>;
294
+ set(data: any, opts?: AppDataOpts): Promise<any>;
295
+ getData(opts?: AppDataOpts): Promise<any>;
296
+ setData(data: any, opts?: AppDataOpts): Promise<any>;
297
+ delete(opts?: AppDataOpts): Promise<any>;
298
+ }
299
+ /**
300
+ * The capability-gated SmartLinks surface handed to a server function. Two access modes:
301
+ * - by APP identity — `appData` / `appRecords` are THIS app's OWN data (any scope), gated by the
302
+ * declared capability only (the caller's role is irrelevant — it's the app's own namespace).
303
+ * - by CALLER identity — everything else (products, attestations, and `app(id)` cross-app reads)
304
+ * is bounded by what the invoking user could do through the permissioned API.
305
+ */
306
+ export interface ServerFunctionSl {
307
+ /** THIS app's records (own namespace). */
308
+ appRecords: any;
309
+ /** Products, scoped by caller authority + declared capability. */
310
+ products: any;
311
+ /** Attestations, scoped by caller authority + declared capability. */
312
+ attestations: any;
313
+ /**
314
+ * THIS app's durable data/config. Default scope follows the function's declared `dataScope`
315
+ * (else `collection`). `appData.global` / `appData.collection` force a scope regardless.
316
+ * Own-data access is gated by `sl:data:read` / `sl:data:write` only.
317
+ */
318
+ appData: AppDataHandle & {
319
+ global: AppDataHandle;
320
+ collection: AppDataHandle;
321
+ };
322
+ /**
323
+ * Read ANOTHER app's data in this collection, by CALLER identity — only what the invoking user
324
+ * could read (admin sees all; otherwise public-filtered, private fields stripped). Read-only;
325
+ * never another app's private/global store, never another collection. Gated by `sl:data:read`.
326
+ */
327
+ app(appId: string): {
328
+ data: Pick<AppDataHandle, 'get' | 'getData'>;
329
+ };
330
+ }
271
331
  /** Identity of whoever invoked a server function. */
272
332
  export interface ServerFunctionCaller {
273
333
  /** Authenticated user id, or `null` for anonymous/public/system invocations. */
@@ -297,7 +357,7 @@ export interface ServerFunctionContext {
297
357
  * only — never a global superuser.
298
358
  * Declared `capabilities` cap what these calls may do.
299
359
  */
300
- sl: any;
360
+ sl: ServerFunctionSl;
301
361
  /**
302
362
  * Capability-gated secret access (both require the `secrets:<ref>` capability).
303
363
  * `get(ref)` resolves the collection's OWN secret first (a client's credential), then falls back
@@ -315,7 +375,29 @@ export interface ServerFunctionContext {
315
375
  /** Structured logging captured into run telemetry. */
316
376
  log: (message: string, data?: Record<string, any>) => void;
317
377
  }
318
- /** The signature every SmartLinks server function implements. */
378
+ /** The HTTP request handed to an `http`-trigger function as `event`. */
379
+ export interface ServerFunctionHttpEvent<TBody = any> {
380
+ method: string;
381
+ /** Parsed JSON body (when the request was JSON). */
382
+ body: TBody;
383
+ query: Record<string, any>;
384
+ /** Raw request headers (lower-cased keys). */
385
+ headers: Record<string, any>;
386
+ /** Raw request body bytes, when captured — for non-JSON inputs (XML, form, …). */
387
+ rawBody?: string | Buffer | null;
388
+ /** The request `content-type`, if any. */
389
+ contentType?: string | null;
390
+ }
391
+ /**
392
+ * The signature every SmartLinks server function implements.
393
+ *
394
+ * RETURN CONTRACT (http trigger): whatever you return **is** the HTTP response.
395
+ * - a plain value → JSON body, HTTP 200 (no `{ ok, result }` envelope);
396
+ * - a web-standard `Response` → passed through verbatim (your status, headers, content-type, body —
397
+ * XML, CSV, binary, redirect, custom status);
398
+ * - throwing → HTTP 500 `{ error, message, code }`. For expected errors, return a `Response` with
399
+ * your own 4xx/5xx.
400
+ */
319
401
  export type ServerFunctionHandler<TEvent = any, TResult = any> = (ctx: ServerFunctionContext, event: TEvent) => Promise<TResult> | TResult;
320
402
  /**
321
403
  * Shape of `app.admin.json` -- the separate admin configuration file pointed to
@@ -403,6 +485,41 @@ export interface AppAdminConfig {
403
485
  * Setup, import, tunable, and metrics configuration lives in a separate
404
486
  * `app.admin.json` file. Use the `admin` field to locate and fetch it.
405
487
  */
488
+ /** A public view's kind: context-aware (tag-tap) vs a full-screen, non-contextual screen. */
489
+ export type PublicViewKind = 'contextual' | 'standalone';
490
+ /** The caller-supplied params a public view expects. */
491
+ export interface PublicViewParams {
492
+ /** Params the view REQUIRES to render (e.g. `['collectionId','pageId']`). */
493
+ required?: string[];
494
+ /** Params the view can use if present (e.g. `['productId','proofId','orientation']`). */
495
+ optional?: string[];
496
+ }
497
+ /**
498
+ * A declared PUBLIC VIEW of the app — one soft-routed entry over the single public bundle
499
+ * (`index.html` → HashRouter), so the platform + Dev Hub can enumerate and target it instead of
500
+ * guessing at undeclared hash routes. A view is `route` + fixed params (`set`) + caller `params` +
501
+ * a `kind`. It is a DELIVERY-agnostic description: the same view renders as a `page` (standalone
502
+ * HTML, hash-routed, its own CSS — embed in an iframe or open directly) or, for a `contextual` view,
503
+ * as a `component` (PublicContainer, props context). NOT a separate build. See
504
+ * docs/design/public-views.md. (Distinct from `linkable`/DeepLinkEntry, which is deep-link
505
+ * discovery; publicViews is the top-level public-entry taxonomy used for preview + tag-tap routing.)
506
+ */
507
+ export interface PublicView {
508
+ /** Stable id, unique within the app. */
509
+ id: string;
510
+ /** Human label (Dev Hub dropdown, platform pickers). */
511
+ title: string;
512
+ /** `contextual` (context-aware, tag-tap target) or `standalone` (full-screen display/kiosk). */
513
+ kind: PublicViewKind;
514
+ /** Hash route within the public bundle. Defaults to `/`. */
515
+ route?: string;
516
+ /** Query params this view PINS (e.g. `{ tvMode: 'true' }`), merged under the caller's params. */
517
+ set?: Record<string, string>;
518
+ /** The params the caller supplies. */
519
+ params?: PublicViewParams;
520
+ /** The default `contextual` view — the tag-tap target. At most one view sets this. */
521
+ default?: boolean;
522
+ }
406
523
  export interface AppManifest {
407
524
  $schema?: string;
408
525
  meta?: {
@@ -491,6 +608,14 @@ export interface AppManifest {
491
608
  * @see DeepLinkEntry
492
609
  */
493
610
  linkable?: DeepLinkEntry[];
611
+ /**
612
+ * The app's PUBLIC VIEWS — the soft-routed entries over the single public bundle
613
+ * (contextual page, display board, kiosk/TV, …), so the platform + Dev Hub can enumerate,
614
+ * preview, and target them. Declares `route` + fixed `set` params + caller `params` + `kind`
615
+ * per view; the `default` contextual view is the tag-tap target. See PublicView +
616
+ * docs/design/public-views.md.
617
+ */
618
+ publicViews?: PublicView[];
494
619
  /**
495
620
  * Executor bundle declaration. Present when the app ships a programmatic executor
496
621
  * for AI-driven configuration, server-side SEO, and LLM content generation.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.21 | Generated: 2026-09-25T17:47:14.766Z
3
+ Version: 2.0.24 | Generated: 2026-09-27T07:52:03.418Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -2132,6 +2132,7 @@ interface AppFunctionDef {
2132
2132
  authority?: AppFunctionAuthority;
2133
2133
  elevated?: boolean;
2134
2134
  capabilities?: string[];
2135
+ dataScope?: 'collection' | 'global';
2135
2136
  apiVersion?: string;
2136
2137
  handler?: string;
2137
2138
  }
@@ -2145,6 +2146,40 @@ interface AppManifestFunctions {
2145
2146
  }
2146
2147
  ```
2147
2148
 
2149
+ **AppDataOpts** (interface)
2150
+ ```typescript
2151
+ interface AppDataOpts {
2152
+ scope?: 'collection' | 'global';
2153
+ productId?: string;
2154
+ variantId?: string;
2155
+ batchId?: string;
2156
+ dataId?: string;
2157
+ queries?: Record<string, any>;
2158
+ }
2159
+ ```
2160
+
2161
+ **AppDataHandle** (interface)
2162
+ ```typescript
2163
+ interface AppDataHandle {
2164
+ get(opts?: AppDataOpts): Promise<any>;
2165
+ set(data: any, opts?: AppDataOpts): Promise<any>;
2166
+ getData(opts?: AppDataOpts): Promise<any>;
2167
+ setData(data: any, opts?: AppDataOpts): Promise<any>;
2168
+ delete(opts?: AppDataOpts): Promise<any>;
2169
+ }
2170
+ ```
2171
+
2172
+ **ServerFunctionSl** (interface)
2173
+ ```typescript
2174
+ interface ServerFunctionSl {
2175
+ appRecords: any;
2176
+ products: any;
2177
+ attestations: any;
2178
+ appData: AppDataHandle & { global: AppDataHandle; collection: AppDataHandle };
2179
+ app(appId: string): { data: Pick<AppDataHandle, 'get' | 'getData'> };
2180
+ }
2181
+ ```
2182
+
2148
2183
  **ServerFunctionCaller** (interface)
2149
2184
  ```typescript
2150
2185
  interface ServerFunctionCaller {
@@ -2161,7 +2196,7 @@ interface ServerFunctionCaller {
2161
2196
  interface ServerFunctionContext {
2162
2197
  collectionId: string;
2163
2198
  appId: string;
2164
- sl: any;
2199
+ sl: ServerFunctionSl;
2165
2200
  secrets: {
2166
2201
  get(ref: string): Promise<string | null>;
2167
2202
  app(ref: string): Promise<string | null>;
@@ -2172,6 +2207,18 @@ interface ServerFunctionContext {
2172
2207
  }
2173
2208
  ```
2174
2209
 
2210
+ **ServerFunctionHttpEvent<TBody = any>** (interface)
2211
+ ```typescript
2212
+ interface ServerFunctionHttpEvent<TBody = any> {
2213
+ method: string;
2214
+ body: TBody;
2215
+ query: Record<string, any>;
2216
+ headers: Record<string, any>;
2217
+ rawBody?: string | Buffer | null;
2218
+ contentType?: string | null;
2219
+ }
2220
+ ```
2221
+
2175
2222
  **AppAdminConfig** (interface)
2176
2223
  ```typescript
2177
2224
  interface AppAdminConfig {
@@ -2233,6 +2280,27 @@ interface AppAdminConfig {
2233
2280
  }
2234
2281
  ```
2235
2282
 
2283
+ **PublicViewParams** (interface)
2284
+ ```typescript
2285
+ interface PublicViewParams {
2286
+ required?: string[];
2287
+ optional?: string[];
2288
+ }
2289
+ ```
2290
+
2291
+ **PublicView** (interface)
2292
+ ```typescript
2293
+ interface PublicView {
2294
+ id: string;
2295
+ title: string;
2296
+ kind: PublicViewKind;
2297
+ route?: string;
2298
+ set?: Record<string, string>;
2299
+ params?: PublicViewParams;
2300
+ default?: boolean;
2301
+ }
2302
+ ```
2303
+
2236
2304
  **AppManifest** (interface)
2237
2305
  ```typescript
2238
2306
  interface AppManifest {
@@ -2267,6 +2335,7 @@ interface AppManifest {
2267
2335
  components: AppContainerComponent[];
2268
2336
  };
2269
2337
  linkable?: DeepLinkEntry[];
2338
+ publicViews?: PublicView[];
2270
2339
  executor?: AppManifestExecutor;
2271
2340
  functions?: AppManifestFunctions;
2272
2341
  [key: string]: any;
@@ -2304,6 +2373,8 @@ interface GetCollectionWidgetsOptions {
2304
2373
 
2305
2374
  **AppFunctionAuthority** = `'caller' | 'collection'`
2306
2375
 
2376
+ **PublicViewKind** = `'contextual' | 'standalone'`
2377
+
2307
2378
  ### appObjects
2308
2379
 
2309
2380
  **PaginatedResponse<T>** (interface)
@@ -8969,7 +9040,7 @@ interface FunctionListResponse {
8969
9040
  **FunctionCallOptions** (interface)
8970
9041
  ```typescript
8971
9042
  interface FunctionCallOptions {
8972
- appId?: string
9043
+ appId?: string; channel?: string
8973
9044
  }
8974
9045
  ```
8975
9046
 
@@ -332,6 +332,40 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
332
332
  | `path` | string | ❌ | Hash route within the app (defaults to `"/"` if omitted) |
333
333
  | `params` | object | ❌ | App-specific query params appended to the URL — do **not** include platform params (`collectionId`, `productId`, etc.) |
334
334
 
335
+ #### `publicViews`
336
+
337
+ Declares the app's **public views** — the soft-routed entries over your single public bundle
338
+ (`index.html` → HashRouter): the contextual page, a display board, a kiosk/TV screen, etc. Without
339
+ this, those routes/modes are invisible to the platform and the Dev Hub. Each view is a `route` + fixed
340
+ `set` params + caller `params` + a `kind`; the `default` **contextual** view is the tag-tap target.
341
+ It's delivery-agnostic — the same view renders as a **page** (standalone, self-CSS, hash-routed, embed
342
+ in an iframe or open directly) or, for a contextual view, as a **component** (`PublicContainer`).
343
+ Not a separate build. (Distinct from `linkable`, which is deep-link discovery.)
344
+
345
+ ```json
346
+ "publicViews": [
347
+ { "id": "page", "title": "Product page", "kind": "contextual", "route": "/", "default": true,
348
+ "params": { "required": ["collectionId"], "optional": ["productId", "proofId"] } },
349
+ { "id": "board", "title": "Display board", "kind": "standalone", "route": "/preview",
350
+ "params": { "required": ["collectionId", "appId", "pageId"], "optional": ["orientation"] } },
351
+ { "id": "tv", "title": "TV / big screen", "kind": "standalone", "route": "/",
352
+ "set": { "tvMode": "true" }, "params": { "required": ["collectionId", "voteId"] } }
353
+ ]
354
+ ```
355
+
356
+ | Field | Type | Required | Description |
357
+ |-------|------|----------|-------------|
358
+ | `id` | string | ✅ | Stable id, unique within the app |
359
+ | `title` | string | ✅ | Human label (Dev Hub dropdown, platform pickers) |
360
+ | `kind` | `"contextual"` \| `"standalone"` | ✅ | Context-aware (tag-tap) vs full-screen, non-contextual |
361
+ | `route` | string | ❌ | Hash route within the public bundle (defaults to `"/"`) |
362
+ | `set` | object | ❌ | Query params this view PINS (e.g. `{ "tvMode": "true" }`), merged under caller params |
363
+ | `params` | `{ required?: string[]; optional?: string[] }` | ❌ | The params the caller supplies |
364
+ | `default` | boolean | ❌ | The default contextual view — the tag-tap target (at most one) |
365
+
366
+ Read context the same way in every delivery with **`SL.readContext(props?)`** (merges props → hash →
367
+ search), instead of hand-rolling the `containerProps || hash || search` chain.
368
+
335
369
  #### `records`
336
370
 
337
371
  Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [App Records Pattern](app-records-pattern.md). Omit if the app does not use scoped records.