@proveanything/smartlinks 2.0.22 → 2.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ export interface SmartLinksContext {
2
+ collectionId?: string;
3
+ appId?: string;
4
+ productId?: string;
5
+ proofId?: string;
6
+ /** Any other declared view params (e.g. pageId, voteId, orientation, tvMode). */
7
+ [key: string]: string | undefined;
8
+ }
9
+ /**
10
+ * Merge the app's context from `overrides` (e.g. container props) → URL hash query → URL search,
11
+ * with the FIRST source winning (props override the URL, hash overrides search). Safe in non-browser
12
+ * environments (returns just the overrides). Empty-string values are kept (a param that was set to "").
13
+ *
14
+ * @example
15
+ * // Works identically whether embedded as a page (URL) or a container (props):
16
+ * const { collectionId, productId } = SL.readContext(containerProps)
17
+ */
18
+ export declare function readContext(overrides?: Record<string, string | undefined>): SmartLinksContext;
@@ -0,0 +1,48 @@
1
+ // src/context.ts
2
+ // Read the app's runtime CONTEXT (collectionId, appId, productId, proofId, and any app-specific
3
+ // params) regardless of how the app was delivered. A public app renders the SAME page whether it is:
4
+ // - a PAGE (index.html / HashRouter) — context arrives in the URL search and/or the hash query, or
5
+ // - a COMPONENT (PublicContainer) — context arrives as props from the parent.
6
+ // This helper merges those sources with a clear precedence so apps stop hand-rolling the
7
+ // `containerProps || hash || search` chain. See docs/design/public-views.md.
8
+ function readSearchParamsInto(out, usp) {
9
+ usp.forEach((value, key) => {
10
+ if (out[key] == null && value != null)
11
+ out[key] = value; // first writer wins
12
+ });
13
+ }
14
+ /**
15
+ * Merge the app's context from `overrides` (e.g. container props) → URL hash query → URL search,
16
+ * with the FIRST source winning (props override the URL, hash overrides search). Safe in non-browser
17
+ * environments (returns just the overrides). Empty-string values are kept (a param that was set to "").
18
+ *
19
+ * @example
20
+ * // Works identically whether embedded as a page (URL) or a container (props):
21
+ * const { collectionId, productId } = SL.readContext(containerProps)
22
+ */
23
+ export function readContext(overrides) {
24
+ const out = {};
25
+ // 1. overrides (container props) win.
26
+ if (overrides) {
27
+ for (const [key, value] of Object.entries(overrides)) {
28
+ if (value != null && out[key] == null)
29
+ out[key] = value;
30
+ }
31
+ }
32
+ try {
33
+ if (typeof window !== 'undefined' && window.location) {
34
+ // 2. hash query (e.g. "#/preview?collectionId=..&pageId=..") — the CDN/hash-router form.
35
+ const hash = window.location.hash || '';
36
+ const q = hash.indexOf('?');
37
+ if (q >= 0)
38
+ readSearchParamsInto(out, new URLSearchParams(hash.slice(q + 1)));
39
+ // 3. search string.
40
+ if (window.location.search)
41
+ readSearchParamsInto(out, new URLSearchParams(window.location.search));
42
+ }
43
+ }
44
+ catch (_a) {
45
+ /* non-browser / bad URL — return what we have */
46
+ }
47
+ return out;
48
+ }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.22 | Generated: 2026-09-25T18:07:22.478Z
3
+ Version: 2.0.25 | Generated: 2026-09-27T12:31:16.701Z
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,25 @@ 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
+ route?: string;
2297
+ set?: Record<string, string>;
2298
+ params?: PublicViewParams;
2299
+ }
2300
+ ```
2301
+
2236
2302
  **AppManifest** (interface)
2237
2303
  ```typescript
