@alvera-ai/platform-sdk 0.18.0 → 0.18.1

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 CHANGED
@@ -259,6 +259,10 @@ numbered steps inside the cookbook whose use case motivates it.
259
259
  DATA ACTIVATION (how data flows in)
260
260
  ───────────────
261
261
  generic_tables.md Schema-on-write tables.
262
+ advanced_migrations.md Hand-written DDL for one lake —
263
+ indexes, foreign keys, anything
264
+ past the CREATE TABLE / ADD COLUMN
265
+ the platform writes for itself.
262
266
  interoperability_contracts.md Liquid template mappings.
263
267
  data_activation_clients.md Ingestion pipelines — both the
264
268
  binding row (CRUD) and the
@@ -298,6 +302,7 @@ wire name (snake_case):
298
302
  api.connectedApps connected_apps.md
299
303
  api.mdm mdm.md
300
304
  api.genericTables generic_tables.md
305
+ api.advancedMigrations advanced_migrations.md
301
306
  api.interoperabilityContracts interoperability_contracts.md
302
307
  api.dataActivationClients data_activation_clients.md
303
308
  api.workflows workflows.md
@@ -327,8 +332,12 @@ Each cookbook structure:
327
332
  are lifted from, anchor file first), `status`. `use_case` is one
328
333
  of the four surfaces — `organic-marketing`,
329
334
  `subscription-saas`, `payments-compliance`,
330
- `primary-care-feedback` — and there is exactly one cookbook per
331
- use case, at `cookbook/<use-case>.md`.
335
+ `primary-care-feedback` — and there is exactly one cookbook per use
336
+ case. Three are at `cookbook/<use-case>.md`; the fourth is at
337
+ `cookbook/primary-care.md`, because `use_case` tracks the integration
338
+ suite it is lifted from (`integration-tests/tests/primary-care-feedback/`)
339
+ and the filename does not. Read `use_case` from the front matter rather
340
+ than deriving it from the filename.
332
341
  - **Problem** — the business-outcome statement in domain terms,
333
342
  sourced from the anchor vitest's behaviour (not from
334
343
  customer-narrative documentation).
