@proveanything/smartlinks 2.0.1 → 2.0.3

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.1 | Generated: 2026-09-16T05:23:18.331Z
3
+ Version: 2.0.3 | Generated: 2026-09-20T09:34:31.596Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -71,9 +71,13 @@ A release is registered on one **channel**, matching how you deploy:
71
71
 
72
72
  | Channel | Deployed from | Typical use |
73
73
  |---|---|---|
74
- | `dev` | **Lovable "Publish"** | Live development / testing |
75
- | `beta` | your pipeline | Staging / preview |
76
- | `prod` | your **Cloud Build** toolset (from git) | Production |
74
+ | `dev` | **Lovable "Publish"** | Live development / testing |
75
+ | `alpha` | your pipeline | Early internal testing |
76
+ | `beta` | your pipeline | Staging / preview |
77
+ | `stable`| your **Cloud Build** toolset (from git) | Production |
78
+
79
+ The canonical channels are **`dev` · `alpha` · `beta` · `stable`**. Legacy `prod`/`production`/
80
+ `live` are accepted and normalise to **`stable`** (there is no separate "prod" channel).
77
81
 
78
82
  A collection chooses which channel it follows, so you can point a test collection at `dev`
79
83
  and exercise a build before it reaches `prod`.
@@ -130,7 +134,7 @@ Content-Type: application/json
130
134
 
131
135
  | Status | Meaning |
132
136
  |---|---|
133
- | `200` | Registered. Body: `{ ok: true, appId, channel, version, functions: [names], registeredAt }` |
137
+ | `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). |
134
138
  | `422` | Validation failed — **the build should fail**. Body: `{ ok: false, errors: [...] }` |
135
139
  | `401` | Missing/invalid deploy key |
136
140
  | `403` | Key not permitted for that channel (e.g. a dev key targeting `prod`) — `CHANNEL_FORBIDDEN` |
@@ -155,7 +159,11 @@ today.)
155
159
 
156
160
  ### What gets validated
157
161
 
158
- - `manifest.meta.appId` must match the `{appId}` in the URL.
162
+ - The **app id is the `{appId}` in the URL** — the platform-assigned id, which is authoritative
163
+ (it's what every collection's config + the CDN path already bind to). `manifest.meta.appId` is
164
+ **ignored** for identity: if it differs, the release still registers under the URL id and you get
165
+ a non-blocking `APPID_IGNORED` warning. So pass your **platform id** in the URL (the script below
166
+ reads it from `SMARTLINKS_APP_ID`), and don't rely on the manifest to declare it.
159
167
  - The **`functions` block** is validated with the *same* rules the runtime enforces
160
168
  (names, triggers, `visibility`/`authority`, the capability grammar, duplicate names) — so
161
169
  a malformed server function is caught **at deploy time**, not at runtime. See
@@ -169,70 +177,42 @@ Registration is the **last step of your build** — after bundles are built and
169
177
  small script that reads your built manifest and POSTs it, and **exits non-zero on failure**
170
178
  so a bad install fails the publish.
171
179
 
172
- ```js
173
- // scripts/register-release.mjs — run as the build's postbuild step
174
- import { readFileSync } from 'node:fs'
175
- import { execSync } from 'node:child_process'
176
-
177
- const API = process.env.SMARTLINKS_API || 'https://smartlinks.app' // a VPC env: set to its host
178
- const KEY = process.env.SMARTLINKS_DEPLOY_KEY // Lovable Build Secret (dev) / CI secret (prod)
179
- const CHANNEL = process.env.SMARTLINKS_CHANNEL // 'dev' | 'beta' | 'prod'; UNSET ⇒ don't register
180
-
181
- // --- Gate: only register when this build is meant to ---
182
- // A preview / live-edit build (no channel) skips quietly so it never fails. A build that
183
- // declares a channel but has no key is a hard error IF it's prod; dev skips quietly.
184
- if (!CHANNEL) {
185
- console.log('ℹ︎ SmartLinks: SMARTLINKS_CHANNEL unset — skipping release registration (preview build).')
186
- process.exit(0)
187
- }
188
- if (!KEY) {
189
- if (CHANNEL === 'prod') { console.error('❌ prod build but SMARTLINKS_DEPLOY_KEY is missing'); process.exit(1) }
190
- console.log(`ℹ︎ SmartLinks: no deploy key for "${CHANNEL}" — skipping registration.`)
191
- process.exit(0)
192
- }
193
-
194
- const manifest = JSON.parse(readFileSync('dist/app.manifest.json', 'utf8'))
195
- const appId = manifest.meta.appId
196
- const version = manifest.meta.version
197
- const gitHash = (() => { try { return execSync('git rev-parse --short HEAD').toString().trim() } catch { return null } })()
198
- // dev: your published Lovable URL; prod: the SmartLinks CDN base. Set SMARTLINKS_BUNDLE_BASE_URL.
199
- const bundleBaseUrl = process.env.SMARTLINKS_BUNDLE_BASE_URL || `https://smartlinks.app/apps/${appId}/${version}`
200
-
201
- const res = await fetch(`${API}/api/v1/apps/${appId}/releases`, {
202
- method: 'POST',
203
- headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
204
- body: JSON.stringify({
205
- channel: CHANNEL, version, manifest, bundleBaseUrl,
206
- build: { at: new Date().toISOString(), gitHash, builder: CHANNEL === 'dev' ? 'lovable' : 'ci' },
207
- }),
208
- })
209
-
210
- const body = await res.json().catch(() => ({}))
211
- if (!res.ok || !body.ok) {
212
- console.error(`❌ SmartLinks registration failed (${res.status}):`)
213
- for (const e of body.errors || []) console.error(` • ${e.path}: ${e.message}`)
214
- process.exit(1) // fail the build
215
- }
216
- console.log(`✅ Registered ${appId}@${version} on "${CHANNEL}" — functions: ${(body.functions || []).join(', ') || 'none'}`)
217
- ```
218
-
219
- Wire it after your bundle build/hash step:
180
+ The SDK ships the script, so you don't copy-paste it — run it as your postbuild step. It reads
181
+ your built manifest, POSTs it, prints any warnings, and exits non-zero on failure.
220
182
 
221
183
  ```jsonc
