@proveanything/smartlinks 2.0.39 → 2.0.41

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.
@@ -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
 
@@ -324,6 +324,40 @@ name (a first-party builtin still wins). Always prefer an appId.
324
324
 
325
325
  ---
326
326
 
327
+ ## Inputs — data handed to your function
328
+
329
+ Declare what your function needs and the platform fetches it **before** the run: read it from `ctx.inputs` instead of calling `ctx.sl` first. One less round trip — and for functions running in the isolated runner, one less network hop.
330
+
331
+ ```jsonc
332
+ // app.manifest.json → functions.definitions[]
333
+ {
334
+ "name": "onProductChanged",
335
+ "trigger": { "type": "event", "eventTypes": ["product.updated"] },
336
+ "capabilities": ["sl:products:read", "sl:data:read"],
337
+ "inputs": { "appConfig": true } // entity is on by default for event triggers
338
+ }
339
+ ```
340
+
341
+ ```js
342
+ export async function onProductChanged(ctx, event) {
343
+ const product = ctx.inputs.entity // the product that changed
344
+ const { interactionIds } = ctx.inputs.appConfig || {} // this app's config for the collection
345
+ }
346
+ ```
347
+
348
+ | Input | What you get | Needs | Triggers |
349
+ |---|---|---|---|
350
+ | `entity` | The record the event is about: the product, your app's record, or the interaction event. **On by default**; `"entity": false` opts out. | read on that resource (`sl:products:read`, `sl:records:read`) | event |
351
+ | `collection` | The collection's public record | — | all |
352
+ | `appConfig` | Your app's config for this collection (where setup like `interactionIds` lives) | `sl:data:read` | all |
353
+ | `product` | A product named by the request: `{ "from": "body.productId" }`, `"query.<field>"` or `"path"` | `sl:products:read` | http |
354
+
355
+ - **Same rules as `ctx.sl`.** Inputs are fetched with your function's authority and capabilities, scoped to the collection it runs in — a product from another collection is never fetched.
356
+ - **Never an error.** Not found, not allowed or larger than 256 KB → that input is `null`. Check before you use it.
357
+ - **Current state.** Event inputs are fetched when your function runs, so you see the record as it is now; `event` still carries the ids.
358
+ - **Checked at publish.** An unknown input, a bad `from`, or a missing capability fails validation.
359
+ - **Testing:** pass `inputs` to the test context — `createFunctionTestContext({ def, inputs: { entity: { id: 'p1', name: 'Kettle' } } })`.
360
+
327
361
  ## Exposing a function to the AI agent
328
362
 
329
363
  An `http` function can be offered to the AI agent as a **callable tool**, alongside the built-in
