@alvera-ai/platform-sdk 0.19.2 → 0.20.0-next.gcd2f231
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/.agent/AGENTS.md +10 -8
- package/.agent/account_management.md +14 -13
- package/.agent/action_status_updaters.md +5 -4
- package/.agent/ai_sandbox.md +3 -3
- package/.agent/authenticated_links.md +247 -0
- package/.agent/cookbook/organic-marketing.md +220 -126
- package/.agent/cookbook/payments-compliance.md +108 -63
- package/.agent/cookbook/primary-care.md +91 -52
- package/.agent/cookbook/subscription-saas.md +130 -99
- package/.agent/data_activation_clients.md +107 -4
- package/.agent/datalakes.md +1 -1
- package/.agent/interoperability_contracts.md +1 -1
- package/.agent/mdm.md +4 -4
- package/.agent/messages.md +9 -3
- package/.agent/mock-services.md +4 -5
- package/.agent/mutations.md +4 -4
- package/.agent/tool-call-configs.md +2 -2
- package/.agent/tools.md +10 -3
- package/.agent/type_naming.md +3 -4
- package/.agent/workflows.md +45 -41
- package/README.md +1 -1
- package/dist/index.d.mts +394 -606
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +124 -327
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/.agent/connected_apps.md +0 -525
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alvera-ai/platform-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0-next.gcd2f231",
|
|
4
4
|
"description": "Typed SDK for the Alvera platform API — manage data sources, tools, generic tables, AI agents, and action status updaters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
package/.agent/connected_apps.md
DELETED
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
# Connected apps
|
|
2
|
-
|
|
3
|
-
A **connected app** is an external web application registered with
|
|
4
|
-
the platform — typically a customer-facing forms portal hosted on
|
|
5
|
-
Cloudflare Workers / Pages or any other deployment target. The
|
|
6
|
-
platform mints magic links into the app from workflow actions:
|
|
7
|
-
each action renders a URL of the form
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
https://<app-primary-url>/t/<short_path>
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
The customer clicks the link, lands on a route the app discovered
|
|
14
|
-
via `/.well-known/routes.json`, and the app calls back to the
|
|
15
|
-
platform to resolve the page (fetch the underlying message + its
|
|
16
|
-
metadata) and update tracking (opened_at, form_submitted_at).
|
|
17
|
-
|
|
18
|
-
SDK namespace: `api.connectedApps`.
|
|
19
|
-
|
|
20
|
-
The namespace splits into **two roles**:
|
|
21
|
-
|
|
22
|
-
- **Datalake-scoped management** — the admin side. CRUD on the
|
|
23
|
-
connected-app row, route discovery + sync. Uses the app's
|
|
24
|
-
UUID `id` as the path key.
|
|
25
|
-
- **Tenant-scoped runtime** — the consumer side. Called BY the
|
|
26
|
-
connected app itself (authenticated with its auto-provisioned
|
|
27
|
-
machine-to-machine API key) when a customer hits a magic link. Uses the app's
|
|
28
|
-
`slug` as the path key.
|
|
29
|
-
|
|
30
|
-
**Connected apps are single-tenant, full stop.** A connected app
|
|
31
|
-
belongs to exactly ONE tenant (and one datalake under it): its
|
|
32
|
-
auto-provisioned M2M key is tenant-scoped, its magic links resolve
|
|
33
|
-
against that tenant's data, and its publishable browser key carries
|
|
34
|
-
that tenant's origin allowlist. Do NOT design a connected app that
|
|
35
|
-
serves multiple tenants from one deployment — per tenant, register
|
|
36
|
-
a separate connected app (separate keys, separate routes). There is
|
|
37
|
-
no cross-tenant resolution path, by construction.
|
|
38
|
-
|
|
39
|
-
Connected apps are **Datalake-DB-resident** — the parent datalake
|
|
40
|
-
must be `status: 'ready'` before you `create()`.
|
|
41
|
-
|
|
42
|
-
## 1. Wire shape
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import type {
|
|
46
|
-
ConnectedAppResponse,
|
|
47
|
-
} from '@alvera-ai/platform-sdk'
|
|
48
|
-
|
|
49
|
-
const { data: created } = await api.connectedApps.create(
|
|
50
|
-
tenantSlug,
|
|
51
|
-
datalakeSlug,
|
|
52
|
-
{
|
|
53
|
-
name: 'The Doctors Center',
|
|
54
|
-
description: 'Patient-facing forms portal',
|
|
55
|
-
mode: 'self_hosted', // 'self_hosted' (only mode active today; see §2)
|
|
56
|
-
urls: [
|
|
57
|
-
{
|
|
58
|
-
url: 'https://the-doctors-center.pages.dev', // app's deployed origin
|
|
59
|
-
is_primary: true, // exactly one URL must be primary
|
|
60
|
-
label: 'Production',
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
)
|
|
65
|
-
// created.id, created.slug — server-derived
|
|
66
|
-
// created.status === 'pending' (transient) → 'synced' after route fetch
|
|
67
|
-
// (an M2M key IS auto-provisioned at create (see §2), but its id is NOT
|
|
68
|
-
// echoed on the resource — no response carries an api_key_id field)
|
|
69
|
-
// created.routes — array populated from .well-known/routes.json
|
|
70
|
-
// (empty until first sync)
|
|
71
|
-
// created.last_synced_at — set when routes successfully validated
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The platform validates `/.well-known/routes.json` at the primary
|
|
75
|
-
URL during create. If validation succeeds, routes are saved and
|
|
76
|
-
the status flips to `'synced'`. If it fails, the row persists
|
|
77
|
-
with `status: 'error'` and the failure message in `error`.
|
|
78
|
-
|
|
79
|
-
## 2. Rules the type cannot encode
|
|
80
|
-
|
|
81
|
-
### URLs: at least one, exactly one primary
|
|
82
|
-
|
|
83
|
-
The `urls` array requires at least one entry, and **exactly one
|
|
84
|
-
entry must have `is_primary: true`**. The primary URL is the host
|
|
85
|
-
the platform fetches `/.well-known/routes.json` from and the
|
|
86
|
-
host that magic links are rooted at. Submitting zero primaries
|
|
87
|
-
or multiple primaries returns a 422 on `/urls`.
|
|
88
|
-
|
|
89
|
-
Additional URLs (staging, preview) are stored for reference but
|
|
90
|
-
aren't used for route discovery or link minting.
|
|
91
|
-
|
|
92
|
-
**`urls` models environments, NOT tracking domains.** The list is
|
|
93
|
-
production / staging / preview deployments of the *same* app — and the
|
|
94
|
-
one flagged `is_primary` silently becomes the domain of **every** magic
|
|
95
|
-
link this app mints (see §6). It is NOT a way to serve links from
|
|
96
|
-
several tracking domains: to mint links on two different domains you
|
|
97
|
-
need **two connected apps**, one per domain. An agent that sees
|
|
98
|
-
`urls: [...]` and reaches for "add another tracking domain here" gets
|
|
99
|
-
this wrong — reach for a second connected app instead.
|
|
100
|
-
|
|
101
|
-
### Route discovery is automatic + re-triggerable
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
/.well-known/routes.json served by the connected app at its primary URL.
|
|
105
|
-
Must return application/json with a JSON array
|
|
106
|
-
of route objects:
|
|
107
|
-
|
|
108
|
-
[
|
|
109
|
-
{ "name": "Review Form", "path": "/forms/review", "description": "..." },
|
|
110
|
-
{ "name": "CAHPS Survey", "path": "/forms/cahps", "description": "..." }
|
|
111
|
-
]
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Validation rules at fetch time:
|
|
115
|
-
|
|
116
|
-
- Response content-type must be `application/json`
|
|
117
|
-
- Body must be a JSON array
|
|
118
|
-
- Each route requires `name` (string) and `path` (string);
|
|
119
|
-
`description` is optional
|
|
120
|
-
|
|
121
|
-
The platform fetches the manifest at two moments:
|
|
122
|
-
|
|
123
|
-
1. **At create time** — automatic. Failure flips the row to
|
|
124
|
-
`status: 'error'` (the row still persists; routes stay empty).
|
|
125
|
-
2. **On explicit `.syncRoutes(id)`** — operator-triggered re-fetch.
|
|
126
|
-
Updates the saved routes and `last_synced_at`.
|
|
127
|
-
|
|
128
|
-
The SDK does NOT auto-re-sync routes on a schedule. Re-sync is
|
|
129
|
-
deliberate.
|
|
130
|
-
|
|
131
|
-
### Modes: only `self_hosted` is active today
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
mode: 'self_hosted' active — operator runs the deployment
|
|
135
|
-
themselves (e.g. `wrangler deploy`); the
|
|
136
|
-
platform registers the resulting URL and
|
|
137
|
-
discovers routes from it.
|
|
138
|
-
mode: 'managed' reserved for a future Cloudflare-Pages
|
|
139
|
-
auto-deploy flow. Not provisioned at
|
|
140
|
-
runtime today — submitting it succeeds
|
|
141
|
-
structurally but no deploy fires.
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Treat `'self_hosted'` as the only practical choice for current
|
|
145
|
-
consumers.
|
|
146
|
-
|
|
147
|
-
### machine-to-machine API key is auto-provisioned
|
|
148
|
-
|
|
149
|
-
Creating a connected app automatically provisions a machine-to-
|
|
150
|
-
machine API key named `ConnectedApp: <app name>` with the `api`
|
|
151
|
-
role. The key is exposed on the response once (typically rendered
|
|
152
|
-
in the create-flow UI) so the operator can configure it as a
|
|
153
|
-
secret in the deployed app's environment. The platform stores only
|
|
154
|
-
the key's id (`api_key_id` on the response).
|
|
155
|
-
|
|
156
|
-
The connected app deployment should be configured with:
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
ALVERA_API_KEY="<auto-provisioned key>"
|
|
160
|
-
ALVERA_DATALAKE_ID="<datalake-uuid>"
|
|
161
|
-
ALVERA_API_URL="https://<platform-host>"
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Deleting the connected app deletes the API key.
|
|
165
|
-
|
|
166
|
-
### `slug` is the runtime reference; `id` is the admin reference
|
|
167
|
-
|
|
168
|
-
Management endpoints (`list`, `get`, `update`, `syncRoutes`,
|
|
169
|
-
`metadataDetails`) take the UUID `id` as the path key — the
|
|
170
|
-
datalake-scoped admin context.
|
|
171
|
-
|
|
172
|
-
Runtime endpoints (`resolvePage`, `updateMessageTracking`) take
|
|
173
|
-
the `slug` and skip the datalake path segment entirely. This is
|
|
174
|
-
deliberate: the connected app deployment is authenticated with
|
|
175
|
-
the tenant-scoped machine-to-machine API key and may not know which datalake
|
|
176
|
-
its row lives under.
|
|
177
|
-
|
|
178
|
-
## 3. Field ownership
|
|
179
|
-
|
|
180
|
-
**Server-derived (Response-only).** Universal set from
|
|
181
|
-
`type_naming.md`, plus:
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
status enum — 'pending' | 'deploying' | 'deployed' | 'synced' | 'error'
|
|
185
|
-
— for the dominant `'self_hosted'` mode the status
|
|
186
|
-
flows 'pending' → 'synced' (or 'error' on a
|
|
187
|
-
failed route fetch). 'deploying' / 'deployed'
|
|
188
|
-
are reserved for the future `'managed'`
|
|
189
|
-
Cloudflare-Pages auto-deploy flow (see §2)
|
|
190
|
-
and won't appear on self-hosted rows today.
|
|
191
|
-
last_synced_at string — ISO 8601; set at successful route sync
|
|
192
|
-
error string — populated when status === 'error'
|
|
193
|
-
routes array — discovered from .well-known/routes.json
|
|
194
|
-
(each: { name, path, description? })
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
(The auto-provisioned M2M key's id is **not** among these — neither
|
|
198
|
-
`.get` nor `get-metadata` returns an `api_key_id`. Don't assert on it
|
|
199
|
-
in a contract hook; the key itself reaches the deployment as
|
|
200
|
-
`ALVERA_API_KEY`, §2.)
|
|
201
|
-
|
|
202
|
-
**Caller-supplied (round-trip).**
|
|
203
|
-
|
|
204
|
-
```
|
|
205
|
-
name required string
|
|
206
|
-
description optional string
|
|
207
|
-
mode required enum — 'self_hosted' (only active)
|
|
208
|
-
urls required array — see §2; exactly one primary
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
**Write-only (Request-only).** None. The auto-provisioned API key's
|
|
212
|
-
plaintext value is returned ONCE on the create response (not on
|
|
213
|
-
subsequent reads), but it isn't a "write-only" field in the
|
|
214
|
-
caller-supplied sense.
|
|
215
|
-
|
|
216
|
-
## 4. Error envelopes
|
|
217
|
-
|
|
218
|
-
Standard JSON:API envelopes per `errors.md`. Common rejections:
|
|
219
|
-
|
|
220
|
-
| `source.pointer` | Cause |
|
|
221
|
-
|-------------------------------|--------------------------------------------------------|
|
|
222
|
-
| `/urls` | empty, missing primary, or multiple primaries |
|
|
223
|
-
| `/urls/0/url` | not a valid URL |
|
|
224
|
-
| `/mode` | value not in enum |
|
|
225
|
-
| `/name` | uniqueness within datalake — server error literal: "name must be unique within a datalake" |
|
|
226
|
-
|
|
227
|
-
Route-fetch failures (primary URL unreachable, manifest malformed)
|
|
228
|
-
do NOT reject the create — the row persists with `status: 'error'`
|
|
229
|
-
and `error` populated. Use `.syncRoutes(id)` to retry after fixing
|
|
230
|
-
the deployment.
|
|
231
|
-
|
|
232
|
-
## 5. Lifecycle
|
|
233
|
-
|
|
234
|
-
### Create
|
|
235
|
-
|
|
236
|
-
Synchronous. The platform attempts a single fetch of
|
|
237
|
-
`/.well-known/routes.json` at the primary URL during create.
|
|
238
|
-
Success → `status: 'synced'` and `routes` populated. Failure →
|
|
239
|
-
`status: 'error'` and `error` populated; the row still exists
|
|
240
|
-
and can be retried via `.syncRoutes`.
|
|
241
|
-
|
|
242
|
-
### Read shapes — management
|
|
243
|
-
|
|
244
|
-
```
|
|
245
|
-
.list(tenantSlug, datalakeSlug, query?) Paged: { data, meta }
|
|
246
|
-
(full Flop query — type_naming.md)
|
|
247
|
-
.get(tenantSlug, datalakeSlug, id) One row, by UUID
|
|
248
|
-
.metadata(tenantSlug, datalakeSlug) Markdown catalog
|
|
249
|
-
.metadataDetails(tenantSlug, datalakeSlug, Markdown for one app
|
|
250
|
-
id)
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Sync routes
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
const { data: ack } = await api.connectedApps.syncRoutes(
|
|
257
|
-
tenantSlug, datalakeSlug, appId,
|
|
258
|
-
)
|
|
259
|
-
// 202 Accepted — the sync runs in a background worker:
|
|
260
|
-
// ack.status — 'enqueued'
|
|
261
|
-
// ack.job_id — background job id
|
|
262
|
-
// ack.connected_app_id — echo of the app id
|
|
263
|
-
// ack.enqueued_at — enqueue timestamp
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
This is the explicit "re-fetch and save" verb — asynchronous. The
|
|
267
|
-
202 acknowledgement proves the app id resolved and a sync job was
|
|
268
|
-
accepted; the platform then refreshes `routes` + `last_synced_at` and
|
|
269
|
-
lands `status: 'synced'` (or `'error'`) on the app row asynchronously.
|
|
270
|
-
Poll `.get()` for the outcome. Use it after deploying a new manifest,
|
|
271
|
-
fixing a primary URL outage, or recovering from `status: 'error'`.
|
|
272
|
-
|
|
273
|
-
**Route-sync is the only "does this domain work" check the platform
|
|
274
|
-
performs — there is NO DNS verification.** The platform never resolves
|
|
275
|
-
or validates the primary URL's DNS/ownership; it just attempts the
|
|
276
|
-
HTTP fetch of `/.well-known/routes.json`. A `status: 'synced'` means
|
|
277
|
-
"the manifest was fetched and parsed", not "this domain is verified as
|
|
278
|
-
yours". Domain trust is your responsibility, not a platform gate.
|
|
279
|
-
|
|
280
|
-
Discovery failure modes the platform handles explicitly — each
|
|
281
|
-
leaves the row at `status: 'error'` with `error` populated:
|
|
282
|
-
|
|
283
|
-
```
|
|
284
|
-
HTTP 404 from primary URL — manifest not found
|
|
285
|
-
HTTP 5xx from primary URL — server error at the app
|
|
286
|
-
request timeout — primary URL slow / hung
|
|
287
|
-
TCP connection refused — deployment not running
|
|
288
|
-
response content-type != application/json — wrong endpoint or proxy
|
|
289
|
-
response body is not a JSON array — manifest schema wrong
|
|
290
|
-
route entry missing required `name` — per-route schema gap
|
|
291
|
-
route entry missing required `path` — per-route schema gap
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Per-route schema failures stop the sync — the platform validates
|
|
295
|
-
every entry in the array and refuses partial saves. Empty
|
|
296
|
-
manifests (`[]`) succeed with zero routes registered.
|
|
297
|
-
|
|
298
|
-
The primary URL has a trailing slash stripped before the
|
|
299
|
-
`/.well-known/routes.json` join, so both `https://app.example.com`
|
|
300
|
-
and `https://app.example.com/` produce the same fetch URL.
|
|
301
|
-
|
|
302
|
-
### Update
|
|
303
|
-
|
|
304
|
-
`update()` replaces the whole resource — resupply the full
|
|
305
|
-
body; there's no partial update. Mutating the primary URL does
|
|
306
|
-
NOT auto-trigger a route sync — call `.syncRoutes` after the
|
|
307
|
-
update if the new primary URL serves a different manifest.
|
|
308
|
-
|
|
309
|
-
### Delete
|
|
310
|
-
|
|
311
|
-
`delete()` removes the connected app AND the auto-provisioned API
|
|
312
|
-
key. Workflows that still reference the app's id in action
|
|
313
|
-
`connected_app_id` fields fail at run time with the action's
|
|
314
|
-
template-render step erroring on the missing relation. Detach
|
|
315
|
-
workflow actions first.
|
|
316
|
-
|
|
317
|
-
Delete latency depends on the app's `mode`:
|
|
318
|
-
|
|
319
|
-
- `mode: 'self_hosted'` — synchronous. The row is gone when
|
|
320
|
-
the `delete()` call returns.
|
|
321
|
-
- `mode: 'managed'` — async. The `delete()` response returns
|
|
322
|
-
immediately, but the actual teardown (Cloudflare Pages
|
|
323
|
-
project deletion + DNS unregistration) runs in the background.
|
|
324
|
-
The row's `status` transitions through
|
|
325
|
-
`'deploying'` (during teardown) before disappearing. Poll
|
|
326
|
-
`.get()` until 404 to confirm removal.
|
|
327
|
-
|
|
328
|
-
## 6. How a tracked link is minted
|
|
329
|
-
|
|
330
|
-
Before the runtime resolve flow, understand what the platform does at
|
|
331
|
-
fire time to produce the link. When a workflow action that carries a
|
|
332
|
-
`connected_app_id` executes, the platform mints a **page token** and
|
|
333
|
-
injects its URL into the message body:
|
|
334
|
-
|
|
335
|
-
```
|
|
336
|
-
action fires
|
|
337
|
-
│ action.connected_app_id + connected_app_route
|
|
338
|
-
│ + rendered connected_app_metadata_template (Liquid → JSON)
|
|
339
|
-
▼
|
|
340
|
-
page token row ── keyed on the MDM SUBJECT id + the route,
|
|
341
|
-
│ carrying the rendered metadata JSON INSIDE it
|
|
342
|
-
▼
|
|
343
|
-
{{ connected_app_form_url }} = https://<primary-url>/t/<url_hash>
|
|
344
|
-
│ (primary-url = the connected app's is_primary URL)
|
|
345
|
-
▼
|
|
346
|
-
message row created with the link in its body
|
|
347
|
-
│
|
|
348
|
-
▼
|
|
349
|
-
page token backfilled with that message's id ← wires token → message
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
Key consequences to design around:
|
|
353
|
-
|
|
354
|
-
- **The `connected_app_metadata_template` is how per-recipient data
|
|
355
|
-
reaches the app without hard-coding a URL.** It's a Liquid template
|
|
356
|
-
that renders to a JSON object, stored *inside* the page token; the
|
|
357
|
-
app reads it back at `resolvePage` time (as `page.metadata`) and uses
|
|
358
|
-
it to prefill / redirect that specific recipient. This is the
|
|
359
|
-
mechanism for "send each recipient to their own destination" — put
|
|
360
|
-
the per-recipient values in the metadata template, not in the URL.
|
|
361
|
-
|
|
362
|
-
- **No MDM subject ⇒ no page token ⇒ no link.** The page token
|
|
363
|
-
*requires* a subject id (it is keyed on the resolved MDM subject). If
|
|
364
|
-
the row didn't resolve to a subject, the mint fails — but **the send
|
|
365
|
-
still goes out**: the mint failure is a logged warning, not an action
|
|
366
|
-
failure, so `{{ connected_app_form_url }}` simply renders **empty**
|
|
367
|
-
and the recipient gets a message with a missing link. A
|
|
368
|
-
message-with-no-link is the symptom of a workflow whose
|
|
369
|
-
`skip_mdm_resolution` was left `true`, or whose row carries no entity
|
|
370
|
-
FK (see `workflows.md` §3). If your action mints links, the workflow
|
|
371
|
-
MUST resolve a subject.
|
|
372
|
-
|
|
373
|
-
- **Tracked links expire — 7 days by default.** The page token is
|
|
374
|
-
stamped with a TTL (604,800 s) at mint; `resolvePage` checks
|
|
375
|
-
`expires_at` and an expired token fails to resolve (HTTP 410).
|
|
376
|
-
"How long does a campaign link stay live?" — a week, unless the
|
|
377
|
-
platform default changes. Long-lead campaigns must account for it.
|
|
378
|
-
|
|
379
|
-
The `{{ connected_app_form_url }}` variable is available only in action
|
|
380
|
-
bodies whose action has a `connected_app_id`; it renders empty
|
|
381
|
-
otherwise.
|
|
382
|
-
|
|
383
|
-
## 7. Runtime: how magic links resolve
|
|
384
|
-
|
|
385
|
-
This section covers the runtime callers — the connected app
|
|
386
|
-
deployment itself, not the admin.
|
|
387
|
-
|
|
388
|
-
At workflow run time, an action with `connected_app_id` +
|
|
389
|
-
`connected_app_route` renders a message body that contains a
|
|
390
|
-
URL of the form:
|
|
391
|
-
|
|
392
|
-
```
|
|
393
|
-
https://<primary-url>/t/<short_path>
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
The customer clicks the link, lands on the app, and the app
|
|
397
|
-
resolves the page:
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
const { data: page } = await api.connectedApps.resolvePage(
|
|
401
|
-
tenantSlug, datalakeSlug, slug, // tenant + datalake + app slug
|
|
402
|
-
{
|
|
403
|
-
short_path: '<the-short-path-from-the-URL>',
|
|
404
|
-
user_agent: req.headers.get('user-agent') ?? '',
|
|
405
|
-
},
|
|
406
|
-
)
|
|
407
|
-
// page.message — the rendered message row
|
|
408
|
-
// page.message.body — the rendered body, real values (raw lake)
|
|
409
|
-
// page.route_path — the route the link was minted against
|
|
410
|
-
// (e.g. '/forms/review')
|
|
411
|
-
// page.metadata — parsed `connected_app_metadata_template`
|
|
412
|
-
// output from the workflow action; arbitrary JSON
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
The app routes to the page named by `route_path`, renders the
|
|
416
|
-
form using `metadata` for prefill, and updates tracking when
|
|
417
|
-
the customer opens or submits the form:
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
await api.connectedApps.updateMessageTracking(
|
|
421
|
-
tenantSlug, datalakeSlug, slug,
|
|
422
|
-
{
|
|
423
|
-
short_path: '<same-short-path>',
|
|
424
|
-
opened_at: new Date().toISOString(),
|
|
425
|
-
form_submitted_at: new Date().toISOString(), // only when form completes
|
|
426
|
-
},
|
|
427
|
-
)
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
Both calls are authenticated with the connected app's
|
|
431
|
-
auto-provisioned machine-to-machine API key (typically configured as
|
|
432
|
-
`ALVERA_API_KEY` in the deployment environment). Both are also on
|
|
433
|
-
the `public_api` (publishable-key) allowlist — alongside `ingest`,
|
|
434
|
-
`ingest-file`, and `mdm/verify` — so a browser-side app holding
|
|
435
|
-
only a publishable key can call them.
|
|
436
|
-
|
|
437
|
-
**This is the engagement seam.** `updateMessageTracking` writes
|
|
438
|
-
`opened_at` / `form_submitted_at` onto the message row — the
|
|
439
|
-
open/click/conversion half of campaign analytics — and it NEVER
|
|
440
|
-
changes `status` (delivery truth belongs to the action status
|
|
441
|
-
updater; see `messages.md` §1 for who writes which field).
|
|
442
|
-
|
|
443
|
-
### Workflow-side wiring
|
|
444
|
-
|
|
445
|
-
A workflow action that emits a magic link must set:
|
|
446
|
-
|
|
447
|
-
```
|
|
448
|
-
connected_app_id UUID — the registered app
|
|
449
|
-
connected_app_route string — must match a discovered route's path
|
|
450
|
-
(e.g. '/forms/review')
|
|
451
|
-
connected_app_metadata_template string — Liquid template rendering a JSON
|
|
452
|
-
object the resolved page returns
|
|
453
|
-
to the app (arbitrary shape; the
|
|
454
|
-
app decides how to use it for
|
|
455
|
-
prefill, headers, etc.)
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
See `workflows.md` §7 for the action-level field detail.
|
|
459
|
-
|
|
460
|
-
## 8. Gotchas
|
|
461
|
-
|
|
462
|
-
1. **Exactly one URL must be `is_primary: true`.** Zero or
|
|
463
|
-
multiple primaries returns 422. The primary is what the
|
|
464
|
-
platform fetches routes from AND what magic links are minted
|
|
465
|
-
against; additional URLs are reference-only.
|
|
466
|
-
|
|
467
|
-
2. **`status: 'error'` after create is recoverable.** A failed
|
|
468
|
-
route fetch (manifest 404, malformed JSON, primary URL DNS
|
|
469
|
-
failure) doesn't reject the row. Fix the deployment and call
|
|
470
|
-
`.syncRoutes(id)` to retry.
|
|
471
|
-
|
|
472
|
-
3. **Mode `'managed'` is reserved.** It's structurally valid in
|
|
473
|
-
the request type but no deploy fires today. Use `'self_hosted'`
|
|
474
|
-
and run your own deployment.
|
|
475
|
-
|
|
476
|
-
4. **`.syncRoutes` is NOT automatic.** The platform only fetches
|
|
477
|
-
the manifest at create time and on explicit re-sync. Operators
|
|
478
|
-
who change their `/.well-known/routes.json` between deploys
|
|
479
|
-
must trigger a sync.
|
|
480
|
-
|
|
481
|
-
5. **Runtime calls use `slug`, not `id`.** `.resolvePage` and
|
|
482
|
-
`.updateMessageTracking` skip the datalake path segment and
|
|
483
|
-
key on the app's slug + tenant. The auto-provisioned M2M key
|
|
484
|
-
is tenant-scoped, so the connected-app deployment authenticates
|
|
485
|
-
without knowing the datalake.
|
|
486
|
-
|
|
487
|
-
6. **`connected_app_route` on a workflow action must match a
|
|
488
|
-
discovered route.** Action templates reference
|
|
489
|
-
`{{ connected_app_form_url }}` which the platform constructs
|
|
490
|
-
from the bound app's primary URL + the route path. A route
|
|
491
|
-
that hasn't been synced into the app's `routes` array still
|
|
492
|
-
accepts the action at create time but the magic-link
|
|
493
|
-
resolution fails when a customer clicks the link.
|
|
494
|
-
|
|
495
|
-
7. **The auto-provisioned API key is revealed ONCE.** It appears
|
|
496
|
-
on the create response and is never returned again. The
|
|
497
|
-
create-flow caller is responsible for capturing the plaintext
|
|
498
|
-
value and configuring it into the connected-app deployment's
|
|
499
|
-
environment. The platform stores only the key id on the row.
|
|
500
|
-
|
|
501
|
-
8. **Route manifests must be JSON arrays, not objects.** A
|
|
502
|
-
common mistake is wrapping the routes in `{ "routes": [...] }`
|
|
503
|
-
— the manifest's top-level must be the array itself.
|
|
504
|
-
|
|
505
|
-
9. **No MDM subject ⇒ no link (but the message still sends).** Minting
|
|
506
|
-
a page token needs the resolved MDM subject; if the workflow row
|
|
507
|
-
didn't resolve one, the mint is skipped with a logged warning and
|
|
508
|
-
`{{ connected_app_form_url }}` renders empty — the recipient gets a
|
|
509
|
-
linkless message, not an error. If an action mints links, the
|
|
510
|
-
workflow must resolve a subject (`skip_mdm_resolution: false` + an
|
|
511
|
-
entity FK on the row; see `workflows.md` §3). See §6.
|
|
512
|
-
|
|
513
|
-
10. **Tracked links expire (7-day default TTL).** A page token is valid
|
|
514
|
-
for 604,800 s from mint; `resolvePage` returns 410 for an expired
|
|
515
|
-
token. Account for it on long-lead campaigns.
|
|
516
|
-
|
|
517
|
-
11. **`urls` are environments, not tracking domains.** The `is_primary`
|
|
518
|
-
URL becomes the domain of every minted link; the others are
|
|
519
|
-
reference-only. Multiple tracking domains = multiple connected apps
|
|
520
|
-
(see §2).
|
|
521
|
-
|
|
522
|
-
12. **No DNS verification exists.** `status: 'synced'` means the
|
|
523
|
-
manifest was fetched and parsed, not that the domain is verified as
|
|
524
|
-
yours. Route-sync is the only reachability check the platform runs
|
|
525
|
-
(see §5).
|