222
184
  // package.json
223
185
  "scripts": {
224
186
  "build": "vite build && … && node scripts/hash-bundles.mjs",
225
- "postbuild": "node scripts/register-release.mjs"
187
+ "postbuild": "smartlinks-register-release" // ships in @proveanything/smartlinks
226
188
  }
227
189
  ```
228
190
 
191
+ It is driven entirely by env vars, so the same command works for dev (Lovable) and prod (CI):
192
+
193
+ | Env var | Required | What it is |
194
+ |---|---|---|
195
+ | `SMARTLINKS_APP_ID` | **yes** | your **platform** app id — authoritative, assigned by SmartLinks (per-project). Not the manifest's `meta.appId`. |
196
+ | `SMARTLINKS_DEPLOY_KEY` | yes* | channel-scoped deploy key. *dev builds skip quietly if unset; `beta`/`stable` hard-fail. |
197
+ | `SMARTLINKS_CHANNEL` | the gate | `dev` \| `alpha` \| `beta` \| `stable` (legacy `prod` → `stable`). **UNSET ⇒ skip** (preview builds never register). |
198
+ | `SMARTLINKS_BUNDLE_BASE_URL` | recommended | where the files are served — **dev:** your Lovable URL; **prod:** the CDN base. |
199
+ | `SMARTLINKS_API` | no | API host; default `https://smartlinks.app`. A VPC env uses its own. |
200
+ | `SMARTLINKS_MANIFEST` | no | manifest path; default `dist/app.manifest.json`. |
201
+
202
+ > The full source is at `scripts/register-release.mjs` in the SDK package if you'd rather vendor it.
203
+
229
204
  Registration is gated by **`SMARTLINKS_CHANNEL`**, so the three Lovable build types behave correctly:
230
205
 
231
206
  | Build | `SMARTLINKS_CHANNEL` | Result |
232
207
  |---|---|---|
233
208
  | **Preview / live-edit** | unset | **skips quietly** — never registers, never fails |
234
209
  | **Dev (Publish)** | `dev` | registers to `dev` with the workspace Build-Secret key + your Lovable `SMARTLINKS_BUNDLE_BASE_URL` |
235
- | **Prod (CI)** | `prod` | registers to `prod` with the prod key; a missing key **hard-fails** |
210
+ | **Prod (CI)** | `stable` | registers to `stable` with the prod/master key; a missing key **hard-fails** |
211
+
212
+ **Two secrets, two scopes:** the **deploy key** is channel-scoped and can be a *workspace-level*
213
+ Lovable Build Secret shared by every app (a dev key only writes `dev`, so sharing it is safe). The
214
+ **`SMARTLINKS_APP_ID` is per-app** — set it as a *project-level* variable on each app, so each
215
+ registers under its own authoritative platform id.
236
216
 
237
217
  The gate is `SMARTLINKS_CHANNEL`, so **set it only where you want a release** — i.e. on the
