@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.
@@ -8,9 +8,16 @@ export interface FunctionListEntry {
8
8
  export interface FunctionListResponse {
9
9
  functions: FunctionListEntry[];
10
10
  }
11
- /** Options for a function call. `appId` scopes resolution to one app (recommended). */
11
+ /**
12
+ * Options for a function call.
13
+ * - `appId` scopes resolution to one app (recommended; falls back to the SDK app context).
14
+ * - `channel` selects which release to run when addressing an app that is NOT enabled on the
15
+ * collection (enablement isn't required — the app is resolved directly by id). Defaults to
16
+ * `stable` server-side, so pass `channel: 'dev'` to test a dev build before installing it.
17
+ */
12
18
  export interface FunctionCallOptions {
13
19
  appId?: string;
20
+ channel?: string;
14
21
  }
15
22
  export declare namespace functions {
16
23
  /**
@@ -14,13 +14,15 @@
14
14
  // which the server resolves by bare name and REJECTS with 409 AMBIGUOUS_FUNCTION when more than one
15
15
  // installed app defines that name. Always prefer an appId.
16
16
  import { post, request, getAppContext } from "../http.js";
17
- function fnPath(surface, collectionId, name, appId) {
17
+ function fnPath(surface, collectionId, name, opts = {}) {
18
+ var _a;
18
19
  const c = encodeURIComponent(collectionId);
19
20
  const n = encodeURIComponent(name);
20
- const app = appId !== null && appId !== void 0 ? appId : getAppContext();
21
+ const app = (_a = opts.appId) !== null && _a !== void 0 ? _a : getAppContext();
22
+ const q = opts.channel ? `?channel=${encodeURIComponent(opts.channel)}` : '';
21
23
  return app
22
- ? `/${surface}/collection/${c}/app/${encodeURIComponent(app)}/functions/${n}`
23
- : `/${surface}/collection/${c}/functions/${n}`; // deprecated flat alias
24
+ ? `/${surface}/collection/${c}/app/${encodeURIComponent(app)}/functions/${n}${q}`
25
+ : `/${surface}/collection/${c}/functions/${n}${q}`; // deprecated flat alias
24
26
  }
25
27
  export var functions;
26
28
  (function (functions) {
@@ -35,7 +37,7 @@ export var functions;
35
37
  * await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app' })
36
38
  */
37
39
  async function call(collectionId, name, body = {}, opts = {}) {
38
- return post(fnPath('public', collectionId, name, opts.appId), body);
40
+ return post(fnPath('public', collectionId, name, opts), body);
39
41
  }
40
42
  functions.call = call;
41
43
  /**
@@ -43,7 +45,7 @@ export var functions;
43
45
  * App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
44
46
  */
45
47
  async function callAdmin(collectionId, name, body = {}, opts = {}) {
46
- return post(fnPath('admin', collectionId, name, opts.appId), body);
48
+ return post(fnPath('admin', collectionId, name, opts), body);
47
49
  }
48
50
  functions.callAdmin = callAdmin;
49
51
  /**
@@ -54,9 +56,10 @@ export var functions;
54
56
  var _a;
55
57
  const c = encodeURIComponent(collectionId);
56
58
  const app = (_a = opts.appId) !== null && _a !== void 0 ? _a : getAppContext();
59
+ const q = opts.channel ? `?channel=${encodeURIComponent(opts.channel)}` : '';
57
60
  const path = app
58
- ? `/public/collection/${c}/app/${encodeURIComponent(app)}/functions`
59
- : `/public/collection/${c}/functions`;
61
+ ? `/public/collection/${c}/app/${encodeURIComponent(app)}/functions${q}`
62
+ : `/public/collection/${c}/functions${q}`;
60
63
  return request(path);
61
64
  }
62
65
  functions.list = list;
@@ -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.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.
@@ -248,10 +248,21 @@ To call a *different* app's function, pass the appId explicitly:
248
248
  await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'some-other-app' })
249
249
  ```
250
250
 
251
+ **The app does NOT have to be enabled on the collection.** Because the appId is explicit, the
252
+ function is resolved directly from the app's release — so you can test an app on any collection
253
+ before installing it (and without it showing up in that collection's menus). Enabling an app is
254
+ currently just a UX courtesy (dropdowns/menus), not a gate on running its functions. For a build
255
+ that isn't the default `stable` channel — e.g. a **dev** app you haven't installed — pass the
256
+ channel:
257
+
258
+ ```ts
259
+ await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app', channel: 'dev' })
260
+ ```
261
+
251
262
  If no appId is available (not set on init, none passed), the call falls back to the **deprecated
252
- flat path** `/collection/:c/functions/:name`, which the server resolves by bare name and **rejects
253
- with `409 AMBIGUOUS_FUNCTION`** when more than one installed app defines that name (a first-party
254
- builtin still wins). Always prefer an appId.
263
+ flat path** `/collection/:c/functions/:name`, which searches only the collection's **enabled** apps,
264
+ resolves by bare name, and **rejects with `409 AMBIGUOUS_FUNCTION`** when more than one defines that
265
+ name (a first-party builtin still wins). Always prefer an appId.
255
266
 
256
267
  > There is no such thing as an app-less function. First-party builtins (e.g. the `functions.ping`
257
268
  > diagnostic) belong to the reserved `smartlinks` app. A "global" function is just an app enabled on
@@ -309,7 +320,8 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
309
320
  | Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
310
321
  | Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
311
322
  | Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
312
- | 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` |
313
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` |
314
326
 
315
327
  Notes:
@@ -376,18 +388,41 @@ POST /public/collection/:collectionId/app/:appId/functions/:name # visibility:
376
388
  GET /{admin|public}/collection/:collectionId/app/:appId/functions # list this app's functions on the surface
377
389
  ```
378
390
 
391
+ The app-scoped route resolves the app **directly by id**, so the app need not be enabled on the
392
+ collection — handy for testing an un-installed (dev) app. Add `?channel=dev` (default `stable`) to
393
+ pick the release. Enablement (`appConfig.apps[]`) currently only controls menus/discovery, not
394
+ whether a function can run.
395
+
379
396
  The bare-name form (`/collection/:c/functions/:name`, no `/app/:appId`) is a **deprecated alias**:
380
- it resolves by name across all installed apps, lets a first-party builtin win, and returns
381
- `409 AMBIGUOUS_FUNCTION` when two apps define the same name. Prefer the app-scoped route (the SDK
382
- emits it automatically once an appId is set — see "Calling a function from your app").
383
-
384
- The request body is delivered to the handler as `event.body` (query string as
385
- `event.query`). A function only runs on its own surface — calling an `admin` function on
386
- the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
387
- `{ error, message }` (HTTP 400) if the handler returned an error. On the **admin surface**
388
- the response also includes `logs` (your `ctx.log` lines) and `durationMs` for quick
389
- debugging; the **public surface returns only `result`** (a public caller never sees your
390
- internal logs). `GET` on either endpoint lists the functions callable on that surface.
397
+ it searches only the collection's **enabled** apps, resolves by name, lets a first-party builtin
398
+ win, and returns `409 AMBIGUOUS_FUNCTION` when two enabled apps define the same name. Prefer the
399
+ app-scoped route (the SDK emits it automatically once an appId is set — see "Calling a function
400
+ from your app").
401
+
402
+ > **Security note (first-party model, today):** because enablement is not an auth gate, any app's
403
+ > function can be invoked on any collection by id. That's fine while all apps are first-party and
404
+ > trusted. When untrusted third-party apps arrive, gate *elevated* (`public` + `collection`)
405
+ > invocation of a **non-enabled** app behind install/consent — the app-scoped resolver is the seam.
406
+
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.
391
426
 
392
427
  The admin surface is collection-admin gated, so an admin function's `caller` authority runs
393
428
  at admin level, attributed to the signed-in admin. The public surface resolves auth if a
@@ -412,7 +447,7 @@ So the end-to-end path is: *write → register the release → enable on a colle
412
447
  Every invocation is recorded as an **execution** activity event, carrying your `ctx.log`
413
448
  lines. Where to look, easiest first:
414
449
 
415
- - **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).
416
451
  - **Deployed test mode** — see below; real run, isolated logging.
417
452
  - **Owner console** — the collection's **Advanced → Errors & Activity → events** tab,
418
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)