@alvera-ai/platform-sdk 0.12.1 → 0.13.0-next.gf3d2a4a

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.
Files changed (55) hide show
  1. package/.agent/AGENTS.md +121 -19
  2. package/.agent/account_management.md +79 -38
  3. package/.agent/action_logs.md +144 -0
  4. package/.agent/action_status_updaters.md +237 -53
  5. package/.agent/ai_agents.md +149 -53
  6. package/.agent/ai_sandbox.md +11 -4
  7. package/.agent/async.md +16 -6
  8. package/.agent/connected_apps.md +116 -4
  9. package/.agent/cookbook/_fixtures/README.md +9 -9
  10. package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid → payments/_compliance_screenings_payments_compliance_screening.liquid} +1 -1
  11. package/.agent/cookbook/_fixtures/{payment_risk/_compliance_screenings_payment_risk_mdm.liquid → payments/_compliance_screenings_payments_mdm.liquid} +1 -1
  12. package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_mdm.liquid → payments/_payment_accounts_payments_mdm.liquid} +1 -1
  13. package/.agent/cookbook/_fixtures/{payment_risk/_payment_accounts_payment_risk_payment_account.liquid → payments/_payment_accounts_payments_payment_account.liquid} +1 -1
  14. package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_customer.liquid → subscription/_customers_subscription_customer.liquid} +1 -1
  15. package/.agent/cookbook/_fixtures/{accounts_receivable/_customers_accounts_receivable_mdm.liquid → subscription/_customers_subscription_mdm.liquid} +1 -1
  16. package/.agent/cookbook/_setup/foundation.md +24 -4
  17. package/.agent/cookbook/_setup/healthcare.md +24 -4
  18. package/.agent/cookbook/_setup/{payment_risk.md → payments.md} +51 -31
  19. package/.agent/cookbook/_setup/{accounts_receivable.md → subscription.md} +51 -31
  20. package/.agent/cookbook/action-status-updaters.md +25 -18
  21. package/.agent/cookbook/bulk-ingest.md +15 -15
  22. package/.agent/cookbook/dunning-sms-for-delinquent.md +21 -21
  23. package/.agent/cookbook/{custom-tables.md → generic-tables.md} +44 -41
  24. package/.agent/cookbook/invite-team.md +19 -13
  25. package/.agent/cookbook/kyc-notification-on-account-activation.md +26 -26
  26. package/.agent/cookbook/marketing-campaign-send.md +1003 -0
  27. package/.agent/cookbook/rest-fetch.md +9 -9
  28. package/.agent/cookbook/sanctions-screening-with-agent-review.md +22 -22
  29. package/.agent/cookbook/system-templates.md +15 -15
  30. package/.agent/cookbook/triage-prospects-by-priority.md +17 -17
  31. package/.agent/cookbook/welcome-sms-for-customers.md +21 -21
  32. package/.agent/data_activation_clients.md +177 -42
  33. package/.agent/data_sources.md +55 -37
  34. package/.agent/datalakes.md +30 -17
  35. package/.agent/debugging.md +48 -14
  36. package/.agent/errors.md +39 -17
  37. package/.agent/generic_tables.md +150 -51
  38. package/.agent/interoperability_contracts.md +114 -15
  39. package/.agent/mdm.md +171 -93
  40. package/.agent/messages.md +252 -0
  41. package/.agent/templates.md +21 -4
  42. package/.agent/tool-call-configs.md +1 -1
  43. package/.agent/tools.md +150 -60
  44. package/.agent/type_naming.md +79 -18
  45. package/.agent/workflows.md +408 -62
  46. package/LICENSE +21 -93
  47. package/README.md +2 -2
  48. package/dist/bin/platform-sdk.mjs +53 -1
  49. package/dist/bin/platform-sdk.mjs.map +1 -1
  50. package/dist/index.d.mts +582 -66
  51. package/dist/index.d.mts.map +1 -1
  52. package/dist/index.mjs +654 -105
  53. package/dist/index.mjs.map +1 -1
  54. package/package.json +10 -6
  55. /package/.agent/cookbook/_fixtures/{accounts_receivable → subscription}/stripe_customers_batch1.csv +0 -0