238
218
  Publish/CI build, not on preview. If your host exposes a publish-only signal (an env var it sets
@@ -159,7 +159,9 @@ interface ServerFunctionContext {
159
159
  /** Outbound HTTP — present only if you declared `network` (host-scoped if `network:<host>`). */
160
160
  fetch: typeof fetch
161
161
 
162
- /** Structured logging, captured into your function's run telemetry. */
162
+ /** Structured logging — returned inline on the admin surface and recorded as an
163
+ * execution activity event (owner Errors & Activity console). See "Seeing your
164
+ * function's runs and logs" below. */
163
165
  log: (message: string, data?: Record<string, any>) => void
164
166
  }
165
167
  ```
@@ -171,6 +173,73 @@ the only way authority stays correct.
171
173
 
172
174
  ---
173
175
 
176
+ ## Runtime — what your function can use
177
+
178
+ Your function runs in a **web-standard sandbox** (think Cloudflare Workers / Deno), **not
179
+ Node**. Concretely it targets the **WinterTC Minimum Common Web Platform API** (WinterTC is the
180
+ Ecma International technical committee for this, formerly WinterCG) — the same surface those
181
+ runtimes guarantee — so what you can rely on is portable, and the future isolated runner will
182
+ host the same bundle unchanged.
183
+
184
+ **Available globals** (no import needed):
185
+ - **Data / encoding:** `JSON`, `URL`, `URLSearchParams`, `TextEncoder` / `TextDecoder`,
186
+ `atob` / `btoa`, `Buffer`, `structuredClone`.
187
+ - **Crypto:** `crypto` (Web Crypto) — `crypto.subtle` for hashing / HMAC / encrypt / sign /
188
+ verify, `crypto.getRandomValues`, `crypto.randomUUID`.
189
+ - **HTTP:** `fetch`, `Headers`, `Request`, `Response`, `FormData`, `Blob`, `File`,
190
+ `AbortController` / `AbortSignal` (use one for **request timeouts**).
191
+ - **Streams & timing:** `ReadableStream` / `WritableStream` / `TransformStream`,
192
+ `CompressionStream` / `DecompressionStream` (gzip/deflate for third-party payloads), the timer
193
+ functions, `queueMicrotask`, `performance`, `console`.
194
+ - **Runtime identity:** `navigator.userAgent` reports the runtime key (currently
195
+ `"SmartLinks-Functions"`) — use it if a portable dependency needs to feature-detect the host.
196
+ It stays stable when functions move to the isolated runner.
197
+
198
+ **Not available:** `require` / `import` of platform or Node modules, `process`, `fs`, and the
199
+ Node built-ins (`node:crypto`, `node:http`, …). There is no ambient database, key, or network
200
+ handle — everything the platform gives you comes through **`ctx`**.
201
+
202
+ **Dependencies:** bundle them. Your build must produce a **self-contained** UMD (deps compiled
203
+ in), and those deps must be **edge-compatible** — pure JS / Web APIs. A library that reaches for
204
+ Node built-ins (e.g. `axios`'s Node adapter, anything using `node:crypto`) will fail to load.
205
+ Prefer the platform primitives above over a dependency; when you do need one, pick edge-safe:
206
+
207
+ | Need | Native? | Recommended (bundle, edge-safe) |
208
+ |---|---|---|
209
+ | JSON | **native** — `JSON.parse/stringify` | — |
210
+ | XML parse / build | no | `fast-xml-parser` |
211
+ | CSV | no | `papaparse` |
212
+ | Schema validation of inputs | no | `zod` |
213
+ | JWT (sign/verify for a third-party API) | Web Crypto can, verbosely | `jose` |
214
+ | Hash / HMAC / encrypt | **native** — `crypto.subtle` | — |
215
+
216
+ **The common actions, and how to do each:**
217
+
218
+ | You want to… | Use | Requires |
219
+ |---|---|---|
220
+ | Call a third-party API (GET/POST) | `ctx.fetch(url, init)` — the standard Fetch API | capability `network` or `network:<host>` |
221
+ | Read a secret (API key, signing key) | `await ctx.secrets.get('<ref>')` | capability `secrets:<ref>` |
222
+ | Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
223
+ | Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
224
+ | Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
225
+
226
+ Notes:
227
+ - **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
228
+ process, then act on the core* — is: check the input, then call `ctx.sl.appRecords.create(…)`,
229
+ `ctx.sl.products.update(…)`, etc. `ctx.sl` is already authenticated as your declared authority
230
+ and capped by your capabilities, so you never construct a SmartLinks API URL or carry a key.
231
+ Use `fetch`/`ctx.fetch` for *third-party* servers; use `ctx.sl` for SmartLinks itself.
232
+ - **There is one `fetch`, and it is capability-gated.** Whether you call the global `fetch` or
233
+ `ctx.fetch`, the behaviour is identical: the call **throws** unless your declared `network`
234
+ (or `network:<host>`) capability covers the target host — the same way Deno's `fetch` throws
235
+ without `--allow-net`. There is no ungated escape hatch. Declare the hosts you need.
236
+ - **Secrets are read-only at runtime.** You fetch a secret you declared; you do **not** set or
237
+ rotate secrets from a function — that's an admin/deploy-time operation on the platform.
238
+ - **Signing a webhook / verifying a signature** is `crypto.subtle` with an HMAC key imported
239
+ from a secret — no Node `crypto` needed.
240
+
241
+ ---
242
+
174
243
  ## Worked example — a public competition entry
175
244
 
176
245
  ```ts
@@ -220,14 +289,41 @@ POST /public/collection/:collectionId/functions/:name # visibility: public (a
220
289
  The request body is delivered to the handler as `event.body` (query string as
221
290
  `event.query`). A function only runs on its own surface — calling an `admin` function on
222
291
  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. `GET` on either endpoint
224
- lists the functions callable on that surface.
292
+ `{ error, message }` (HTTP 400) if the handler returned an error. On the **admin surface**
293
+ the response also includes `logs` (your `ctx.log` lines) and `durationMs` for quick
294
+ debugging; the **public surface returns only `result`** (a public caller never sees your
295
+ internal logs). `GET` on either endpoint lists the functions callable on that surface.
225
296
 
226
297
  The admin surface is collection-admin gated, so an admin function's `caller` authority runs
227
298
  at admin level, attributed to the signed-in admin. The public surface resolves auth if a
228
299
  token is present (→ `owner`) and treats its absence as anonymous (→ `public`); a
229
300
  `collection`-authority function runs elevated regardless.
230
301
 
302
+ ### Before it will resolve
303
+
304
+ An http call returns `404 FUNCTION_NOT_FOUND` unless **both** of these are true (this is the
305
+ most common first-run surprise):
306
+
307
+ 1. **The app's release is registered on the channel** the collection follows (see
308
+ [deploying-apps.md](deploying-apps.md)) — that's what publishes the function bundle into
309
+ the registry the runtime loads from.
310
+ 2. **The app is enabled on the collection** (`appConfig.apps[]`, on that same channel — see
311
+ [appConfig.md](appConfig.md)).
312
+
313
+ So the end-to-end path is: *write → register the release → enable on a collection → call.*
314
+
315
+ ### Seeing your function's runs and logs
316
+
317
+ Every invocation is recorded as an **execution** activity event, carrying your `ctx.log`
318
+ lines. Where to look, easiest first:
319
+
320
+ - **Admin-surface response** — `logs` + `durationMs` come straight back in the JSON.
321
+ - **Deployed test mode** — see below; real run, isolated logging.
322
+ - **Owner console** — the collection's **Advanced → Errors & Activity → events** tab,
323
+ filtered to **source = execution**: each run shows as `function <name> ok` (or an error),
324
+ and the detail carries your `ctx.log` output. (Telemetry is streamed, so allow a few
325
+ seconds.)
326
+
231
327
  ## Testing & preview
