@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.
- package/dist/api/functions.d.ts +2 -2
- package/dist/api/functions.js +7 -3
- package/dist/api/interactions.d.ts +14 -1
- package/dist/api/interactions.js +27 -0
- package/dist/docs/API_SUMMARY.md +40 -19
- package/dist/docs/headless-providers.md +8 -0
- package/dist/docs/interactions.md +65 -55
- package/dist/docs/overview.md +1 -1
- package/dist/docs/site-seo.md +4 -3
- package/dist/headless.d.ts +15 -1
- package/dist/headless.js +26 -2
- package/dist/openapi.yaml +17 -1
- package/dist/types/interaction.d.ts +16 -1
- package/docs/API_SUMMARY.md +40 -19
- package/docs/headless-providers.md +8 -0
- package/docs/interactions.md +65 -55
- package/docs/overview.md +1 -1
- package/docs/site-seo.md +4 -3
- package/openapi.yaml +17 -1
- package/package.json +1 -1
package/dist/headless.d.ts
CHANGED
|
@@ -8,12 +8,26 @@ export interface HeadlessValidation {
|
|
|
8
8
|
ok: boolean;
|
|
9
9
|
errors: HeadlessIssue[];
|
|
10
10
|
warnings: HeadlessIssue[];
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The read recipe for each type: the declared one, else the standard one for its storage — plus what
|
|
13
|
+
* the call returns and where an item's fields are. Those two always come from the storage kind (the
|
|
14
|
+
* platform's response shape), never from the app, so a custom recipe can't leave them out.
|
|
15
|
+
*/
|
|
12
16
|
recipes: Record<string, {
|
|
13
17
|
list: string;
|
|
14
18
|
get?: string;
|
|
19
|
+
returns: string;
|
|
20
|
+
item: string;
|
|
15
21
|
}>;
|
|
16
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* What a type's read calls return, and where one item's declared fields live — fixed by the storage
|
|
25
|
+
* kind (see app-objects.md "Paginated List Responses"). Sites must read items from `response.data`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function responseShape(type: AppDataType): {
|
|
28
|
+
returns: string;
|
|
29
|
+
item: string;
|
|
30
|
+
};
|
|
17
31
|
/** The standard SDK read calls for a type, from how it's stored. `appId` / `collectionId` are the caller's variables. */
|
|
18
32
|
export declare function standardRecipe(type: AppDataType): {
|
|
19
33
|
list: string;
|
package/dist/headless.js
CHANGED
|
@@ -18,13 +18,36 @@ const PLATFORM_REFS = new Set(['product', 'contact', 'proof']);
|
|
|
18
18
|
const SEO_HELPERS = new Set(['faqPage', 'product', 'article', 'breadcrumbs', 'organization', 'localBusiness']);
|
|
19
19
|
const STORAGE_KINDS = new Set(['record', 'case', 'thread', 'config']);
|
|
20
20
|
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
21
|
+
/**
|
|
22
|
+
* What a type's read calls return, and where one item's declared fields live — fixed by the storage
|
|
23
|
+
* kind (see app-objects.md "Paginated List Responses"). Sites must read items from `response.data`.
|
|
24
|
+
*/
|
|
25
|
+
export function responseShape(type) {
|
|
26
|
+
const s = type.storage;
|
|
27
|
+
const first = Object.keys(type.fields || {})[0] || 'field';
|
|
28
|
+
const paged = (name) => `list → { data: ${name}[], pagination: { total, limit, offset, hasMore } }. The items are in response.data (not response.items / response.records); page with offset/limit while pagination.hasMore. get → one ${name}.`;
|
|
29
|
+
switch (s && s.kind) {
|
|
30
|
+
case 'record':
|
|
31
|
+
return { returns: paged('AppRecord'), item: `Each record's declared fields are in record.data (e.g. record.data.${first}); record.id, record.productId, record.status, record.createdAt are top-level.` };
|
|
32
|
+
case 'case':
|
|
33
|
+
return { returns: paged('AppCase'), item: `Each case's declared fields are in case.data (e.g. case.data.${first}); id, status, category and dates are top-level.` };
|
|
34
|
+
case 'thread':
|
|
35
|
+
return { returns: paged('AppThread'), item: `Each thread's declared fields are in thread.data (e.g. thread.data.${first}); replies are in thread.replies.` };
|
|
36
|
+
case 'config': {
|
|
37
|
+
const key = s.key;
|
|
38
|
+
return { returns: "The app's configuration object for the collection.", item: key ? `The items are in config.${key}; each item's fields are its declared fields.` : 'The declared fields are top-level properties of the config object.' };
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
return { returns: '(unknown storage)', item: '' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
21
44
|
/** The standard SDK read calls for a type, from how it's stored. `appId` / `collectionId` are the caller's variables. */
|
|
22
45
|
export function standardRecipe(type) {
|
|
23
46
|
const s = type.storage;
|
|
24
47
|
switch (s && s.kind) {
|
|
25
48
|
case 'record':
|
|
26
49
|
return {
|
|
27
|
-
list: `SL.app.records.list(collectionId, appId, { recordType: '${s.recordType}', limit: 50 })
|
|
50
|
+
list: `SL.app.records.list(collectionId, appId, { recordType: '${s.recordType}', limit: 50 })`,
|
|
28
51
|
get: 'SL.app.records.get(collectionId, appId, recordId)',
|
|
29
52
|
};
|
|
30
53
|
case 'case':
|
|
@@ -179,7 +202,8 @@ export function validate(manifest, opts = {}) {
|
|
|
179
202
|
if (samples && samples.length)
|
|
180
203
|
checkItems('real item', typeId, type, samples, { errors, warnings });
|
|
181
204
|
const std = standardRecipe(type);
|
|
182
|
-
|
|
205
|
+
const get = (type.read && type.read.get) || std.get;
|
|
206
|
+
recipes[typeId] = Object.assign(Object.assign({ list: (type.read && type.read.list) || std.list }, (get ? { get } : {})), responseShape(type));
|
|
183
207
|
}
|
|
184
208
|
}
|
|
185
209
|
// ---- headless
|
package/dist/openapi.yaml
CHANGED
|
@@ -26623,8 +26623,24 @@ components:
|
|
|
26623
26623
|
type: object
|
|
26624
26624
|
additionalProperties: true
|
|
26625
26625
|
required:
|
|
26626
|
-
- id
|
|
26627
26626
|
- appId
|
|
26627
|
+
EnsureInteractionTypeInput:
|
|
26628
|
+
type: object
|
|
26629
|
+
properties:
|
|
26630
|
+
appId:
|
|
26631
|
+
type: string
|
|
26632
|
+
key:
|
|
26633
|
+
type: string
|
|
26634
|
+
permissions:
|
|
26635
|
+
$ref: "#/components/schemas/InteractionPermissions"
|
|
26636
|
+
display:
|
|
26637
|
+
$ref: "#/components/schemas/InteractionDisplay"
|
|
26638
|
+
data:
|
|
26639
|
+
type: object
|
|
26640
|
+
additionalProperties: true
|
|
26641
|
+
required:
|
|
26642
|
+
- appId
|
|
26643
|
+
- key
|
|
26628
26644
|
UpdateInteractionTypeBody:
|
|
26629
26645
|
type: object
|
|
26630
26646
|
properties:
|
|
@@ -195,10 +195,25 @@ export interface InteractionTypeList {
|
|
|
195
195
|
limit: number;
|
|
196
196
|
offset: number;
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Body for interactions.create(). The SERVER mints the type's id (a UUID) — read it from the
|
|
200
|
+
* returned record. Prefer interactions.ensureType(), which also finds an existing type by its key.
|
|
201
|
+
*/
|
|
198
202
|
export interface CreateInteractionTypeBody {
|
|
199
|
-
id
|
|
203
|
+
/** @deprecated Ignored — the server mints the id. Use the `id` of the returned record (or ensureType). */
|
|
204
|
+
id?: string;
|
|
205
|
+
appId: string;
|
|
206
|
+
permissions?: InteractionPermissions;
|
|
207
|
+
data?: Record<string, unknown>;
|
|
208
|
+
}
|
|
209
|
+
/** Input for interactions.ensureType(): find this app's type by `key`, or create it. */
|
|
210
|
+
export interface EnsureInteractionTypeInput {
|
|
200
211
|
appId: string;
|
|
212
|
+
/** Your readable, stable name for the type, e.g. 'vote' — stored as data.interactionType. NOT the id. */
|
|
213
|
+
key: string;
|
|
201
214
|
permissions?: InteractionPermissions;
|
|
215
|
+
display?: InteractionDisplay;
|
|
216
|
+
/** Extra definition data (e.g. effects); merged under data alongside interactionType/display. */
|
|
202
217
|
data?: Record<string, unknown>;
|
|
203
218
|
}
|
|
204
219
|
export interface UpdateInteractionTypeBody {
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
package/docs/interactions.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
18
|
+
## The one rule: create in admin → store the id in config → read it everywhere
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
45
|
+
Why it's split this way:
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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 | `
|
|
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.
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 | ✅ |
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 `
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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)
|
package/docs/overview.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/docs/site-seo.md
CHANGED
|
@@ -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
|
-
- **
|
|
12
|
-
`/book`). They go into the sitemap and `llms.txt`
|
|
13
|
-
|
|
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
|