@abloatai/ablo 0.5.1 → 0.7.0

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 (129) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +248 -124
  3. package/dist/BaseSyncedStore.d.ts +3 -3
  4. package/dist/BaseSyncedStore.js +3 -3
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +91 -93
  8. package/dist/client/Ablo.js +122 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +116 -90
  14. package/dist/client/createModelProxy.js +128 -128
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +5 -5
  18. package/dist/coordination/index.d.ts +6 -0
  19. package/dist/coordination/index.js +6 -0
  20. package/dist/coordination/schema.d.ts +329 -0
  21. package/dist/coordination/schema.js +209 -0
  22. package/dist/core/QueryView.d.ts +4 -1
  23. package/dist/core/QueryView.js +1 -1
  24. package/dist/core/index.d.ts +2 -0
  25. package/dist/core/index.js +7 -0
  26. package/dist/core/query-utils.d.ts +7 -10
  27. package/dist/core/query-utils.js +2 -3
  28. package/dist/errorCodes.d.ts +264 -0
  29. package/dist/errorCodes.js +251 -0
  30. package/dist/errors.d.ts +59 -14
  31. package/dist/errors.js +73 -12
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +8 -12
  34. package/dist/interfaces/index.d.ts +2 -10
  35. package/dist/mutators/Transaction.d.ts +2 -2
  36. package/dist/mutators/Transaction.js +2 -2
  37. package/dist/mutators/mutateActions.d.ts +44 -0
  38. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  39. package/dist/mutators/readerActions.d.ts +32 -0
  40. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  41. package/dist/policy/index.d.ts +1 -1
  42. package/dist/policy/index.js +1 -1
  43. package/dist/policy/types.d.ts +31 -0
  44. package/dist/policy/types.js +15 -0
  45. package/dist/query/types.d.ts +1 -1
  46. package/dist/react/AbloProvider.d.ts +13 -1
  47. package/dist/react/AbloProvider.js +14 -6
  48. package/dist/react/context.d.ts +4 -4
  49. package/dist/react/index.d.ts +4 -5
  50. package/dist/react/index.js +3 -7
  51. package/dist/react/useAblo.d.ts +14 -14
  52. package/dist/react/useAblo.js +26 -26
  53. package/dist/react/useIntent.d.ts +2 -2
  54. package/dist/react/useIntent.js +2 -2
  55. package/dist/react/useMutators.d.ts +1 -1
  56. package/dist/react/usePresence.d.ts +3 -3
  57. package/dist/react/usePresence.js +4 -4
  58. package/dist/react/useUndoScope.d.ts +1 -1
  59. package/dist/schema/ddl.d.ts +62 -0
  60. package/dist/schema/ddl.js +317 -0
  61. package/dist/schema/diff.d.ts +167 -0
  62. package/dist/schema/diff.js +280 -0
  63. package/dist/schema/field.d.ts +16 -19
  64. package/dist/schema/field.js +30 -17
  65. package/dist/schema/generate.d.ts +19 -0
  66. package/dist/schema/generate.js +87 -0
  67. package/dist/schema/index.d.ts +9 -3
  68. package/dist/schema/index.js +14 -2
  69. package/dist/schema/model.d.ts +87 -25
  70. package/dist/schema/model.js +33 -3
  71. package/dist/schema/relation.d.ts +17 -0
  72. package/dist/schema/roles.d.ts +148 -0
  73. package/dist/schema/roles.js +149 -0
  74. package/dist/schema/schema.d.ts +10 -69
  75. package/dist/schema/schema.js +58 -24
  76. package/dist/schema/select.d.ts +25 -0
  77. package/dist/schema/select.js +55 -0
  78. package/dist/schema/serialize.d.ts +96 -0
  79. package/dist/schema/serialize.js +231 -0
  80. package/dist/schema/sugar.d.ts +20 -3
  81. package/dist/schema/sugar.js +5 -1
  82. package/dist/schema/tenancy.d.ts +66 -0
  83. package/dist/schema/tenancy.js +58 -0
  84. package/dist/sync/HydrationCoordinator.d.ts +2 -0
  85. package/dist/sync/HydrationCoordinator.js +23 -17
  86. package/dist/sync/SyncWebSocket.d.ts +17 -0
  87. package/dist/sync/SyncWebSocket.js +46 -1
  88. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  89. package/dist/sync/awaitIntentGrant.js +60 -0
  90. package/dist/sync/createIntentStream.d.ts +2 -1
  91. package/dist/sync/createIntentStream.js +89 -5
  92. package/dist/sync/createPresenceStream.js +1 -1
  93. package/dist/sync/participants.d.ts +2 -2
  94. package/dist/sync/participants.js +9 -18
  95. package/dist/types/global.d.ts +43 -52
  96. package/dist/types/global.js +16 -18
  97. package/dist/types/streams.d.ts +90 -42
  98. package/docs/api-keys.md +44 -0
  99. package/docs/api.md +72 -173
  100. package/docs/audit.md +5 -5
  101. package/docs/cli.md +212 -0
  102. package/docs/client-behavior.md +42 -43
  103. package/docs/coordination.md +343 -0
  104. package/docs/data-sources.md +16 -16
  105. package/docs/examples/agent-human.md +30 -32
  106. package/docs/examples/ai-sdk-tool.md +32 -33
  107. package/docs/examples/existing-python-backend.md +38 -36
  108. package/docs/examples/nextjs.md +24 -25
  109. package/docs/examples/scoped-agent.md +78 -0
  110. package/docs/examples/server-agent.md +20 -61
  111. package/docs/guarantees.md +34 -56
  112. package/docs/identity.md +529 -0
  113. package/docs/index.md +18 -24
  114. package/docs/integration-guide.md +130 -144
  115. package/docs/interaction-model.md +32 -95
  116. package/docs/mcp/claude-code.md +3 -3
  117. package/docs/mcp/cursor.md +1 -1
  118. package/docs/mcp/windsurf.md +1 -1
  119. package/docs/mcp.md +11 -26
  120. package/docs/quickstart.md +43 -49
  121. package/docs/react.md +74 -24
  122. package/docs/roadmap.md +17 -7
  123. package/llms.txt +34 -39
  124. package/package.json +8 -1
  125. package/dist/react/useMutate.d.ts +0 -83
  126. package/dist/react/useQuery.d.ts +0 -123
  127. package/dist/react/useQuery.js +0 -145
  128. package/dist/react/useReader.d.ts +0 -69
  129. package/docs/capabilities.md +0 -163
