@proveanything/smartlinks 2.0.38 → 2.0.40

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.
@@ -62,7 +62,7 @@ export declare namespace functions {
62
62
  }): string;
63
63
  /**
64
64
  * Call a PUBLIC app server function inline (surface `'public'`).
65
- * App-scoped: `POST /public/collection/:c/app/:appId/functions/:name`.
65
+ * App-scoped: `POST /fn/public/collection/:c/app/:appId/functions/:name`.
66
66
  *
67
67
  * @example
68
68
  * // App calling its own function (appId from initializeApi({ appId })):
@@ -73,7 +73,7 @@ export declare namespace functions {
73
73
  function call<T = FunctionCallResult>(collectionId: string, name: string, body?: Record<string, any>, opts?: FunctionCallOptions): Promise<T>;
74
74
  /**
75
75
  * Call an ADMIN app server function (surface `'admin'`; requires an admin session).
76
- * App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
76
+ * App-scoped: `POST /fn/admin/collection/:c/app/:appId/functions/:name`.
77
77
  */
78
78
  function callAdmin<T = FunctionCallResult>(collectionId: string, name: string, body?: Record<string, any>, opts?: FunctionCallOptions): Promise<T>;
79
79
  /**
@@ -14,6 +14,10 @@
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
  //
17
+ // PREFIX. App-scoped calls go to /fn/{public|admin}/collection/…/functions/… (under the API base,
18
+ // so /api/v1/fn/…): one prefix for all function traffic, which the platform can route to its own
19
+ // service. The same call without /fn still works (older SDKs). The flat alias has no /fn form.
20
+ //
17
21
  // RELEASE CHANNEL. The channel is part of the URL — /collection/:c/app/:appId/<channel>/functions/:name
18
22
  // — never a query param (the function owns its query string, and a configured URL such as a webhook
19
23
  // can only ever hit the channel it names). With NO channel the server runs the release the collection
@@ -55,7 +59,7 @@ function appBase(surface, collectionId, opts) {
55
59
  if (!app)
56
60
  return `/${surface}/collection/${c}`; // deprecated flat alias — resolves installed apps only
57
61
  const ch = resolveFunctionChannel(opts, app);
58
- return `/${surface}/collection/${c}/app/${encodeURIComponent(app)}${ch ? `/${ch}` : ''}`;
62
+ return `/fn/${surface}/collection/${c}/app/${encodeURIComponent(app)}${ch ? `/${ch}` : ''}`;
59
63
  }
60
64
  /** The API path a function call goes to (exported for hosts/tests that need the exact URL). */
