@abloatai/ablo 0.3.1 → 0.4.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 (87) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/NOTICE +2 -2
  3. package/README.md +27 -25
  4. package/dist/agent/Agent.d.ts +1 -1
  5. package/dist/agent/Agent.js +1 -1
  6. package/dist/agent/index.d.ts +4 -4
  7. package/dist/agent/index.js +6 -6
  8. package/dist/agent/types.d.ts +1 -1
  9. package/dist/ai-sdk/index.d.ts +3 -3
  10. package/dist/ai-sdk/index.js +3 -3
  11. package/dist/ai-sdk/intent-broadcast.d.ts +1 -1
  12. package/dist/ai-sdk/intent-broadcast.js +1 -1
  13. package/dist/auth/index.d.ts +1 -1
  14. package/dist/client/Ablo.d.ts +8 -14
  15. package/dist/client/Ablo.js +32 -1
  16. package/dist/client/auth.d.ts +3 -3
  17. package/dist/client/auth.js +5 -5
  18. package/dist/client/createModelProxy.d.ts +110 -32
  19. package/dist/client/createModelProxy.js +77 -38
  20. package/dist/client/index.d.ts +2 -2
  21. package/dist/client/index.js +2 -2
  22. package/dist/config/index.d.ts +1 -1
  23. package/dist/config/index.js +1 -1
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/core/index.js +2 -2
  26. package/dist/errors.d.ts +1 -1
  27. package/dist/errors.js +1 -1
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +9 -9
  30. package/dist/interfaces/headless.d.ts +1 -1
  31. package/dist/interfaces/headless.js +2 -2
  32. package/dist/policy/index.d.ts +2 -2
  33. package/dist/policy/index.js +2 -2
  34. package/dist/principal.d.ts +1 -1
  35. package/dist/principal.js +1 -1
  36. package/dist/react/ClientSideSuspense.d.ts +1 -1
  37. package/dist/react/SyncGroupProvider.js +1 -1
  38. package/dist/react/context.d.ts +1 -1
  39. package/dist/react/context.js +1 -1
  40. package/dist/react/index.d.ts +1 -1
  41. package/dist/react/index.js +1 -1
  42. package/dist/react/useCurrentUserId.js +1 -1
  43. package/dist/react/useErrorListener.js +1 -1
  44. package/dist/react/useMutate.d.ts +1 -1
  45. package/dist/react/useMutationFailureListener.js +1 -1
  46. package/dist/react/useReader.d.ts +1 -1
  47. package/dist/schema/field.d.ts +1 -1
  48. package/dist/schema/field.js +1 -1
  49. package/dist/schema/index.d.ts +2 -2
  50. package/dist/schema/index.js +2 -2
  51. package/dist/schema/model.d.ts +2 -2
  52. package/dist/schema/model.js +2 -2
  53. package/dist/schema/queries.d.ts +1 -1
  54. package/dist/schema/queries.js +1 -1
  55. package/dist/schema/relation.d.ts +1 -1
  56. package/dist/schema/relation.js +1 -1
  57. package/dist/schema/schema.d.ts +1 -1
  58. package/dist/schema/schema.js +1 -1
  59. package/dist/source/index.d.ts +22 -28
  60. package/dist/source/index.js +23 -20
  61. package/dist/source/pushQueue.d.ts +1 -1
  62. package/dist/source/pushQueue.js +2 -2
  63. package/dist/sync/SyncWebSocket.d.ts +14 -0
  64. package/dist/sync/createIntentStream.js +7 -0
  65. package/dist/testing/fixtures/models.d.ts +1 -1
  66. package/dist/testing/fixtures/models.js +1 -1
  67. package/dist/testing/helpers/react-wrapper.d.ts +2 -2
  68. package/dist/testing/helpers/react-wrapper.js +2 -2
  69. package/dist/testing/index.d.ts +1 -1
  70. package/dist/testing/index.js +1 -1
  71. package/dist/types/streams.d.ts +39 -0
  72. package/docs/api.md +78 -20
  73. package/docs/data-sources.md +50 -16
  74. package/docs/examples/ai-sdk-tool.md +14 -31
  75. package/docs/examples/existing-python-backend.md +6 -6
  76. package/docs/integration-guide.md +8 -7
  77. package/docs/interaction-model.md +16 -4
  78. package/docs/mcp.md +1 -1
  79. package/docs/quickstart.md +20 -18
  80. package/examples/data-source/README.md +1 -1
  81. package/examples/data-source/ablo-driver.ts +5 -5
  82. package/examples/data-source/customer-server.ts +10 -10
  83. package/examples/data-source/run.ts +9 -11
  84. package/examples/data-source/schema.ts +1 -1
  85. package/examples/quickstart.ts +2 -2
  86. package/llms.txt +1 -1
  87. package/package.json +1 -1
