@abloatai/ablo 0.24.0 → 0.26.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/AGENTS.md +5 -3
- package/CHANGELOG.md +33 -0
- package/README.md +45 -36
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +583 -287
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/data-sources.md +12 -5
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/llms.txt +4 -2
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
|
-
import type { AbloOptions
|
|
8
|
+
import type { AbloOptions } from './options.js';
|
|
9
|
+
import type { CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget, CreateSessionParams, AbloSession } from './resourceTypes.js';
|
|
9
10
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
10
11
|
import type { Duration } from '../utils/duration.js';
|
|
11
12
|
import type { Claim } from '../types/streams.js';
|
|
@@ -19,7 +20,19 @@ export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
|
|
|
19
20
|
* `ClaimLog` works identically for headless (server-agent) evals.
|
|
20
21
|
*/
|
|
21
22
|
readonly observability?: SyncObservabilityProvider;
|
|
23
|
+
/**
|
|
24
|
+
* Per-request deadline in milliseconds for the stateless HTTP transport.
|
|
25
|
+
* Every request this client issues is aborted after this long and surfaces
|
|
26
|
+
* as a retryable connection error — without it a black-holed server hangs
|
|
27
|
+
* a headless agent forever (browsers never time fetch out on their own).
|
|
28
|
+
* Pass `0` to disable the deadline.
|
|
29
|
+
*
|
|
30
|
+
* @default 30_000
|
|
31
|
+
*/
|
|
32
|
+
readonly timeoutMs?: number;
|
|
22
33
|
};
|
|
34
|
+
/** Default per-request deadline for the stateless HTTP transport. */
|
|
35
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
23
36
|
export interface AbloApiClaims {
|
|
24
37
|
create(options: ClaimCreateOptions): Promise<Claim>;
|
|
25
38
|
list(target?: Partial<ModelTarget>): Promise<readonly ModelClaim[]>;
|
package/dist/client/ApiClient.js
CHANGED
|
@@ -9,9 +9,12 @@ import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloVal
|
|
|
9
9
|
import { reconcileFunctionalUpdate, } from './functionalUpdate.js';
|
|
10
10
|
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
|
|
11
11
|
import { registerDataSource } from './registerDataSource.js';
|
|
12
|
+
import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
|
|
12
13
|
import { toSeconds } from '../utils/duration.js';
|
|
13
14
|
import { mintSession } from './sessionMint.js';
|
|
14
15
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
16
|
+
/** Default per-request deadline for the stateless HTTP transport. */
|
|
17
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
15
18
|
const DEFAULT_AGENT_LEASE = '10m';
|
|
16
19
|
export function createProtocolClient(options) {
|
|
17
20
|
const env = readProcessEnv();
|
|
@@ -101,6 +104,9 @@ export function createProtocolClient(options) {
|
|
|
101
104
|
const headers = {
|
|
102
105
|
'Content-Type': 'application/json',
|
|
103
106
|
Authorization: `Bearer ${token}`,
|
|
107
|
+
// Protocol handshake for the HTTP transport (wire/protocolVersion.ts):
|
|
108
|
+
// the server answers an out-of-range version with a typed 426.
|
|
109
|
+
[PROTOCOL_VERSION_HEADER]: String(PROTOCOL_VERSION),
|
|
104
110
|
};
|
|
105
111
|
for (const [key, value] of Object.entries(options.defaultHeaders ?? {})) {
|
|
106
112
|
if (value == null) {
|
|
@@ -120,20 +126,66 @@ export function createProtocolClient(options) {
|
|
|
120
126
|
}
|
|
121
127
|
return target.toString();
|
|
122
128
|
}
|
|
129
|
+
const requestTimeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
123
130
|
async function requestJson(path, init) {
|
|
124
131
|
await ready();
|
|
125
132
|
const { idempotencyKey, ...requestInit } = init;
|
|
126
133
|
const headers = await authHeaders();
|
|
127
134
|
if (idempotencyKey)
|
|
128
135
|
headers['Idempotency-Key'] = idempotencyKey;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
136
|
+
// Deadline: abort the request after `timeoutMs` so a black-holed server
|
|
137
|
+
// can't hang the caller forever (fetch has NO default timeout in browsers,
|
|
138
|
+
// and only undici's generous defaults in Node). A caller-supplied signal
|
|
139
|
+
// is combined with the deadline via a shared controller — the portable
|
|
140
|
+
// equivalent of `AbortSignal.any([caller, AbortSignal.timeout(t)])`,
|
|
141
|
+
// which older runtimes (and the jsdom test env) don't implement. The
|
|
142
|
+
// same pattern already guards `query/client.ts` and `BootstrapHelper`.
|
|
143
|
+
const callerSignal = requestInit.signal ?? undefined;
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const onCallerAbort = () => { controller.abort(callerSignal?.reason); };
|
|
146
|
+
if (callerSignal) {
|
|
147
|
+
if (callerSignal.aborted)
|
|
148
|
+
onCallerAbort();
|
|
149
|
+
else
|
|
150
|
+
callerSignal.addEventListener('abort', onCallerAbort, { once: true });
|
|
151
|
+
}
|
|
152
|
+
let timedOut = false;
|
|
153
|
+
const deadline = requestTimeoutMs > 0
|
|
154
|
+
? setTimeout(() => {
|
|
155
|
+
timedOut = true;
|
|
156
|
+
controller.abort();
|
|
157
|
+
}, requestTimeoutMs)
|
|
158
|
+
: null;
|
|
159
|
+
let res;
|
|
160
|
+
let bodyText;
|
|
161
|
+
try {
|
|
162
|
+
res = await fetchImpl(endpoint(path), {
|
|
163
|
+
...requestInit,
|
|
164
|
+
signal: controller.signal,
|
|
165
|
+
headers: {
|
|
166
|
+
...headers,
|
|
167
|
+
...requestInit.headers,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
// Keep the deadline armed while the body streams — a server that sends
|
|
171
|
+
// headers then stalls the body is the same hang with better manners.
|
|
172
|
+
bodyText = await res.text();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (timedOut) {
|
|
176
|
+
// Retryable by contract: `wait_for_timeout` is a registered transient
|
|
177
|
+
// transport code, so `isRetryableCode` steers callers to retry.
|
|
178
|
+
throw new AbloConnectionError(`The Ablo API did not respond within ${requestTimeoutMs}ms ` +
|
|
179
|
+
`(${requestInit.method ?? 'GET'} ${path}). The request was aborted; ` +
|
|
180
|
+
'it is safe to retry.', { code: 'wait_for_timeout', cause: error });
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (deadline)
|
|
186
|
+
clearTimeout(deadline);
|
|
187
|
+
callerSignal?.removeEventListener('abort', onCallerAbort);
|
|
188
|
+
}
|
|
137
189
|
const body = parseBody(bodyText);
|
|
138
190
|
if (!res.ok) {
|
|
139
191
|
throw translateHttpError(res.status, body ?? `Ablo API request failed: ${res.status} ${res.statusText}`, res.headers.get('x-request-id') ?? undefined);
|
|
@@ -617,7 +669,9 @@ export function createProtocolClient(options) {
|
|
|
617
669
|
throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
|
|
618
670
|
`The HTTP client cannot await the grant without a WebSocket.`, { code: 'claim_queued' });
|
|
619
671
|
}
|
|
620
|
-
|
|
672
|
+
// `claimId` is the queued-response field name (routes/claims.ts) — a
|
|
673
|
+
// pre-rename regression duplicated the `body.id` arm and dropped it.
|
|
674
|
+
return body.claim?.id ?? body.id ?? body.claimId ?? createClaimId();
|
|
621
675
|
};
|
|
622
676
|
const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
|
|
623
677
|
async function claimImpl(params) {
|
package/dist/client/auth.d.ts
CHANGED
|
@@ -12,20 +12,13 @@
|
|
|
12
12
|
* explicit options so generated apps do not accrete hidden env knobs.
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* from `./Ablo` so existing import paths work; defined here so this module
|
|
20
|
-
* has no circular dependency back to `Ablo.ts`.
|
|
21
|
-
*
|
|
22
|
-
* Contract: resolve a token; resolve `null` when the login itself is gone
|
|
23
|
-
* (terminal → the credential lifecycle treats this as `session_expired` and
|
|
24
|
-
* signs out); or THROW on a transient failure (→ back off and retry, never
|
|
25
|
-
* sign out). A long-lived static `apiKey` string needs none of this — it is
|
|
26
|
-
* used as-is. This is the single credential resolver the SDK supports.
|
|
15
|
+
* The credential-resolver callable contract. Defined in the `credentialEndpoint`
|
|
16
|
+
* leaf (which produces one) and re-exported here — the historical home — so
|
|
17
|
+
* `./auth` importers keep working while the module graph stays one-directional
|
|
18
|
+
* (`auth → credentialEndpoint`, never back). See there for the full contract.
|
|
27
19
|
*/
|
|
28
|
-
|
|
20
|
+
import type { ApiKeySetter } from './credentialEndpoint.js';
|
|
21
|
+
export type { ApiKeySetter };
|
|
29
22
|
export interface AuthResolveInput {
|
|
30
23
|
/**
|
|
31
24
|
* The full options bag the caller passed to `Ablo()`. Resolvers
|
|
@@ -34,6 +27,7 @@ export interface AuthResolveInput {
|
|
|
34
27
|
*/
|
|
35
28
|
readonly options: {
|
|
36
29
|
readonly apiKey?: string | ApiKeySetter | null;
|
|
30
|
+
readonly authEndpoint?: string | ApiKeySetter | null;
|
|
37
31
|
readonly authToken?: string | null;
|
|
38
32
|
readonly baseURL?: string | null;
|
|
39
33
|
readonly databaseUrl?: string | null;
|
|
@@ -88,9 +82,7 @@ export declare function resolveDatabaseUrl(input: AuthResolveInput): string | nu
|
|
|
88
82
|
export declare function warnIfDatabaseUrlEnvIgnored(input: AuthResolveInput, warn?: (message: string) => void): void;
|
|
89
83
|
export declare function warnIfDatabaseUrlDeprecated(input: AuthResolveInput, warn?: (message: string) => void): void;
|
|
90
84
|
export declare function warnIfCliKeyMismatch(input: AuthResolveInput, warn?: (message: string) => void): Promise<void>;
|
|
91
|
-
export
|
|
92
|
-
export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
|
|
93
|
-
export declare const ABLO_DEFAULT_BASE_URL = "https://api.abloatai.com";
|
|
85
|
+
export { ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, ABLO_DEFAULT_BASE_URL } from './hostedEndpoints.js';
|
|
94
86
|
/**
|
|
95
87
|
* Normalize old hosted aliases to the public API domain. Self-hosted/custom
|
|
96
88
|
* URLs pass through unchanged; only first-party legacy hosts are rewritten.
|
|
@@ -133,4 +125,3 @@ export declare function resolveBootstrapBaseUrl(input: {
|
|
|
133
125
|
readonly url: string;
|
|
134
126
|
readonly bootstrapBaseUrl?: string;
|
|
135
127
|
}): string;
|
|
136
|
-
export {};
|
package/dist/client/auth.js
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* is the only environment fallback. Other routing/auth overrides are
|
|
12
12
|
* explicit options so generated apps do not accrete hidden env knobs.
|
|
13
13
|
*/
|
|
14
|
-
import { AbloAuthenticationError } from '../errors.js';
|
|
14
|
+
import { AbloAuthenticationError, AbloValidationError } from '../errors.js';
|
|
15
15
|
import { classifyCredentialKind } from '../auth/credentialPolicy.js';
|
|
16
|
+
import { ABLO_HOSTED_API_DOMAIN, ABLO_DEFAULT_BASE_URL } from './hostedEndpoints.js';
|
|
17
|
+
import { isCredentialEndpoint, createEndpointCredentialResolver } from './credentialEndpoint.js';
|
|
16
18
|
/**
|
|
17
19
|
* Read `process.env` defensively. Works in browser (where `process`
|
|
18
20
|
* is undefined), Node, and edge runtimes that expose a partial
|
|
@@ -23,7 +25,37 @@ export function readProcessEnv() {
|
|
|
23
25
|
return maybeGlobal.process?.env ?? {};
|
|
24
26
|
}
|
|
25
27
|
export function resolveApiKey(input) {
|
|
26
|
-
|
|
28
|
+
// `authEndpoint` — the NAMED session-mint field (Liveblocks `authEndpoint` /
|
|
29
|
+
// Ably `authUrl`): a route string the SDK exchanges for a short-lived token,
|
|
30
|
+
// or an async resolver for custom exchanges. Resolved HERE — the one
|
|
31
|
+
// resolution point all three factory branches (WS, `transport: 'http'`,
|
|
32
|
+
// protocol client) share — into an `ApiKeySetter`, so every downstream
|
|
33
|
+
// consumer sees the familiar resolver and the credential lifecycle drives
|
|
34
|
+
// renewal off it.
|
|
35
|
+
const endpoint = input.options.authEndpoint;
|
|
36
|
+
const configured = input.options.apiKey;
|
|
37
|
+
if (endpoint != null) {
|
|
38
|
+
if (configured != null) {
|
|
39
|
+
throw new AbloValidationError('Ablo: pass either `apiKey` (a key the process holds) or `authEndpoint` ' +
|
|
40
|
+
'(a route that mints the token) — not both; the client cannot know ' +
|
|
41
|
+
'which credential to use.', { code: 'invalid_options', param: 'authEndpoint' });
|
|
42
|
+
}
|
|
43
|
+
if (typeof endpoint === 'function')
|
|
44
|
+
return endpoint;
|
|
45
|
+
if (!isCredentialEndpoint(endpoint)) {
|
|
46
|
+
throw new AbloValidationError('`authEndpoint` expects a URL or path (e.g. \'/api/ablo-session\') or an ' +
|
|
47
|
+
'async resolver — a key string belongs in `apiKey`.', { code: 'invalid_options', param: 'authEndpoint' });
|
|
48
|
+
}
|
|
49
|
+
return createEndpointCredentialResolver(endpoint);
|
|
50
|
+
}
|
|
51
|
+
// `apiKey` also accepts the endpoint-string shape directly (same detection —
|
|
52
|
+
// key strings are `sk_`/`ek_`/`rk_`-prefixed so the shapes can't collide).
|
|
53
|
+
// Deliberately only the EXPLICIT option: an `ABLO_API_KEY` env value is
|
|
54
|
+
// always a literal key, never endpoint-detected.
|
|
55
|
+
if (typeof configured === 'string' && isCredentialEndpoint(configured)) {
|
|
56
|
+
return createEndpointCredentialResolver(configured);
|
|
57
|
+
}
|
|
58
|
+
return configured ?? input.env.ABLO_API_KEY ?? null;
|
|
27
59
|
}
|
|
28
60
|
export function resolveAuthToken(input) {
|
|
29
61
|
return input.options.authToken ?? null;
|
|
@@ -41,6 +73,10 @@ export function modeFromApiKey(key) {
|
|
|
41
73
|
}
|
|
42
74
|
function resolveStaticApiKey(input) {
|
|
43
75
|
if (typeof input.options.apiKey === 'string') {
|
|
76
|
+
// An endpoint-string `apiKey` is not a key — it never participates in
|
|
77
|
+
// CLI-mode mismatch checks (its minted tokens carry the mode instead).
|
|
78
|
+
if (isCredentialEndpoint(input.options.apiKey))
|
|
79
|
+
return null;
|
|
44
80
|
return { key: input.options.apiKey, source: 'option' };
|
|
45
81
|
}
|
|
46
82
|
if (input.options.apiKey !== undefined && input.options.apiKey !== null) {
|
|
@@ -299,9 +335,9 @@ export async function warnIfCliKeyMismatch(input, warn) {
|
|
|
299
335
|
else if (typeof console !== 'undefined')
|
|
300
336
|
console.warn('[Ablo]', mismatch.message);
|
|
301
337
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
export
|
|
338
|
+
// Declared in the dependency-free `hostedEndpoints.ts` leaf (single source of
|
|
339
|
+
// the hosted domain); re-exported here so existing import paths keep working.
|
|
340
|
+
export { ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, ABLO_DEFAULT_BASE_URL } from './hostedEndpoints.js';
|
|
305
341
|
const LEGACY_HOSTED_API_HOSTS = new Set([
|
|
306
342
|
'mesh.ablo.finance',
|
|
307
343
|
'mesh-staging.ablo.finance',
|
|
@@ -448,6 +484,6 @@ function ensureApiSuffix(httpBase) {
|
|
|
448
484
|
catch {
|
|
449
485
|
// Should be unreachable post-`normalizeAbloHostedBaseUrl` (which yields an
|
|
450
486
|
// absolute URL), but fall back to a string check rather than throwing.
|
|
451
|
-
return
|
|
487
|
+
return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`;
|
|
452
488
|
}
|
|
453
489
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default `[Ablo]` console logger + its level resolution.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `Ablo.ts`; exported so other entry points that build a
|
|
5
|
+
* default logger (e.g. the agent runtime) can reuse the same gating instead
|
|
6
|
+
* of hand-rolling a console shim.
|
|
7
|
+
*/
|
|
8
|
+
import type { SyncLogger } from '../interfaces/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Level threshold for the default console logger.
|
|
11
|
+
*
|
|
12
|
+
* The SDK emits a `debug` line per model and per property during schema
|
|
13
|
+
* registration (see `ModelRegistry`), plus assorted lifecycle chatter. That
|
|
14
|
+
* is verbose by design but carries no actionable signal for app consumers, so
|
|
15
|
+
* the default threshold is `warn` — `debug`/`info` are dropped unless a
|
|
16
|
+
* consumer opts in.
|
|
17
|
+
*
|
|
18
|
+
* Opt back in with `ABLO_LOG_LEVEL=debug` (or `info`/`warn`/`error`/`silent`).
|
|
19
|
+
* Passing a custom `logger` to `Ablo({ logger })` bypasses this entirely.
|
|
20
|
+
*/
|
|
21
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the effective level. Precedence: explicit `logLevel` option →
|
|
24
|
+
* `debug: true` (⇒ debug) → `ABLO_LOG_LEVEL` env → default `warn`. `debug: false`
|
|
25
|
+
* / omitted just means "don't raise the level" — it falls through to env/default
|
|
26
|
+
* rather than force-silencing an ops-set env override.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveLogLevel(opts?: {
|
|
29
|
+
debug?: boolean;
|
|
30
|
+
logLevel?: LogLevel;
|
|
31
|
+
}): LogLevel;
|
|
32
|
+
/**
|
|
33
|
+
* Build the default logger, gated at `level` and prefixed `[Ablo]` so a creator
|
|
34
|
+
* with a console full of other tools' logs can see at a glance what's ours.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createConsoleLogger(level: LogLevel): SyncLogger;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default `[Ablo]` console logger + its level resolution.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `Ablo.ts`; exported so other entry points that build a
|
|
5
|
+
* default logger (e.g. the agent runtime) can reuse the same gating instead
|
|
6
|
+
* of hand-rolling a console shim.
|
|
7
|
+
*/
|
|
8
|
+
const LOG_LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the effective level. Precedence: explicit `logLevel` option →
|
|
11
|
+
* `debug: true` (⇒ debug) → `ABLO_LOG_LEVEL` env → default `warn`. `debug: false`
|
|
12
|
+
* / omitted just means "don't raise the level" — it falls through to env/default
|
|
13
|
+
* rather than force-silencing an ops-set env override.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveLogLevel(opts) {
|
|
16
|
+
if (opts?.logLevel && opts.logLevel in LOG_LEVEL_RANK)
|
|
17
|
+
return opts.logLevel;
|
|
18
|
+
if (opts?.debug === true)
|
|
19
|
+
return 'debug';
|
|
20
|
+
// `globalThis.process` guard keeps this safe in browser/edge runtimes that
|
|
21
|
+
// have no `process` binding — there we fall through to the default.
|
|
22
|
+
const raw = globalThis.process?.env?.ABLO_LOG_LEVEL;
|
|
23
|
+
const normalized = raw?.toLowerCase();
|
|
24
|
+
if (normalized && normalized in LOG_LEVEL_RANK)
|
|
25
|
+
return normalized;
|
|
26
|
+
return 'warn';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the default logger, gated at `level` and prefixed `[Ablo]` so a creator
|
|
30
|
+
* with a console full of other tools' logs can see at a glance what's ours.
|
|
31
|
+
*/
|
|
32
|
+
export function createConsoleLogger(level) {
|
|
33
|
+
const threshold = LOG_LEVEL_RANK[level];
|
|
34
|
+
const emit = (lvl, fn, args) => {
|
|
35
|
+
if (typeof console === 'undefined' || LOG_LEVEL_RANK[lvl] < threshold)
|
|
36
|
+
return;
|
|
37
|
+
fn('[Ablo]', ...args);
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
debug: (...args) => { emit('debug', console.debug, args); },
|
|
41
|
+
info: (...args) => { emit('info', console.info, args); },
|
|
42
|
+
warn: (...args) => { emit('warn', console.warn, args); },
|
|
43
|
+
error: (...args) => { emit('error', console.error, args); },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -62,7 +62,7 @@ export function createInternalComponents(input) {
|
|
|
62
62
|
// WebSocket delta stream keeps hydrated rows fresh so repeat reads serve
|
|
63
63
|
// pure-local with no network; after a drop, deltas may have been missed, so
|
|
64
64
|
// the next read of each query must re-confirm with the server once.
|
|
65
|
-
syncClient.on('sync:reconnecting', () => hydration.invalidate());
|
|
65
|
+
syncClient.on('sync:reconnecting', () => { hydration.invalidate(); });
|
|
66
66
|
return {
|
|
67
67
|
modelRegistry,
|
|
68
68
|
objectPool,
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
* client assembles the `ablo.<model>` lookup table from these.
|
|
16
16
|
*/
|
|
17
17
|
import { autorun } from 'mobx';
|
|
18
|
-
import { AbloClaimedError, AbloValidationError, formatClaimedErrorMessage, toAbloError, } from '../errors.js';
|
|
18
|
+
import { AbloClaimedError, AbloStaleContextError, AbloValidationError, formatClaimedErrorMessage, toAbloError, } from '../errors.js';
|
|
19
19
|
import { descriptionFromMeta } from '../coordination/schema.js';
|
|
20
20
|
import { reconcileFunctionalUpdate, } from './functionalUpdate.js';
|
|
21
21
|
import { Model, modelAsRow } from '../Model.js';
|
|
22
22
|
import { toMs } from '../utils/duration.js';
|
|
23
|
+
import { LEASE_TTL_MS } from '../wire/protocol.js';
|
|
23
24
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
24
25
|
import { ModelScope } from '../types/index.js';
|
|
25
26
|
const modelClientMeta = new WeakMap();
|
|
@@ -85,9 +86,11 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
85
86
|
// TTL (a genuine epoch-ms expiry, not a fabricated watermark), defaulting to
|
|
86
87
|
// the server's keepalive lease window when no TTL was requested.
|
|
87
88
|
const activeClaims = new Map();
|
|
88
|
-
// Server keepalive lease window (Hub `LEASE_RENEW_TTL_MS`
|
|
89
|
-
//
|
|
90
|
-
|
|
89
|
+
// Server keepalive lease window (Hub `LEASE_RENEW_TTL_MS` — the same
|
|
90
|
+
// `LEASE_TTL_MS` from wire/protocol.ts, so client estimate and server
|
|
91
|
+
// lease can't drift). The fallback expiry estimate when a claim is
|
|
92
|
+
// taken without an explicit TTL.
|
|
93
|
+
const DEFAULT_LEASE_TTL_MS = LEASE_TTL_MS;
|
|
91
94
|
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
92
95
|
value !== null &&
|
|
93
96
|
value.object === 'claim' &&
|
|
@@ -338,8 +341,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
338
341
|
if (options?.filter) {
|
|
339
342
|
result = result.filter(options.filter);
|
|
340
343
|
}
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
const orderEntry = options?.orderBy ? Object.entries(options.orderBy)[0] : undefined;
|
|
345
|
+
if (orderEntry) {
|
|
346
|
+
const [field, dir] = orderEntry;
|
|
343
347
|
result = [...result].sort((a, b) => {
|
|
344
348
|
const av = a[field];
|
|
345
349
|
const bv = b[field];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credentialEndpoint — the endpoint-string shape of `apiKey`.
|
|
3
|
+
*
|
|
4
|
+
* `Ablo({ schema, apiKey: '/api/ablo-session' })` — point the client at your
|
|
5
|
+
* session-mint route and the SDK owns the exchange: it POSTs the endpoint,
|
|
6
|
+
* parses the minted token, keeps it fresh (the credential lifecycle), and
|
|
7
|
+
* classifies failures onto the resolver tri-state. This is the Ably `authUrl`
|
|
8
|
+
* / Liveblocks `authEndpoint` model — the string form is the 95% case; the
|
|
9
|
+
* function form of `apiKey` remains the escape hatch for custom headers,
|
|
10
|
+
* bodies, or non-HTTP mints.
|
|
11
|
+
*
|
|
12
|
+
* Detection is by prefix: a string starting with `/`, `http://`, or
|
|
13
|
+
* `https://` is an endpoint; anything else is a literal key. Real Ablo keys
|
|
14
|
+
* are `sk_`/`pk_`/`ek_`/`rk_`-prefixed, so the two shapes cannot collide.
|
|
15
|
+
* (`ABLO_API_KEY` env values are NEVER endpoint-detected — the env var is
|
|
16
|
+
* always a literal key; see `resolveApiKey`.)
|
|
17
|
+
*
|
|
18
|
+
* Wire contract — matches the `ablo init` scaffold route exactly:
|
|
19
|
+
* POST <endpoint> (same-origin, `credentials: 'include'` so cookies flow)
|
|
20
|
+
* → 200 `{ token, expiresAt? }` fresh short-lived `ek_`/`rk_`
|
|
21
|
+
* → 200 `{ token: null }` or 401/403 the login itself is gone (sign out)
|
|
22
|
+
* → anything else transient — retry, NEVER sign out
|
|
23
|
+
*
|
|
24
|
+
* The tri-state mapping is the whole point of building this in: hand-written
|
|
25
|
+
* thunks routinely get it wrong (mapping any `!res.ok` to `null` signs the
|
|
26
|
+
* user out on a 500). Encoded here once, every consumer inherits the correct
|
|
27
|
+
* terminal-vs-transient split (the Liveblocks `{ error: "forbidden" }` vs
|
|
28
|
+
* retry contract, translated to HTTP statuses).
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Async callable that resolves the current credential. Mirrors the shape
|
|
32
|
+
* Anthropic / OpenAI / Stripe ship — used for credential rotation
|
|
33
|
+
* (e.g. AWS STS, GCP IAM, Vault) AND the short-lived per-user browser
|
|
34
|
+
* path (mint a fresh `ek_`/`rk_` from the signed-in session). Re-exported
|
|
35
|
+
* from `./auth` (and thence `./Ablo`) so existing import paths work; defined
|
|
36
|
+
* in this leaf so the credential resolver it types has no import cycle.
|
|
37
|
+
*
|
|
38
|
+
* Contract: resolve a token; resolve `null` when the login itself is gone
|
|
39
|
+
* (terminal → the credential lifecycle treats this as `session_expired` and
|
|
40
|
+
* signs out); or THROW on a transient failure (→ back off and retry, never
|
|
41
|
+
* sign out). A long-lived static `apiKey` string needs none of this — it is
|
|
42
|
+
* used as-is. This is the single credential resolver the SDK supports.
|
|
43
|
+
*/
|
|
44
|
+
export type ApiKeySetter = () => Promise<string | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Is this `apiKey` string a session-mint endpoint rather than a literal key?
|
|
47
|
+
* Prefix rule: `/relative/path`, `http://…`, or `https://…`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isCredentialEndpoint(value: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Build the resolver behind an endpoint-string `apiKey`. Conforms to the
|
|
52
|
+
* `ApiKeySetter` contract end-to-end:
|
|
53
|
+
* - resolves the minted token string on success,
|
|
54
|
+
* - resolves `null` when the login is gone (401/403, or an explicit
|
|
55
|
+
* `{ token: null }`) → terminal, the client signs out,
|
|
56
|
+
* - THROWS on anything transient (network failure, 5xx/429, malformed
|
|
57
|
+
* response) → the lifecycle backs off and retries, never signs out.
|
|
58
|
+
*
|
|
59
|
+
* A relative endpoint invoked server-side (Node fetch has no origin) throws —
|
|
60
|
+
* transient by contract, and `credentialLifecycle` already translates that
|
|
61
|
+
* exact failure into an actionable "use an absolute URL server-side" warning.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createEndpointCredentialResolver(endpoint: string): ApiKeySetter;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credentialEndpoint — the endpoint-string shape of `apiKey`.
|
|
3
|
+
*
|
|
4
|
+
* `Ablo({ schema, apiKey: '/api/ablo-session' })` — point the client at your
|
|
5
|
+
* session-mint route and the SDK owns the exchange: it POSTs the endpoint,
|
|
6
|
+
* parses the minted token, keeps it fresh (the credential lifecycle), and
|
|
7
|
+
* classifies failures onto the resolver tri-state. This is the Ably `authUrl`
|
|
8
|
+
* / Liveblocks `authEndpoint` model — the string form is the 95% case; the
|
|
9
|
+
* function form of `apiKey` remains the escape hatch for custom headers,
|
|
10
|
+
* bodies, or non-HTTP mints.
|
|
11
|
+
*
|
|
12
|
+
* Detection is by prefix: a string starting with `/`, `http://`, or
|
|
13
|
+
* `https://` is an endpoint; anything else is a literal key. Real Ablo keys
|
|
14
|
+
* are `sk_`/`pk_`/`ek_`/`rk_`-prefixed, so the two shapes cannot collide.
|
|
15
|
+
* (`ABLO_API_KEY` env values are NEVER endpoint-detected — the env var is
|
|
16
|
+
* always a literal key; see `resolveApiKey`.)
|
|
17
|
+
*
|
|
18
|
+
* Wire contract — matches the `ablo init` scaffold route exactly:
|
|
19
|
+
* POST <endpoint> (same-origin, `credentials: 'include'` so cookies flow)
|
|
20
|
+
* → 200 `{ token, expiresAt? }` fresh short-lived `ek_`/`rk_`
|
|
21
|
+
* → 200 `{ token: null }` or 401/403 the login itself is gone (sign out)
|
|
22
|
+
* → anything else transient — retry, NEVER sign out
|
|
23
|
+
*
|
|
24
|
+
* The tri-state mapping is the whole point of building this in: hand-written
|
|
25
|
+
* thunks routinely get it wrong (mapping any `!res.ok` to `null` signs the
|
|
26
|
+
* user out on a 500). Encoded here once, every consumer inherits the correct
|
|
27
|
+
* terminal-vs-transient split (the Liveblocks `{ error: "forbidden" }` vs
|
|
28
|
+
* retry contract, translated to HTTP statuses).
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Is this `apiKey` string a session-mint endpoint rather than a literal key?
|
|
32
|
+
* Prefix rule: `/relative/path`, `http://…`, or `https://…`.
|
|
33
|
+
*/
|
|
34
|
+
export function isCredentialEndpoint(value) {
|
|
35
|
+
return value.startsWith('/') || /^https?:\/\//i.test(value);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the resolver behind an endpoint-string `apiKey`. Conforms to the
|
|
39
|
+
* `ApiKeySetter` contract end-to-end:
|
|
40
|
+
* - resolves the minted token string on success,
|
|
41
|
+
* - resolves `null` when the login is gone (401/403, or an explicit
|
|
42
|
+
* `{ token: null }`) → terminal, the client signs out,
|
|
43
|
+
* - THROWS on anything transient (network failure, 5xx/429, malformed
|
|
44
|
+
* response) → the lifecycle backs off and retries, never signs out.
|
|
45
|
+
*
|
|
46
|
+
* A relative endpoint invoked server-side (Node fetch has no origin) throws —
|
|
47
|
+
* transient by contract, and `credentialLifecycle` already translates that
|
|
48
|
+
* exact failure into an actionable "use an absolute URL server-side" warning.
|
|
49
|
+
*/
|
|
50
|
+
export function createEndpointCredentialResolver(endpoint) {
|
|
51
|
+
return async () => {
|
|
52
|
+
// fetch() rejections (offline, DNS, relative URL in Node) propagate as-is:
|
|
53
|
+
// a throw IS the transient signal in the resolver contract.
|
|
54
|
+
const res = await fetch(endpoint, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
credentials: 'include',
|
|
57
|
+
});
|
|
58
|
+
// The login itself is gone — terminal. Only these two statuses sign out.
|
|
59
|
+
if (res.status === 401 || res.status === 403)
|
|
60
|
+
return null;
|
|
61
|
+
// 5xx / 429 / anything unexpected: the login may be perfectly valid —
|
|
62
|
+
// transient, retry later.
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
throw new Error(`credential endpoint ${endpoint} answered ${res.status} — transient, will retry`);
|
|
65
|
+
}
|
|
66
|
+
let body;
|
|
67
|
+
try {
|
|
68
|
+
body = await res.json();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`credential endpoint ${endpoint} returned non-JSON — expected { token }`);
|
|
72
|
+
}
|
|
73
|
+
// Distinguish "explicitly signed out" ({ token: null }) from "not a mint
|
|
74
|
+
// endpoint at all" (no `token` key — misconfiguration must be LOUD and
|
|
75
|
+
// transient, never a silent sign-out).
|
|
76
|
+
if (typeof body !== 'object' || body === null || !('token' in body)) {
|
|
77
|
+
throw new Error(`credential endpoint ${endpoint} returned no \`token\` field — expected { token }`);
|
|
78
|
+
}
|
|
79
|
+
const token = body.token;
|
|
80
|
+
if (token === null || token === undefined)
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof token !== 'string') {
|
|
83
|
+
throw new Error(`credential endpoint ${endpoint} returned a non-string \`token\``);
|
|
84
|
+
}
|
|
85
|
+
return token;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical Ablo Cloud endpoint constants — a ZERO-dependency leaf.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place the hosted API host is declared. Everything else
|
|
5
|
+
* (`client/auth.ts` URL resolution, the CLI's `DEFAULT_URL`, the Data Source
|
|
6
|
+
* connector's dial-out base, the generated OpenAPI `servers` entry, the
|
|
7
|
+
* network probe's default) imports from here, so the next API-domain
|
|
8
|
+
* migration is ONE edit — the `LEGACY_HOSTED_API_HOSTS` rewrite list in
|
|
9
|
+
* `client/auth.ts` (four retired ablo.finance hosts) is proof such
|
|
10
|
+
* migrations happen.
|
|
11
|
+
*
|
|
12
|
+
* Kept dependency-free on purpose: `schema/openapi.ts` and `source/connector.ts`
|
|
13
|
+
* consume it, and routing them through `client/auth.ts` would drag the error
|
|
14
|
+
* registry + credential policy (and a `schema → client → errors →
|
|
15
|
+
* coordination → schema` cycle) into subpaths that are otherwise clean.
|
|
16
|
+
*/
|
|
17
|
+
/** The hosted API domain (no scheme). */
|
|
18
|
+
export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
|
|
19
|
+
/** The hosted HTTP origin — `https://` + {@link ABLO_HOSTED_API_DOMAIN}. */
|
|
20
|
+
export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
|
|
21
|
+
/** Default `baseURL` when the caller passes none. Same value as
|
|
22
|
+
* {@link ABLO_HOSTED_HTTP_BASE_URL}; kept as a distinct name because it is
|
|
23
|
+
* the documented client-options default. */
|
|
24
|
+
export declare const ABLO_DEFAULT_BASE_URL = "https://api.abloatai.com";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical Ablo Cloud endpoint constants — a ZERO-dependency leaf.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place the hosted API host is declared. Everything else
|
|
5
|
+
* (`client/auth.ts` URL resolution, the CLI's `DEFAULT_URL`, the Data Source
|
|
6
|
+
* connector's dial-out base, the generated OpenAPI `servers` entry, the
|
|
7
|
+
* network probe's default) imports from here, so the next API-domain
|
|
8
|
+
* migration is ONE edit — the `LEGACY_HOSTED_API_HOSTS` rewrite list in
|
|
9
|
+
* `client/auth.ts` (four retired ablo.finance hosts) is proof such
|
|
10
|
+
* migrations happen.
|
|
11
|
+
*
|
|
12
|
+
* Kept dependency-free on purpose: `schema/openapi.ts` and `source/connector.ts`
|
|
13
|
+
* consume it, and routing them through `client/auth.ts` would drag the error
|
|
14
|
+
* registry + credential policy (and a `schema → client → errors →
|
|
15
|
+
* coordination → schema` cycle) into subpaths that are otherwise clean.
|
|
16
|
+
*/
|
|
17
|
+
/** The hosted API domain (no scheme). */
|
|
18
|
+
export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
|
|
19
|
+
/** The hosted HTTP origin — `https://` + {@link ABLO_HOSTED_API_DOMAIN}. */
|
|
20
|
+
export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
21
|
+
/** Default `baseURL` when the caller passes none. Same value as
|
|
22
|
+
* {@link ABLO_HOSTED_HTTP_BASE_URL}; kept as a distinct name because it is
|
|
23
|
+
* the documented client-options default. */
|
|
24
|
+
export const ABLO_DEFAULT_BASE_URL = ABLO_HOSTED_HTTP_BASE_URL;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* surface as the browser client — typed proxies, stateless transport.
|
|
24
24
|
*/
|
|
25
25
|
import { type AbloApiClientOptions } from './ApiClient.js';
|
|
26
|
-
import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions, CreateSessionParams, AbloSession } from './
|
|
26
|
+
import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions, CreateSessionParams, AbloSession } from './resourceTypes.js';
|
|
27
27
|
import type { ModelCreateParams, ModelDeleteParams, ServerReadOptions, ModelRetrieveParams, ModelUpdateParams } from './createModelProxy.js';
|
|
28
28
|
import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
|
|
29
29
|
import type { ModelUpdater, ContentionOptions } from './functionalUpdate.js';
|
package/dist/client/identity.js
CHANGED
|
@@ -168,9 +168,10 @@ async function resolveHosted(input) {
|
|
|
168
168
|
const baseUrl = input.baseUrl;
|
|
169
169
|
// The refresh path re-runs `exchangeApiKey` with a freshly-resolved apiKey, so
|
|
170
170
|
// it needs the same argument bag the policy used for the initial exchange.
|
|
171
|
+
const participantKind = input.kind === 'agent' ? 'agent' : 'system';
|
|
171
172
|
const exchangeArgs = {
|
|
172
173
|
baseUrl,
|
|
173
|
-
participantKind
|
|
174
|
+
participantKind,
|
|
174
175
|
participantId: input.options.agentId ?? input.options.user?.id,
|
|
175
176
|
wideScope: true,
|
|
176
177
|
ttlSeconds: 3600,
|