@@ -0,0 +1,202 @@
1
+ # Advanced migrations
2
+
3
+ SQL a person wrote by hand for one datalake.
4
+
5
+ The platform writes its own DDL when a generic table is created or gains a
6
+ column — `CREATE TABLE` and `ALTER TABLE … ADD COLUMN`, and nothing else.
7
+ Everything past that is yours to write here: an index, a foreign key, a column
8
+ rename, a type change, a drop. The same migrator runs both.
9
+
10
+ **One name, two spellings.** Outwardly these are *advanced migrations*, which is
11
+ what the API path and this guide call them. The platform stores them in
12
+ `datalake_migrations`, and its own logs and internals use that name. Both are
13
+ correct; do not coin a third.
14
+
15
+ api.advancedMigrations → this guide
16
+ /datalakes/{slug}/advanced-migrations
17
+
18
+ ## 1. Wire shape
19
+
20
+ ```typescript
21
+ const { data: created } = await api.advancedMigrations.create(
22
+ tenantSlug,
23
+ datalakeSlug, // scope comes from the PATH — see below
24
+ {
25
+ name: 'AddEmailIndexToContactSubmissions',
26
+ up_statement:
27
+ 'CREATE INDEX IF NOT EXISTS idx_contact_submissions_email ' +
28
+ 'ON alvera_custom_contact_submissions (email)',
29
+ down_statement:
30
+ 'DROP INDEX IF EXISTS idx_contact_submissions_email',
31
+ },
32
+ )
33
+ // created.id — server-derived UUID
34
+ // created.version — server-assigned from a sequence; orders execution
35
+ // created.datalake_id — server-derived from the path
36
+ // created.checksum — server-computed; the drift fingerprint
37
+ ```
38
+
39
+ Three fields, all required, all strings:
40
+
41
+ ```
42
+ name required string — capitalized identifier, unique per datalake
43
+ up_statement required string — the DDL to run
44
+ down_statement required string — the DDL that reverses it
45
+ ```
46
+
47
+ **`datalake_id` is NOT a body field.** This is the one place the shape departs
48
+ from every other datalake-scoped kind. Elsewhere — tools, workflows, generic
49
+ tables — you send `datalake_id` in the body. Here it is `readOnly`: the scope
50
+ comes from `datalakeSlug` in the path, and the server fills the field in. Sending
51
+ it is not how you say which lake this belongs to.
52
+
53
+ **`version` is server-assigned and orders execution.** It comes from a sequence,
54
+ not from you, and it is the thing that decides what runs first — not the name,
55
+ and not the order you declared them in. Generated migrations carry a year-style
56
+ prefix in their names to read well beside yours; that prefix is cosmetic.
57
+
58
+ ## 2. Rules the type cannot encode
59
+
60
+ ### An applied migration is frozen — update and delete both refuse
61
+
62
+ Once a migration has run successfully, `update()` and `delete()` each answer
63
+ **409 Conflict**. The reasons differ and both are worth knowing.
64
+
65
+ **Update** is refused because a statement that has already executed cannot be
66
+ rewritten after the fact: the new text would no longer describe what happened.
67
+
68
+ **Delete** is refused because the run log cascades on delete, so removing an
69
+ applied migration would also erase the record of it having run.
70
+
71
+ So a migration has exactly two lives. Before it runs it is ordinary and
72
+ editable. After it runs it is history. **Changing applied DDL means writing a
73
+ NEW migration that alters what the old one built** — the same shape as changing
74
+ a generic table column's `privacy_requirement`, which is delete-and-recreate
75
+ rather than an edit.
76
+
77
+ "Applied" means precisely: a run log row exists for this migration with
78
+ direction `up` and status `success`. A failed run does not freeze it.
79
+
80
+ ### Write the statement to be safe on a second run
81
+
82
+ The platform tracks what it has applied and will not run an applied migration
83
+ twice, so you do not need `IF NOT EXISTS` for that. You need it for the other
84
+ case: a run that failed **partway**. The applied-migrations record stops a second
85
+ run; it does not undo a first one that got halfway.
86
+
87
+ ```sql
88
+ -- safe to meet again
89
+ CREATE INDEX IF NOT EXISTS idx_submissions_email ON … (email)
90
+
91
+ -- Postgres has no ADD CONSTRAINT IF NOT EXISTS, so guard it yourself
92
+ DO $$
93
+ BEGIN
94
+ IF NOT EXISTS (
95
+ SELECT 1 FROM pg_constraint WHERE conname = 'fk_submissions_lead'
96
+ ) THEN
97
+ ALTER TABLE alvera_custom_contact_submissions
98
+ ADD CONSTRAINT fk_submissions_lead
99
+ FOREIGN KEY (lead_id) REFERENCES alvera_custom_leads (id);
100
+ END IF;
101
+ END $$;
102
+ ```
103
+
104
+ The same applies to `down_statement`, which is likelier to be run against a
105
+ database that is not in the state you assumed.
106
+
107
+ ### The table names are the platform's, not yours
108
+
109
+ A generic table you declared as `contact_submissions` is physically
110
+ `alvera_custom_contact_submissions`. Read the name back from the created table
111
+ rather than composing it — see `generic_tables.md` — because your migration
112
+ statement has to name the physical table, and getting it wrong is a runtime
113
+ failure inside DDL rather than a validation error.
114
+
115
+ ### Ordering is real, and it is the version's job
116
+
117
+ Adding an index and then a foreign key that depends on it are two migrations,
118
+ and the sequence decides which runs first. Two migrations created in one session
119
+ run in creation order because the sequence is monotonic — but if you need B
120
+ after A, that is a fact about your two `version` values, not about how you
121
+ listed them.
122
+
123
+ ## 3. Field ownership
124
+
125
+ **Caller-authored (Request).** `name`, `up_statement`, `down_statement`. That is
126
+ the whole writable surface.
127
+
128
+ **Server-derived (Response-only).** `id`, `datalake_id`, `version`, `checksum`,
129
+ `inserted_at`, `updated_at`. None may be sent; see `type_naming.md` for why the
130
+ Writable types exclude them.
131
+
132
+ `name` is unique per datalake, so two lakes may each carry a
133
+ `AddEmailIndex` and neither collides.
134
+
135
+ ## 4. Error envelopes
136
+
137
+ Standard JSON:API envelope throughout — see `errors.md`.
138
+
139
+ | Status | When |
140
+ |---|---|
141
+ | 422 | a required field is missing, or `name` collides within this datalake |
142
+ | 409 | the migration has already been applied — `update` and `delete` both |
143
+ | 404 | the id belongs to a different datalake (deliberately not "403": its existence is not leaked) |
144
+
145
+ A 409 here is not a retryable condition. It is telling you the migration is
146
+ history and you want a new one.
147
+
148
+ ## 5. Lifecycle
149
+
150
+ ```
151
+ create → the row exists; nothing has run yet
152
+ migrate → the datalake's migrator runs it, in version order
153
+ (POST /datalakes/{slug}/migrate — asynchronous, 202)
154
+ applied → a run log row: direction up, status success
155
+ from here: update 409, delete 409
156
+ ```
157
+
158
+ Creating a migration does **not** run it. The datalake's migrate call does, and
159
+ it is asynchronous — it answers 202 as soon as the job is enqueued. Poll the
160
+ datalake for `status: ready` the way you would after creating one; see
161
+ `datalakes.md`.
162
+
163
+ ## 6. Gotchas
164
+
165
+ **A created migration that never ran is invisible in the database.** The row
166
+ exists, `list()` returns it, and nothing has happened to any table. Reading the
167
+ catalog is the only way to know whether it took effect — the migration record
168
+ says what you asked for, not what is true.
169
+
170
+ **Proving one worked means asking the database, not the API.** The response
171
+ carries `checksum`, `datalake_id`, `down_statement`, `id`, `inserted_at`,
172
+ `name`, `up_statement`, `updated_at` and `version` — no status, no applied
173
+ flag, and `version` is execution ORDER rather than proof of execution. So the
174
+ honest check is a query against the catalog for the object the statement was
175
+ supposed to create:
176
+
177
+ ```typescript
178
+ // an index
179
+ const { data: indexRows } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
180
+ sql: `select 1 from pg_indexes
181
+ where tablename = 'alvera_custom_contact_submissions'
182
+ and indexname = 'idx_contact_submissions_email'`,
183
+ mode: 'raw',
184
+ })
185
+
186
+ // a foreign key
187
+ const { data: fkRows } = await api.datalakes.executeSql(tenantSlug, datalakeSlug, {
188
+ sql: `select 1 from information_schema.table_constraints
189
+ where constraint_type = 'FOREIGN KEY'
190
+ and table_name = 'alvera_custom_contact_submissions'
191
+ and constraint_name = 'fk_submissions_lead'`,
192
+ mode: 'raw',
193
+ })
194
+ ```
195
+
196
+ **`down_statement` is required and is rarely exercised.** Nothing makes you
197
+ prove it works, and it is the statement you will want on your worst day. Write
198
+ it as carefully as the `up`.
199
+
200
+ **A migration belongs to exactly one datalake.** There is no shared or
201
+ tenant-level migration. Three lakes needing the same index need three
202
+ migrations — which is also why `name` only has to be unique per lake.
@@ -261,6 +261,75 @@ The tier is chosen per call, not per datalake — `mode` on
261
261
  `executeSql`, `dataAccessMode` on a dataset search. Nothing about
