@abloatai/ablo 0.14.0 → 0.15.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/Database.d.ts +1 -1
  3. package/dist/auth/index.d.ts +4 -0
  4. package/dist/auth/index.js +1 -0
  5. package/dist/cli.cjs +54 -1
  6. package/dist/client/Ablo.d.ts +34 -3
  7. package/dist/client/Ablo.js +11 -4
  8. package/dist/client/ApiClient.js +3 -0
  9. package/dist/client/sessionMint.js +1 -0
  10. package/dist/client/writeOptionsSchema.d.ts +4 -6
  11. package/dist/client/writeOptionsSchema.js +1 -1
  12. package/dist/coordination/schema.d.ts +90 -12
  13. package/dist/coordination/schema.js +99 -4
  14. package/dist/errors.d.ts +19 -0
  15. package/dist/errors.js +21 -0
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +10 -1
  18. package/dist/interfaces/index.d.ts +23 -2
  19. package/dist/policy/types.d.ts +35 -3
  20. package/dist/policy/types.js +20 -7
  21. package/dist/server/commit.d.ts +26 -0
  22. package/dist/source/connector-protocol.d.ts +159 -0
  23. package/dist/source/connector-protocol.js +161 -0
  24. package/dist/source/connector.d.ts +96 -0
  25. package/dist/source/connector.js +264 -0
  26. package/dist/source/contract.d.ts +4 -6
  27. package/dist/source/contract.js +1 -1
  28. package/dist/source/index.d.ts +3 -1
  29. package/dist/source/index.js +6 -0
  30. package/dist/sync/SyncWebSocket.d.ts +32 -5
  31. package/dist/sync/SyncWebSocket.js +40 -6
  32. package/dist/transactions/TransactionQueue.d.ts +7 -1
  33. package/dist/transactions/TransactionQueue.js +43 -2
  34. package/dist/wire/frames.d.ts +28 -4
  35. package/docs/concurrency-convention.md +222 -0
  36. package/docs/coordination.md +5 -0
  37. package/docs/data-sources.md +41 -0
  38. package/package.json +6 -1