@@ -1,27 +1,25 @@
1
1
  /**
2
- * Typed-global augmentation point for SDK consumers.
2
+ * Type registration point for SDK consumers.
3
3
  *
4
- * Consumers declare their Schema, Presence, Intents, and UserMeta ONCE in a
5
- * `.d.ts` file and every SDK hook — `useQuery`, `useOne`, `useMutate`,
6
- * `usePresence`, `useIntent` — reads its types from the resolved global.
7
- * No generics at call sites. No `schema` runtime arg passed per hook call.
4
+ * A consumer registers their Schema, Presence, Intents, and UserMeta ONCE by
5
+ * augmenting the {@link Register} interface, and every SDK hook — `useAblo`,
6
+ * `useQuery`, `useOne`, `usePresence`, `useIntent` — reads its types from the
7
+ * resolved registration. No generics at call sites, no `schema` arg per call.
8
8
  *
9
- * This is the same canonical TypeScript declaration-merging pattern that
10
- * Next.js uses for `process.env` / `NodeJS.ProcessEnv`, that CSS Modules
11
- * use for `declare module '*.module.css'`, and that Liveblocks uses for
12
- * `interface Liveblocks`. It's a language feature, not a library trick
13
- * any file in the compilation can augment the global `AbloSync` interface
14
- * and every consumer of the resolved types below picks up the augmentation
15
- * automatically.
9
+ * Registration is done via **module augmentation** of `@abloatai/ablo`
10
+ * the same pattern TanStack Router uses for its `Register` interface. The brand
11
+ * lives in the module specifier, so the interface is just `Register` (not a
12
+ * global, not prefixed). It's a language feature, not a library trick: any file
13
+ * in the compilation can augment it and every resolver below picks it up.
16
14
  *
17
15
  * Consumer example:
18
16
  *
19
17
  * ```ts
20
- * // apps/your-app/src/ablo-sync.d.ts
18
+ * // apps/your-app/src/ablo.d.ts
21
19
  * import type { schema } from './your-schema';
22
20
  *
23
- * declare global {
24
- * interface AbloSync {
21
+ * declare module '@abloatai/ablo' {
22
+ * interface Register {
25
23
  * Schema: typeof schema;
26
24
  * Presence: { cursor: { x: number; y: number } | null };
27
25
  * Intents: { editLayer: { layerId: string } };
@@ -31,17 +29,15 @@
31
29
  * export {};
32
30
  * ```
33
31
  *
34
- * If `AbloSync` is never declared, every resolver falls back to the
35
- * `DefaultSyncShape` — a loose `Record<string, unknown>` shape that keeps
36
- * SDK consumers compiling without typed benefits until they opt in.
32
+ * If `Register` is never augmented, every resolver falls back to
33
+ * {@link DefaultSyncShape} — a loose shape that keeps consumers compiling
34
+ * without typed benefits until they opt in.
37
35
  */