package/dist/http.d.ts CHANGED
@@ -71,6 +71,11 @@ export declare function initializeApi(options: {
71
71
  awaitAuth?: boolean;
72
72
  /** How long (ms) to wait for the async token before letting requests proceed anyway. Default 8000. */
73
73
  awaitAuthTimeoutMs?: number;
74
+ /**
75
+ * proxyMode only: how long (ms) a request waits for the parent host's reply before failing with a
76
+ * 504 SmartlinksApiError (errorCode PROXY_TIMEOUT). Default 90000. 0 = wait indefinitely.
77
+ */
78
+ proxyTimeoutMs?: number;
74
79
  iframeAutoResize?: boolean;
75
80
  logger?: Logger;
76
81
  /**
package/dist/http.js CHANGED
@@ -31,6 +31,10 @@ let baseURL = null;
31
31
  let apiKey = undefined;
32
32
  let bearerToken = undefined;
33
33
  let proxyMode = false;
34
+ // How long a request through the parent (proxyMode) waits for the host's reply. Without a limit, a host
35
+ // that never answers (no responder, a dropped message) leaves the call pending forever: no network
36
+ // request in the frame, no error. 90 s covers the longest platform call (a 60 s function deadline).
37
+ let proxyTimeoutMs = 90000;
34
38
  let ngrokSkipBrowserWarning = false;
35
39
  let extraHeadersGlobal = {};
36
40
  /**
@@ -522,6 +526,8 @@ export function initializeApi(options) {
522
526
  });
523
527
  }
524
528
  proxyMode = !!options.proxyMode;
529
+ if (options.proxyTimeoutMs !== undefined)
530
+ proxyTimeoutMs = Math.max(0, Number(options.proxyTimeoutMs) || 0);
525
531
  // Auto-enable ngrok skip header if domain contains .ngrok.io and user did not explicitly set the flag.
526
532
  // Infer ngrok usage from common domains (.ngrok.io or .ngrok-free.dev)
527
533
  const inferredNgrok = /(\.ngrok\.io|\.ngrok-free\.dev)(\b|\/)/i.test(baseURL);
@@ -1082,11 +1088,30 @@ async function proxyRequest(method, path, body, headers, options) {
1082
1088
  };
1083
1089
  logDebug('[smartlinks] proxy:postMessage', { id, method, path, headers: headers ? redactHeaders(headers) : undefined, hasBody: !!body });
1084
1090
  return new Promise((resolve, reject) => {
1085
- proxyPending[id] = { resolve, reject };
1091
+ proxyPending[id] = withProxyTimeout(id, `${method} ${path}`, resolve, reject);
1086
1092
  window.parent.postMessage(msg, "*");
1087
- // Optionally: add a timeout here to reject if no response
1088
1093
  });
1089
1094
  }
1095
+ /**
1096
+ * Wrap a pending proxy request's resolve/reject with the proxy timeout (proxyTimeoutMs): if the host
1097
+ * hasn't answered by then, the request fails with a 504 PROXY_TIMEOUT instead of hanging forever.
1098
+ */
1099
+ function withProxyTimeout(id, what, resolve, reject) {
1100
+ if (!(proxyTimeoutMs > 0))
1101
+ return { resolve, reject };
1102
+ const timer = setTimeout(() => {
1103
+ if (!proxyPending[id])
1104
+ return;
1105
+ delete proxyPending[id];
1106
+ const message = `The host page did not answer ${what} within ${proxyTimeoutMs >= 1000 ? `${Math.round(proxyTimeoutMs / 1000)}s` : `${proxyTimeoutMs}ms`} (proxyMode). The parent may not be handling SmartLinks proxy requests.`;
1107
+ logDebug('[smartlinks] proxy:timeout', { id, what });
1108
+ reject(new SmartlinksApiError(message, 504, { code: 504, errorCode: 'PROXY_TIMEOUT', message }));
1109
+ }, proxyTimeoutMs);
1110
+ return {
1111
+ resolve: (data) => { clearTimeout(timer); resolve(data); },
1112
+ reject: (err) => { clearTimeout(timer); reject(err); },
1113
+ };
1114
+ }
1090
1115
  /**
1091
1116
  * Upload a FormData payload via proxy with progress events using chunked postMessage.
1092
1117
  * Parent is expected to implement the counterpart protocol.
@@ -1794,8 +1819,7 @@ export async function sendCustomProxyMessage(request, params) {
1794
1819
  };
1795
1820
  logDebug('[smartlinks] proxy:custom postMessage', { id, request, params: safeBodyPreview(params) });
1796
1821
  return new Promise((resolve, reject) => {
1797
- proxyPending[id] = { resolve, reject };
1822
+ proxyPending[id] = withProxyTimeout(id, `custom request ${request}`, resolve, reject);
1798
1823
  window.parent.postMessage(msg, "*");
1799
- // Optionally: add a timeout here to reject if no response
1800
1824
  });
1801
1825
  }
package/dist/openapi.yaml CHANGED
@@ -19124,6 +19124,8 @@ components:
19124
19124
  enum:
19125
19125
  - collection
19126
19126
  - global
19127
+ inputs:
19128
+ $ref: "#/components/schemas/AppFunctionInputs"
19127
19129
  apiVersion:
19128
19130
  type: string
19129
19131
  handler:
@@ -19233,6 +19235,8 @@ components:
19233
19235
  fetch:
19234
19236
  type: object
19235
19237
  additionalProperties: true
19238
+ inputs:
19239
+ $ref: "#/components/schemas/ServerFunctionInputs"
19236
19240
  required:
19237
19241
  - collectionId
19238
19242
  - appId
@@ -19240,6 +19244,34 @@ components:
19240
19244
  - secrets
19241
19245
  - caller
19242
19246
  - fetch
19247
+ - inputs
19248
+ AppFunctionInputs:
19249
+ type: object
19250
+ properties:
19251
+ entity:
19252
+ type: boolean
19253
+ collection:
19254
+ type: boolean
19255
+ appConfig:
19256
+ type: boolean
19257
+ product:
19258
+ type: object
19259
+ additionalProperties: true
19260
+ ServerFunctionInputs:
19261
+ type: object
19262
+ properties:
19263
+ entity:
19264
+ type: object
19265
+ additionalProperties: true
19266
+ collection:
19267
+ type: object
19268
+ additionalProperties: true
19269
+ appConfig:
19270
+ type: object
19271
+ additionalProperties: true
19272
+ product:
19273
+ type: object
19274
+ additionalProperties: true
19243
19275
  ServerFunctionHttpEvent:
19244
19276
  type: object
19245
19277
  properties:
@@ -26623,8 +26655,24 @@ components:
26623
26655
  type: object
26624
26656
  additionalProperties: true
26625
26657
  required:
26626
- - id
26627
26658
  - appId
26659
+ EnsureInteractionTypeInput:
26660
+ type: object
26661
+ properties:
26662
+ appId:
26663
+ type: string
26664
+ key:
26665
+ type: string
26666
+ permissions:
26667
+ $ref: "#/components/schemas/InteractionPermissions"
26668
+ display:
26669
+ $ref: "#/components/schemas/InteractionDisplay"
26670
+ data:
26671
+ type: object
26672
+ additionalProperties: true
26673
+ required:
26674
+ - appId
26675
+ - key
26628
26676
  UpdateInteractionTypeBody:
26629
26677
  type: object
26630
26678
  properties:
@@ -1,4 +1,4 @@
1
- import type { AppFunctionDef, ServerFunctionContext } from '../types/appManifest.js';
1
+ import type { AppFunctionDef, ServerFunctionContext, ServerFunctionInputs } from '../types/appManifest.js';
2
2
  export declare class CapabilityError extends Error {
3
3
  capability: string;
4
4
  code: string;
@@ -62,6 +62,8 @@ export interface CreateFunctionTestContextOptions {
62
62
  sl?: TestSlImpl;
63
63
  /** Backing fetch (defaults to global fetch). Still gated by the `network` capability. */
64
64
  fetch?: typeof fetch;
65
+ /** What the platform would have prefetched as `ctx.inputs` (e.g. `{ entity: product }`). Default `{}`. */
66
+ inputs?: ServerFunctionInputs;
65
67
  }
66
68
  export interface FunctionTestContext extends ServerFunctionContext {
67
69
  /** Captured log lines (also written via ctx.log). */
@@ -176,6 +176,7 @@ export function createFunctionTestContext(opts) {
176
176
  log: (message, data) => {
177
177
  logs.push(Object.assign({ at: new Date().toISOString(), message: String(message) }, (data ? { data } : {})));
178
178
  },
179
+ inputs: Object.freeze(Object.assign({}, (opts.inputs || {}))),
179
180
  logs,
180
181
  };
181
182
  }
@@ -268,6 +268,14 @@ export interface AppFunctionDef {
268
268
  * Per-call `{ scope }` and the `appData.global` / `appData.collection` handles override this.
269
269
  */
270
270
  dataScope?: 'collection' | 'global';
271
+ /**
272
+ * Data the platform fetches BEFORE the run and hands over as `ctx.inputs` — no round trip from your
273
+ * code (for isolated functions, no network hop). Same authority and capabilities as the equivalent
274
+ * `ctx.sl` read; a missing, denied or oversized (> 256 KB) input arrives as `null`, never an error.
275
+ * Event-trigger functions get `entity` (the record the event is about) by default; `entity: false`
276
+ * opts out. See server-functions.md → "Inputs".
277
+ */
278
+ inputs?: AppFunctionInputs;
271
279
  /** SDK/manifest API version this function targets (pinned for runtime compatibility). */
272
280
  apiVersion?: string;
273
281
  /** Exported handler name in the functions bundle. Defaults to `name`. */
@@ -403,6 +411,29 @@ export interface ServerFunctionContext {
403
411
  fetch: typeof fetch;
404
412
  /** Structured logging captured into run telemetry. */
405
413
  log: (message: string, data?: Record<string, any>) => void;
414
+ /** Data prefetched for this run, per the definition's `inputs` (read-only; `{}` when none). */
415
+ inputs: Readonly<ServerFunctionInputs>;
416
+ }
417
+ /** What a function asks to have prefetched (`AppFunctionDef.inputs`). */
418
+ export interface AppFunctionInputs {
419
+ /** Event triggers: the record the event is about — product (needs `sl:products:read`), app record of
420
+ * this app (`sl:records:read`), or the interaction event itself. On by default; `false` opts out. */
421
+ entity?: boolean;
422
+ /** The collection's public record. */
423
+ collection?: boolean;
424
+ /** This app's config for the collection — e.g. its `interactionIds`. Needs `sl:data:read`. */
425
+ appConfig?: boolean;
426
+ /** Http triggers: a product named by the request. Needs `sl:products:read`. */
427
+ product?: {
428
+ from: `body.${string}` | `query.${string}` | 'path';
429
+ };
430
+ }
431
+ /** `ctx.inputs` — what was prefetched for this run (keys you declared; `null` when unavailable). */
432
+ export interface ServerFunctionInputs {
433
+ entity?: Record<string, any> | null;
434
+ collection?: Record<string, any> | null;
435
+ appConfig?: Record<string, any> | null;
436
+ product?: Record<string, any> | null;
406
437
  }
407
438
  /** The HTTP request handed to an `http`-trigger function as `event`. */
408
439
  export interface ServerFunctionHttpEvent<TBody = any> {
@@ -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: string;
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 {