@@ -72,9 +72,9 @@ Liquid's stock tag set (`{% if %}`, `{% for %}`, `{% assign %}`,
72
72
  `{% capture %}` writes to a Liquid-scope variable; it is not a
73
73
  runtime eval escape hatch.
74
74
 
75
- ### The 11-filter custom allowlist
75
+ ### The 13-filter custom allowlist
76
76
 
77
- Beyond stock Liquid, the platform exposes exactly **eleven**
77
+ Beyond stock Liquid, the platform exposes exactly **thirteen**
78
78
  custom filters. Adding new ones requires a platform release —
79
79
  consumers cannot extend the set:
80
80
 
@@ -101,6 +101,13 @@ consumers cannot extend the set:
101
101
  now Current UTC datetime, ISO 8601.
102
102
  Example: {{ '' | now }} → 2026-03-15T14:30:00Z
103
103
 
104
+ minutes_ago Unix ms N minutes before a unix-ms input. Bounds
105
+ the start of an ActionStatusUpdater poll window.
106
+ Example: {{ now | minutes_ago: 45 }} → 1773585000000
107
+
108
+ hours_ago Unix ms N hours before a unix-ms input.
109
+ Example: {{ now | hours_ago: 2 }} → 1773578400000
110
+
104
111
  uuid Generate a new v4 UUID.
105
112
  Example: {{ '' | uuid }} → a1b2c3d4-…
106
113
 
@@ -218,7 +225,7 @@ it sits inside the same Layer-3 boundary:
218
225
  │
219
226
  ┌────────────────────▼────────────────────────┐
220
227
  │ Layer 2 — Liquid sandbox │
221
- │ 11-filter allowlist │
228
+ │ 13-filter allowlist │
222
229
  │ + parse-on-write │
223
230
  │ + render-time output_schema check │
224
231
  │ Decides: what the consumer-authored body │
@@ -260,7 +267,7 @@ guarantees it captures back-stop Layer 1.
260
267
  `'regulated'` only when the action genuinely needs raw
261
268
  values, and pair with a session role that's authorised.
262
269
 
263
- 2. **Use only the 11 custom filters** named in Layer 2. Bodies
270
+ 2. **Use only the 13 custom filters** named in Layer 2. Bodies
264
271
  that reference other filter names will silently fall through
265
272
  to stock Liquid behavior and produce malformed output.
266
273
 
package/.agent/async.md CHANGED
@@ -40,10 +40,16 @@ resource-specific. Common examples:
40
40
  | Resource | Terminal success | Terminal failure |
41
41
  |-----------------------------------|------------------|-------------------------|
42
42
  | Datalake (create + migrate) | `ready` | — (no terminal failure) |
43
- | Data Activation Client run | `success` | `error` |
44
- | AWS Lambda tool deploy | `deployed` | `failed` |
43
+ | AWS Lambda tool deploy | `create_complete`| `create_failed` |
45
44
  | Workflow run | `completed` | `failed` |
46
45
 
46
+ **A data-activation-client run is NOT on this list — it is not a
47
+ `waitUntilReady` status-poll target.** `.ingest` / `.ingestFile` /
48
+ `.runManually` return a `202` + a `batch_id` (not a saved row and not
49
+ a pollable status enum); progress is tracked by diffing the DAC's log
50
+ and dataset rows by that `batch_id`, not by polling a `success`/`error`
51
+ status. See `data_activation_clients.md` §6.
52
+
47
53
  The Datalake row is the odd one: its top-level `status` enum is
48
54
  `new | processing | ready` only — there is no terminal failure
49
55
  value. A migration that fails leaves the datalake at
@@ -59,7 +65,10 @@ CloudFormation deploy status nested under the polymorphic
59
65
  `body`), not on the top-level `tool.status` enum (which is
60
66
  `draft | active | inactive | error | marked_for_deletion`).
61
67
  `tool.status` flips to `'active'` after a successful deploy
62
- but `stack_status` is the more precise readiness signal — see
68
+ but `stack_status` is the more precise readiness signal: its
69
+ terminal-success value is the CloudFormation `create_complete`
70
+ (`update_complete` on a redeploy), and a failed deploy lands on
71
+ a `*_failed` state (`create_failed`, `rollback_failed`, …) — see
63
72
  `tools.md` §2 for the per-body field set.