232
328
 
233
329
  You don't have to deploy to find out whether a function works. There are three levels of
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.1 | Generated: 2026-09-16T05:23:18.331Z
3
+ Version: 2.0.3 | Generated: 2026-09-20T09:34:31.596Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -71,9 +71,13 @@ A release is registered on one **channel**, matching how you deploy:
71
71
 
72
72
  | Channel | Deployed from | Typical use |
73
73
  |---|---|---|
74
- | `dev` | **Lovable "Publish"** | Live development / testing |
75
- | `beta` | your pipeline | Staging / preview |
76
- | `prod` | your **Cloud Build** toolset (from git) | Production |
74
+ | `dev` | **Lovable "Publish"** | Live development / testing |
75
+ | `alpha` | your pipeline | Early internal testing |
76
+ | `beta` | your pipeline | Staging / preview |
77
+ | `stable`| your **Cloud Build** toolset (from git) | Production |
78
+
79
+ The canonical channels are **`dev` · `alpha` · `beta` · `stable`**. Legacy `prod`/`production`/
80
+ `live` are accepted and normalise to **`stable`** (there is no separate "prod" channel).
77
81
 
78
82
  A collection chooses which channel it follows, so you can point a test collection at `dev`
79
83
  and exercise a build before it reaches `prod`.
@@ -130,7 +134,7 @@ Content-Type: application/json
130
134
 
131
135
  | Status | Meaning |
132
136
  |---|---|