61
65
  export function functionPath(surface, collectionId, name, opts = {}) {
@@ -94,7 +98,7 @@ export var functions;
94
98
  functions.siteUrl = siteUrl;
95
99
  /**
96
100
  * Call a PUBLIC app server function inline (surface `'public'`).
97
- * App-scoped: `POST /public/collection/:c/app/:appId/functions/:name`.
101
+ * App-scoped: `POST /fn/public/collection/:c/app/:appId/functions/:name`.
98
102
  *
99
103
  * @example
100
104
  * // App calling its own function (appId from initializeApi({ appId })):
@@ -108,7 +112,7 @@ export var functions;
108
112
  functions.call = call;
109
113
  /**
110
114
  * Call an ADMIN app server function (surface `'admin'`; requires an admin session).
111
- * App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
115
+ * App-scoped: `POST /fn/admin/collection/:c/app/:appId/functions/:name`.
112
116
  */
113
117
  async function callAdmin(collectionId, name, body = {}, opts = {}) {
114
118
  return post(fnPath('admin', collectionId, name, opts), body);
@@ -1,4 +1,4 @@
1
- import type { AdminInteractionsCountsByOutcomeRequest, AdminInteractionsQueryRequest, AdminInteractionsAggregateRequest, AdminInteractionsAggregateResponse, AppendInteractionBody, UpdateInteractionBody, OutcomeCount, InteractionEventRow, PublicInteractionsCountsByOutcomeRequest, PublicInteractionsByUserRequest, SubmitInteractionResponse, SubmitInteractionError, CreateInteractionTypeBody, UpdateInteractionTypeBody, ListInteractionTypesQuery, InteractionTypeRecord, InteractionTypeList } from "../types/interaction.js";
1
+ import type { AdminInteractionsCountsByOutcomeRequest, AdminInteractionsQueryRequest, AdminInteractionsAggregateRequest, AdminInteractionsAggregateResponse, AppendInteractionBody, UpdateInteractionBody, OutcomeCount, InteractionEventRow, PublicInteractionsCountsByOutcomeRequest, PublicInteractionsByUserRequest, SubmitInteractionResponse, SubmitInteractionError, CreateInteractionTypeBody, EnsureInteractionTypeInput, UpdateInteractionTypeBody, ListInteractionTypesQuery, InteractionTypeRecord, InteractionTypeList } from "../types/interaction.js";
2
2
  export declare namespace interactions {
3
3
  /**
4
4
  * POST /admin/collection/:collectionId/interactions/query
@@ -45,6 +45,19 @@ export declare namespace interactions {
45
45
  function submitPublicEvent(collectionId: string, body: AppendInteractionBody): Promise<SubmitInteractionResponse | SubmitInteractionError>;
46
46
  function create(collectionId: string, body: CreateInteractionTypeBody): Promise<InteractionTypeRecord>;
47
47
  function list(collectionId: string, query?: ListInteractionTypesQuery): Promise<InteractionTypeList>;
48
+ /**
49
+ * Find this app's interaction type by its readable `key` (data.interactionType), or create it.
50
+ * Returns the type record — its `id` is the server-minted UUID every event must use.
51
+ * ADMIN only (run it in your admin/setup screen), then store the id in your app config so
52
+ * public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup.
53
+ *
54
+ * @example
55
+ * const vote = await SL.interactions.ensureType(collectionId, {
56
+ * appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true },
57
+ * })
58
+ * // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
59
+ */
60
+ function ensureType(collectionId: string, input: EnsureInteractionTypeInput): Promise<InteractionTypeRecord>;
48
61
  function get(collectionId: string, id: string): Promise<InteractionTypeRecord>;
49
62
  function update(collectionId: string, id: string, patchBody: UpdateInteractionTypeBody): Promise<InteractionTypeRecord>;
50
63
  function remove(collectionId: string, id: string): Promise<void>;
@@ -102,6 +102,33 @@ export var interactions;
102
102
  return request(path);
103
103
  }
104
104
  interactions.list = list;
105
+ /**
106
+ * Find this app's interaction type by its readable `key` (data.interactionType), or create it.
107
+ * Returns the type record — its `id` is the server-minted UUID every event must use.
108
+ * ADMIN only (run it in your admin/setup screen), then store the id in your app config so
109
+ * public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup.
110
+ *
111
+ * @example
112
+ * const vote = await SL.interactions.ensureType(collectionId, {
113
+ * appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true },
114
+ * })
115
+ * // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
116
+ */
117
+ async function ensureType(collectionId, input) {
118
+ const { appId, key, permissions, display, data } = input;
119
+ if (!appId || !key)
120
+ throw new Error('interactions.ensureType: appId and key are required');
121
+ for (let offset = 0;; offset += 200) {
122
+ const page = await list(collectionId, { appId, limit: 200, offset });
123
+ const found = (page.items || []).find((t) => t.data && t.data.interactionType === key);
124
+ if (found)
125
+ return found;
126
+ if (!page.items || page.items.length < 200)
127
+ break;
128
+ }
129
+ return create(collectionId, Object.assign(Object.assign({ appId }, (permissions ? { permissions } : {})), { data: Object.assign(Object.assign(Object.assign({}, (data || {})), { interactionType: key }), (display ? { display } : {})) }));
130
+ }
131
+ interactions.ensureType = ensureType;
105
132
  async function get(collectionId, id) {
106
133
  const path = `/admin/collection/${encodeURIComponent(collectionId)}/interactions/${encodeURIComponent(id)}`;
107
134
  return request(path);
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.38 | Generated: 2026-10-05T12:10:36.598Z
3
+ Version: 2.0.40 | Generated: 2026-10-05T14:51:09.158Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -26,6 +26,12 @@ For detailed guides on specific features:
26
26
  - **[iframe Responder](iframe-responder.md)** - iframe integration and cross-origin communication (incl. hand-rolled streaming protocol)
27
27
  - **[Utilities](utils.md)** - Helper functions for building portal paths, URLs, and common tasks
28
28
  - **[UI Utils](ui-utils.md)** - Reusable, themeable admin UI React component library for microapps
29
+ - **[Headless Providers](headless-providers.md)** - Declaring an app's content for other sites: the manifest `data` + `headless` blocks (types, storage, public fields, examples, read recipes, categories), `SL.headless.validate`, `smartlinks-headless`, and the procedure for adding a headless mode
30
+ - **[Websites: SEO + GEO](site-seo.md)** - For apps served as websites: platform-generated robots/sitemap/llms.txt, canonical addresses, `SL.seo.head` / `SL.seo.jsonLd` / `SL.seo.schema.*`, `SL.site.ready()`, routes in `sitemap-paths.txt`
31
+ - **[Agent Tools](agent-tools.md)** - Exposing app functions as AI agent tools
32
+ - **[Host Dependency Contract](host-dependency-contract.md)** - The shared dependencies (React, the SDK…) the host provides, and what an app must externalise
33
+ - **[Theme Tokens](theme-tokens.md)** - The `--sl-*` semantic token contract hosts set and apps bind to (`theme.css`)
34
+ - **[CSS Baseline](css-baseline.md)** - The frozen `sl-*` structural helper classes hosts guarantee
29
35
  - **[Caching](caching.md)** - Multi-tier caching strategy (in-memory, SessionStorage, IndexedDB) used by the SDK
30
36
  - **[Native Facade](native-facade.md)** - Contract layer for accessing device capabilities (share, NFC, haptics) across host shells
31
37
  - **[i18n](i18n.md)** - Internationalization and localization
@@ -7269,13 +7275,24 @@ interface InteractionTypeList {
7269
7275
  **CreateInteractionTypeBody** (interface)
7270
7276
  ```typescript
7271
7277
  interface CreateInteractionTypeBody {
7272
- id: string
7278
+ id?: string
7273
7279
  appId: string
7274
7280
  permissions?: InteractionPermissions
7275
7281
  data?: Record<string, unknown>
7276
7282
  }
7277
7283
  ```
7278
7284
 
7285
+ **EnsureInteractionTypeInput** (interface)
7286
+ ```typescript
7287
+ interface EnsureInteractionTypeInput {
7288
+ appId: string
7289
+ key: string
7290
+ permissions?: InteractionPermissions
7291
+ display?: InteractionDisplay
7292
+ data?: Record<string, unknown>
7293
+ }
7294
+ ```
7295
+
7279
7296
  **UpdateInteractionTypeBody** (interface)
7280
7297
  ```typescript
7281
7298
  interface UpdateInteractionTypeBody {
@@ -11192,22 +11209,22 @@ The release channel a call targets, or undefined for "the collection's installed
11192
11209
  **functionPath**(surface: 'public' | 'admin', collectionId: string, name: string, opts: FunctionCallOptions = {}) → `string`
11193
11210
  The API path a function call goes to (exported for hosts/tests that need the exact URL).
11194
11211
 
11195
- **siteUrl**(collection: { siteHost?: string | null } | string,
11196
- name: string,
11212
+ **siteUrl**(collection: { siteHost?: string | null } | string,
11213
+ name: string,
11197
11214
  opts: FunctionSiteUrlOptions & { appId?: string } = {}) → `string`
11198
11215
  The PUBLIC address of an app function on the collection's own site — what you give a third party as a webhook URL, or call from the collection's public pages: `https://<siteHost>/_fn/<appId>[/<channel>]/<name>[/<path>]`. Every HTTP method the function declares works there, with the raw body for signature checks. Pass the collection (or its siteHost). This address is for public/integration calls; signed-in calls from your app keep using {@link call} / {@link callAdmin}, so the user's SmartLinks session never goes to a tenant hostname. const col = await SL.collection.get(collectionId) const hookUrl = SL.functions.siteUrl(col, 'stripeWebhook', { appId: 'my-shop' }) // → https://acme.smartlinks.host/_fn/my-shop/stripeWebhook
11199
11216
 
11200
- **call**(collectionId: string,
11201
- name: string,
11202
- body: Record<string, any> = {},
11217
+ **call**(collectionId: string,
11218
+ name: string,
11219
+ body: Record<string, any> = {},
11203
11220
  opts: FunctionCallOptions = {}) → `Promise<T>`
11204
- 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' })
11221
+ Call a PUBLIC app server function inline (surface `'public'`). App-scoped: `POST /fn/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' })
11205
11222
 
11206
- **callAdmin**(collectionId: string,
11207
- name: string,
11208
- body: Record<string, any> = {},
11223
+ **callAdmin**(collectionId: string,
11224
+ name: string,
11225
+ body: Record<string, any> = {},
11209
11226
  opts: FunctionCallOptions = {}) → `Promise<T>`
11210
- Call an ADMIN app server function (surface `'admin'`; requires an admin session). App-scoped: `POST /admin/collection/:c/app/:appId/functions/:name`.
11227
+ Call an ADMIN app server function (surface `'admin'`; requires an admin session). App-scoped: `POST /fn/admin/collection/:c/app/:appId/functions/:name`.
11211
11228
 
11212
11229
  **list**(collectionId: string, opts: FunctionCallOptions = {}) → `Promise<FunctionListResponse>`
11213
11230
  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`.
@@ -11319,36 +11336,40 @@ POST /api/v1/public/collection/:collectionId/interactions/submit Submits an inte
11319
11336
  query: ListInteractionTypesQuery = {}) → `Promise<InteractionTypeList>`
11320
11337
  POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11321
11338
 
11339
+ **ensureType**(collectionId: string,
11340
+ input: EnsureInteractionTypeInput) → `Promise<InteractionTypeRecord>`
11341
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11342
+
11322
11343
  **get**(collectionId: string,
11323
11344
  id: string) → `Promise<InteractionTypeRecord>`
11324
- POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11345
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11325
11346
 
11326
11347
  **update**(collectionId: string,
11327
11348
  id: string,
11328
11349
  patchBody: UpdateInteractionTypeBody) → `Promise<InteractionTypeRecord>`
11329
- POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11350
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11330
11351
 
11331
11352
  **remove**(collectionId: string,
11332
11353
  id: string) → `Promise<void>`
11333
- POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11354
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11334
11355
 
11335
11356
  **publicCountsByOutcome**(collectionId: string,
11336
11357
  body: PublicInteractionsCountsByOutcomeRequest,
11337
11358
  authToken?: string) → `Promise<OutcomeCount[]>`
11338
- POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11359
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11339
11360
 
11340
11361
  **publicMyInteractions**(collectionId: string,
11341
11362
  body: PublicInteractionsByUserRequest,
11342
11363
  authToken?: string) → `Promise<InteractionEventRow[]>`
11343
- POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11364
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11344
11365
 
11345
11366
  **publicList**(collectionId: string,
11346
11367
  query: ListInteractionTypesQuery = {}) → `Promise<InteractionTypeList>`
11347
- POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11368
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11348
11369
 
11349
11370
  **publicGet**(collectionId: string,
11350
11371
  id: string) → `Promise<InteractionTypeRecord>`
11351
- POST /api/v1/public/collection/:collectionId/interactions/submit Submits an interaction event from a public/client-side context. `interactionId` must reference an existing interaction type definition. This endpoint does not create interaction definitions. When the interaction has `allowAnonymousSubmit: true`, neither `userId` nor `contactId` is required. Pass `anonId` inside `metadata` to enable device-level deduplication via `uniquePerAnonId`.
11372
+ Find this app's interaction type by its readable `key` (data.interactionType), or create it. Returns the type record — its `id` is the server-minted UUID every event must use. ADMIN only (run it in your admin/setup screen), then store the id in your app config so public code can read it: `interactionIds: { vote: type.id }`. Safe to run on every setup. const vote = await SL.interactions.ensureType(collectionId, { appId: 'my-app', key: 'vote', permissions: { allowPublicSubmit: true, uniquePerUser: true }, }) // vote.id → '52bab6fa-…' — store it in config; never hardcode 'vote' as an interactionId
11352
11373
 
11353
11374
  ### jobs
11354
11375
 
@@ -50,6 +50,14 @@ Site builders (the Forge agent included) read these declarations to decide "inst
50
50
  | `thread` | App threads: discussions, Q&A, reviews | `SL.app.threads.list / get` |
51
51
  | `config` (+ optional `key`) | App configuration: settings, small fixed lists | `SL.appConfiguration.getConfig({ collectionId, appId })` |
52
52
 
53
+ **What the read calls return.** This is fixed by the storage kind, so a provider never needs to describe it, and site builders get it from the checker and `forge-cms describe`:
54
+
55
+ - **`record`, `case`, `thread`:** `list` returns `{ data: Item[], pagination: { total, limit, offset, hasMore } }`.
56
+ - The items are in **`response.data`**, never `response.items` or `response.records`.
57
+ - Each item's declared fields are in **`item.data`** (e.g. `record.data.question`). `id`, `status`, `productId` and the dates are top-level.
58
+ - Page with `offset` / `limit` while `pagination.hasMore`. `get` returns one item.
59
+ - **`config`:** the configuration object. Items are in `config.<key>` when the type names a `key`.
60
+
53
61
  Products, contacts and proofs are platform data. Reference them with `ref` fields (`"to": "product"`); never redeclare them.
54
62
 
55
63
  **Field types:** `string`, `text`, `richtext` (HTML), `markdown`, `number`, `boolean`, `date` (YYYY-MM-DD), `datetime`, `enum` (+ `options`), `url`, `image` and `file` (a URL or `{ url, ... }`), `ref` and `ref[]` (+ `to`: a declared type id, or `product` / `contact` / `proof`), `string[]`, `json` (avoid where a typed field fits).
@@ -12,43 +12,50 @@ Interactions have two distinct layers:
12
12
 
13
13
  | Layer | Purpose |
14
14
  |-------|---------|
15
- | **Interaction Types** | Definitions stored in the database — configure an interaction's ID, permissions, and display metadata once per collection |
16
- | **Interaction Events** | Individual event records logged each time a user performs that interaction |
15
+ | **Interaction Types** | Definitions stored in the database, once per collection: permissions, display metadata, effects. **The server mints each type's `id` (a UUID).** |
16
+ | **Interaction Events** | Individual event records logged each time a user performs that interaction. Each event's `interactionId` is a type's minted `id`. |
17
17
 
18
- Critical rule: event `interactionId` values must reference an existing interaction type definition in that collection. Do not generate random IDs in app code and submit events against them.
18
+ ## The one rule: create in admin → store the id in config → read it everywhere
19
19
 
20
- ```text
21
- ┌──────────────────────────────────────────────────────────────────┐
22
- │ Your App │
23
- │ │
24
- │ 1. Create type once: interactions.create(collectionId, { │
25
- │ id: 'vote', permissions: { uniquePerUser: true } }) │
26
- │ -> definition exists in platform │
27
- │ │
28
- │ 2. Log events: interactions.appendEvent(collectionId, { │
29
- │ interactionId: 'vote', outcome: 'option-a', userId }) │
30
- │ (must match the created definition ID) │
31
- │ │
32
- │ 3. Read results: interactions.countsByOutcome(collectionId, │
33
- │ { interactionId: 'vote' }) │
34
- │ → [{ outcome: 'option-a', count: 42 }, ...] │
35
- └──────────────────────────────────────────────────────────────────┘
20
+ **You never choose an interaction id, and you never write one as a string literal.** You choose a readable *key* (e.g. `'vote'`); the server mints the *id* (e.g. `'52bab6fa-…'`). Events must carry the id.
21
+
22
+ ```typescript
23
+ // 1. ADMIN / setup screen — find-or-create the type by your key, then store its minted id in app config.
24
+ const vote = await SL.interactions.ensureType(collectionId, {
25
+ appId: 'my-app',
26
+ key: 'vote', // your readable name — NOT the id
27
+ permissions: { allowPublicSubmit: true, uniquePerUser: true },
28
+ display: { title: 'Vote' },
29
+ });
30
+ const config = await SL.appConfiguration.getConfig({ collectionId, appId: 'my-app', admin: true });
31
+ await SL.appConfiguration.setConfig({
32
+ collectionId, appId: 'my-app', admin: true,
33
+ config: { ...config, interactionIds: { ...config?.interactionIds, vote: vote.id } },
34
+ });
35
+
36
+ // 2. ANYWHERE ELSE (public widget, container, server function) — read the id from config.
37
+ const { interactionIds } = await SL.appConfiguration.getConfig({ collectionId, appId: 'my-app' });
38
+ await SL.interactions.submitPublicEvent(collectionId, {
39
+ appId: 'my-app',
40
+ interactionId: interactionIds.vote, // the minted id, from config
41
+ outcome: 'option-a',
42
+ });
36
43
  ```
37
44
 
38
- ### Required Workflow (Do Not Invent IDs)
45
+ Why it's split this way:
39
46
 
40
- 1. Create an interaction type definition (admin endpoint) before recording any events.
41
- 2. Reuse that same definition ID for every `appendEvent` / `submitPublicEvent` call.
42
- 3. Treat unknown IDs as configuration errors, not as values your app should auto-create.
47
+ - **Types can only be created on the admin surface.** Public code (widgets, the portal) can submit events but can't create types — so the public side can only learn the id from somewhere admin put it. App config is that place.
48
+ - **`ensureType` is safe to re-run.** It returns the existing type when one with that `key` exists for your app, so run it every time your setup screen saves.
49
+ - **Unknown ids are configuration errors.** If `interactionIds.vote` is missing, the app hasn't been set up in this collection: show "not configured", don't submit, and never fall back to a literal like `'vote'`. (`interactions.create()` exists too, but it ignores any `id` you pass — read `id` from the record it returns.)
43
50
 
44
- If your app currently hardcodes strings like `"poll"` or `"entry"`, make sure those IDs are actually created as interaction types during setup.
51
+ The examples below use `interactionIds` — the map you read from config in step 2.
45
52
 
46
53
  ---
47
54
 
48
55
  ## Common Use Cases
49
56
 
50
- | Use Case | `interactionId` example | `outcome` example |
51
- |----------|-------------------------|-------------------|
57
+ | Use Case | type `key` (for `ensureType`) | `outcome` example |
58
+ |----------|-------------------------------|-------------------|
52
59
  | Competition entry | `competition-entry` | `"entered"` |
53
60
  | Voting / polling | `vote` | `"option-a"` |
54
61
  | Mailing list signup | `newsletter-signup` | `"subscribed"` |
@@ -62,12 +69,14 @@ If your app currently hardcodes strings like `"poll"` or `"entry"`, make sure th
62
69
 
63
70
  Interaction types are defined once per collection and control permissions, display metadata, and uniqueness constraints.
64
71
 
65
- ### Create a Type
72
+ ### Create a Type (find-or-create)
73
+
74
+ Admin only. Returns the type record; its `id` is the minted UUID — store it in app config (see [the one rule](#the-one-rule-create-in-admin--store-the-id-in-config--read-it-everywhere)).
66
75
 
67
76
  ```typescript
68
- await SL.interactions.create(collectionId, {
69
- id: 'vote',
77
+ const vote = await SL.interactions.ensureType(collectionId, {
70
78
  appId: 'my-app',
79
+ key: 'vote', // stored as data.interactionType; how ensureType finds it again
71
80
  permissions: {
72
81
  allowPublicSubmit: true,
73
82
  uniquePerUser: true,
@@ -75,35 +84,34 @@ await SL.interactions.create(collectionId, {
75
84
  endAt: '2026-06-30T23:59:59Z',
76
85
  allowPublicSummary: true,
77
86
  },
78
- data: {
79
- display: {
80
- title: 'Vote',
81
- description: 'Cast your vote for the competition.',
82
- },
87
+ display: {
88
+ title: 'Vote',
89
+ description: 'Cast your vote for the competition.',
83
90
  },
84
91
  });
92
+ // vote.id → '52bab6fa-d868-4616-9342-6731b0f332b2'
85
93
  ```
86
94
 
87
95
  ### Update / Delete a Type
88
96
 
89
97
  ```typescript
90
98
  // Update permissions or display
91
- await SL.interactions.update(collectionId, 'vote', {
99
+ await SL.interactions.update(collectionId, interactionIds.vote, {
92
100
  permissions: { endAt: '2026-07-15T23:59:59Z' },
93
101
  });
94
102
 
95
- // Delete the definition (does not delete existing events)
96
- await SL.interactions.remove(collectionId, 'vote');
103
+ // Delete the definition (does not delete existing events) — and remove it from your config
104
+ await SL.interactions.remove(collectionId, interactionIds.vote);
97
105
  ```
98
106
 
99
107
  ### List / Get Types
100
108
 
101
109
  ```typescript
102
- // Admin: list all types for an app
110
+ // Admin: list all types for an app (each has data.interactionType = its key)
103
111
  const { items } = await SL.interactions.list(collectionId, { appId: 'my-app' });
104
112
 
105
113
  // Admin: get a single type
106
- const type = await SL.interactions.get(collectionId, 'vote');
114
+ const type = await SL.interactions.get(collectionId, interactionIds.vote);
107
115
 
108
116
  // Public: list available types (respects permissions)
109
117
  const { items } = await SL.interactions.publicList(collectionId, { appId: 'my-app' });
@@ -113,7 +121,7 @@ const { items } = await SL.interactions.publicList(collectionId, { appId: 'my-ap
113
121
 
114
122
  ## Logging Events
115
123
 
116
- Before logging events, ensure the referenced interaction type already exists. Event ingestion is not intended to create new interaction definitions.
124
+ Every `interactionId` below is a minted type id read from your app config (`interactionIds`, see [the one rule](#the-one-rule-create-in-admin--store-the-id-in-config--read-it-everywhere)). Events never create types: an id that isn't a type defined for this collection and app is a configuration error.
117
125
 
118
126
  ### Admin Event Append
119
127
 
@@ -122,7 +130,7 @@ Use on the server side or in admin flows. Requires `userId` **or** `contactId`.
122
130
  ```typescript
123
131
  await SL.interactions.appendEvent(collectionId, {
124
132
  appId: 'my-app',
125
- interactionId: 'vote',
133
+ interactionId: interactionIds.vote,
126
134
  outcome: 'option-a', // The result / choice — used by countsByOutcome()
127
135
  userId: 'user_abc123', // One of userId or contactId is required
128
136
  productId: 'prod_xyz', // Optional — scope to a specific product
@@ -139,7 +147,7 @@ Use in client-side app code. Hits the public endpoint and respects interaction p
139
147
  // Authenticated submission
140
148
  await SL.interactions.submitPublicEvent(collectionId, {
141
149
  appId: 'my-app',
142
- interactionId: 'competition-entry',
150
+ interactionId: interactionIds.competitionEntry,
143
151
  outcome: 'entered',
144
152
  contactId: currentUser.contactId,
145
153
  metadata: { answer: 'Paris' },
@@ -148,7 +156,7 @@ await SL.interactions.submitPublicEvent(collectionId, {
148
156
  // Anonymous submission (interaction must have allowAnonymousSubmit: true)
149
157
  const response = await SL.interactions.submitPublicEvent(collectionId, {
150
158
  appId: 'my-app',
151
- interactionId: 'nps-score',
159
+ interactionId: interactionIds.npsScore,
152
160
  outcome: '9',
153
161
  metadata: {
154
162
  anonId: SL.utils.getAnonId(), // device-level dedup signal
@@ -169,7 +177,7 @@ if (!response.success) {
169
177
  ```typescript
170
178
  await SL.interactions.updateEvent(collectionId, {
171
179
  eventId: 'evt_abc123', // Required — the event to update
172
- interactionId: 'vote',
180
+ interactionId: interactionIds.vote,
173
181
  userId: 'user_abc123',
174
182
  outcome: 'option-b', // Override the outcome
175
183
  status: 'deleted', // Soft-delete the event
@@ -180,7 +188,7 @@ await SL.interactions.updateEvent(collectionId, {
180
188
 
181
189
  | Field | Type | Required | Description |
182
190
  |-------|------|----------|-------------|
183
- | `interactionId` | string | ✅ | Existing interaction type ID (must already be defined in this collection) |
191
+ | `interactionId` | string | ✅ | The type's minted `id` (from `ensureType`, stored in app config) — never a literal you made up |
184
192
  | `userId` or `contactId` | string | ✅ (one of) | The actor. `appendEvent` / `updateEvent` require one of these |
185
193
  | `appId` | string | ❌ | Scopes the event to your app |
186
194
  | `outcome` | string | ❌ | The result or choice — what `countsByOutcome()` aggregates |
@@ -205,7 +213,7 @@ The primary analytics function — returns how many times each outcome was recor
205
213
  // Admin (full access, deduplication options)
206
214
  const results = await SL.interactions.countsByOutcome(collectionId, {
207
215
  appId: 'my-app',
208
- interactionId: 'vote',
216
+ interactionId: interactionIds.vote,
209
217
  scope: 'round-1', // Optional — filter by scope
210
218
  from: '2026-06-01', // Optional — date range
211
219
  to: '2026-06-30',
@@ -216,7 +224,7 @@ const results = await SL.interactions.countsByOutcome(collectionId, {
216
224
  // Public (respects allowPublicSummary permission)
217
225
  const results = await SL.interactions.publicCountsByOutcome(
218
226
  collectionId,
219
- { appId: 'my-app', interactionId: 'vote' },
227
+ { appId: 'my-app', interactionId: interactionIds.vote },
220
228
  authToken // Optional — pass if user is authenticated
221
229
  );
222
230
  ```
@@ -228,7 +236,7 @@ Flexible admin query for raw interaction events:
228
236
  ```typescript
229
237
  const events = await SL.interactions.query(collectionId, {
230
238
  appId: 'my-app',
231
- interactionId: 'vote',
239
+ interactionId: interactionIds.vote,
232
240
  userId: 'user_abc123', // Filter by user
233
241
  outcome: 'option-a', // Filter by outcome
234
242
  from: '2026-06-01T00:00Z',
@@ -247,7 +255,7 @@ Lets authenticated users see their own events:
247
255
  ```typescript
248
256
  const myEvents = await SL.interactions.publicMyInteractions(
249
257
  collectionId,
250
- { appId: 'my-app', interactionId: 'vote' },
258
+ { appId: 'my-app', interactionId: interactionIds.vote },
251
259
  authToken
252
260
  );
253
261
  ```
@@ -290,7 +298,7 @@ Platform emits event to Journey trigger
290
298
  Journey step runs: send confirmation email, update CRM, award points, etc.
291
299
  ```
292
300
 
293
- When defining a journey trigger, reference the `interactionId` that should fire it. The interaction `outcome` and `metadata` are available as variables in journey steps.
301
+ When defining a journey trigger, reference the interaction type (its minted `id`) that should fire it. The interaction `outcome` and `metadata` are available as variables in journey steps.
294
302
 
295
303
  ---
296
304
 
@@ -316,9 +324,9 @@ A failure in one effect is logged and swallowed; subsequent effects still run.
316
324
  Pass `data.effects` when creating or updating an interaction type:
317
325
 
318
326
  ```typescript
319
- await SL.interactions.create(collectionId, {
320
- id: 'donation',
327
+ await SL.interactions.ensureType(collectionId, {
321
328
  appId: 'my-app',
329
+ key: 'donation',
322
330
  permissions: { allowPublicSubmit: true },
323
331
  data: {
324
332
  effects: [
@@ -351,7 +359,7 @@ All effect `config` values support `{{token}}` interpolation resolved from the e
351
359
  | `{{eventId}}` | BigQuery event UUID |
352
360
  | `{{collectionId}}` | Collection / brand ID |
353
361
  | `{{appId}}` | App ID that submitted the event |
354
- | `{{interactionId}}` | Interaction definition ID |
362
+ | `{{interactionId}}` | Interaction type id (the minted UUID) |
355
363
  | `{{contactId}}` | Contact UUID (if resolved) |
356
364
  | `{{userId}}` | Firebase UID (if authenticated) |
357
365
  | `{{productId}}` | Product ID (if provided with event) |
@@ -496,7 +504,8 @@ import type {
496
504
  InteractionPermissions, // Full permissions config shape
497
505
  InteractionTypeRecord, // Definition record from create() / get()
498
506
  InteractionTypeList, // { items, limit, offset }
499
- CreateInteractionTypeBody, // Body for create()
507
+ CreateInteractionTypeBody, // Body for create() — the server mints the id
508
+ EnsureInteractionTypeInput, // Input for ensureType() — { appId, key, permissions?, display?, data? }
500
509
  UpdateInteractionTypeBody, // Body for update()
501
510
  AdminInteractionsQueryRequest, // query() filter options
502
511
  AdminInteractionsCountsByOutcomeRequest,
@@ -520,7 +529,8 @@ import type {
520
529
 
521
530
  ## Best Practices
522
531
 
523
- - Use descriptive `interactionId` values: `warranty-registration`, `competition-entry`, `newsletter-vote`
532
+ - Never hardcode an `interactionId`: create types with `ensureType` in admin, keep their ids in app config (`interactionIds.<key>`), read them everywhere else
533
+ - Use descriptive type keys: `warranty-registration`, `competition-entry`, `newsletter-vote`
524
534
  - Use `outcome` to capture the choice — it's the key field that `countsByOutcome()` aggregates on
525
535
  - Include `metadata` for richer analytics and debugging (`source`, `device`, `region`, etc.)
526
536
  - Use `uniquePerUser: true` for actions that should only happen once (votes, registrations)
@@ -280,7 +280,7 @@ See `docs/ai.md` for complete documentation.
280
280
 
281
281
  The `SL.interactions` namespace tracks user engagement — competition entries, votes, form submissions, warranty registrations. Events can trigger automated journeys and communications.
282
282
 
283
- Important: create interaction type definitions first, then submit events using those existing IDs. Do not invent `interactionId` values in client code.
283
+ **The one rule:** you never choose an interaction id. In admin, `SL.interactions.ensureType(collectionId, { appId, key: 'vote', … })` finds-or-creates the type and returns its server-minted `id`; store it in app config (`interactionIds: { vote: type.id }`); everywhere else — public widgets included — read `interactionIds.vote` from config and pass that as `interactionId`. Never write an interactionId as a string literal.
284
284
 
285
285
  Key functions: `submitPublicEvent()`, `appendEvent()` (admin), `countsByOutcome()`, `query()`.
286
286
 
@@ -8,9 +8,10 @@ get all of this from Hub.
8
8
  - **`/robots.txt`, `/sitemap.xml`, `/llms.txt`** are generated on the site's own address. Don't ship
9
9
  `robots.txt` or `sitemap.xml`: one build serves many sites, and sitemap URLs must be absolute on
10
10
  the site's host, which the build can't know.
11
- - **Declare your routes** in `public/sitemap-paths.txt`, one path per line (`/`, `/menu`,
12
- `/book`). They go into the sitemap and `llms.txt` on the right host. Pre-rendered pages
13
- (`about/index.html`) are found automatically.
11
+ - **List every page** in `public/sitemap-paths.txt`, one `<path> [Title]` per line, in menu order
12
+ (`/ Home`, `/menu Menu`, `/book Book a table`). They go into the sitemap and `llms.txt` (titled)
13
+ on the right host, and Forge's preview uses the same list as its page menu. Keep it in step with
14
+ the router. Pre-rendered pages (`about/index.html`) are found automatically.
14
15
  - **Canonical address.** A site can answer on its automatic address, a chosen name and a custom
15
16
  domain. Every page gets `Link: <https://{canonical}{path}>; rel="canonical"`, so search engines
16
17
  consolidate on one: the custom domain, else the chosen name, else the automatic address. Don't set