@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/api/functions.d.ts +8 -1
- package/dist/api/functions.js +11 -8
- package/dist/context.d.ts +18 -0
- package/dist/context.js +48 -0
- package/dist/docs/API_SUMMARY.md +74 -3
- package/dist/docs/app-manifest.md +34 -0
- package/dist/docs/server-functions.md +51 -16
- package/dist/http.d.ts +8 -0
- package/dist/http.js +33 -0
- package/dist/iframe.d.ts +14 -0
- package/dist/iframe.js +43 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/openapi.yaml +111 -2
- package/dist/testing/index.d.ts +19 -0
- package/dist/testing/index.js +30 -0
- package/dist/types/appManifest.d.ts +127 -2
- package/docs/API_SUMMARY.md +74 -3
- package/docs/app-manifest.md +34 -0
- package/docs/server-functions.md +51 -16
- package/openapi.yaml +111 -2
- package/package.json +1 -1
package/dist/api/functions.d.ts
CHANGED
|
@@ -8,9 +8,16 @@ export interface FunctionListEntry {
|
|
|
8
8
|
export interface FunctionListResponse {
|
|
9
9
|
functions: FunctionListEntry[];
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
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
|
/**
|
package/dist/api/functions.js
CHANGED
|
@@ -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,
|
|
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 &&
|
|
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
|
|
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
|
|
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;
|
package/dist/context.js
ADDED
|
@@ -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
|
+
}
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
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:
|
|
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
|
|
253
|
-
with `409 AMBIGUOUS_FUNCTION`** when more than one
|
|
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
|
|
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
|
|
381
|
-
`409 AMBIGUOUS_FUNCTION` when two apps define the same name. Prefer the
|
|
382
|
-
emits it automatically once an appId is set — see "Calling a function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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** —
|
|
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)
|