262
262
  the datalake decides it.
263
263
 
264
+ ### How the copies get turned on
265
+
266
+ Nothing in this SDK turns them on directly, and that is worth stating
267
+ before the mechanism, because the field involved is one you cannot send.
268
+
269
+ **Through the `alvera` CLI it is one boolean on the datalake block:**
270
+
271
+ ```toml
272
+ [datalakes.my-lake]
273
+ enable_tokenization = true
274
+ ```
275
+
276
+ `enable_tokenization` is a **CLI-only manifest key**. The CLI strips it
277
+ before the request reaches the API — there is no such field on
278
+ `DatalakeRequest`, and adding one to an SDK call does nothing. What it
279
+ drives is a separate endpoint, `POST /datalakes/<slug>/tokenization`,
280
+ which the CLI calls on your behalf after the raw lake reaches `ready`.
281
+ Calling that endpoint yourself is the SDK-level equivalent.
282
+
283
+ Four things about it that are not guessable from the field:
284
+
285
+ **Both copies or neither.** They are created, destroyed and billed
286
+ together. There is no way to ask for a tokenized lake without a redacted
287
+ one.
288
+
289
+ **Order is handled for you, and it matters.** A derived lake's own
290
+ migration creates its tables and starts the copy, so the raw lake must be
291
+ `ready` first. `apply` turns the copies on after that, then waits for
292
+ both to reach `ready` themselves.
293
+
294
+ **It only goes one way.** Nothing turns tokenization off — no endpoint
295
+ deletes a derived lake — which is why the platform declined to make it an
296
+ authored field in the first place: `true → false` would let a caller
297
+ declare a state the platform cannot honour.
298
+
299
+ **`[tokenization.<slug>]` is retired.** If you meet a manifest carrying
300
+ that block, it is pre-0.18 and the CLI now refuses it by name.
301
+
302
+ ### What turning them on actually requires
303
+
304
+ One boolean, three prerequisites — and the platform **probes** each and
305
+ **refuses** when it is missing. It never issues `CREATE DATABASE` or
306
+ `CreateBucket`, deliberately: a platform that can create one can create
307
+ it in the wrong place, and a datalake's storage belongs to the operator.
308
+
309
+ **1 · Two more databases.** Named from the raw lake's own database name
310
+ with the mode appended — `<raw>_tokenized` and `<raw>_redacted`. Postgres
311
+ truncates an over-long identifier *silently*, which would land two
312
+ derived lakes on one physical database, so a name that would exceed the
313
+ 63-byte identifier limit is refused rather than truncated. Long raw
314
+ database names are where that bites.
315
+
316
+ **2 · On two more Postgres servers.** A derived lake does not live beside
317
+ the raw one. Locally the tokenized copy is on `:5433` and the redacted on
318
+ `:5434`, beside the raw lake on `:5432`. Separate servers on purpose: a
319
+ derived lake runs `postgresql_anonymizer` and PL/Python, which the raw
320
+ lake — holding the real values — must not have.
321
+
322
+ **3 · Three buckets, not one.** The raw lake's bucket plus
323
+ `<bucket>-tokenized` and `<bucket>-redacted`.
324
+
325
+ **The failure mode is worth knowing in advance, because it is
326
+ mis-attributed by default.** A missing copy bucket is reported against
327
+ the **primary** lake, naming the lake rather than the bucket that is
328
+ actually absent — and it surfaces minutes into a create, as a 422 that
329
+ reads like a platform defect. It has been diagnosed as one. If a create
330
+ fails on cloud storage for a lake declaring the copies, check that all
331
+ three buckets exist before looking anywhere else.
332
+
264
333
  ### What decides what a reader sees