38
36
  /**
39
- * Default fallback shapes used when the consumer hasn't declared
40
- * `interface AbloSync`. `DefaultSyncShape.Schema` is intentionally
41
- * structural — it carries `{ models: Record<string, unknown> }` so hooks
42
- * can still validate the model key argument against *something*, just
43
- * without producing a typed entity shape. Once the consumer augments the
44
- * global, every resolver below picks up the augmented types automatically.
37
+ * Default fallback shapes used when the consumer hasn't augmented
38
+ * {@link Register}. `DefaultSyncShape.Schema` is intentionally structural — it
39
+ * carries `{ models: Record<string, unknown> }` so hooks can still validate the
40
+ * model key argument against *something*, just without a typed entity shape.
45
41
  */
46
42
  export interface DefaultSyncShape {
47
43
  readonly Schema: {
@@ -53,54 +49,49 @@ export interface DefaultSyncShape {
53
49
  readonly id: string;
54
50
  };
55
51
  }
56
- declare global {
57
- /**
58
- * Global augmentation target. Consumers augment this via
59
- * `declare global { interface AbloSync { Schema: ...; Presence: ...; ... } }`.
60
- * Empty by default every SDK resolver falls back to {@link DefaultSyncShape}
61
- * when an expected key is absent.
62
- */
63
- interface AbloSync {
64
- }
52
+ /**
53
+ * The registration interface. Consumers augment it via
54
+ * `declare module '@abloatai/ablo' { interface Register { Schema: ...; … } }`.
55
+ * Empty by default every SDK resolver falls back to {@link DefaultSyncShape}
56
+ * when an expected key is absent. Exported from the package root so the module
57
+ * augmentation merges into this declaration.
58
+ */
59
+ export interface Register {
65
60
  }
66
61
  /**
67
- * The consumer's schema, or the default shape if undeclared. Hooks use
68
- * this to type their model-key argument and to infer the entity type
69
- * returned from queries/mutations.
62
+ * The consumer's schema, or the default shape if unregistered. Hooks use this
63
+ * to type their model-key argument and infer the entity type returned.
70
64
  */
71
- export type ResolveSchema = AbloSync extends {
65
+ export type ResolveSchema = Register extends {
72
66
  Schema: infer S;
73
67
  } ? S extends {
74
68
  models: Record<string, unknown>;
75
69
  } ? S : DefaultSyncShape['Schema'] : DefaultSyncShape['Schema'];
76
70
  /**
77
- * The consumer's presence shape, or the default shape if undeclared.
78
- * Used by `usePresence`. The shape is free-form — any serializable JSON
79
- * the consumer wants to broadcast per session.
71
+ * The consumer's presence shape, or the default if unregistered. Used by
72
+ * `usePresence`. Free-form — any serializable JSON broadcast per session.
80
73
  */
81
- export type ResolvePresence = AbloSync extends {
74
+ export type ResolvePresence = Register extends {
82
75
  Presence: infer P;
83
76
  } ? P : DefaultSyncShape['Presence'];
84
77
  /**
85
- * The consumer's intent vocabulary, or the default if undeclared. Keys
86
- * are intent names; values are the claim payload for each intent. Used
87
- * by `useIntent(intentName)`.
78
+ * The consumer's intent vocabulary, or the default if unregistered. Keys are
79
+ * intent names; values are the claim payload for each intent. Used by
80
+ * `useIntent(intentName)`.
88
81
  */
89
- export type ResolveIntents = AbloSync extends {
82
+ export type ResolveIntents = Register extends {
90
83
  Intents: infer I;
91
84
  } ? I : DefaultSyncShape['Intents'];
92
85
  /**
93
- * The consumer's user-metadata shape, or the default if undeclared.
94
- * Carries identity info the consumer trusts from their auth layer —
95
- * the SDK doesn't validate this.
86
+ * The consumer's user-metadata shape, or the default if unregistered. Carries
87
+ * identity info the consumer trusts from their auth layer — not SDK-validated.
96
88
  */
97
- export type ResolveUserMeta = AbloSync extends {
89
+ export type ResolveUserMeta = Register extends {
98
90
  UserMeta: infer U;
99
91
  } ? U : DefaultSyncShape['UserMeta'];
100
92
  /**
101
- * The keys of the consumer's schema models. `useQuery(modelKey)` narrows
102
- * its first argument to this union, so unknown key literals fail at
103
- * compile time.
93
+ * The keys of the consumer's schema models. `useQuery(modelKey)` narrows its
94
+ * first argument to this union, so unknown key literals fail at compile time.
104
95
  */
105
96
  export type ResolveModelKey = ResolveSchema extends {
106
97
  models: infer M;
@@ -1,27 +1,25 @@
1
1
  /**
2
- * Typed-global augmentation point for SDK consumers.
2
+ * Type registration point for SDK consumers.
3
3
  *
4
- * Consumers declare their Schema, Presence, Intents, and UserMeta ONCE in a
5
- * `.d.ts` file and every SDK hook — `useQuery`, `useOne`, `useMutate`,
6
- * `usePresence`, `useIntent` — reads its types from the resolved global.
7
- * No generics at call sites. No `schema` runtime arg passed per hook call.
4
+ * A consumer registers their Schema, Presence, Intents, and UserMeta ONCE by
5
+ * augmenting the {@link Register} interface, and every SDK hook — `useAblo`,
6
+ * `useQuery`, `useOne`, `usePresence`, `useIntent` — reads its types from the
7
+ * resolved registration. No generics at call sites, no `schema` arg per call.
8
8
  *
9
- * This is the same canonical TypeScript declaration-merging pattern that
10
- * Next.js uses for `process.env` / `NodeJS.ProcessEnv`, that CSS Modules
11
- * use for `declare module '*.module.css'`, and that Liveblocks uses for
12
- * `interface Liveblocks`. It's a language feature, not a library trick
13
- * any file in the compilation can augment the global `AbloSync` interface
14
- * and every consumer of the resolved types below picks up the augmentation
15
- * automatically.
9
+ * Registration is done via **module augmentation** of `@abloatai/ablo`
10
+ * the same pattern TanStack Router uses for its `Register` interface. The brand
11
+ * lives in the module specifier, so the interface is just `Register` (not a
12
+ * global, not prefixed). It's a language feature, not a library trick: any file
13
+ * in the compilation can augment it and every resolver below picks it up.
16
14
  *
17
15
  * Consumer example:
18
16
  *
19
17
  * ```ts
20
- * // apps/your-app/src/ablo-sync.d.ts
18
+ * // apps/your-app/src/ablo.d.ts
21
19
  * import type { schema } from './your-schema';
22
20
  *
23
- * declare global {
24
- * interface AbloSync {
21
+ * declare module '@abloatai/ablo' {
22
+ * interface Register {
25
23
  * Schema: typeof schema;
26
24
  * Presence: { cursor: { x: number; y: number } | null };
27
25
  * Intents: { editLayer: { layerId: string } };
@@ -31,8 +29,8 @@
31
29
  * export {};
32
30
  * ```
33
31
  *
34
- * If `AbloSync` is never declared, every resolver falls back to the
35
- * `DefaultSyncShape` — a loose `Record<string, unknown>` shape that keeps
36
- * SDK consumers compiling without typed benefits until they opt in.
32
+ * If `Register` is never augmented, every resolver falls back to
33
+ * {@link DefaultSyncShape} — a loose shape that keeps consumers compiling
34
+ * without typed benefits until they opt in.
37
35
  */
38
36
  export {};
@@ -9,6 +9,8 @@
9
9
  * the shared coordination substrate.
10
10
  */
11
11
  import type { InferModel, Schema } from '../schema/schema.js';
12
+ import type { TargetRange, OnStaleMode, IntentClaim, PresenceKind } from '../coordination/schema.js';
13
+ export type { TargetRange, OnStaleMode, IntentClaim, PresenceKind };
12
14
  /**
13
15
  * Any JSON-serializable value. Used where the SDK accepts free-form
14
16
  * metadata that will be persisted / transported as JSON — avoids
@@ -113,13 +115,6 @@ export interface ContextChange {
113
115
  * snapshot. Defaults to `'reject'` when `readAt` is provided without
114
116
  * `onStale`.
115
117
  */
116
- export type OnStaleMode = 'reject' | 'flag' | 'merge' | 'force';
117
- export interface TargetRange {
118
- readonly startLine: number;
119
- readonly endLine: number;
120
- readonly startColumn?: number;
121
- readonly endColumn?: number;
122
- }
123
118
  /**
124
119
  * A pointer to one entity, optionally narrowed to a structured
125
120
  * subtarget. `type` and `id` are customer schema vocabulary; `path`,
@@ -175,7 +170,7 @@ export interface PresenceStream {
175
170
  /**
176
171
  * Reactive view of every OTHER participant's current activity on
177
172
  * this participant's sync groups. Reads return the current snapshot;
178
- * pair with `subscribe(listener)` below to get notified on changes.
173
+ * pair with `onChange(listener)` below to get notified on changes.
179
174
  *
180
175
  * An LLM pipeline can include `presence.others` in its system prompt
181
176
  * so the model literally reasons with knowledge of what other
@@ -209,7 +204,7 @@ export interface PresenceStream {
209
204
  * });
210
205
  * ```
211
206
  */
212
- subscribe(listener: () => void): () => void;
207
+ onChange(listener: () => void): () => void;
213
208
  /**
214
209
  * Async-iterable view of the peer roster. Each iteration yields the
215
210
  * current `others` snapshot on every mutation — so the consumer
@@ -304,32 +299,6 @@ export interface Peer {
304
299
  /** Pending-mutation intents this participant has declared. */
305
300
  readonly activeIntents?: ReadonlyArray<IntentClaim>;
306
301
  }
307
- /**
308
- * Pending-mutation intent on the wire. Declared via `intent_begin`,
309
- * cleared on `intent_abandon` / commit / disconnect / TTL expiry.
310
- * Server stamps `declaredAt` and `expiresAt` (ms epoch). The SDK's
311
- * `IntentStream.others` exposes a richer `ActiveIntent` view (defined
312
- * below) that adds `heldBy` so callers know which participant owns it.
313
- */
314
- export interface IntentClaim {
315
- readonly intentId: string;
316
- readonly entityType: string;
317
- readonly entityId: string;
318
- readonly path?: string;
319
- readonly range?: TargetRange;
320
- readonly action: string;
321
- readonly field?: string;
322
- readonly meta?: Record<string, unknown>;
323
- readonly declaredAt: number;
324
- readonly expiresAt: number;
325
- }
326
- /**
327
- * Transition type carried on every presence frame from the server.
328
- * - `'enter'` — first frame the receiver sees for this peer.
329
- * - `'update'` — activity / intent change on an already-known peer.
330
- * - `'leave'` — peer departed (explicit disconnect or TTL expiry).
331
- */
332
- export type PresenceKind = 'enter' | 'update' | 'leave';
333
302
  /** Outbound `presence_update` payload. */
334
303
  export interface PresenceUpdatePayload {
335
304
  readonly status: 'online' | 'away' | 'offline' | (string & {});
@@ -371,6 +340,15 @@ export interface ClaimOptions extends IntentOptions {
371
340
  * app-specific phases.
372
341
  */
373
342
  readonly reason?: string;
343
+ /**
344
+ * Join the server's fair FIFO queue on contention instead of being
345
+ * rejected. The grant arrives asynchronously (`intent_acquired` if the
346
+ * target was free, `intent_granted` once promoted to the head of the line).
347
+ * The low-level `claim` returns its handle immediately regardless; callers
348
+ * that need to *wait* for the grant use the awaiting wrappers
349
+ * (`ablo.<model>.claim`), which pair this flag with `awaitIntentGrant`.
350
+ */
351
+ readonly queue?: boolean;
374
352
  }
375
353
  export interface IntentStream {
376
354
  /**
@@ -394,6 +372,23 @@ export interface IntentStream {
394
372
  * below to get notified on change.
395
373
  */
396
374
  readonly others: ReadonlyArray<ActiveIntent>;
375
+ /**
376
+ * Reactive view of the wait queue on one target — the FIFO line of
377
+ * `status: 'queued'` intents behind the current holder, each with its
378
+ * `action`, `heldBy`, and `position`. Synced from the server's per-entity
379
+ * `intent_queue` frame; empty when no one's waiting. Pair with
380
+ * `subscribe(...)` for change notifications.
381
+ */
382
+ queueFor(target: PresenceTarget): readonly Intent[];
383
+ /**
384
+ * Re-rank the wait queue on a target — move the listed waiters to the front
385
+ * in the given order; unlisted waiters keep their relative FIFO order behind
386
+ * them. Pass the `Intent[]` from `queueFor(target)` in the order you want
387
+ * (each `Intent` carries its `heldBy` + `id`). Privileged: the server gates
388
+ * it (a participant lacking the `intent.reorder` capability is denied), so
389
+ * this is fire-and-forget — the new order arrives reactively via `queueFor`.
390
+ */
391
+ reorder(target: PresenceTarget, order: readonly Intent[]): void;
397
392
  /**
398
393
  * Framework-agnostic reactivity. Same contract as
399
394
  * `PresenceStream.subscribe` — register a listener fired on every
@@ -401,7 +396,7 @@ export interface IntentStream {
401
396
  * returns an unsubscribe fn. Use `useSyncExternalStore` in React or
402
397
  * `autorun` in MobX.
403
398
  */
404
- subscribe(listener: () => void): () => void;
399
+ onChange(listener: () => void): () => void;
405
400
  /**
406
401
  * Observe server-side intent rejections. Fires when the server
407
402
  * rejects an `intents.writing(...)` / `announce(...)` call because
@@ -418,6 +413,23 @@ export interface IntentStream {
418
413
  * Returns an unsubscribe fn.
419
414
  */
420
415
  onRejected(listener: (rejection: IntentRejection) => void): () => void;
416
+ /**
417
+ * Observe LOSING an intent you held — distinct from `onRejected` (a claim the
418
+ * server refused). Fires on the server's `intent_lost` frame, carrying why:
419
+ * `'preempted'` (a privileged participant evicted you) or `'expired'` (your
420
+ * TTL lapsed). Lets a holder react — re-plan vs re-claim — instead of
421
+ * silently discovering the lease gone via presence.
422
+ *
423
+ * ```ts
424
+ * participant.intents.onLost((lost) => {
425
+ * if (lost.reason === 'preempted') replanAgainst(lost.target);
426
+ * else reclaim(lost.target);
427
+ * });
428
+ * ```
429
+ *
430
+ * Returns an unsubscribe fn.
431
+ */
432
+ onLost(listener: (lost: IntentLost) => void): () => void;
421
433
  /**
422
434
  * Async-iterable view of everyone else's open intents. Each
423
435
  * iteration yields the current snapshot on every mutation.
@@ -456,6 +468,31 @@ export interface IntentRejection {
456
468
  /** When the existing claim expires (ms since epoch). */
457
469
  readonly heldByExpiresAt: number;
458
470
  }
471
+ /**
472
+ * You LOST an intent you were HOLDING — distinct from `IntentRejection` (a
473
+ * claim the server refused you). Delivered via `onLost`.
474
+ */
475
+ export interface IntentLost {
476
+ /** The held claim's id that you just lost. */
477
+ readonly intentId: string;
478
+ /**
479
+ * How you lost it. `'preempted'`: a privileged participant (one holding the
480
+ * `intent.preempt` capability) evicted you and took the lease — its work now
481
+ * supersedes yours, so re-plan against the new holder rather than blindly
482
+ * re-claiming. `'expired'`: your TTL lapsed without finishing — re-claim if
483
+ * you still need it.
484
+ */
485
+ readonly reason: 'expired' | 'preempted';
486
+ /** The target you no longer hold. */
487
+ readonly target: {
488
+ readonly entityType: string;
489
+ readonly entityId: string;
490
+ readonly path?: string;
491
+ readonly range?: TargetRange;
492
+ readonly field?: string;
493
+ readonly meta?: Record<string, unknown>;
494
+ };
495
+ }
459
496
  export interface IntentDeclaration {
460
497
  readonly target: EntityRef;
461
498
  /** Human-readable reason — "rewriting title" / "restyling chart". */
@@ -494,8 +531,13 @@ export interface ActiveIntent extends IntentDeclaration {
494
531
  readonly announcedAt: string;
495
532
  readonly expiresAt: string;
496
533
  }
497
- /** Every lifecycle state of a coordination intent, in one enum. */
498
- export type IntentStatus = 'active' | 'committed' | 'expired' | 'canceled';
534
+ /**
535
+ * Every lifecycle state of a coordination intent, in one enum.
536
+ * `active` = the current holder (the lock). `queued` = waiting in the FIFO
537
+ * line behind the holder (carries `position`). The terminal states drop the
538
+ * intent from the synced set.
539
+ */
540
+ export type IntentStatus = 'active' | 'queued' | 'committed' | 'expired' | 'canceled';
499
541
  /** Options for waiting on a target to become free. */
500
542
  export interface IntentWaitOptions {
501
543
  readonly timeout?: number;
@@ -509,10 +551,11 @@ export interface IntentWaitOptions {
509
551
  *
510
552
  * Deliberately omits a Stripe-style `next_action`: a contender's only
511
553
  * response is "wait until free, then re-read", and the runtime performs
512
- * that uniformly at the tool boundary (`IntentHandle.whenFree()` + the
513
- * stale-context guard that forces a re-read). Encoding a constant
514
- * instruction the engine always takes would be the kind of ceremony this
515
- * object exists to remove.
554
+ * that uniformly `claim` serializes behind the holder via the server
555
+ * FIFO queue (or low-level `intents.waitFor` to wait without claiming), and the
556
+ * stale-context guard forces the re-read. Encoding a constant instruction
557
+ * the engine always takes would be the kind of ceremony this object exists
558
+ * to remove.
516
559
  */
517
560
  export interface Intent {
518
561
  readonly object: 'intent';
@@ -532,4 +575,9 @@ export interface Intent {
532
575
  readonly createdAt?: string;
533
576
  /** Ms-epoch the server auto-expires it if the holder doesn't finish. */
534
577
  readonly expiresAt: string;
578
+ /**
579
+ * 0-based place in the FIFO line — present only when `status: 'queued'`
580
+ * (`0` = next in line behind the holder). Absent for the active holder.
581
+ */
582
+ readonly position?: number;
535
583
  }
package/docs/api-keys.md CHANGED
@@ -22,3 +22,47 @@ Use API keys from trusted runtimes:
22
22
  - webhooks
23
23
 
24
24
  Never ship a secret API key to a browser bundle.
25
+
26
+ ## Test mode and sandboxes
27
+
28
+ Test and live keys are the same shape; the prefix names the environment:
29
+
30
+ - `sk_test_…` — a key bound to a **sandbox**. Its reads and writes are isolated
31
+ to that sandbox and are invisible to live keys (and to other sandboxes).
32
+ - `sk_live_…` — a key against your live data.
33
+
34
+ Every org has a default **Test mode** sandbox, plus any number of additional
35
+ sandboxes you create. **Data is isolated per sandbox; the schema is shared
36
+ across the whole org.** A schema you push from a test key defines the same
37
+ models your live keys see — only the rows differ. This mirrors how Stripe
38
+ separates test and live data while keeping the API shape identical.
39
+
40
+ ## Scopes
41
+
42
+ Keys carry scopes following the principle of least privilege — each key gets
43
+ only what its job needs. A secret key with **no scopes** has full org authority
44
+ (the default for a `sk_live_` backend key); a key with a non-empty scope set is
45
+ restricted to exactly those grants:
46
+
47
+ - `schema:push` — author the org schema (`ablo schema push`, `ablo dev`). A
48
+ high-risk, org-wide grant: because schema is shared, a push affects the live
49
+ table shape. A full-authority key has it implicitly; a *restricted* key (such
50
+ as a sandbox key) needs it granted explicitly.
51
+ - `sandbox:<id>` — marks the key as belonging to a sandbox (its data isolation
52
+ comes from the sandbox binding, not this scope string).
53
+
54
+ A key minted from the default **Test mode** sandbox carries `schema:push`, so
55
+ `ablo dev` works out of the box. Keys from other sandboxes are **data-only** by
56
+ default — enable "schema authoring" when minting if you want that key to push
57
+ schema too. Hand data-only keys to embedded apps and CI agents; reserve
58
+ schema-authoring keys for the developer running `ablo dev`.
59
+
60
+ ### `ablo dev`
61
+
62
+ ```sh
63
+ ABLO_API_KEY=sk_test_… npx ablo dev
64
+ ```
65
+
66
+ Pushes your `ablo/schema.ts` to the test sandbox, prints the one line you need
67
+ in `.env.local`, and re-pushes on every save. It refuses `sk_live_` keys so a
68
+ tight save loop can never churn production data.