@proveanything/smartlinks 2.0.20 → 2.0.22

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,20 +8,37 @@ export interface FunctionListEntry {
8
8
  export interface FunctionListResponse {
9
9
  functions: FunctionListEntry[];
10
10
  }
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
+ */
18
+ export interface FunctionCallOptions {
19
+ appId?: string;
20
+ channel?: string;
21
+ }
11
22
  export declare namespace functions {
12
23
  /**
13
- * Call a PUBLIC app server function inline.
14
- * `POST /public/collection/:collectionId/functions/:name` — surface `'public'`.
24
+ * Call a PUBLIC app server function inline (surface `'public'`).
25
+ * App-scoped: `POST /public/collection/:c/app/:appId/functions/:name`.
15
26
  *
16
27
  * @example
17
- * const { value } = await SL.functions.call<{ value: number }>(collectionId, 'increment')
28
+ * // App calling its own function (appId from initializeApi({ appId })):
29
+ * const { value } = await SL.functions.call<{ value: number }>(collectionId, 'pressCounter')
30
+ * // Or address another app explicitly:
31
+ * await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app' })
18
32
  */
19
- function call<T = FunctionCallResult>(collectionId: string, name: string, body?: Record<string, any>): Promise<T>;
33
+ function call<T = FunctionCallResult>(collectionId: string, name: string, body?: Record<string, any>, opts?: FunctionCallOptions): Promise<T>;
20
34
  /**
21
35
  * Call an ADMIN app server function (surface `'admin'`; requires an admin session).
22
- * `POST /admin/collection/:collectionId/functions/:name`.
36
+ * App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
37
+ */
38
+ function callAdmin<T = FunctionCallResult>(collectionId: string, name: string, body?: Record<string, any>, opts?: FunctionCallOptions): Promise<T>;
39
+ /**
40
+ * List the public functions available for a collection (discovery). Scoped to one app when an
41
+ * appId is given (or set as the SDK app context): `GET /public/collection/:c[/app/:appId]/functions`.
23
42
  */
24
- function callAdmin<T = FunctionCallResult>(collectionId: string, name: string, body?: Record<string, any>): Promise<T>;
25
- /** List the public functions available for a collection (discovery). `GET /public/collection/:c/functions`. */
26
- function list(collectionId: string): Promise<FunctionListResponse>;
43
+ function list(collectionId: string, opts?: FunctionCallOptions): Promise<FunctionListResponse>;
27
44
  }
@@ -5,33 +5,61 @@
5
5
  // client half. The PUBLIC route runs a function inline on the caller's own authority (owner if
6
6
  // signed in via authKit, else public); the ADMIN route runs on the admin surface (needs an admin
7
7
  // session). The returned value is the function's own result (`event.body` is what you pass here).
8
- import { post, request } from "../http.js";
8
+ //
9
+ // ADDRESSING. A function always belongs to an app, so the canonical route is app-scoped:
10
+ // /collection/:c/app/:appId/functions/:name
11
+ // The appId comes from `opts.appId`, else the SDK's app context (initializeApi({ appId })) — so an
12
+ // app calling its OWN function just writes `SL.functions.call(collectionId, name)`. With no appId
13
+ // available, the call falls back to the DEPRECATED flat path `/collection/:c/functions/:name`,
14
+ // which the server resolves by bare name and REJECTS with 409 AMBIGUOUS_FUNCTION when more than one
15
+ // installed app defines that name. Always prefer an appId.
16
+ import { post, request, getAppContext } from "../http.js";
17
+ function fnPath(surface, collectionId, name, opts = {}) {
18
+ var _a;
19
+ const c = encodeURIComponent(collectionId);
20
+ const n = encodeURIComponent(name);
21
+ const app = (_a = opts.appId) !== null && _a !== void 0 ? _a : getAppContext();
22
+ const q = opts.channel ? `?channel=${encodeURIComponent(opts.channel)}` : '';
23
+ return app
24
+ ? `/${surface}/collection/${c}/app/${encodeURIComponent(app)}/functions/${n}${q}`
25
+ : `/${surface}/collection/${c}/functions/${n}${q}`; // deprecated flat alias
26
+ }
9
27
  export var functions;
10
28
  (function (functions) {
11
29
  /**
12
- * Call a PUBLIC app server function inline.
13
- * `POST /public/collection/:collectionId/functions/:name` — surface `'public'`.
30
+ * Call a PUBLIC app server function inline (surface `'public'`).
31
+ * App-scoped: `POST /public/collection/:c/app/:appId/functions/:name`.
14
32
  *
15
33
  * @example
16
- * const { value } = await SL.functions.call<{ value: number }>(collectionId, 'increment')
34
+ * // App calling its own function (appId from initializeApi({ appId })):
35
+ * const { value } = await SL.functions.call<{ value: number }>(collectionId, 'pressCounter')
36
+ * // Or address another app explicitly:
37
+ * await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app' })
17
38
  */
18
- async function call(collectionId, name, body = {}) {
19
- const path = `/public/collection/${encodeURIComponent(collectionId)}/functions/${encodeURIComponent(name)}`;
20
- return post(path, body);
39
+ async function call(collectionId, name, body = {}, opts = {}) {
40
+ return post(fnPath('public', collectionId, name, opts), body);
21
41
  }
22
42
  functions.call = call;
23
43
  /**
24
44
  * Call an ADMIN app server function (surface `'admin'`; requires an admin session).
25
- * `POST /admin/collection/:collectionId/functions/:name`.
45
+ * App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
26
46
  */
27
- async function callAdmin(collectionId, name, body = {}) {
28
- const path = `/admin/collection/${encodeURIComponent(collectionId)}/functions/${encodeURIComponent(name)}`;
29
- return post(path, body);
47
+ async function callAdmin(collectionId, name, body = {}, opts = {}) {
48
+ return post(fnPath('admin', collectionId, name, opts), body);
30
49
  }
31
50
  functions.callAdmin = callAdmin;
32
- /** List the public functions available for a collection (discovery). `GET /public/collection/:c/functions`. */
33
- async function list(collectionId) {
34
- const path = `/public/collection/${encodeURIComponent(collectionId)}/functions`;
51
+ /**
52
+ * List the public functions available for a collection (discovery). Scoped to one app when an
53
+ * appId is given (or set as the SDK app context): `GET /public/collection/:c[/app/:appId]/functions`.
54
+ */
55
+ async function list(collectionId, opts = {}) {
56
+ var _a;
57
+ const c = encodeURIComponent(collectionId);
58
+ const app = (_a = opts.appId) !== null && _a !== void 0 ? _a : getAppContext();
59
+ const q = opts.channel ? `?channel=${encodeURIComponent(opts.channel)}` : '';
60
+ const path = app
61
+ ? `/public/collection/${c}/app/${encodeURIComponent(app)}/functions${q}`
62
+ : `/public/collection/${c}/functions${q}`;
35
63
  return request(path);
36
64
  }
37
65
  functions.list = list;
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.20 | Generated: 2026-09-25T11:48:23.950Z
3
+ Version: 2.0.22 | Generated: 2026-09-25T18:07:22.478Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -166,6 +166,12 @@ Reset the diagnostics counters (does not touch the cache itself).
166
166
  **isProxyEnabled**() → `boolean`
167
167
  Return whether proxy mode is currently enabled.
168
168
 
169
+ **getAppContext**() → `string | undefined`
170
+ The current app context (appId), if the SDK was initialized with one.
171
+
172
+ **setAppContext**(id: string | undefined) → `void`
173
+ Set (or clear) the current app context — the appId used to scope SL.functions calls.
174
+
169
175
  **initializeApi**(options: {
170
176
  baseURL: string
171
177
  apiKey?: string
@@ -8960,6 +8966,13 @@ interface FunctionListResponse {
8960
8966
  }
8961
8967
  ```
8962
8968
 
8969
+ **FunctionCallOptions** (interface)
8970
+ ```typescript
8971
+ interface FunctionCallOptions {
8972
+ appId?: string; channel?: string
8973
+ }
8974
+ ```
8975
+
8963
8976
  **FunctionCallResult** = `any`
8964
8977
 
8965
8978
  ### sequence (api)
@@ -10458,16 +10471,18 @@ Delete a form for a collection (admin only).
10458
10471
 
10459
10472
  **call**(collectionId: string,
10460
10473
  name: string,
10461
- body: Record<string, any> = {}) → `Promise<T>`
10462
- Call a PUBLIC app server function inline. `POST /public/collection/:collectionId/functions/:name` — surface `'public'`. const { value } = await SL.functions.call<{ value: number }>(collectionId, 'increment')
10474
+ body: Record<string, any> = {},
10475
+ opts: FunctionCallOptions = {}) → `Promise<T>`
10476
+ Call a PUBLIC app server function inline (surface `'public'`). App-scoped: `POST /public/collection/:c/app/:appId/functions/:name`. // App calling its own function (appId from initializeApi({ appId })): const { value } = await SL.functions.call<{ value: number }>(collectionId, 'pressCounter') // Or address another app explicitly: await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app' })
10463
10477
 
10464
10478
  **callAdmin**(collectionId: string,
10465
10479
  name: string,
10466
- body: Record<string, any> = {}) → `Promise<T>`
10467
- Call an ADMIN app server function (surface `'admin'`; requires an admin session). `POST /admin/collection/:collectionId/functions/:name`.
10480
+ body: Record<string, any> = {},
10481
+ opts: FunctionCallOptions = {}) → `Promise<T>`
10482
+ Call an ADMIN app server function (surface `'admin'`; requires an admin session). App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
10468
10483
 
10469
- **list**(collectionId: string) → `Promise<FunctionListResponse>`
10470
- List the public functions available for a collection (discovery). `GET /public/collection/:c/functions`.
10484
+ **list**(collectionId: string, opts: FunctionCallOptions = {}) → `Promise<FunctionListResponse>`
10485
+ List the public functions available for a collection (discovery). Scoped to one app when an appId is given (or set as the SDK app context): `GET /public/collection/:c[/app/:appId]/functions`.
10471
10486
 
10472
10487
  ### http
10473
10488
 
@@ -181,11 +181,14 @@ interface ServerFunctionContext {
181
181
  collectionId: string
182
182
  appId: string
183
183
 
184
- /** SmartLinks SDK, pre-scoped to your declared `authority`. Capabilities cap what it may do. */
184
+ /** SmartLinks SDK, pre-scoped to your declared `authority`. Capabilities cap what it may do.
185
+ * Key resources: `sl.appRecords` (per-app records), `sl.appData` (general/app-wide config +
186
+ * data — URLs, flags, counters, arrays), `sl.products`, `sl.attestations`. */
185
187
  sl: SmartLinks
186
188
 
187
- /** Capability-gated secrets. Resolves only refs you declared via `secrets:<ref>`. */
188
- secrets: { get(ref: string): Promise<string | null> }
189
+ /** Capability-gated secrets (need `secrets:<ref>`). `get(ref)` resolves the collection's OWN
190
+ * secret first, then the app-level one; `app(ref)` reads the app-level secret only. */
191
+ secrets: { get(ref: string): Promise<string | null>; app(ref: string): Promise<string | null> }
189
192
 
190
193
  /** Who invoked this function. */
191
194
  caller: {
@@ -211,6 +214,61 @@ already authenticated as your declared authority and scoped to the collection. D
211
214
  to construct your own SDK client or carry your own key; that's what `ctx.sl` is for, and it's
212
215
  the only way authority stays correct.
213
216
 
217
+ ### Calling a function from your app (client side)
218
+
219
+ From a page/UI, invoke an `http`-trigger function with the SDK — no manual fetch, no URL building:
220
+
221
+ ```ts
222
+ // public function (runs on the caller's authority — signed-in owner, else public):
223
+ const res = await SL.functions.call(collectionId, 'increment', { /* becomes event.body */ })
224
+ // admin surface (needs an admin session):
225
+ const res = await SL.functions.callAdmin(collectionId, 'recomputeTotals', { … })
226
+ ```
227
+
228
+ `res` is whatever your handler returns. See the `edge-function-test` example for a full page +
229
+ function + manifest.
230
+
231
+ #### Functions are ALWAYS app-scoped
232
+
233
+ A function belongs to an app, so the canonical route carries the appId:
234
+ `POST /collection/:collectionId/app/:appId/functions/:name`. Two different installed apps can each
235
+ ship a function of the same name without clashing.
236
+
237
+ The SDK fills the appId in for you. Initialize with your app's id once, then call by bare name —
238
+ the SDK routes it app-scoped:
239
+
240
+ ```ts
241
+ SL.initializeApi({ baseURL, appId: 'my-counter-app' }) // usually done by the host/bootstrap
242
+ await SL.functions.call(collectionId, 'pressCounter') // → /collection/:c/app/my-counter-app/functions/pressCounter
243
+ ```
244
+
245
+ To call a *different* app's function, pass the appId explicitly:
246
+
247
+ ```ts
248
+ await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'some-other-app' })
249
+ ```
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
+
262
+ If no appId is available (not set on init, none passed), the call falls back to the **deprecated
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.
266
+
267
+ > There is no such thing as an app-less function. First-party builtins (e.g. the `functions.ping`
268
+ > diagnostic) belong to the reserved `smartlinks` app. A "global" function is just an app enabled on
269
+ > the `global` collection and called at `/collection/global/app/:appId/functions/:name` — same two
270
+ > facets (collection + app), with `global` as the sentinel collection.
271
+
214
272
  ---
215
273
 
216
274
  ## Runtime — what your function can use
@@ -262,6 +320,8 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
262
320
  | Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
263
321
  | Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
264
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` |
324
+ | 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` |
265
325
 
266
326
  Notes:
267
327
  - **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
@@ -318,14 +378,31 @@ other secrets, or any other collection.
318
378
 
319
379
  ## Invoking an http function
320
380
 
321
- An `http` function is called by POSTing to the collection's functions endpoint on the
381
+ An `http` function is called by POSTing to the app-scoped functions endpoint on the
322
382
  surface that matches its `visibility`:
323
383
 
324
384
  ```
325
- POST /admin/collection/:collectionId/functions/:name # visibility: admin (collection-admin auth)
326
- POST /public/collection/:collectionId/functions/:name # visibility: public (auth optional)
385
+ POST /admin/collection/:collectionId/app/:appId/functions/:name # visibility: admin (collection-admin auth)
386
+ POST /public/collection/:collectionId/app/:appId/functions/:name # visibility: public (auth optional)
387
+ GET /{admin|public}/collection/:collectionId/app/:appId/functions # list this app's functions on the surface
327
388
  ```
328
389
 
390
+ The app-scoped route resolves the app **directly by id**, so the app need not be enabled on the
391
+ collection — handy for testing an un-installed (dev) app. Add `?channel=dev` (default `stable`) to
392
+ pick the release. Enablement (`appConfig.apps[]`) currently only controls menus/discovery, not
393
+ whether a function can run.
394
+
395
+ The bare-name form (`/collection/:c/functions/:name`, no `/app/:appId`) is a **deprecated alias**:
396
+ it searches only the collection's **enabled** apps, resolves by name, lets a first-party builtin
397
+ win, and returns `409 AMBIGUOUS_FUNCTION` when two enabled apps define the same name. Prefer the
398
+ app-scoped route (the SDK emits it automatically once an appId is set — see "Calling a function
399
+ from your app").
400
+
401
+ > **Security note (first-party model, today):** because enablement is not an auth gate, any app's
402
+ > function can be invoked on any collection by id. That's fine while all apps are first-party and
403
+ > trusted. When untrusted third-party apps arrive, gate *elevated* (`public` + `collection`)
404
+ > invocation of a **non-enabled** app behind install/consent — the app-scoped resolver is the seam.
405
+
329
406
  The request body is delivered to the handler as `event.body` (query string as
330
407
  `event.query`). A function only runs on its own surface — calling an `admin` function on
331
408
  the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
package/dist/http.d.ts CHANGED
@@ -27,6 +27,10 @@ export declare function getHttpCacheDiagnostics(): {
27
27
  export declare function resetHttpCacheDiagnostics(): void;
28
28
  /** Return whether proxy mode is currently enabled. */
29
29
  export declare function isProxyEnabled(): boolean;
30
+ /** The current app context (appId), if the SDK was initialized with one. */
31
+ export declare function getAppContext(): string | undefined;
32
+ /** Set (or clear) the current app context — the appId used to scope SL.functions calls. */
33
+ export declare function setAppContext(id: string | undefined): void;
30
34
  export declare function initializeApi(options: {
31
35
  baseURL: string;
32
36
  apiKey?: string;
@@ -42,6 +46,12 @@ export declare function initializeApi(options: {
42
46
  * across re-initialization when not supplied.
43
47
  */
44
48
  platform?: 'native' | 'web';
49
+ /**
50
+ * The appId of the micro-app initializing the SDK. When set, `SL.functions.call(...)` and
51
+ * `callAdmin(...)` resolve app-scoped by default (no need to pass appId on every call).
52
+ * Preserved across re-initialization when not supplied.
53
+ */
54
+ appId?: string;
45
55
  iframeAutoResize?: boolean;
46
56
  logger?: Logger;
47
57
  /**
package/dist/http.js CHANGED
@@ -45,6 +45,13 @@ let clientPlatform = undefined;
45
45
  * data call that touches the granted proof (attestations, threads, app data).
46
46
  */
47
47
  let grantToken = undefined;
48
+ /**
49
+ * The current app context — the appId of the micro-app this SDK instance belongs to.
50
+ * Set via initializeApi({ appId }) or setAppContext(). Lets an app call its OWN server
51
+ * functions without repeating its id: SL.functions.call(collectionId, name) resolves
52
+ * app-scoped when this is set. Explicit `appId` on a call always overrides it.
53
+ */
54
+ let appContextId = undefined;
48
55
  /** Whether initializeApi has been successfully called at least once. */
49
56
  let initialized = false;
50
57
  /** Safely returns the current browser hostname, or an empty string in non-browser / Node environments. */
@@ -293,6 +300,14 @@ function logDebug(...args) {
293
300
  export function isProxyEnabled() {
294
301
  return proxyMode;
295
302
  }
303
+ /** The current app context (appId), if the SDK was initialized with one. */
304
+ export function getAppContext() {
305
+ return appContextId;
306
+ }
307
+ /** Set (or clear) the current app context — the appId used to scope SL.functions calls. */
308
+ export function setAppContext(id) {
309
+ appContextId = id;
310
+ }
296
311
  function maskSensitive(value) {
297
312
  if (!value)
298
313
  return value;
@@ -443,6 +458,9 @@ export function initializeApi(options) {
443
458
  }
444
459
  baseURL = normalizedBaseURL;
445
460
  apiKey = options.apiKey;
461
+ // Preserve the app context across re-inits that omit it (mirrors platform/token handling).
462
+ if (options.appId !== undefined)
463
+ appContextId = options.appId;
446
464
  // Enable token persistence before restoring the token.
447
465
  if (options.persistToken !== undefined)
448
466
  tokenPersistenceEnabled = options.persistToken;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, getHttpCacheDiagnostics, resetHttpCacheDiagnostics, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, getBaseURL, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken } from "./http.js";
1
+ export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, getHttpCacheDiagnostics, resetHttpCacheDiagnostics, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, getBaseURL, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken, getAppContext, setAppContext } from "./http.js";
2
2
  export * from "./api/index.js";
3
3
  export * from "./types/index.js";
4
4
  export { iframe } from "./iframe.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  // Top-level entrypoint of the npm package. Re-export initializeApi + all namespaces.
3
- export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, getHttpCacheDiagnostics, resetHttpCacheDiagnostics, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, getBaseURL, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken } from "./http.js";
3
+ export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, getHttpCacheDiagnostics, resetHttpCacheDiagnostics, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, getBaseURL, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken, getAppContext, setAppContext } from "./http.js";
4
4
  export * from "./api/index.js";
5
5
  export * from "./types/index.js";
6
6
  // Iframe namespace
package/dist/openapi.yaml CHANGED
@@ -72,8 +72,6 @@ tags:
72
72
  description: Product facet querying and aggregation.
73
73
  - name: form
74
74
  description: Form definitions and submissions.
75
- - name: functions
76
- description: functions API
77
75
  - name: integrations
78
76
  description: Third-party integration flows and imports.
79
77
  - name: interactions
@@ -12152,32 +12150,6 @@ paths:
12152
12150
  description: Unauthorized
12153
12151
  404:
12154
12152
  description: Not found
12155
- /public/collection/{collectionId}/functions:
12156
- get:
12157
- tags:
12158
- - functions
12159
- summary: List the public functions available for a collection (discovery).
12160
- operationId: functions_list
12161
- security: []
12162
- parameters:
12163
- - name: collectionId
12164
- in: path
12165
- required: true
12166
- schema:
12167
- type: string
12168
- responses:
12169
- 200:
12170
- description: Success
12171
- content:
12172
- application/json:
12173
- schema:
12174
- $ref: "#/components/schemas/FunctionListResponse"
12175
- 400:
12176
- description: Bad request
12177
- 401:
12178
- description: Unauthorized
12179
- 404:
12180
- description: Not found
12181
12153
  /public/collection/{collectionId}/interactions:
12182
12154
  get:
12183
12155
  tags:
@@ -29314,6 +29286,12 @@ components:
29314
29286
  $ref: "#/components/schemas/FunctionListEntry"
29315
29287
  required:
29316
29288
  - functions
29289
+ FunctionCallOptions:
29290
+ type: object
29291
+ properties:
29292
+ appId:
29293
+ type: object
29294
+ additionalProperties: true
29317
29295
  AllocateSequenceInput:
29318
29296
  type: object
29319
29297
  properties:
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.20 | Generated: 2026-09-25T11:48:23.950Z
3
+ Version: 2.0.22 | Generated: 2026-09-25T18:07:22.478Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -166,6 +166,12 @@ Reset the diagnostics counters (does not touch the cache itself).
166
166
  **isProxyEnabled**() → `boolean`
167
167
  Return whether proxy mode is currently enabled.
168
168
 
169
+ **getAppContext**() → `string | undefined`
170
+ The current app context (appId), if the SDK was initialized with one.
171
+
172
+ **setAppContext**(id: string | undefined) → `void`
173
+ Set (or clear) the current app context — the appId used to scope SL.functions calls.
174
+
169
175
  **initializeApi**(options: {
170
176
  baseURL: string
171
177
  apiKey?: string
@@ -8960,6 +8966,13 @@ interface FunctionListResponse {
8960
8966
  }
8961
8967
  ```
8962
8968
 
8969
+ **FunctionCallOptions** (interface)
8970
+ ```typescript
8971
+ interface FunctionCallOptions {
8972
+ appId?: string; channel?: string
8973
+ }
8974
+ ```
8975
+
8963
8976
  **FunctionCallResult** = `any`
8964
8977
 
8965
8978
  ### sequence (api)
@@ -10458,16 +10471,18 @@ Delete a form for a collection (admin only).
10458
10471
 
10459
10472
  **call**(collectionId: string,
10460
10473
  name: string,
10461
- body: Record<string, any> = {}) → `Promise<T>`
10462
- Call a PUBLIC app server function inline. `POST /public/collection/:collectionId/functions/:name` — surface `'public'`. const { value } = await SL.functions.call<{ value: number }>(collectionId, 'increment')
10474
+ body: Record<string, any> = {},
10475
+ opts: FunctionCallOptions = {}) → `Promise<T>`
10476
+ Call a PUBLIC app server function inline (surface `'public'`). App-scoped: `POST /public/collection/:c/app/:appId/functions/:name`. // App calling its own function (appId from initializeApi({ appId })): const { value } = await SL.functions.call<{ value: number }>(collectionId, 'pressCounter') // Or address another app explicitly: await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'my-counter-app' })
10463
10477
 
10464
10478
  **callAdmin**(collectionId: string,
10465
10479
  name: string,
10466
- body: Record<string, any> = {}) → `Promise<T>`
10467
- Call an ADMIN app server function (surface `'admin'`; requires an admin session). `POST /admin/collection/:collectionId/functions/:name`.
10480
+ body: Record<string, any> = {},
10481
+ opts: FunctionCallOptions = {}) → `Promise<T>`
10482
+ Call an ADMIN app server function (surface `'admin'`; requires an admin session). App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
10468
10483
 
10469
- **list**(collectionId: string) → `Promise<FunctionListResponse>`
10470
- List the public functions available for a collection (discovery). `GET /public/collection/:c/functions`.
10484
+ **list**(collectionId: string, opts: FunctionCallOptions = {}) → `Promise<FunctionListResponse>`
10485
+ List the public functions available for a collection (discovery). Scoped to one app when an appId is given (or set as the SDK app context): `GET /public/collection/:c[/app/:appId]/functions`.
10471
10486
 
10472
10487
  ### http
10473
10488
 
@@ -181,11 +181,14 @@ interface ServerFunctionContext {
181
181
  collectionId: string
182
182
  appId: string
183
183
 
184
- /** SmartLinks SDK, pre-scoped to your declared `authority`. Capabilities cap what it may do. */
184
+ /** SmartLinks SDK, pre-scoped to your declared `authority`. Capabilities cap what it may do.
185
+ * Key resources: `sl.appRecords` (per-app records), `sl.appData` (general/app-wide config +
186
+ * data — URLs, flags, counters, arrays), `sl.products`, `sl.attestations`. */
185
187
  sl: SmartLinks
186
188
 
187
- /** Capability-gated secrets. Resolves only refs you declared via `secrets:<ref>`. */
188
- secrets: { get(ref: string): Promise<string | null> }
189
+ /** Capability-gated secrets (need `secrets:<ref>`). `get(ref)` resolves the collection's OWN
190
+ * secret first, then the app-level one; `app(ref)` reads the app-level secret only. */
191
+ secrets: { get(ref: string): Promise<string | null>; app(ref: string): Promise<string | null> }
189
192
 
190
193
  /** Who invoked this function. */
191
194
  caller: {
@@ -211,6 +214,61 @@ already authenticated as your declared authority and scoped to the collection. D
211
214
  to construct your own SDK client or carry your own key; that's what `ctx.sl` is for, and it's
212
215
  the only way authority stays correct.
213
216
 
217
+ ### Calling a function from your app (client side)
218
+
219
+ From a page/UI, invoke an `http`-trigger function with the SDK — no manual fetch, no URL building:
220
+
221
+ ```ts
222
+ // public function (runs on the caller's authority — signed-in owner, else public):
223
+ const res = await SL.functions.call(collectionId, 'increment', { /* becomes event.body */ })
224
+ // admin surface (needs an admin session):
225
+ const res = await SL.functions.callAdmin(collectionId, 'recomputeTotals', { … })
226
+ ```
227
+
228
+ `res` is whatever your handler returns. See the `edge-function-test` example for a full page +
229
+ function + manifest.
230
+
231
+ #### Functions are ALWAYS app-scoped
232
+
233
+ A function belongs to an app, so the canonical route carries the appId:
234
+ `POST /collection/:collectionId/app/:appId/functions/:name`. Two different installed apps can each
235
+ ship a function of the same name without clashing.
236
+
237
+ The SDK fills the appId in for you. Initialize with your app's id once, then call by bare name —
238
+ the SDK routes it app-scoped:
239
+
240
+ ```ts
241
+ SL.initializeApi({ baseURL, appId: 'my-counter-app' }) // usually done by the host/bootstrap
242
+ await SL.functions.call(collectionId, 'pressCounter') // → /collection/:c/app/my-counter-app/functions/pressCounter
243
+ ```
244
+
245
+ To call a *different* app's function, pass the appId explicitly:
246
+
247
+ ```ts
248
+ await SL.functions.call(collectionId, 'pressCounter', {}, { appId: 'some-other-app' })
249
+ ```
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
+
262
+ If no appId is available (not set on init, none passed), the call falls back to the **deprecated
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.
266
+
267
+ > There is no such thing as an app-less function. First-party builtins (e.g. the `functions.ping`
268
+ > diagnostic) belong to the reserved `smartlinks` app. A "global" function is just an app enabled on
269
+ > the `global` collection and called at `/collection/global/app/:appId/functions/:name` — same two
270
+ > facets (collection + app), with `global` as the sentinel collection.
271
+
214
272
  ---
215
273
 
216
274
  ## Runtime — what your function can use
@@ -262,6 +320,8 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
262
320
  | Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
263
321
  | Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
264
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` |
324
+ | 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` |
265
325
 
266
326
  Notes:
267
327
  - **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
@@ -318,14 +378,31 @@ other secrets, or any other collection.
318
378
 
319
379
  ## Invoking an http function
320
380
 
321
- An `http` function is called by POSTing to the collection's functions endpoint on the
381
+ An `http` function is called by POSTing to the app-scoped functions endpoint on the
322
382
  surface that matches its `visibility`:
323
383
 
324
384
  ```
325
- POST /admin/collection/:collectionId/functions/:name # visibility: admin (collection-admin auth)
326
- POST /public/collection/:collectionId/functions/:name # visibility: public (auth optional)
385
+ POST /admin/collection/:collectionId/app/:appId/functions/:name # visibility: admin (collection-admin auth)
386
+ POST /public/collection/:collectionId/app/:appId/functions/:name # visibility: public (auth optional)
387
+ GET /{admin|public}/collection/:collectionId/app/:appId/functions # list this app's functions on the surface
327
388
  ```
328
389
 
390
+ The app-scoped route resolves the app **directly by id**, so the app need not be enabled on the
391
+ collection — handy for testing an un-installed (dev) app. Add `?channel=dev` (default `stable`) to
392
+ pick the release. Enablement (`appConfig.apps[]`) currently only controls menus/discovery, not
393
+ whether a function can run.
394
+
395
+ The bare-name form (`/collection/:c/functions/:name`, no `/app/:appId`) is a **deprecated alias**:
396
+ it searches only the collection's **enabled** apps, resolves by name, lets a first-party builtin
397
+ win, and returns `409 AMBIGUOUS_FUNCTION` when two enabled apps define the same name. Prefer the
398
+ app-scoped route (the SDK emits it automatically once an appId is set — see "Calling a function
399
+ from your app").
400
+
401
+ > **Security note (first-party model, today):** because enablement is not an auth gate, any app's
402
+ > function can be invoked on any collection by id. That's fine while all apps are first-party and
403
+ > trusted. When untrusted third-party apps arrive, gate *elevated* (`public` + `collection`)
404
+ > invocation of a **non-enabled** app behind install/consent — the app-scoped resolver is the seam.
405
+
329
406
  The request body is delivered to the handler as `event.body` (query string as
330
407
  `event.query`). A function only runs on its own surface — calling an `admin` function on
331
408
  the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
package/openapi.yaml CHANGED
@@ -72,8 +72,6 @@ tags:
72
72
  description: Product facet querying and aggregation.
73
73
  - name: form
74
74
  description: Form definitions and submissions.
75
- - name: functions
76
- description: functions API
77
75
  - name: integrations
78
76
  description: Third-party integration flows and imports.
79
77
  - name: interactions
@@ -12152,32 +12150,6 @@ paths:
12152
12150
  description: Unauthorized
12153
12151
  404:
12154
12152
  description: Not found
12155
- /public/collection/{collectionId}/functions:
12156
- get:
12157
- tags:
12158
- - functions
12159
- summary: List the public functions available for a collection (discovery).
12160
- operationId: functions_list
12161
- security: []
12162
- parameters:
12163
- - name: collectionId
12164
- in: path
12165
- required: true
12166
- schema:
12167
- type: string
12168
- responses:
12169
- 200:
12170
- description: Success
12171
- content:
12172
- application/json:
12173
- schema:
12174
- $ref: "#/components/schemas/FunctionListResponse"
12175
- 400:
12176
- description: Bad request
12177
- 401:
12178
- description: Unauthorized
12179
- 404:
12180
- description: Not found
12181
12153
  /public/collection/{collectionId}/interactions:
12182
12154
  get:
12183
12155
  tags:
@@ -29314,6 +29286,12 @@ components:
29314
29286
  $ref: "#/components/schemas/FunctionListEntry"
29315
29287
  required:
29316
29288
  - functions
29289
+ FunctionCallOptions:
29290
+ type: object
29291
+ properties:
29292
+ appId:
29293
+ type: object
29294
+ additionalProperties: true
29317
29295
  AllocateSequenceInput:
29318
29296
  type: object
29319
29297
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",