@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.
- package/CHANGELOG.md +39 -1
- package/NOTICE +2 -2
- package/README.md +27 -25
- package/dist/agent/Agent.d.ts +1 -1
- package/dist/agent/Agent.js +1 -1
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +6 -6
- package/dist/agent/types.d.ts +1 -1
- package/dist/ai-sdk/index.d.ts +3 -3
- package/dist/ai-sdk/index.js +3 -3
- package/dist/ai-sdk/intent-broadcast.d.ts +1 -1
- package/dist/ai-sdk/intent-broadcast.js +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +8 -14
- package/dist/client/Ablo.js +32 -1
- package/dist/client/auth.d.ts +3 -3
- package/dist/client/auth.js +5 -5
- package/dist/client/createModelProxy.d.ts +110 -32
- package/dist/client/createModelProxy.js +77 -38
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -2
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +9 -9
- package/dist/interfaces/headless.d.ts +1 -1
- package/dist/interfaces/headless.js +2 -2
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +2 -2
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +1 -1
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/SyncGroupProvider.js +1 -1
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutate.d.ts +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useReader.d.ts +1 -1
- package/dist/schema/field.d.ts +1 -1
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +2 -2
- package/dist/schema/model.js +2 -2
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -1
- package/dist/schema/relation.d.ts +1 -1
- package/dist/schema/relation.js +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/source/index.d.ts +22 -28
- package/dist/source/index.js +23 -20
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +2 -2
- package/dist/sync/SyncWebSocket.d.ts +14 -0
- package/dist/sync/createIntentStream.js +7 -0
- package/dist/testing/fixtures/models.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -1
- package/dist/testing/helpers/react-wrapper.d.ts +2 -2
- package/dist/testing/helpers/react-wrapper.js +2 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/types/streams.d.ts +39 -0
- package/docs/api.md +78 -20
- package/docs/data-sources.md +50 -16
- package/docs/examples/ai-sdk-tool.md +14 -31
- package/docs/examples/existing-python-backend.md +6 -6
- package/docs/integration-guide.md +8 -7
- package/docs/interaction-model.md +16 -4
- package/docs/mcp.md +1 -1
- package/docs/quickstart.md +20 -18
- package/examples/data-source/README.md +1 -1
- package/examples/data-source/ablo-driver.ts +5 -5
- package/examples/data-source/customer-server.ts +10 -10
- package/examples/data-source/run.ts +9 -11
- package/examples/data-source/schema.ts +1 -1
- package/examples/quickstart.ts +2 -2
- package/llms.txt +1 -1
- 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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
24
|
+
* import { useReader } from '@abloatai/ablo/react';
|
|
25
25
|
*
|
|
26
26
|
* function useTaskMutations() {
|
|
27
27
|
* const read = useReader(schema, 'tasks');
|
package/dist/schema/field.d.ts
CHANGED
package/dist/schema/field.js
CHANGED
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ablo/
|
|
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/
|
|
8
|
+
* import { defineSchema, model, relation } from '@abloatai/ablo/schema';
|
|
9
9
|
*
|
|
10
10
|
* export const schema = defineSchema({
|
|
11
11
|
* tasks: model({
|
package/dist/schema/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ablo/
|
|
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/
|
|
8
|
+
* import { defineSchema, model, relation } from '@abloatai/ablo/schema';
|
|
9
9
|
*
|
|
10
10
|
* export const schema = defineSchema({
|
|
11
11
|
* tasks: model({
|
package/dist/schema/model.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* import { z } from 'zod';
|
|
9
|
-
* import { model, relation } from '@ablo/
|
|
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/
|
|
296
|
+
* import { model, relation } from '@abloatai/ablo/schema';
|
|
297
297
|
*
|
|
298
298
|
* const tasks = model({
|
|
299
299
|
* title: z.string(),
|
package/dist/schema/model.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* import { z } from 'zod';
|
|
9
|
-
* import { model, relation } from '@ablo/
|
|
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/
|
|
27
|
+
* import { model, relation } from '@abloatai/ablo/schema';
|
|
28
28
|
*
|
|
29
29
|
* const tasks = model({
|
|
30
30
|
* title: z.string(),
|
package/dist/schema/queries.d.ts
CHANGED
package/dist/schema/queries.js
CHANGED
package/dist/schema/relation.js
CHANGED
package/dist/schema/schema.d.ts
CHANGED
package/dist/schema/schema.js
CHANGED
package/dist/source/index.d.ts
CHANGED
|
@@ -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:
|
|
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
|
|
195
|
-
*
|
|
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
|
|
224
|
+
export type SourceApiKey = string | ((context: SourceAuthorizeContext) => Promise<string> | string);
|
|
225
225
|
export interface SourceSignatureOptions {
|
|
226
|
-
readonly
|
|
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
|
|
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
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
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
|
|
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 `
|
|
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
|
|
303
|
-
* look up the scopes for that key in their store.
|
|
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
|
|
307
|
-
*
|
|
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
|
-
*
|
|
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
|
|
404
|
+
export type DataSourceApiKey = SourceApiKey;
|
|
411
405
|
export type DataSourceSignatureOptions = SourceSignatureOptions;
|
|
412
406
|
export type DataSourceSignatureVerificationOptions = SourceSignatureVerificationOptions;
|
|
413
407
|
export type DataSourceSignatureVerificationResult = SourceSignatureVerificationResult;
|
package/dist/source/index.js
CHANGED
|
@@ -79,13 +79,13 @@ function bufferToBase64(buffer) {
|
|
|
79
79
|
}
|
|
80
80
|
return btoa(binary);
|
|
81
81
|
}
|
|
82
|
-
async function hmacSha256Base64(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
151
|
-
if (!
|
|
149
|
+
async function resolveApiKey(apiKey, context) {
|
|
150
|
+
if (!apiKey)
|
|
152
151
|
return null;
|
|
153
|
-
return typeof
|
|
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
|
|
211
|
+
const apiKey = await resolveApiKey(options.apiKey, {
|
|
213
212
|
request,
|
|
214
213
|
body,
|
|
215
214
|
rawBody,
|
|
216
215
|
});
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
63
|
+
readonly apiKey: string;
|
|
64
64
|
readonly storage: PushQueueStorage;
|
|
65
65
|
/**
|
|
66
66
|
* Override the retry delays. Default: Standard Webhooks schedule.
|
package/dist/source/pushQueue.js
CHANGED
|
@@ -84,9 +84,9 @@ export function createPushQueue(options) {
|
|
|
84
84
|
let signed;
|
|
85
85
|
try {
|
|
86
86
|
signed = await signAbloSourceRequest({
|
|
87
|
-
|
|
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,
|
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
40
|
+
* import { renderSyncHook, createMockSyncStore } from '@abloatai/ablo/testing';
|
|
41
41
|
*
|
|
42
42
|
* const mockStore = createMockSyncStore();
|
|
43
43
|
* mockStore.addModel(Task, myTask);
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ablo/
|
|
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.
|
package/dist/testing/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ablo/
|
|
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.
|
package/dist/types/streams.d.ts
CHANGED
|
@@ -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
|
+
}
|