133
- | `200` | Registered. Body: `{ ok: true, appId, channel, version, functions: [names], registeredAt }` |
137
+ | `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). |
134
138
  | `422` | Validation failed — **the build should fail**. Body: `{ ok: false, errors: [...] }` |
135
139
  | `401` | Missing/invalid deploy key |
136
140
  | `403` | Key not permitted for that channel (e.g. a dev key targeting `prod`) — `CHANNEL_FORBIDDEN` |
@@ -155,7 +159,11 @@ today.)
155
159
 
156
160
  ### What gets validated
157
161
 
158
- - `manifest.meta.appId` must match the `{appId}` in the URL.
162
+ - The **app id is the `{appId}` in the URL** — the platform-assigned id, which is authoritative
163
+ (it's what every collection's config + the CDN path already bind to). `manifest.meta.appId` is
164
+ **ignored** for identity: if it differs, the release still registers under the URL id and you get
165
+ a non-blocking `APPID_IGNORED` warning. So pass your **platform id** in the URL (the script below
166
+ reads it from `SMARTLINKS_APP_ID`), and don't rely on the manifest to declare it.
159
167
  - The **`functions` block** is validated with the *same* rules the runtime enforces
160
168
  (names, triggers, `visibility`/`authority`, the capability grammar, duplicate names) — so
161
169
  a malformed server function is caught **at deploy time**, not at runtime. See
@@ -169,70 +177,42 @@ Registration is the **last step of your build** — after bundles are built and
169
177
  small script that reads your built manifest and POSTs it, and **exits non-zero on failure**
170
178
  so a bad install fails the publish.
171
179
 
172
- ```js
173
- // scripts/register-release.mjs — run as the build's postbuild step
174
- import { readFileSync } from 'node:fs'
175
- import { execSync } from 'node:child_process'
176
-
177
- const API = process.env.SMARTLINKS_API || 'https://smartlinks.app' // a VPC env: set to its host
178
- const KEY = process.env.SMARTLINKS_DEPLOY_KEY // Lovable Build Secret (dev) / CI secret (prod)
179
- const CHANNEL = process.env.SMARTLINKS_CHANNEL // 'dev' | 'beta' | 'prod'; UNSET ⇒ don't register
180
-
181
- // --- Gate: only register when this build is meant to ---
182
- // A preview / live-edit build (no channel) skips quietly so it never fails. A build that
183
- // declares a channel but has no key is a hard error IF it's prod; dev skips quietly.
184
- if (!CHANNEL) {
185
- console.log('ℹ︎ SmartLinks: SMARTLINKS_CHANNEL unset — skipping release registration (preview build).')
186
- process.exit(0)
187
- }
188
- if (!KEY) {
189
- if (CHANNEL === 'prod') { console.error('❌ prod build but SMARTLINKS_DEPLOY_KEY is missing'); process.exit(1) }
190
- console.log(`ℹ︎ SmartLinks: no deploy key for "${CHANNEL}" — skipping registration.`)
191
- process.exit(0)
192
- }
193
-
194
- const manifest = JSON.parse(readFileSync('dist/app.manifest.json', 'utf8'))
195
- const appId = manifest.meta.appId
196
- const version = manifest.meta.version
197
- const gitHash = (() => { try { return execSync('git rev-parse --short HEAD').toString().trim() } catch { return null } })()
198
- // dev: your published Lovable URL; prod: the SmartLinks CDN base. Set SMARTLINKS_BUNDLE_BASE_URL.
199
- const bundleBaseUrl = process.env.SMARTLINKS_BUNDLE_BASE_URL || `https://smartlinks.app/apps/${appId}/${version}`
200
-
201
- const res = await fetch(`${API}/api/v1/apps/${appId}/releases`, {
202
- method: 'POST',
203
- headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
204
- body: JSON.stringify({
205
- channel: CHANNEL, version, manifest, bundleBaseUrl,
206
- build: { at: new Date().toISOString(), gitHash, builder: CHANNEL === 'dev' ? 'lovable' : 'ci' },
207
- }),
208
- })
209
-
210
- const body = await res.json().catch(() => ({}))
211
- if (!res.ok || !body.ok) {
212
- console.error(`❌ SmartLinks registration failed (${res.status}):`)
213
- for (const e of body.errors || []) console.error(` • ${e.path}: ${e.message}`)
214
- process.exit(1) // fail the build
215
- }
216
- console.log(`✅ Registered ${appId}@${version} on "${CHANNEL}" — functions: ${(body.functions || []).join(', ') || 'none'}`)
217
- ```
218
-
219
- Wire it after your bundle build/hash step:
180
+ The SDK ships the script, so you don't copy-paste it — run it as your postbuild step. It reads
181
+ your built manifest, POSTs it, prints any warnings, and exits non-zero on failure.
220
182
 
221
183
  ```jsonc
222
184
  // package.json
223
185
  "scripts": {
224
186
  "build": "vite build && … && node scripts/hash-bundles.mjs",
225
- "postbuild": "node scripts/register-release.mjs"
187
+ "postbuild": "smartlinks-register-release" // ships in @proveanything/smartlinks
226
188
  }
227
189
  ```
228
190
 
191
+ It is driven entirely by env vars, so the same command works for dev (Lovable) and prod (CI):
192
+
193
+ | Env var | Required | What it is |
194
+ |---|---|---|
195
+ | `SMARTLINKS_APP_ID` | **yes** | your **platform** app id — authoritative, assigned by SmartLinks (per-project). Not the manifest's `meta.appId`. |
196
+ | `SMARTLINKS_DEPLOY_KEY` | yes* | channel-scoped deploy key. *dev builds skip quietly if unset; `beta`/`stable` hard-fail. |
197
+ | `SMARTLINKS_CHANNEL` | the gate | `dev` \| `alpha` \| `beta` \| `stable` (legacy `prod` → `stable`). **UNSET ⇒ skip** (preview builds never register). |
198
+ | `SMARTLINKS_BUNDLE_BASE_URL` | recommended | where the files are served — **dev:** your Lovable URL; **prod:** the CDN base. |
199
+ | `SMARTLINKS_API` | no | API host; default `https://smartlinks.app`. A VPC env uses its own. |
200
+ | `SMARTLINKS_MANIFEST` | no | manifest path; default `dist/app.manifest.json`. |
201
+
202
+ > The full source is at `scripts/register-release.mjs` in the SDK package if you'd rather vendor it.
203
+
229
204
  Registration is gated by **`SMARTLINKS_CHANNEL`**, so the three Lovable build types behave correctly:
230
205
 
231
206
  | Build | `SMARTLINKS_CHANNEL` | Result |
232
207
  |---|---|---|
233
208
  | **Preview / live-edit** | unset | **skips quietly** — never registers, never fails |
234
209
  | **Dev (Publish)** | `dev` | registers to `dev` with the workspace Build-Secret key + your Lovable `SMARTLINKS_BUNDLE_BASE_URL` |
