@proveanything/smartlinks 2.0.2 → 2.0.4
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/docs/API_SUMMARY.md +1 -1
- package/dist/docs/app-manifest.md +18 -0
- package/dist/docs/deploying-apps.md +26 -1
- package/dist/docs/overview.md +2 -0
- package/dist/docs/server-functions.md +140 -3
- package/docs/API_SUMMARY.md +1 -1
- package/docs/app-manifest.md +18 -0
- package/docs/deploying-apps.md +26 -1
- package/docs/overview.md +2 -0
- package/docs/server-functions.md +140 -3
- package/package.json +4 -2
- package/scripts/publish.mjs +99 -0
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -312,6 +312,24 @@ See the **[Executor Model guide](executor.md)** for the full build setup, SEO co
|
|
|
312
312
|
| `llmContent.function` | string | Name of the `getLLMContent` export |
|
|
313
313
|
| `llmContent.timeout` | number | Timeout in ms (default 500) |
|
|
314
314
|
|
|
315
|
+
#### `functions`
|
|
316
|
+
|
|
317
|
+
Declares app-authored **server functions** — server-side handlers `(ctx, event) ⇒ result`
|
|
318
|
+
(http / event / cron), shipped in a self-contained UMD bundle. Omit if the app has no server
|
|
319
|
+
functions. The security model (`visibility`/`authority`/`capabilities`), the runtime surface, how
|
|
320
|
+
to build the bundle, and how to invoke are all in the **[Server Functions guide](server-functions.md)** —
|
|
321
|
+
this is just the manifest shape.
|
|
322
|
+
|
|
323
|
+
| Field | Type | Description |
|
|
324
|
+
|-------|------|-------------|
|
|
325
|
+
| `files.js.umd` | string | UMD bundle path (e.g. `dist/functions.umd.js`) — a self-contained bundle exporting each handler by name |
|
|
326
|
+
| `definitions[]` | object[] | One entry per function |
|
|
327
|
+
| `definitions[].name` | string | Function name; also the handler export name (override with `handler`) |
|
|
328
|
+
| `definitions[].trigger` | object | `{ type: "http"|"event"|"cron", … }` |
|
|
329
|
+
| `definitions[].visibility` | string | `admin` \| `public` (http only) — who may call it |
|
|
330
|
+
| `definitions[].authority` | string | `caller` \| `collection` — whose authority it runs as |
|
|
331
|
+
| `definitions[].capabilities` | string[] | Least-privilege grants (`sl:<res>:<read\|write>`, `network[:<host>]`, `secrets:<ref>`) |
|
|
332
|
+
|
|
315
333
|
---
|
|
316
334
|
|
|
317
335
|
## `app.admin.json`
|
|
@@ -16,6 +16,31 @@ This guide covers registration and how to wire it into your build.
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## Fast dev publish (recommended for dev)
|
|
20
|
+
|
|
21
|
+
For the **dev** channel there's a one-step path that does *both* halves — SmartLinks **hosts and
|
|
22
|
+
registers** — so you don't host bundles anywhere yourself (no Lovable, no CDN of your own) and
|
|
23
|
+
don't run a separate register step:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# build your app first (produces dist/ with app.manifest.json), then:
|
|
27
|
+
smartlinks-publish # uploads dist/ → SmartLinks hosts it + registers the dev release
|
|
28
|
+
smartlinks-publish --watch # re-publish on every change (save → live)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Env: `SMARTLINKS_APP_ID` (your platform id), `SMARTLINKS_DEPLOY_KEY` (dev key — dev-only, safe to
|
|
32
|
+
keep locally), optional `SMARTLINKS_API` / `--dir`. It uploads the built files to
|
|
33
|
+
`POST /api/v1/apps/{appId}/publish?channel=dev`; the server validates the manifest + functions,
|
|
34
|
+
writes the bundle to `smartlinks.app/apps/{appId}/dev/…` with **`no-store` caching** (every reload
|
|
35
|
+
is instantly fresh — no cache-busting, no invalidation), and registers the release. It works from
|
|
36
|
+
**anywhere** — local, Claude, CI, or a build step — because the builder builds and the platform
|
|
37
|
+
hosts; there's no dependency on any external host for dev.
|
|
38
|
+
|
|
39
|
+
Use this for dev. Use the **register-a-URL** flow below when *you* host the bundle (a beta/stable
|
|
40
|
+
CDN deploy, or an external host) and just want to register where it lives + its manifest.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
19
44
|
## Two "wheres": the API host vs the bundle CDN
|
|
20
45
|
|
|
21
46
|
Deploying touches two different locations — keep them distinct:
|
|
@@ -134,7 +159,7 @@ Content-Type: application/json
|
|
|
134
159
|
|
|
135
160
|
| Status | Meaning |
|
|
136
161
|
|---|---|
|
|
137
|
-
| `200` | Registered. Body: `{ ok: true, appId, channel, version, functions: [names], registeredAt }` |
|
|
162
|
+
| `200` | Registered. Body: `{ ok: true, appId, channel, version, functions: [names], registeredAt, warnings? }` — `warnings` is present for non-blocking issues (e.g. `APPID_IGNORED` when the manifest declared a different id). |
|
|
138
163
|
| `422` | Validation failed — **the build should fail**. Body: `{ ok: false, errors: [...] }` |
|
|
139
164
|
| `401` | Missing/invalid deploy key |
|
|
140
165
|
| `403` | Key not permitted for that channel (e.g. a dev key targeting `prod`) — `CHANNEL_FORBIDDEN` |
|
package/dist/docs/overview.md
CHANGED
|
@@ -63,6 +63,8 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
63
63
|
| **Containers** | `docs/containers.md` | Building full-app embeddable containers (lazy-loaded) |
|
|
64
64
|
| **Mobile Admin Container** | `docs/mobile-admin-container.md` | Building a separate Capacitor-aware mobile admin bundle for field operators |
|
|
65
65
|
| **Executors** | `docs/executor.md` | Building executor bundles for SEO, LLM content, programmatic config |
|
|
66
|
+
| **Server Functions** | `docs/server-functions.md` | App-authored server-side functions `(ctx, event) ⇒ result`: security model, runtime surface, invoking |
|
|
67
|
+
| **Deploying & Registering** | `docs/deploying-apps.md` | Getting your app into the platform: fast dev publish, channels, deploy keys, registering releases |
|
|
66
68
|
| **Deep Linking** | `docs/deep-link-discovery.md` | URL state management, navigable states, portal menus, AI nav |
|
|
67
69
|
| **Portal Back Button** | `docs/portal-back-button.md` | Hierarchy-aware "up" navigation inside embedded apps |
|
|
68
70
|
| **Portal Request Action** | `docs/portal-request-action.md` | Triggering portal built-in actions (__qrScanner, __share, __logout, etc.) from sub-apps |
|
|
@@ -63,6 +63,47 @@ in a bundle alongside your widgets/containers:
|
|
|
63
63
|
|
|
64
64
|
Each definition maps to an exported handler of the same `name` (override with `handler`).
|
|
65
65
|
|
|
66
|
+
## Building the functions bundle
|
|
67
|
+
|
|
68
|
+
The handlers ship as a **self-contained UMD** — one file, all dependencies compiled in, no
|
|
69
|
+
`require`/`import` of platform or Node modules (see [Runtime](#runtime--what-your-function-can-use)).
|
|
70
|
+
Your functions entry just **named-exports** each handler:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// src/functions/index.js — the functions entry
|
|
74
|
+
export async function submitCompetitionEntry(ctx, event) { /* … */ }
|
|
75
|
+
export async function onProofCreated(ctx, event) { /* … */ }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Build it to `dist/functions.umd.js` with a Vite lib build — **UMD format, nothing externalised**
|
|
79
|
+
(so it's self-contained), and don't wipe the widgets you built into the same `dist/`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// vite.functions.config.ts
|
|
83
|
+
import { defineConfig } from 'vite'
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
build: {
|
|
86
|
+
lib: { entry: 'src/functions/index.js', formats: ['umd'], name: 'functions',
|
|
87
|
+
fileName: () => 'functions.umd.js' },
|
|
88
|
+
outDir: 'dist',
|
|
89
|
+
emptyOutDir: false, // co-exist with the widgets/containers build
|
|
90
|
+
rollupOptions: { external: [] } // bundle everything — no Node/platform externals
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// package.json — build widgets/containers as usual, then the functions bundle
|
|
97
|
+
"scripts": {
|
|
98
|
+
"build": "vite build && vite build --config vite.functions.config.ts"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
That emits `dist/functions.umd.js` (matching `functions.files.js.umd`), whose named exports the
|
|
103
|
+
platform pairs with your manifest `definitions`. Then **deploy** it — for dev,
|
|
104
|
+
`smartlinks-publish` uploads `dist/` and registers in one step (see
|
|
105
|
+
[Deploying & registering](deploying-apps.md)).
|
|
106
|
+
|
|
66
107
|
---
|
|
67
108
|
|
|
68
109
|
## The two security questions every function answers
|
|
@@ -159,7 +200,9 @@ interface ServerFunctionContext {
|
|
|
159
200
|
/** Outbound HTTP — present only if you declared `network` (host-scoped if `network:<host>`). */
|
|
160
201
|
fetch: typeof fetch
|
|
161
202
|
|
|
162
|
-
/** Structured logging
|
|
203
|
+
/** Structured logging — returned inline on the admin surface and recorded as an
|
|
204
|
+
* execution activity event (owner Errors & Activity console). See "Seeing your
|
|
205
|
+
* function's runs and logs" below. */
|
|
163
206
|
log: (message: string, data?: Record<string, any>) => void
|
|
164
207
|
}
|
|
165
208
|
```
|
|
@@ -171,6 +214,73 @@ the only way authority stays correct.
|
|
|
171
214
|
|
|
172
215
|
---
|
|
173
216
|
|
|
217
|
+
## Runtime — what your function can use
|
|
218
|
+
|
|
219
|
+
Your function runs in a **web-standard sandbox** (think Cloudflare Workers / Deno), **not
|
|
220
|
+
Node**. Concretely it targets the **WinterTC Minimum Common Web Platform API** (WinterTC is the
|
|
221
|
+
Ecma International technical committee for this, formerly WinterCG) — the same surface those
|
|
222
|
+
runtimes guarantee — so what you can rely on is portable, and the future isolated runner will
|
|
223
|
+
host the same bundle unchanged.
|
|
224
|
+
|
|
225
|
+
**Available globals** (no import needed):
|
|
226
|
+
- **Data / encoding:** `JSON`, `URL`, `URLSearchParams`, `TextEncoder` / `TextDecoder`,
|
|
227
|
+
`atob` / `btoa`, `Buffer`, `structuredClone`.
|
|
228
|
+
- **Crypto:** `crypto` (Web Crypto) — `crypto.subtle` for hashing / HMAC / encrypt / sign /
|
|
229
|
+
verify, `crypto.getRandomValues`, `crypto.randomUUID`.
|
|
230
|
+
- **HTTP:** `fetch`, `Headers`, `Request`, `Response`, `FormData`, `Blob`, `File`,
|
|
231
|
+
`AbortController` / `AbortSignal` (use one for **request timeouts**).
|
|
232
|
+
- **Streams & timing:** `ReadableStream` / `WritableStream` / `TransformStream`,
|
|
233
|
+
`CompressionStream` / `DecompressionStream` (gzip/deflate for third-party payloads), the timer
|
|
234
|
+
functions, `queueMicrotask`, `performance`, `console`.
|
|
235
|
+
- **Runtime identity:** `navigator.userAgent` reports the runtime key (currently
|
|
236
|
+
`"SmartLinks-Functions"`) — use it if a portable dependency needs to feature-detect the host.
|
|
237
|
+
It stays stable when functions move to the isolated runner.
|
|
238
|
+
|
|
239
|
+
**Not available:** `require` / `import` of platform or Node modules, `process`, `fs`, and the
|
|
240
|
+
Node built-ins (`node:crypto`, `node:http`, …). There is no ambient database, key, or network
|
|
241
|
+
handle — everything the platform gives you comes through **`ctx`**.
|
|
242
|
+
|
|
243
|
+
**Dependencies:** bundle them. Your build must produce a **self-contained** UMD (deps compiled
|
|
244
|
+
in), and those deps must be **edge-compatible** — pure JS / Web APIs. A library that reaches for
|
|
245
|
+
Node built-ins (e.g. `axios`'s Node adapter, anything using `node:crypto`) will fail to load.
|
|
246
|
+
Prefer the platform primitives above over a dependency; when you do need one, pick edge-safe:
|
|
247
|
+
|
|
248
|
+
| Need | Native? | Recommended (bundle, edge-safe) |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| JSON | **native** — `JSON.parse/stringify` | — |
|
|
251
|
+
| XML parse / build | no | `fast-xml-parser` |
|
|
252
|
+
| CSV | no | `papaparse` |
|
|
253
|
+
| Schema validation of inputs | no | `zod` |
|
|
254
|
+
| JWT (sign/verify for a third-party API) | Web Crypto can, verbosely | `jose` |
|
|
255
|
+
| Hash / HMAC / encrypt | **native** — `crypto.subtle` | — |
|
|
256
|
+
|
|
257
|
+
**The common actions, and how to do each:**
|
|
258
|
+
|
|
259
|
+
| You want to… | Use | Requires |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| Call a third-party API (GET/POST) | `ctx.fetch(url, init)` — the standard Fetch API | capability `network` or `network:<host>` |
|
|
262
|
+
| Read a secret (API key, signing key) | `await ctx.secrets.get('<ref>')` | capability `secrets:<ref>` |
|
|
263
|
+
| Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
|
|
264
|
+
| Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
|
|
265
|
+
| Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
|
|
266
|
+
|
|
267
|
+
Notes:
|
|
268
|
+
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
269
|
+
process, then act on the core* — is: check the input, then call `ctx.sl.appRecords.create(…)`,
|
|
270
|
+
`ctx.sl.products.update(…)`, etc. `ctx.sl` is already authenticated as your declared authority
|
|
271
|
+
and capped by your capabilities, so you never construct a SmartLinks API URL or carry a key.
|
|
272
|
+
Use `fetch`/`ctx.fetch` for *third-party* servers; use `ctx.sl` for SmartLinks itself.
|
|
273
|
+
- **There is one `fetch`, and it is capability-gated.** Whether you call the global `fetch` or
|
|
274
|
+
`ctx.fetch`, the behaviour is identical: the call **throws** unless your declared `network`
|
|
275
|
+
(or `network:<host>`) capability covers the target host — the same way Deno's `fetch` throws
|
|
276
|
+
without `--allow-net`. There is no ungated escape hatch. Declare the hosts you need.
|
|
277
|
+
- **Secrets are read-only at runtime.** You fetch a secret you declared; you do **not** set or
|
|
278
|
+
rotate secrets from a function — that's an admin/deploy-time operation on the platform.
|
|
279
|
+
- **Signing a webhook / verifying a signature** is `crypto.subtle` with an HMAC key imported
|
|
280
|
+
from a secret — no Node `crypto` needed.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
174
284
|
## Worked example — a public competition entry
|
|
175
285
|
|
|
176
286
|
```ts
|
|
@@ -220,14 +330,41 @@ POST /public/collection/:collectionId/functions/:name # visibility: public (a
|
|
|
220
330
|
The request body is delivered to the handler as `event.body` (query string as
|
|
221
331
|
`event.query`). A function only runs on its own surface — calling an `admin` function on
|
|
222
332
|
the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
|
|
223
|
-
`{ error, message }` (HTTP 400) if the handler returned an error.
|
|
224
|
-
|
|
333
|
+
`{ error, message }` (HTTP 400) if the handler returned an error. On the **admin surface**
|
|
334
|
+
the response also includes `logs` (your `ctx.log` lines) and `durationMs` for quick
|
|
335
|
+
debugging; the **public surface returns only `result`** (a public caller never sees your
|
|
336
|
+
internal logs). `GET` on either endpoint lists the functions callable on that surface.
|
|
225
337
|
|
|
226
338
|
The admin surface is collection-admin gated, so an admin function's `caller` authority runs
|
|
227
339
|
at admin level, attributed to the signed-in admin. The public surface resolves auth if a
|
|
228
340
|
token is present (→ `owner`) and treats its absence as anonymous (→ `public`); a
|
|
229
341
|
`collection`-authority function runs elevated regardless.
|
|
230
342
|
|
|
343
|
+
### Before it will resolve
|
|
344
|
+
|
|
345
|
+
An http call returns `404 FUNCTION_NOT_FOUND` unless **both** of these are true (this is the
|
|
346
|
+
most common first-run surprise):
|
|
347
|
+
|
|
348
|
+
1. **The app's release is registered on the channel** the collection follows (see
|
|
349
|
+
[deploying-apps.md](deploying-apps.md)) — that's what publishes the function bundle into
|
|
350
|
+
the registry the runtime loads from.
|
|
351
|
+
2. **The app is enabled on the collection** (`appConfig.apps[]`, on that same channel — see
|
|
352
|
+
[appConfig.md](appConfig.md)).
|
|
353
|
+
|
|
354
|
+
So the end-to-end path is: *write → register the release → enable on a collection → call.*
|
|
355
|
+
|
|
356
|
+
### Seeing your function's runs and logs
|
|
357
|
+
|
|
358
|
+
Every invocation is recorded as an **execution** activity event, carrying your `ctx.log`
|
|
359
|
+
lines. Where to look, easiest first:
|
|
360
|
+
|
|
361
|
+
- **Admin-surface response** — `logs` + `durationMs` come straight back in the JSON.
|
|
362
|
+
- **Deployed test mode** — see below; real run, isolated logging.
|
|
363
|
+
- **Owner console** — the collection's **Advanced → Errors & Activity → events** tab,
|
|
364
|
+
filtered to **source = execution**: each run shows as `function <name> ok` (or an error),
|
|
365
|
+
and the detail carries your `ctx.log` output. (Telemetry is streamed, so allow a few
|
|
366
|
+
seconds.)
|
|
367
|
+
|
|
231
368
|
## Testing & preview
|
|
232
369
|
|
|
233
370
|
You don't have to deploy to find out whether a function works. There are three levels of
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/app-manifest.md
CHANGED
|
@@ -312,6 +312,24 @@ See the **[Executor Model guide](executor.md)** for the full build setup, SEO co
|
|
|
312
312
|
| `llmContent.function` | string | Name of the `getLLMContent` export |
|
|
313
313
|
| `llmContent.timeout` | number | Timeout in ms (default 500) |
|
|
314
314
|
|
|
315
|
+
#### `functions`
|
|
316
|
+
|
|
317
|
+
Declares app-authored **server functions** — server-side handlers `(ctx, event) ⇒ result`
|
|
318
|
+
(http / event / cron), shipped in a self-contained UMD bundle. Omit if the app has no server
|
|
319
|
+
functions. The security model (`visibility`/`authority`/`capabilities`), the runtime surface, how
|
|
320
|
+
to build the bundle, and how to invoke are all in the **[Server Functions guide](server-functions.md)** —
|
|
321
|
+
this is just the manifest shape.
|
|
322
|
+
|
|
323
|
+
| Field | Type | Description |
|
|
324
|
+
|-------|------|-------------|
|
|
325
|
+
| `files.js.umd` | string | UMD bundle path (e.g. `dist/functions.umd.js`) — a self-contained bundle exporting each handler by name |
|
|
326
|
+
| `definitions[]` | object[] | One entry per function |
|
|
327
|
+
| `definitions[].name` | string | Function name; also the handler export name (override with `handler`) |
|
|
328
|
+
| `definitions[].trigger` | object | `{ type: "http"|"event"|"cron", … }` |
|
|
329
|
+
| `definitions[].visibility` | string | `admin` \| `public` (http only) — who may call it |
|
|
330
|
+
| `definitions[].authority` | string | `caller` \| `collection` — whose authority it runs as |
|
|
331
|
+
| `definitions[].capabilities` | string[] | Least-privilege grants (`sl:<res>:<read\|write>`, `network[:<host>]`, `secrets:<ref>`) |
|
|
332
|
+
|
|
315
333
|
---
|
|
316
334
|
|
|
317
335
|
## `app.admin.json`
|
package/docs/deploying-apps.md
CHANGED
|
@@ -16,6 +16,31 @@ This guide covers registration and how to wire it into your build.
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## Fast dev publish (recommended for dev)
|
|
20
|
+
|
|
21
|
+
For the **dev** channel there's a one-step path that does *both* halves — SmartLinks **hosts and
|
|
22
|
+
registers** — so you don't host bundles anywhere yourself (no Lovable, no CDN of your own) and
|
|
23
|
+
don't run a separate register step:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# build your app first (produces dist/ with app.manifest.json), then:
|
|
27
|
+
smartlinks-publish # uploads dist/ → SmartLinks hosts it + registers the dev release
|
|
28
|
+
smartlinks-publish --watch # re-publish on every change (save → live)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Env: `SMARTLINKS_APP_ID` (your platform id), `SMARTLINKS_DEPLOY_KEY` (dev key — dev-only, safe to
|
|
32
|
+
keep locally), optional `SMARTLINKS_API` / `--dir`. It uploads the built files to
|
|
33
|
+
`POST /api/v1/apps/{appId}/publish?channel=dev`; the server validates the manifest + functions,
|
|
34
|
+
writes the bundle to `smartlinks.app/apps/{appId}/dev/…` with **`no-store` caching** (every reload
|
|
35
|
+
is instantly fresh — no cache-busting, no invalidation), and registers the release. It works from
|
|
36
|
+
**anywhere** — local, Claude, CI, or a build step — because the builder builds and the platform
|
|
37
|
+
hosts; there's no dependency on any external host for dev.
|
|
38
|
+
|
|
39
|
+
Use this for dev. Use the **register-a-URL** flow below when *you* host the bundle (a beta/stable
|
|
40
|
+
CDN deploy, or an external host) and just want to register where it lives + its manifest.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
19
44
|
## Two "wheres": the API host vs the bundle CDN
|
|
20
45
|
|
|
21
46
|
Deploying touches two different locations — keep them distinct:
|
|
@@ -134,7 +159,7 @@ Content-Type: application/json
|
|
|
134
159
|
|
|
135
160
|
| Status | Meaning |
|
|
136
161
|
|---|---|
|
|
137
|
-
| `200` | Registered. Body: `{ ok: true, appId, channel, version, functions: [names], registeredAt }` |
|
|
162
|
+
| `200` | Registered. Body: `{ ok: true, appId, channel, version, functions: [names], registeredAt, warnings? }` — `warnings` is present for non-blocking issues (e.g. `APPID_IGNORED` when the manifest declared a different id). |
|
|
138
163
|
| `422` | Validation failed — **the build should fail**. Body: `{ ok: false, errors: [...] }` |
|
|
139
164
|
| `401` | Missing/invalid deploy key |
|
|
140
165
|
| `403` | Key not permitted for that channel (e.g. a dev key targeting `prod`) — `CHANNEL_FORBIDDEN` |
|
package/docs/overview.md
CHANGED
|
@@ -63,6 +63,8 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
63
63
|
| **Containers** | `docs/containers.md` | Building full-app embeddable containers (lazy-loaded) |
|
|
64
64
|
| **Mobile Admin Container** | `docs/mobile-admin-container.md` | Building a separate Capacitor-aware mobile admin bundle for field operators |
|
|
65
65
|
| **Executors** | `docs/executor.md` | Building executor bundles for SEO, LLM content, programmatic config |
|
|
66
|
+
| **Server Functions** | `docs/server-functions.md` | App-authored server-side functions `(ctx, event) ⇒ result`: security model, runtime surface, invoking |
|
|
67
|
+
| **Deploying & Registering** | `docs/deploying-apps.md` | Getting your app into the platform: fast dev publish, channels, deploy keys, registering releases |
|
|
66
68
|
| **Deep Linking** | `docs/deep-link-discovery.md` | URL state management, navigable states, portal menus, AI nav |
|
|
67
69
|
| **Portal Back Button** | `docs/portal-back-button.md` | Hierarchy-aware "up" navigation inside embedded apps |
|
|
68
70
|
| **Portal Request Action** | `docs/portal-request-action.md` | Triggering portal built-in actions (__qrScanner, __share, __logout, etc.) from sub-apps |
|
package/docs/server-functions.md
CHANGED
|
@@ -63,6 +63,47 @@ in a bundle alongside your widgets/containers:
|
|
|
63
63
|
|
|
64
64
|
Each definition maps to an exported handler of the same `name` (override with `handler`).
|
|
65
65
|
|
|
66
|
+
## Building the functions bundle
|
|
67
|
+
|
|
68
|
+
The handlers ship as a **self-contained UMD** — one file, all dependencies compiled in, no
|
|
69
|
+
`require`/`import` of platform or Node modules (see [Runtime](#runtime--what-your-function-can-use)).
|
|
70
|
+
Your functions entry just **named-exports** each handler:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// src/functions/index.js — the functions entry
|
|
74
|
+
export async function submitCompetitionEntry(ctx, event) { /* … */ }
|
|
75
|
+
export async function onProofCreated(ctx, event) { /* … */ }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Build it to `dist/functions.umd.js` with a Vite lib build — **UMD format, nothing externalised**
|
|
79
|
+
(so it's self-contained), and don't wipe the widgets you built into the same `dist/`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// vite.functions.config.ts
|
|
83
|
+
import { defineConfig } from 'vite'
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
build: {
|
|
86
|
+
lib: { entry: 'src/functions/index.js', formats: ['umd'], name: 'functions',
|
|
87
|
+
fileName: () => 'functions.umd.js' },
|
|
88
|
+
outDir: 'dist',
|
|
89
|
+
emptyOutDir: false, // co-exist with the widgets/containers build
|
|
90
|
+
rollupOptions: { external: [] } // bundle everything — no Node/platform externals
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// package.json — build widgets/containers as usual, then the functions bundle
|
|
97
|
+
"scripts": {
|
|
98
|
+
"build": "vite build && vite build --config vite.functions.config.ts"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
That emits `dist/functions.umd.js` (matching `functions.files.js.umd`), whose named exports the
|
|
103
|
+
platform pairs with your manifest `definitions`. Then **deploy** it — for dev,
|
|
104
|
+
`smartlinks-publish` uploads `dist/` and registers in one step (see
|
|
105
|
+
[Deploying & registering](deploying-apps.md)).
|
|
106
|
+
|
|
66
107
|
---
|
|
67
108
|
|
|
68
109
|
## The two security questions every function answers
|
|
@@ -159,7 +200,9 @@ interface ServerFunctionContext {
|
|
|
159
200
|
/** Outbound HTTP — present only if you declared `network` (host-scoped if `network:<host>`). */
|
|
160
201
|
fetch: typeof fetch
|
|
161
202
|
|
|
162
|
-
/** Structured logging
|
|
203
|
+
/** Structured logging — returned inline on the admin surface and recorded as an
|
|
204
|
+
* execution activity event (owner Errors & Activity console). See "Seeing your
|
|
205
|
+
* function's runs and logs" below. */
|
|
163
206
|
log: (message: string, data?: Record<string, any>) => void
|
|
164
207
|
}
|
|
165
208
|
```
|
|
@@ -171,6 +214,73 @@ the only way authority stays correct.
|
|
|
171
214
|
|
|
172
215
|
---
|
|
173
216
|
|
|
217
|
+
## Runtime — what your function can use
|
|
218
|
+
|
|
219
|
+
Your function runs in a **web-standard sandbox** (think Cloudflare Workers / Deno), **not
|
|
220
|
+
Node**. Concretely it targets the **WinterTC Minimum Common Web Platform API** (WinterTC is the
|
|
221
|
+
Ecma International technical committee for this, formerly WinterCG) — the same surface those
|
|
222
|
+
runtimes guarantee — so what you can rely on is portable, and the future isolated runner will
|
|
223
|
+
host the same bundle unchanged.
|
|
224
|
+
|
|
225
|
+
**Available globals** (no import needed):
|
|
226
|
+
- **Data / encoding:** `JSON`, `URL`, `URLSearchParams`, `TextEncoder` / `TextDecoder`,
|
|
227
|
+
`atob` / `btoa`, `Buffer`, `structuredClone`.
|
|
228
|
+
- **Crypto:** `crypto` (Web Crypto) — `crypto.subtle` for hashing / HMAC / encrypt / sign /
|
|
229
|
+
verify, `crypto.getRandomValues`, `crypto.randomUUID`.
|
|
230
|
+
- **HTTP:** `fetch`, `Headers`, `Request`, `Response`, `FormData`, `Blob`, `File`,
|
|
231
|
+
`AbortController` / `AbortSignal` (use one for **request timeouts**).
|
|
232
|
+
- **Streams & timing:** `ReadableStream` / `WritableStream` / `TransformStream`,
|
|
233
|
+
`CompressionStream` / `DecompressionStream` (gzip/deflate for third-party payloads), the timer
|
|
234
|
+
functions, `queueMicrotask`, `performance`, `console`.
|
|
235
|
+
- **Runtime identity:** `navigator.userAgent` reports the runtime key (currently
|
|
236
|
+
`"SmartLinks-Functions"`) — use it if a portable dependency needs to feature-detect the host.
|
|
237
|
+
It stays stable when functions move to the isolated runner.
|
|
238
|
+
|
|
239
|
+
**Not available:** `require` / `import` of platform or Node modules, `process`, `fs`, and the
|
|
240
|
+
Node built-ins (`node:crypto`, `node:http`, …). There is no ambient database, key, or network
|
|
241
|
+
handle — everything the platform gives you comes through **`ctx`**.
|
|
242
|
+
|
|
243
|
+
**Dependencies:** bundle them. Your build must produce a **self-contained** UMD (deps compiled
|
|
244
|
+
in), and those deps must be **edge-compatible** — pure JS / Web APIs. A library that reaches for
|
|
245
|
+
Node built-ins (e.g. `axios`'s Node adapter, anything using `node:crypto`) will fail to load.
|
|
246
|
+
Prefer the platform primitives above over a dependency; when you do need one, pick edge-safe:
|
|
247
|
+
|
|
248
|
+
| Need | Native? | Recommended (bundle, edge-safe) |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| JSON | **native** — `JSON.parse/stringify` | — |
|
|
251
|
+
| XML parse / build | no | `fast-xml-parser` |
|
|
252
|
+
| CSV | no | `papaparse` |
|
|
253
|
+
| Schema validation of inputs | no | `zod` |
|
|
254
|
+
| JWT (sign/verify for a third-party API) | Web Crypto can, verbosely | `jose` |
|
|
255
|
+
| Hash / HMAC / encrypt | **native** — `crypto.subtle` | — |
|
|
256
|
+
|
|
257
|
+
**The common actions, and how to do each:**
|
|
258
|
+
|
|
259
|
+
| You want to… | Use | Requires |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| Call a third-party API (GET/POST) | `ctx.fetch(url, init)` — the standard Fetch API | capability `network` or `network:<host>` |
|
|
262
|
+
| Read a secret (API key, signing key) | `await ctx.secrets.get('<ref>')` | capability `secrets:<ref>` |
|
|
263
|
+
| Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
|
|
264
|
+
| Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
|
|
265
|
+
| Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
|
|
266
|
+
|
|
267
|
+
Notes:
|
|
268
|
+
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
269
|
+
process, then act on the core* — is: check the input, then call `ctx.sl.appRecords.create(…)`,
|
|
270
|
+
`ctx.sl.products.update(…)`, etc. `ctx.sl` is already authenticated as your declared authority
|
|
271
|
+
and capped by your capabilities, so you never construct a SmartLinks API URL or carry a key.
|
|
272
|
+
Use `fetch`/`ctx.fetch` for *third-party* servers; use `ctx.sl` for SmartLinks itself.
|
|
273
|
+
- **There is one `fetch`, and it is capability-gated.** Whether you call the global `fetch` or
|
|
274
|
+
`ctx.fetch`, the behaviour is identical: the call **throws** unless your declared `network`
|
|
275
|
+
(or `network:<host>`) capability covers the target host — the same way Deno's `fetch` throws
|
|
276
|
+
without `--allow-net`. There is no ungated escape hatch. Declare the hosts you need.
|
|
277
|
+
- **Secrets are read-only at runtime.** You fetch a secret you declared; you do **not** set or
|
|
278
|
+
rotate secrets from a function — that's an admin/deploy-time operation on the platform.
|
|
279
|
+
- **Signing a webhook / verifying a signature** is `crypto.subtle` with an HMAC key imported
|
|
280
|
+
from a secret — no Node `crypto` needed.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
174
284
|
## Worked example — a public competition entry
|
|
175
285
|
|
|
176
286
|
```ts
|
|
@@ -220,14 +330,41 @@ POST /public/collection/:collectionId/functions/:name # visibility: public (a
|
|
|
220
330
|
The request body is delivered to the handler as `event.body` (query string as
|
|
221
331
|
`event.query`). A function only runs on its own surface — calling an `admin` function on
|
|
222
332
|
the public endpoint is a `403`. The response is `{ ok: true, result }` on success, or
|
|
223
|
-
`{ error, message }` (HTTP 400) if the handler returned an error.
|
|
224
|
-
|
|
333
|
+
`{ error, message }` (HTTP 400) if the handler returned an error. On the **admin surface**
|
|
334
|
+
the response also includes `logs` (your `ctx.log` lines) and `durationMs` for quick
|
|
335
|
+
debugging; the **public surface returns only `result`** (a public caller never sees your
|
|
336
|
+
internal logs). `GET` on either endpoint lists the functions callable on that surface.
|
|
225
337
|
|
|
226
338
|
The admin surface is collection-admin gated, so an admin function's `caller` authority runs
|
|
227
339
|
at admin level, attributed to the signed-in admin. The public surface resolves auth if a
|
|
228
340
|
token is present (→ `owner`) and treats its absence as anonymous (→ `public`); a
|
|
229
341
|
`collection`-authority function runs elevated regardless.
|
|
230
342
|
|
|
343
|
+
### Before it will resolve
|
|
344
|
+
|
|
345
|
+
An http call returns `404 FUNCTION_NOT_FOUND` unless **both** of these are true (this is the
|
|
346
|
+
most common first-run surprise):
|
|
347
|
+
|
|
348
|
+
1. **The app's release is registered on the channel** the collection follows (see
|
|
349
|
+
[deploying-apps.md](deploying-apps.md)) — that's what publishes the function bundle into
|
|
350
|
+
the registry the runtime loads from.
|
|
351
|
+
2. **The app is enabled on the collection** (`appConfig.apps[]`, on that same channel — see
|
|
352
|
+
[appConfig.md](appConfig.md)).
|
|
353
|
+
|
|
354
|
+
So the end-to-end path is: *write → register the release → enable on a collection → call.*
|
|
355
|
+
|
|
356
|
+
### Seeing your function's runs and logs
|
|
357
|
+
|
|
358
|
+
Every invocation is recorded as an **execution** activity event, carrying your `ctx.log`
|
|
359
|
+
lines. Where to look, easiest first:
|
|
360
|
+
|
|
361
|
+
- **Admin-surface response** — `logs` + `durationMs` come straight back in the JSON.
|
|
362
|
+
- **Deployed test mode** — see below; real run, isolated logging.
|
|
363
|
+
- **Owner console** — the collection's **Advanced → Errors & Activity → events** tab,
|
|
364
|
+
filtered to **source = execution**: each run shows as `function <name> ok` (or an error),
|
|
365
|
+
and the detail carries your `ctx.log` output. (Telemetry is streamed, so allow a few
|
|
366
|
+
seconds.)
|
|
367
|
+
|
|
231
368
|
## Testing & preview
|
|
232
369
|
|
|
233
370
|
You don't have to deploy to find out whether a function works. There are three levels of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proveanything/smartlinks",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"smartlinks-register-release": "scripts/register-release.mjs"
|
|
18
|
+
"smartlinks-register-release": "scripts/register-release.mjs",
|
|
19
|
+
"smartlinks-publish": "scripts/publish.mjs"
|
|
19
20
|
},
|
|
20
21
|
"files": [
|
|
21
22
|
"dist/",
|
|
22
23
|
"docs/",
|
|
23
24
|
"scripts/register-release.mjs",
|
|
25
|
+
"scripts/publish.mjs",
|
|
24
26
|
"openapi.yaml",
|
|
25
27
|
"README.md"
|
|
26
28
|
],
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* publish — the FAST dev publish for a SmartLinks app. Builds nothing itself: it takes an already-
|
|
4
|
+
* built `dist/`, uploads it to the SmartLinks publish endpoint, which writes it straight to the CDN
|
|
5
|
+
* (dev = no-store, instantly fresh) and registers the release. One call = host + register, no Cloud
|
|
6
|
+
* Build, no Lovable. Works from anywhere: local, Claude, CI, or a build step.
|
|
7
|
+
*
|
|
8
|
+
* smartlinks-publish # build dist/ yourself first, then this
|
|
9
|
+
* smartlinks-publish --watch # re-publish on file change (save → live)
|
|
10
|
+
*
|
|
11
|
+
* Env:
|
|
12
|
+
* SMARTLINKS_APP_ID the platform app id (required)
|
|
13
|
+
* SMARTLINKS_DEPLOY_KEY channel-scoped deploy key (dev key is dev-only, safe to keep locally)
|
|
14
|
+
* SMARTLINKS_API API host (default https://smartlinks.app)
|
|
15
|
+
* SMARTLINKS_CHANNEL channel (default dev; only dev is supported by this fast path today)
|
|
16
|
+
* Flags: --dir <dist> --watch --channel <dev>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, readdirSync, statSync, watch } from 'node:fs'
|
|
20
|
+
import { join, relative, sep } from 'node:path'
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2)
|
|
23
|
+
const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : def }
|
|
24
|
+
|
|
25
|
+
const API = process.env.SMARTLINKS_API || 'https://smartlinks.app'
|
|
26
|
+
const KEY = process.env.SMARTLINKS_DEPLOY_KEY
|
|
27
|
+
const APP_ID = process.env.SMARTLINKS_APP_ID
|
|
28
|
+
const CHANNEL = flag('--channel', process.env.SMARTLINKS_CHANNEL || 'dev')
|
|
29
|
+
const DIR = flag('--dir', 'dist')
|
|
30
|
+
const WATCH = args.includes('--watch')
|
|
31
|
+
|
|
32
|
+
// Mirror of the server's write allowlist — a bundle is static web assets only.
|
|
33
|
+
const ALLOWED = new Set(['.js', '.mjs', '.css', '.json', '.map', '.wasm', '.html', '.txt',
|
|
34
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.woff', '.woff2', '.ttf', '.otf'])
|
|
35
|
+
const extOf = (p) => { const i = p.lastIndexOf('.'); return i < 0 ? '' : p.slice(i).toLowerCase() }
|
|
36
|
+
|
|
37
|
+
function fail(msg) { console.error(`❌ ${msg}`); process.exit(1) }
|
|
38
|
+
if (!APP_ID) fail('SMARTLINKS_APP_ID is required')
|
|
39
|
+
if (!KEY) fail('SMARTLINKS_DEPLOY_KEY is required')
|
|
40
|
+
|
|
41
|
+
function collectFiles(dir) {
|
|
42
|
+
const out = {}
|
|
43
|
+
const walk = (d) => {
|
|
44
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
45
|
+
const full = join(d, e.name)
|
|
46
|
+
if (e.isDirectory()) { walk(full); continue }
|
|
47
|
+
if (!ALLOWED.has(extOf(e.name))) continue
|
|
48
|
+
const rel = relative(dir, full).split(sep).join('/') // forward slashes for the bucket
|
|
49
|
+
out[rel] = readFileSync(full).toString('base64')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
walk(dir)
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function publishOnce() {
|
|
57
|
+
let stat
|
|
58
|
+
try { stat = statSync(DIR) } catch { fail(`build dir not found: ${DIR} (build first)`) }
|
|
59
|
+
if (!stat.isDirectory()) fail(`${DIR} is not a directory`)
|
|
60
|
+
|
|
61
|
+
const files = collectFiles(DIR)
|
|
62
|
+
if (!files['app.manifest.json']) fail(`${DIR}/app.manifest.json not found — is this a built app bundle?`)
|
|
63
|
+
|
|
64
|
+
const t0 = Date.now()
|
|
65
|
+
const res = await fetch(`${API}/api/v1/apps/${APP_ID}/publish?channel=${encodeURIComponent(CHANNEL)}`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
|
|
68
|
+
body: JSON.stringify({ files }),
|
|
69
|
+
}).catch((e) => fail(`request failed — ${e.message}`))
|
|
70
|
+
|
|
71
|
+
const b = await res.json().catch(() => ({}))
|
|
72
|
+
if (!res.ok || !b.ok) {
|
|
73
|
+
console.error(`❌ publish failed (${res.status}):`)
|
|
74
|
+
for (const e of b.errors || []) console.error(` • ${e.path || ''}: ${e.message}`)
|
|
75
|
+
if (WATCH) return false // keep watching; don't exit
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
for (const w of b.warnings || []) console.warn(` ⚠︎ ${w.path}: ${w.message}`)
|
|
79
|
+
console.log(`✅ ${APP_ID}@${b.version} → ${CHANNEL} (${Object.keys(files).length} files, ${Date.now() - t0}ms) — functions: ${(b.functions || []).join(', ') || 'none'}`)
|
|
80
|
+
console.log(` ${b.bundleBaseUrl}`)
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!WATCH) {
|
|
85
|
+
await publishOnce()
|
|
86
|
+
} else {
|
|
87
|
+
console.log(`👀 watching ${DIR} — publishing ${APP_ID} to ${CHANNEL} on change (Ctrl+C to stop)`)
|
|
88
|
+
await publishOnce()
|
|
89
|
+
let timer = null
|
|
90
|
+
let publishing = false
|
|
91
|
+
watch(DIR, { recursive: true }, () => {
|
|
92
|
+
clearTimeout(timer)
|
|
93
|
+
timer = setTimeout(async () => {
|
|
94
|
+
if (publishing) return
|
|
95
|
+
publishing = true
|
|
96
|
+
try { await publishOnce() } finally { publishing = false }
|
|
97
|
+
}, 300) // debounce a burst of file writes from one build
|
|
98
|
+
})
|
|
99
|
+
}
|