64
73
 
65
74
  Each resource's MD §5 (Lifecycle) names its terminal-success
@@ -79,9 +88,10 @@ the caller.
79
88
  ## Idempotency on create
80
89
 
81
90
  `create()` calls are NOT idempotent by default. Repeating a
82
- `create()` with the same body yields a 422 on uniqueness fields
83
- (typically the name within scope). Consumers handling retry
84
- should:
91
+ `create()` with the same body yields a **409 Conflict** on
92
+ uniqueness fields (typically the name within scope — uniqueness
93
+ collisions are 409, not 422; see `errors.md`). Consumers handling
94
+ retry should:
85
95
 
86
96
  1. Catch the thrown error after the call returns (see `errors.md`).
87
97
  2. Inspect for a uniqueness `detail` on a `source.pointer`
@@ -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
 
@@ -79,6 +88,15 @@ or multiple primaries returns a 422 on `/urls`.
79
88
  Additional URLs (staging, preview) are stored for reference but
80
89
  aren't used for route discovery or link minting.
81
90
 
91
+ **`urls` models environments, NOT tracking domains.** The list is
92
+ production / staging / preview deployments of the *same* app — and the
93
+ one flagged `is_primary` silently becomes the domain of **every** magic
94
+ link this app mints (see §6). It is NOT a way to serve links from
95
+ several tracking domains: to mint links on two different domains you
96
+ need **two connected apps**, one per domain. An agent that sees
97
+ `urls: [...]` and reaches for "add another tracking domain here" gets
98
+ this wrong — reach for a second connected app instead.
99
+
82
100
  ### Route discovery is automatic + re-triggerable
83
101
 
84
102
  ```
@@ -219,7 +237,8 @@ and can be retried via `.syncRoutes`.
219
237
  ### Read shapes — management
220
238
 
221
239
  ```
222
- .list(tenantSlug, datalakeSlug) Paged: { data, meta }
240
+ .list(tenantSlug, datalakeSlug, query?) Paged: { data, meta }
241
+ (full Flop query — type_naming.md)
223
242
  .get(tenantSlug, datalakeSlug, id) One row, by UUID
224
243
  .metadata(tenantSlug, datalakeSlug) Markdown catalog