2238
2304
  interface AppManifest {
@@ -2267,6 +2333,7 @@ interface AppManifest {
2267
2333
  components: AppContainerComponent[];
2268
2334
  };
2269
2335
  linkable?: DeepLinkEntry[];
2336
+ publicViews?: PublicView[];
2270
2337
  executor?: AppManifestExecutor;
2271
2338
  functions?: AppManifestFunctions;
2272
2339
  [key: string]: any;
@@ -332,6 +332,48 @@ 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** — **standalone, external screens** served over your single public
338
+ bundle (`index.html` → HashRouter): a display board, a kiosk/TV screen, a dashboard, a public share
339
+ page. Without this, those routes/modes are invisible to the platform and the Dev Hub. Each view is a
340
+ `route` + fixed `set` params + caller `params`.
341
+
342
+ > **Public views are NOT portal.** They are completely external and independent. Their only input is
343
+ > **URL parameters** (often a `collectionId`, as a plain param). They expect **no** portal wrapper, **no**
344
+ > host-managed auth/session, and **no** physical-twin context (no QR/NFC/tag scan). They open at a URL
345
+ > and stand alone. For the two **portal** surfaces — where the app renders *inside* portal, and the
346
+ > entry points portal menus link to — see below.
347
+
348
+ ```json
349
+ "publicViews": [
350
+ { "id": "board", "title": "Display board", "route": "/display",
351
+ "params": { "required": ["collectionId"], "optional": ["orientation"] } },
352
+ { "id": "tv", "title": "TV / big screen", "route": "/", "set": { "tvMode": "true" },
353
+ "params": { "required": ["collectionId", "voteId"] } }
354
+ ]
355
+ ```
356
+
357
+ | Field | Type | Required | Description |
358
+ |-------|------|----------|-------------|
359
+ | `id` | string | ✅ | Stable id, unique within the app |
360
+ | `title` | string | ✅ | Human label (Dev Hub dropdown, platform pickers) |
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 via the URL |
364
+
365
+ Read the URL params with **`SL.readContext()`** (merges hash → search) instead of hand-rolling it.
366
+
367
+ ##### Public views vs the two portal surfaces
368
+
369
+ An app can have any combination of three setup styles. Only public views are external:
370
+
371
+ | Style | Declared by | Lives | Context |
372
+ |-------|-------------|-------|---------|
373
+ | **Portal components** — where the app renders inside portal (collection / product / batch / variant / proof) | module registry `components.*` (set at publish) | Inside portal | Physical twin (QR/NFC scan) |
374
+ | **Portal deep links** — the entry points portal menus/tabs/side-menus link to (e.g. list view vs calendar view) | `linkable` / `DeepLinkEntry` | Inside portal | Portal (menu picks the entry) |
375
+ | **Public views** — standalone external screens | `publicViews` (this block) | Outside portal | URL params only |
376
+
335
377
  #### `records`
336
378
 
337
379
  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.
@@ -320,7 +320,8 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
320
320
  | Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
321
321
  | Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
322
322
  | Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
323
- | Read/write general or app-wide data (config, URLs, counters, arrays) | `ctx.sl.appData.get()` / `ctx.sl.appData.set({…})` — pass `{ scope: 'global' }` for app-wide (shared by every install; global writes need collection authority) | `sl:data:read` / `sl:data:write` |
323
+ | Read/write your app's own data (config, URLs, counters, arrays) | `ctx.sl.appData.get()` / `ctx.sl.appData.set({…})` — collection-scoped by default; `ctx.sl.appData.global.*` (or `{ scope:'global' }`, or declare `dataScope:'global'`) for the app-wide bucket. It's your OWN namespace, so **any** authority may read/write it | `sl:data:read` / `sl:data:write` |
324
+ | Read ANOTHER app's data (as the caller may see it) | `ctx.sl.app('other-app').data.get()` — caller-authority, this collection, public-filtered, read-only | `sl:data:read` |
324
325
  | Guarantee a UNIQUE claim (pool of numbers, one-per-user, idempotency key) | `ctx.sl.appRecords.create({ ref: 'ball:57' })` — the DB unique index rejects a duplicate `ref` (catch = "already taken") | `sl:records:write` |
325
326
 
326
327
  Notes:
@@ -403,13 +404,25 @@ from your app").
403
404
  > trusted. When untrusted third-party apps arrive, gate *elevated* (`public` + `collection`)
404
405
  > invocation of a **non-enabled** app behind install/consent — the app-scoped resolver is the seam.
405
406
 
406
- The request body is delivered to the handler as `event.body` (query string as
407
- `event.query`). A function only runs on its own surface — calling an `admin` function on
408
- the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
409
- `{ error, message }` (HTTP 400) if the handler returned an error. On the **admin surface**
410
- the response also includes `logs` (your `ctx.log` lines) and `durationMs` for quick
411
- debugging; the **public surface returns only `result`** (a public caller never sees your
412
- internal logs). `GET` on either endpoint lists the functions callable on that surface.
407
+ The handler receives the request as `event`: `event.body` (parsed JSON), `event.query`,
408
+ `event.headers`, `event.rawBody` (raw bytes, for XML/form/other inputs), and `event.contentType`.
409
+ A function only runs on its own surface — calling an `admin` function on the public endpoint is a
410
+ `403`. `GET` on either endpoint lists the functions callable on that surface.
411
+
412
+ **Response contract — your return IS the response (no envelope):**
413
+ - Return a **plain value** → it becomes the JSON body, HTTP `200`. (`SL.functions.call()` gives you
414
+ that value directly — `res.value`, not `res.result.value`.)
415
+ - Return a web-standard **`Response`** → passed through verbatim: your status, headers, content-type
416
+ and body (XML, CSV, text, binary, redirect, custom status). `Response` is a runtime global.
417
+ ```js
418
+ return new Response(toXml(data), { status: 200, headers: { "content-type": "application/xml" } })
419
+ ```
420
+ - **Throw** → HTTP `500` `{ error: "FUNCTION_ERROR", message, code }`. For *expected* errors (400/404/
421
+ 409…), return a `Response` with your own status + body.
422
+
423
+ One rule: **plain object ⇒ 200; want any other status/headers/content-type ⇒ return a `Response`.**
424
+ Timing comes back in an `X-SL-Function-Duration-Ms` header (admin surface); your `ctx.log` lines land
425
+ in the collection's Errors & Activity feed — the body stays purely your output.
413
426
 
414
427
  The admin surface is collection-admin gated, so an admin function's `caller` authority runs
415
428
  at admin level, attributed to the signed-in admin. The public surface resolves auth if a
@@ -434,7 +447,7 @@ So the end-to-end path is: *write → register the release → enable on a colle
434
447
  Every invocation is recorded as an **execution** activity event, carrying your `ctx.log`
435
448
  lines. Where to look, easiest first:
436
449
 
437
- - **Admin-surface response** — `logs` + `durationMs` come straight back in the JSON.
450
+ - **Admin-surface response** — timing comes back in the `X-SL-Function-Duration-Ms` header (logs are in Errors & Activity, not the body).
438
451
  - **Deployed test mode** — see below; real run, isolated logging.
439
452
  - **Owner console** — the collection's **Advanced → Errors & Activity → events** tab,
440
453
  filtered to **source = execution**: each run shows as `function <name> ok` (or an error),
package/dist/http.d.ts CHANGED
@@ -52,6 +52,14 @@ export declare function initializeApi(options: {
52
52
  * Preserved across re-initialization when not supplied.
53
53
  */
54
54
  appId?: string;
55
+ /**
56
+ * Declares that a bearer token will arrive asynchronously (e.g. handed by the host over
57
+ * postMessage in a dev/direct embed). Until setBearerToken() is called, outgoing requests wait
58
+ * rather than firing unauthenticated. Ignored if a bearerToken is already present.
59
+ */
60
+ awaitAuth?: boolean;
61
+ /** How long (ms) to wait for the async token before letting requests proceed anyway. Default 8000. */
62
+ awaitAuthTimeoutMs?: number;
55
63
  iframeAutoResize?: boolean;
56
64
  logger?: Logger;
57
65
  /**
package/dist/http.js CHANGED
@@ -52,6 +52,16 @@ let grantToken = undefined;
52
52
  * app-scoped when this is set. Explicit `appId` on a call always overrides it.
53
53
  */
54
54
  let appContextId = undefined;
55
+ /**
56
+ * Auth-ready gate. When an app declares (initializeApi({ awaitAuth: true })) that its bearer token
57
+ * will arrive ASYNCHRONOUSLY — e.g. a dev app embedded in the console that receives its token over
58
+ * postMessage — outgoing requests await this until setBearerToken() is called, so the app's first
59
+ * calls don't race the token and 401. Resolved by default (no gating); a timeout resolves it anyway
60
+ * so a token that never arrives can't hang the app forever.
61
+ */
62
+ let authReady = Promise.resolve();
63
+ let resolveAuthReady = null;
64
+ function ensureAuthReady() { return authReady; }
55
65
  /** Whether initializeApi has been successfully called at least once. */
56
66
  let initialized = false;
57
67
  /** Safely returns the current browser hostname, or an empty string in non-browser / Node environments. */
@@ -481,6 +491,18 @@ export function initializeApi(options) {
481
491
  }
482
492
  }
483
493
  // else: preserve the existing runtime bearerToken.
494
+ // Open the auth-ready gate when the caller declares a token is coming but none is set yet, so
495
+ // requests wait for setBearerToken() instead of firing unauthenticated. A timeout releases it.
496
+ if (options.awaitAuth && !bearerToken && !resolveAuthReady) {
497
+ authReady = new Promise((res) => {
498
+ var _a;
499
+ resolveAuthReady = res;
500
+ setTimeout(() => { if (resolveAuthReady) {
501
+ resolveAuthReady = null;
502
+ res();
503
+ } }, (_a = options.awaitAuthTimeoutMs) !== null && _a !== void 0 ? _a : 8000);
504
+ });
505
+ }
484
506
  proxyMode = !!options.proxyMode;
485
507
  // Auto-enable ngrok skip header if domain contains .ngrok.io and user did not explicitly set the flag.
486
508
  // Infer ngrok usage from common domains (.ngrok.io or .ngrok-free.dev)
@@ -530,6 +552,12 @@ export function setExtraHeaders(headers) {
530
552
  * login or logout event.
531
553
  */
532
554
  export function setBearerToken(token) {
555
+ // Release any auth-ready gate the moment a real token arrives (even if the value is unchanged),
556
+ // so requests that were waiting for the async handoff can proceed.
557
+ if (token && resolveAuthReady) {
558
+ resolveAuthReady();
559
+ resolveAuthReady = null;
560
+ }
533
561
  if (token === bearerToken)
534
562
  return;
535
563
  bearerToken = token;
@@ -1189,6 +1217,7 @@ export async function proxyUploadFormData(path, formData, onProgress) {
1189
1217
  * Node-safe: IndexedDB calls are no-ops when IDB is unavailable.
1190
1218
  */
1191
1219
  export async function request(path) {
1220
+ await ensureAuthReady();
1192
1221
  const skipCache = shouldSkipCache(path);
1193
1222
  const cacheKey = buildCacheKey(path);
1194
1223
  const ttl = skipCache ? 0 : getTtlForPath(path);
@@ -1314,6 +1343,7 @@ export async function request(path) {
1314
1343
  * Returns the parsed JSON as T, or throws an Error.
1315
1344
  */
1316
1345
  export async function post(path, body, extraHeaders) {
1346
+ await ensureAuthReady();
1317
1347
  if (proxyMode) {
1318
1348
  logDebug('[smartlinks] POST via proxy', { path, body: safeBodyPreview(body) });
1319
1349
  const result = await proxyRequest("POST", path, body, extraHeaders);
@@ -1375,6 +1405,7 @@ export async function post(path, body, extraHeaders) {
1375
1405
  * Returns the parsed JSON as T, or throws an Error.
1376
1406
  */
1377
1407
  export async function put(path, body, extraHeaders) {
1408
+ await ensureAuthReady();
1378
1409
  if (proxyMode) {
1379
1410
  logDebug('[smartlinks] PUT via proxy', { path, body: safeBodyPreview(body) });
1380
1411
  const result = await proxyRequest("PUT", path, body, extraHeaders);
@@ -1436,6 +1467,7 @@ export async function put(path, body, extraHeaders) {
1436
1467
  * Returns the parsed JSON as T, or throws an Error.
1437
1468
  */
1438
1469
  export async function patch(path, body, extraHeaders) {
1470
+ await ensureAuthReady();
1439
1471
  if (proxyMode) {
1440
1472
  logDebug('[smartlinks] PATCH via proxy', { path, body: safeBodyPreview(body) });
1441
1473
  const result = await proxyRequest("PATCH", path, body, extraHeaders);
@@ -1648,6 +1680,7 @@ export async function requestStream(path, options) {
1648
1680
  * Returns the parsed JSON as T, or throws an Error.
1649
1681
  */
1650
1682
  export async function del(path, extraHeaders) {
1683
+ await ensureAuthReady();
1651
1684
  if (proxyMode) {
1652
1685
  logDebug('[smartlinks] DELETE via proxy', { path });
1653
1686
  const result = await proxyRequest("DELETE", path, undefined, extraHeaders);
package/dist/iframe.d.ts CHANGED
@@ -21,6 +21,20 @@ export declare namespace iframe {
21
21
  export function disableAutoIframeResize(): void;
22
22
  /** Send a custom message to parent (browser-only). */
23
23
  export function sendParentCustom(type: string, payload: Record<string, any>): void;
24
+ /**
25
+ * Ask the embedding host (parent window) to hand this app a bearer token, for DIRECT (non-proxied)
26
+ * mode. Posts `{ type: 'smartlinks:request-auth' }` and resolves with the token from the host's
27
+ * `{ type: 'smartlinks:auth', bearerToken }` reply, or `null` on timeout / when not embedded.
28
+ *
29
+ * Use in a first-party/dev embed where the host owns the session and hands it down (the console dev
30
+ * preview), then feed it to the SDK:
31
+ * SL.initializeApi({ baseURL, proxyMode: false, awaitAuth: true })
32
+ * SL.iframe.requestParentAuth().then(t => t && SL.setBearerToken(t))
33
+ * Untrusted third-party embeds should use proxyMode instead (never hold the user's token).
34
+ */
35
+ export function requestParentAuth(opts?: {
36
+ timeoutMs?: number;
37
+ }): Promise<string | null>;
24
38
  /** Returns true if running inside an iframe (browser). */
25
39
  export function isIframe(): boolean;
26
40
  /** Returns true if ResizeObserver is supported in current environment. */
package/dist/iframe.js CHANGED
@@ -122,6 +122,49 @@ export var iframe;
122
122
  postParentMessage(type, payload);
123
123
  }
124
124
  iframe.sendParentCustom = sendParentCustom;
125
+ /**
126
+ * Ask the embedding host (parent window) to hand this app a bearer token, for DIRECT (non-proxied)
127
+ * mode. Posts `{ type: 'smartlinks:request-auth' }` and resolves with the token from the host's
128
+ * `{ type: 'smartlinks:auth', bearerToken }` reply, or `null` on timeout / when not embedded.
129
+ *
130
+ * Use in a first-party/dev embed where the host owns the session and hands it down (the console dev
131
+ * preview), then feed it to the SDK:
132
+ * SL.initializeApi({ baseURL, proxyMode: false, awaitAuth: true })
133
+ * SL.iframe.requestParentAuth().then(t => t && SL.setBearerToken(t))
134
+ * Untrusted third-party embeds should use proxyMode instead (never hold the user's token).
135
+ */
136
+ function requestParentAuth(opts) {
137
+ if (!inIframe())
138
+ return Promise.resolve(null);
139
+ return new Promise((resolve) => {
140
+ var _a;
141
+ let done = false;
142
+ const finish = (v) => {
143
+ if (done)
144
+ return;
145
+ done = true;
146
+ clearTimeout(timer);
147
+ try {
148
+ window.removeEventListener('message', onMsg);
149
+ }
150
+ catch ( /* ignore */_a) { /* ignore */ }
151
+ resolve(v);
152
+ };
153
+ const onMsg = (e) => {
154
+ const d = e && e.data;
155
+ if (d && d.type === 'smartlinks:auth' && typeof d.bearerToken === 'string' && d.bearerToken) {
156
+ finish(d.bearerToken);
157
+ }
158
+ };
159
+ const timer = setTimeout(() => finish(null), (_a = opts === null || opts === void 0 ? void 0 : opts.timeoutMs) !== null && _a !== void 0 ? _a : 8000);
160
+ try {
161
+ window.addEventListener('message', onMsg);
162
+ }
163
+ catch ( /* ignore */_b) { /* ignore */ }
164
+ postParentMessage('smartlinks:request-auth', {});
165
+ });
166
+ }
167
+ iframe.requestParentAuth = requestParentAuth;
125
168
  /** Returns true if running inside an iframe (browser). */
126
169
  function isIframe() {
127
170
  return inIframe();
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, in
2
2
  export * from "./api/index.js";
3
3
  export * from "./types/index.js";
4
4
  export { iframe } from "./iframe.js";
5
+ export { readContext } from "./context.js";
6
+ export type { SmartLinksContext } from "./context.js";
5
7
  export type { InvalidateCacheOptions } from "./http.js";
6
8
  export * as cache from './cache.js';
7
9
  export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder.js';
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ export * from "./api/index.js";
5
5
  export * from "./types/index.js";
6
6
  // Iframe namespace
7
7
  export { iframe } from "./iframe.js";
8
+ // App context reader (props → hash → search), for pages and containers alike
9
+ export { readContext } from "./context.js";
8
10
  import * as cache_1 from './cache.js';
9
11
  export { cache_1 as cache };
10
12
  // IframeResponder (also exported via iframe namespace)
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,35 @@ 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
+ route:
18484
+ type: string
18485
+ set:
18486
+ type: object
18487
+ additionalProperties:
18488
+ type: string
18489
+ params:
18490
+ $ref: "#/components/schemas/PublicViewParams"
18491
+ required:
18492
+ - id
18493
+ - title
18400
18494
  AppManifest:
18401
18495
  type: object
18402
18496
  properties:
@@ -18472,6 +18566,10 @@ components:
18472
18566
  type: array
18473
18567
  items:
18474
18568
  $ref: "#/components/schemas/DeepLinkEntry"
18569
+ publicViews:
18570
+ type: array
18571
+ items:
18572
+ $ref: "#/components/schemas/PublicView"
18475
18573
  executor:
18476
18574
  $ref: "#/components/schemas/AppManifestExecutor"
18477
18575
  functions:
@@ -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,43 @@ 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
+ /** The caller-supplied params a public view expects. */
489
+ export interface PublicViewParams {
490
+ /** Params the view REQUIRES to render (e.g. `['collectionId','pageId']`). */
491
+ required?: string[];
492
+ /** Params the view can use if present (e.g. `['orientation','theme']`). */
493
+ optional?: string[];
494
+ }
495
+ /**
496
+ * A declared PUBLIC VIEW of the app — a STANDALONE, freeform screen served over the app's single
497
+ * public bundle (`index.html` → HashRouter), enumerable/targetable by the platform + Dev Hub instead
498
+ * of guessing at undeclared hash routes. A view is `route` + fixed params (`set`) + caller `params`.
499
+ *
500
+ * PUBLIC VIEWS ARE NOT PORTAL. They are completely external, independent pages — display boards,
501
+ * projector/kiosk screens, dashboards, public share pages. Their ONLY input is URL parameters (often a
502
+ * `collectionId`, as a plain param). They expect NO portal wrapper, NO host-managed auth/session, and
503
+ * NO physical-twin context (no QR/NFC/tag scan). They open at a URL and stand alone.
504
+ *
505
+ * This is distinct from the two PORTAL-side surfaces, which are declared elsewhere and are context-fed
506
+ * by the physical twin the portal resolves:
507
+ * - WHERE the app renders inside portal (collection/product/batch/variant/proof) → the module
508
+ * registry `components.*` (see prove docs/design/module-registry-fields.md).
509
+ * - The different entry points portal MENUS/TABS/SIDE-MENUS link to (e.g. one app with a list view
510
+ * and a calendar view) → `linkable`/DeepLinkEntry below.
511
+ * See docs/design/public-views.md.
512
+ */
513
+ export interface PublicView {
514
+ /** Stable id, unique within the app. */
515
+ id: string;
516
+ /** Human label (Dev Hub dropdown, platform pickers). */
517
+ title: string;
518
+ /** Hash route within the public bundle. Defaults to `/`. */
519
+ route?: string;
520
+ /** Query params this view PINS (e.g. `{ tvMode: 'true' }`), merged under the caller's params. */
521
+ set?: Record<string, string>;
522
+ /** The params the caller supplies via the URL. */
523
+ params?: PublicViewParams;
524
+ }
406
525
  export interface AppManifest {
407
526
  $schema?: string;
408
527
  meta?: {
@@ -484,13 +603,24 @@ export interface AppManifest {
484
603
  components: AppContainerComponent[];
485
604
  };
486
605
  /**
487
- * Static deep-linkable states built into this app.
488
- * These are fixed routes that exist regardless of content — declared once at build time.
489
- * Dynamic content entries (e.g. CMS pages) are stored separately in `appConfig.linkable`.
490
- * Consumers should merge both sources to get the full set of navigable states.
606
+ * PORTAL deep-linkable states built into this app — the entry points the portal's menu system
607
+ * (bottom menu, side menus, tabs) can link to. Use these when one app has several ways to enter/load
608
+ * its portal component (e.g. a list view and a calendar view, or a viewer and an editor) that
609
+ * different parts of portal should link to independently. These live INSIDE portal.
610
+ * Fixed routes are declared here at build time; dynamic content entries (e.g. CMS pages) are stored
611
+ * separately in `appConfig.linkable` — merge both for the full navigable set.
491
612
  * @see DeepLinkEntry
492
613
  */
493
614
  linkable?: DeepLinkEntry[];
615
+ /**
616
+ * The app's PUBLIC VIEWS — STANDALONE, external screens (display boards, kiosks/TVs, dashboards,
617
+ * public pages) served over the single public bundle, so the platform + Dev Hub can enumerate,
618
+ * preview, and target them. Declares `route` + fixed `set` params + caller `params` per view.
619
+ * NOT portal: no portal wrapper, no host auth/session, no physical-twin context — URL params only.
620
+ * For portal surfaces use `components.*` (where it renders) and `linkable` (portal menu entries).
621
+ * See PublicView + docs/design/public-views.md.
622
+ */
623
+ publicViews?: PublicView[];
494
624
  /**
495
625
  * Executor bundle declaration. Present when the app ships a programmatic executor
496
626
  * 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.22 | Generated: 2026-09-25T18:07:22.478Z
3
+ Version: 2.0.25 | Generated: 2026-09-27T12:31:16.701Z
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,25 @@ 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
+ route?: string;
2297
+ set?: Record<string, string>;
2298
+ params?: PublicViewParams;
2299
+ }
2300
+ ```
2301
+
2236
2302
  **AppManifest** (interface)
2237
2303
  ```typescript
2238
2304
  interface AppManifest {
@@ -2267,6 +2333,7 @@ interface AppManifest {
2267
2333
  components: AppContainerComponent[];
2268
2334
  };
2269
2335
  linkable?: DeepLinkEntry[];
2336
+ publicViews?: PublicView[];
2270
2337
  executor?: AppManifestExecutor;
2271
2338
  functions?: AppManifestFunctions;
2272
2339
  [key: string]: any;
@@ -332,6 +332,48 @@ 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** — **standalone, external screens** served over your single public
338
+ bundle (`index.html` → HashRouter): a display board, a kiosk/TV screen, a dashboard, a public share
339
+ page. Without this, those routes/modes are invisible to the platform and the Dev Hub. Each view is a
340
+ `route` + fixed `set` params + caller `params`.
341
+
342
+ > **Public views are NOT portal.** They are completely external and independent. Their only input is
343
+ > **URL parameters** (often a `collectionId`, as a plain param). They expect **no** portal wrapper, **no**
344
+ > host-managed auth/session, and **no** physical-twin context (no QR/NFC/tag scan). They open at a URL
345
+ > and stand alone. For the two **portal** surfaces — where the app renders *inside* portal, and the
346
+ > entry points portal menus link to — see below.
347
+
348
+ ```json
349
+ "publicViews": [
350
+ { "id": "board", "title": "Display board", "route": "/display",
351
+ "params": { "required": ["collectionId"], "optional": ["orientation"] } },
352
+ { "id": "tv", "title": "TV / big screen", "route": "/", "set": { "tvMode": "true" },
353
+ "params": { "required": ["collectionId", "voteId"] } }
354
+ ]
355
+ ```
356
+
357
+ | Field | Type | Required | Description |
358
+ |-------|------|----------|-------------|
359
+ | `id` | string | ✅ | Stable id, unique within the app |
360
+ | `title` | string | ✅ | Human label (Dev Hub dropdown, platform pickers) |
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 via the URL |
364
+
365
+ Read the URL params with **`SL.readContext()`** (merges hash → search) instead of hand-rolling it.
366
+
367
+ ##### Public views vs the two portal surfaces
368
+
369
+ An app can have any combination of three setup styles. Only public views are external:
370
+
371
+ | Style | Declared by | Lives | Context |
372
+ |-------|-------------|-------|---------|
373
+ | **Portal components** — where the app renders inside portal (collection / product / batch / variant / proof) | module registry `components.*` (set at publish) | Inside portal | Physical twin (QR/NFC scan) |
374
+ | **Portal deep links** — the entry points portal menus/tabs/side-menus link to (e.g. list view vs calendar view) | `linkable` / `DeepLinkEntry` | Inside portal | Portal (menu picks the entry) |
375
+ | **Public views** — standalone external screens | `publicViews` (this block) | Outside portal | URL params only |
376
+
335
377
  #### `records`
336
378
 
337
379
  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.
@@ -320,7 +320,8 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
320
320
  | Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
321
321
  | Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
322
322
  | Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
323
- | Read/write general or app-wide data (config, URLs, counters, arrays) | `ctx.sl.appData.get()` / `ctx.sl.appData.set({…})` — pass `{ scope: 'global' }` for app-wide (shared by every install; global writes need collection authority) | `sl:data:read` / `sl:data:write` |
323
+ | Read/write your app's own data (config, URLs, counters, arrays) | `ctx.sl.appData.get()` / `ctx.sl.appData.set({…})` — collection-scoped by default; `ctx.sl.appData.global.*` (or `{ scope:'global' }`, or declare `dataScope:'global'`) for the app-wide bucket. It's your OWN namespace, so **any** authority may read/write it | `sl:data:read` / `sl:data:write` |
324
+ | Read ANOTHER app's data (as the caller may see it) | `ctx.sl.app('other-app').data.get()` — caller-authority, this collection, public-filtered, read-only | `sl:data:read` |
324
325
  | Guarantee a UNIQUE claim (pool of numbers, one-per-user, idempotency key) | `ctx.sl.appRecords.create({ ref: 'ball:57' })` — the DB unique index rejects a duplicate `ref` (catch = "already taken") | `sl:records:write` |
325
326
 
326
327
  Notes:
@@ -403,13 +404,25 @@ from your app").
403
404
  > trusted. When untrusted third-party apps arrive, gate *elevated* (`public` + `collection`)
404
405
  > invocation of a **non-enabled** app behind install/consent — the app-scoped resolver is the seam.
405
406
 
406
- The request body is delivered to the handler as `event.body` (query string as
407
- `event.query`). A function only runs on its own surface — calling an `admin` function on
408
- the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
409
- `{ error, message }` (HTTP 400) if the handler returned an error. On the **admin surface**
410
- the response also includes `logs` (your `ctx.log` lines) and `durationMs` for quick
411
- debugging; the **public surface returns only `result`** (a public caller never sees your
412
- internal logs). `GET` on either endpoint lists the functions callable on that surface.
407
+ The handler receives the request as `event`: `event.body` (parsed JSON), `event.query`,
408
+ `event.headers`, `event.rawBody` (raw bytes, for XML/form/other inputs), and `event.contentType`.
409
+ A function only runs on its own surface — calling an `admin` function on the public endpoint is a
410
+ `403`. `GET` on either endpoint lists the functions callable on that surface.
411
+
412
+ **Response contract — your return IS the response (no envelope):**
413
+ - Return a **plain value** → it becomes the JSON body, HTTP `200`. (`SL.functions.call()` gives you
414
+ that value directly — `res.value`, not `res.result.value`.)
415
+ - Return a web-standard **`Response`** → passed through verbatim: your status, headers, content-type
416
+ and body (XML, CSV, text, binary, redirect, custom status). `Response` is a runtime global.
417
+ ```js
418
+ return new Response(toXml(data), { status: 200, headers: { "content-type": "application/xml" } })
419
+ ```
420
+ - **Throw** → HTTP `500` `{ error: "FUNCTION_ERROR", message, code }`. For *expected* errors (400/404/
421
+ 409…), return a `Response` with your own status + body.
422
+
423
+ One rule: **plain object ⇒ 200; want any other status/headers/content-type ⇒ return a `Response`.**
424
+ Timing comes back in an `X-SL-Function-Duration-Ms` header (admin surface); your `ctx.log` lines land
425
+ in the collection's Errors & Activity feed — the body stays purely your output.
413
426
 
414
427
  The admin surface is collection-admin gated, so an admin function's `caller` authority runs
415
428
  at admin level, attributed to the signed-in admin. The public surface resolves auth if a
@@ -434,7 +447,7 @@ So the end-to-end path is: *write → register the release → enable on a colle
434
447
  Every invocation is recorded as an **execution** activity event, carrying your `ctx.log`
435
448
  lines. Where to look, easiest first:
436
449
 
437
- - **Admin-surface response** — `logs` + `durationMs` come straight back in the JSON.
450
+ - **Admin-surface response** — timing comes back in the `X-SL-Function-Duration-Ms` header (logs are in Errors & Activity, not the body).
438
451
  - **Deployed test mode** — see below; real run, isolated logging.
439
452
  - **Owner console** — the collection's **Advanced → Errors & Activity → events** tab,
440
453
  filtered to **source = execution**: each run shows as `function <name> ok` (or an error),
package/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,35 @@ 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
+ route:
18484
+ type: string
18485
+ set:
18486
+ type: object
18487
+ additionalProperties:
18488
+ type: string
18489
+ params:
18490
+ $ref: "#/components/schemas/PublicViewParams"
18491
+ required:
18492
+ - id
18493
+ - title
18400
18494
  AppManifest:
18401
18495
  type: object
18402
18496
  properties:
@@ -18472,6 +18566,10 @@ components:
18472
18566
  type: array
18473
18567
  items:
18474
18568
  $ref: "#/components/schemas/DeepLinkEntry"
18569
+ publicViews:
18570
+ type: array
18571
+ items:
18572
+ $ref: "#/components/schemas/PublicView"
18475
18573
  executor:
18476
18574
  $ref: "#/components/schemas/AppManifestExecutor"
18477
18575
  functions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.22",
3
+ "version": "2.0.25",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -65,8 +65,7 @@
65
65
  "author": "Glenn Shoosmith",
66
66
  "license": "MIT",
67
67
  "publishConfig": {
68
- "access": "public",
69
- "tag": "next"
68
+ "access": "public"
70
69
  },
71
70
  "dependencies": {
72
71
  "cross-fetch": "^3.1.5",