265
334
 
266
335
  **`privacy_requirement` on the column.** That single declaration is
@@ -57,6 +57,17 @@ const { data: created } = await api.genericTables.create(
57
57
  is_unique: true,
58
58
  privacy_requirement: 'none',
59
59
  },
60
+ {
61
+ name: 'email',
62
+ title: 'Email',
63
+ type: 'string',
64
+ description: 'Submitter email',
65
+ is_unique: false,
66
+ // The masking pair: WHETHER, and WHAT it is so the right
67
+ // function masks it. See "a pair" below.
68
+ privacy_requirement: 'tokenize',
69
+ semantic_kind: 'email',
70
+ },
60
71
  // ... more columns
61
72
  ],
62
73
  },
@@ -122,8 +133,29 @@ is_array optional bool — column holds an array of `type` values
122
133
  description required string — narrative; surfaces in catalogs
123
134
  is_unique required bool — Postgres UNIQUE constraint at migration time
124
135
  privacy_requirement required enum — 'none' | 'tokenize' | 'redact'
136
+ semantic_kind optional enum — 'string' | 'text' | 'email' | 'phone'
137
+ | 'date' | 'json' | 'image' | 'pdf'
138
+ | 'attachment'; nullable
125
139
  ```
126
140
 
141
+ #### `privacy_requirement` and `semantic_kind` are a pair
142
+
143
+ They answer two different questions and a masked column needs both.
144
+
145
+ **`privacy_requirement` says WHETHER the column is masked** in the derived
146
+ lakes — see the table in §2 below. Immutable once the column exists.
147
+
148
+ **`semantic_kind` says WHAT the value is, which decides WHICH function
149
+ masks it.** A tokenized email should come back looking like an email; a
150
+ tokenized phone like a phone. Without one, the platform can only treat
151
+ the value as free text, so **a masked column with no `semantic_kind` gets
152
+ a worse mask than it should** — still masked, but less useful to
153
+ everything downstream that expected the shape to survive.
154
+
155
+ It is optional, and nothing fails without it, which is exactly why it is
156
+ easy to miss. **Set it on every column you mark `tokenize` or `redact`.**
157
+ On a `none` column it is documentation and costs nothing.
158
+
127
159
  #### Column types
128
160
 
129
161
  ```