235
- | **Prod (CI)** | `prod` | registers to `prod` with the prod key; a missing key **hard-fails** |
210
+ | **Prod (CI)** | `stable` | registers to `stable` with the prod/master key; a missing key **hard-fails** |
211
+
212
+ **Two secrets, two scopes:** the **deploy key** is channel-scoped and can be a *workspace-level*
213
+ Lovable Build Secret shared by every app (a dev key only writes `dev`, so sharing it is safe). The
214
+ **`SMARTLINKS_APP_ID` is per-app** — set it as a *project-level* variable on each app, so each
215
+ registers under its own authoritative platform id.
236
216
 
237
217
  The gate is `SMARTLINKS_CHANNEL`, so **set it only where you want a release** — i.e. on the
238
218
  Publish/CI build, not on preview. If your host exposes a publish-only signal (an env var it sets
@@ -159,7 +159,9 @@ interface ServerFunctionContext {
159
159
  /** Outbound HTTP — present only if you declared `network` (host-scoped if `network:<host>`). */
160
160
  fetch: typeof fetch
161
161
 
162
- /** Structured logging, captured into your function's run telemetry. */
162
+ /** Structured logging — returned inline on the admin surface and recorded as an
163
+ * execution activity event (owner Errors & Activity console). See "Seeing your
164
+ * function's runs and logs" below. */
163
165
  log: (message: string, data?: Record<string, any>) => void
164
166
  }
165
167
  ```
@@ -171,6 +173,73 @@ the only way authority stays correct.
171
173
 
172
174
  ---
173
175
 
176
+ ## Runtime — what your function can use
177
+
178
+ Your function runs in a **web-standard sandbox** (think Cloudflare Workers / Deno), **not
179
+ Node**. Concretely it targets the **WinterTC Minimum Common Web Platform API** (WinterTC is the
180
+ Ecma International technical committee for this, formerly WinterCG) — the same surface those
181
+ runtimes guarantee — so what you can rely on is portable, and the future isolated runner will
182
+ host the same bundle unchanged.
183
+
184
+ **Available globals** (no import needed):
185
+ - **Data / encoding:** `JSON`, `URL`, `URLSearchParams`, `TextEncoder` / `TextDecoder`,
186
+ `atob` / `btoa`, `Buffer`, `structuredClone`.
187
+ - **Crypto:** `crypto` (Web Crypto) — `crypto.subtle` for hashing / HMAC / encrypt / sign /
188
+ verify, `crypto.getRandomValues`, `crypto.randomUUID`.
189
+ - **HTTP:** `fetch`, `Headers`, `Request`, `Response`, `FormData`, `Blob`, `File`,
190
+ `AbortController` / `AbortSignal` (use one for **request timeouts**).
191
+ - **Streams & timing:** `ReadableStream` / `WritableStream` / `TransformStream`,
192
+ `CompressionStream` / `DecompressionStream` (gzip/deflate for third-party payloads), the timer
193
+ functions, `queueMicrotask`, `performance`, `console`.
194
+ - **Runtime identity:** `navigator.userAgent` reports the runtime key (currently
195
+ `"SmartLinks-Functions"`) — use it if a portable dependency needs to feature-detect the host.
196
+ It stays stable when functions move to the isolated runner.
197
+
198
+ **Not available:** `require` / `import` of platform or Node modules, `process`, `fs`, and the
199
+ Node built-ins (`node:crypto`, `node:http`, …). There is no ambient database, key, or network
200
+ handle — everything the platform gives you comes through **`ctx`**.
201
+
202
+ **Dependencies:** bundle them. Your build must produce a **self-contained** UMD (deps compiled
203
+ in), and those deps must be **edge-compatible** — pure JS / Web APIs. A library that reaches for
204
+ Node built-ins (e.g. `axios`'s Node adapter, anything using `node:crypto`) will fail to load.
205
+ Prefer the platform primitives above over a dependency; when you do need one, pick edge-safe:
206
+
207
+ | Need | Native? | Recommended (bundle, edge-safe) |
208
+ |---|---|---|
209
+ | JSON | **native** — `JSON.parse/stringify` | — |
210
+ | XML parse / build | no | `fast-xml-parser` |
211
+ | CSV | no | `papaparse` |
212
+ | Schema validation of inputs | no | `zod` |
213
+ | JWT (sign/verify for a third-party API) | Web Crypto can, verbosely | `jose` |
214
+ | Hash / HMAC / encrypt | **native** — `crypto.subtle` | — |
215
+
216
+ **The common actions, and how to do each:**
217
+
218
+ | You want to… | Use | Requires |
219
+ |---|---|---|
220
+ | Call a third-party API (GET/POST) | `ctx.fetch(url, init)` — the standard Fetch API | capability `network` or `network:<host>` |
221
+ | Read a secret (API key, signing key) | `await ctx.secrets.get('<ref>')` | capability `secrets:<ref>` |
222
+ | Hash / HMAC-sign / verify / encrypt | `crypto.subtle` (Web Crypto) | — |
223
+ | Random id / bytes | `crypto.randomUUID()` / `crypto.getRandomValues()` | — |
224
+ | Read or write SmartLinks data | `ctx.sl.*` (records, products, attestations, …) | the matching `sl:<res>:<read\|write>` |
225
+
226
+ Notes:
227
+ - **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
228
+ process, then act on the core* — is: check the input, then call `ctx.sl.appRecords.create(…)`,
229
+ `ctx.sl.products.update(…)`, etc. `ctx.sl` is already authenticated as your declared authority
230
+ and capped by your capabilities, so you never construct a SmartLinks API URL or carry a key.
231
+ Use `fetch`/`ctx.fetch` for *third-party* servers; use `ctx.sl` for SmartLinks itself.
232
+ - **There is one `fetch`, and it is capability-gated.** Whether you call the global `fetch` or
233
+ `ctx.fetch`, the behaviour is identical: the call **throws** unless your declared `network`
234
+ (or `network:<host>`) capability covers the target host — the same way Deno's `fetch` throws
235
+ without `--allow-net`. There is no ungated escape hatch. Declare the hosts you need.
236
+ - **Secrets are read-only at runtime.** You fetch a secret you declared; you do **not** set or
237
+ rotate secrets from a function — that's an admin/deploy-time operation on the platform.
238
+ - **Signing a webhook / verifying a signature** is `crypto.subtle` with an HMAC key imported
239
+ from a secret — no Node `crypto` needed.
240
+
241
+ ---
242
+
174
243
  ## Worked example — a public competition entry
175
244
 
176
245
  ```ts
@@ -220,14 +289,41 @@ POST /public/collection/:collectionId/functions/:name # visibility: public (a
220
289
  The request body is delivered to the handler as `event.body` (query string as
221
290
  `event.query`). A function only runs on its own surface — calling an `admin` function on
222
291
  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. `GET` on either endpoint
224
- lists the functions callable on that surface.
292
+ `{ error, message }` (HTTP 400) if the handler returned an error. On the **admin surface**
293
+ the response also includes `logs` (your `ctx.log` lines) and `durationMs` for quick
294
+ debugging; the **public surface returns only `result`** (a public caller never sees your
295
+ internal logs). `GET` on either endpoint lists the functions callable on that surface.
225
296
 
226
297
  The admin surface is collection-admin gated, so an admin function's `caller` authority runs
227
298
  at admin level, attributed to the signed-in admin. The public surface resolves auth if a
228
299
  token is present (→ `owner`) and treats its absence as anonymous (→ `public`); a
229
300
  `collection`-authority function runs elevated regardless.
230
301
 
302
+ ### Before it will resolve
303
+
304
+ An http call returns `404 FUNCTION_NOT_FOUND` unless **both** of these are true (this is the
305
+ most common first-run surprise):
306
+
307
+ 1. **The app's release is registered on the channel** the collection follows (see
308
+ [deploying-apps.md](deploying-apps.md)) — that's what publishes the function bundle into
309
+ the registry the runtime loads from.
310
+ 2. **The app is enabled on the collection** (`appConfig.apps[]`, on that same channel — see
311
+ [appConfig.md](appConfig.md)).
312
+
313
+ So the end-to-end path is: *write → register the release → enable on a collection → call.*
314
+
315
+ ### Seeing your function's runs and logs
316
+
317
+ Every invocation is recorded as an **execution** activity event, carrying your `ctx.log`
318
+ lines. Where to look, easiest first:
319
+
320
+ - **Admin-surface response** — `logs` + `durationMs` come straight back in the JSON.
321
+ - **Deployed test mode** — see below; real run, isolated logging.
322
+ - **Owner console** — the collection's **Advanced → Errors & Activity → events** tab,
323
+ filtered to **source = execution**: each run shows as `function <name> ok` (or an error),
324
+ and the detail carries your `ctx.log` output. (Telemetry is streamed, so allow a few
325
+ seconds.)
326
+
231
327
  ## Testing & preview
