@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alvera-ai/platform-sdk",
3
- "version": "0.19.2",
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,
@@ -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).