@@ -4,7 +4,7 @@
4
4
 
5
5
  How the platform reaches mocked third-party APIs, on a laptop and from deployed
6
6
  environments. Two mock surfaces exist and they answer different questions:
7
- **WireMock** stands in for third-party SaaS APIs, **LocalStack** stands in for AWS.
7
+ **WireMock** stands in for third-party SaaS APIs, **MiniStack** stands in for AWS.
8
8
 
9
9
  ## The two surfaces
10
10
 
@@ -12,14 +12,15 @@ environments. Two mock surfaces exist and they answer different questions:
12
12
  `wm_mappings/`, with response bodies in `wm_response_files/`. It covers the
13
13
  third-party APIs the platform integrates with — Athenahealth, Twilio, Mailgun,
14
14
  Stripe, SendGrid, HubSpot, QuickBooks, Airtable, Atomic FI, the LLM providers,
15
- and a small set of AWS endpoints LocalStack cannot serve.
15
+ and a small set of AWS endpoints MiniStack cannot serve.
16
16
 
17
- **LocalStack** serves AWS itself: `s3,lambda,sns,ssm,sqs,sts,logs,secretsmanager,cloudformation`
17
+ **MiniStack** serves AWS itself: `s3,lambda,sns,ssm,sqs,sts,logs,secretsmanager,cloudformation`
18
18
  (`local-dependencies.yml`). See [Datalakes](datalakes.md) for how datalake
19
19
  provisioning uses it.
20
20
 
21
- The division is not arbitrary. LocalStack Community **cannot** serve
22
- `pinpoint-sms-voice-v2` (501 — Ultimate tier only), so End User Messaging points
21
+ The division is not arbitrary. No AWS emulator here serves
22
+ `pinpoint-sms-voice-v2` — LocalStack Community answered 501 (Ultimate tier
23
+ only) and MiniStack has no such service at all — so End User Messaging points
23
24
  at WireMock for its send, its S3 media staging, and its STS credential probe. A
24
25
  tool has one `endpoint_url`, so all three land on the same host.
25
26
 
@@ -62,11 +63,11 @@ the hosted instance — see below.
62
63
 
63
64
  ## Running locally
64
65
 
65
- `make run-backing-services` starts both. WireMock listens on `:8080`, LocalStack
66
+ `make run-backing-services` starts both. WireMock listens on `:8080`, MiniStack
66
67
  on `:4566`. Tool bodies reach them by setting `endpoint_url` — blank means real
67
68
  AWS, any value replaces host, port and scheme.
68
69
 
69
- LocalStack's S3 state does **not** survive a container restart. Recreate a bucket
70
+ MiniStack's S3 state does **not** survive a container restart. Recreate a bucket
70
71
  after one:
71
72
 