@@ -27,7 +27,7 @@ export function useCurrentUserId() {
27
27
  if (!ctx) {
28
28
  throw new AbloValidationError('useCurrentUserId: no <AbloProvider> mounted above this component. ' +
29
29
  'Wrap your tree with <AbloProvider ...> from ' +
30
- '@ablo/sync-engine/react.', { code: 'no_ablo_provider' });
30
+ '@abloatai/ablo/react.', { code: 'no_ablo_provider' });
31
31
  }
32
32
  return ctx.currentUserId;
33
33
  }
@@ -25,7 +25,7 @@ export function useErrorListener(listener) {
25
25
  const ctx = useContext(AbloInternalContext);
26
26
  if (!ctx) {
27
27
  throw new AbloValidationError('useErrorListener: no <AbloProvider> mounted above this component. ' +
28
- 'Wrap your tree with <AbloProvider ...> from @ablo/sync-engine/react.', { code: 'no_ablo_provider' });
28
+ 'Wrap your tree with <AbloProvider ...> from @abloatai/ablo/react.', { code: 'no_ablo_provider' });
29
29
  }
30
30
  // Stash the latest callback in a ref so the effect subscription
31
31
  // stays stable across renders. Matches the `useEventCallback`
@@ -13,7 +13,7 @@ type GlobalMutateActions<K extends string> = ResolveSchema extends Schema ? K ex
13
13
  *
14
14
  * @example
15
15
  * import { schema } from '@ablo/schema';
16
- * import { useMutate } from '@ablo/sync-engine/react';
16
+ * import { useMutate } from '@abloatai/ablo/react';
17
17
  *
18
18
  * const tasks = useMutate(schema, 'tasks');
19
19
  *
@@ -25,7 +25,7 @@ export function useMutationFailureListener(listener) {
25
25
  const ctx = useContext(AbloInternalContext);
26
26
  if (!ctx) {
27
27
  throw new AbloValidationError('useMutationFailureListener: no <AbloProvider> mounted above this component. ' +
28
- 'Wrap your tree with <AbloProvider ...> from @ablo/sync-engine/react.', { code: 'no_ablo_provider' });
28
+ 'Wrap your tree with <AbloProvider ...> from @abloatai/ablo/react.', { code: 'no_ablo_provider' });
29
29
  }
30
30
  const ref = useRef(listener);
31
31
  ref.current = listener;
@@ -21,7 +21,7 @@ type GlobalReaderActions<K extends string> = ResolveSchema extends Schema ? K ex
21
21
  *
22
22
  * @example
23
23
  * import { schema } from '@ablo/schema';
24
- * import { useReader } from '@ablo/sync-engine/react';
24
+ * import { useReader } from '@abloatai/ablo/react';
25
25
  *