232
328
 
233
329
  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.1",
3
+ "version": "2.0.3",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,9 +14,13 @@
14
14
  "default": "./dist/testing/index.js"
15
15
  }
16
16
  },
17
+ "bin": {
18
+ "smartlinks-register-release": "scripts/register-release.mjs"
19
+ },
17
20
  "files": [
18
21
  "dist/",
19
22
  "docs/",
23
+ "scripts/register-release.mjs",
20
24
  "openapi.yaml",
21
25
  "README.md"
22
26
  ],
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * register-release — the post-build "register on publish" step for a SmartLinks app.
4
+ *
5
+ * Runs at the END of a build (after the bundles are built + hashed): reads the built
6
+ * app.manifest.json and POSTs it to the SmartLinks install endpoint so the release is
7
+ * validated + recorded in the registry (push model). Exits non-zero on failure so a bad
8
+ * manifest / function fails the publish.
9
+ *
10
+ * ONE script for both paths:
11
+ * - dev from Lovable → SMARTLINKS_CHANNEL=dev, dev deploy key, bundleBaseUrl = your Lovable URL
12
+ * - prod/beta via CI → SMARTLINKS_CHANNEL=stable|beta, prod key, bundleBaseUrl = the CDN base
13
+ *
14
+ * IDENTITY (important): the SmartLinks app id is assigned by the PLATFORM, not self-declared in
15
+ * the manifest. Pass it as SMARTLINKS_APP_ID. The manifest's meta.appId is ignored by the
16
+ * backend (the installed id is authoritative); if they differ the backend just warns. If
17
+ * SMARTLINKS_APP_ID is unset we fall back to manifest.meta.appId with a warning — set it.
18
+ *
19
+ * Env:
20
+ * SMARTLINKS_APP_ID the app's platform id (REQUIRED for correctness; per-project)
21
+ * SMARTLINKS_DEPLOY_KEY channel-scoped deploy key (dev key is safe to keep as a Lovable
22
+ * workspace Build Secret — its scope is the dev channel only)
23
+ * SMARTLINKS_CHANNEL dev | alpha | beta | stable (also accepts legacy 'prod' → stable);
24
+ * UNSET ⇒ skip quietly (preview/live-edit builds never register)
25
+ * SMARTLINKS_API API host (default https://smartlinks.app); a VPC env uses its own
26
+ * SMARTLINKS_BUNDLE_BASE_URL where the built files are served (dev: your Lovable URL;
27
+ * prod: https://smartlinks.app/apps/<appId>/<version>)
28
+ * SMARTLINKS_MANIFEST manifest path (default dist/app.manifest.json)
29
+ */
30
+
31
+ import { readFileSync } from 'node:fs'
32
+ import { execSync } from 'node:child_process'
33
+
34
+ const API = process.env.SMARTLINKS_API || 'https://smartlinks.app'
35
+ const KEY = process.env.SMARTLINKS_DEPLOY_KEY
36
+ const CHANNEL = process.env.SMARTLINKS_CHANNEL
37
+ const MANIFEST = process.env.SMARTLINKS_MANIFEST || 'dist/app.manifest.json'
38
+
39
+ // --- Gate: only register when this build is meant to ship a release ---
40
+ if (!CHANNEL) {
41
+ console.log('ℹ︎ SmartLinks: SMARTLINKS_CHANNEL unset — skipping release registration (preview build).')
42
+ process.exit(0)
43
+ }
44
+ if (!KEY) {
45
+ // A prod/beta build with no key is a real misconfiguration; dev skips quietly.
46
+ if (CHANNEL !== 'dev') { console.error(`❌ SmartLinks: "${CHANNEL}" build but SMARTLINKS_DEPLOY_KEY is missing`); process.exit(1) }
47
+ console.log(`ℹ︎ SmartLinks: no deploy key for "${CHANNEL}" — skipping registration.`)
48
+ process.exit(0)
49
+ }
50
+
51
+ let manifest
52
+ try {
53
+ manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
54
+ } catch (e) {
55
+ console.error(`❌ SmartLinks: could not read manifest at ${MANIFEST} — ${e.message}`)
56
+ process.exit(1)
57
+ }
58
+
59
+ // The platform id is authoritative. Fall back to the manifest's declared id only if it wasn't
60
+ // injected — and say so, because that self-declared id is exactly what drifts.
61
+ let appId = process.env.SMARTLINKS_APP_ID
62
+ if (!appId) {
63
+ appId = manifest?.meta?.appId
64
+ console.warn(`⚠︎ SmartLinks: SMARTLINKS_APP_ID not set — falling back to manifest.meta.appId "${appId}". Set SMARTLINKS_APP_ID to the platform id to be safe.`)
65
+ }
66
+ if (!appId) { console.error('❌ SmartLinks: no app id (set SMARTLINKS_APP_ID)'); process.exit(1) }
67
+
68
+ const version = String(manifest?.meta?.version || '')
69
+ if (!version) { console.error('❌ SmartLinks: manifest.meta.version is required'); process.exit(1) }
70
+
71
+ const gitHash = (() => { try { return execSync('git rev-parse --short HEAD').toString().trim() } catch { return null } })()
72
+ const bundleBaseUrl = process.env.SMARTLINKS_BUNDLE_BASE_URL || `${API}/apps/${appId}/${version}`
73
+
74
+ const res = await fetch(`${API}/api/v1/apps/${appId}/releases`, {
75
+ method: 'POST',
76
+ headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
77
+ body: JSON.stringify({
78
+ channel: CHANNEL, version, manifest, bundleBaseUrl,
79
+ build: { at: new Date().toISOString(), gitHash, builder: CHANNEL === 'dev' ? 'lovable' : 'ci' },
80
+ }),
81
+ }).catch((e) => { console.error(`❌ SmartLinks: request failed — ${e.message}`); process.exit(1) })
82
+
83
+ const body = await res.json().catch(() => ({}))
84
+ if (!res.ok || !body.ok) {
85
+ console.error(`❌ SmartLinks registration failed (${res.status}):`)
86
+ for (const e of body.errors || []) console.error(` • ${e.path}: ${e.message}`)
87
+ process.exit(1) // fail the build
88
+ }
89
+ // Non-blocking warnings (e.g. the manifest declared a different id, which the platform ignores).
90
+ for (const w of body.warnings || []) console.warn(` ⚠︎ ${w.path}: ${w.message}`)
91
+ console.log(`✅ Registered ${appId}@${version} on "${body.channel || CHANNEL}" — functions: ${(body.functions || []).join(', ') || 'none'}`)