@proveanything/smartlinks 2.0.40 → 2.0.42
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/secrets.d.ts +21 -1
- package/dist/api/secrets.js +40 -6
- package/dist/docs/API_SUMMARY.md +43 -2
- package/dist/docs/iframe-responder.md +310 -308
- package/dist/docs/integrations.md +1 -1
- package/dist/docs/server-functions.md +135 -5
- package/dist/http.d.ts +5 -0
- package/dist/http.js +28 -4
- package/dist/openapi.yaml +39 -0
- package/dist/testing/index.d.ts +3 -1
- package/dist/testing/index.js +1 -0
- package/dist/types/appManifest.d.ts +37 -1
- package/docs/API_SUMMARY.md +43 -2
- package/docs/iframe-responder.md +310 -308
- package/docs/integrations.md +1 -1
- package/docs/server-functions.md +135 -5
- package/openapi.yaml +39 -0
- package/package.json +1 -1
- package/scripts/doctor.mjs +6 -3
|
@@ -167,10 +167,10 @@ Declare the minimum you need.
|
|
|
167
167
|
| `sl:<resource>:read` / `sl:<resource>:write` | SDK access to that resource, e.g. `sl:products:read`, `sl:records:write`, `sl:attestations:write`, `sl:contacts:write`. |
|
|
168
168
|
| `network` | `ctx.fetch` to any host. |
|
|
169
169
|
| `network:<host>` | `ctx.fetch` to that host only (repeat for several). Prefer this over blanket `network`. |
|
|
170
|
-
| `secrets:<
|
|
170
|
+
| `secrets:<name>` | `ctx.secrets.get('<name>')` for that one secret (see [Secrets](#secrets--api-keys-per-collection)). |
|
|
171
171
|
|
|
172
|
-
If you don't declare `network`, `ctx.fetch` is absent.
|
|
173
|
-
`
|
|
172
|
+
If you don't declare `network`, `ctx.fetch` is absent. Reading a secret you didn't declare with
|
|
173
|
+
`secrets:<name>` throws.
|
|
174
174
|
|
|
175
175
|
---
|
|
176
176
|
|
|
@@ -186,8 +186,8 @@ interface ServerFunctionContext {
|
|
|
186
186
|
* data — URLs, flags, counters, arrays), `sl.products`, `sl.attestations`. */
|
|
187
187
|
sl: SmartLinks
|
|
188
188
|
|
|
189
|
-
/** Capability-gated secrets (need `secrets:<
|
|
190
|
-
*
|
|
189
|
+
/** Capability-gated secrets (need `secrets:<name>`). `get(name)` reads this app's own secret on the
|
|
190
|
+
* collection first, then the app-level one; `app(name)` reads the app-level secret only. */
|
|
191
191
|
secrets: { get(ref: string): Promise<string | null>; app(ref: string): Promise<string | null> }
|
|
192
192
|
|
|
193
193
|
/** Who invoked this function. */
|
|
@@ -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
|
|
@@ -527,6 +561,102 @@ other secrets, or any other collection.
|
|
|
527
561
|
|
|
528
562
|
---
|
|
529
563
|
|
|
564
|
+
## Secrets — API keys per collection
|
|
565
|
+
|
|
566
|
+
A function that calls another system needs that system's key, and each collection (each customer)
|
|
567
|
+
has its own. Your app's **admin screen** saves it; your **functions** read it by the same name.
|
|
568
|
+
|
|
569
|
+
**1. Declare it** on every function that reads it:
|
|
570
|
+
|
|
571
|
+
```jsonc
|
|
572
|
+
{ "name": "pullOrders", "trigger": { "type": "cron", "schedule": "0,30 * * * *" },
|
|
573
|
+
"capabilities": ["network:api.example-shop.com", "secrets:shop-api-key"] }
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**2. Save it from your admin screen**, signed in as a collection admin:
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
await SL.secrets.put(collectionId, 'shop-api-key', apiKeyFromTheForm) // create or replace
|
|
580
|
+
const { secrets } = await SL.secrets.listOwn(collectionId) // names + masked hints, e.g. "••••1234"
|
|
581
|
+
await SL.secrets.removeOwn(collectionId, 'shop-api-key')
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The app comes from the SDK's app context (`initializeApi({ appId })`), or pass `{ appId }` as the
|
|
585
|
+
last argument. Secrets are write-only: nothing ever returns the value, so show the hint ("saved,
|
|
586
|
+
ends 1234") and offer "replace", not "edit". Build this into your own settings screen; the platform
|
|
587
|
+
doesn't prompt for it at install.
|
|
588
|
+
|
|
589
|
+
**3. Read it in the function:**
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
const key = await ctx.secrets.get('shop-api-key') // null until an admin has saved it
|
|
593
|
+
if (!key) { ctx.log('shop not connected yet'); return { skipped: 'not connected' } }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Each app's secrets are its own.** `shop-api-key` saved by your app and `shop-api-key` saved by
|
|
597
|
+
another app on the same collection are two different secrets; your functions only ever read yours.
|
|
598
|
+
|
|
599
|
+
`ctx.secrets.get(name)` looks in this order:
|
|
600
|
+
1. your app's secret on this collection (saved with `SL.secrets.put`);
|
|
601
|
+
2. *SmartLinks' own apps only:* a collection-wide secret of that name. Third-party apps never read these;
|
|
602
|
+
3. your app's shared, app-level secret (the same for every collection), if Forge has one for it.
|
|
603
|
+
|
|
604
|
+
Don't use `SL.secrets.set` for this. It creates a collection secret with a **generated** ref
|
|
605
|
+
(`sec_…`) for integration flows, which a function can't name in its manifest.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Scheduled functions (cron)
|
|
610
|
+
|
|
611
|
+
A `cron` function runs on a schedule, on **every collection that has the app installed**, with
|
|
612
|
+
`collection` authority (there's no caller). It's the shape for polling another system ("pull new
|
|
613
|
+
orders every 15 minutes"), nightly reconciles, digests and clean-ups.
|
|
614
|
+
|
|
615
|
+
```jsonc
|
|
616
|
+
{
|
|
617
|
+
"name": "pullOrders",
|
|
618
|
+
"trigger": { "type": "cron", "schedule": "*/15 * * * *" },
|
|
619
|
+
"capabilities": ["sl:records:write", "network:api.example-shop.com", "secrets:shop-api-key"]
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
"name": "morningDigest",
|
|
623
|
+
"trigger": { "type": "cron", "schedule": "0 9 * * MON-FRI", "timezone": "Europe/London" },
|
|
624
|
+
"capabilities": ["sl:records:read", "network:hooks.slack.com", "secrets:slack-webhook"]
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
- **`schedule`** — standard 5-field cron: minute, hour, day-of-month, month, day-of-week. Lists,
|
|
629
|
+
ranges and steps (`0,30`, `9-17`, `*/15`), month and day names (`JAN`, `MON-FRI`; Sunday is 0 or 7),
|
|
630
|
+
and `@hourly` / `@daily` / `@weekly` / `@monthly` / `@yearly`. When both day-of-month and
|
|
631
|
+
day-of-week are restricted, a day matching either fires (as in cron).
|
|
632
|
+
- **`timezone`** *(optional)* — an IANA zone like `"Europe/London"`; the schedule follows its
|
|
633
|
+
summer time. Default UTC.
|
|
634
|
+
- **At most every 5 minutes.** A faster schedule, a bad expression or an unknown zone fails the
|
|
635
|
+
release (`SCHEDULE_INVALID`), so it never silently doesn't run.
|
|
636
|
+
|
|
637
|
+
**What runs:** each installation runs the current release of the channel it's installed on, so a
|
|
638
|
+
test collection with the app on `dev` runs your dev build on its schedule. Disabled installs and
|
|
639
|
+
expired dev installs don't run.
|
|
640
|
+
|
|
641
|
+
**The event** your handler receives:
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
{ type: 'cron', schedule: '*/15 * * * *', timezone: null, scheduledAt: '2026-10-06T10:15:00.000Z' }
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
`scheduledAt` is the minute the run was due, which is not always the moment it starts: runs queue,
|
|
648
|
+
and a late one still carries its own minute. Use it, not `Date.now()`, as the end of the window
|
|
649
|
+
you're syncing.
|
|
650
|
+
|
|
651
|
+
**Write it to be safe to run twice.** Each (collection, function, minute) is enqueued once, but a
|
|
652
|
+
run that fails part-way can be retried, and a slow run can still be going when the next one starts.
|
|
653
|
+
Keep a cursor (last `scheduledAt` or the other system's own cursor) in the app's records or config,
|
|
654
|
+
and upsert by the other system's ID instead of inserting blindly.
|
|
655
|
+
|
|
656
|
+
Each run is logged like any other function run, and a failure shows in the collection's Errors view.
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
530
660
|
## Invoking an http function
|
|
531
661
|
|
|
532
662
|
An `http` function is called by POSTing to the app-scoped functions endpoint on the
|
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
|
@@ -19086,6 +19086,8 @@ components:
|
|
|
19086
19086
|
type: string
|
|
19087
19087
|
schedule:
|
|
19088
19088
|
type: string
|
|
19089
|
+
timezone:
|
|
19090
|
+
type: string
|
|
19089
19091
|
route:
|
|
19090
19092
|
type: string
|
|
19091
19093
|
methods:
|
|
@@ -19124,6 +19126,8 @@ components:
|
|
|
19124
19126
|
enum:
|
|
19125
19127
|
- collection
|
|
19126
19128
|
- global
|
|
19129
|
+
inputs:
|
|
19130
|
+
$ref: "#/components/schemas/AppFunctionInputs"
|
|
19127
19131
|
apiVersion:
|
|
19128
19132
|
type: string
|
|
19129
19133
|
handler:
|
|
@@ -19233,6 +19237,8 @@ components:
|
|
|
19233
19237
|
fetch:
|
|
19234
19238
|
type: object
|
|
19235
19239
|
additionalProperties: true
|
|
19240
|
+
inputs:
|
|
19241
|
+
$ref: "#/components/schemas/ServerFunctionInputs"
|
|
19236
19242
|
required:
|
|
19237
19243
|
- collectionId
|
|
19238
19244
|
- appId
|
|
@@ -19240,6 +19246,34 @@ components:
|
|
|
19240
19246
|
- secrets
|
|
19241
19247
|
- caller
|
|
19242
19248
|
- fetch
|
|
19249
|
+
- inputs
|
|
19250
|
+
AppFunctionInputs:
|
|
19251
|
+
type: object
|
|
19252
|
+
properties:
|
|
19253
|
+
entity:
|
|
19254
|
+
type: boolean
|
|
19255
|
+
collection:
|
|
19256
|
+
type: boolean
|
|
19257
|
+
appConfig:
|
|
19258
|
+
type: boolean
|
|
19259
|
+
product:
|
|
19260
|
+
type: object
|
|
19261
|
+
additionalProperties: true
|
|
19262
|
+
ServerFunctionInputs:
|
|
19263
|
+
type: object
|
|
19264
|
+
properties:
|
|
19265
|
+
entity:
|
|
19266
|
+
type: object
|
|
19267
|
+
additionalProperties: true
|
|
19268
|
+
collection:
|
|
19269
|
+
type: object
|
|
19270
|
+
additionalProperties: true
|
|
19271
|
+
appConfig:
|
|
19272
|
+
type: object
|
|
19273
|
+
additionalProperties: true
|
|
19274
|
+
product:
|
|
19275
|
+
type: object
|
|
19276
|
+
additionalProperties: true
|
|
19243
19277
|
ServerFunctionHttpEvent:
|
|
19244
19278
|
type: object
|
|
19245
19279
|
properties:
|
|
@@ -30749,6 +30783,11 @@ components:
|
|
|
30749
30783
|
type: string
|
|
30750
30784
|
host:
|
|
30751
30785
|
type: string
|
|
30786
|
+
AppSecretOptions:
|
|
30787
|
+
type: object
|
|
30788
|
+
properties:
|
|
30789
|
+
appId:
|
|
30790
|
+
type: string
|
|
30752
30791
|
AllocateSequenceInput:
|
|
30753
30792
|
type: object
|
|
30754
30793
|
properties:
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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). */
|
package/dist/testing/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -213,8 +213,13 @@ export interface AppFunctionTrigger {
|
|
|
213
213
|
type: AppFunctionTriggerType;
|
|
214
214
|
/** `event`: event types this function subscribes to, e.g. `['interaction.submitted:comp-entry']`. */
|
|
215
215
|
eventTypes?: string[];
|
|
216
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* `cron`: standard 5-field crontab expression (`"0,30 * * * *"`, `"0 9 * * MON-FRI"`, or `@hourly` /
|
|
218
|
+
* `@daily` / `@weekly` / `@monthly`), evaluated in UTC unless `timezone` is set. At most every 5 minutes.
|
|
219
|
+
*/
|
|
217
220
|
schedule?: string;
|
|
221
|
+
/** `cron`: IANA time zone the schedule is read in, e.g. `"Europe/London"` (follows summer time). Default UTC. */
|
|
222
|
+
timezone?: string;
|
|
218
223
|
/** `http`: URL path segment the function is exposed at (defaults to the function `name`). */
|
|
219
224
|
route?: string;
|
|
220
225
|
/** `http`: accepted HTTP methods (defaults to `['POST']`). */
|
|
@@ -268,6 +273,14 @@ export interface AppFunctionDef {
|
|
|
268
273
|
* Per-call `{ scope }` and the `appData.global` / `appData.collection` handles override this.
|
|
269
274
|
*/
|
|
270
275
|
dataScope?: 'collection' | 'global';
|
|
276
|
+
/**
|
|
277
|
+
* Data the platform fetches BEFORE the run and hands over as `ctx.inputs` — no round trip from your
|
|
278
|
+
* code (for isolated functions, no network hop). Same authority and capabilities as the equivalent
|
|
279
|
+
* `ctx.sl` read; a missing, denied or oversized (> 256 KB) input arrives as `null`, never an error.
|
|
280
|
+
* Event-trigger functions get `entity` (the record the event is about) by default; `entity: false`
|
|
281
|
+
* opts out. See server-functions.md → "Inputs".
|
|
282
|
+
*/
|
|
283
|
+
inputs?: AppFunctionInputs;
|
|
271
284
|
/** SDK/manifest API version this function targets (pinned for runtime compatibility). */
|
|
272
285
|
apiVersion?: string;
|
|
273
286
|
/** Exported handler name in the functions bundle. Defaults to `name`. */
|
|
@@ -403,6 +416,29 @@ export interface ServerFunctionContext {
|
|
|
403
416
|
fetch: typeof fetch;
|
|
404
417
|
/** Structured logging captured into run telemetry. */
|
|
405
418
|
log: (message: string, data?: Record<string, any>) => void;
|
|
419
|
+
/** Data prefetched for this run, per the definition's `inputs` (read-only; `{}` when none). */
|
|
420
|
+
inputs: Readonly<ServerFunctionInputs>;
|
|
421
|
+
}
|
|
422
|
+
/** What a function asks to have prefetched (`AppFunctionDef.inputs`). */
|
|
423
|
+
export interface AppFunctionInputs {
|
|
424
|
+
/** Event triggers: the record the event is about — product (needs `sl:products:read`), app record of
|
|
425
|
+
* this app (`sl:records:read`), or the interaction event itself. On by default; `false` opts out. */
|
|
426
|
+
entity?: boolean;
|
|
427
|
+
/** The collection's public record. */
|
|
428
|
+
collection?: boolean;
|
|
429
|
+
/** This app's config for the collection — e.g. its `interactionIds`. Needs `sl:data:read`. */
|
|
430
|
+
appConfig?: boolean;
|
|
431
|
+
/** Http triggers: a product named by the request. Needs `sl:products:read`. */
|
|
432
|
+
product?: {
|
|
433
|
+
from: `body.${string}` | `query.${string}` | 'path';
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/** `ctx.inputs` — what was prefetched for this run (keys you declared; `null` when unavailable). */
|
|
437
|
+
export interface ServerFunctionInputs {
|
|
438
|
+
entity?: Record<string, any> | null;
|
|
439
|
+
collection?: Record<string, any> | null;
|
|
440
|
+
appConfig?: Record<string, any> | null;
|
|
441
|
+
product?: Record<string, any> | null;
|
|
406
442
|
}
|
|
407
443
|
/** The HTTP request handed to an `http`-trigger function as `event`. */
|
|
408
444
|
export interface ServerFunctionHttpEvent<TBody = any> {
|
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.42 | Generated: 2026-10-06T15:17:04.562Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2523,6 +2523,7 @@ interface AppFunctionTrigger {
|
|
|
2523
2523
|
type: AppFunctionTriggerType;
|
|
2524
2524
|
eventTypes?: string[];
|
|
2525
2525
|
schedule?: string;
|
|
2526
|
+
timezone?: string;
|
|
2526
2527
|
route?: string;
|
|
2527
2528
|
methods?: Array<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>;
|
|
2528
2529
|
}
|
|
@@ -2539,6 +2540,7 @@ interface AppFunctionDef {
|
|
|
2539
2540
|
elevated?: boolean;
|
|
2540
2541
|
capabilities?: string[];
|
|
2541
2542
|
dataScope?: 'collection' | 'global';
|
|
2543
|
+
inputs?: AppFunctionInputs;
|
|
2542
2544
|
apiVersion?: string;
|
|
2543
2545
|
handler?: string;
|
|
2544
2546
|
agent?: AppFunctionAgentExposure;
|
|
@@ -2622,6 +2624,27 @@ interface ServerFunctionContext {
|
|
|
2622
2624
|
caller: ServerFunctionCaller;
|
|
2623
2625
|
fetch: typeof fetch;
|
|
2624
2626
|
log: (message: string, data?: Record<string, any>) => void;
|
|
2627
|
+
inputs: Readonly<ServerFunctionInputs>;
|
|
2628
|
+
}
|
|
2629
|
+
```
|
|
2630
|
+
|
|
2631
|
+
**AppFunctionInputs** (interface)
|
|
2632
|
+
```typescript
|
|
2633
|
+
interface AppFunctionInputs {
|
|
2634
|
+
entity?: boolean;
|
|
2635
|
+
collection?: boolean;
|
|
2636
|
+
appConfig?: boolean;
|
|
2637
|
+
product?: { from: `body.${string}` | `query.${string}` | 'path' };
|
|
2638
|
+
}
|
|
2639
|
+
```
|
|
2640
|
+
|
|
2641
|
+
**ServerFunctionInputs** (interface)
|
|
2642
|
+
```typescript
|
|
2643
|
+
interface ServerFunctionInputs {
|
|
2644
|
+
entity?: Record<string, any> | null;
|
|
2645
|
+
collection?: Record<string, any> | null;
|
|
2646
|
+
appConfig?: Record<string, any> | null;
|
|
2647
|
+
product?: Record<string, any> | null;
|
|
2625
2648
|
}
|
|
2626
2649
|
```
|
|
2627
2650
|
|
|
@@ -9697,6 +9720,15 @@ interface FunctionSiteUrlOptions {
|
|
|
9697
9720
|
|
|
9698
9721
|
**FunctionCallResult** = `any`
|
|
9699
9722
|
|
|
9723
|
+
### secrets (api)
|
|
9724
|
+
|
|
9725
|
+
**AppSecretOptions** (interface)
|
|
9726
|
+
```typescript
|
|
9727
|
+
interface AppSecretOptions {
|
|
9728
|
+
appId?: string
|
|
9729
|
+
}
|
|
9730
|
+
```
|
|
9731
|
+
|
|
9700
9732
|
### sequence (api)
|
|
9701
9733
|
|
|
9702
9734
|
**AllocateSequenceInput** (interface)
|
|
@@ -12026,11 +12058,20 @@ Fetch + extract a web page: clean markdown, page metadata, and any schema.org JS
|
|
|
12026
12058
|
|
|
12027
12059
|
### secrets
|
|
12028
12060
|
|
|
12061
|
+
**put**(collectionId: string, name: string, value: string, opts: AppSecretOptions = {}) → `Promise<SetSecretResult>`
|
|
12062
|
+
Save this app's secret `name` on the collection (create or replace) — what the app's server functions read with `ctx.secrets.get(name)` (declare `secrets:<name>` in the manifest). Call it from your admin screen as a collection admin. Write-only: returns `{ ref: name, hint }`, never the value. PUT /app/:appId/secrets/:name
|
|
12063
|
+
|
|
12064
|
+
**listOwn**(collectionId: string, opts: AppSecretOptions = {}) → `Promise<SecretList>`
|
|
12065
|
+
This app's secrets on the collection: names + masked hints (never values). GET /app/:appId/secrets
|
|
12066
|
+
|
|
12067
|
+
**removeOwn**(collectionId: string, name: string, opts: AppSecretOptions = {}) → `Promise<`
|
|
12068
|
+
Delete this app's secret `name`. DELETE /app/:appId/secrets/:name
|
|
12069
|
+
|
|
12029
12070
|
**list**(collectionId: string, query: ListSecretsQuery = {}) → `Promise<SecretList>`
|
|
12030
12071
|
List secrets as refs + masked hints + metadata (never values). GET /secrets
|
|
12031
12072
|
|
|
12032
12073
|
**set**(collectionId: string, input: SetSecretInput) → `Promise<SetSecretResult>`
|
|
12033
|
-
Create a secret. POST /secrets → { ref, hint }. Store the ref on a flow.
|
|
12074
|
+
Create a collection secret with a GENERATED ref. POST /secrets → { ref, hint }. Store the ref on a flow. For a secret your app's server functions read by name, use `put` instead.
|
|
12034
12075
|
|
|
12035
12076
|
**get**(collectionId: string, ref: string) → `Promise<SecretMeta>`
|
|
12036
12077
|
Metadata for one secret (never the value). GET /secrets/:ref
|