72
73
  ```bash
@@ -176,10 +177,10 @@ identical POSTs return three *different* bodies in a fixed order. Matching hashe
176
177
  across three successive calls proves both environments walked the same state
177
178
  machine — something no stateless matcher could fake.
178
179
 
179
- ## LocalStack at the edge
180
+ ## MiniStack at the edge
180
181
 
181
- Not done, and deliberately scoped apart. LocalStack is also just an image and
182
- rides the same mechanism, but Containers **sleep by design** and LocalStack's S3
182
+ Not done, and deliberately scoped apart. MiniStack is also just an image and
183
+ rides the same mechanism, but Containers **sleep by design** and MiniStack's S3
183
184
  state already does not survive a restart. What is an occasional annoyance locally
184
185
  becomes routine when instances sleep between runs. Anything depending on objects
185
186
  persisting must move to R2, which speaks S3 natively — migration work, not a flag
@@ -188,5 +189,5 @@ flip.
188
189
  ## Related
189
190
 
190
191
  - [Tools](tools.md) — tool bodies, `endpoint_url`, and REST API auth methods
191
- - [Datalakes](datalakes.md) — how datalake provisioning uses LocalStack
192
+ - [Datalakes](datalakes.md) — how datalake provisioning uses MiniStack
192
193
  - [Connected Apps](connected_apps.md) — the third-party integrations the corpus stands in for
package/dist/index.d.mts CHANGED
@@ -127,6 +127,18 @@ type ActionExecutionLogResponse = {
127
127
  * Creation timestamp
128
128
  */
129
129
  inserted_at?: string;
130
+ /**
131
+ * How many times the job has been attempted. `0` means it has not run yet.
132
+ */
133
+ readonly job_attempt?: number | null;
134
+ /**
135
+ * When the job will next be attempted. Equal to `scheduled_at` until something moves it; a snoozing job carries the instant it wakes.
136
+ */
137
+ readonly job_next_attempt_at?: string | null;
138
+ /**
139
+ * State of the job carrying this action — one of `available`, `scheduled`, `executing`, `retryable`, `completed`, `discarded`, `cancelled`. Null when no job carries it.
140
+ */
141
+ readonly job_state?: string | null;
130
142
  /**
131
143
  * Rendered message body for this action (mode-driven; null when no message was sent)
132
144
  */
@@ -441,7 +453,7 @@ type CloudWatchLogGroupResponse = {
441
453
  auth_method: 'access_key' | 'iam_role' | 'assume_role';
442
454
  base_filter_pattern?: ComplexTemplateConfigResponse | null;
443
455
  /**
444
- * Custom CloudWatch Logs endpoint URL (e.g., http://localhost:4566 for LocalStack)
456
+ * Custom CloudWatch Logs endpoint URL (e.g., http://localhost:4566 for MiniStack)
445
457
  */
446
458
  endpoint_url?: string | null;
447
459
  /**
@@ -878,7 +890,7 @@ type ExecuteActionRequest = {
878
890
  */
879
891
  mode?: 'live' | 'dry_run';
880
892
  /**
881
- * When true, the action's `trigger_template` schedule is bypassed and the queued job is dispatched immediately. The ActionExecutionLog still records the trigger-rendered `scheduled_at` — only the Oban job is fast-forwarded. Use this to force a future-triggered action (e.g. a year-roll birthday SMS) to fire now, which is the only way to drive such a workflow to completion inside a live test or cookbook run.
893
+ * When true, the action's `trigger_template` schedule is bypassed and the queued job is dispatched immediately. The ActionExecutionLog still records the trigger-rendered `scheduled_at` — only the Oban job is fast-forwarded. Use this to force an action whose trigger renders a future instant (e.g. a year-roll birthday SMS) to fire now, which is the only way to drive such a workflow to completion inside a live test or cookbook run. An action whose trigger renders a past instant does not need it: a past `scheduled_at` is staged on the next pass and runs on its own.
882
894
  */
883
895
  trigger_override?: boolean;
884
896
  };
@@ -2568,17 +2580,17 @@ type DatalakeResponse = {
2568
2580
  */
2569
2581
  db_reader_enable_ssl: boolean;
2570
2582
  /**
2571
- * Reader DB host
2583
+ * Reader DB host. Null while a derived lake's database is still being provisioned
2572
2584
  */
2573
- db_reader_host: string;
2585
+ db_reader_host: string | null;
2574
2586
  /**
2575
2587
  * Reader DB name
2576
2588
  */
2577
2589
  db_reader_name: string;
2578
2590
  /**
2579
- * Reader DB port
2591
+ * Reader DB port. Null while a derived lake's database is still being provisioned
2580
2592
  */
2581
- db_reader_port: number;
2593
+ db_reader_port: number | null;
2582
2594
  /**
2583
2595
  * Reader DB schema name
2584
2596
  */
@@ -2592,17 +2604,17 @@ type DatalakeResponse = {
2592
2604
  */
2593
2605
  db_writer_enable_ssl: boolean;
2594
2606
  /**
2595
- * Writer DB host
2607
+ * Writer DB host. Null while a derived lake's database is still being provisioned
2596
2608
  */
2597
- db_writer_host: string;
2609
+ db_writer_host: string | null;
2598
2610
  /**
2599
2611
  * Writer DB name
2600
2612
  */
2601
2613
  db_writer_name: string;
2602
2614
  /**
2603
- * Writer DB port
2615
+ * Writer DB port. Null while a derived lake's database is still being provisioned
2604
2616
  */
2605
- db_writer_port: number;
2617
+ db_writer_port: number | null;
2606
2618
  /**
2607
2619
  * Writer DB schema name
2608
2620
  */
@@ -2642,7 +2654,7 @@ type DatalakeResponse = {
2642
2654
  /**
2643
2655
  * Datalake setup status
2644
2656
  */
2645
- readonly status?: 'new' | 'processing' | 'ready';
2657
+ readonly status?: 'provisioning' | 'new' | 'processing' | 'ready';
2646
2658
  tenant?: TenantResponse;
2647
2659
  /**
2648
2660
  * Datalake reporting timezone. Closed whitelist of 8 US timezones — general IANA values (including `UTC`) are rejected.
@@ -2685,6 +2697,10 @@ type AlveraApiError = {
2685
2697
  */
2686
2698
  title: string;
2687
2699
  }>;
2700
+ /**
2701
+ * Which derived copy failed to provision — `tokenized` or `redacted`. `null` on every other structured error, which is why it is not required. Without it a client is told a copy of the pair failed but not which one, and a copy is repaired one mode at a time.
2702
+ */
2703
+ mode?: string | null;
2688
2704
  };
2689
2705
  /**
2690
2706
  * CloudStorageAwsResponse
@@ -3216,7 +3232,7 @@ type SnsResponse = {
3216
3232
  auth_method: 'access_key' | 'iam_role' | 'assume_role';
3217
3233
  base_message?: ComplexTemplateConfigResponse | null;
3218
3234
  /**
3219
- * Custom SNS endpoint URL (e.g., http://localhost:4566 for LocalStack); leave blank for real AWS
3235
+ * Custom SNS endpoint URL (e.g., http://localhost:4566 for MiniStack); leave blank for real AWS
3220
3236
  */
3221
3237
  endpoint_url?: string | null;
3222
3238
  /**
@@ -3561,12 +3577,16 @@ type WorkflowLogResponse = {
3561
3577
  * Completed action count
3562
3578
  */
3563
3579
  actions_completed?: number;
3580
+ /**
3581
+ * Actions that have started and not finished. A run showing `actions_pending: 1, actions_executing: 0` is waiting to be picked up; one showing `actions_pending: 1, actions_executing: 1` is mid-flight and may be stuck.
3582
+ */
3583
+ actions_executing?: number;
3564
3584
  /**
3565
3585
  * Failed action count
3566
3586
  */
3567
3587
  actions_failed?: number;
3568
3588
  /**
3569
- * Pending action count
3589
+ * Unfinished action count — actions still queued plus actions already running. `actions_executing` is the running share of it.
3570
3590
  */
3571
3591
  actions_pending?: number;
3572
3592
  /**
@@ -3930,6 +3950,14 @@ type TwilioRequest = {
3930
3950
  * Twilio Account SID (required on a primary; supplied by the primary on a variant)
3931
3951
  */
3932
3952
  account_sid?: string | null;
3953
+ /**
3954
+ * Twilio API Key SID (required when auth_method is api_key)
3955
+ */
3956
+ api_key_sid?: string | null;
3957
+ /**
3958
+ * How this primary authenticates to Twilio. Defaults to auth_token.
3959
+ */
3960
+ auth_method?: 'auth_token' | 'api_key';
3933
3961
  base_message?: ComplexTemplateConfigRequest;
3934
3962
  /**
3935
3963
  * Custom Twilio API base URL (e.g., http://localhost:8080 for WireMock); leave blank for https://api.twilio.com
@@ -4589,6 +4617,14 @@ type TwilioResponse = {
4589
4617
  * Twilio Account SID (required on a primary; supplied by the primary on a variant)
4590
4618
  */
4591
4619
  account_sid?: string | null;
4620
+ /**
4621
+ * Twilio API Key SID (required when auth_method is api_key)
4622
+ */
4623
+ api_key_sid?: string | null;
4624
+ /**
4625
+ * How this primary authenticates to Twilio. Defaults to auth_token.
4626
+ */
4627
+ auth_method?: 'auth_token' | 'api_key';
4592
4628
  base_message?: ComplexTemplateConfigResponse | null;
4593
4629
  /**
4594
4630
  * Custom Twilio API base URL (e.g., http://localhost:8080 for WireMock); leave blank for https://api.twilio.com
@@ -4643,9 +4679,9 @@ type DatalakeRequestWritable = {
4643
4679
  */
4644
4680
  db_reader_enable_ssl: boolean;
4645
4681
  /**
4646
- * Reader DB host
4682
+ * Reader DB host. Null while a derived lake's database is still being provisioned
4647
4683
  */
4648
- db_reader_host: string;
4684
+ db_reader_host: string | null;
4649
4685
  /**
4650
4686
  * Reader DB name
4651
4687
  */
@@ -4655,9 +4691,9 @@ type DatalakeRequestWritable = {
4655
4691
  */
4656
4692
  db_reader_pass: string;
4657
4693
  /**
4658
- * Reader DB port
4694
+ * Reader DB port. Null while a derived lake's database is still being provisioned
4659
4695
  */
4660
- db_reader_port: number;
4696
+ db_reader_port: number | null;
4661
4697
  /**
4662
4698
  * Reader DB schema name
4663
4699
  */
@@ -4675,9 +4711,9 @@ type DatalakeRequestWritable = {
4675
4711
  */
4676
4712
  db_writer_enable_ssl: boolean;
4677
4713
  /**
4678
- * Writer DB host
4714
+ * Writer DB host. Null while a derived lake's database is still being provisioned
4679
4715
  */
4680
- db_writer_host: string;
4716
+ db_writer_host: string | null;
4681
4717
  /**
4682
4718
  * Writer DB name
4683
4719
  */
@@ -4687,9 +4723,9 @@ type DatalakeRequestWritable = {
4687
4723
  */
4688
4724
  db_writer_pass: string;
4689
4725
  /**
4690
- * Writer DB port
4726
+ * Writer DB port. Null while a derived lake's database is still being provisioned
4691
4727
  */
4692
- db_writer_port: number;
4728
+ db_writer_port: number | null;
4693
4729
  /**
4694
4730
  * Writer DB schema name
4695
4731
  */
@@ -5485,7 +5521,7 @@ type CloudWatchLogGroupRequestWritable = {
5485
5521
  auth_method: 'access_key' | 'iam_role' | 'assume_role';
5486
5522
  base_filter_pattern?: ComplexTemplateConfigRequest;
5487
5523
  /**
5488
- * Custom CloudWatch Logs endpoint URL (e.g., http://localhost:4566 for LocalStack)
5524
+ * Custom CloudWatch Logs endpoint URL (e.g., http://localhost:4566 for MiniStack)
5489
5525
  */
5490
5526
  endpoint_url?: string | null;
5491
5527
  /**
@@ -5521,7 +5557,7 @@ type SnsRequestWritable = {
5521
5557
  auth_method: 'access_key' | 'iam_role' | 'assume_role';
5522
5558
  base_message?: ComplexTemplateConfigRequest;
5523
5559
  /**
5524
- * Custom SNS endpoint URL (e.g., http://localhost:4566 for LocalStack); leave blank for real AWS
5560
+ * Custom SNS endpoint URL (e.g., http://localhost:4566 for MiniStack); leave blank for real AWS
5525
5561
  */
5526
5562
  endpoint_url?: string | null;
5527
5563
  /**
@@ -6000,6 +6036,18 @@ type TwilioRequestWritable = {
6000
6036
  * Twilio Account SID (required on a primary; supplied by the primary on a variant)
6001
6037
  */
6002
6038
  account_sid?: string | null;
6039
+ /**
6040
+ * Twilio API Key Secret (required when auth_method is api_key)
6041
+ */
6042
+ api_key_secret?: string | null;
6043
+ /**
6044
+ * Twilio API Key SID (required when auth_method is api_key)
6045
+ */
6046
+ api_key_sid?: string | null;
6047
+ /**
6048
+ * How this primary authenticates to Twilio. Defaults to auth_token.
6049
+ */
6050
+ auth_method?: 'auth_token' | 'api_key';
6003
6051
  /**
6004
6052
  * Twilio Auth Token (required on a primary; supplied by the primary on a variant)
6005
6053
  */