@proveanything/smartlinks 1.17.5 → 1.17.6

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.
@@ -0,0 +1,234 @@
1
+ # Server functions ("edge functions")
2
+
3
+ A **server function** is arbitrary server-side JavaScript your app deploys directly into
4
+ SmartLinks. It runs on the SmartLinks servers — with access to the full SDK, to your app's
5
+ secrets, and to outbound network — so you can do things a browser app can't: validate and
6
+ write on the server, call third-party systems with credentials the client never sees, react
7
+ to events, and run scheduled work.
8
+
9
+ Every server function has the **same shape**, no matter how it's triggered or where it runs:
10
+
11
+ ```ts
12
+ export async function myFunction(ctx, event) {
13
+ // ctx — a SmartLinks SDK pre-scoped to your declared authority, plus secrets, caller, fetch, log
14
+ // event — the trigger payload (the HTTP body, the event, or the cron tick)
15
+ return { ok: true } // returned to the caller (http) or recorded as the run result (event/cron)
16
+ }
17
+ ```
18
+
19
+ You never receive a raw API key or a superuser client. The platform builds `ctx` fresh for
20
+ each invocation and **pre-scopes every handle to exactly what your function declared** — this
21
+ is how a function stays safe even when it runs with elevated authority.
22
+
23
+ ---
24
+
25
+ ## Declaring a function
26
+
27
+ Functions are declared in your `app.manifest.json` under `functions`, and the handlers ship
28
+ in a bundle alongside your widgets/containers:
29
+
30
+ ```jsonc
31
+ {
32
+ "functions": {
33
+ "files": { "js": { "umd": "dist/functions.umd.js" } },
34
+ "definitions": [
35
+ {
36
+ "name": "submitCompetitionEntry",
37
+ "description": "Validate a competition entry and record it server-side.",
38
+ "trigger": { "type": "http", "methods": ["POST"] },
39
+ "visibility": "public", // WHO may call it
40
+ "authority": "collection", // WHOSE authority it runs as
41
+ "capabilities": ["sl:records:write", "network:api.recaptcha.net"],
42
+ "apiVersion": "2026-09"
43
+ },
44
+ {
45
+ "name": "onProofCreated",
46
+ "trigger": { "type": "event", "eventTypes": ["proof.created"] },
47
+ "capabilities": ["sl:records:write", "secrets:crm-key", "network"]
48
+ },
49
+ {
50
+ "name": "nightlyReconcile",
51
+ "trigger": { "type": "cron", "schedule": "0 2 * * *" },
52
+ "capabilities": ["sl:products:read", "sl:records:write"]
53
+ }
54
+ ]
55
+ }
56
+ }
57
+ ```
58
+
59
+ Each definition maps to an exported handler of the same `name` (override with `handler`).
60
+
61
+ ---
62
+
63
+ ## The two security questions every function answers
64
+
65
+ Server-side code needs two questions answered up front. SmartLinks makes both **explicit and
66
+ declarative** — you state them in the manifest, and the platform enforces them. This is the
67
+ same split every extensible platform lands on (Salesforce `with/without sharing`, Shopify
68
+ online/offline tokens, Lambda's invoke-policy vs execution-role).
69
+
70
+ ### 1. `visibility` — who is allowed to call it? *(http only)*
71
+
72
+ | Value | Meaning |
73
+ |---|---|
74
+ | `admin` *(default)* | Callable only from an authenticated admin surface. |
75
+ | `public` | Publicly callable — no authenticated user required. |
76
+
77
+ ### 2. `authority` — whose authority does it run as?
78
+
79
+ This decides what `ctx.sl` can do.
80
+
81
+ | Value | `ctx.sl` is scoped to | Use when |
82
+ |---|---|---|
83
+ | `caller` *(default)* | The **invoking user** (their session/JWT). Can only do what that user could. | Admin actions that should respect the user's own permissions and be attributed to them. |
84
+ | `collection` | A **collection-admin principal for _this_ collection only** — never a global superuser. | A privileged server-side action a public/anonymous caller can't be trusted to do directly. |
85
+
86
+ `authority` **defaults to `caller`** — secure by default. `event`- and `cron`-triggered
87
+ functions have no external caller, so they always run as `collection`.
88
+
89
+ ### The combinations
90
+
91
+ - **`http` + `admin` + `caller`** — a delegated admin function; runs as the signed-in admin.
92
+ - **`http` + `public` + `caller`** — runs as the anonymous/public user (limited). The safe public default.
93
+ - **`http` + `public` + `collection`** — publicly callable, runs with collection authority.
94
+ The powerful one (e.g. "submit competition entry" needs to validate then write a record no
95
+ anonymous user may write directly). **Your responsibility:** validate the input and prevent
96
+ abuse — see below.
97
+ - **`event` / `cron`** — background work; runs as `collection`.
98
+
99
+ > ### ⚠️ Public + collection authority is the sharp edge
100
+ > A `public` + `collection` function is reachable by anyone and runs with elevated authority.
101
+ > The platform shrinks the blast radius for you — the authority is capped to **this one
102
+ > collection**, and further capped by your declared **capabilities** (a competition-entry
103
+ > function that declares only `sl:records:write` cannot delete products or read other
104
+ > secrets, even though it's "elevated"). But **validating the request is still your job**:
105
+ > check the payload, rate-limit using `ctx.caller`, guard against replay. Treat the function
106
+ > body as a trust boundary.
107
+
108
+ ---
109
+
110
+ ## Capabilities — least privilege, declared and capped
111
+
112
+ `capabilities` is the allow-list of what your function may reach. It is surfaced at install
113
+ time for consent and **enforced at runtime** — including for `collection`-authority functions.
114
+ Declare the minimum you need.
115
+
116
+ | Capability | Grants |
117
+ |---|---|
118
+ | `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`. |
119
+ | `network` | `ctx.fetch` to any host. |
120
+ | `network:<host>` | `ctx.fetch` to that host only (repeat for several). Prefer this over blanket `network`. |
121
+ | `secrets:<ref>` | `ctx.secrets.get('<ref>')` for that one secret ref. |
122
+
123
+ If you don't declare `network`, `ctx.fetch` is absent. If you don't declare a `secrets:<ref>`,
124
+ `ctx.secrets.get('<ref>')` returns `null`.
125
+
126
+ ---
127
+
128
+ ## The `ctx` object
129
+
130
+ ```ts
131
+ interface ServerFunctionContext {
132
+ collectionId: string
133
+ appId: string
134
+
135
+ /** SmartLinks SDK, pre-scoped to your declared `authority`. Capabilities cap what it may do. */
136
+ sl: SmartLinks
137
+
138
+ /** Capability-gated secrets. Resolves only refs you declared via `secrets:<ref>`. */
139
+ secrets: { get(ref: string): Promise<string | null> }
140
+
141
+ /** Who invoked this function. */
142
+ caller: {
143
+ userId: string | null // null for anonymous/public/system calls
144
+ anonymous: boolean
145
+ origin?: string | null // http
146
+ ip?: string | null // http
147
+ via: 'http' | 'event' | 'cron'
148
+ }
149
+
150
+ /** Outbound HTTP — present only if you declared `network` (host-scoped if `network:<host>`). */
151
+ fetch: typeof fetch
152
+
153
+ /** Structured logging, captured into your function's run telemetry. */
154
+ log: (message: string, data?: Record<string, any>) => void
155
+ }
156
+ ```
157
+
158
+ **Use `ctx.sl` for anything SmartLinks** — it is the same SDK surface you use client-side,
159
+ already authenticated as your declared authority and scoped to the collection. Do **not** try
160
+ to construct your own SDK client or carry your own key; that's what `ctx.sl` is for, and it's
161
+ the only way authority stays correct.
162
+
163
+ ---
164
+
165
+ ## Worked example — a public competition entry
166
+
167
+ ```ts
168
+ // dist/functions — handler for the manifest definition above
169
+ export async function submitCompetitionEntry(ctx, event) {
170
+ const { name, email, answer, captchaToken } = event.body || {}
171
+
172
+ // 1. Validate — this is YOUR job on a public function.
173
+ if (!email || !answer) return { ok: false, error: 'missing_fields' }
174
+
175
+ // 2. Use a declared secret + declared host to verify a captcha.
176
+ const secret = await ctx.secrets.get('recaptcha-secret')
177
+ const verify = await ctx.fetch('https://api.recaptcha.net/verify', {
178
+ method: 'POST',
179
+ body: new URLSearchParams({ secret, response: captchaToken }),
180
+ }).then(r => r.json())
181
+ if (!verify.success) return { ok: false, error: 'captcha_failed' }
182
+
183
+ // 3. Write with collection authority — something an anonymous caller can't do directly.
184
+ const entry = await ctx.sl.appRecords.create({
185
+ recordType: 'competition-entry',
186
+ data: { name, email, answer, ip: ctx.caller.ip, submittedAt: new Date().toISOString() },
187
+ })
188
+
189
+ ctx.log('entry recorded', { entryId: entry.id })
190
+ return { ok: true, entryId: entry.id }
191
+ }
192
+ ```
193
+
194
+ Declared as `public` + `collection` + `['sl:records:write', 'secrets:recaptcha-secret',
195
+ 'network:api.recaptcha.net']`, this function is publicly callable, verifies the request
196
+ itself, and writes a record no anonymous user could write — but it cannot touch products,
197
+ other secrets, or any other collection.
198
+
199
+ ---
200
+
201
+ ## Invoking an http function
202
+
203
+ An `http` function is called by POSTing to the collection's functions endpoint on the
204
+ surface that matches its `visibility`:
205
+
206
+ ```
207
+ POST /admin/collection/:collectionId/functions/:name # visibility: admin (collection-admin auth)
208
+ POST /public/collection/:collectionId/functions/:name # visibility: public (auth optional)
209
+ ```
210
+
211
+ The request body is delivered to the handler as `event.body` (query string as
212
+ `event.query`). A function only runs on its own surface — calling an `admin` function on
213
+ the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
214
+ `{ error, message }` (HTTP 400) if the handler returned an error. `GET` on either endpoint
215
+ lists the functions callable on that surface.
216
+
217
+ The admin surface is collection-admin gated, so an admin function's `caller` authority runs
218
+ at admin level, attributed to the signed-in admin. The public surface resolves auth if a
219
+ token is present (→ `owner`) and treats its absence as anonymous (→ `public`); a
220
+ `collection`-authority function runs elevated regardless.
221
+
222
+ ## Where functions run (and why it doesn't change how you write them)
223
+
224
+ SmartLinks runs first-party (trusted) functions **in-process** and untrusted third-party
225
+ functions in an **isolated runner**. The difference is enforcement, not authoring:
226
+
227
+ - **In-process** trusts the author; `ctx` is built directly.
228
+ - **Isolated runner** enforces the `authority` boundary and `capabilities` at the container
229
+ edge — `ctx.sl` is a proxy over the declared authority, `ctx.fetch` is filtered to the
230
+ declared hosts, and there is no ambient filesystem, network, or environment.
231
+
232
+ Because the **contract is identical**, a function you write today runs unchanged if it later
233
+ moves lanes. Write to the `ctx` contract and declare your capabilities honestly, and the
234
+ platform takes care of the rest.
package/openapi.yaml CHANGED
@@ -38,6 +38,7 @@ tags:
38
38
  - name: crate
39
39
  - name: facets
40
40
  - name: form
41
+ - name: integrations
41
42
  - name: interactions
42
43
  - name: jobs
43
44
  - name: journeys
@@ -4164,6 +4165,33 @@ paths:
4164
4165
  description: Unauthorized
4165
4166
  404:
4166
4167
  description: Not found
4168
+ /admin/collection/{collectionId}/integrations/record-types:
4169
+ get:
4170
+ tags:
4171
+ - integrations
4172
+ summary: Discover the app-record types present in a collection + which app owns each (introspected), for picking a sub-record source/trigger.
4173
+ operationId: integrations_listRecordTypes
4174
+ security:
4175
+ - bearerAuth: []
4176
+ parameters:
4177
+ - name: collectionId
4178
+ in: path
4179
+ required: true
4180
+ schema:
4181
+ type: string
4182
+ responses:
4183
+ 200:
4184
+ description: Success
4185
+ content:
4186
+ application/json:
4187
+ schema:
4188
+ $ref: "#/components/schemas/RecordTypesResponse"
4189
+ 400:
4190
+ description: Bad request
4191
+ 401:
4192
+ description: Unauthorized
4193
+ 404:
4194
+ description: Not found
4167
4195
  /admin/collection/{collectionId}/interactions:
4168
4196
  get:
4169
4197
  tags:
@@ -17215,6 +17243,107 @@ components:
17215
17243
  required:
17216
17244
  - files
17217
17245
  - function
17246
+ AppFunctionTrigger:
17247
+ type: object
17248
+ properties:
17249
+ type:
17250
+ $ref: "#/components/schemas/AppFunctionTriggerType"
17251
+ eventTypes:
17252
+ type: array
17253
+ items:
17254
+ type: string
17255
+ schedule:
17256
+ type: string
17257
+ route:
17258
+ type: string
17259
+ methods:
17260
+ type: array
17261
+ items:
17262
+ type: string
17263
+ enum:
17264
+ - GET
17265
+ - POST
17266
+ - PUT
17267
+ - PATCH
17268
+ - DELETE
17269
+ required:
17270
+ - type
17271
+ AppFunctionDef:
17272
+ type: object
17273
+ properties:
17274
+ name:
17275
+ type: string
17276
+ description:
17277
+ type: string
17278
+ trigger:
17279
+ $ref: "#/components/schemas/AppFunctionTrigger"
17280
+ visibility:
17281
+ $ref: "#/components/schemas/AppFunctionVisibility"
17282
+ authority:
17283
+ $ref: "#/components/schemas/AppFunctionAuthority"
17284
+ capabilities:
17285
+ type: array
17286
+ items:
17287
+ type: string
17288
+ apiVersion:
17289
+ type: string
17290
+ handler:
17291
+ type: string
17292
+ required:
17293
+ - name
17294
+ - trigger
17295
+ AppManifestFunctions:
17296
+ type: object
17297
+ properties:
17298
+ files:
17299
+ $ref: "#/components/schemas/AppManifestFiles"
17300
+ definitions:
17301
+ type: array
17302
+ items:
17303
+ $ref: "#/components/schemas/AppFunctionDef"
17304
+ required:
17305
+ - files
17306
+ - definitions
17307
+ ServerFunctionCaller:
17308
+ type: object
17309
+ properties:
17310
+ userId:
17311
+ type: string
17312
+ anonymous:
17313
+ type: boolean
17314
+ origin:
17315
+ type: string
17316
+ ip:
17317
+ type: string
17318
+ via:
17319
+ $ref: "#/components/schemas/AppFunctionTriggerType"
17320
+ required:
17321
+ - userId
17322
+ - anonymous
17323
+ - via
17324
+ ServerFunctionContext:
17325
+ type: object
17326
+ properties:
17327
+ collectionId:
17328
+ type: string
17329
+ appId:
17330
+ type: string
17331
+ sl: {}
17332
+ secrets:
17333
+ type: object
17334
+ additionalProperties: true
17335
+ caller:
17336
+ $ref: "#/components/schemas/ServerFunctionCaller"
17337
+ fetch:
17338
+ type: object
17339
+ additionalProperties: true
17340
+ required:
17341
+ - collectionId
17342
+ - appId
17343
+ - sl
17344
+ - secrets
17345
+ - caller
17346
+ - fetch
17218
17347
  AppAdminConfig:
17219
17348
  type: object
17220
17349
  properties:
@@ -17357,6 +17486,8 @@ components:
17357
17486
  $ref: "#/components/schemas/DeepLinkEntry"
17358
17487
  executor:
17359
17488
  $ref: "#/components/schemas/AppManifestExecutor"
17489
+ functions:
17490
+ $ref: "#/components/schemas/AppManifestFunctions"
17360
17491
  required:
17361
17492
  - name
17362
17493
  - version
@@ -17397,6 +17528,22 @@ components:
17397
17528
  properties:
17398
17529
  force:
17399
17530
  type: boolean
17531
+ AppFunctionTriggerType:
17532
+ type: string
17533
+ enum:
17534
+ - http
17535
+ - event
17536
+ - cron
17537
+ AppFunctionVisibility:
17538
+ type: string
17539
+ enum:
17540
+ - admin
17541
+ - public
17542
+ AppFunctionAuthority:
17543
+ type: string
17544
+ enum:
17545
+ - caller
17546
+ - collection
17400
17547
  PaginatedResponse:
17401
17548
  type: object
17402
17549
  properties:
@@ -23429,6 +23576,28 @@ components:
23429
23576
  properties:
23430
23577
  purpose:
23431
23578
  type: string
23579
+ RecordTypeInfo:
23580
+ type: object
23581
+ properties:
23582
+ appId:
23583
+ type: string
23584
+ recordType:
23585
+ type: string
23586
+ count:
23587
+ type: number
23588
+ required:
23589
+ - appId
23590
+ - recordType
23591
+ - count
23592
+ RecordTypesResponse:
23593
+ type: object
23594
+ properties:
23595
+ recordTypes:
23596
+ type: array
23597
+ items:
23598
+ $ref: "#/components/schemas/RecordTypeInfo"
23599
+ required:
23600
+ - recordTypes
23432
23601
  RunFlowResult:
23433
23602
  type: object
23434
23603
  additionalProperties: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.17.5",
3
+ "version": "1.17.6",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",