225
244
  .metadataDetails(tenantSlug, datalakeSlug, Markdown for one app
@@ -246,6 +265,13 @@ lands `status: 'synced'` (or `'error'`) on the app row asynchronously.
246
265
  Poll `.get()` for the outcome. Use it after deploying a new manifest,
247
266
  fixing a primary URL outage, or recovering from `status: 'error'`.
248
267
 
268
+ **Route-sync is the only "does this domain work" check the platform
269
+ performs — there is NO DNS verification.** The platform never resolves
270
+ or validates the primary URL's DNS/ownership; it just attempts the
271
+ HTTP fetch of `/.well-known/routes.json`. A `status: 'synced'` means
272
+ "the manifest was fetched and parsed", not "this domain is verified as
273
+ yours". Domain trust is your responsibility, not a platform gate.
274
+
249
275
  Discovery failure modes the platform handles explicitly — each
250
276
  leaves the row at `status: 'error'` with `error` populated:
251
277
 
@@ -294,7 +320,62 @@ Delete latency depends on the app's `mode`:
294
320
  `'deploying'` (during teardown) before disappearing. Poll
295
321
  `.get()` until 404 to confirm removal.
296
322
 
297
- ## 6. Runtime: how magic links resolve
323
+ ## 6. How a tracked link is minted
324
+
325
+ Before the runtime resolve flow, understand what the platform does at
326
+ fire time to produce the link. When a workflow action that carries a
327
+ `connected_app_id` executes, the platform mints a **page token** and
328
+ injects its URL into the message body:
329
+
330
+ ```
331
+ action fires
332
+ │ action.connected_app_id + connected_app_route
333
+ │ + rendered connected_app_metadata_template (Liquid → JSON)
334
+ ▼
335
+ page token row ── keyed on the MDM SUBJECT id + the route,
336
+ │ carrying the rendered metadata JSON INSIDE it
337
+ ▼
338
+ {{ connected_app_form_url }} = https://<primary-url>/t/<url_hash>
339
+ │ (primary-url = the connected app's is_primary URL)
340
+ ▼
341
+ message row created with the link in its body
342
+ │
343
+ ▼
344
+ page token backfilled with that message's id ← wires token → message
345
+ ```
346
+
347
+ Key consequences to design around:
348
+
349
+ - **The `connected_app_metadata_template` is how per-recipient data
350
+ reaches the app without hard-coding a URL.** It's a Liquid template
351
+ that renders to a JSON object, stored *inside* the page token; the
352
+ app reads it back at `resolvePage` time (as `page.metadata`) and uses
353
+ it to prefill / redirect that specific recipient. This is the
354
+ mechanism for "send each recipient to their own destination" — put
355
+ the per-recipient values in the metadata template, not in the URL.
356
+
357
+ - **No MDM subject ⇒ no page token ⇒ no link.** The page token
358
+ *requires* a subject id (it is keyed on the resolved MDM subject). If
359
+ the row didn't resolve to a subject, the mint fails — but **the send
360
+ still goes out**: the mint failure is a logged warning, not an action
361
+ failure, so `{{ connected_app_form_url }}` simply renders **empty**
362
+ and the recipient gets a message with a missing link. A
363
+ message-with-no-link is the symptom of a workflow whose
364
+ `skip_mdm_resolution` was left `true`, or whose row carries no entity
365
+ FK (see `workflows.md` §3). If your action mints links, the workflow
366
+ MUST resolve a subject.
367
+
368
+ - **Tracked links expire — 7 days by default.** The page token is
369
+ stamped with a TTL (604,800 s) at mint; `resolvePage` checks
370
+ `expires_at` and an expired token fails to resolve (HTTP 410).
371
+ "How long does a campaign link stay live?" — a week, unless the
372
+ platform default changes. Long-lead campaigns must account for it.
373
+
374
+ The `{{ connected_app_form_url }}` variable is available only in action
375
+ bodies whose action has a `connected_app_id`; it renders empty
376
+ otherwise.
377
+
378
+ ## 7. Runtime: how magic links resolve
298
379
 
299
380
  This section covers the runtime callers — the connected app
300
381
  deployment itself, not the admin.
@@ -343,7 +424,16 @@ await api.connectedApps.updateMessageTracking(
343
424
 
344
425
  Both calls are authenticated with the connected app's
345
426
  auto-provisioned machine-to-machine API key (typically configured as
346
- `ALVERA_API_KEY` in the deployment environment).
427
+ `ALVERA_API_KEY` in the deployment environment). Both are also on
428
+ the `public_api` (publishable-key) allowlist — alongside `ingest`,
429
+ `ingest-file`, and `mdm/verify` — so a browser-side app holding
430
+ only a publishable key can call them.
431
+
432
+ **This is the engagement seam.** `updateMessageTracking` writes
433
+ `opened_at` / `form_submitted_at` onto the message row — the
434
+ open/click/conversion half of campaign analytics — and it NEVER
435
+ changes `status` (delivery truth belongs to the action status
436
+ updater; see `messages.md` §1 for who writes which field).
347
437
 
348
438
  ### Workflow-side wiring
349
439
 
@@ -362,7 +452,7 @@ connected_app_metadata_template string — Liquid template rendering a JSON
362
452
 
363
453
  See `workflows.md` §7 for the action-level field detail.
364
454
 
365
- ## 7. Gotchas
455
+ ## 8. Gotchas
366
456
 
367
457
  1. **Exactly one URL must be `is_primary: true`.** Zero or
368
458
  multiple primaries returns 422. The primary is what the
@@ -406,3 +496,25 @@ See `workflows.md` §7 for the action-level field detail.
406
496
  8. **Route manifests must be JSON arrays, not objects.** A
407
497
  common mistake is wrapping the routes in `{ "routes": [...] }`
408
498
  — the manifest's top-level must be the array itself.
499
+
500
+ 9. **No MDM subject ⇒ no link (but the message still sends).** Minting
501
+ a page token needs the resolved MDM subject; if the workflow row
502
+ didn't resolve one, the mint is skipped with a logged warning and
503
+ `{{ connected_app_form_url }}` renders empty — the recipient gets a
504
+ linkless message, not an error. If an action mints links, the
505
+ workflow must resolve a subject (`skip_mdm_resolution: false` + an
506
+ entity FK on the row; see `workflows.md` §3). See §6.
507
+
508
+ 10. **Tracked links expire (7-day default TTL).** A page token is valid
509
+ for 604,800 s from mint; `resolvePage` returns 410 for an expired
510
+ token. Account for it on long-lead campaigns.
511
+
512
+ 11. **`urls` are environments, not tracking domains.** The `is_primary`
513
+ URL becomes the domain of every minted link; the others are
514
+ reference-only. Multiple tracking domains = multiple connected apps
515
+ (see §2).
516
+
517
+ 12. **No DNS verification exists.** `status: 'synced'` means the
518
+ manifest was fetched and parsed, not that the domain is verified as
519
+ yours. Route-sync is the only reachability check the platform runs
520
+ (see §5).
@@ -88,19 +88,19 @@ The `contact-us-triage-with-llm` cookbook ingests into a generic
88
88
  table via the auto-provisioned identity contract, so it loads no
89
89
  vendored template.
90
90
 
91
- ### `accounts_receivable/`
91
+ ### `subscription/`
92
92
 
93
93
  | File | Consumed by cookbook(s) | Source-of-truth |
94
94
  |---|---|---|
95
- | `_customers_accounts_receivable_customer.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` | `priv/liquid_templates/data_activation/interoperability/accounts_receivable/stripe/_customers_accounts_receivable_customer.liquid` |
96
- | `_customers_accounts_receivable_mdm.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` | `priv/liquid_templates/data_activation/interoperability/accounts_receivable/stripe/_customers_accounts_receivable_mdm.liquid` |
97
- | `stripe_customers_batch1.csv` (data) | `bulk-ingest` | `integration-tests/tests/accounts_receivable/fixtures/csv/stripe_customers_batch1.csv` |
95
+ | `_customers_subscription_customer.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` | `priv/liquid_templates/data_activation/interoperability/subscription/stripe/_customers_subscription_customer.liquid` |
96
+ | `_customers_subscription_mdm.liquid` | `welcome-sms-for-customers`, `dunning-sms-for-delinquent`, `bulk-ingest`, `rest-fetch` | `priv/liquid_templates/data_activation/interoperability/subscription/stripe/_customers_subscription_mdm.liquid` |
97
+ | `stripe_customers_batch1.csv` (data) | `bulk-ingest` | `integration-tests/tests/subscription/fixtures/csv/stripe_customers_batch1.csv` |
98
98
 
99
- ### `payment_risk/`
99
+ ### `payments/`
100
100
 
101
101
  | File | Consumed by cookbook(s) | Source-of-truth |
102
102
  |---|---|---|
103
- | `_payment_accounts_payment_risk_payment_account.liquid` | `kyc-notification-on-account-activation` | `priv/liquid_templates/data_activation/interoperability/payment_risk/atomic_fi/_payment_accounts_payment_risk_payment_account.liquid` |
104
- | `_payment_accounts_payment_risk_mdm.liquid` | `kyc-notification-on-account-activation` | `priv/liquid_templates/data_activation/interoperability/payment_risk/atomic_fi/_payment_accounts_payment_risk_mdm.liquid` |
105
- | `_compliance_screenings_payment_risk_compliance_screening.liquid` | `sanctions-screening-with-agent-review` | `priv/liquid_templates/data_activation/interoperability/payment_risk/atomic_fi/_compliance_screenings_payment_risk_compliance_screening.liquid` |
106
- | `_compliance_screenings_payment_risk_mdm.liquid` | `sanctions-screening-with-agent-review` | `priv/liquid_templates/data_activation/interoperability/payment_risk/atomic_fi/_compliance_screenings_payment_risk_mdm.liquid` |
103
+ | `_payment_accounts_payments_payment_account.liquid` | `kyc-notification-on-account-activation` | `priv/liquid_templates/data_activation/interoperability/payments/atomic_fi/_payment_accounts_payments_payment_account.liquid` |
104
+ | `_payment_accounts_payments_mdm.liquid` | `kyc-notification-on-account-activation` | `priv/liquid_templates/data_activation/interoperability/payments/atomic_fi/_payment_accounts_payments_mdm.liquid` |
105
+ | `_compliance_screenings_payments_compliance_screening.liquid` | `sanctions-screening-with-agent-review` | `priv/liquid_templates/data_activation/interoperability/payments/atomic_fi/_compliance_screenings_payments_compliance_screening.liquid` |
106
+ | `_compliance_screenings_payments_mdm.liquid` | `sanctions-screening-with-agent-review` | `priv/liquid_templates/data_activation/interoperability/payments/atomic_fi/_compliance_screenings_payments_mdm.liquid` |
@@ -1,4 +1,4 @@
1
- {% comment %}Atomic FI ComplianceScreenings → PaymentRisk ComplianceScreening attrs
1
+ {% comment %}Atomic FI ComplianceScreenings → Payments ComplianceScreening attrs
2
2
 
3
3
  Source: GET /api/compliance-screenings on Atomic FI (paginated; the
4
4
  fetch pipeline extracts the `data` array via the response_extractor_key
@@ -1,4 +1,4 @@
1
- {% comment %}Atomic FI ComplianceScreenings → PaymentRisk MDM Input
1
+ {% comment %}Atomic FI ComplianceScreenings → Payments MDM Input
2
2
 
3
3
  A CS row from Atomic FI references an account_holder UUID
4
4
  (`p.account_holder_id`). MDM-resolution side uses that as the external
@@ -1,4 +1,4 @@
1
- {% comment %}Atomic FI PaymentAccount → PaymentRisk MDM Input
1
+ {% comment %}Atomic FI PaymentAccount → Payments MDM Input
2
2
 
3
3
  A PaymentAccount row from Atomic FI references an account_holder UUID
4
4
  (`p.account_holder_id`) — that's the canonical external identity used by
@@ -1,4 +1,4 @@
1
- {% comment %}Atomic FI PaymentAccount → PaymentRisk PaymentAccount attrs
1
+ {% comment %}Atomic FI PaymentAccount → Payments PaymentAccount attrs
2
2
 
3
3
  Source: GET /api/payment-accounts on Atomic FI (paginated; the fetch
4
4
  pipeline extracts the `data` array via the response_extractor_key and
@@ -1,4 +1,4 @@
1
- {% comment %}Stripe Customers → Accounts Receivable Customer attrs{% endcomment %}
1
+ {% comment %}Stripe Customers → Subscription Customer attrs{% endcomment %}
2
2
  {% assign p = msg %}
3
3
  {
4
4
  "customer_type": "{{ p.customer_type | default: "individual" | json_escape }}",
@@ -1,4 +1,4 @@
1
- {% comment %}Stripe Customers → Accounts Receivable MDM Input{% endcomment %}
1
+ {% comment %}Stripe Customers → Subscription MDM Input{% endcomment %}
2
2
  {% assign p = msg %}
3
3
  {
4
4
  "identifiers": [
@@ -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). Then sign up a per-run industry-admin
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 createSession({
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.auth.signUp({
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 createSession({
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.
@@ -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). Then sign up a per-run industry-admin
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 createSession({
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.auth.signUp({
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 @@ healthcare 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 createSession({
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
 
@@ -263,6 +280,9 @@ After this setup runs, the closure-scoped slots are populated as:
263
280
  - `ctx.sarahEmail` / `ctx.sarahPassword` — the per-run admin
264
281
  credentials (kept on `ctx` because no scenario cookbook needs
265
282
  to re-authenticate by default)
283
+ - `ctx.tenantApiKey` — the tenant's publishable API key (minted in
284
+ setup via `admin.createTenantApiKey`); thread it into any
285
+ additional tenant-scoped `createSession` a scenario performs
266
286
 
267
287
  Scenario cookbooks under `industry: healthcare` start at their own
268
288
  `§001` with these slots already populated.