@alvera-ai/platform-sdk 0.12.1-next.f7f8a3c → 0.12.1-next.g1cfe79a
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 +45 -1
- package/.agent/account_management.md +33 -8
- package/.agent/action_logs.md +139 -0
- package/.agent/action_status_updaters.md +36 -3
- package/.agent/ai_agents.md +2 -1
- package/.agent/connected_apps.md +21 -2
- package/.agent/cookbook/_setup/accounts_receivable.md +24 -4
- package/.agent/cookbook/_setup/foundation.md +24 -4
- package/.agent/cookbook/_setup/healthcare.md +24 -4
- package/.agent/cookbook/_setup/payment_risk.md +24 -4
- package/.agent/cookbook/invite-team.md +13 -7
- package/.agent/data_activation_clients.md +31 -2
- package/.agent/data_sources.md +4 -3
- package/.agent/datalakes.md +2 -2
- package/.agent/debugging.md +1 -0
- package/.agent/generic_tables.md +2 -1
- package/.agent/interoperability_contracts.md +2 -1
- package/.agent/mdm.md +10 -0
- package/.agent/messages.md +172 -0
- package/.agent/tools.md +6 -4
- package/.agent/type_naming.md +26 -1
- package/.agent/workflows.md +137 -23
- package/LICENSE +21 -93
- package/README.md +1 -1
- package/dist/index.d.mts +340 -33
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +558 -94
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/.agent/AGENTS.md
CHANGED
|
@@ -114,19 +114,50 @@ platform. Two-step auth + client construction:
|
|
|
114
114
|
email,
|
|
115
115
|
password,
|
|
116
116
|
tenantSlug, // optional; omit for tenantless session
|
|
117
|
+
apiKey, // REQUIRED. tenant-scoped sign-in is 401'd without a
|
|
118
|
+
// resolvable X-API-Key; pass '' for root/tenantless
|
|
119
|
+
// (the field is required by the type, inert on the wire)
|
|
117
120
|
})
|
|
118
121
|
|
|
119
122
|
// 2. build a typed client
|
|
120
123
|
const api: PlatformApi = createIsolatedPlatformApi({
|
|
121
124
|
baseUrl,
|
|
122
125
|
sessionToken: session.sessionToken,
|
|
126
|
+
apiKey, // rides as X-API-Key on every request
|
|
123
127
|
})
|
|
124
128
|
|
|
129
|
+
**Two credentials travel together on every request** (Firebase's
|
|
130
|
+
"API key + ID token" pairing, not an either/or): the session token
|
|
131
|
+
(`Authorization: Bearer …`) is what the platform authenticates and
|
|
132
|
+
authorizes as — Bearer always wins identity; the **publishable API
|
|
133
|
+
key** (`X-API-Key: …`) identifies the client and carries the
|
|
134
|
+
origin policy — a `public_api` key declares `allowed_origins`, and
|
|
135
|
+
a browser request's `Origin` is checked against the key that
|
|
136
|
+
resolves. A Bearer session *inherits the origin policy of the key
|
|
137
|
+
that minted it*. Where the key comes from: a tenant admin mints it
|
|
138
|
+
in the platform's API-keys page (Settings → API Keys), or
|
|
139
|
+
programmatically via `api.admin.createTenantApiKey(tenantSlug,
|
|
140
|
+
{ name, data_access_mode })` (platform-admin-gated).
|
|
141
|
+
|
|
125
142
|
Sessions come in three scopes:
|
|
126
143
|
- **root** — Alvera root admin; user signup + confirmation
|
|
144
|
+
(`apiKey: ''` — no tenant to scope a key to)
|
|
127
145
|
- **tenantless** — authenticated user with no tenant chosen yet;
|
|
128
146
|
used once to create a tenant via `api.tenants.create(...)`
|
|
147
|
+
(`apiKey: ''`)
|
|
129
148
|
- **tenant-scoped** — the canonical Bearer for tenant operations
|
|
149
|
+
(**requires a real tenant API key** — the login 401s with
|
|
150
|
+
`X-API-Key required to sign in to a tenant` otherwise)
|
|
151
|
+
|
|
152
|
+
**Build apps tenant-scoped, and only tenant-scoped.** The root and
|
|
153
|
+
tenantless scopes exist for the platform's own UI and for
|
|
154
|
+
admin/bootstrap flows (signup, confirmation, tenant creation,
|
|
155
|
+
test-suite setup) — they are NOT app-building surfaces; an app that
|
|
156
|
+
holds a root or tenantless Bearer is misdesigned. An app
|
|
157
|
+
authenticates against exactly one tenant with that tenant's
|
|
158
|
+
publishable key. Likewise, connected apps are **single-tenant by
|
|
159
|
+
construction** — one connected app per tenant, never one deployment
|
|
160
|
+
serving many tenants (see `connected_apps.md`).
|
|
130
161
|
|
|
131
162
|
Consumers holding multiple concurrent clients in the same file
|
|
132
163
|
(e.g. integration tests that need root + tenantless + tenant-
|
|
@@ -189,7 +220,13 @@ business outcome.
|
|
|
189
220
|
WORKFLOWS (composition on top)
|
|
190
221
|
─────────
|
|
191
222
|
workflows.md Filter + decision + action;
|
|
192
|
-
standard + agent-driven variants
|
|
223
|
+
standard + agent-driven variants;
|
|
224
|
+
§8 watching a run.
|
|
225
|
+
messages.md The messages DATASET — what went
|
|
226
|
+
out, delivery status, engagement.
|
|
227
|
+
action_logs.md The action_logs DATASET — per-
|
|
228
|
+
action outcomes, batch_id joins,
|
|
229
|
+
audience export.
|
|
193
230
|
```
|
|
194
231
|
|
|
195
232
|
Axes are mostly disjoint but a few resources cross. AI Agents
|
|
@@ -217,6 +254,13 @@ wire name (snake_case):
|
|
|
217
254
|
api.dataActivationClients data_activation_clients.md
|
|
218
255
|
api.workflows workflows.md
|
|
219
256
|
|
|
257
|
+
Two corpus pages document DATASETS, not namespaces — read
|
|
258
|
+
through `api.datasets` with SINGULAR dataset types (the plural
|
|
259
|
+
forms are the SQL table names used by `executeSql`):
|
|
260
|
+
|
|
261
|
+
dataset type 'message' (table messages) messages.md
|
|
262
|
+
dataset type 'action_log' (table action_logs) action_logs.md
|
|
263
|
+
|
|
220
264
|
## Cookbooks (Golden Path scenarios)
|
|
221
265
|
|
|
222
266
|
`.agent/cookbook/<slug>.md` files are pure-markdown scenario
|
|
@@ -47,28 +47,51 @@ request.
|
|
|
47
47
|
import { createSession } from '@alvera-ai/platform-sdk'
|
|
48
48
|
|
|
49
49
|
// root admin (no tenant) — for confirmUser, signUp on behalf of, etc.
|
|
50
|
+
// apiKey is required by the type but inert for non-tenant scopes — pass ''.
|
|
50
51
|
const root = await createSession({
|
|
51
|
-
baseUrl, email: rootEmail, password: rootPassword,
|
|
52
|
+
baseUrl, email: rootEmail, password: rootPassword, apiKey: '',
|
|
52
53
|
})
|
|
53
54
|
|
|
54
55
|
// tenantless — authenticated user who has not picked a tenant yet
|
|
55
56
|
const tenantless = await createSession({
|
|
56
|
-
baseUrl, email, password,
|
|
57
|
+
baseUrl, email, password, apiKey: '',
|
|
57
58
|
})
|
|
58
59
|
// tenantless.tenant === null
|
|
59
60
|
|
|
60
|
-
// tenant-scoped — the canonical Bearer for tenant operations
|
|
61
|
+
// tenant-scoped — the canonical Bearer for tenant operations.
|
|
62
|
+
// REQUIRES a real tenant API key (sent as X-API-Key): the server 401s
|
|
63
|
+
// with "X-API-Key required to sign in to a tenant" without one. The
|
|
64
|
+
// minted Bearer inherits the origin policy of the key that minted it.
|
|
61
65
|
const tenant = await createSession({
|
|
62
|
-
baseUrl, email, password, tenantSlug,
|
|
66
|
+
baseUrl, email, password, tenantSlug, apiKey: tenantApiKey,
|
|
63
67
|
})
|
|
64
68
|
// tenant.tenant.slug === tenantSlug
|
|
65
69
|
// tenant.role.name === 'tenant_admin' | 'member' | ...
|
|
66
70
|
```
|
|
67
71
|
|
|
72
|
+
The tenant API key is minted by a tenant admin in the platform's
|
|
73
|
+
API-keys page (Settings → API Keys), or programmatically via the
|
|
74
|
+
platform-admin-gated side door:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const { data: minted } = await rootApi.admin.createTenantApiKey(tenantSlug, {
|
|
78
|
+
name: 'Bootstrap Key',
|
|
79
|
+
data_access_mode: 'unregulated',
|
|
80
|
+
})
|
|
81
|
+
// minted.api_key — the publishable key value (thread into createSession)
|
|
82
|
+
```
|
|
83
|
+
|
|
68
84
|
See `AGENTS.md` "Sessions come in three scopes" for when each is
|
|
69
85
|
appropriate. Tenantless is a transient state used once: to create
|
|
70
86
|
a tenant (creator path) or accept an invitation (invitee path).
|
|
71
87
|
|
|
88
|
+
**Scope discipline for app builders:** root and tenantless sessions
|
|
89
|
+
are platform-UI / admin-bootstrap surfaces (signup, confirmation,
|
|
90
|
+
tenant creation, test setup) — never ship them inside an app. An
|
|
91
|
+
app authenticates **tenant-scoped against exactly one tenant**,
|
|
92
|
+
with that tenant's publishable key; connected apps in particular
|
|
93
|
+
are single-tenant by construction (`connected_apps.md`).
|
|
94
|
+
|
|
72
95
|
### Explicit teardown — `revokeSession`
|
|
73
96
|
|
|
74
97
|
The inverse of `createSession`. Revokes the current Bearer on
|
|
@@ -214,7 +237,7 @@ client-side"). Use the returned slug for downstream calls.
|
|
|
214
237
|
**Gotcha**: the response carries a `session_token` field, but this
|
|
215
238
|
is an *incidental* token — do not treat it as a freshly minted
|
|
216
239
|
tenant-scoped Bearer. The authoritative pattern is to call
|
|
217
|
-
`createSession({ ..., tenantSlug })` after `tenants.create`. This
|
|
240
|
+
`createSession({ ..., tenantSlug, apiKey })` after `tenants.create`. This
|
|
218
241
|
gives a fresh Bearer with all role + tenant claims correctly
|
|
219
242
|
populated.
|
|
220
243
|
|
|
@@ -223,6 +246,8 @@ populated.
|
|
|
223
246
|
```typescript
|
|
224
247
|
const { data: tenants } = await api.tenants.list()
|
|
225
248
|
// tenants.data: TenantResponse[] — every tenant the current Bearer can see
|
|
249
|
+
// takes an optional Flop query (page/page_size/order_by/filters —
|
|
250
|
+
// see type_naming.md "List response types")
|
|
226
251
|
```
|
|
227
252
|
|
|
228
253
|
A root Bearer sees every tenant; a tenant-scoped Bearer typically
|
|
@@ -277,7 +302,7 @@ the invitation enum:
|
|
|
277
302
|
| `'datalake_api'` | Programmatic datalake access | provisioned, not invited |
|
|
278
303
|
|
|
279
304
|
```typescript
|
|
280
|
-
const session = await createSession({ baseUrl, email, password, tenantSlug })
|
|
305
|
+
const session = await createSession({ baseUrl, email, password, tenantSlug, apiKey: tenantApiKey })
|
|
281
306
|
// session.role.name === 'tenant_admin' if accepted from a role: 'admin' invitation
|
|
282
307
|
// session.role.name === 'member' if accepted from a role: 'member' invitation
|
|
283
308
|
// session.role.name === 'researcher' if accepted from a role: 'researcher' invitation
|
|
@@ -327,7 +352,7 @@ const mine = invites.data.find(i => i.tenant.slug === tenantSlug)
|
|
|
327
352
|
const { data: membership } = await tenantlessApi.invitations.accept(mine.id)
|
|
328
353
|
|
|
329
354
|
// Then mint a tenant-scoped Bearer
|
|
330
|
-
const session = await createSession({ baseUrl, email, password, tenantSlug })
|
|
355
|
+
const session = await createSession({ baseUrl, email, password, tenantSlug, apiKey: tenantApiKey })
|
|
331
356
|
// session.role.name === 'member'
|
|
332
357
|
```
|
|
333
358
|
|
|
@@ -419,7 +444,7 @@ treat as "already exists" via `api.tenants.list` lookup.
|
|
|
419
444
|
|
|
420
445
|
2. **The `session_token` returned by `tenants.create` is
|
|
421
446
|
incidental.** Always re-mint a fresh tenant-scoped Bearer via
|
|
422
|
-
`createSession({ ..., tenantSlug })` after creating a tenant.
|
|
447
|
+
`createSession({ ..., tenantSlug, apiKey })` after creating a tenant.
|
|
423
448
|
This guarantees clean role+tenant claims on the Bearer.
|
|
424
449
|
|
|
425
450
|
3. **Tenantless Bearer is a transient state.** Only two
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Action logs (datalake dataset)
|
|
2
|
+
|
|
3
|
+
One row = one action outcome for one subject. Where `messages`
|
|
4
|
+
answers "what went out", `action_logs` answers "what did each
|
|
5
|
+
action do, per subject, per run" — including the actions that did
|
|
6
|
+
NOT send (skipped, filtered, failed).
|
|
7
|
+
|
|
8
|
+
Like `messages`, this is a **dataset** — read it via
|
|
9
|
+
`api.datasets.createUserSearch` + `api.datasets.search` with
|
|
10
|
+
dataset type `'action_log'` — **singular**; the plural
|
|
11
|
+
`action_logs` is the SQL *table* name (`executeSql`) and 422s as
|
|
12
|
+
a dataset type. Base SQL alias: `ral`. Rows exist only for
|
|
13
|
+
**live** mode — dry runs never write here.
|
|
14
|
+
|
|
15
|
+
## 1. When a row appears (the scheduled gap)
|
|
16
|
+
|
|
17
|
+
Not all at once. Two moments:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
at run time — failed / filtered / skipped outcomes
|
|
21
|
+
land immediately
|
|
22
|
+
at execution time — a scheduled action gets its row only
|
|
23
|
+
when the trigger clock fires and it
|
|
24
|
+
actually executes
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Between those two moments a scheduled send is INVISIBLE here: its
|
|
28
|
+
execution log exists (the platform accepted it), but no
|
|
29
|
+
action_log row and no message row yet. A UI that only reads the
|
|
30
|
+
datalake shows nothing and looks broken — read the execution logs
|
|
31
|
+
(`workflows.md` §6) and say "scheduled — nothing sent yet".
|
|
32
|
+
|
|
33
|
+
## 2. Fields
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
id, workflow_id, action_id, workflow_execution_log_id,
|
|
37
|
+
batch_id — the run this outcome belongs to; THE
|
|
38
|
+
campaign key (messages does not have it)
|
|
39
|
+
decision_key — which action fired
|
|
40
|
+
mdm_subject_id, subject_name, session_id
|
|
41
|
+
action_type ('sms'|'email'|'voice'|'data_exchange'), channel,
|
|
42
|
+
classification
|
|
43
|
+
status — see §3
|
|
44
|
+
external_id — provider id; joins to messages.external_id
|
|
45
|
+
sent_at, tool_call_payload, idempotency_key, metadata,
|
|
46
|
+
attachments, body, sender_tool_id
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 3. Statuses
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pending → queued → sent → delivered
|
|
53
|
+
└──→ failed
|
|
54
|
+
|
|
55
|
+
skipped — the action's runtime_filter said no; nothing sent
|
|
56
|
+
filtered — the parent run was filtered out; nothing sent
|
|
57
|
+
cancelled — manually cancelled
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Initial status mirrors the action execution log's outcome; a
|
|
61
|
+
status updater with an `action_log_config` can move
|
|
62
|
+
`sent → delivered/failed` here too (optional — many setups only
|
|
63
|
+
reconcile messages).
|
|
64
|
+
|
|
65
|
+
## 4. How everything joins
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
run (batch_id) ──── WorkflowRunLog "how is the batch doing"
|
|
69
|
+
│
|
|
70
|
+
├── WorkflowExecutionLog (per subject) "what did the pipeline decide"
|
|
71
|
+
│ │
|
|
72
|
+
│ └── ActionExecutionLog "what did each action attempt"
|
|
73
|
+
│ │ (live + actually executed)
|
|
74
|
+
│ ▼
|
|
75
|
+
└────────── action_logs row "the outcome, queryable"
|
|
76
|
+
│ external_id
|
|
77
|
+
▼
|
|
78
|
+
messages row "the communication + delivery"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Left side lives in the platform DB (read via
|
|
82
|
+
`api.workflows.batchLogs` / `api.workflows.workflowLogs`); the
|
|
83
|
+
bottom two are datalake datasets. `batch_id` ties the datalake
|
|
84
|
+
rows back to the run; `external_id` ties an action_log to its
|
|
85
|
+
message.
|
|
86
|
+
|
|
87
|
+
## 5. Reading + audience export
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// dataset type is SINGULAR 'action_log' (the table is plural)
|
|
91
|
+
const { data: search } = await api.datasets.createUserSearch(
|
|
92
|
+
tenantSlug, datalakeSlug, 'action_log',
|
|
93
|
+
{ search_query: `ral.batch_id = '${batchId}'` },
|
|
94
|
+
)
|
|
95
|
+
const { data: page } = await api.datasets.search(
|
|
96
|
+
tenantSlug, datalakeSlug, 'action_log',
|
|
97
|
+
{ userSearchId: search.id, dataAccessMode: 'unregulated' },
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Filterable columns (Flop): `status`, `batch_id`, `action_type`,
|
|
102
|
+
`channel`, `classification`, `workflow_id`, `sent_at`,
|
|
103
|
+
`external_id`, `mdm_subject_id`, `inserted_at`.
|
|
104
|
+
|
|
105
|
+
"Who did this campaign go to" = the query above (subject columns
|
|
106
|
+
+ status per row — exportable as-is). For bulk export the platform
|
|
107
|
+
also merges each finished batch into NDJSON artifacts in cold
|
|
108
|
+
storage:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
<datalake>/workflows/<wf_slug>/<batch_id>/merged.ndjson (per-subject)
|
|
112
|
+
<datalake>/workflows/<wf_slug>/<batch_id>/<decision_key>/merged.ndjson (per-action payloads)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The batch log row (`api.workflows.batchLogs.get`) carries the
|
|
116
|
+
download references once the batch completes.
|
|
117
|
+
|
|
118
|
+
## 6. Gotchas
|
|
119
|
+
|
|
120
|
+
1. **The scheduled gap (§1).** No action_log row ≠ nothing
|
|
121
|
+
happened. Check the execution logs before declaring a run
|
|
122
|
+
empty.
|
|
123
|
+
|
|
124
|
+
2. **`batch_id` is a string, not a UUID** — manual runs look like
|
|
125
|
+
`manual:<uuid>`; DAC-triggered runs carry the ingest batch id.
|
|
126
|
+
Compare as strings.
|
|
127
|
+
|
|
128
|
+
3. **One subject can have many rows for the same decision.**
|
|
129
|
+
Re-fires via `workflows.execute` with `manual_override: true`
|
|
130
|
+
(the retry path) create a NEW action_log each time. Latest row
|
|
131
|
+
wins for "current state"; the full set is the audit trail.
|
|
132
|
+
|
|
133
|
+
4. **`skipped`/`filtered` rows are successes of the control
|
|
134
|
+
system, not failures.** Only `failed` is the retry surface
|
|
135
|
+
(`workflows.md` §8).
|
|
136
|
+
|
|
137
|
+
5. **`tool_call_payload` is the rendered call** — what was
|
|
138
|
+
actually sent to the tool, after Liquid. When a message looks
|
|
139
|
+
wrong, read this before blaming the template engine.
|
|
@@ -204,11 +204,12 @@ branch.
|
|
|
204
204
|
### Read shapes
|
|
205
205
|
|
|
206
206
|
```
|
|
207
|
-
.list(tenantSlug, datalakeSlug)
|
|
208
|
-
|
|
207
|
+
.list(tenantSlug, datalakeSlug, query?) Paged: { data, meta }
|
|
208
|
+
(full Flop query — type_naming.md)
|
|
209
|
+
.get(tenantSlug, datalakeSlug, id) One row — UUID only
|
|
209
210
|
.metadata(tenantSlug, datalakeSlug) Markdown — catalog
|
|
210
211
|
.metadataDetails(tenantSlug, datalakeSlug, Markdown — one updater
|
|
211
|
-
|
|
212
|
+
id) (UUID only)
|
|
212
213
|
```
|
|
213
214
|
|
|
214
215
|
### Update
|
|
@@ -262,3 +263,35 @@ check or retry for partial-delete states.
|
|
|
262
263
|
transient delivery delays don't fall between two polls.
|
|
263
264
|
The defaults in the wire-shape example illustrate this
|
|
264
265
|
pattern: 30-minute cron, 45-minute window.
|
|
266
|
+
|
|
267
|
+
## 7. Where delivery truth comes from (consumer view)
|
|
268
|
+
|
|
269
|
+
If you only READ the platform (an operator UI, a dashboard), this
|
|
270
|
+
is the part that matters:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
dispatch writes: messages row, status 'sent',
|
|
274
|
+
external_id = provider's id
|
|
275
|
+
THIS updater writes: 'delivered' or 'failed' +
|
|
276
|
+
failure_reason (+ delivered_at)
|
|
277
|
+
nothing else writes those.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- **No updater configured → no delivery truth.** Rows stay `sent`
|
|
281
|
+
forever; a delivery-rate number computed from them is fiction.
|
|
282
|
+
Updaters are created manually (this page §1) — the platform does
|
|
283
|
+
not auto-create them per sender tool.
|
|
284
|
+
- **The join is `{external_id, set_params}`,** exact-match, silent
|
|
285
|
+
on misses (gotcha 3). "Eventually honest": a just-sent row is
|
|
286
|
+
neither delivered nor failed yet — show the in-between state.
|
|
287
|
+
- **Retry is not here.** The updater only reports. Re-sending a
|
|
288
|
+
failed message is `workflows.execute` for that subject +
|
|
289
|
+
decision (`mode: 'live'`, `manual_override: true`) — the same
|
|
290
|
+
call as any manual dispatch; there is no retry endpoint. See
|
|
291
|
+
`workflows.md` §8.
|
|
292
|
+
- **Engagement is not here either.** `opened_at` /
|
|
293
|
+
`form_submitted_at` arrive via
|
|
294
|
+
`connectedApps.updateMessageTracking` (magic-link seam,
|
|
295
|
+
`connected_apps.md` §6) and never touch `status`.
|
|
296
|
+
- Field-by-field semantics of the rows being updated:
|
|
297
|
+
`messages.md`, `action_logs.md`.
|
package/.agent/ai_agents.md
CHANGED
|
@@ -331,7 +331,8 @@ once `enabled: true`.
|
|
|
331
331
|
### Read shapes
|
|
332
332
|
|
|
333
333
|
```
|
|
334
|
-
.list(tenantSlug, datalakeSlug)
|
|
334
|
+
.list(tenantSlug, datalakeSlug, query?) Paged: { data, meta }
|
|
335
|
+
(full Flop query — type_naming.md)
|
|
335
336
|
.get(tenantSlug, datalakeSlug, id) One row, BY ID (UUID — not slug)
|
|
336
337
|
.metadata(tenantSlug, datalakeSlug) Markdown catalog
|
|
337
338
|
.metadataDetails(tenantSlug, datalakeSlug, Markdown for one agent;
|
package/.agent/connected_apps.md
CHANGED
|
@@ -27,6 +27,15 @@ The namespace splits into **two roles**:
|
|
|
27
27
|
machine-to-machine API key) when a customer hits a magic link. Uses the app's
|
|
28
28
|
`slug` as the path key.
|
|
29
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
|
+
|
|
30
39
|
Connected apps are **Datalake-DB-resident** — the parent datalake
|
|
31
40
|
must be `status: 'ready'` before you `create()`.
|
|
32
41
|
|
|
@@ -219,7 +228,8 @@ and can be retried via `.syncRoutes`.
|
|
|
219
228
|
### Read shapes — management
|
|
220
229
|
|
|
221
230
|
```
|
|
222
|
-
.list(tenantSlug, datalakeSlug)
|
|
231
|
+
.list(tenantSlug, datalakeSlug, query?) Paged: { data, meta }
|
|
232
|
+
(full Flop query — type_naming.md)
|
|
223
233
|
.get(tenantSlug, datalakeSlug, id) One row, by UUID
|
|
224
234
|
.metadata(tenantSlug, datalakeSlug) Markdown catalog
|
|
225
235
|
.metadataDetails(tenantSlug, datalakeSlug, Markdown for one app
|
|
@@ -343,7 +353,16 @@ await api.connectedApps.updateMessageTracking(
|
|
|
343
353
|
|
|
344
354
|
Both calls are authenticated with the connected app's
|
|
345
355
|
auto-provisioned machine-to-machine API key (typically configured as
|
|
346
|
-
`ALVERA_API_KEY` in the deployment environment).
|
|
356
|
+
`ALVERA_API_KEY` in the deployment environment). Both are also on
|
|
357
|
+
the `public_api` (publishable-key) allowlist — alongside `ingest`,
|
|
358
|
+
`ingest-file`, and `mdm/verify` — so a browser-side app holding
|
|
359
|
+
only a publishable key can call them.
|
|
360
|
+
|
|
361
|
+
**This is the engagement seam.** `updateMessageTracking` writes
|
|
362
|
+
`opened_at` / `form_submitted_at` onto the message row — the
|
|
363
|
+
open/click/conversion half of campaign analytics — and it NEVER
|
|
364
|
+
changes `status` (delivery truth belongs to the action status
|
|
365
|
+
updater; see `messages.md` §1 for who writes which field).
|
|
347
366
|
|
|
348
367
|
### Workflow-side wiring
|
|
349
368
|
|
|
@@ -36,13 +36,15 @@ having to copy-paste this prelude.
|
|
|
36
36
|
## 001 — root admin signs in and provisions a fresh industry-admin user
|
|
37
37
|
|
|
38
38
|
Authenticate as the platform's root admin (`admin@dev.local` /
|
|
39
|
-
`devpassword` in local dev)
|
|
39
|
+
`devpassword` in local dev) via the tenantless bootstrap login —
|
|
40
|
+
keyless by structural necessity (no tenant exists yet to scope a
|
|
41
|
+
key to), and a dev/test-only surface. Then sign up a per-run industry-admin
|
|
40
42
|
user (`sarah`) under a unique email derived from `runSuffix`, and
|
|
41
43
|
confirm them so they can sign in. Both the signup and the confirm
|
|
42
44
|
endpoints require root scope.
|
|
43
45
|
|
|
44
46
|
```typescript
|
|
45
|
-
ctx.rootSession = await
|
|
47
|
+
ctx.rootSession = await createBootstrapSession({
|
|
46
48
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
47
49
|
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
48
50
|
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
@@ -50,12 +52,13 @@ ctx.rootSession = await createSession({
|
|
|
50
52
|
ctx.rootApi = createIsolatedPlatformApi({
|
|
51
53
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
52
54
|
sessionToken: ctx.rootSession.sessionToken,
|
|
55
|
+
apiKey: '',
|
|
53
56
|
})
|
|
54
57
|
|
|
55
58
|
ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
|
|
56
59
|
ctx.sarahPassword = 'CookbookPass1!'
|
|
57
60
|
|
|
58
|
-
const signUpResp = await ctx.rootApi.
|
|
61
|
+
const signUpResp = await ctx.rootApi.admin.signUp({
|
|
59
62
|
email: ctx.sarahEmail,
|
|
60
63
|
password: ctx.sarahPassword,
|
|
61
64
|
first_name: 'Cookbook',
|
|
@@ -74,7 +77,7 @@ captured into the closure-scoped `tenantSlug` slot for downstream
|
|
|
74
77
|
steps.
|
|
75
78
|
|
|
76
79
|
```typescript
|
|
77
|
-
ctx.sarahTenantlessSession = await
|
|
80
|
+
ctx.sarahTenantlessSession = await createBootstrapSession({
|
|
78
81
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
79
82
|
email: ctx.sarahEmail,
|
|
80
83
|
password: ctx.sarahPassword,
|
|
@@ -82,6 +85,7 @@ ctx.sarahTenantlessSession = await createSession({
|
|
|
82
85
|
ctx.sarahTenantlessApi = createIsolatedPlatformApi({
|
|
83
86
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
84
87
|
sessionToken: ctx.sarahTenantlessSession.sessionToken,
|
|
88
|
+
apiKey: '',
|
|
85
89
|
})
|
|
86
90
|
|
|
87
91
|
const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
|
|
@@ -98,15 +102,28 @@ is assigned to the closure-scoped `api` slot so the scenario
|
|
|
98
102
|
cookbook's steps inherit it via inlining.
|
|
99
103
|
|
|
100
104
|
```typescript
|
|
105
|
+
// A tenant-scoped login requires X-API-Key — mint a public_api key for
|
|
106
|
+
// the fresh tenant via the platform-admin side door (root Bearer). In the
|
|
107
|
+
// web console this is Settings -> API Keys; any of the tenant's keys
|
|
108
|
+
// satisfies the login gate, and the minted Bearer inherits this key's
|
|
109
|
+
// origin policy.
|
|
110
|
+
const { data: mintedKey } = await ctx.rootApi.admin.createTenantApiKey(tenantSlug, {
|
|
111
|
+
name: `Cookbook Bootstrap Key ${runSuffix}`,
|
|
112
|
+
data_access_mode: 'unregulated',
|
|
113
|
+
})
|
|
114
|
+
ctx.tenantApiKey = mintedKey.api_key
|
|
115
|
+
|
|
101
116
|
const sarahTenantSession = await createSession({
|
|
102
117
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
103
118
|
email: ctx.sarahEmail,
|
|
104
119
|
password: ctx.sarahPassword,
|
|
105
120
|
tenantSlug,
|
|
121
|
+
apiKey: ctx.tenantApiKey,
|
|
106
122
|
})
|
|
107
123
|
api = createIsolatedPlatformApi({
|
|
108
124
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
109
125
|
sessionToken: sarahTenantSession.sessionToken,
|
|
126
|
+
apiKey: ctx.tenantApiKey,
|
|
110
127
|
})
|
|
111
128
|
```
|
|
112
129
|
|
|
@@ -266,6 +283,9 @@ After this setup runs, the closure-scoped slots are populated as:
|
|
|
266
283
|
- `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
|
|
267
284
|
credentials (kept on `ctx` because no scenario cookbook needs
|
|
268
285
|
to re-authenticate by default)
|
|
286
|
+
- `ctx.tenantApiKey` — the tenant's publishable API key (minted in
|
|
287
|
+
setup via `admin.createTenantApiKey`); thread it into any
|
|
288
|
+
additional tenant-scoped `createSession` a scenario performs
|
|
269
289
|
|
|
270
290
|
Scenario cookbooks under `industry: accounts_receivable` start at
|
|
271
291
|
their own `§001` with these slots already populated.
|
|
@@ -35,13 +35,15 @@ having to copy-paste this prelude.
|
|
|
35
35
|
## 001 — root admin signs in and provisions a fresh industry-admin user
|
|
36
36
|
|
|
37
37
|
Authenticate as the platform's root admin (`admin@dev.local` /
|
|
38
|
-
`devpassword` in local dev)
|
|
38
|
+
`devpassword` in local dev) via the tenantless bootstrap login —
|
|
39
|
+
keyless by structural necessity (no tenant exists yet to scope a
|
|
40
|
+
key to), and a dev/test-only surface. Then sign up a per-run industry-admin
|
|
39
41
|
user (`sarah`) under a unique email derived from `runSuffix`, and
|
|
40
42
|
confirm them so they can sign in. Both the signup and the confirm
|
|
41
43
|
endpoints require root scope.
|
|
42
44
|
|
|
43
45
|
```typescript
|
|
44
|
-
ctx.rootSession = await
|
|
46
|
+
ctx.rootSession = await createBootstrapSession({
|
|
45
47
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
46
48
|
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
47
49
|
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
@@ -49,12 +51,13 @@ ctx.rootSession = await createSession({
|
|
|
49
51
|
ctx.rootApi = createIsolatedPlatformApi({
|
|
50
52
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
51
53
|
sessionToken: ctx.rootSession.sessionToken,
|
|
54
|
+
apiKey: '',
|
|
52
55
|
})
|
|
53
56
|
|
|
54
57
|
ctx.sarahEmail = `cookbook-sarah-${runSuffix}@dev.local`
|
|
55
58
|
ctx.sarahPassword = 'CookbookPass1!'
|
|
56
59
|
|
|
57
|
-
const signUpResp = await ctx.rootApi.
|
|
60
|
+
const signUpResp = await ctx.rootApi.admin.signUp({
|
|
58
61
|
email: ctx.sarahEmail,
|
|
59
62
|
password: ctx.sarahPassword,
|
|
60
63
|
first_name: 'Cookbook',
|
|
@@ -72,7 +75,7 @@ foundation tenant. The tenant's server-derived `slug` is captured
|
|
|
72
75
|
into the closure-scoped `tenantSlug` slot for downstream steps.
|
|
73
76
|
|
|
74
77
|
```typescript
|
|
75
|
-
ctx.sarahTenantlessSession = await
|
|
78
|
+
ctx.sarahTenantlessSession = await createBootstrapSession({
|
|
76
79
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
77
80
|
email: ctx.sarahEmail,
|
|
78
81
|
password: ctx.sarahPassword,
|
|
@@ -80,6 +83,7 @@ ctx.sarahTenantlessSession = await createSession({
|
|
|
80
83
|
ctx.sarahTenantlessApi = createIsolatedPlatformApi({
|
|
81
84
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
82
85
|
sessionToken: ctx.sarahTenantlessSession.sessionToken,
|
|
86
|
+
apiKey: '',
|
|
83
87
|
})
|
|
84
88
|
|
|
85
89
|
const tenantResp = await ctx.sarahTenantlessApi.tenants.create({
|
|
@@ -96,15 +100,28 @@ is assigned to the closure-scoped `api` slot so the scenario
|
|
|
96
100
|
cookbook's steps inherit it via inlining.
|
|
97
101
|
|
|
98
102
|
```typescript
|
|
103
|
+
// A tenant-scoped login requires X-API-Key — mint a public_api key for
|
|
104
|
+
// the fresh tenant via the platform-admin side door (root Bearer). In the
|
|
105
|
+
// web console this is Settings -> API Keys; any of the tenant's keys
|
|
106
|
+
// satisfies the login gate, and the minted Bearer inherits this key's
|
|
107
|
+
// origin policy.
|
|
108
|
+
const { data: mintedKey } = await ctx.rootApi.admin.createTenantApiKey(tenantSlug, {
|
|
109
|
+
name: `Cookbook Bootstrap Key ${runSuffix}`,
|
|
110
|
+
data_access_mode: 'unregulated',
|
|
111
|
+
})
|
|
112
|
+
ctx.tenantApiKey = mintedKey.api_key
|
|
113
|
+
|
|
99
114
|
const sarahTenantSession = await createSession({
|
|
100
115
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
101
116
|
email: ctx.sarahEmail,
|
|
102
117
|
password: ctx.sarahPassword,
|
|
103
118
|
tenantSlug,
|
|
119
|
+
apiKey: ctx.tenantApiKey,
|
|
104
120
|
})
|
|
105
121
|
api = createIsolatedPlatformApi({
|
|
106
122
|
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
107
123
|
sessionToken: sarahTenantSession.sessionToken,
|
|
124
|
+
apiKey: ctx.tenantApiKey,
|
|
108
125
|
})
|
|
109
126
|
```
|
|
110
127
|
|
|
@@ -262,6 +279,9 @@ After this setup runs, the closure-scoped slots are populated as:
|
|
|
262
279
|
- `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
|
|
263
280
|
credentials (kept on `ctx` because no scenario cookbook needs
|
|
264
281
|
to re-authenticate by default)
|
|
282
|
+
- `ctx.tenantApiKey` — the tenant's publishable API key (minted in
|
|
283
|
+
setup via `admin.createTenantApiKey`); thread it into any
|
|
284
|
+
additional tenant-scoped `createSession` a scenario performs
|
|
265
285
|
|
|
266
286
|
Scenario cookbooks under `industry: foundation` start at their own
|
|
267
287
|
`§001` with these slots already populated.
|