@alvera-ai/platform-sdk 0.12.1 → 0.13.0-next.g14c04ff
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 +121 -19
- package/.agent/account_management.md +110 -38
- package/.agent/action_logs.md +144 -0
- package/.agent/action_status_updaters.md +302 -53
- package/.agent/ai_agents.md +149 -53
- package/.agent/ai_sandbox.md +11 -4
- package/.agent/async.md +16 -6
- package/.agent/connected_apps.md +123 -6
- package/.agent/cookbook/_fixtures/README.md +9 -9
- package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid → payments/_compliance_screenings_payments_compliance_screening.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_mdm.liquid → payments/_compliance_screenings_payments_mdm.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_mdm.liquid → payments/_payment_accounts_payments_mdm.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_payment_account.liquid → payments/_payment_accounts_payments_payment_account.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_customer.liquid → subscription/_customers_subscription_customer.liquid} +1 -1
- package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_mdm.liquid → subscription/_customers_subscription_mdm.liquid} +1 -1
- package/.agent/cookbook/_setup/foundation.md +24 -4
- package/.agent/cookbook/_setup/healthcare.md +24 -4
- package/.agent/cookbook/_setup/{payment_risk.md → payments.md} +51 -31
- package/.agent/cookbook/_setup/{accounts_receivable.md → subscription.md} +51 -31
- package/.agent/cookbook/action-status-updaters.md +41 -18
- package/.agent/cookbook/bulk-ingest.md +15 -15
- package/.agent/cookbook/dunning-sms-for-delinquent.md +21 -21
- package/.agent/cookbook/fork-and-apply-cold.md +192 -0
- package/.agent/cookbook/{custom-tables.md → generic-tables.md} +44 -41
- package/.agent/cookbook/invite-team.md +19 -13
- package/.agent/cookbook/kyc-notification-on-account-activation.md +26 -26
- package/.agent/cookbook/marketing-campaign-send.md +1003 -0
- package/.agent/cookbook/paginated-restapi-poller.md +284 -0
- package/.agent/cookbook/rest-fetch.md +9 -9
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +22 -22
- package/.agent/cookbook/system-templates.md +15 -15
- package/.agent/cookbook/triage-prospects-by-priority.md +17 -17
- package/.agent/cookbook/welcome-sms-for-customers.md +21 -21
- package/.agent/data_activation_clients.md +177 -42
- package/.agent/data_sources.md +55 -37
- package/.agent/datalakes.md +54 -17
- package/.agent/debugging.md +48 -14
- package/.agent/errors.md +39 -17
- package/.agent/generic_tables.md +150 -51
- package/.agent/interoperability_contracts.md +114 -15
- package/.agent/mdm.md +171 -93
- package/.agent/messages.md +252 -0
- package/.agent/templates.md +21 -4
- package/.agent/tool-call-configs.md +12 -1
- package/.agent/tools.md +181 -63
- package/.agent/type_naming.md +83 -18
- package/.agent/workflows.md +408 -62
- package/LICENSE +21 -93
- package/README.md +2 -2
- package/dist/bin/platform-sdk.mjs +53 -1
- package/dist/bin/platform-sdk.mjs.map +1 -1
- package/dist/index.d.mts +737 -70
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +656 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -6
- /package/.agent/cookbook/_fixtures/{accounts_receivable → subscription}/stripe_customers_batch1.csv +0 -0
package/.agent/AGENTS.md
CHANGED
|
@@ -52,6 +52,28 @@ opening it. If you find yourself emitting a claim about a kind's
|
|
|
52
52
|
wire shape without having opened that kind's MD in the current
|
|
53
53
|
session, **stop and open it now**.
|
|
54
54
|
|
|
55
|
+
### You are NOT entitled to conclude "the platform cannot do X"
|
|
56
|
+
|
|
57
|
+
This corpus is your window onto the platform, not its floor plan.
|
|
58
|
+
**Absence from these guides is not evidence the platform lacks a
|
|
59
|
+
capability** — it usually just means no use case had yet forced that
|
|
60
|
+
capability into the docs. The platform is deliberately generic; the
|
|
61
|
+
same primitives back many verticals, and each new build surfaces a
|
|
62
|
+
fresh batch of capabilities the corpus never had a reason to spell out.
|
|
63
|
+
|
|
64
|
+
So when a build seems to need something you can't find here, the honest
|
|
65
|
+
output is a **problem statement to the platform team** ("I need to do
|
|
66
|
+
X; the corpus doesn't document a way — does one exist?"), **not a
|
|
67
|
+
verdict** ("the platform can't do X"). A wrong "can't" is expensive: it
|
|
68
|
+
sends an agent to build the wrong thing or to file a request for a
|
|
69
|
+
feature that already ships. Real examples that were *all* in the
|
|
70
|
+
platform but missing from the corpus until a single build forced them
|
|
71
|
+
in: SMS quiet-hours send windows, pausing a scheduled campaign by
|
|
72
|
+
editing an action's `runtime_filter`, scheduling on the action rather
|
|
73
|
+
than the run, per-recipient tracked-link minting, and per-variant
|
|
74
|
+
`GROUP BY decision_key` analytics. Each was momentarily mistaken for a
|
|
75
|
+
platform gap. Don't repeat that — escalate the question, don't rule.
|
|
76
|
+
|
|
55
77
|
---
|
|
56
78
|
|
|
57
79
|
## If you know MCP — vocabulary mapping
|
|
@@ -100,33 +122,91 @@ construction.
|
|
|
100
122
|
## SDK auth + client construction
|
|
101
123
|
|
|
102
124
|
The SDK is a strict-TypeScript REST client for the Alvera
|
|
103
|
-
platform. Two-step auth + client construction
|
|
125
|
+
platform. Two-step auth + client construction — and there are **two**
|
|
126
|
+
session minters, not one (see `account_management.md` for the full
|
|
127
|
+
treatment):
|
|
104
128
|
|
|
105
129
|
import {
|
|
106
|
-
|
|
130
|
+
createBootstrapSession, // tenantless / root Bearer
|
|
131
|
+
createSession, // tenant-scoped Bearer
|
|
107
132
|
createIsolatedPlatformApi,
|
|
108
133
|
type PlatformApi,
|
|
109
134
|
} from '@alvera-ai/platform-sdk'
|
|
110
135
|
|
|
111
|
-
//
|
|
136
|
+
// 1a. tenantless / root — POST /api/v1/admin/bootstrap-session.
|
|
137
|
+
// Keyless by structural necessity (it mints the FIRST Bearer of an
|
|
138
|
+
// environment, before any tenant key exists). Returned tenant + role
|
|
139
|
+
// are always null. Dev / integration-test route — absent from prod.
|
|
140
|
+
const bootstrap = await createBootstrapSession({ baseUrl, email, password })
|
|
141
|
+
|
|
142
|
+
// 1b. tenant-scoped — the canonical app session. tenantSlug AND apiKey
|
|
143
|
+
// are BOTH required; sign-in 401s ("X-API-Key required to sign in to
|
|
144
|
+
// a tenant") without a resolvable X-API-Key.
|
|
112
145
|
const session = await createSession({
|
|
113
146
|
baseUrl,
|
|
114
147
|
email,
|
|
115
148
|
password,
|
|
116
|
-
tenantSlug, //
|
|
149
|
+
tenantSlug, // REQUIRED
|
|
150
|
+
apiKey, // REQUIRED — the tenant's publishable key
|
|
117
151
|
})
|
|
118
152
|
|
|
119
153
|
// 2. build a typed client
|
|
120
154
|
const api: PlatformApi = createIsolatedPlatformApi({
|
|
121
155
|
baseUrl,
|
|
122
156
|
sessionToken: session.sessionToken,
|
|
157
|
+
apiKey, // rides as X-API-Key on every request
|
|
123
158
|
})
|
|
124
159
|
|
|
125
|
-
|
|
126
|
-
|
|
160
|
+
**Two credentials travel together on every request** (Firebase's
|
|
161
|
+
"API key + ID token" pairing, not an either/or): the session token
|
|
162
|
+
(`Authorization: Bearer …`) is what the platform authenticates and
|
|
163
|
+
authorizes as — Bearer always wins identity; the **publishable API
|
|
164
|
+
key** (`X-API-Key: …`) identifies the client and carries the
|
|
165
|
+
origin policy — a `public_api` key declares `allowed_origins`, and
|
|
166
|
+
a browser request's `Origin` is checked against the key that
|
|
167
|
+
resolves. A Bearer session *inherits the origin policy of the key
|
|
168
|
+
that minted it*. Where the key comes from: a tenant admin mints it
|
|
169
|
+
in the platform's API-keys page (Settings → API Keys), or
|
|
170
|
+
programmatically via `api.admin.createTenantApiKey(tenantSlug,
|
|
171
|
+
{ name, data_access_mode })` (platform-admin-gated).
|
|
172
|
+
|
|
173
|
+
**A CORS preflight is NOT proof the key admits your origin.** A
|
|
174
|
+
browser `OPTIONS` preflight carries no `X-API-Key` (only the method
|
|
175
|
+
+ `Access-Control-Request-Headers`), so the server cannot resolve a
|
|
176
|
+
key and answers permissively — EVERY origin gets a friendly
|
|
177
|
+
`access-control-allow-origin` echo. The enforced policy runs on the
|
|
178
|
+
actual authenticated request, against the resolved key's
|
|
179
|
+
`allowed_origins`. Never verify origins with a preflight — verify
|
|
180
|
+
with a real signed-in round-trip. Triage for a failing browser
|
|
181
|
+
sign-in: (1) console says the key env var is missing → your `.env`
|
|
182
|
+
didn't load (restart the dev server after editing it); (2) the REAL
|
|
183
|
+
call is blocked by CORS (no ACAO on it) → the key's
|
|
184
|
+
`allowed_origins` doesn't include your dev origin — mint/patch the
|
|
185
|
+
key, and don't trust the earlier green preflight; (3) a `401` with
|
|
186
|
+
NO CORS error → the request PASSED CORS; the fault is the
|
|
187
|
+
credentials or the key not resolving, not origins.
|
|
188
|
+
|
|
189
|
+
Sessions come in three scopes, minted by two functions:
|
|
190
|
+
- **root** — Alvera root admin; user signup + confirmation. Minted by
|
|
191
|
+
`createBootstrapSession` (a tenantless Bearer that belongs to a
|
|
192
|
+
platform-admin user).
|
|
127
193
|
- **tenantless** — authenticated user with no tenant chosen yet;
|
|
128
|
-
used once to create a tenant via `api.tenants.create(...)
|
|
129
|
-
|
|
194
|
+
used once to create a tenant via `api.tenants.create(...)`. Also
|
|
195
|
+
minted by `createBootstrapSession`.
|
|
196
|
+
- **tenant-scoped** — the canonical Bearer for tenant operations,
|
|
197
|
+
minted by `createSession` (**requires both `tenantSlug` and a real
|
|
198
|
+
tenant API key** — the login 401s with `X-API-Key required to sign
|
|
199
|
+
in to a tenant` otherwise).
|
|
200
|
+
|
|
201
|
+
**Build apps tenant-scoped, and only tenant-scoped.** The root and
|
|
202
|
+
tenantless scopes exist for the platform's own UI and for
|
|
203
|
+
admin/bootstrap flows (signup, confirmation, tenant creation,
|
|
204
|
+
test-suite setup) — they are NOT app-building surfaces; an app that
|
|
205
|
+
holds a root or tenantless Bearer is misdesigned. An app
|
|
206
|
+
authenticates against exactly one tenant with that tenant's
|
|
207
|
+
publishable key. Likewise, connected apps are **single-tenant by
|
|
208
|
+
construction** — one connected app per tenant, never one deployment
|
|
209
|
+
serving many tenants (see `connected_apps.md`).
|
|
130
210
|
|
|
131
211
|
Consumers holding multiple concurrent clients in the same file
|
|
132
212
|
(e.g. integration tests that need root + tenantless + tenant-
|
|
@@ -189,7 +269,13 @@ business outcome.
|
|
|
189
269
|
WORKFLOWS (composition on top)
|
|
190
270
|
─────────
|
|
191
271
|
workflows.md Filter + decision + action;
|
|
192
|
-
standard + agent-driven variants
|
|
272
|
+
standard + agent-driven variants;
|
|
273
|
+
§8 watching a run.
|
|
274
|
+
messages.md The messages DATASET — what went
|
|
275
|
+
out, delivery status, engagement.
|
|
276
|
+
action_logs.md The action_logs DATASET — per-
|
|
277
|
+
action outcomes, batch_id joins,
|
|
278
|
+
audience export.
|
|
193
279
|
```
|
|
194
280
|
|
|
195
281
|
Axes are mostly disjoint but a few resources cross. AI Agents
|
|
@@ -217,6 +303,13 @@ wire name (snake_case):
|
|
|
217
303
|
api.dataActivationClients data_activation_clients.md
|
|
218
304
|
api.workflows workflows.md
|
|
219
305
|
|
|
306
|
+
Two corpus pages document DATASETS, not namespaces — read
|
|
307
|
+
through `api.datasets` with SINGULAR dataset types (the plural
|
|
308
|
+
forms are the SQL table names used by `executeSql`):
|
|
309
|
+
|
|
310
|
+
dataset type 'message' (table messages) messages.md
|
|
311
|
+
dataset type 'action_log' (table action_logs) action_logs.md
|
|
312
|
+
|
|
220
313
|
## Cookbooks (Golden Path scenarios)
|
|
221
314
|
|
|
222
315
|
`.agent/cookbook/<slug>.md` files are pure-markdown scenario
|
|
@@ -357,16 +450,18 @@ The `cookbook/` directory holds two kinds of recipe, both validated by
|
|
|
357
450
|
an outcome.
|
|
358
451
|
- **Capability docs** (`<capability>.md`) — one platform capability each,
|
|
359
452
|
shown as the minimal call sequence that proves it (e.g. `bulk-ingest`,
|
|
360
|
-
`ai-agent-invoke`, `
|
|
453
|
+
`ai-agent-invoke`, `generic-tables`, `invite-team`). Read these to learn one
|
|
361
454
|
capability in isolation; cookbooks weave them into outcomes.
|
|
362
455
|
|
|
363
456
|
### Available cookbooks
|
|
364
457
|
|
|
365
|
-
The
|
|
458
|
+
The ten business-cookbook scenarios. Each is anchored to a green
|
|
366
459
|
end-to-end vitest scenario in the platform's integration-tests suite and is
|
|
367
460
|
verified at dev time by `make validate-cookbook` at the platform-sdk repo root.
|
|
368
|
-
|
|
369
|
-
|
|
461
|
+
This index is mirrored into the managed block `alvera llm-export` writes to
|
|
462
|
+
consumer `AGENTS.md` files (`buildManagedBlock` in both the SDK and CLI
|
|
463
|
+
packages — kept in sync by hand; the llm-export test suites pin the shipped
|
|
464
|
+
copy, so a dropped entry breaks a test).
|
|
370
465
|
|
|
371
466
|
<!-- BEGIN:cookbook-index -->
|
|
372
467
|
|
|
@@ -380,7 +475,7 @@ into consumer `<cwd>/AGENTS.md` files as a managed block.
|
|
|
380
475
|
buckets (appointment / job-application / spam) via an LLM
|
|
381
476
|
agent, route each to a tailored SMS action.
|
|
382
477
|
|
|
383
|
-
**
|
|
478
|
+
**Subscription**
|
|
384
479
|
|
|
385
480
|
- [welcome-sms-for-customers](./cookbook/welcome-sms-for-customers.md)
|
|
386
481
|
— Send a welcome SMS to newly contracted customers with a
|
|
@@ -394,7 +489,7 @@ into consumer `<cwd>/AGENTS.md` files as a managed block.
|
|
|
394
489
|
(high / medium / low) via an LLM agent, route each band to a
|
|
395
490
|
tailored SMS action.
|
|
396
491
|
|
|
397
|
-
**
|
|
492
|
+
**Payments**
|
|
398
493
|
|
|
399
494
|
- [kyc-notification-on-account-activation](./cookbook/kyc-notification-on-account-activation.md)
|
|
400
495
|
— Send a KYC-notification SMS when a payment account
|
|
@@ -413,6 +508,11 @@ into consumer `<cwd>/AGENTS.md` files as a managed block.
|
|
|
413
508
|
— Score inbound leads into four bands
|
|
414
509
|
(hot / warm / cold / spam) via an LLM agent, route each band
|
|
415
510
|
to a tailored SMS action.
|
|
511
|
+
- [marketing-campaign-send](./cookbook/marketing-campaign-send.md)
|
|
512
|
+
— Send an A/B marketing campaign across SMS and email: the
|
|
513
|
+
suppression and reachability gates and the A/B split all live
|
|
514
|
+
in the workflow, each send carries a per-recipient short link,
|
|
515
|
+
and the reply re-attaches to the customer who sent it.
|
|
416
516
|
|
|
417
517
|
<!-- END:cookbook-index -->
|
|
418
518
|
|
|
@@ -430,8 +530,8 @@ full wire shape.
|
|
|
430
530
|
document/image and pull structured JSON out of it (`aiAgents.invoke` with
|
|
431
531
|
files). The correct file-vision path — the agent reads the file; the DAC
|
|
432
532
|
ingests it.
|
|
433
|
-
- [
|
|
434
|
-
built-in datasets don't model; deploy → ingest →
|
|
533
|
+
- [generic-tables](./cookbook/generic-tables.md) — stand up a generic table the
|
|
534
|
+
built-in datasets don't model; deploy → ingest → read back via executeSql.
|
|
435
535
|
- [action-status-updaters](./cookbook/action-status-updaters.md) — reconcile
|
|
436
536
|
the delivery status of messages you send, on a schedule.
|
|
437
537
|
- [system-templates](./cookbook/system-templates.md) — discover the platform's
|
|
@@ -447,8 +547,10 @@ full wire shape.
|
|
|
447
547
|
|
|
448
548
|
Cross-resource utilities not documented in this corpus:
|
|
449
549
|
|
|
450
|
-
api.
|
|
451
|
-
|
|
550
|
+
api.admin signUp + confirmUser + createTenantApiKey
|
|
551
|
+
(dev / integration-test fixtures)
|
|
552
|
+
api.sessions verify; top-level createBootstrapSession /
|
|
553
|
+
createSession / revokeSession mint + revoke Bearers
|
|
452
554
|
api.tenants create + tenant lifecycle
|
|
453
555
|
api.datasets cross-resource search / metadata /
|
|
454
556
|
user-saved searches
|
|
@@ -15,14 +15,14 @@ together:
|
|
|
15
15
|
|
|
16
16
|
| Namespace | Methods |
|
|
17
17
|
|--------------------|-----------------------------------------------|
|
|
18
|
-
| `api.
|
|
19
|
-
| `api.admin` | `confirmUser` — **dev fixture only** |
|
|
18
|
+
| `api.admin` | `signUp`, `confirmUser`, `createTenantApiKey` — **dev / integration-test fixtures only** |
|
|
20
19
|
| `api.sessions` | `verify` |
|
|
21
20
|
| `api.tenants` | `create`, `list` |
|
|
22
21
|
| `api.invitations` | `create`, `list`, `accept` |
|
|
23
22
|
| `api.ping` | unauthenticated health check (`GET /api/ping`) |
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
23
|
+
| `createBootstrapSession` | mint a **tenantless** Bearer (top-level helper, not a namespace) |
|
|
24
|
+
| `createSession` | mint a **tenant-scoped** Bearer (top-level helper, not a namespace) |
|
|
25
|
+
| `revokeSession` | revoke the current Bearer (top-level helper, parallel to the two minters) |
|
|
26
26
|
|
|
27
27
|
Two top-level concerns sit above the surface:
|
|
28
28
|
|
|
@@ -39,36 +39,100 @@ The Bearer carries the trust-boundary coordinates and travels in
|
|
|
39
39
|
the `Authorization: Bearer <token>` header on every subsequent
|
|
40
40
|
request.
|
|
41
41
|
|
|
42
|
-
### Three
|
|
42
|
+
### Three scopes, two minters
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Sessions come in three scopes — **root**, **tenantless**, and
|
|
45
|
+
**tenant-scoped** — but there are only **two** functions that mint a
|
|
46
|
+
Bearer. `createBootstrapSession` mints the keyless tenantless Bearer
|
|
47
|
+
(root is a tenantless Bearer that happens to belong to a platform-admin
|
|
48
|
+
user); `createSession` mints the tenant-scoped Bearer.
|
|
45
49
|
|
|
46
50
|
```typescript
|
|
47
|
-
import { createSession } from '@alvera-ai/platform-sdk'
|
|
51
|
+
import { createBootstrapSession, createSession } from '@alvera-ai/platform-sdk'
|
|
48
52
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// tenantless — POST /api/v1/admin/bootstrap-session. Keyless by structural
|
|
54
|
+
// necessity (it mints the very first Bearer of an environment, before any
|
|
55
|
+
// tenant key exists). Returned tenant + role are ALWAYS null. This is an
|
|
56
|
+
// integration-test / dev-only route — absent from prod builds.
|
|
57
|
+
const tenantless = await createBootstrapSession({
|
|
58
|
+
baseUrl, email, password,
|
|
52
59
|
})
|
|
60
|
+
// tenantless.tenant === null && tenantless.role === null
|
|
53
61
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
// root — the SAME call, with a platform-admin user's credentials. The
|
|
63
|
+
// admin capability comes from the user, not a different function. Use it
|
|
64
|
+
// for admin.signUp / admin.confirmUser / admin.createTenantApiKey.
|
|
65
|
+
const root = await createBootstrapSession({
|
|
66
|
+
baseUrl, email: rootEmail, password: rootPassword,
|
|
57
67
|
})
|
|
58
|
-
// tenantless.tenant === null
|
|
59
68
|
|
|
60
|
-
// tenant-scoped — the canonical Bearer for tenant
|
|
69
|
+
// tenant-scoped — POST /api/v1/sessions, the canonical Bearer for tenant
|
|
70
|
+
// operations. REQUIRES both tenantSlug AND a resolvable tenant publishable
|
|
71
|
+
// key (sent as X-API-Key): the server 401s with "X-API-Key required to
|
|
72
|
+
// sign in to a tenant" without one, and 403s a key belonging to a different
|
|
73
|
+
// tenant. The minted Bearer inherits the origin policy of the key that
|
|
74
|
+
// minted it, and that same key must accompany every subsequent request.
|
|
61
75
|
const tenant = await createSession({
|
|
62
|
-
baseUrl, email, password, tenantSlug,
|
|
76
|
+
baseUrl, email, password, tenantSlug, apiKey: tenantApiKey,
|
|
63
77
|
})
|
|
64
78
|
// tenant.tenant.slug === tenantSlug
|
|
65
79
|
// tenant.role.name === 'tenant_admin' | 'member' | ...
|
|
66
80
|
```
|
|
67
81
|
|
|
82
|
+
The tenant API key is minted by a tenant admin in the platform's
|
|
83
|
+
API-keys page (Settings → API Keys), or programmatically via the
|
|
84
|
+
platform-admin side door (a root Bearer):
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const { data: minted } = await rootApi.admin.createTenantApiKey(tenantSlug, {
|
|
88
|
+
name: 'Bootstrap Key',
|
|
89
|
+
data_access_mode: 'unregulated',
|
|
90
|
+
})
|
|
91
|
+
// minted.api_key — the publishable key value (thread into createSession)
|
|
92
|
+
```
|
|
93
|
+
|
|
68
94
|
See `AGENTS.md` "Sessions come in three scopes" for when each is
|
|
69
95
|
appropriate. Tenantless is a transient state used once: to create
|
|
70
96
|
a tenant (creator path) or accept an invitation (invitee path).
|
|
71
97
|
|
|
98
|
+
**Scope discipline for app builders:** root and tenantless sessions
|
|
99
|
+
are platform-UI / admin-bootstrap surfaces (signup, confirmation,
|
|
100
|
+
tenant creation, test setup) — never ship them inside an app. An
|
|
101
|
+
app authenticates **tenant-scoped against exactly one tenant**,
|
|
102
|
+
with that tenant's publishable key; connected apps in particular
|
|
103
|
+
are single-tenant by construction (`connected_apps.md`).
|
|
104
|
+
|
|
105
|
+
### Key-only clients — machine-to-machine on the publishable key
|
|
106
|
+
|
|
107
|
+
`sessionToken` is optional on the client config. A deployed
|
|
108
|
+
connected app that authenticates purely on its tenant's publishable
|
|
109
|
+
key constructs with just the key — no session mint, no fabricated
|
|
110
|
+
empty token:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const api = createIsolatedPlatformApi({ baseUrl, apiKey })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Such a client sends `X-API-Key` and **no `Authorization` header**,
|
|
117
|
+
and reaches exactly the `public_api` (publishable-key) allowlist —
|
|
118
|
+
five calls:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
api.connectedApps.resolvePage mint/read a delivered page
|
|
122
|
+
api.connectedApps.updateMessageTracking opened_at / form_submitted_at
|
|
123
|
+
api.dataActivationClients.ingest submit rows
|
|
124
|
+
api.dataActivationClients.ingestFile submit a file
|
|
125
|
+
api.mdm.verify resolve an identity
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
(The same allowlist `connected_apps.md` documents — that guide owns
|
|
129
|
+
the runtime-call details.) Every call outside these five returns
|
|
130
|
+
401; that ceiling is the point, not an error to work around.
|
|
131
|
+
Minting a session and passing `sessionToken` unlocks the full
|
|
132
|
+
tenant-scoped API exactly as before — the two credentials are
|
|
133
|
+
complementary (the key identifies the client; the Bearer
|
|
134
|
+
authorizes), never substitutes.
|
|
135
|
+
|
|
72
136
|
### Explicit teardown — `revokeSession`
|
|
73
137
|
|
|
74
138
|
The inverse of `createSession`. Revokes the current Bearer on
|
|
@@ -110,7 +174,9 @@ local server before authentication failures muddy the diagnostic.
|
|
|
110
174
|
### Sign up
|
|
111
175
|
|
|
112
176
|
```typescript
|
|
113
|
-
|
|
177
|
+
// signUp lives under the admin namespace (there is no `api.auth`); it is an
|
|
178
|
+
// integration-test fixture, reachable only from a root Bearer.
|
|
179
|
+
const { data: user } = await rootApi.admin.signUp({
|
|
114
180
|
email: 'sarah@example.com',
|
|
115
181
|
password: '...',
|
|
116
182
|
first_name: 'Sarah',
|
|
@@ -180,11 +246,11 @@ to many tenants; a Bearer is scoped to exactly one.
|
|
|
180
246
|
```
|
|
181
247
|
┌─ creator path ─┐ ┌─ invitee path ─┐
|
|
182
248
|
│ │
|
|
183
|
-
|
|
249
|
+
admin.signUp admin.signUp
|
|
184
250
|
│ │
|
|
185
251
|
api.admin.confirmUser* api.admin.confirmUser* (* production: email link)
|
|
186
252
|
│ │
|
|
187
|
-
|
|
253
|
+
createBootstrapSession createBootstrapSession
|
|
188
254
|
│ │
|
|
189
255
|
api.tenants.create api.invitations.list → .accept
|
|
190
256
|
│ │
|
|
@@ -203,26 +269,30 @@ role specified in their invitation.
|
|
|
203
269
|
const { data } = await tenantlessApi.tenants.create({
|
|
204
270
|
name: 'Acme Health',
|
|
205
271
|
})
|
|
206
|
-
// data.
|
|
207
|
-
// data.session_token — see "Gotchas" below
|
|
272
|
+
// data.id, data.slug, data.name — server-derived (flat TenantResponse)
|
|
208
273
|
```
|
|
209
274
|
|
|
210
275
|
The slug is server-derived from `name` per the universal slug
|
|
211
276
|
rules (see `type_naming.md` "Never pre-compute the slug
|
|
212
277
|
client-side"). Use the returned slug for downstream calls.
|
|
213
278
|
|
|
214
|
-
**Gotcha**:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
279
|
+
**Gotcha**: `tenants.create` returns only the tenant — a flat
|
|
280
|
+
`TenantResponse` (`id`, `slug`, `name`, `description`), **not** a Bearer
|
|
281
|
+
and with no `session_token`. The caller's tenantless Bearer stays valid,
|
|
282
|
+
but it is still tenantless. To act on the new tenant, mint a tenant-scoped
|
|
283
|
+
Bearer via `createSession({ ..., tenantSlug, apiKey })` — which is also
|
|
284
|
+
where the mandatory `X-API-Key` first enters the flow. (The tenant's
|
|
285
|
+
default `public_api` key is auto-minted at creation, but its plaintext is
|
|
286
|
+
not returned here — read it from Settings → API Keys, or mint a fresh key
|
|
287
|
+
via `admin.createTenantApiKey`.)
|
|
220
288
|
|
|
221
289
|
### `api.tenants.list`
|
|
222
290
|
|
|
223
291
|
```typescript
|
|
224
292
|
const { data: tenants } = await api.tenants.list()
|
|
225
293
|
// tenants.data: TenantResponse[] — every tenant the current Bearer can see
|
|
294
|
+
// takes an optional Flop query (page/page_size/order_by/filters —
|
|
295
|
+
// see type_naming.md "List response types")
|
|
226
296
|
```
|
|
227
297
|
|
|
228
298
|
A root Bearer sees every tenant; a tenant-scoped Bearer typically
|
|
@@ -271,13 +341,13 @@ the invitation enum:
|
|
|
271
341
|
|-------------------------|------------------------------------|--------------|
|
|
272
342
|
| `'tenant_admin'` | Full tenant management | invitation `'admin'` |
|
|
273
343
|
| `'member'` | Standard tenant user | invitation `'member'` |
|
|
274
|
-
| `'
|
|
344
|
+
| `'public_api'` | Lowest-privilege publishable key (Stripe `pk_` analogue); the mandatory `X-API-Key` companion | provisioned, not invited |
|
|
275
345
|
| `'datalake_admin'` | Full datalake management (requires `datalake_id` on session) | console-mediated |
|
|
276
346
|
| `'researcher'` | Tokenized-only datalake access | invitation `'researcher'` |
|
|
277
347
|
| `'datalake_api'` | Programmatic datalake access | provisioned, not invited |
|
|
278
348
|
|
|
279
349
|
```typescript
|
|
280
|
-
const session = await createSession({ baseUrl, email, password, tenantSlug })
|
|
350
|
+
const session = await createSession({ baseUrl, email, password, tenantSlug, apiKey: tenantApiKey })
|
|
281
351
|
// session.role.name === 'tenant_admin' if accepted from a role: 'admin' invitation
|
|
282
352
|
// session.role.name === 'member' if accepted from a role: 'member' invitation
|
|
283
353
|
// session.role.name === 'researcher' if accepted from a role: 'researcher' invitation
|
|
@@ -327,7 +397,7 @@ const mine = invites.data.find(i => i.tenant.slug === tenantSlug)
|
|
|
327
397
|
const { data: membership } = await tenantlessApi.invitations.accept(mine.id)
|
|
328
398
|
|
|
329
399
|
// Then mint a tenant-scoped Bearer
|
|
330
|
-
const session = await createSession({ baseUrl, email, password, tenantSlug })
|
|
400
|
+
const session = await createSession({ baseUrl, email, password, tenantSlug, apiKey: tenantApiKey })
|
|
331
401
|
// session.role.name === 'member'
|
|
332
402
|
```
|
|
333
403
|
|
|
@@ -399,16 +469,18 @@ documented in `errors.md`. Common rejections:
|
|
|
399
469
|
|
|
400
470
|
| `source.pointer` / shape | Cause |
|
|
401
471
|
|-------------------------------------------------|----------------------------------------------|
|
|
402
|
-
| `/email` "has already been taken" | `
|
|
472
|
+
| `/email` "has already been taken" | `admin.signUp` with an existing email |
|
|
403
473
|
| `/password` "is too short" | password below minimum length |
|
|
404
474
|
| top-level 401 (no envelope) | Bearer missing, expired, or revoked |
|
|
405
475
|
| top-level 403 (no envelope) | role lacks permission for the action |
|
|
406
476
|
| `/email` "already invited" | duplicate `invitations.create` |
|
|
407
477
|
| `/tenant_slug` "not found" | tenant slug doesn't exist or invisible to Bearer |
|
|
408
478
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
479
|
+
A `tenants.create` `/name` uniqueness collision comes back as a
|
|
480
|
+
**409 Conflict** (not 422 — uniqueness constraints route to 409; see
|
|
481
|
+
`errors.md`) — names must be unique platform-wide. Re-attempt with a
|
|
482
|
+
different name or treat as "already exists" via `api.tenants.list`
|
|
483
|
+
lookup.
|
|
412
484
|
|
|
413
485
|
## 6. Gotchas
|
|
414
486
|
|
|
@@ -417,10 +489,10 @@ treat as "already exists" via `api.tenants.list` lookup.
|
|
|
417
489
|
the SDK method as a test fixture; do not ship code that calls
|
|
418
490
|
it from a customer-facing path.
|
|
419
491
|
|
|
420
|
-
2.
|
|
421
|
-
|
|
422
|
-
`
|
|
423
|
-
|
|
492
|
+
2. **`tenants.create` returns a tenant, not a Bearer.** The response is
|
|
493
|
+
a flat `TenantResponse` (`id`, `slug`, `name`, `description`) with no
|
|
494
|
+
`session_token`. After creating a tenant, mint a tenant-scoped Bearer
|
|
495
|
+
via `createSession({ ..., tenantSlug, apiKey })` to act on it.
|
|
424
496
|
|
|
425
497
|
3. **Tenantless Bearer is a transient state.** Only two
|
|
426
498
|
operations are useful with it: `tenants.create` (creator path)
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
context_key — companion string key; like decision_key, it is
|
|
41
|
+
SQL-lane only (neither is Flop-filterable, §5)
|
|
42
|
+
mdm_subject_id, subject_name, session_id
|
|
43
|
+
action_type ('sms'|'email'|'voice'|'data_exchange'), channel,
|
|
44
|
+
classification
|
|
45
|
+
status — see §3
|
|
46
|
+
external_id — provider id; joins to messages.external_id
|
|
47
|
+
sent_at, tool_call_payload, idempotency_key, metadata,
|
|
48
|
+
attachments, body, sender_tool_id
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 3. Statuses
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pending → queued → sent → delivered
|
|
55
|
+
└──→ failed
|
|
56
|
+
|
|
57
|
+
skipped — the action's runtime_filter said no; nothing sent
|
|
58
|
+
filtered — the parent run was filtered out; nothing sent
|
|
59
|
+
cancelled — manually cancelled
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Initial status mirrors the action execution log's outcome; a
|
|
63
|
+
status updater with an `action_log_config` can move
|
|
64
|
+
`sent → delivered/failed` here too (optional — many setups only
|
|
65
|
+
reconcile messages).
|
|
66
|
+
|
|
67
|
+
## 4. How everything joins
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
run (batch_id) ──── WorkflowRunLog "how is the batch doing"
|
|
71
|
+
│
|
|
72
|
+
├── WorkflowExecutionLog (per subject) "what did the pipeline decide"
|
|
73
|
+
│ │
|
|
74
|
+
│ └── ActionExecutionLog "what did each action attempt"
|
|
75
|
+
│ │ (live + actually executed)
|
|
76
|
+
│ ▼
|
|
77
|
+
└────────── action_logs row "the outcome, queryable"
|
|
78
|
+
│ external_id
|
|
79
|
+
▼
|
|
80
|
+
messages row "the communication + delivery"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Left side lives in the platform DB (read via
|
|
84
|
+
`api.workflows.batchLogs` / `api.workflows.workflowLogs`); the
|
|
85
|
+
bottom two are datalake datasets. `batch_id` ties the datalake
|
|
86
|
+
rows back to the run; `external_id` ties an action_log to its
|
|
87
|
+
message.
|
|
88
|
+
|
|
89
|
+
## 5. Reading + audience export
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// dataset type is SINGULAR 'action_log' (the table is plural)
|
|
93
|
+
const { data: search } = await api.datasets.createUserSearch(
|
|
94
|
+
tenantSlug, datalakeSlug, 'action_log',
|
|
95
|
+
{ search_query: `ral.batch_id = '${batchId}'` },
|
|
96
|
+
)
|
|
97
|
+
const { data: page } = await api.datasets.search(
|
|
98
|
+
tenantSlug, datalakeSlug, 'action_log',
|
|
99
|
+
{ userSearchId: search.id, dataAccessMode: 'unregulated' },
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Filterable columns (Flop, the full set): `id`, `inserted_at`,
|
|
104
|
+
`action_type`, `channel`, `status`, `batch_id`, `classification`,
|
|
105
|
+
`workflow_id`, `sent_at`, `external_id`, `mdm_subject_id`. Note
|
|
106
|
+
there is **no `global_search`** here (unlike the message dataset) —
|
|
107
|
+
`decision_key`/`context_key` are not in this set either; scope them
|
|
108
|
+
via the `search_query` SQL fragment above.
|
|
109
|
+
|
|
110
|
+
"Who did this campaign go to" = the query above (subject columns
|
|
111
|
+
+ status per row — exportable as-is). For bulk export the platform
|
|
112
|
+
also merges each finished batch into NDJSON artifacts in cold
|
|
113
|
+
storage:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
<datalake>/workflows/<wf_slug>/<batch_id>/merged.ndjson (per-subject)
|
|
117
|
+
<datalake>/workflows/<wf_slug>/<batch_id>/<decision_key>/merged.ndjson (per-action payloads)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The batch log row (`api.workflows.batchLogs.get`) carries the
|
|
121
|
+
download references once the batch completes.
|
|
122
|
+
|
|
123
|
+
## 6. Gotchas
|
|
124
|
+
|
|
125
|
+
1. **The scheduled gap (§1).** No action_log row ≠ nothing
|
|
126
|
+
happened. Check the execution logs before declaring a run
|
|
127
|
+
empty.
|
|
128
|
+
|
|
129
|
+
2. **`batch_id` is a string, not a UUID** — manual runs look like
|
|
130
|
+
`manual:<uuid>`; DAC-triggered runs carry the ingest batch id.
|
|
131
|
+
Compare as strings.
|
|
132
|
+
|
|
133
|
+
3. **One subject can have many rows for the same decision.**
|
|
134
|
+
Re-fires via `workflows.execute` with `manual_override: true`
|
|
135
|
+
(the retry path) create a NEW action_log each time. Latest row
|
|
136
|
+
wins for "current state"; the full set is the audit trail.
|
|
137
|
+
|
|
138
|
+
4. **`skipped`/`filtered` rows are successes of the control
|
|
139
|
+
system, not failures.** Only `failed` is the retry surface
|
|
140
|
+
(`workflows.md` §8).
|
|
141
|
+
|
|
142
|
+
5. **`tool_call_payload` is the rendered call** — what was
|
|
143
|
+
actually sent to the tool, after Liquid. When a message looks
|
|
144
|
+
wrong, read this before blaming the template engine.
|