26
26
  * function useTaskMutations() {
27
27
  * const read = useReader(schema, 'tasks');
@@ -6,7 +6,7 @@
6
6
  * survives `.optional()`, `.nullable()`, and `.default()` chain calls.
7
7
  *
8
8
  * Usage:
9
- * import { field } from '@ablo/sync-engine/schema';
9
+ * import { field } from '@abloatai/ablo/schema';
10
10
  *
11
11
  * const tasks = model({
12
12
  * title: field.string(),
@@ -6,7 +6,7 @@
6
6
  * survives `.optional()`, `.nullable()`, and `.default()` chain calls.
7
7
  *
8
8
  * Usage:
9
- * import { field } from '@ablo/sync-engine/schema';
9
+ * import { field } from '@abloatai/ablo/schema';
10
10
  *
11
11
  * const tasks = model({
12
12
  * title: field.string(),
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @ablo/sync-engine/schema — Schema Definition DSL
2
+ * @abloatai/ablo/schema — Schema Definition DSL
3
3
  *
4
4
  * Define your data models with Zod. Types are inferred automatically.
5
5
  *
6
6
  * ```ts
7
7
  * import { z } from 'zod';
8
- * import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
8
+ * import { defineSchema, model, relation } from '@abloatai/ablo/schema';
9
9
  *
10
10
  * export const schema = defineSchema({
11
11
  * tasks: model({
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @ablo/sync-engine/schema — Schema Definition DSL
2
+ * @abloatai/ablo/schema — Schema Definition DSL
3
3
  *
4
4
  * Define your data models with Zod. Types are inferred automatically.
5
5
  *
6
6
  * ```ts
7
7
  * import { z } from 'zod';
8
- * import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
8
+ * import { defineSchema, model, relation } from '@abloatai/ablo/schema';
9
9
  *
10
10
  * export const schema = defineSchema({
11
11
  * tasks: model({
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Usage:
8
8
  * import { z } from 'zod';
9
- * import { model, relation } from '@ablo/sync-engine/schema';
9
+ * import { model, relation } from '@abloatai/ablo/schema';
10
10
  *
11
11
  * const tasks = model({
12
12
  * title: z.string(),
@@ -293,7 +293,7 @@ export interface ModelDef<Shape extends z.ZodRawShape = z.ZodRawShape, R extends
293
293
  *
294
294
  * ```ts
295
295
  * import { z } from 'zod';
296
- * import { model, relation } from '@ablo/sync-engine/schema';
296
+ * import { model, relation } from '@abloatai/ablo/schema';
297
297
  *
298
298
  * const tasks = model({
299
299
  * title: z.string(),
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Usage:
8
8
  * import { z } from 'zod';
9
- * import { model, relation } from '@ablo/sync-engine/schema';
9
+ * import { model, relation } from '@abloatai/ablo/schema';
10
10
  *
11
11
  * const tasks = model({
12
12
  * title: z.string(),
@@ -24,7 +24,7 @@ import { getFieldMeta, inferFieldMetaFromZod } from './field.js';
24
24
  *
25
25
  * ```ts
26
26
  * import { z } from 'zod';
27
- * import { model, relation } from '@ablo/sync-engine/schema';
27
+ * import { model, relation } from '@abloatai/ablo/schema';
28
28
  *
29
29
  * const tasks = model({
30
30
  * title: z.string(),
@@ -10,7 +10,7 @@
10
10
  * import { z } from 'zod';
11
11
  * import {
12
12
  * defineSchema, defineQueries, model, query, relation,
13
- * } from '@ablo/sync-engine/schema';
13
+ * } from '@abloatai/ablo/schema';
14
14
  *
15
15
  * const schema = defineSchema({
16
16
  * slideLayer: model(
@@ -10,7 +10,7 @@
10
10
  * import { z } from 'zod';
11
11
  * import {
12
12
  * defineSchema, defineQueries, model, query, relation,
13
- * } from '@ablo/sync-engine/schema';
13
+ * } from '@abloatai/ablo/schema';
14
14
  *
15
15
  * const schema = defineSchema({
16
16
  * slideLayer: model(
@@ -7,7 +7,7 @@
7
7
  * - Query include/join support
8
8
  *
9
9
  * Usage:
10
- * import { relation } from '@ablo/sync-engine/schema';
10
+ * import { relation } from '@abloatai/ablo/schema';
11
11
  *
12
12
  * const taskRelations = {
13
13
  * project: relation.belongsTo('projects', 'projectId'),
@@ -7,7 +7,7 @@
7
7
  * - Query include/join support
8
8
  *
9
9
  * Usage:
10
- * import { relation } from '@ablo/sync-engine/schema';
10
+ * import { relation } from '@abloatai/ablo/schema';
11
11
  *
12
12
  * const taskRelations = {
13
13
  * project: relation.belongsTo('projects', 'projectId'),
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Usage:
7
7
  * import { z } from 'zod';
8
- * import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
8
+ * import { defineSchema, model, relation } from '@abloatai/ablo/schema';
9
9
  *
10
10
  * const schema = defineSchema({
11
11
  * tasks: model({
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Usage:
7
7
  * import { z } from 'zod';
8
- * import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
8
+ * import { defineSchema, model, relation } from '@abloatai/ablo/schema';
9
9
  *
10
10
  * const schema = defineSchema({
11
11
  * tasks: model({
@@ -141,7 +141,7 @@ export interface SourceCommitParams<TAuth = unknown> {
141
141
  }
142
142
  /**
143
143
  * Operation-level permission tag used by `resolveScopes`. Mirrors the
144
- * four wire request types: a secret/key carries the set of operations
144
+ * four wire request types: an API key carries the set of operations
145
145
  * it's allowed to invoke. Stripe's restricted-key model at the
146
146
  * operation granularity — model-level scoping is a future addition.
147
147
  */
@@ -191,8 +191,8 @@ export interface SourceHandlerContext<TAuth = unknown> {
191
191
  * can use it in `authorize` (to reject out-of-scope calls) and in
192
192
  * `list` / `load` (to filter rows the participant is allowed to see).
193
193
  *
194
- * Absent for calls made without scope context (legacy callers,
195
- * tests, or single-tenant deployments that haven't enabled it).
194
+ * Absent for calls made without scope context, such as tests or
195
+ * single-tenant deployments that do not need scoped fan-out yet.
196
196
  */
197
197
  readonly scope?: SourceRequestContext;
198
198
  }
@@ -221,9 +221,9 @@ type SourceModels<S extends SchemaRecord, TAuth> = Partial<{
221
221
  readonly id: string;
222
222
  } & Record<string, unknown>, InferCreate<Schema<S>, K>, TAuth>;
223
223
  }>;
224
- export type SourceSecret = string | ((context: SourceAuthorizeContext) => Promise<string> | string);
224
+ export type SourceApiKey = string | ((context: SourceAuthorizeContext) => Promise<string> | string);
225
225
  export interface SourceSignatureOptions {
226
- readonly secret: string;
226
+ readonly apiKey: string;
227
227
  readonly body: string;
228
228
  /**
229
229
  * Unique message id (`webhook-id` per the Standard Webhooks spec).
@@ -231,12 +231,15 @@ export interface SourceSignatureOptions {
231
231
  * receivers may dedupe by it.
232
232
  */
233
233
  readonly messageId: string;
234
+ /**
235
+ * Unix timestamp in seconds. Defaults to the current time.
236
+ */
234
237
  readonly timestamp?: number;
235
238
  }
236
239
  export interface SourceSignatureVerificationOptions {
237
240
  readonly request: Request;
238
241
  readonly body: string;
239
- readonly secret: string;
242
+ readonly apiKey: string;
240
243
  readonly toleranceMs?: number;
241
244
  }
242
245
  export interface SourceSignatureVerificationResult {
@@ -263,21 +266,13 @@ export declare class SourceSignatureError extends Error {
263
266
  export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
264
267
  readonly schema: Schema<S>;
265
268
  /**
266
- * Shared secret for Ablo Cloud -> customer source calls. When set,
267
- * abloSource verifies HMAC_SHA256(secret, `${timestamp}.${body}`)
268
- * before `authorize` or any model handler runs.
269
- *
270
- * @deprecated Use `signingSecret`. Kept so existing source routes
271
- * do not break during the Data Source naming cleanup.
272
- */
273
- readonly secret?: SourceSecret;
274
- /**
275
- * Signing secret for Ablo -> customer Data Source calls. The value is
276
- * created for the Data Source endpoint in Ablo and stored in the
277
- * customer's app environment. Used to verify Standard Webhooks
278
- * headers before any handler runs.
269
+ * Customer-visible Ablo credential. In the API-key-only onboarding
270
+ * path, Ablo signs Data Source calls with the same project API key
271
+ * that the customer's server-side SDK uses. This keeps the customer
272
+ * env surface to one Ablo credential while preserving signed request
273
+ * verification before any handler runs.
279
274
  */
280
- readonly signingSecret?: SourceSecret;
275
+ readonly apiKey: SourceApiKey;
281
276
  /**
282
277
  * Clock-skew window for signed source requests. Default: 5 minutes.
283
278
  */
@@ -287,7 +282,7 @@ export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
287
282
  * a database handle, account scope, or current actor. Keep database
288
283
  * credentials in this function's environment; never send them to Ablo.
289
284
  *
290
- * Signature verification is handled by `secret` before this function
285
+ * Signature verification is handled by `apiKey` before this function
291
286
  * runs. `authorize` should only attach business context.
292
287
  */
293
288
  readonly authorize?: (context: SourceAuthorizeContext) => Promise<TAuth> | TAuth;
@@ -299,12 +294,11 @@ export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
299
294
  * runs.
300
295
  *
301
296
  * Customers typically extract a key id from the request (e.g.
302
- * `webhook-id` prefix, a custom header, or the secret itself) and
303
- * look up the scopes for that key in their store. Mirrors Stripe
304
- * restricted keys: one secret can read, another can read + write.
297
+ * `webhook-id` prefix, a custom header, or the API key itself) and
298
+ * look up the scopes for that key in their store.
305
299
  *
306
- * When omitted, all operations are allowed (back-compat). Returning
307
- * an empty set denies all operations.
300
+ * When omitted, all operations are allowed. Returning an empty set
301
+ * denies all operations.
308
302
  */
309
303
  readonly resolveScopes?: (params: {
310
304
  readonly auth: TAuth;
@@ -351,7 +345,7 @@ export type SourceListRequest = {
351
345
  export type SourceCommitRequest = {
352
346
  readonly type: 'commit';
353
347
  /**
354
- * Legacy single-model hint. Omit for cross-model commits; top-level
348
+ * Optional single-model hint. Omit for cross-model commits; top-level
355
349
  * `commit` receives the whole operation array unchanged.
356
350
  */
357
351
  readonly model?: string;
@@ -407,7 +401,7 @@ export type DataSourceAuthorizeContext = SourceAuthorizeContext;
407
401
  export type DataSourceHandlerContext<TAuth = unknown> = SourceHandlerContext<TAuth>;
408
402
  export type DataSourceModelHandlers<Row, CreateInput, TAuth = unknown> = SourceModelHandlers<Row, CreateInput, TAuth>;
409
403
  export type DataSourceCommitHandler<TAuth = unknown> = SourceCommitHandler<TAuth>;
410
- export type DataSourceSecret = SourceSecret;
404
+ export type DataSourceApiKey = SourceApiKey;
411
405
  export type DataSourceSignatureOptions = SourceSignatureOptions;
412
406
  export type DataSourceSignatureVerificationOptions = SourceSignatureVerificationOptions;
413
407
  export type DataSourceSignatureVerificationResult = SourceSignatureVerificationResult;
@@ -79,13 +79,13 @@ function bufferToBase64(buffer) {
79
79
  }
80
80
  return btoa(binary);
81
81
  }
82
- async function hmacSha256Base64(secret, payload) {
82
+ async function hmacSha256Base64(apiKey, payload) {
83
83
  const crypto = globalThis.crypto?.subtle;
84
84
  if (!crypto) {
85
85
  throw new SourceSignatureError('source_signature_invalid', 'WebCrypto HMAC support is unavailable in this runtime');
86
86
  }
87
87
  const encoder = new TextEncoder();
88
- const key = await crypto.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
88
+ const key = await crypto.importKey('raw', encoder.encode(apiKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
89
89
  return bufferToBase64(await crypto.sign('HMAC', key, encoder.encode(payload)));
90
90
  }
91
91
  /**
@@ -101,12 +101,9 @@ function timingSafeEqual(expected, actual) {
101
101
  return diff === 0;
102
102
  }
103
103
  export async function signAbloSourceRequest(options) {
104
- const signedAt = options.timestamp ?? Date.now();
104
+ const signedAt = options.timestamp ?? Math.floor(Date.now() / 1000);
105
105
  // Standard Webhooks signing input: `${msg_id}.${timestamp}.${payload}`
106
- // Timestamps are seconds-since-epoch in the spec; we keep millis on
107
- // the wire for backwards compatibility with our existing tolerance
108
- // window — the receiver compares them millis-to-millis.
109
- const signature = await hmacSha256Base64(options.secret, `${options.messageId}.${signedAt}.${options.body}`);
106
+ const signature = await hmacSha256Base64(options.apiKey, `${options.messageId}.${signedAt}.${options.body}`);
110
107
  return {
111
108
  signedAt,
112
109
  signature,
@@ -131,14 +128,16 @@ export async function verifyAbloSourceRequest(options) {
131
128
  throw new SourceSignatureError('source_timestamp_invalid', 'Invalid webhook-timestamp header');
132
129
  }
133
130
  const toleranceMs = options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS;
134
- if (Math.abs(Date.now() - signedAt) > toleranceMs) {
131
+ const nowSeconds = Math.floor(Date.now() / 1000);
132
+ const toleranceSeconds = Math.ceil(toleranceMs / 1000);
133
+ if (Math.abs(nowSeconds - signedAt) > toleranceSeconds) {
135
134
  throw new SourceSignatureError('source_timestamp_expired', 'webhook-timestamp is outside the allowed clock-skew window');
136
135
  }
137
136
  const presented = parseSignatureHeader(getHeader(options.request, ABLO_SOURCE_HEADERS.signature));
138
137
  if (presented.length === 0) {
139
138
  throw new SourceSignatureError('source_signature_missing', 'Missing webhook-signature header');
140
139
  }
141
- const expected = await hmacSha256Base64(options.secret, `${messageId}.${signedAt}.${options.body}`);
140
+ const expected = await hmacSha256Base64(options.apiKey, `${messageId}.${signedAt}.${options.body}`);
142
141
  // Accept any presented signature that matches — supports key
143
142
  // rotation per the Standard Webhooks spec.
144
143
  const ok = presented.some((sig) => timingSafeEqual(expected, sig));
@@ -147,10 +146,10 @@ export async function verifyAbloSourceRequest(options) {
147
146
  }
148
147
  return { messageId, signedAt };
149
148
  }
150
- async function resolveSecret(secret, context) {
151
- if (!secret)
149
+ async function resolveApiKey(apiKey, context) {
150
+ if (!apiKey)
152
151
  return null;
153
- return typeof secret === 'function' ? secret(context) : secret;
152
+ return typeof apiKey === 'function' ? apiKey(context) : apiKey;
154
153
  }
155
154
  /**
156
155
  * Map a wire request to its scope tag. Each request type corresponds
@@ -209,19 +208,23 @@ export function abloSource(options) {
209
208
  }
210
209
  let signature = null;
211
210
  try {
212
- const secret = await resolveSecret(options.signingSecret ?? options.secret, {
211
+ const apiKey = await resolveApiKey(options.apiKey, {
213
212
  request,
214
213
  body,
215
214
  rawBody,
216
215
  });
217
- if (secret) {
218
- signature = await verifyAbloSourceRequest({
219
- request,
220
- body: rawBody,
221
- secret,
222
- toleranceMs: options.signatureToleranceMs,
223
- });
216
+ if (!apiKey) {
217
+ return json({
218
+ error: 'source_api_key_missing',
219
+ message: 'Data Source apiKey is required',
220
+ }, 401);
224
221
  }
222
+ signature = await verifyAbloSourceRequest({
223
+ request,
224
+ body: rawBody,
225
+ apiKey,
226
+ toleranceMs: options.signatureToleranceMs,
227
+ });
225
228
  }
226
229
  catch (err) {
227
230
  if (err instanceof SourceSignatureError) {
@@ -60,7 +60,7 @@ export interface PushQueueStorage {
60
60
  export declare const STANDARD_WEBHOOKS_RETRY_SCHEDULE: readonly number[];
61
61
  export interface PushQueueOptions {
62
62
  readonly endpoint: string;
63
- readonly secret: string;
63
+ readonly apiKey: string;
64
64
  readonly storage: PushQueueStorage;
65
65
  /**
66
66
  * Override the retry delays. Default: Standard Webhooks schedule.
@@ -84,9 +84,9 @@ export function createPushQueue(options) {
84
84
  let signed;
85
85
  try {
86
86
  signed = await signAbloSourceRequest({
87
- secret: options.secret,
87
+ apiKey: options.apiKey,
88
88
  body: rawBody,
89
- timestamp: now(),
89
+ timestamp: Math.floor(now() / 1000),
90
90
  // Reuse the queue id as the webhook-id across all retry
91
91
  // attempts so the receiver can dedupe replays per spec.
92
92
  messageId: item.id,
@@ -193,6 +193,20 @@ export interface PresenceUpdateEvent {
193
193
  meta?: Record<string, unknown>;
194
194
  declaredAt: number;
195
195
  expiresAt: number;
196
+ /**
197
+ * Lifecycle state. Additive — older servers omit it and the reader
198
+ * treats absence as `'active'`. Terminal states (`committed` /
199
+ * `expired` / `canceled`) ride one frame as the claim ends so peers
200
+ * learn *how* it resolved before it drops from the active set.
201
+ */
202
+ status?: 'active' | 'committed' | 'expired' | 'canceled';
203
+ error?: {
204
+ code: string;
205
+ message?: string;
206
+ heldBy?: string;
207
+ heldByIntentId?: string;
208
+ heldByExpiresAt?: number;
209
+ };
196
210
  }>;
197
211
  localTime?: string;
198
212
  type?: string;
@@ -78,6 +78,13 @@ export function createIntentStream(config, transport = null) {
78
78
  }
79
79
  }
80
80
  for (const claim of event.activeIntents ?? []) {
81
+ // Terminal-status entries (committed / expired / canceled) are
82
+ // one-shot "this claim ended" signals. The holder sweep above
83
+ // already removed the prior active entry; skipping the re-add
84
+ // drops it from `others`, which is what resolves a contender's
85
+ // `settled()`. Absent status means active (wire back-compat).
86
+ if (claim.status && claim.status !== 'active')
87
+ continue;
81
88
  activeByIntentId.set(claim.intentId, {
82
89
  id: claim.intentId,
83
90
  heldBy: event.userId,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Test Model subclasses for @ablo/sync-engine tests.
2
+ * Test Model subclasses for @abloatai/ablo tests.
3
3
  *
4
4
  * Lightweight Model implementations with FK relationships matching
5
5
  * the MODEL_CREATE_PRIORITY map in TransactionQueue:
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Test Model subclasses for @ablo/sync-engine tests.
2
+ * Test Model subclasses for @abloatai/ablo tests.
3
3
  *
4
4
  * Lightweight Model implementations with FK relationships matching
5
5
  * the MODEL_CREATE_PRIORITY map in TransactionQueue:
@@ -19,7 +19,7 @@ export interface TestWrapperOptions {
19
19
  *
20
20
  * @example
21
21
  * import { renderHook } from '@testing-library/react';
22
- * import { createReactTestWrapper, createMockSyncStore } from '@ablo/sync-engine/testing';
22
+ * import { createReactTestWrapper, createMockSyncStore } from '@abloatai/ablo/testing';
23
23
  *
24
24
  * const mockStore = createMockSyncStore();
25
25
  * mockStore.setModels(Task, [task1, task2]);
@@ -40,7 +40,7 @@ export declare function createReactTestWrapper(options?: TestWrapperOptions): Re
40
40
  * consumers without React tests to install it.
41
41
  *
42
42
  * @example
43
- * import { renderSyncHook, createMockSyncStore } from '@ablo/sync-engine/testing';
43
+ * import { renderSyncHook, createMockSyncStore } from '@abloatai/ablo/testing';
44
44
  *
45
45
  * const mockStore = createMockSyncStore();
46
46
  * mockStore.addModel(Task, myTask);
@@ -13,7 +13,7 @@ import { MockSyncStore, createMockSyncStore } from '../mocks/MockSyncStore.js';
13
13
  *
14
14
  * @example
15
15
  * import { renderHook } from '@testing-library/react';
16
- * import { createReactTestWrapper, createMockSyncStore } from '@ablo/sync-engine/testing';
16
+ * import { createReactTestWrapper, createMockSyncStore } from '@abloatai/ablo/testing';
17
17
  *
18
18
  * const mockStore = createMockSyncStore();
19
19
  * mockStore.setModels(Task, [task1, task2]);
@@ -37,7 +37,7 @@ export function createReactTestWrapper(options = {}) {
37
37
  * consumers without React tests to install it.
38
38
  *
39
39
  * @example
40
- * import { renderSyncHook, createMockSyncStore } from '@ablo/sync-engine/testing';
40
+ * import { renderSyncHook, createMockSyncStore } from '@abloatai/ablo/testing';
41
41
  *
42
42
  * const mockStore = createMockSyncStore();
43
43
  * mockStore.addModel(Task, myTask);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @ablo/sync-engine/testing — Public test utilities for SDK consumers.
2
+ * @abloatai/ablo/testing — Public test utilities for SDK consumers.
3
3
  *
4
4
  * Provides mock implementations, fixture factories, and test harnesses
5
5
  * for writing integration tests against the sync engine.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @ablo/sync-engine/testing — Public test utilities for SDK consumers.
2
+ * @abloatai/ablo/testing — Public test utilities for SDK consumers.
3
3
  *
4
4
  * Provides mock implementations, fixture factories, and test harnesses
5
5
  * for writing integration tests against the sync engine.
@@ -493,3 +493,42 @@ export interface ActiveIntent extends IntentDeclaration {
493
493
  readonly announcedAt: string;
494
494
  readonly expiresAt: string;
495
495
  }
496
+ /** Every lifecycle state of a coordination intent, in one enum. */
497
+ export type IntentStatus = 'active' | 'committed' | 'expired' | 'canceled';
498
+ /** Options for waiting on a target to become free. */
499
+ export interface IntentWaitOptions {
500
+ readonly timeout?: number;
501
+ readonly pollInterval?: number;
502
+ readonly signal?: AbortSignal;
503
+ }
504
+ /**
505
+ * The coordination state of one entity. Self-describing on the wire via
506
+ * `object: 'intent'`. Existence with `status: 'active'` *is* the lock;
507
+ * the fields *are* the awareness ("agent X is editing this until Y").
508
+ *
509
+ * Deliberately omits a Stripe-style `next_action`: a contender's only
510
+ * response is "wait for release, then re-read", and the runtime performs
511
+ * that uniformly at the tool boundary (`IntentHandle.settled()` + the
512
+ * stale-context guard that forces a re-read). Encoding a constant
513
+ * instruction the engine always takes would be the kind of ceremony this
514
+ * object exists to remove.
515
+ */
516
+ export interface Intent {
517
+ readonly object: 'intent';
518
+ readonly id: string;
519
+ readonly status: IntentStatus;
520
+ /** What is being coordinated. */
521
+ readonly target: EntityRef;
522
+ /** Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. */
523
+ readonly action: string;
524
+ /** Participant holding it. */
525
+ readonly heldBy: string;
526
+ readonly participantKind: 'human' | 'agent';
527
+ /**
528
+ * Ms-epoch the holder opened it. Optional until the lease wire carries
529
+ * it — derived shapes (e.g. mapped from a presence frame) may omit it.
530
+ */
531
+ readonly createdAt?: string;
532
+ /** Ms-epoch the server auto-expires it if the holder doesn't finish. */
533
+ readonly expiresAt: string;
534
+ }