@@ -409,6 +409,10 @@ export class TransactionQueue extends EventEmitter {
409
409
  executingCount = 0;
410
410
  // Optimistic update tracking
411
411
  optimisticUpdates = new Map();
412
+ // Stale-context notifications (CoAgent/MTPO notify-instead-of-abort) keyed by
413
+ // transaction id, populated from the commit ack and drained by
414
+ // `waitForCommitReceipt` so the receipt carries the self-heal signal.
415
+ commitNotifications = new Map();
412
416
  // LINEAR PATTERN: Track delta confirmation timeouts for awaiting_delta transactions
413
417
  // Following Replicache/PowerSync pattern: retry with backoff instead of rolling back
414
418
  deltaConfirmationTimeouts = new Map();
@@ -1127,6 +1131,20 @@ export class TransactionQueue extends EventEmitter {
1127
1131
  const result = await this.mutationExecutor.commit(operations);
1128
1132
  const lastSyncId = result?.lastSyncId ?? 0;
1129
1133
  this.noteAck(lastSyncId);
1134
+ // Notify-instead-of-abort: the server returned stale-context
1135
+ // notifications for `onStale: 'notify'` ops whose premise moved.
1136
+ // Every notified op was HELD (not written) — its optimistic state
1137
+ // must be rolled back here; no delta will ever confirm it. We also
1138
+ // stamp the signal so `waitForCommitReceipt` carries it onto the
1139
+ // receipt.
1140
+ const notifications = result?.notifications;
1141
+ const heldIds = new Set((notifications ?? []).map((n) => n.id));
1142
+ for (const { tx } of batchOps) {
1143
+ const txNotifs = notifications?.filter((n) => n.id === tx.modelId);
1144
+ if (txNotifs && txNotifs.length > 0) {
1145
+ this.commitNotifications.set(tx.id, txNotifs);
1146
+ }
1147
+ }
1130
1148
  // Detect server bug: lastSyncId 0 means mutation succeeded but no sync delta was emitted
1131
1149
  if (lastSyncId === 0) {
1132
1150
  getContext().observability.captureCommitZeroSyncId({
@@ -1137,6 +1155,19 @@ export class TransactionQueue extends EventEmitter {
1137
1155
  // LINEAR PATTERN: Mark as awaiting_delta with syncId threshold
1138
1156
  // Transactions will be confirmed when any delta with id >= lastSyncId arrives
1139
1157
  for (const { tx } of batchOps) {
1158
+ // Held op ('notify'): the server withheld the write, so no delta
1159
+ // will confirm it. Roll back the optimistic update (server
1160
+ // wins) and complete the transaction now — the agent self-heals
1161
+ // from the notification rather than waiting out the delta
1162
+ // timeout. The receipt still resolves (commit succeeded).
1163
+ if (heldIds.has(tx.modelId)) {
1164
+ await this.rollbackOptimistic(tx, 'conflict_server_wins');
1165
+ this.store.updateStatus(tx.id, 'completed');
1166
+ this.emit('transaction:completed', tx);
1167
+ this.emit(`transaction:completed:${tx.id}`, tx);
1168
+ this.optimisticUpdates.delete(tx.id);
1169
+ continue;
1170
+ }
1140
1171
  tx.syncIdNeededForCompletion = lastSyncId;
1141
1172
  // Safety net: when lastSyncId is 0, DELETE transactions should be confirmed
1142
1173
  // immediately. DELETEs are idempotent — if no delta was emitted, the entity
@@ -1528,6 +1559,7 @@ export class TransactionQueue extends EventEmitter {
1528
1559
  kind: 'commit',
1529
1560
  operations: [...operations],
1530
1561
  causedByTaskId: options.causedByTaskId ?? null,
1562
+ ...(options.reads ? { reads: options.reads } : {}),
1531
1563
  status: 'pending',
1532
1564
  createdAt: Date.now(),
1533
1565
  attempts: 0,
@@ -1565,6 +1597,7 @@ export class TransactionQueue extends EventEmitter {
1565
1597
  const result = await this.mutationExecutor.commit(tx.operations, {
1566
1598
  idempotencyKey: tx.id,
1567
1599
  causedByTaskId: tx.causedByTaskId ?? undefined,
1600
+ ...(tx.reads ? { reads: tx.reads } : {}),
1568
1601
  });
1569
1602
  tx.lastSyncId = result?.lastSyncId ?? 0;
1570
1603
  this.noteAck(tx.lastSyncId);
@@ -1610,10 +1643,18 @@ export class TransactionQueue extends EventEmitter {
1610
1643
  * of `ablo.commits.create()`.
1611
1644
  */
1612
1645
  waitForCommitReceipt(clientTxId) {
1646
+ // Drain any stale-context notifications stamped for this tx on the ack.
1647
+ const drainNotifications = () => {
1648
+ const n = this.commitNotifications.get(clientTxId);
1649
+ if (!n)
1650
+ return undefined;
1651
+ this.commitNotifications.delete(clientTxId);
1652
+ return n.length > 0 ? n : undefined;
1653
+ };
1613
1654
  return new Promise((resolve, reject) => {
1614
1655
  const existing = this.commitStore.get(clientTxId);
1615
1656
  if (existing?.status === 'completed') {
1616
- resolve({ lastSyncId: existing.lastSyncId ?? 0 });
1657
+ resolve({ lastSyncId: existing.lastSyncId ?? 0, notifications: drainNotifications() });
1617
1658
  return;
1618
1659
  }
1619
1660
  if (existing?.status === 'failed' && existing.error) {
@@ -1622,7 +1663,7 @@ export class TransactionQueue extends EventEmitter {
1622
1663
  }
1623
1664
  const onCompleted = (tx) => {
1624
1665
  cleanup();
1625
- resolve({ lastSyncId: tx.lastSyncId ?? 0 });
1666
+ resolve({ lastSyncId: tx.lastSyncId ?? 0, notifications: drainNotifications() });
1626
1667
  };
1627
1668
  const onFailed = ({ error }) => {
1628
1669
  cleanup();
@@ -18,7 +18,7 @@
18
18
  * Changing any shape here is a wire-contract change — it requires
19
19
  * coordinated client + server updates.
20
20
  */
21
- import type { OnStaleMode } from '../coordination/index.js';
21
+ import type { OnStaleMode, StaleNotification, ReadDependency } from '../coordination/index.js';
22
22
  import type { ErrorCode, RequiredCapability } from '../errors.js';
23
23
  /**
24
24
  * A single operation within a {@link CommitMessage} batch. The atomic unit
@@ -44,9 +44,10 @@ export interface CommitOperation {
44
44
  */
45
45
  readAt?: number | null;
46
46
  /**
47
- * Mode on stale detection. `'reject'` (default) throws
48
- * AbloStaleContextError; `'force'` applies unconditionally. `'flag'` /
49
- * `'merge'` are reserved, not yet implemented.
47
+ * Mode on stale detection (non-coercion). `'reject'` (default) throws
48
+ * AbloStaleContextError; `'overwrite'` applies unconditionally (blind LWW);
49
+ * `'notify'` holds the write and returns a `StaleNotification` for the actor
50
+ * to resolve.
50
51
  */
51
52
  onStale?: OnStaleMode | null;
52
53
  }
@@ -84,6 +85,15 @@ export interface CommitMessage {
84
85
  * `null` (the audit pane treats null as "no prompt-side context").
85
86
  */
86
87
  causedByTaskId?: string | null;
88
+ /**
89
+ * Batch-level read dependencies (the STORM "did anything I looked at
90
+ * change?" layer). Each entry is a row (`{model,id,readAt,fields?}`) or a
91
+ * sync group (`{group,readAt}`) the batch's writes were premised on; the
92
+ * server validates none moved since `readAt` and fires the entry's
93
+ * `onStale` disposition over the batch. Omitted ⇒ only write-targets are
94
+ * checked (legacy behavior).
95
+ */
96
+ reads?: ReadDependency[] | null;
87
97
  };
88
98
  }
89
99
  /**
@@ -105,6 +115,20 @@ export interface MutationResultMessage {
105
115
  status?: 'confirmed' | 'rejected';
106
116
  lastSyncId?: number;
107
117
  ops?: number;
118
+ /**
119
+ * Stale-context notifications for `onStale: 'notify' ops whose
120
+ * premise moved concurrently. Present only on a successful ack that hit a
121
+ * notify-resolved conflict; the client surfaces these via the
122
+ * `conflict:notified` event and the commit receipt instead of rejecting.
123
+ */
124
+ notifications?: StaleNotification[];
125
+ /**
126
+ * Ids of UPDATE/DELETE targets that matched ZERO rows (don't exist or are
127
+ * outside the org). Present (non-empty) only when a write missed. The
128
+ * client turns this into a loud `AbloNotFoundError` for the affected
129
+ * caller instead of treating the no-op as success.
130
+ */
131
+ missingIds?: string[];
108
132
  error?: {
109
133
  code: ErrorCode;
110
134
  message: string;
@@ -0,0 +1,222 @@
1
+ # Concurrency Convention
2
+
3
+ > The governing convention for how Ablo resolves concurrent writes to shared
4
+ > state, and the boundaries of that convention. This is the contract; the
5
+ > three-layer mechanics live in [`coordination.md`](./coordination.md).
6
+
7
+ ---
8
+
9
+ ## 1. The principle: non-coercion
10
+
11
+ **The engine surfaces the truthful current state and lets the intelligent actor —
12
+ agent or human — decide what to do. It does not force a resolution.**
13
+
14
+ That is the whole convention. Everything below is a consequence of it.
15
+
16
+ Classical concurrency control is *coercive*: it imposes the remedy. Two-phase
17
+ locking forces a block; optimistic concurrency forces an abort. Ablo's wager is
18
+ that the actor in the loop (an agent reasoning over the change, or a human
19
+ watching the row) is better placed to resolve a conflict than a fixed rule baked
20
+ into the storage layer. So the engine's job narrows to one thing: **report what
21
+ is true, on time, and get out of the way.**
22
+
23
+ There are two forms of non-coercion, and they are the same principle at two
24
+ moments in time:
25
+
26
+ | form | when | mechanism |
27
+ |---|---|---|
28
+ | **Claim** | *prospective* — before you act | reserve the row; others queue. Coordinate so the conflict never forms. |
29
+ | **Notification** | *in-flight* — after a concurrent change | surface the changed value; the actor resolves and re-issues. |
30
+
31
+ Use a claim when you will hold the row across a slow read→reason→write gap. Use a
32
+ notification when you didn't, and the premise moved under you.
33
+
34
+ ---
35
+
36
+ ## 2. The dispositions (`onStale`)
37
+
38
+ Every guarded write (and every read dependency, §4) declares how a stale premise
39
+ should be handled. Three modes, split by whether they **force** an outcome:
40
+
41
+ | mode | coercive? | what the engine does | who resolves | use when |
42
+ |---|---|---|---|---|
43
+ | `notify` | **No** — surface + delegate | Holds the write (does **not** apply it); returns a `StaleNotification` with the current value. | The actor (agent or human) reconciles and re-issues. | The aligned mode: tell the actor what changed, let it solve. |
44
+ | `reject` | **Yes** — force-abort | Throws `AbloStaleContextError`; the batch is discarded. | The caller retries from scratch. | Hard invariants; legacy/strict callers. The current default. |
45
+ | `overwrite` | **Yes** — force-clobber | Overwrites blindly last-writer-wins; **no** signal. | Nobody. | You genuinely own the field and concurrent values are noise. |
46
+
47
+ > `notify` is the convention. `reject` and `overwrite` are escape hatches for the
48
+ > two ends — "never let this be wrong" and "never bother me." They are not the
49
+ > spirit; they are the boundary of it.
50
+
51
+ ---
52
+
53
+ ## 3. What is checked: two footprints
54
+
55
+ A conflict is a **footprint intersection** — your operation's footprint overlaps
56
+ a concurrent delta. Ablo checks two footprints, and they are independent:
57
+
58
+ | footprint | declared by | question | scope |
59
+ |---|---|---|---|
60
+ | **Write-target** | per-op `readAt` | "did a row I'm **writing** change since I read it?" | the rows in `operations[]` |
61
+ | **Read-set** | batch-level `reads[]` | "did anything I **looked at** change since I read it?" | rows/groups in `reads[]`, even if not written |
62
+
63
+ The write-target check alone is the narrow case the canary anomaly defeats: an
64
+ agent reads `deal.stage`, writes `task.status`, and a peer moves `deal.stage` —
65
+ `task` never changed, so a write-target-only check waves it through. The read-set
66
+ closes that gap.
67
+
68
+ ---
69
+
70
+ ## 4. The read-set (`reads[]`)
71
+
72
+ A commit may declare, at the batch level, the premises its writes depended on.
73
+ Two granularities, developer's choice per entry:
74
+
75
+ ```ts
76
+ reads: [
77
+ { model: 'Slide', id: 's-1', readAt: N, fields?: ['title'] }, // ROW premise
78
+ { group: 'deck:abc', readAt: N, onStale: 'notify' }, // GROUP premise
79
+ ]
80
+ ```
81
+
82
+ - **Row** — did this specific row (optionally these fields) change? The literal
83
+ per-object premise.
84
+ - **Group** — did *anything* in this sync group change? `group` is a sync-group
85
+ key (`deck:abc`, `slide:s1`, `org:X`) — the same unit a participant **watches
86
+ and claims**. This is the more Ablo-native granularity.
87
+
88
+ **Boundary — a stale read fires over the whole batch.** A read dependency is a
89
+ premise for *all* the writes in the commit, so its disposition governs the batch:
90
+ `reject` aborts it, `notify` holds **every** write and notifies, `overwrite`
91
+ lets them land. Per-entry `onStale` defaults to `reject`.
92
+
93
+ ---
94
+
95
+ ## 5. The notification (`StaleNotification`)
96
+
97
+ The non-coercive modes hand back data instead of throwing. The signal is
98
+ delivered **twice**, by design — once as a value, once as an event:
99
+
100
+ - On the **commit receipt**: `receipt.notifications` (and `CommitResult.notifications`).
101
+ - On the **event channel**: `conflict:notified` (mirrors `reconciliation:needed` /
102
+ `sync:rollback`).
103
+
104
+ Shape (canonical in `coordination/schema.ts`):
105
+
106
+ | field | meaning |
107
+ |---|---|
108
+ | `object` | Stripe-style type tag — `'stale_notification'` |
109
+ | `model`, `id` | the conflicting row (for a group dep, both are the group key) |
110
+ | `group?` | set when this is a group-scoped notification |
111
+ | `readAt` | the watermark the committer reasoned against |
112
+ | `observedSyncId` | the newest delta on the premise — re-read at/after this |
113
+ | `conflictingFields` | fields that moved (empty for group / whole-entity) |
114
+ | `currentValues` | the live values of those fields — the premise to reconcile against (empty for group) |
115
+ | `writtenBy` | `{ kind, id }` of the concurrent author, reported faithfully |
116
+
117
+ Only `notify` produces a notification (the write was held). `reject` throws and
118
+ `overwrite` is silent — neither notifies.
119
+
120
+ ### 5.1 The receive → reconcile loop
121
+
122
+ You receive the signal two ways (same payload), then re-commit against the fresh
123
+ watermark. The engine never re-issues for you — the actor decides.
124
+
125
+ ```ts
126
+ // Trigger: a guarded write under the non-coercive mode.
127
+ const receipt = await ablo.task.update({
128
+ id, data: { status: 'blocked' },
129
+ readAt: myWatermark,
130
+ onStale: 'notify',
131
+ });
132
+
133
+ // Receive — pull: the held write surfaces on the receipt.
134
+ for (const n of receipt.notifications ?? []) reconcile(n);
135
+
136
+ // Receive — push: the same StaleNotification[] fires ambiently on the socket.
137
+ ws.subscribe('conflict:notified', ({ notifications }) => notifications.forEach(reconcile));
138
+
139
+ function reconcile(n: StaleNotification) {
140
+ // n.currentValues — what's actually there now (e.g. { status: 'done' })
141
+ // n.writtenBy — who moved it (e.g. { kind: 'agent', id: 'agent-b' })
142
+ if (!stillValid(n.currentValues)) return; // premise gone → drop the write
143
+
144
+ return ablo.task.update({
145
+ id: n.id,
146
+ data: { status: 'blocked' },
147
+ readAt: n.observedSyncId, // adopt the new high-water mark — this is what terminates the loop
148
+ onStale: 'notify',
149
+ });
150
+ }
151
+ ```
152
+
153
+ The loop **terminates** because each retry advances `readAt` to `observedSyncId`;
154
+ a peer that keeps writing only ever notifies you against a *newer* baseline, never
155
+ the same one twice. A group read-dep reconciles identically, except `group` is set
156
+ and `currentValues` is empty (re-read the group).
157
+
158
+ ---
159
+
160
+ ## 6. Boundaries & invariants
161
+
162
+ What the convention **guarantees**, and where it **stops**:
163
+
164
+ 1. **Engine surfaces, actor decides.** For `flag`/`merge` the engine never
165
+ repairs, merges, or re-plans. It reports `currentValues` and the actor (agent
166
+ or human) owns the resolution. The engine does not distinguish them — it is
167
+ actor-neutral by design.
168
+
169
+ 2. **Truthfulness.** `currentValues` / `observedSyncId` reflect committed state at
170
+ detection time, inside the same transaction as the write. A notification is
171
+ never speculative.
172
+
173
+ 3. **Termination (no livelock).** The monotonic `sync_id` landing order is the
174
+ serialization order. The stale committer always yields/recomputes — an
175
+ asymmetry that rules out the symmetric notify-rewrite livelock. Unbounded
176
+ retry is bounded by the client's reconciliation retry cap.
177
+
178
+ 4. **Scope: reversible DB state only.** The convention governs writes to the
179
+ shared database, which are inherently reversible (prior value in
180
+ `sync_deltas`). **Irreversible external side-effects** (emails, payments,
181
+ third-party calls) are *out of scope* — the engine cannot hold or undo them,
182
+ so they must not be gated by `flag`/`merge`.
183
+
184
+ 5. **Defaults.** A plain write (no `readAt`) is last-writer-wins with **no**
185
+ check. A guarded write with `readAt` but no `onStale` defaults to `reject`
186
+ (back-compat). *Open decision (§7).*
187
+
188
+ 6. **Policy seam.** Custom `ConflictPolicy` functions see **write-target**
189
+ conflicts (`stale_context` / `claim_held`). **Read-set** conflicts are
190
+ currently resolved directly via each entry's `onStale`, not through the policy
191
+ seam. *Open decision (§7).*
192
+
193
+ 7. **Claims win when held.** A non-holder writing to a claimed row is rejected
194
+ (`AbloClaimedError`) regardless of `readAt` — the prospective form takes
195
+ precedence over the in-flight form. Only `user`/`system` principals may
196
+ `bypass` a foreign claim; agents may not.
197
+
198
+ ---
199
+
200
+ ## 7. Open decisions (bounded, not yet made)
201
+
202
+ These are deliberately left open; they change behavior and are the user's call.
203
+
204
+ - **Default disposition for agents.** Should an agent-participant guarded write
205
+ default to `flag` (philosophy-aligned: surface, don't force) instead of
206
+ `reject` (back-compat)? Trade-off: alignment vs. a behavior change for existing
207
+ agent callers.
208
+ - **Read-deps through the policy seam.** Should read-set conflicts also pass
209
+ through `ConflictPolicy` (requires a group-aware conflict shape), or stay on
210
+ the direct `onStale` mapping?
211
+
212
+ ---
213
+
214
+ ## 8. Out of scope
215
+
216
+ - Irreversible external side-effects (§6.4) — not gated by this convention.
217
+ - Cross-object *serializability proof*. The read-set is a sound premise check,
218
+ not a full precedence-graph guarantee; it needs declared reads to catch a
219
+ premise, and a caller that declares none gets only write-target checking.
220
+ - Identity → participant-kind mapping. `writtenBy.kind` reports whatever
221
+ authenticated (an `sk_` key resolves to `system`, not `agent`); how identities
222
+ map to kinds is a separate concern.
@@ -1,5 +1,10 @@
1
1
  # Coordination Reference
2
2
 
3
+ > **Governing convention:** [`concurrency-convention.md`](./concurrency-convention.md)
4
+ > — the non-coercion principle (surface state, let the actor decide), the full
5
+ > `onStale` taxonomy, the read-set (`reads[]`), and the boundaries. Read that for
6
+ > the *why* and the contract; this reference is the *how* (claim mechanics + API).
7
+
3
8
  Coordinate long-running work on a row so humans and agents don't clobber each
4
9
  other. Most writes need none of this — a plain `ablo.<model>.update({ id, data })`
5
10
  is **last-write-wins** by default. For lost-update detection, take a claim or pass
@@ -205,6 +205,47 @@ await ablo.weatherReports.update({
205
205
  });
206
206
  ```
207
207
 
208
+ ## Local development & locked-down VPC — the reverse channel
209
+
210
+ The signed route above is an **inbound webhook**: Ablo Cloud calls your HTTPS
211
+ endpoint. That needs a public URL — which `localhost` doesn't have, and a
212
+ locked-down VPC won't expose. The reverse channel fixes both. A connector you run
213
+ next to your database dials an **outbound** WebSocket to Ablo Cloud and serves the
214
+ same `commit`/`load`/`list` requests over it — the Stripe-CLI `stripe listen`
215
+ pattern. No tunnel, no public endpoint, and your database credentials never leave
216
+ your process.
217
+
218
+ It wraps the **same handler** your deployed route uses — share the options object,
219
+ so there's zero handler drift:
220
+
221
+ ```ts
222
+ // scripts/ablo-connector.ts — run locally, or as a sidecar in a private VPC
223
+ import { dataSource, createSourceConnector } from '@abloatai/ablo';
224
+ import { sourceOptions } from '@/ablo/source'; // the same object route.ts passes
225
+
226
+ const connector = createSourceConnector({
227
+ apiKey: process.env.ABLO_API_KEY!, // sk_test_* for the dev loop
228
+ handler: dataSource(sourceOptions), // the unchanged (Request) => Response
229
+ });
230
+
231
+ const controller = new AbortController();
232
+ await connector.run(controller.signal); // dials out, serves until aborted
233
+ ```
234
+
235
+ When a connector is attached for a source, Ablo Cloud drains that source's
236
+ `commit`/`load`/`list` down the socket instead of POSTing the webhook; when none
237
+ is attached, the inbound webhook path is used unchanged. The drained requests
238
+ carry the **same** Standard Webhooks signature, so `dataSource` verifies them
239
+ exactly as on the webhook path — the transport changes, the trust model does not.
240
+
241
+ **Production / no-public-URL deploy.** By default the connector is gated to
242
+ `sk_test_*` keys (the dev-loop affordance). A customer who genuinely cannot expose
243
+ an inbound endpoint — a locked-down VPC — can run the connector as their deployed
244
+ production transport by opting that source into `reverseChannelProd` and using an
245
+ `sk_live_*` key. The inbound webhook remains the default for everyone else (it's
246
+ stateless and lower-latency); the reverse channel is the escape hatch for
247
+ no-inbound environments.
248
+
208
249
  ## Commit Request
209
250
 
210
251
  When Ablo calls your Data Source, it sends a signed JSON request:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -88,6 +88,11 @@
88
88
  "import": "./dist/keys/index.js",
89
89
  "default": "./dist/keys/index.js"
90
90
  },
91
+ "./auth": {
92
+ "types": "./dist/auth/index.d.ts",
93
+ "import": "./dist/auth/index.js",
94
+ "default": "./dist/auth/index.js"
95
+ },
91
96
  "./environment": {
92
97
  "types": "./dist/environment.d.ts",
93
98
  "import": "./dist/environment.js",