@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
package/dist/cli.cjs
CHANGED
|
@@ -211978,9 +211978,9 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|
|
211978
211978
|
}
|
|
211979
211979
|
});
|
|
211980
211980
|
|
|
211981
|
-
// node_modules
|
|
211981
|
+
// node_modules/balanced-match/dist/commonjs/index.js
|
|
211982
211982
|
var require_commonjs = __commonJS({
|
|
211983
|
-
"node_modules
|
|
211983
|
+
"node_modules/balanced-match/dist/commonjs/index.js"(exports2) {
|
|
211984
211984
|
"use strict";
|
|
211985
211985
|
init_cjs_shims();
|
|
211986
211986
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -212041,9 +212041,9 @@ var require_commonjs = __commonJS({
|
|
|
212041
212041
|
}
|
|
212042
212042
|
});
|
|
212043
212043
|
|
|
212044
|
-
// node_modules
|
|
212044
|
+
// node_modules/brace-expansion/dist/commonjs/index.js
|
|
212045
212045
|
var require_commonjs2 = __commonJS({
|
|
212046
|
-
"node_modules
|
|
212046
|
+
"node_modules/brace-expansion/dist/commonjs/index.js"(exports2) {
|
|
212047
212047
|
"use strict";
|
|
212048
212048
|
init_cjs_shims();
|
|
212049
212049
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -212120,17 +212120,19 @@ var require_commonjs2 = __commonJS({
|
|
|
212120
212120
|
}
|
|
212121
212121
|
function expand_(str, max, isTop) {
|
|
212122
212122
|
const expansions = [];
|
|
212123
|
-
|
|
212124
|
-
|
|
212125
|
-
|
|
212126
|
-
|
|
212127
|
-
|
|
212128
|
-
|
|
212129
|
-
|
|
212130
|
-
|
|
212131
|
-
|
|
212123
|
+
for (; ; ) {
|
|
212124
|
+
const m2 = (0, balanced_match_1.balanced)("{", "}", str);
|
|
212125
|
+
if (!m2)
|
|
212126
|
+
return [str];
|
|
212127
|
+
const pre = m2.pre;
|
|
212128
|
+
if (/\$$/.test(m2.pre)) {
|
|
212129
|
+
const post2 = m2.post.length ? expand_(m2.post, max, false) : [""];
|
|
212130
|
+
for (let k3 = 0; k3 < post2.length && k3 < max; k3++) {
|
|
212131
|
+
const expansion = pre + "{" + m2.body + "}" + post2[k3];
|
|
212132
|
+
expansions.push(expansion);
|
|
212133
|
+
}
|
|
212134
|
+
return expansions;
|
|
212132
212135
|
}
|
|
212133
|
-
} else {
|
|
212134
212136
|
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m2.body);
|
|
212135
212137
|
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m2.body);
|
|
212136
212138
|
const isSequence = isNumericSequence || isAlphaSequence;
|
|
@@ -212138,10 +212140,12 @@ var require_commonjs2 = __commonJS({
|
|
|
212138
212140
|
if (!isSequence && !isOptions) {
|
|
212139
212141
|
if (m2.post.match(/,(?!,).*\}/)) {
|
|
212140
212142
|
str = m2.pre + "{" + m2.body + escClose + m2.post;
|
|
212141
|
-
|
|
212143
|
+
isTop = true;
|
|
212144
|
+
continue;
|
|
212142
212145
|
}
|
|
212143
212146
|
return [str];
|
|
212144
212147
|
}
|
|
212148
|
+
const post = m2.post.length ? expand_(m2.post, max, false) : [""];
|
|
212145
212149
|
let n;
|
|
212146
212150
|
if (isSequence) {
|
|
212147
212151
|
n = m2.body.split(/\.\./);
|
|
@@ -212205,15 +212209,15 @@ var require_commonjs2 = __commonJS({
|
|
|
212205
212209
|
}
|
|
212206
212210
|
}
|
|
212207
212211
|
}
|
|
212212
|
+
return expansions;
|
|
212208
212213
|
}
|
|
212209
|
-
return expansions;
|
|
212210
212214
|
}
|
|
212211
212215
|
}
|
|
212212
212216
|
});
|
|
212213
212217
|
|
|
212214
|
-
// node_modules
|
|
212218
|
+
// node_modules/minimatch/dist/commonjs/assert-valid-pattern.js
|
|
212215
212219
|
var require_assert_valid_pattern = __commonJS({
|
|
212216
|
-
"node_modules
|
|
212220
|
+
"node_modules/minimatch/dist/commonjs/assert-valid-pattern.js"(exports2) {
|
|
212217
212221
|
"use strict";
|
|
212218
212222
|
init_cjs_shims();
|
|
212219
212223
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -212231,9 +212235,9 @@ var require_assert_valid_pattern = __commonJS({
|
|
|
212231
212235
|
}
|
|
212232
212236
|
});
|
|
212233
212237
|
|
|
212234
|
-
// node_modules
|
|
212238
|
+
// node_modules/minimatch/dist/commonjs/brace-expressions.js
|
|
212235
212239
|
var require_brace_expressions = __commonJS({
|
|
212236
|
-
"node_modules
|
|
212240
|
+
"node_modules/minimatch/dist/commonjs/brace-expressions.js"(exports2) {
|
|
212237
212241
|
"use strict";
|
|
212238
212242
|
init_cjs_shims();
|
|
212239
212243
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -212349,9 +212353,9 @@ var require_brace_expressions = __commonJS({
|
|
|
212349
212353
|
}
|
|
212350
212354
|
});
|
|
212351
212355
|
|
|
212352
|
-
// node_modules
|
|
212356
|
+
// node_modules/minimatch/dist/commonjs/unescape.js
|
|
212353
212357
|
var require_unescape = __commonJS({
|
|
212354
|
-
"node_modules
|
|
212358
|
+
"node_modules/minimatch/dist/commonjs/unescape.js"(exports2) {
|
|
212355
212359
|
"use strict";
|
|
212356
212360
|
init_cjs_shims();
|
|
212357
212361
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -212366,9 +212370,9 @@ var require_unescape = __commonJS({
|
|
|
212366
212370
|
}
|
|
212367
212371
|
});
|
|
212368
212372
|
|
|
212369
|
-
// node_modules
|
|
212373
|
+
// node_modules/minimatch/dist/commonjs/ast.js
|
|
212370
212374
|
var require_ast = __commonJS({
|
|
212371
|
-
"node_modules
|
|
212375
|
+
"node_modules/minimatch/dist/commonjs/ast.js"(exports2) {
|
|
212372
212376
|
"use strict";
|
|
212373
212377
|
init_cjs_shims();
|
|
212374
212378
|
var _a;
|
|
@@ -213021,9 +213025,9 @@ var require_ast = __commonJS({
|
|
|
213021
213025
|
}
|
|
213022
213026
|
});
|
|
213023
213027
|
|
|
213024
|
-
// node_modules
|
|
213028
|
+
// node_modules/minimatch/dist/commonjs/escape.js
|
|
213025
213029
|
var require_escape = __commonJS({
|
|
213026
|
-
"node_modules
|
|
213030
|
+
"node_modules/minimatch/dist/commonjs/escape.js"(exports2) {
|
|
213027
213031
|
"use strict";
|
|
213028
213032
|
init_cjs_shims();
|
|
213029
213033
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -213038,9 +213042,9 @@ var require_escape = __commonJS({
|
|
|
213038
213042
|
}
|
|
213039
213043
|
});
|
|
213040
213044
|
|
|
213041
|
-
// node_modules
|
|
213045
|
+
// node_modules/minimatch/dist/commonjs/index.js
|
|
213042
213046
|
var require_commonjs3 = __commonJS({
|
|
213043
|
-
"node_modules
|
|
213047
|
+
"node_modules/minimatch/dist/commonjs/index.js"(exports2) {
|
|
213044
213048
|
"use strict";
|
|
213045
213049
|
init_cjs_shims();
|
|
213046
213050
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -214561,9 +214565,9 @@ var require_is_glob = __commonJS({
|
|
|
214561
214565
|
}
|
|
214562
214566
|
});
|
|
214563
214567
|
|
|
214564
|
-
// node_modules/glob-parent/index.js
|
|
214568
|
+
// node_modules/fast-glob/node_modules/glob-parent/index.js
|
|
214565
214569
|
var require_glob_parent = __commonJS({
|
|
214566
|
-
"node_modules/glob-parent/index.js"(exports2, module2) {
|
|
214570
|
+
"node_modules/fast-glob/node_modules/glob-parent/index.js"(exports2, module2) {
|
|
214567
214571
|
"use strict";
|
|
214568
214572
|
init_cjs_shims();
|
|
214569
214573
|
var isGlob = require_is_glob();
|
|
@@ -276766,30 +276770,30 @@ var wire = (category, httpStatus, retryable, message, recovery) => ({ category,
|
|
|
276766
276770
|
var client = (category, message) => ({ category, surface: "client", retryable: false, message });
|
|
276767
276771
|
var ERROR_CODES = {
|
|
276768
276772
|
// ── auth (401) ─────────────────────────────────────────────────────
|
|
276769
|
-
apikey_invalid: wire("auth", 401, false, "API key
|
|
276770
|
-
apikey_revoked: wire("auth", 401, false, "API key has been revoked."),
|
|
276773
|
+
apikey_invalid: wire("auth", 401, false, "This API key isn't one Ablo recognizes \u2014 it may be mistyped, truncated, or belong to a different environment. Check the key and try again."),
|
|
276774
|
+
apikey_revoked: wire("auth", 401, false, "This API key has been revoked and can no longer be used. Mint a new key from the dashboard."),
|
|
276771
276775
|
// THE sync-engine access credential — the Stripe-style ephemeral key
|
|
276772
276776
|
// (`ek_` for users, `rk_` for agents) minted server-side from the login and
|
|
276773
276777
|
// presented as a Bearer. Its expiry is routine and re-mintable: get a fresh
|
|
276774
276778
|
// key from the still-valid session and retry — NEVER a sign-out. (An agent's
|
|
276775
276779
|
// expired `rk_` must not log a human out either.) This is the ONLY code on
|
|
276776
276780
|
// the silent re-mint path; see RecoveryClass `access_credential_expiry`.
|
|
276777
|
-
apikey_expired: wire("auth", 401, false, "API key has expired.", "access_credential_expiry"),
|
|
276778
|
-
apikey_missing: wire("auth", 401, false, "
|
|
276779
|
-
api_key_required: wire("auth", 401, false, "This operation requires an API key."),
|
|
276780
|
-
capability_id_missing: wire("auth", 401, false, "
|
|
276781
|
-
exchange_failed: wire("auth", 401, false, "The API
|
|
276782
|
-
identity_resolve_failed: wire("auth", 401, false, "
|
|
276781
|
+
apikey_expired: wire("auth", 401, false, "This ephemeral API key has expired. Mint a fresh key from your still-valid session and retry the request.", "access_credential_expiry"),
|
|
276782
|
+
apikey_missing: wire("auth", 401, false, "The request arrived without an API key. Send one as `Authorization: Bearer <key>`."),
|
|
276783
|
+
api_key_required: wire("auth", 401, false, "This operation requires an API key, and none was presented. Send one as `Authorization: Bearer <key>`."),
|
|
276784
|
+
capability_id_missing: wire("auth", 401, false, "This request must name a capability id, but none was provided."),
|
|
276785
|
+
exchange_failed: wire("auth", 401, false, "The API key could not be exchanged for a working credential \u2014 the exchange was rejected. Check that the key is still valid."),
|
|
276786
|
+
identity_resolve_failed: wire("auth", 401, false, "The server could not resolve an identity for this credential \u2014 the identity lookup was rejected. Check that the credential is still valid."),
|
|
276783
276787
|
auth_no_credentials: wire("auth", 401, false, "No recognized authentication credential was presented \u2014 no API key and no bearer JWT. Send `Authorization: Bearer <token>`."),
|
|
276784
|
-
identity_missing_organization: wire("auth", 401, false, "Authentication succeeded but
|
|
276788
|
+
identity_missing_organization: wire("auth", 401, false, "Authentication succeeded, but the credential resolves to no organization, so requests cannot be scoped. Check that the key or token carries an organization."),
|
|
276785
276789
|
// The long-lived login is gone — terminal, drives sign-out + re-auth.
|
|
276786
|
-
session_expired: wire("auth", 401, false, "
|
|
276790
|
+
session_expired: wire("auth", 401, false, "Your session has expired or is no longer valid. Sign in again to continue.", "session_expiry"),
|
|
276787
276791
|
// `jwt_invalid` is the residual fallback; the codes below split out the
|
|
276788
276792
|
// specific failure modes so an integrating customer can tell "I registered
|
|
276789
276793
|
// the wrong JWKS" from "my token has no org claim" from "wrong audience"
|
|
276790
276794
|
// rather than getting one opaque code for all of them.
|
|
276791
|
-
jwt_invalid: wire("auth", 401, false, "The bearer JWT could not
|
|
276792
|
-
jwt_malformed: wire("auth", 401, false, "The bearer
|
|
276795
|
+
jwt_invalid: wire("auth", 401, false, "The bearer JWT failed validation for a reason the server could not classify further. Check the token's issuer, signature, audience, and expiry."),
|
|
276796
|
+
jwt_malformed: wire("auth", 401, false, "The bearer token is not a well-formed JWT and could not be decoded. Check that the full, unmodified token was sent."),
|
|
276793
276797
|
jwt_missing_issuer: wire("auth", 401, false, "The bearer JWT has no `iss` (issuer) claim, so it cannot be routed to a trusted issuer."),
|
|
276794
276798
|
jwt_issuer_untrusted: wire("auth", 401, false, "The bearer JWT's `iss` is not a registered trusted issuer. Register it via POST /v1/trusted-issuers, or check the token's issuer claim."),
|
|
276795
276799
|
jwt_signature_invalid: wire("auth", 401, false, "The bearer JWT's signature could not be verified against the issuer's JWKS (wrong key, rotated key, or forged token)."),
|
|
@@ -276801,24 +276805,24 @@ var ERROR_CODES = {
|
|
|
276801
276805
|
// When a customer DOES present an external-IdP JWT, its expiry means
|
|
276802
276806
|
// re-authenticate against that IdP, so it classifies as a session expiry
|
|
276803
276807
|
// (which also keeps `isSessionErrorResponse` behaviour unchanged).
|
|
276804
|
-
jwt_expired: wire("auth", 401, false, "The bearer JWT has expired
|
|
276808
|
+
jwt_expired: wire("auth", 401, false, "The bearer JWT has expired. Obtain a fresh token from your identity provider and retry.", "session_expiry"),
|
|
276805
276809
|
jwt_org_membership_denied: wire("auth", 403, false, "The bearer JWT's subject is not an active member of the organization in its `org_id` claim (removed, suspended, or the claim does not match a membership)."),
|
|
276806
|
-
file_upload_auth_required: wire("auth", 401, false, "File
|
|
276807
|
-
browser_apikey_blocked: client("auth", "
|
|
276808
|
-
browser_database_url_blocked: client("auth", "A database connection string
|
|
276809
|
-
datasource_registration_failed: client("auth", "
|
|
276810
|
-
datasource_connection_unsupported: wire("validation", 400, false, "This deployment
|
|
276810
|
+
file_upload_auth_required: wire("auth", 401, false, "File uploads require an authenticated session. Sign in and retry."),
|
|
276811
|
+
browser_apikey_blocked: client("auth", "A raw API key was used from a browser, where anyone can read it. Keep secret keys server-side and hand the browser a short-lived ephemeral key instead."),
|
|
276812
|
+
browser_database_url_blocked: client("auth", "A database connection string was used from a browser context. It carries database credentials, so it must stay server-side."),
|
|
276813
|
+
datasource_registration_failed: client("auth", "The provided `databaseUrl` could not be registered as a data source. Check that the connection string is valid and the database is reachable."),
|
|
276814
|
+
datasource_connection_unsupported: wire("validation", 400, false, "This deployment does not accept direct connection-string data sources. Register a signed Data Source endpoint instead."),
|
|
276811
276815
|
datasource_direct_deprecated: wire("validation", 410, false, "The direct (connection string) datasource is deprecated. Register a signed Data Source endpoint instead \u2014 your app owns the write and your credentials never leave it."),
|
|
276812
276816
|
// ── permission / capability (403) ──────────────────────────────────
|
|
276813
|
-
capability_scope_denied: wire("capability", 403, false, "
|
|
276814
|
-
issuer_register_forbidden: wire("permission", 403, false, "Registering a trusted issuer requires a secret (sk_) API key."),
|
|
276815
|
-
capability_invalid: wire("capability", 403, false, "
|
|
276817
|
+
capability_scope_denied: wire("capability", 403, false, "This action falls outside the scope granted to the connection, so it was denied."),
|
|
276818
|
+
issuer_register_forbidden: wire("permission", 403, false, "Registering a trusted issuer requires a secret (`sk_`) API key. The key presented is not a secret key."),
|
|
276819
|
+
capability_invalid: wire("capability", 403, false, "This capability cannot be used \u2014 it is unknown, revoked, or expired. Request a fresh grant."),
|
|
276816
276820
|
test_database_not_registered: wire("permission", 403, false, "Test mode requires a registered dev database for this org \u2014 run `npx ablo init`, or construct the client with `databaseUrl` using your test key."),
|
|
276817
276821
|
tenant_routing_failed: wire("server", 500, true, "The org's registered database could not be resolved or dialed. Ablo never falls back to shared storage for a dedicated tenant \u2014 retry, and check the datasource status if it persists."),
|
|
276818
|
-
database_role_cannot_enforce_rls: wire("permission", 403, false, "The
|
|
276819
|
-
database_role_unreadable: wire("permission", 403, false, "
|
|
276820
|
-
database_tables_unforced_rls: wire("permission", 403, false, "
|
|
276821
|
-
database_host_not_allowed: wire("permission", 403, false, "The
|
|
276822
|
+
database_role_cannot_enforce_rls: wire("permission", 403, false, "The database role Ablo connects with is a superuser or has `BYPASSRLS`, so Postgres will not enforce row-level security for it. Connect with a role that is subject to RLS."),
|
|
276823
|
+
database_role_unreadable: wire("permission", 403, false, "Ablo could not introspect the database role it connects with, so it cannot verify that row-level security is enforced."),
|
|
276824
|
+
database_tables_unforced_rls: wire("permission", 403, false, "Some synced tables do not have `FORCE ROW LEVEL SECURITY` applied, so the table owner can bypass row isolation. Run `ALTER TABLE ... FORCE ROW LEVEL SECURITY` on each synced table."),
|
|
276825
|
+
database_host_not_allowed: wire("permission", 403, false, "The database host resolves to a private, loopback, or link-local address, which Ablo's servers will not connect to. Use a publicly resolvable host."),
|
|
276822
276826
|
// Deprecated spellings of the `database_*` codes above — still emitted by
|
|
276823
276827
|
// older servers; kept so they classify identically. Do not use in new code.
|
|
276824
276828
|
byo_role_cannot_enforce_rls: wire("permission", 403, false, "The direct Postgres connector role cannot enforce row-level security."),
|
|
@@ -276835,50 +276839,60 @@ var ERROR_CODES = {
|
|
|
276835
276839
|
// behind the holder) or re-read and rebase. `retryable: true` here turned
|
|
276836
276840
|
// every cross-client claim conflict into an infinite client resend loop
|
|
276837
276841
|
// (~150ms storm — found by the claims journey, 2026-06-10).
|
|
276838
|
-
claim_conflict: wire("claim", 409, false, "
|
|
276839
|
-
claim_lost: wire("claim", 409, false, "
|
|
276840
|
-
entity_claimed: wire("claim", 409, false, "
|
|
276841
|
-
malformed_claim: wire("claim", 400, false, "The claim payload
|
|
276842
|
-
malformed_subscription: wire("validation", 400, false, "The update_subscription payload was malformed; expected { syncGroups: string[] }
|
|
276843
|
-
model_claimed: wire("claim", 409, false, "
|
|
276844
|
-
model_claimed_timeout: wire("claim", 409, false, "
|
|
276842
|
+
claim_conflict: wire("claim", 409, false, "Another participant holds a claim on this row, so the write was rejected. Take a claim with `ablo.<model>.claim` to queue fairly behind the holder, or re-read and rebase."),
|
|
276843
|
+
claim_lost: wire("claim", 409, false, "The claim held on this row was lost before the write could apply. Re-acquire the claim and retry."),
|
|
276844
|
+
entity_claimed: wire("claim", 409, false, "This row is currently claimed by another participant, so the write was blocked. Queue behind the holder with `ablo.<model>.claim`, or wait for the claim to clear."),
|
|
276845
|
+
malformed_claim: wire("claim", 400, false, "The claim payload could not be parsed. A claim must name the model and the entity it targets; check the payload shape and resend."),
|
|
276846
|
+
malformed_subscription: wire("validation", 400, false, "The `update_subscription` payload was malformed; expected `{ syncGroups: string[] }`."),
|
|
276847
|
+
model_claimed: wire("claim", 409, false, "Another participant holds a claim on this row. Read `claim.state` to see who holds it, or queue behind them with a claim of your own."),
|
|
276848
|
+
model_claimed_timeout: wire("claim", 409, false, "Another participant held a claim on this row and did not release it in time. Retry, or read `claim.state` to see who holds it."),
|
|
276845
276849
|
model_claim_not_configured: client("claim", "Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically \u2014 there is no per-model claim configuration to add. This appears only when a model proxy is constructed directly without that runtime (an internal/advanced path)."),
|
|
276846
276850
|
model_watch_not_configured: client("claim", "watch() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport)."),
|
|
276847
276851
|
// ── stale context / idempotency (409) ──────────────────────────────
|
|
276848
|
-
stale_context: wire("conflict", 409, true, "The
|
|
276852
|
+
stale_context: wire("conflict", 409, true, "The row changed after you read it \u2014 the write's `readAt` watermark is older than the current row version. Re-read the row and retry."),
|
|
276849
276853
|
// Raised by the functional `update(id, current => next)` form once its
|
|
276850
276854
|
// internal reconcile budget is exhausted — the row stayed continuously
|
|
276851
276855
|
// contended. Client-side: the SDK already retried; the caller decides whether
|
|
276852
276856
|
// to back off, raise `retries`, or move the row to the WebSocket transport.
|
|
276853
|
-
contention_exhausted: client("conflict", "A functional update
|
|
276854
|
-
update_aborted: client("conflict", "The functional update
|
|
276855
|
-
idempotency_conflict: wire("conflict", 409, false, "
|
|
276856
|
-
idempotency_key_too_long: wire("validation", 400, false, "The supplied Idempotency-Key exceeds the maximum length."),
|
|
276857
|
+
contention_exhausted: client("conflict", "A functional update kept losing to concurrent writes and exhausted its reconcile budget. Back off and retry, raise `retries`, or move the row to the WebSocket transport."),
|
|
276858
|
+
update_aborted: client("conflict", "The functional update was aborted via its `AbortSignal` before the write landed; nothing was written."),
|
|
276859
|
+
idempotency_conflict: wire("conflict", 409, false, "This `Idempotency-Key` was already used with a different request body. Reuse a key only to retry an identical request; otherwise generate a new one."),
|
|
276860
|
+
idempotency_key_too_long: wire("validation", 400, false, "The supplied `Idempotency-Key` exceeds the maximum length. Use a shorter key \u2014 a UUID works well."),
|
|
276857
276861
|
// ── validation (400 / 422) ─────────────────────────────────────────
|
|
276858
276862
|
write_options_invalid: client("validation", "The write options (`idempotencyKey` / `label` / `wait` / `readAt` / `onStale` / `claim`) failed validation against the write-options schema."),
|
|
276859
276863
|
source_operation_id_required: client("validation", "A data-source operation arrived without the entity `id` it targets."),
|
|
276860
|
-
source_adapter_misconfigured: client("validation", "The data-source ORM adapter could not map a schema model onto the backing client
|
|
276861
|
-
|
|
276864
|
+
source_adapter_misconfigured: client("validation", "The data-source ORM adapter could not map a schema model onto the backing client \u2014 the client exposes no matching delegate or model. Check that the adapter and schema agree on model names."),
|
|
276865
|
+
// Wire since 2026-07-01: the sync-server validates every pushed/polled
|
|
276866
|
+
// source event before appending to the log and rejects the whole batch
|
|
276867
|
+
// with this code (`param` names the offending index + field path, e.g.
|
|
276868
|
+
// `events[3].entityId`). Also raised client-side by
|
|
276869
|
+
// `sourceEventForOperation` when an outbox event cannot be built.
|
|
276870
|
+
source_event_invalid: wire("validation", 400, false, "A data-source event was malformed \u2014 missing or invalid id, model, entityId, type, or field value. The whole event batch was rejected and nothing was ingested; fix the offending outbox row and re-send."),
|
|
276862
276871
|
duration_invalid: client("validation", 'A duration value was not a number of seconds or a "500ms" | "30s" | "3m" | "24h" string.'),
|
|
276863
276872
|
schema_definition_invalid: client("validation", "A schema definition value was invalid (bad column identifier, non-finite backfill, or unsupported schema-JSON version)."),
|
|
276864
276873
|
cli_invalid_arguments: client("validation", "The CLI was invoked with an unknown flag or a malformed flag value."),
|
|
276865
|
-
turn_validation_failed: wire("validation", 422, false, "The agent turn failed server-side validation."),
|
|
276874
|
+
turn_validation_failed: wire("validation", 422, false, "The agent turn payload failed server-side validation and was not applied."),
|
|
276866
276875
|
commit_operation_required: wire("validation", 400, false, "A commit must carry `operation` or `operations`."),
|
|
276876
|
+
// Wire since 2026-07-01: both commit transports (WS `commit` frame and HTTP
|
|
276877
|
+
// `/v1/commits`) validate every operation against `commitOperationSchema`
|
|
276878
|
+
// (`wire/frames.ts`) and reject the whole batch with this code. `param`
|
|
276879
|
+
// names the offending index + field path (e.g. `operations[3].readAt`).
|
|
276880
|
+
commit_operation_invalid: wire("validation", 400, false, "A commit operation failed validation against the wire commit-operation schema \u2014 wrong field type (e.g. a string `readAt`), unknown `type`, or missing `model`. The whole batch was rejected; the error names the offending operation index and field path."),
|
|
276867
276881
|
commit_operation_model_required: wire("validation", 400, false, "A commit operation is missing its `model`."),
|
|
276868
|
-
commit_operations_ambiguous: wire("validation", 400, false, "A commit supplied both `operation` and `operations`."),
|
|
276882
|
+
commit_operations_ambiguous: wire("validation", 400, false, "A commit supplied both `operation` and `operations`. Send one or the other, not both."),
|
|
276869
276883
|
commit_too_many_operations: wire("validation", 400, false, "A commit exceeded the per-commit operation limit; split it into smaller batches."),
|
|
276870
|
-
model_required_field_missing: wire("validation", 400, false, "
|
|
276871
|
-
model_identifier_missing: wire("validation", 400, false, "The
|
|
276872
|
-
snapshot_reserved_key: wire("validation", 400, false, "
|
|
276873
|
-
mesh_message_invalid_input: wire("validation", 400, false, "The mesh message failed input validation."),
|
|
276874
|
-
mesh_message_from_id_spoof: wire("validation", 403, false, "The mesh message `from` id does not match the authenticated sender."),
|
|
276875
|
-
mesh_message_from_kind_mismatch: wire("validation", 403, false, "The mesh message `from` kind does not match the sender."),
|
|
276876
|
-
agent_perception_missing_context: wire("validation", 422, false, "The agent perception request
|
|
276884
|
+
model_required_field_missing: wire("validation", 400, false, "The write is missing a field the model marks as required. Include the field and retry."),
|
|
276885
|
+
model_identifier_missing: wire("validation", 400, false, "The payload is missing the model's identifier, so the target row cannot be determined. Include the `id` field."),
|
|
276886
|
+
snapshot_reserved_key: wire("validation", 400, false, "The snapshot uses a key name that is reserved by the runtime. Rename the key and retry."),
|
|
276887
|
+
mesh_message_invalid_input: wire("validation", 400, false, "The mesh message payload failed input validation and was not delivered."),
|
|
276888
|
+
mesh_message_from_id_spoof: wire("validation", 403, false, "The mesh message's `from` id does not match the authenticated sender, so it was rejected \u2014 participants may only send as themselves."),
|
|
276889
|
+
mesh_message_from_kind_mismatch: wire("validation", 403, false, "The mesh message's `from` kind does not match the kind of the authenticated sender, so it was rejected."),
|
|
276890
|
+
agent_perception_missing_context: wire("validation", 422, false, "The agent perception request is missing context it needs to run. Include the required context fields and retry."),
|
|
276877
276891
|
// ── not found (404) ────────────────────────────────────────────────
|
|
276878
|
-
entity_not_found: wire("not_found", 404, false, "
|
|
276879
|
-
model_not_found: wire("not_found", 404, false, "
|
|
276880
|
-
mutate_update_entity_not_found: wire("not_found", 404, false, "The
|
|
276881
|
-
task_id_missing: wire("server", 502, true, "The task-create response
|
|
276892
|
+
entity_not_found: wire("not_found", 404, false, "No row exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
|
|
276893
|
+
model_not_found: wire("not_found", 404, false, "No row of this model exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
|
|
276894
|
+
mutate_update_entity_not_found: wire("not_found", 404, false, "The row targeted by this update does not exist \u2014 it may have been deleted since you read it. Re-read before retrying."),
|
|
276895
|
+
task_id_missing: wire("server", 502, true, "The task-create response arrived without a task id, so the result cannot be used. Retry the request."),
|
|
276882
276896
|
// ── data integrity / DB constraints ────────────────────────────────
|
|
276883
276897
|
// Emitted when a write is rejected by a database integrity constraint
|
|
276884
276898
|
// (Postgres class-23). All NON-retryable: the same payload re-sent
|
|
@@ -276886,83 +276900,83 @@ var ERROR_CODES = {
|
|
|
276886
276900
|
// retry. The server normalizer maps SQLSTATE → these codes and tucks the
|
|
276887
276901
|
// raw constraint/column/table detail into `details` rather than leaking
|
|
276888
276902
|
// the driver's message text onto the wire.
|
|
276889
|
-
not_null_violation: wire("validation", 400, false, "
|
|
276890
|
-
foreign_key_violation: wire("conflict", 409, false, "
|
|
276891
|
-
unique_violation: wire("conflict", 409, false, "
|
|
276892
|
-
check_violation: wire("validation", 400, false, "
|
|
276893
|
-
constraint_violation: wire("validation", 400, false, "
|
|
276903
|
+
not_null_violation: wire("validation", 400, false, "The database rejected the write because a required column was left empty \u2014 a not-null constraint. The error details name the column; supply a value and retry."),
|
|
276904
|
+
foreign_key_violation: wire("conflict", 409, false, "The database rejected the write on a foreign-key constraint: a referenced row does not exist, or the row being deleted is still referenced by others. The error details name the constraint."),
|
|
276905
|
+
unique_violation: wire("conflict", 409, false, "The write duplicates a value that must be unique \u2014 another row already holds it. Choose a different value, or update the existing row."),
|
|
276906
|
+
check_violation: wire("validation", 400, false, "The database rejected a value that fails one of its check constraints. The error details name the constraint; adjust the value and retry."),
|
|
276907
|
+
constraint_violation: wire("validation", 400, false, "The database rejected the write on an integrity constraint. The error details identify the specific constraint."),
|
|
276894
276908
|
column_type_mismatch: wire("validation", 400, false, "A structured (JSON) value was written to a column whose database type cannot hold it. Ablo adapts a json field to either a jsonb column (native) or a text column (serialized) \u2014 but a scalar column (integer, boolean, uuid, timestamp, \u2026) cannot store a JSON object or array. Use a jsonb or text column for this field. Ablo adapts to your column; it does not alter your schema."),
|
|
276895
276909
|
// ── tenant / unknown model (400) ───────────────────────────────────
|
|
276896
276910
|
server_execute_unknown_model: wire("tenant", 400, false, "Wrote to a model the server does not know. The server keeps its own copy of the schema \u2014 run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` before writing to new or changed models."),
|
|
276897
276911
|
mutate_create_unknown_model: wire("tenant", 400, false, "Created a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` first \u2014 the server keeps its own copy of the schema."),
|
|
276898
|
-
tenant_model_columns_unknown: wire("tenant", 400, false, "The
|
|
276899
|
-
tenant_model_missing_organization_id: wire("tenant", 400, false, "
|
|
276912
|
+
tenant_model_columns_unknown: wire("tenant", 400, false, "The columns for this model could not be resolved in the tenant database, so the operation cannot be mapped onto its table."),
|
|
276913
|
+
tenant_model_missing_organization_id: wire("tenant", 400, false, "This model's table has no `organization_id` column, which Ablo requires to isolate rows by organization. Add the column before syncing this model."),
|
|
276900
276914
|
// ── schema migration / declaration (validation) ────────────────────
|
|
276901
|
-
schema_mutable_missing_meta: wire("schema", 400, false, "
|
|
276902
|
-
schema_scope_kind_invalid: wire("schema", 400, false, "A scope
|
|
276903
|
-
schema_field_not_camelcase: wire("schema", 400, false, "A schema field name is not camelCase."),
|
|
276904
|
-
schema_field_consecutive_caps: wire("schema", 400, false, "A schema field name
|
|
276915
|
+
schema_mutable_missing_meta: wire("schema", 400, false, "The schema is declared mutable but is missing its required `meta` block."),
|
|
276916
|
+
schema_scope_kind_invalid: wire("schema", 400, false, "A scope declaration in the schema uses a kind the engine does not recognize."),
|
|
276917
|
+
schema_field_not_camelcase: wire("schema", 400, false, "A schema field name is not camelCase. Rename the field (for example `dueDate`) \u2014 Ablo derives column names from camelCase field names."),
|
|
276918
|
+
schema_field_consecutive_caps: wire("schema", 400, false, "A schema field name contains consecutive capital letters, which cannot be mapped to a column name unambiguously. Write acronyms in lower case (`apiKey`, not `APIKey`)."),
|
|
276905
276919
|
schema_reserved_field: client("schema", "A model redeclared a reserved base field (id, createdAt, updatedAt, organizationId, createdBy) that the SDK provides automatically."),
|
|
276906
|
-
schema_grants_shape_invalid: wire("schema", 400, false, "A grants declaration has an invalid shape."),
|
|
276907
|
-
schema_grants_identifier_unsafe: wire("schema", 400, false, "A grants declaration
|
|
276908
|
-
schema_grants_relation_kind: wire("schema", 400, false, "A grants relation
|
|
276909
|
-
schema_grants_relation_missing: wire("schema", 400, false, "A grants declaration
|
|
276910
|
-
schema_grants_target_not_scope_root: wire("schema", 400, false, "A grants
|
|
276911
|
-
drop_field: client("schema", "
|
|
276912
|
-
drop_model: client("schema", "
|
|
276913
|
-
lossy_recreate: client("schema", "
|
|
276914
|
-
made_required: client("schema", "
|
|
276915
|
-
required_field_added: client("schema", "
|
|
276916
|
-
enum_value_removed: client("schema", "
|
|
276917
|
-
risky_cast: client("schema", "
|
|
276920
|
+
schema_grants_shape_invalid: wire("schema", 400, false, "A `grants` declaration in the schema has an invalid shape and could not be parsed."),
|
|
276921
|
+
schema_grants_identifier_unsafe: wire("schema", 400, false, "A `grants` declaration references an identifier that is not safe to use in SQL. Use plain column and relation names."),
|
|
276922
|
+
schema_grants_relation_kind: wire("schema", 400, false, "A `grants` declaration references a relation of a kind it cannot traverse."),
|
|
276923
|
+
schema_grants_relation_missing: wire("schema", 400, false, "A `grants` declaration references a relation the model does not define. Check the relation name against the model."),
|
|
276924
|
+
schema_grants_target_not_scope_root: wire("schema", 400, false, "A `grants` declaration targets a model that is not a scope root, so access cannot be derived from it."),
|
|
276925
|
+
drop_field: client("schema", "This migration would drop an existing field, destroying the data stored in it."),
|
|
276926
|
+
drop_model: client("schema", "This migration would drop an entire model and its table, destroying the rows stored in it."),
|
|
276927
|
+
lossy_recreate: client("schema", "This migration can only apply by recreating the table, which would not preserve its existing rows."),
|
|
276928
|
+
made_required: client("schema", "This migration makes an existing optional field required, which rows without a value for it would violate."),
|
|
276929
|
+
required_field_added: client("schema", "This migration adds a new required field that existing rows have no value for."),
|
|
276930
|
+
enum_value_removed: client("schema", "This migration removes an enum value that existing rows may still hold."),
|
|
276931
|
+
risky_cast: client("schema", "This migration changes a column to a type its current values may not convert to cleanly."),
|
|
276918
276932
|
// ── claim / lease (409 / transport) ───────────────────────────────
|
|
276919
|
-
claim_lease_unavailable: wire("claim", 503, true, "The claim-lease coordination subsystem is unavailable
|
|
276920
|
-
claim_not_wired: client("claim", "
|
|
276921
|
-
claim_queued: wire("claim", 409, true, "The claim was queued behind
|
|
276922
|
-
claim_wait_aborted: wire("claim", 409, true, "
|
|
276923
|
-
claim_wait_poll_interval_required: client("claim", "
|
|
276924
|
-
grant_timeout: wire("claim", 504, true, "
|
|
276925
|
-
slide_intent_missing_deck_id: wire("claim", 400, false, "
|
|
276926
|
-
slide_intent_unknown_sibling: wire("claim", 400, false, "
|
|
276933
|
+
claim_lease_unavailable: wire("claim", 503, true, "The claim-lease coordination subsystem is temporarily unavailable, so the claim could not be processed. Retry shortly."),
|
|
276934
|
+
claim_not_wired: client("claim", "Claims were used, but this runtime has no claim support wired in. The standard `Ablo({ schema, apiKey })` client wires it up automatically."),
|
|
276935
|
+
claim_queued: wire("claim", 409, true, "The claim was queued behind the current lease holder and will be granted in turn. Wait, or read `claim.queue` to see your position."),
|
|
276936
|
+
claim_wait_aborted: wire("claim", 409, true, "The wait for this claim lease was aborted before the lease was granted."),
|
|
276937
|
+
claim_wait_poll_interval_required: client("claim", "Waiting on a claim requires a poll interval, and none was provided."),
|
|
276938
|
+
grant_timeout: wire("claim", 504, true, "The wait for a capability grant timed out before one arrived. Retry the request."),
|
|
276939
|
+
slide_intent_missing_deck_id: wire("claim", 400, false, "This slide claim is missing the id of the deck it belongs to."),
|
|
276940
|
+
slide_intent_unknown_sibling: wire("claim", 400, false, "This slide claim references a sibling slide that does not exist in the deck."),
|
|
276927
276941
|
// ── bootstrap (transport) ──────────────────────────────────────────
|
|
276928
|
-
bootstrap_fetch_timeout: wire("bootstrap", 504, true, "The bootstrap fetch timed out."),
|
|
276929
|
-
bootstrap_offline: wire("bootstrap", 503, true, "Bootstrap could not run because the client is offline."),
|
|
276930
|
-
bootstrap_offline_no_cache: wire("bootstrap", 503, false, "
|
|
276931
|
-
bootstrap_response_invalid: wire("bootstrap", 502, true, "The bootstrap response
|
|
276932
|
-
bootstrap_response_schema_invalid: wire("bootstrap", 502, true, "The bootstrap response failed schema validation."),
|
|
276942
|
+
bootstrap_fetch_timeout: wire("bootstrap", 504, true, "The initial bootstrap fetch timed out before the server responded. Retry shortly."),
|
|
276943
|
+
bootstrap_offline: wire("bootstrap", 503, true, "Bootstrap could not run because the client is offline. It can proceed once the network returns."),
|
|
276944
|
+
bootstrap_offline_no_cache: wire("bootstrap", 503, false, "The client is offline and no cached snapshot is available to start from, so there is no data to load until the network returns."),
|
|
276945
|
+
bootstrap_response_invalid: wire("bootstrap", 502, true, "The bootstrap response could not be parsed. Retrying may succeed."),
|
|
276946
|
+
bootstrap_response_schema_invalid: wire("bootstrap", 502, true, "The bootstrap response parsed but failed schema validation, so it was not applied. Retrying may succeed."),
|
|
276933
276947
|
// ── transport / connection ─────────────────────────────────────────
|
|
276934
|
-
exchange_malformed_response: wire("transport", 502, true, "The credential exchange returned a
|
|
276935
|
-
exchange_network_error: wire("transport", 503, true, "A network error
|
|
276936
|
-
source_network_error: wire("transport", 503, true, "A network error occurred talking to the source."),
|
|
276937
|
-
identity_network_error: wire("transport", 503, true, "A network error occurred resolving identity."),
|
|
276938
|
-
commit_no_result: wire("transport", 504, true, "The commit was sent but no result frame arrived."),
|
|
276939
|
-
commit_failed: wire("transport", 500, true, "The commit failed to apply."),
|
|
276940
|
-
commit_offline_grace_expired: wire("transport", 503, false, "The offline grace window expired before
|
|
276941
|
-
queue_too_deep: wire("transport", 503, true, "The transaction queue
|
|
276942
|
-
flush_timeout: wire("transport", 504, true, "
|
|
276943
|
-
wait_for_timeout: wire("transport", 504, true, "A wait-for condition timed out."),
|
|
276948
|
+
exchange_malformed_response: wire("transport", 502, true, "The credential exchange returned a response that could not be parsed. Retrying may succeed."),
|
|
276949
|
+
exchange_network_error: wire("transport", 503, true, "A network error interrupted the credential exchange. Check connectivity and retry."),
|
|
276950
|
+
source_network_error: wire("transport", 503, true, "A network error occurred while talking to the data source. Check connectivity and retry."),
|
|
276951
|
+
identity_network_error: wire("transport", 503, true, "A network error occurred while resolving your identity. Check connectivity and retry."),
|
|
276952
|
+
commit_no_result: wire("transport", 504, true, "The commit was sent, but no result frame arrived, so its outcome is unknown. It is safe to retry."),
|
|
276953
|
+
commit_failed: wire("transport", 500, true, "The commit reached the server but failed to apply. Retrying may succeed."),
|
|
276954
|
+
commit_offline_grace_expired: wire("transport", 503, false, "The offline grace window expired before this commit could be sent, so it was not applied. Re-apply the change once the connection returns."),
|
|
276955
|
+
queue_too_deep: wire("transport", 503, true, "The transaction queue is over its depth limit, so new writes are being rejected until it drains. Retry shortly."),
|
|
276956
|
+
flush_timeout: wire("transport", 504, true, "Flushing the transaction queue timed out before every pending write was sent. Retry once connectivity stabilizes."),
|
|
276957
|
+
wait_for_timeout: wire("transport", 504, true, "A wait-for condition timed out before it was satisfied. Retry, or extend the timeout."),
|
|
276944
276958
|
instance_at_capacity: wire("transport", 503, true, "The server is at connection capacity. Retry shortly \u2014 transient and not specific to your credentials."),
|
|
276945
|
-
fetch_unavailable: client("transport", "
|
|
276946
|
-
base_url_missing: client("transport", "
|
|
276947
|
-
sync_not_ready: client("transport", "A sync operation
|
|
276948
|
-
ws_not_ready: client("transport", "A frame was sent before the WebSocket was
|
|
276959
|
+
fetch_unavailable: client("transport", "This environment provides no `fetch` implementation, so HTTP requests cannot be made. Run on a platform with `fetch` (Node 18+, modern browsers) or supply a polyfill."),
|
|
276960
|
+
base_url_missing: client("transport", "The client has no base URL configured, so it cannot address the server. Set the base URL when constructing the client."),
|
|
276961
|
+
sync_not_ready: client("transport", "A sync operation ran before the client finished initializing. Wait for the client to be ready before syncing."),
|
|
276962
|
+
ws_not_ready: client("transport", "A frame was sent before the WebSocket connection was established. Wait for the connection to open before sending."),
|
|
276949
276963
|
// ── quota / rate limit (429) ──────────────────────────────────────
|
|
276950
|
-
quota_exceeded: wire("rate_limit", 429, true, "
|
|
276964
|
+
quota_exceeded: wire("rate_limit", 429, true, "Your organization has used up its configured usage quota. Requests will succeed again once the quota resets or the limit is raised."),
|
|
276951
276965
|
connection_limit_exceeded: wire("rate_limit", 429, true, "Too many concurrent WebSocket connections for this principal or organization. Close idle connections, or retry once others drain."),
|
|
276952
276966
|
// Per-CREDENTIAL request-rate limit — the fast (RPS/burst) axis, distinct from
|
|
276953
276967
|
// the slow-axis `quota_exceeded` (org daily/monthly usage). Keyed per API key,
|
|
276954
276968
|
// so one noisy key backs off without affecting the rest of the org. The
|
|
276955
276969
|
// `Retry-After` header carries the bucket-refill delay.
|
|
276956
|
-
rate_limit_exceeded: wire("rate_limit", 429, true, "This API key is sending requests
|
|
276970
|
+
rate_limit_exceeded: wire("rate_limit", 429, true, "This API key is sending requests faster than its rate limit allows. Slow down and retry after the delay in the `Retry-After` header."),
|
|
276957
276971
|
// ── server (5xx) ───────────────────────────────────────────────────
|
|
276958
|
-
internal_error: wire("server", 500, true, "
|
|
276959
|
-
quota_lookup_failed: wire("server", 503, true, "The
|
|
276972
|
+
internal_error: wire("server", 500, true, "Something went wrong on Ablo's side \u2014 an unexpected server error. It is safe to retry."),
|
|
276973
|
+
quota_lookup_failed: wire("server", 503, true, "The server could not load this organization's quota state, so the request was rejected rather than admitted unchecked. Retry shortly."),
|
|
276960
276974
|
// The per-key rate-limiter backend (Redis) was unreachable and the API is
|
|
276961
276975
|
// configured to FAIL CLOSED on that path, so the request was rejected rather
|
|
276962
276976
|
// than admitted unchecked. Retryable: the next attempt re-probes the backend.
|
|
276963
276977
|
rate_limiter_unavailable: wire("server", 503, true, "The rate-limiter backend is unavailable and this endpoint is configured to fail closed; retry shortly."),
|
|
276964
|
-
turn_open_failed: wire("server", 500, true, "The agent turn
|
|
276965
|
-
turn_close_failed: wire("server", 500, true, "The agent turn
|
|
276978
|
+
turn_open_failed: wire("server", 500, true, "The agent turn could not be opened on the server. It is safe to retry."),
|
|
276979
|
+
turn_close_failed: wire("server", 500, true, "The agent turn could not be closed cleanly on the server. It is safe to retry the close."),
|
|
276966
276980
|
// ── client-only invariants (never serialized) ──────────────────────
|
|
276967
276981
|
invalid_options: client("client", "The Ablo client was constructed with invalid or incomplete options."),
|
|
276968
276982
|
no_ablo_provider: client("client", "An Ablo hook was used outside of an Ablo provider."),
|
|
@@ -277008,52 +277022,65 @@ var ERROR_CODES = {
|
|
|
277008
277022
|
capability_id_required: wire("validation", 400, false, "A capability id is required for this request."),
|
|
277009
277023
|
organization_mismatch: wire("permission", 403, false, "The request targeted an organization the caller is not scoped to."),
|
|
277010
277024
|
project_scope_denied: wire("permission", 403, false, "The request targeted a project the caller's key is not scoped to."),
|
|
277011
|
-
project_slug_taken: wire("validation", 409, false, "A project with this slug already exists in the organization."),
|
|
277025
|
+
project_slug_taken: wire("validation", 409, false, "A project with this slug already exists in the organization. Choose a different slug."),
|
|
277012
277026
|
forbidden: wire("permission", 403, false, "The caller lacks permission for this operation."),
|
|
277013
|
-
source_api_key_unresolved: wire("auth", 401, false, "The
|
|
277027
|
+
source_api_key_unresolved: wire("auth", 401, false, "The API key presented for this data source could not be resolved to a known key. Check the key and its environment."),
|
|
277014
277028
|
capability_auth_disabled: wire("server", 503, false, "Capability authentication is disabled on this server."),
|
|
277015
|
-
provisioner_unavailable: wire("server", 503, false, "
|
|
277016
|
-
invalid_model: wire("validation", 400, false, "The request
|
|
277017
|
-
invalid_id: wire("validation", 400, false, "The request
|
|
277029
|
+
provisioner_unavailable: wire("server", 503, false, "This deployment has no database provisioner configured, so tables cannot be created here."),
|
|
277030
|
+
invalid_model: wire("validation", 400, false, "The model name in the request is not a valid model identifier."),
|
|
277031
|
+
invalid_id: wire("validation", 400, false, "The id in the request is not a valid identifier."),
|
|
277018
277032
|
unknown_model: wire("tenant", 400, false, "Named a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` \u2014 the server keeps its own copy of the schema."),
|
|
277019
|
-
model_not_tenant_scoped: wire("tenant", 400, false, "
|
|
277033
|
+
model_not_tenant_scoped: wire("tenant", 400, false, "This model is not tenant-scoped, so it cannot be queried through the tenant-scoped read path."),
|
|
277020
277034
|
schema_table_invalid: wire("schema", 500, false, "The model's table identifier is invalid."),
|
|
277021
277035
|
schema_scope_invalid: wire("schema", 500, false, "The model's scope predicate could not be built."),
|
|
277022
|
-
entity_fetch_failed: wire("server", 500, true, "The
|
|
277023
|
-
events_required: wire("validation", 400, false, "The request must include a non-empty events array."),
|
|
277024
|
-
ingest_failed: wire("validation", 400, false, "The source-event ingest
|
|
277025
|
-
migration_failed: wire("server", 500, false, "The schema migration failed
|
|
277026
|
-
|
|
277027
|
-
|
|
277036
|
+
entity_fetch_failed: wire("server", 500, true, "The server failed to fetch the requested entity. It is safe to retry."),
|
|
277037
|
+
events_required: wire("validation", 400, false, "The request must include a non-empty `events` array."),
|
|
277038
|
+
ingest_failed: wire("validation", 400, false, "The source-event batch was rejected during ingest and nothing was appended. Check the events against the expected shape and re-send."),
|
|
277039
|
+
migration_failed: wire("server", 500, false, "The schema migration failed while applying and did not complete."),
|
|
277040
|
+
schema_provisioning_forbidden: wire(
|
|
277041
|
+
"permission",
|
|
277042
|
+
403,
|
|
277043
|
+
false,
|
|
277044
|
+
"Schema registration could not create tables in the target database: the engine is not permitted to run DDL there."
|
|
277045
|
+
),
|
|
277046
|
+
model_query_failed: wire("validation", 400, false, "The model query failed to execute. Check the query filters and operators."),
|
|
277047
|
+
queries_required: wire("validation", 400, false, "The request must include a non-empty `queries` array."),
|
|
277028
277048
|
query_unsupported_operator: wire("validation", 400, false, "The query used an unsupported operator."),
|
|
277029
|
-
|
|
277030
|
-
|
|
277049
|
+
query_invalid_like_pattern: wire("validation", 400, false, "The `LIKE` pattern must not end with an escape character."),
|
|
277050
|
+
query_invalid_boolean: wire("validation", 400, false, "The query compared a boolean column against an invalid boolean literal."),
|
|
277051
|
+
protocol_version_unsupported: wire("transport", 426, false, "The client sync-protocol version is outside the range this server supports \u2014 upgrade the SDK (or the server was rolled back mid-fleet)."),
|
|
277052
|
+
database_unreachable: wire("validation", 400, false, "Ablo could not reach this database to check that it can stream replication. The connection string may be wrong, the host may not be reachable from Ablo's servers, or the credentials may not be accepted."),
|
|
277053
|
+
database_not_replication_ready: wire("validation", 400, false, "This database is not set up for logical replication yet. Every failing item \u2014 wal_level, the publication, the replication grant, a replica identity \u2014 is listed in the error details with its exact fix. `ablo connect` prints the one-time setup; `ablo connect --check` verifies it."),
|
|
277054
|
+
query_unknown_relation: wire("validation", 400, false, "The query references a relation the model does not define. Check the relation name against the schema."),
|
|
277055
|
+
query_relation_target_unknown: wire("schema", 500, false, "A relation in the query targets a model the schema does not define."),
|
|
277031
277056
|
query_invalid_identifier: wire("validation", 400, false, "The query contained an invalid identifier."),
|
|
277032
277057
|
org_id_required: wire("validation", 400, false, "An organization id is required for this request."),
|
|
277033
|
-
presence_identity_required: wire("validation", 400, false, "
|
|
277034
|
-
upload_fields_required: wire("validation", 400, false, "
|
|
277035
|
-
upload_items_required: wire("validation", 400, false, "The request must include a non-empty items array."),
|
|
277036
|
-
presigned_url_failed: wire("server", 500, true, "
|
|
277058
|
+
presence_identity_required: wire("validation", 400, false, "Presence requests must carry both `userId` and `organizationId`."),
|
|
277059
|
+
upload_fields_required: wire("validation", 400, false, "The upload request is missing a required field."),
|
|
277060
|
+
upload_items_required: wire("validation", 400, false, "The request must include a non-empty `items` array."),
|
|
277061
|
+
presigned_url_failed: wire("server", 500, true, "The server could not generate a presigned upload URL. It is safe to retry."),
|
|
277062
|
+
upload_not_configured: wire("server", 503, false, "Uploads are not configured on this deployment: the upload storage bucket and CDN domain are unset."),
|
|
277037
277063
|
task_id_required: wire("validation", 400, false, "A task id is required for this request."),
|
|
277038
|
-
claim_id_required: wire("validation", 400, false, "
|
|
277064
|
+
claim_id_required: wire("validation", 400, false, "A claim id is required for this request."),
|
|
277039
277065
|
commit_operation_action_required: wire("validation", 400, false, "A commit operation is missing its `action`."),
|
|
277040
277066
|
commit_operation_unsupported: wire("validation", 400, false, "A commit operation used an unsupported `action`."),
|
|
277041
277067
|
usage_invalid: wire("validation", 400, false, "The usage request was invalid."),
|
|
277042
277068
|
invalid_request: wire("validation", 400, false, "The request parameters were invalid."),
|
|
277043
277069
|
capability_not_found: wire("not_found", 404, false, "No capability exists with the given id."),
|
|
277044
|
-
invalid_participant_kind: wire("validation", 400, false, "The participant kind is
|
|
277045
|
-
invalid_sync_group: wire("validation", 400, false,
|
|
277046
|
-
narrow_scope_required: wire("validation", 400, false, "
|
|
277047
|
-
wide_scope_forbidden: wire("permission", 403, false, "
|
|
277048
|
-
capability_required: wire("auth", 401, false, "This operation requires a capability."),
|
|
277070
|
+
invalid_participant_kind: wire("validation", 400, false, "The participant kind is not one the server recognizes."),
|
|
277071
|
+
invalid_sync_group: wire("validation", 400, false, "Sync groups must be `default` or `<namespace>:<id>`."),
|
|
277072
|
+
narrow_scope_required: wire("validation", 400, false, "This request requires a scope narrowed to specific resources; the presented scope is too broad."),
|
|
277073
|
+
wide_scope_forbidden: wire("permission", 403, false, "This caller may not use a wide scope. Request a scope narrowed to the resources you need."),
|
|
277074
|
+
capability_required: wire("auth", 401, false, "This operation requires a capability, and none was presented."),
|
|
277049
277075
|
parent_turn_not_found: wire("not_found", 404, false, "The referenced parent turn does not exist."),
|
|
277050
|
-
parent_turn_foreign_agent: wire("permission", 403, false, "The parent turn belongs to a different agent."),
|
|
277076
|
+
parent_turn_foreign_agent: wire("permission", 403, false, "The referenced parent turn belongs to a different agent, so it cannot be used here."),
|
|
277051
277077
|
turn_not_found: wire("not_found", 404, false, "The referenced turn does not exist."),
|
|
277052
|
-
turn_foreign_agent: wire("permission", 403, false, "
|
|
277053
|
-
invalid_intent: wire("validation", 400, false, "The claim request was
|
|
277078
|
+
turn_foreign_agent: wire("permission", 403, false, "This turn belongs to a different agent, so the caller cannot act on it."),
|
|
277079
|
+
invalid_intent: wire("validation", 400, false, "The claim request was malformed and could not be processed."),
|
|
277054
277080
|
schema_too_large: wire("validation", 413, false, "The submitted schema exceeds the maximum size."),
|
|
277081
|
+
request_too_large: wire("validation", 413, false, "The request body exceeds the maximum size."),
|
|
277055
277082
|
invalid_schema: wire("validation", 400, false, "The submitted schema could not be parsed."),
|
|
277056
|
-
incompatible_change: wire("conflict", 409, false, "The schema change is incompatible with the
|
|
277083
|
+
incompatible_change: wire("conflict", 409, false, "The schema change is incompatible with the schema currently deployed and cannot be applied as-is.")
|
|
277057
277084
|
};
|
|
277058
277085
|
function errorCodeSpec(code) {
|
|
277059
277086
|
return ERROR_CODES[code];
|
|
@@ -279596,7 +279623,7 @@ function readProjectDatabaseUrl(cwd = process.cwd()) {
|
|
|
279596
279623
|
for (const name of [".env.local", ".env"]) {
|
|
279597
279624
|
const path = (0, import_path.resolve)(cwd, name);
|
|
279598
279625
|
if (!(0, import_fs2.existsSync)(path)) continue;
|
|
279599
|
-
const match = (0, import_fs2.readFileSync)(path, "utf8")
|
|
279626
|
+
const match = /^DATABASE_URL=(.+)$/m.exec((0, import_fs2.readFileSync)(path, "utf8"));
|
|
279600
279627
|
if (match?.[1]) return match[1].trim().replace(/^["']|["']$/g, "");
|
|
279601
279628
|
}
|
|
279602
279629
|
return null;
|
|
@@ -279606,7 +279633,7 @@ function readProjectApiKey(cwd = process.cwd()) {
|
|
|
279606
279633
|
for (const name of [".env.local", ".env"]) {
|
|
279607
279634
|
const path = (0, import_path.resolve)(cwd, name);
|
|
279608
279635
|
if (!(0, import_fs2.existsSync)(path)) continue;
|
|
279609
|
-
const match = (0, import_fs2.readFileSync)(path, "utf8")
|
|
279636
|
+
const match = /^ABLO_API_KEY=(.+)$/m.exec((0, import_fs2.readFileSync)(path, "utf8"));
|
|
279610
279637
|
if (match?.[1]) return { key: match[1].trim().replace(/^["']|["']$/g, ""), source: name };
|
|
279611
279638
|
}
|
|
279612
279639
|
return null;
|
|
@@ -279641,6 +279668,12 @@ var import_path3 = require("path");
|
|
|
279641
279668
|
var import_child_process = require("child_process");
|
|
279642
279669
|
var import_schema2 = require("@abloatai/ablo/schema");
|
|
279643
279670
|
|
|
279671
|
+
// src/client/hostedEndpoints.ts
|
|
279672
|
+
init_cjs_shims();
|
|
279673
|
+
var ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
|
|
279674
|
+
var ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
279675
|
+
var ABLO_DEFAULT_BASE_URL = ABLO_HOSTED_HTTP_BASE_URL;
|
|
279676
|
+
|
|
279644
279677
|
// src/cli/config.ts
|
|
279645
279678
|
init_cjs_shims();
|
|
279646
279679
|
var import_os2 = require("os");
|
|
@@ -279872,12 +279905,18 @@ function guardActiveProjectKey() {
|
|
|
279872
279905
|
const available = Object.entries(profiles).filter(([, keys]) => hasKey(keys)).map(([name]) => name);
|
|
279873
279906
|
return { ok: hasKey(profiles[activeProfile]), activeProfile, available };
|
|
279874
279907
|
}
|
|
279908
|
+
function resolveEffectiveApiKey(modeOverride, cwd) {
|
|
279909
|
+
const fromProject = readProjectApiKey(cwd);
|
|
279910
|
+
if (fromProject) return { key: fromProject.key, source: fromProject.source };
|
|
279911
|
+
const key = resolveApiKey(modeOverride);
|
|
279912
|
+
return key ? { key, source: "stored" } : { key: void 0, source: null };
|
|
279913
|
+
}
|
|
279875
279914
|
function resolvePushPlan() {
|
|
279876
|
-
const
|
|
279877
|
-
if (
|
|
279878
|
-
|
|
279879
|
-
|
|
279880
|
-
return { flow:
|
|
279915
|
+
const { key, source } = resolveEffectiveApiKey();
|
|
279916
|
+
if (key != null && source != null && source !== "stored") {
|
|
279917
|
+
return { flow: modeFromKey(key) ?? getMode(), apiKey: key, source };
|
|
279918
|
+
}
|
|
279919
|
+
return { flow: getMode(), apiKey: key, source };
|
|
279881
279920
|
}
|
|
279882
279921
|
|
|
279883
279922
|
// src/cli/theme.ts
|
|
@@ -279902,7 +279941,7 @@ function coerceBackfill(raw) {
|
|
|
279902
279941
|
}
|
|
279903
279942
|
var DEFAULT_SCHEMA_PATH = "ablo/schema.ts";
|
|
279904
279943
|
var DEFAULT_EXPORT = "schema";
|
|
279905
|
-
var DEFAULT_URL =
|
|
279944
|
+
var DEFAULT_URL = ABLO_DEFAULT_BASE_URL;
|
|
279906
279945
|
function fmtSignal(s) {
|
|
279907
279946
|
const sig = s;
|
|
279908
279947
|
const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
|
|
@@ -280045,7 +280084,9 @@ function schemaGitState(schemaPath) {
|
|
|
280045
280084
|
}
|
|
280046
280085
|
async function fetchActiveSchema(url, apiKey) {
|
|
280047
280086
|
const ctrl = new AbortController();
|
|
280048
|
-
const t = setTimeout(() =>
|
|
280087
|
+
const t = setTimeout(() => {
|
|
280088
|
+
ctrl.abort();
|
|
280089
|
+
}, 3e3);
|
|
280049
280090
|
try {
|
|
280050
280091
|
const res = await fetch(`${url}/api/schema`, {
|
|
280051
280092
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
@@ -280112,7 +280153,7 @@ async function confirmPush(args, env) {
|
|
|
280112
280153
|
const project = getActiveProject();
|
|
280113
280154
|
const expected = project?.slug ?? "production";
|
|
280114
280155
|
const typed = await he({
|
|
280115
|
-
message: `This deploys to ${import_picocolors3.default.
|
|
280156
|
+
message: `This deploys to ${import_picocolors3.default.bold("production")}. Type ${import_picocolors3.default.bold(expected)} to confirm:`,
|
|
280116
280157
|
placeholder: expected
|
|
280117
280158
|
});
|
|
280118
280159
|
if (pD(typed) || String(typed).trim() !== expected) {
|
|
@@ -280122,7 +280163,7 @@ async function confirmPush(args, env) {
|
|
|
280122
280163
|
return;
|
|
280123
280164
|
}
|
|
280124
280165
|
if (!isProd && !args.yes && tty) {
|
|
280125
|
-
const ok = await ye({ message: `Apply to ${import_picocolors3.default.
|
|
280166
|
+
const ok = await ye({ message: `Apply to ${import_picocolors3.default.bold("sandbox")}?` });
|
|
280126
280167
|
if (pD(ok) || !ok) {
|
|
280127
280168
|
xe("Aborted.");
|
|
280128
280169
|
process.exit(1);
|
|
@@ -280131,11 +280172,11 @@ async function confirmPush(args, env) {
|
|
|
280131
280172
|
}
|
|
280132
280173
|
function printPushTarget(opts) {
|
|
280133
280174
|
const env = modeFromKey(opts.apiKey);
|
|
280134
|
-
const envLabel = env === "production" ? import_picocolors3.default.
|
|
280175
|
+
const envLabel = env === "production" ? import_picocolors3.default.bold("production") : env === "sandbox" ? import_picocolors3.default.bold("sandbox") : import_picocolors3.default.yellow("unknown env");
|
|
280135
280176
|
const project = getActiveProject();
|
|
280136
|
-
const projectLabel = project ? `${import_picocolors3.default.bold(project.slug)} ${import_picocolors3.default.dim(`(${project.id})`)}` : `${import_picocolors3.default.bold("default")} ${import_picocolors3.default.dim("(org-default
|
|
280177
|
+
const projectLabel = project ? `${import_picocolors3.default.bold(project.slug)} ${import_picocolors3.default.dim(`(${project.id})`)}` : `${import_picocolors3.default.bold("default")} ${import_picocolors3.default.dim("(org-default)")}`;
|
|
280137
280178
|
const cliMode = getMode();
|
|
280138
|
-
const modeNote = env && env !== cliMode ? ` ${import_picocolors3.default.yellow(`(
|
|
280179
|
+
const modeNote = env && env !== cliMode ? ` ${import_picocolors3.default.yellow(`(cli mode: ${cliMode})`)}` : "";
|
|
280139
280180
|
console.log(`
|
|
280140
280181
|
${brand("ablo")} ${import_picocolors3.default.dim("push")} ${import_picocolors3.default.dim("\u2192")} ${envLabel}${modeNote}`);
|
|
280141
280182
|
console.log(` ${import_picocolors3.default.dim("project")} ${projectLabel}`);
|
|
@@ -280144,20 +280185,20 @@ function printPushTarget(opts) {
|
|
|
280144
280185
|
` ${import_picocolors3.default.dim("key")} ${maskKey(opts.apiKey)} ${import_picocolors3.default.dim(`(${describeKeySource(opts.keySource)})`)}`
|
|
280145
280186
|
);
|
|
280146
280187
|
console.log(
|
|
280147
|
-
` ${import_picocolors3.default.dim("schema")} ${import_picocolors3.default.bold(opts.schemaPath)} ${import_picocolors3.default.dim(
|
|
280188
|
+
` ${import_picocolors3.default.dim("schema")} ${import_picocolors3.default.bold(opts.schemaPath)} ${import_picocolors3.default.dim(`${opts.modelCount} models, hash ${opts.hash}`)}
|
|
280148
280189
|
`
|
|
280149
280190
|
);
|
|
280150
280191
|
}
|
|
280151
280192
|
function describeKeySource(source) {
|
|
280152
280193
|
switch (source) {
|
|
280153
280194
|
case "env":
|
|
280154
|
-
return "ABLO_API_KEY
|
|
280195
|
+
return "ABLO_API_KEY";
|
|
280155
280196
|
case ".env.local":
|
|
280156
280197
|
return ".env.local";
|
|
280157
280198
|
case ".env":
|
|
280158
280199
|
return ".env";
|
|
280159
|
-
case "
|
|
280160
|
-
return "
|
|
280200
|
+
case "stored":
|
|
280201
|
+
return "ablo login";
|
|
280161
280202
|
}
|
|
280162
280203
|
}
|
|
280163
280204
|
async function push(argv) {
|
|
@@ -280170,14 +280211,9 @@ async function push(argv) {
|
|
|
280170
280211
|
}
|
|
280171
280212
|
let keySource = "env";
|
|
280172
280213
|
if (!args.apiKey) {
|
|
280173
|
-
const
|
|
280174
|
-
|
|
280175
|
-
|
|
280176
|
-
keySource = fromProject.source;
|
|
280177
|
-
} else {
|
|
280178
|
-
args.apiKey = resolveApiKey();
|
|
280179
|
-
keySource = "login";
|
|
280180
|
-
}
|
|
280214
|
+
const resolved = resolveEffectiveApiKey();
|
|
280215
|
+
args.apiKey = resolved.key;
|
|
280216
|
+
keySource = resolved.source ?? "stored";
|
|
280181
280217
|
}
|
|
280182
280218
|
if (!args.apiKey) {
|
|
280183
280219
|
console.error(
|
|
@@ -280268,6 +280304,12 @@ async function push(argv) {
|
|
|
280268
280304
|
` One or more synced tables don't have FORCE ROW LEVEL SECURITY. Run ${import_picocolors3.default.bold("npx ablo migrate")} to (re)apply the tenant policies, then re-push.`
|
|
280269
280305
|
)
|
|
280270
280306
|
);
|
|
280307
|
+
} else if (code === "capability_scope_denied" || code === "capability_invalid") {
|
|
280308
|
+
console.error(
|
|
280309
|
+
import_picocolors3.default.dim(
|
|
280310
|
+
` This is a ${import_picocolors3.default.bold("database privilege")} error (Postgres 42501 / row-level security), not a key scope \u2014 a different API key won't help. The role behind this org's database can't write the target. Provision a writable, RLS-scoped role with ${import_picocolors3.default.bold("npx ablo migrate")}, or check the org's database registration. See docs/plans/read-path-logical-replication-vs-hosting.md.`
|
|
280311
|
+
)
|
|
280312
|
+
);
|
|
280271
280313
|
} else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
|
|
280272
280314
|
console.error(
|
|
280273
280315
|
import_picocolors3.default.dim(
|
|
@@ -280277,7 +280319,7 @@ async function push(argv) {
|
|
|
280277
280319
|
} else {
|
|
280278
280320
|
console.error(
|
|
280279
280321
|
import_picocolors3.default.dim(
|
|
280280
|
-
` This key isn't authorized to push schema (needs ${import_picocolors3.default.bold("schema:push")}). ` + (keySource === "
|
|
280322
|
+
` This key isn't authorized to push schema (needs ${import_picocolors3.default.bold("schema:push")}). ` + (keySource === "stored" ? `It's your stored ${import_picocolors3.default.bold("ablo login")} sandbox key \u2014 a key in ${import_picocolors3.default.bold(".env.local")} or ${import_picocolors3.default.bold("ABLO_API_KEY")} takes precedence, so put a schema:push key there (sandbox ${import_picocolors3.default.bold("sk_test_")} or production ${import_picocolors3.default.bold("sk_live_")}) and re-push. ` : `Use a schema:push key \u2014 a sandbox ${import_picocolors3.default.bold("sk_test_")} or production ${import_picocolors3.default.bold("sk_live_")}. `) + `Manage keys at https://abloatai.com`
|
|
280281
280323
|
)
|
|
280282
280324
|
);
|
|
280283
280325
|
}
|
|
@@ -280341,32 +280383,36 @@ function planFor(schema, targetSchema = "public") {
|
|
|
280341
280383
|
return { statements: [...plan.statements, ...adapterTables], concurrent: plan.concurrent ?? [] };
|
|
280342
280384
|
}
|
|
280343
280385
|
var log = {
|
|
280344
|
-
info: (msg, fields) =>
|
|
280345
|
-
|
|
280346
|
-
|
|
280386
|
+
info: (msg, fields) => {
|
|
280387
|
+
console.log(`[migrate] ${msg}`, fields);
|
|
280388
|
+
},
|
|
280389
|
+
warn: (msg, fields) => {
|
|
280390
|
+
console.warn(import_picocolors4.default.yellow(`[migrate] ${msg}`), fields);
|
|
280391
|
+
},
|
|
280392
|
+
error: (msg, fields) => {
|
|
280393
|
+
console.error(import_picocolors4.default.red(`[migrate] ${msg}`), fields);
|
|
280394
|
+
}
|
|
280347
280395
|
};
|
|
280348
|
-
var PG_LOCK_NOT_AVAILABLE = "55P03";
|
|
280349
|
-
var LOCK_TIMEOUT = process.env.ABLO_SCHEMA_LOCK_TIMEOUT ?? process.env.ABLO_DDL_LOCK_TIMEOUT ?? "5s";
|
|
280350
|
-
var MAX_LOCK_ATTEMPTS = 5;
|
|
280351
280396
|
async function applyStatements(dbUrl, targetSchema, statements, concurrent = []) {
|
|
280352
280397
|
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
280353
280398
|
} });
|
|
280354
280399
|
const total = statements.length;
|
|
280355
280400
|
const startedAt = Date.now();
|
|
280401
|
+
const lockTimeout = (0, import_schema3.resolveDdlLockTimeout)();
|
|
280402
|
+
const maxLockAttempts = (0, import_schema3.resolveDdlMaxLockAttempts)();
|
|
280356
280403
|
log.info("applying migration plan", { targetSchema, statements: total });
|
|
280357
280404
|
try {
|
|
280358
280405
|
for (let attempt = 1; ; attempt++) {
|
|
280359
280406
|
try {
|
|
280360
280407
|
await sql.begin(async (tx) => {
|
|
280361
|
-
await tx.unsafe(`SET LOCAL lock_timeout = '${
|
|
280408
|
+
await tx.unsafe(`SET LOCAL lock_timeout = '${lockTimeout}'`);
|
|
280362
280409
|
await tx`SELECT pg_advisory_xact_lock(hashtext(${`provision:${targetSchema}`}))`;
|
|
280363
|
-
for (
|
|
280364
|
-
const statement = statements[index];
|
|
280410
|
+
for (const [index, statement] of statements.entries()) {
|
|
280365
280411
|
try {
|
|
280366
280412
|
await tx.unsafe(statement);
|
|
280367
280413
|
} catch (err) {
|
|
280368
280414
|
const pg = err ?? {};
|
|
280369
|
-
if (pg.code === PG_LOCK_NOT_AVAILABLE) throw err;
|
|
280415
|
+
if (pg.code === import_schema3.PG_LOCK_NOT_AVAILABLE) throw err;
|
|
280370
280416
|
log.error("migration plan failed", {
|
|
280371
280417
|
code: "migration_failed",
|
|
280372
280418
|
durationMs: Date.now() - startedAt,
|
|
@@ -280384,8 +280430,8 @@ async function applyStatements(dbUrl, targetSchema, statements, concurrent = [])
|
|
|
280384
280430
|
break;
|
|
280385
280431
|
} catch (err) {
|
|
280386
280432
|
const pg = err ?? {};
|
|
280387
|
-
if (pg.code === PG_LOCK_NOT_AVAILABLE && attempt <
|
|
280388
|
-
const backoffMs =
|
|
280433
|
+
if (pg.code === import_schema3.PG_LOCK_NOT_AVAILABLE && attempt < maxLockAttempts) {
|
|
280434
|
+
const backoffMs = (0, import_schema3.ddlLockRetryBackoffMs)(attempt);
|
|
280389
280435
|
log.warn("schema change blocked by a lock; backing off and retrying", { targetSchema, attempt, backoffMs });
|
|
280390
280436
|
await new Promise((resolve6) => setTimeout(resolve6, backoffMs));
|
|
280391
280437
|
continue;
|
|
@@ -280464,6 +280510,8 @@ var ABLO_PUBLICATION = "ablo_publication";
|
|
|
280464
280510
|
var ABLO_REPLICATION_ROLE = "ablo_replicator";
|
|
280465
280511
|
function parseConnectArgs(argv) {
|
|
280466
280512
|
let check2 = false;
|
|
280513
|
+
let register = false;
|
|
280514
|
+
let auditInfra = false;
|
|
280467
280515
|
let tables = [];
|
|
280468
280516
|
let role = ABLO_REPLICATION_ROLE;
|
|
280469
280517
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -280472,6 +280520,12 @@ function parseConnectArgs(argv) {
|
|
|
280472
280520
|
case "--check":
|
|
280473
280521
|
check2 = true;
|
|
280474
280522
|
break;
|
|
280523
|
+
case "--register":
|
|
280524
|
+
register = true;
|
|
280525
|
+
break;
|
|
280526
|
+
case "--audit-infra":
|
|
280527
|
+
auditInfra = true;
|
|
280528
|
+
break;
|
|
280475
280529
|
case "--tables": {
|
|
280476
280530
|
const value = argv[++i] ?? "";
|
|
280477
280531
|
tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
@@ -280484,7 +280538,7 @@ function parseConnectArgs(argv) {
|
|
|
280484
280538
|
throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
|
|
280485
280539
|
}
|
|
280486
280540
|
}
|
|
280487
|
-
return { check: check2, tables, role };
|
|
280541
|
+
return { check: check2, register, auditInfra, tables, role };
|
|
280488
280542
|
}
|
|
280489
280543
|
function quoteIdent(id) {
|
|
280490
280544
|
return `"${id.replace(/"/g, '""')}"`;
|
|
@@ -280508,12 +280562,12 @@ function connectSetupSql(input) {
|
|
|
280508
280562
|
function printConnectRecipe(args) {
|
|
280509
280563
|
const sql = connectSetupSql({ tables: args.tables, role: args.role });
|
|
280510
280564
|
console.log(`
|
|
280511
|
-
${brand("ablo")} ${import_picocolors5.default.dim("connect")} ${import_picocolors5.default.dim("logical replication \u2014 the
|
|
280565
|
+
${brand("ablo")} ${import_picocolors5.default.dim("connect")} ${import_picocolors5.default.dim("logical replication \u2014 the read path (your writes stay on your own backend)")}
|
|
280512
280566
|
`);
|
|
280513
280567
|
console.log(
|
|
280514
|
-
` Ablo
|
|
280515
|
-
schema. Your app
|
|
280516
|
-
Run this once against your Postgres ${import_picocolors5.default.dim("(as a superuser /
|
|
280568
|
+
` Ablo consumes your write-ahead log (WAL) via logical replication and ${import_picocolors5.default.bold("never")} runs DDL,
|
|
280569
|
+
owns, hosts, or migrates your schema. Your app continues to own the write path \u2014 Ablo tails
|
|
280570
|
+
the changes and serves them as live shapes. Run this once against your Postgres ${import_picocolors5.default.dim("(as a superuser / DB owner)")}:
|
|
280517
280571
|
`
|
|
280518
280572
|
);
|
|
280519
280573
|
console.log(` ${import_picocolors5.default.bold("1.")} Enable logical decoding ${import_picocolors5.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
|
|
@@ -280549,12 +280603,24 @@ function printConnectRecipe(args) {
|
|
|
280549
280603
|
);
|
|
280550
280604
|
console.log(
|
|
280551
280605
|
import_picocolors5.default.dim(
|
|
280552
|
-
` Reminder: Ablo never writes to your database
|
|
280553
|
-
|
|
280606
|
+
` Reminder: this is the READ path \u2014 Ablo tails your WAL and never writes to your database.
|
|
280607
|
+
Your writes keep going through your own backend. (Ablo HOSTING your rows, or dialing in via
|
|
280608
|
+
${import_picocolors5.default.bold("databaseUrl")}, is the deprecated posture \u2014 see the read-path decision doc.)`
|
|
280554
280609
|
)
|
|
280555
280610
|
);
|
|
280556
280611
|
console.log();
|
|
280557
280612
|
}
|
|
280613
|
+
var SYNC_INFRA_RELATIONS = [
|
|
280614
|
+
"sync_deltas",
|
|
280615
|
+
"sync_id_seq",
|
|
280616
|
+
"sync_metadata",
|
|
280617
|
+
"mutation_log"
|
|
280618
|
+
];
|
|
280619
|
+
var SYNC_INFRA_TYPES = [
|
|
280620
|
+
"participant_kind",
|
|
280621
|
+
"backfill_provenance",
|
|
280622
|
+
"confirmation_state"
|
|
280623
|
+
];
|
|
280558
280624
|
function printCheckItem(item) {
|
|
280559
280625
|
if (item.ok) {
|
|
280560
280626
|
console.log(` ${import_picocolors5.default.green("\u2713")} ${item.label}`);
|
|
@@ -280568,24 +280634,28 @@ function printCheckItem(item) {
|
|
|
280568
280634
|
async function probeReadiness(sql, opts = {}) {
|
|
280569
280635
|
const publication = opts.publication ?? ABLO_PUBLICATION;
|
|
280570
280636
|
const items = [];
|
|
280571
|
-
const walRows = await sql.unsafe(
|
|
280637
|
+
const walRows = await sql.unsafe(
|
|
280638
|
+
`SELECT setting FROM pg_settings WHERE name = 'wal_level'`
|
|
280639
|
+
);
|
|
280572
280640
|
const walLevel = walRows[0]?.setting ?? "unknown";
|
|
280573
280641
|
items.push(
|
|
280574
280642
|
walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors5.default.bold("logical")}` } : {
|
|
280575
280643
|
ok: false,
|
|
280576
280644
|
label: `wal_level is ${import_picocolors5.default.bold(walLevel)} (need ${import_picocolors5.default.bold("logical")})`,
|
|
280577
280645
|
fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
|
|
280578
|
-
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot
|
|
280646
|
+
On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
|
|
280647
|
+
On Neon enable Logical Replication in the project (Console \u2192 Settings \u2192 Logical Replication, or the API) \u2014 Neon forbids ALTER SYSTEM; the toggle sets wal_level=logical.`
|
|
280579
280648
|
}
|
|
280580
280649
|
);
|
|
280581
280650
|
const pubRows = await sql.unsafe(
|
|
280582
280651
|
`SELECT puballtables FROM pg_publication WHERE pubname = $1`,
|
|
280583
280652
|
[publication]
|
|
280584
280653
|
);
|
|
280654
|
+
const pubRow = pubRows[0];
|
|
280585
280655
|
items.push(
|
|
280586
|
-
|
|
280656
|
+
pubRow ? {
|
|
280587
280657
|
ok: true,
|
|
280588
|
-
label: `publication ${import_picocolors5.default.bold(publication)} exists ${import_picocolors5.default.dim(
|
|
280658
|
+
label: `publication ${import_picocolors5.default.bold(publication)} exists ${import_picocolors5.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
|
|
280589
280659
|
} : {
|
|
280590
280660
|
ok: false,
|
|
280591
280661
|
label: `publication ${import_picocolors5.default.bold(publication)} not found`,
|
|
@@ -280636,17 +280706,35 @@ On RDS: GRANT rds_replication TO <your_role>;`
|
|
|
280636
280706
|
}
|
|
280637
280707
|
return items;
|
|
280638
280708
|
}
|
|
280639
|
-
async function
|
|
280709
|
+
async function auditTenantSyncInfra(sql) {
|
|
280710
|
+
const artifacts = [];
|
|
280711
|
+
for (const name of SYNC_INFRA_RELATIONS) {
|
|
280712
|
+
const rows = await sql.unsafe(
|
|
280713
|
+
`SELECT to_regclass($1)::text AS reg`,
|
|
280714
|
+
[`public.${name}`]
|
|
280715
|
+
);
|
|
280716
|
+
artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
|
|
280717
|
+
}
|
|
280718
|
+
for (const name of SYNC_INFRA_TYPES) {
|
|
280719
|
+
const rows = await sql.unsafe(
|
|
280720
|
+
`SELECT to_regtype($1)::text AS reg`,
|
|
280721
|
+
[`public.${name}`]
|
|
280722
|
+
);
|
|
280723
|
+
artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
|
|
280724
|
+
}
|
|
280725
|
+
return artifacts;
|
|
280726
|
+
}
|
|
280727
|
+
function requireDatabaseUrl(verb) {
|
|
280640
280728
|
const dbUrl = readProjectDatabaseUrl();
|
|
280641
280729
|
if (!dbUrl) {
|
|
280642
280730
|
console.error(
|
|
280643
|
-
import_picocolors5.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors5.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors5.default.bold(
|
|
280731
|
+
import_picocolors5.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors5.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors5.default.bold(`ablo connect ${verb}`)}.`)
|
|
280644
280732
|
);
|
|
280645
280733
|
process.exit(1);
|
|
280646
280734
|
}
|
|
280647
|
-
|
|
280648
|
-
|
|
280649
|
-
|
|
280735
|
+
return dbUrl;
|
|
280736
|
+
}
|
|
280737
|
+
async function probeAndReport(dbUrl) {
|
|
280650
280738
|
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
280651
280739
|
} });
|
|
280652
280740
|
let items;
|
|
@@ -280660,7 +280748,14 @@ async function runCheck() {
|
|
|
280660
280748
|
}
|
|
280661
280749
|
await sql.end({ timeout: 2 });
|
|
280662
280750
|
for (const item of items) printCheckItem(item);
|
|
280663
|
-
|
|
280751
|
+
return items.filter((i) => !i.ok).length;
|
|
280752
|
+
}
|
|
280753
|
+
async function runCheck() {
|
|
280754
|
+
const dbUrl = requireDatabaseUrl("--check");
|
|
280755
|
+
console.log(`
|
|
280756
|
+
${brand("ablo")} ${import_picocolors5.default.dim("connect --check")} ${import_picocolors5.default.dim("logical-replication readiness")}
|
|
280757
|
+
`);
|
|
280758
|
+
const failures = await probeAndReport(dbUrl);
|
|
280664
280759
|
console.log();
|
|
280665
280760
|
if (failures === 0) {
|
|
280666
280761
|
console.log(` ${import_picocolors5.default.green("\u2713")} Ready \u2014 Ablo can connect and tail this database's WAL.
|
|
@@ -280673,6 +280768,114 @@ async function runCheck() {
|
|
|
280673
280768
|
);
|
|
280674
280769
|
process.exit(1);
|
|
280675
280770
|
}
|
|
280771
|
+
async function runRegister() {
|
|
280772
|
+
const dbUrl = requireDatabaseUrl("--register");
|
|
280773
|
+
const apiKey = resolveApiKey();
|
|
280774
|
+
if (!apiKey) {
|
|
280775
|
+
console.error(
|
|
280776
|
+
import_picocolors5.default.red(" Not logged in.") + import_picocolors5.default.dim(` Run ${import_picocolors5.default.bold("ablo login")} (or set ${import_picocolors5.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`)
|
|
280777
|
+
);
|
|
280778
|
+
process.exit(1);
|
|
280779
|
+
}
|
|
280780
|
+
console.log(`
|
|
280781
|
+
${brand("ablo")} ${import_picocolors5.default.dim("connect --register")} ${import_picocolors5.default.dim("register this database for replication")}
|
|
280782
|
+
`);
|
|
280783
|
+
const failures = await probeAndReport(dbUrl);
|
|
280784
|
+
if (failures > 0) {
|
|
280785
|
+
console.log(
|
|
280786
|
+
`
|
|
280787
|
+
${import_picocolors5.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors5.default.dim("\u2014 a database that isn\u2019t replication-ready can\u2019t stream. Fix the above, then re-run.")}
|
|
280788
|
+
`
|
|
280789
|
+
);
|
|
280790
|
+
process.exit(1);
|
|
280791
|
+
}
|
|
280792
|
+
const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
280793
|
+
let res;
|
|
280794
|
+
try {
|
|
280795
|
+
res = await fetch(`${apiUrl2}/v1/datasources`, {
|
|
280796
|
+
method: "POST",
|
|
280797
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
280798
|
+
body: JSON.stringify({ connectionString: dbUrl })
|
|
280799
|
+
});
|
|
280800
|
+
} catch (err) {
|
|
280801
|
+
console.error(import_picocolors5.default.red(`
|
|
280802
|
+
Couldn't reach ${apiUrl2}: ${err instanceof Error ? err.message : String(err)}
|
|
280803
|
+
`));
|
|
280804
|
+
process.exit(1);
|
|
280805
|
+
}
|
|
280806
|
+
if (res.ok) {
|
|
280807
|
+
const body2 = await res.json().catch(() => ({}));
|
|
280808
|
+
console.log(
|
|
280809
|
+
`
|
|
280810
|
+
${import_picocolors5.default.green("\u2713")} Registered${body2.host ? ` ${import_picocolors5.default.dim(body2.host)}` : ""}${body2.id ? ` ${import_picocolors5.default.dim(`(${body2.id})`)}` : ""} \u2014 Ablo will replicate this database on the next sync.
|
|
280811
|
+
`
|
|
280812
|
+
);
|
|
280813
|
+
process.exit(0);
|
|
280814
|
+
}
|
|
280815
|
+
const body = await res.json().catch(() => ({}));
|
|
280816
|
+
const code = body.code ?? body.error?.code;
|
|
280817
|
+
const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
|
|
280818
|
+
console.error(import_picocolors5.default.red(`
|
|
280819
|
+
Registration failed: ${message}`));
|
|
280820
|
+
if (code === "forbidden") {
|
|
280821
|
+
console.error(import_picocolors5.default.dim(` Registering a database needs a ${import_picocolors5.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors5.default.bold("ablo login")} for one.`));
|
|
280822
|
+
} else if (code === "datasource_connection_unsupported") {
|
|
280823
|
+
console.error(
|
|
280824
|
+
import_picocolors5.default.dim(` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`)
|
|
280825
|
+
);
|
|
280826
|
+
} else if (code === "database_not_replication_ready") {
|
|
280827
|
+
for (const f of body.details?.failures ?? []) {
|
|
280828
|
+
console.error(` ${import_picocolors5.default.red("\u2717")} ${import_picocolors5.default.bold(f.item ?? "item")}${f.actual ? import_picocolors5.default.dim(` (${f.actual})`) : ""}`);
|
|
280829
|
+
if (f.fix) for (const line of f.fix.split("\n")) console.error(` ${import_picocolors5.default.red("\u2022")} ${line}`);
|
|
280830
|
+
}
|
|
280831
|
+
console.error(import_picocolors5.default.dim(`
|
|
280832
|
+
Apply the fixes, verify with ${import_picocolors5.default.bold("ablo connect --check")}, then re-run.`));
|
|
280833
|
+
} else if (code === "database_unreachable") {
|
|
280834
|
+
if (body.details?.reason) console.error(import_picocolors5.default.dim(` ${body.details.reason}`));
|
|
280835
|
+
console.error(
|
|
280836
|
+
import_picocolors5.default.dim(
|
|
280837
|
+
` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
|
|
280838
|
+
Postgres can't be replicated by the hosted service. Use a reachable host (or a tunnel),
|
|
280839
|
+
or the signed ${import_picocolors5.default.bold("dataSource()")} endpoint fallback.`
|
|
280840
|
+
)
|
|
280841
|
+
);
|
|
280842
|
+
}
|
|
280843
|
+
console.error();
|
|
280844
|
+
process.exit(1);
|
|
280845
|
+
}
|
|
280846
|
+
async function runAuditInfra() {
|
|
280847
|
+
const dbUrl = requireDatabaseUrl("--audit-infra");
|
|
280848
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
280849
|
+
} });
|
|
280850
|
+
let artifacts;
|
|
280851
|
+
try {
|
|
280852
|
+
artifacts = await auditTenantSyncInfra(sql);
|
|
280853
|
+
} catch (err) {
|
|
280854
|
+
const pg = err ?? {};
|
|
280855
|
+
console.error(import_picocolors5.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
|
|
280856
|
+
await sql.end({ timeout: 2 });
|
|
280857
|
+
process.exit(1);
|
|
280858
|
+
}
|
|
280859
|
+
await sql.end({ timeout: 2 });
|
|
280860
|
+
console.log(`
|
|
280861
|
+
${brand("ablo")} ${import_picocolors5.default.dim("connect --audit-infra")} ${import_picocolors5.default.dim("Stage 5 tenant DB sync-infra audit")}
|
|
280862
|
+
`);
|
|
280863
|
+
const present = artifacts.filter((a) => a.present);
|
|
280864
|
+
if (present.length === 0) {
|
|
280865
|
+
console.log(` ${import_picocolors5.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
|
|
280866
|
+
`);
|
|
280867
|
+
process.exit(0);
|
|
280868
|
+
}
|
|
280869
|
+
for (const artifact of present) {
|
|
280870
|
+
const label = artifact.kind === "type" ? "type" : "relation";
|
|
280871
|
+
console.log(` ${import_picocolors5.default.yellow("!")} ${label} ${import_picocolors5.default.bold(`public.${artifact.name}`)} exists`);
|
|
280872
|
+
}
|
|
280873
|
+
console.log(
|
|
280874
|
+
`
|
|
280875
|
+
${import_picocolors5.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors5.default.dim("\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow ") + import_picocolors5.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors5.default.dim(".\n")
|
|
280876
|
+
);
|
|
280877
|
+
process.exit(1);
|
|
280878
|
+
}
|
|
280676
280879
|
async function connect(argv) {
|
|
280677
280880
|
let args;
|
|
280678
280881
|
try {
|
|
@@ -280685,18 +280888,28 @@ async function connect(argv) {
|
|
|
280685
280888
|
await runCheck();
|
|
280686
280889
|
return;
|
|
280687
280890
|
}
|
|
280891
|
+
if (args.register) {
|
|
280892
|
+
await runRegister();
|
|
280893
|
+
return;
|
|
280894
|
+
}
|
|
280895
|
+
if (args.auditInfra) {
|
|
280896
|
+
await runAuditInfra();
|
|
280897
|
+
return;
|
|
280898
|
+
}
|
|
280688
280899
|
printConnectRecipe(args);
|
|
280689
280900
|
}
|
|
280690
|
-
var CONNECT_USAGE = ` ablo connect \u2014 connect
|
|
280901
|
+
var CONNECT_USAGE = ` ablo connect \u2014 connect your database for the read path, via logical replication
|
|
280691
280902
|
|
|
280692
|
-
Ablo
|
|
280693
|
-
|
|
280903
|
+
The Electric/PowerSync/Zero model: Ablo consumes your WAL via logical replication and
|
|
280904
|
+
never runs DDL, owns, hosts, or migrates your schema. Your app continues to own the write path.
|
|
280694
280905
|
|
|
280695
280906
|
Usage:
|
|
280696
280907
|
npx ablo connect Print the exact setup SQL for your Postgres
|
|
280697
280908
|
npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
|
|
280698
280909
|
npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
|
|
280699
|
-
npx ablo connect --check Validate DATABASE_URL is replication-ready
|
|
280910
|
+
npx ablo connect --check Validate DATABASE_URL is replication-ready
|
|
280911
|
+
npx ablo connect --register Register DATABASE_URL so Ablo replicates it (one self-service step)
|
|
280912
|
+
npx ablo connect --audit-infra Read-only Stage 5 audit for deprecated Ablo sync tables/types`;
|
|
280700
280913
|
|
|
280701
280914
|
// src/cli/generate.ts
|
|
280702
280915
|
init_cjs_shims();
|
|
@@ -280823,16 +281036,17 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
|
280823
281036
|
action = `Created ${import_picocolors7.default.bold(".env.local")} with ${import_picocolors7.default.bold("ABLO_API_KEY")}`;
|
|
280824
281037
|
} else {
|
|
280825
281038
|
const content = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
280826
|
-
const match =
|
|
281039
|
+
const match = /^ABLO_API_KEY=(.*)$/m.exec(content);
|
|
281040
|
+
const existing = match?.[1] ?? "";
|
|
280827
281041
|
if (!match) {
|
|
280828
281042
|
(0, import_fs7.appendFileSync)(envPath, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
|
|
280829
281043
|
`);
|
|
280830
281044
|
action = `Added ${import_picocolors7.default.bold("ABLO_API_KEY")} to ${import_picocolors7.default.bold(".env.local")}`;
|
|
280831
|
-
} else if (
|
|
281045
|
+
} else if (existing === apiKey) {
|
|
280832
281046
|
action = `${import_picocolors7.default.bold(".env.local")} already has this key`;
|
|
280833
281047
|
} else {
|
|
280834
281048
|
(0, import_fs7.writeFileSync)(envPath, content.replace(/^ABLO_API_KEY=.*$/m, line));
|
|
280835
|
-
action = `Updated ${import_picocolors7.default.bold("ABLO_API_KEY")} in ${import_picocolors7.default.bold(".env.local")} ${import_picocolors7.default.dim(`(was ${
|
|
281049
|
+
action = `Updated ${import_picocolors7.default.bold("ABLO_API_KEY")} in ${import_picocolors7.default.bold(".env.local")} ${import_picocolors7.default.dim(`(was ${existing.slice(0, 12)}\u2026)`)}`;
|
|
280836
281050
|
}
|
|
280837
281051
|
}
|
|
280838
281052
|
const gitignorePath = (0, import_path5.resolve)(cwd, ".gitignore");
|
|
@@ -280910,7 +281124,7 @@ async function dev(argv) {
|
|
|
280910
281124
|
console.error(import_picocolors7.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
280911
281125
|
process.exit(1);
|
|
280912
281126
|
}
|
|
280913
|
-
if (!args.apiKey) args.apiKey =
|
|
281127
|
+
if (!args.apiKey) args.apiKey = resolveEffectiveApiKey("sandbox").key;
|
|
280914
281128
|
const key = classifyKey(args.apiKey, getMode());
|
|
280915
281129
|
if (!key.ok) {
|
|
280916
281130
|
console.error(import_picocolors7.default.red(` ${key.reason}`));
|
|
@@ -281034,7 +281248,7 @@ async function deviceLogin(argv, deps = {}) {
|
|
|
281034
281248
|
headers: { "content-type": "application/json" },
|
|
281035
281249
|
body: JSON.stringify({ client_id: CLIENT_ID, scope: "openid profile email" })
|
|
281036
281250
|
}).catch(() => null);
|
|
281037
|
-
if (!codeRes
|
|
281251
|
+
if (!codeRes?.ok) {
|
|
281038
281252
|
M2.error(`Couldn't start login against ${AUTH_URL}. Is the dashboard reachable?`);
|
|
281039
281253
|
process.exit(1);
|
|
281040
281254
|
}
|
|
@@ -281104,7 +281318,7 @@ ${import_picocolors8.default.dim(url)}`, "Approve in your browser");
|
|
|
281104
281318
|
...targetProject ? { project_slug: targetProject } : {}
|
|
281105
281319
|
})
|
|
281106
281320
|
}).catch(() => null);
|
|
281107
|
-
if (!provRes
|
|
281321
|
+
if (!provRes?.ok) {
|
|
281108
281322
|
s.stop("Could not provision a key.");
|
|
281109
281323
|
const reason = provRes ? (await provRes.json().catch(() => ({}))).error : void 0;
|
|
281110
281324
|
if (reason) M2.error(reason);
|
|
@@ -281418,7 +281632,9 @@ function expiryLabel(iso) {
|
|
|
281418
281632
|
}
|
|
281419
281633
|
async function ping(apiUrl2) {
|
|
281420
281634
|
const ctrl = new AbortController();
|
|
281421
|
-
const t = setTimeout(() =>
|
|
281635
|
+
const t = setTimeout(() => {
|
|
281636
|
+
ctrl.abort();
|
|
281637
|
+
}, 3e3);
|
|
281422
281638
|
try {
|
|
281423
281639
|
const res = await fetch(`${apiUrl2}/api/health`, { signal: ctrl.signal });
|
|
281424
281640
|
return res.ok;
|
|
@@ -281431,7 +281647,9 @@ async function ping(apiUrl2) {
|
|
|
281431
281647
|
async function fetchPushedSchema(apiUrl2, apiKey) {
|
|
281432
281648
|
if (!apiKey) return null;
|
|
281433
281649
|
const ctrl = new AbortController();
|
|
281434
|
-
const t = setTimeout(() =>
|
|
281650
|
+
const t = setTimeout(() => {
|
|
281651
|
+
ctrl.abort();
|
|
281652
|
+
}, 3e3);
|
|
281435
281653
|
try {
|
|
281436
281654
|
const res = await fetch(`${apiUrl2}/api/schema`, {
|
|
281437
281655
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
@@ -281445,6 +281663,47 @@ async function fetchPushedSchema(apiUrl2, apiKey) {
|
|
|
281445
281663
|
clearTimeout(t);
|
|
281446
281664
|
}
|
|
281447
281665
|
}
|
|
281666
|
+
async function sampleRead(apiUrl2, apiKey, modelTypename, n) {
|
|
281667
|
+
const ctrl = new AbortController();
|
|
281668
|
+
const t = setTimeout(() => {
|
|
281669
|
+
ctrl.abort();
|
|
281670
|
+
}, 4e3);
|
|
281671
|
+
try {
|
|
281672
|
+
const res = await fetch(
|
|
281673
|
+
`${apiUrl2}/v1/models/${encodeURIComponent(modelTypename)}/__ablo_health_probe_${n}__`,
|
|
281674
|
+
{ headers: { authorization: `Bearer ${apiKey}` }, signal: ctrl.signal }
|
|
281675
|
+
);
|
|
281676
|
+
if (res.ok) return "routed";
|
|
281677
|
+
let code;
|
|
281678
|
+
try {
|
|
281679
|
+
const body = await res.json();
|
|
281680
|
+
code = body.code ?? body.error?.code;
|
|
281681
|
+
} catch {
|
|
281682
|
+
}
|
|
281683
|
+
if (code === "entity_not_found") return "routed";
|
|
281684
|
+
if (code === "tenant_routing_failed") return "no_route";
|
|
281685
|
+
if (res.status === 401 || res.status === 403) return { forbidden: code };
|
|
281686
|
+
return { other: `${res.status}${code ? ` ${code}` : ""}` };
|
|
281687
|
+
} catch {
|
|
281688
|
+
return { other: "unreachable" };
|
|
281689
|
+
} finally {
|
|
281690
|
+
clearTimeout(t);
|
|
281691
|
+
}
|
|
281692
|
+
}
|
|
281693
|
+
async function probeDataPlane(apiUrl2, apiKey, modelTypename) {
|
|
281694
|
+
if (!apiKey) return { status: "skipped" };
|
|
281695
|
+
const samples = [];
|
|
281696
|
+
for (let i = 0; i < 3; i++) samples.push(await sampleRead(apiUrl2, apiKey, modelTypename, i));
|
|
281697
|
+
const forbidden = samples.find((s) => typeof s === "object" && "forbidden" in s);
|
|
281698
|
+
if (forbidden) return { status: "forbidden", detail: forbidden.forbidden };
|
|
281699
|
+
const routed = samples.filter((s) => s === "routed").length;
|
|
281700
|
+
const noRoute = samples.filter((s) => s === "no_route").length;
|
|
281701
|
+
if (routed === samples.length) return { status: "ok" };
|
|
281702
|
+
if (noRoute === samples.length) return { status: "no_database" };
|
|
281703
|
+
if (routed > 0 && noRoute > 0) return { status: "intermittent", ok: routed, failed: noRoute };
|
|
281704
|
+
const other = samples.find((s) => typeof s === "object" && "other" in s);
|
|
281705
|
+
return { status: "unknown", detail: other?.other ?? "inconclusive" };
|
|
281706
|
+
}
|
|
281448
281707
|
function formatConflict(conflict) {
|
|
281449
281708
|
if (!conflict) return "";
|
|
281450
281709
|
const parts = ["user", "agent", "system"].flatMap((k3) => conflict[k3] ? [`${k3}:${conflict[k3]}`] : []);
|
|
@@ -281459,12 +281718,19 @@ async function status(args = []) {
|
|
|
281459
281718
|
const key2 = describeEffectiveKey(mode2, process.env.ABLO_API_KEY, entry);
|
|
281460
281719
|
const plan2 = resolvePushPlan();
|
|
281461
281720
|
const activeProject2 = getActiveProject();
|
|
281462
|
-
const
|
|
281463
|
-
const pushed = await fetchPushedSchema(apiUrl2,
|
|
281721
|
+
const effective2 = resolveEffectiveApiKey();
|
|
281722
|
+
const pushed = await fetchPushedSchema(apiUrl2, effective2.key);
|
|
281464
281723
|
const out = {
|
|
281465
281724
|
mode: mode2,
|
|
281466
281725
|
// The locally-active project (`ablo projects use`); null = org-default.
|
|
281467
281726
|
project: activeProject2 ?? null,
|
|
281727
|
+
// The credential the CLI resolves for requests, with its provenance —
|
|
281728
|
+
// 'env' | '.env.local' | '.env' | 'stored' (users debug key confusion
|
|
281729
|
+
// constantly; the SOURCE is usually the answer).
|
|
281730
|
+
effectiveKey: {
|
|
281731
|
+
prefix: effective2.key ? effective2.key.slice(0, 12) : null,
|
|
281732
|
+
source: effective2.source
|
|
281733
|
+
},
|
|
281468
281734
|
keyPrefix: key2.keyPrefix,
|
|
281469
281735
|
keySource: key2.keySource,
|
|
281470
281736
|
keyMode: key2.keyMode,
|
|
@@ -281498,9 +281764,11 @@ async function status(args = []) {
|
|
|
281498
281764
|
console.log(`
|
|
281499
281765
|
${brand("ablo")} ${import_picocolors11.default.dim("status")}
|
|
281500
281766
|
`);
|
|
281501
|
-
|
|
281767
|
+
const effective = resolveEffectiveApiKey();
|
|
281768
|
+
if (effective.key && effective.source && effective.source !== "stored") {
|
|
281769
|
+
const label = effective.source === "env" ? "ABLO_API_KEY env" : effective.source;
|
|
281502
281770
|
console.log(
|
|
281503
|
-
` ${import_picocolors11.default.dim("key")} ${
|
|
281771
|
+
` ${import_picocolors11.default.dim("key")} ${effective.key.slice(0, 12)}\u2026 ${import_picocolors11.default.dim(`(${label} \u2014 overrides stored)`)}`
|
|
281504
281772
|
);
|
|
281505
281773
|
} else if (!cfg) {
|
|
281506
281774
|
console.log(` ${import_picocolors11.default.yellow("!")} Not logged in \u2014 run ${import_picocolors11.default.bold("ablo login")}.`);
|
|
@@ -281535,9 +281803,9 @@ async function status(args = []) {
|
|
|
281535
281803
|
const reachable = await ping(apiUrl2);
|
|
281536
281804
|
console.log(reachable ? import_picocolors11.default.green("reachable") : import_picocolors11.default.red("unreachable"));
|
|
281537
281805
|
if (reachable) {
|
|
281538
|
-
const introspectKey =
|
|
281806
|
+
const introspectKey = effective.key;
|
|
281539
281807
|
const pushed = await fetchPushedSchema(apiUrl2, introspectKey);
|
|
281540
|
-
if (pushed
|
|
281808
|
+
if (pushed?.active) {
|
|
281541
281809
|
const when = pushed.pushedAt ? ` ${import_picocolors11.default.dim(`@ ${pushed.pushedAt.slice(0, 10)}`)}` : "";
|
|
281542
281810
|
const ver = pushed.version != null ? ` ${import_picocolors11.default.dim(`(rev ${pushed.version})`)}` : "";
|
|
281543
281811
|
console.log(` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.bold(`${pushed.models.length} models pushed`)}${ver}${when}`);
|
|
@@ -281550,6 +281818,29 @@ async function status(args = []) {
|
|
|
281550
281818
|
} else if (pushed && !pushed.active) {
|
|
281551
281819
|
console.log(` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.yellow("none pushed")} ${import_picocolors11.default.dim(`(run ${import_picocolors11.default.bold("ablo push")} or ${import_picocolors11.default.bold("ablo dev")})`)}`);
|
|
281552
281820
|
}
|
|
281821
|
+
const firstPushedModel = pushed?.active ? pushed.models[0] : void 0;
|
|
281822
|
+
if (firstPushedModel !== void 0) {
|
|
281823
|
+
const probe = await probeDataPlane(apiUrl2, introspectKey, firstPushedModel.typename);
|
|
281824
|
+
if (probe.status === "no_database") {
|
|
281825
|
+
console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.red("\u2717 no database registered")}${org ? import_picocolors11.default.dim(` for org ${org}`) : ""}`);
|
|
281826
|
+
console.log(
|
|
281827
|
+
` ${import_picocolors11.default.dim(
|
|
281828
|
+
`reads/writes will fail with ${import_picocolors11.default.bold("tenant_routing_failed")}. Connect one with ${import_picocolors11.default.bold("ablo connect")}, or point ${import_picocolors11.default.bold("ABLO_API_KEY")} at an org that has a database.`
|
|
281829
|
+
)}`
|
|
281830
|
+
);
|
|
281831
|
+
} else if (probe.status === "intermittent") {
|
|
281832
|
+
console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.red(`\u2717 database routing is intermittent`)} ${import_picocolors11.default.dim(`(${probe.ok} ok / ${probe.failed} failed of ${probe.ok + probe.failed})`)}${org ? import_picocolors11.default.dim(` for org ${org}`) : ""}`);
|
|
281833
|
+
console.log(
|
|
281834
|
+
` ${import_picocolors11.default.dim(
|
|
281835
|
+
`some reads/writes fail with ${import_picocolors11.default.bold("tenant_routing_failed")} \u2014 the registration is unstable. Re-establish it with ${import_picocolors11.default.bold("ablo connect")} (or check for a recent server redeploy).`
|
|
281836
|
+
)}`
|
|
281837
|
+
);
|
|
281838
|
+
} else if (probe.status === "forbidden") {
|
|
281839
|
+
console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.yellow("? key not authorized to read")}${probe.detail ? import_picocolors11.default.dim(` (${probe.detail})`) : ""}`);
|
|
281840
|
+
} else if (probe.status === "unknown") {
|
|
281841
|
+
console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.yellow(`? data-plane check inconclusive (${probe.detail})`)}`);
|
|
281842
|
+
}
|
|
281843
|
+
}
|
|
281553
281844
|
}
|
|
281554
281845
|
console.log();
|
|
281555
281846
|
}
|
|
@@ -281720,8 +282011,10 @@ function parseMode(args) {
|
|
|
281720
282011
|
function positional(args) {
|
|
281721
282012
|
for (let i = 0; i < args.length; i++) {
|
|
281722
282013
|
const a = args[i];
|
|
282014
|
+
if (a === void 0) continue;
|
|
281723
282015
|
if (a.startsWith("-")) {
|
|
281724
|
-
|
|
282016
|
+
const next = args[i + 1];
|
|
282017
|
+
if (!a.includes("=") && next && !next.startsWith("-")) i++;
|
|
281725
282018
|
continue;
|
|
281726
282019
|
}
|
|
281727
282020
|
return a;
|
|
@@ -282042,8 +282335,8 @@ function verbRewrite(call, verb) {
|
|
|
282042
282335
|
const params = VERB_ARGS[verb];
|
|
282043
282336
|
if (!params) return null;
|
|
282044
282337
|
const args = call.getArguments();
|
|
282045
|
-
if (args.length === 0) return null;
|
|
282046
282338
|
const first = args[0];
|
|
282339
|
+
if (first === void 0) return null;
|
|
282047
282340
|
const calleeText = call.getExpression().getText();
|
|
282048
282341
|
if (verb === "create") {
|
|
282049
282342
|
if (import_ts_morph.Node.isObjectLiteralExpression(first) && hasKey2(first, "data")) return null;
|
|
@@ -282092,7 +282385,7 @@ async function upgrade(argv) {
|
|
|
282092
282385
|
edits.push({ file, line: node.getStartLineNumber(), rule, before, after });
|
|
282093
282386
|
};
|
|
282094
282387
|
const flag2 = (node, rule, hint) => {
|
|
282095
|
-
manual.push({ file, line: node.getStartLineNumber(), rule, snippet: node.getText().split("\n")[0].slice(0, 120), hint });
|
|
282388
|
+
manual.push({ file, line: node.getStartLineNumber(), rule, snippet: (node.getText().split("\n")[0] ?? "").slice(0, 120), hint });
|
|
282096
282389
|
};
|
|
282097
282390
|
for (const imp of sf.getImportDeclarations()) {
|
|
282098
282391
|
if (!ABLO_REACT.has(imp.getModuleSpecifierValue())) continue;
|
|
@@ -282122,13 +282415,13 @@ async function upgrade(argv) {
|
|
|
282122
282415
|
if (method in VERB_ARGS) {
|
|
282123
282416
|
const next = verbRewrite(call, method);
|
|
282124
282417
|
if (next) {
|
|
282125
|
-
record(call, `${method}\u2192object-param`, call.getText().split("\n")[0].slice(0, 80), next.slice(0, 80));
|
|
282418
|
+
record(call, `${method}\u2192object-param`, (call.getText().split("\n")[0] ?? "").slice(0, 80), next.slice(0, 80));
|
|
282126
282419
|
call.replaceWithText(next);
|
|
282127
282420
|
}
|
|
282128
282421
|
} else if (method === "load") {
|
|
282129
282422
|
const next = loadRewrite(call);
|
|
282130
282423
|
if (next) {
|
|
282131
|
-
record(call, "load\u2192retrieve/list", call.getText().split("\n")[0].slice(0, 80), next.slice(0, 80));
|
|
282424
|
+
record(call, "load\u2192retrieve/list", (call.getText().split("\n")[0] ?? "").slice(0, 80), next.slice(0, 80));
|
|
282132
282425
|
call.replaceWithText(next);
|
|
282133
282426
|
}
|
|
282134
282427
|
}
|
|
@@ -282438,7 +282731,7 @@ function stripComments(src) {
|
|
|
282438
282731
|
let inStr = false;
|
|
282439
282732
|
let res = "";
|
|
282440
282733
|
for (let i = 0; i < line.length; i++) {
|
|
282441
|
-
const c = line
|
|
282734
|
+
const c = line.charAt(i);
|
|
282442
282735
|
if (c === '"') {
|
|
282443
282736
|
inStr = !inStr;
|
|
282444
282737
|
res += c;
|
|
@@ -282466,8 +282759,10 @@ function parseBlocks(srcRaw) {
|
|
|
282466
282759
|
}
|
|
282467
282760
|
const body = src.slice(openIdx + 1, j2 - 1);
|
|
282468
282761
|
const kw = m2[1];
|
|
282762
|
+
const name = m2[2];
|
|
282763
|
+
if (name === void 0) throw new Error("unparseable block header in the Prisma schema");
|
|
282469
282764
|
const type = kw === "model" ? "model" : kw === "enum" ? "enum" : "other";
|
|
282470
|
-
blocks.push({ type, name
|
|
282765
|
+
blocks.push({ type, name, body });
|
|
282471
282766
|
headerRe.lastIndex = j2;
|
|
282472
282767
|
}
|
|
282473
282768
|
return blocks;
|
|
@@ -282478,7 +282773,7 @@ function parseEnumMembers(body) {
|
|
|
282478
282773
|
const t = line.trim();
|
|
282479
282774
|
if (!t || t.startsWith("@") || t.startsWith("@@")) continue;
|
|
282480
282775
|
const first = t.split(/\s+/)[0];
|
|
282481
|
-
if (/^\w+$/.test(first)) members.push(first);
|
|
282776
|
+
if (first !== void 0 && /^\w+$/.test(first)) members.push(first);
|
|
282482
282777
|
}
|
|
282483
282778
|
return members;
|
|
282484
282779
|
}
|
|
@@ -282487,9 +282782,11 @@ function parseFieldLine(line) {
|
|
|
282487
282782
|
if (!t || t.startsWith("@@") || t.startsWith("//")) return null;
|
|
282488
282783
|
const m2 = /^(\w+)\s+([A-Za-z_]\w*)(\[\])?(\?)?\s*(.*)$/.exec(t);
|
|
282489
282784
|
if (!m2) return null;
|
|
282785
|
+
const [, name, type] = m2;
|
|
282786
|
+
if (name === void 0 || type === void 0) return null;
|
|
282490
282787
|
return {
|
|
282491
|
-
name
|
|
282492
|
-
type
|
|
282788
|
+
name,
|
|
282789
|
+
type,
|
|
282493
282790
|
list: Boolean(m2[3]),
|
|
282494
282791
|
optional: Boolean(m2[4]),
|
|
282495
282792
|
attrs: m2[5] ?? ""
|
|
@@ -282502,11 +282799,11 @@ function blockMap(body) {
|
|
|
282502
282799
|
return /@@map\("([^"]+)"\)/.exec(body)?.[1];
|
|
282503
282800
|
}
|
|
282504
282801
|
function relationFkFields(attrs) {
|
|
282505
|
-
const rel = /@relation\(([^)]*)\)/.exec(attrs);
|
|
282506
|
-
if (
|
|
282507
|
-
const fields = /fields:\s*\[([^\]]*)\]/.exec(rel[1]
|
|
282508
|
-
if (
|
|
282509
|
-
return fields
|
|
282802
|
+
const rel = /@relation\(([^)]*)\)/.exec(attrs)?.[1];
|
|
282803
|
+
if (rel === void 0) return null;
|
|
282804
|
+
const fields = /fields:\s*\[([^\]]*)\]/.exec(rel)?.[1];
|
|
282805
|
+
if (fields === void 0) return null;
|
|
282806
|
+
return fields.split(",").map((s) => s.trim()).filter(Boolean);
|
|
282510
282807
|
}
|
|
282511
282808
|
function parsePrismaSchema(src) {
|
|
282512
282809
|
const blocks = parseBlocks(src);
|
|
@@ -282531,8 +282828,9 @@ function parsePrismaSchema(src) {
|
|
|
282531
282828
|
const column = mapAttr(raw.attrs);
|
|
282532
282829
|
if (modelNames.has(raw.type)) {
|
|
282533
282830
|
const fks = relationFkFields(raw.attrs);
|
|
282534
|
-
|
|
282535
|
-
|
|
282831
|
+
const fkField = fks && fks.length === 1 ? fks[0] : void 0;
|
|
282832
|
+
if (fkField !== void 0) {
|
|
282833
|
+
relations.push({ name: raw.name, target: tableOf.get(raw.type) ?? raw.type, fkField });
|
|
282536
282834
|
}
|
|
282537
282835
|
continue;
|
|
282538
282836
|
}
|
|
@@ -282595,6 +282893,7 @@ function parsePrismaPullArgs(argv) {
|
|
|
282595
282893
|
let force = false;
|
|
282596
282894
|
for (let i = 0; i < argv.length; i++) {
|
|
282597
282895
|
const arg = argv[i];
|
|
282896
|
+
if (arg === void 0) continue;
|
|
282598
282897
|
switch (arg) {
|
|
282599
282898
|
case "--schema":
|
|
282600
282899
|
schema = argv[++i] ?? schema;
|
|
@@ -282744,8 +283043,8 @@ async function lowerDrizzleModule(mod) {
|
|
|
282744
283043
|
const { foreignKeys } = getTableConfig(table);
|
|
282745
283044
|
for (const fk of foreignKeys) {
|
|
282746
283045
|
const ref = fk.reference();
|
|
282747
|
-
|
|
282748
|
-
|
|
283046
|
+
const [localCol, ...extraCols] = ref.columns;
|
|
283047
|
+
if (localCol === void 0 || extraCols.length > 0) continue;
|
|
282749
283048
|
const fkField = fieldKeyByColumn.get(localCol) ?? fieldKeyByColName.get(localCol.name) ?? localCol.name;
|
|
282750
283049
|
const target = getTableName(ref.foreignTable);
|
|
282751
283050
|
relations.push({ name: stripIdSuffix(fkField), target, fkField });
|
|
@@ -282773,6 +283072,7 @@ function parseDrizzlePullArgs(argv) {
|
|
|
282773
283072
|
let force = false;
|
|
282774
283073
|
for (let i = 0; i < argv.length; i++) {
|
|
282775
283074
|
const arg = argv[i];
|
|
283075
|
+
if (arg === void 0) continue;
|
|
282776
283076
|
switch (arg) {
|
|
282777
283077
|
case "--schema":
|
|
282778
283078
|
schema = argv[++i] ?? schema;
|
|
@@ -282832,7 +283132,7 @@ async function drizzlePull(argv) {
|
|
|
282832
283132
|
result = await buildSchemaSourceFromDrizzle({ mod, importPath: args.importPath });
|
|
282833
283133
|
} catch (err) {
|
|
282834
283134
|
const msg = err instanceof Error ? err.message : String(err);
|
|
282835
|
-
const hint =
|
|
283135
|
+
const hint = msg.includes("Cannot find package 'drizzle-orm'") ? import_picocolors18.default.dim(` (install ${import_picocolors18.default.bold("drizzle-orm")} in this project)`) : "";
|
|
282836
283136
|
console.error(import_picocolors18.default.red(` Couldn't load the schema: ${msg}`) + hint);
|
|
282837
283137
|
process.exit(1);
|
|
282838
283138
|
}
|
|
@@ -282864,7 +283164,7 @@ var RECOVERY_HINT = {
|
|
|
282864
283164
|
permission: "Your key isn't allowed to do this \u2014 check its scopes or role.",
|
|
282865
283165
|
session_expiry: "Your session expired \u2014 sign in again.",
|
|
282866
283166
|
access_credential_expiry: "Your access credential expired \u2014 refresh it and retry.",
|
|
282867
|
-
auth_blocked: "Authentication was blocked."
|
|
283167
|
+
auth_blocked: "Authentication was blocked \u2014 the credential itself was rejected. Check that the key matches this environment."
|
|
282868
283168
|
};
|
|
282869
283169
|
function titleForType(type) {
|
|
282870
283170
|
const core = type.replace(/^Ablo/, "").replace(/Error$/, "");
|
|
@@ -282900,7 +283200,9 @@ function renderKnownDetails(details, line) {
|
|
|
282900
283200
|
}
|
|
282901
283201
|
}
|
|
282902
283202
|
function renderCliError(err, opts = {}) {
|
|
282903
|
-
const line = opts.write ?? ((l2) =>
|
|
283203
|
+
const line = opts.write ?? ((l2) => {
|
|
283204
|
+
console.error(l2);
|
|
283205
|
+
});
|
|
282904
283206
|
const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
|
|
282905
283207
|
if (err instanceof AbloError) {
|
|
282906
283208
|
const codeTag = err.code ? ` ${import_picocolors19.default.dim(`[${err.code}]`)}` : "";
|
|
@@ -283055,6 +283357,7 @@ async function main() {
|
|
|
283055
283357
|
console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
|
|
283056
283358
|
console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
|
|
283057
283359
|
console.log(` npx ablo connect --check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
|
|
283360
|
+
console.log(` npx ablo connect --audit-infra Read-only audit for deprecated Ablo sync infra in a customer DB`);
|
|
283058
283361
|
console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
|
|
283059
283362
|
console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
|
|
283060
283363
|
console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
|
|
@@ -283080,7 +283383,7 @@ function bailIfCancelled(value) {
|
|
|
283080
283383
|
}
|
|
283081
283384
|
var INIT_FRAMEWORKS = ["nextjs", "vite", "remix", "vanilla"];
|
|
283082
283385
|
var INIT_AUTHS = ["apikey", "firebase", "auth0", "clerk", "supabase", "betterauth", "jwt"];
|
|
283083
|
-
var INIT_STORAGES = ["endpoint", "direct", "datasource"];
|
|
283386
|
+
var INIT_STORAGES = ["replication", "endpoint", "direct", "datasource"];
|
|
283084
283387
|
function parseInitArgs(args) {
|
|
283085
283388
|
const has = (flag2) => args.includes(flag2);
|
|
283086
283389
|
const val = (flag2) => {
|
|
@@ -283128,7 +283431,7 @@ function detectOrm(override) {
|
|
|
283128
283431
|
try {
|
|
283129
283432
|
const pkg = JSON.parse((0, import_fs12.readFileSync)("package.json", "utf-8"));
|
|
283130
283433
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
283131
|
-
if (deps["@prisma/client"] || deps
|
|
283434
|
+
if (deps["@prisma/client"] || deps.prisma) return "prisma";
|
|
283132
283435
|
if (deps["drizzle-orm"]) return "drizzle";
|
|
283133
283436
|
} catch {
|
|
283134
283437
|
}
|
|
@@ -283206,15 +283509,15 @@ async function init(args = []) {
|
|
|
283206
283509
|
const storageChoice = await chooseOption(
|
|
283207
283510
|
"storage",
|
|
283208
283511
|
opts.storage,
|
|
283209
|
-
"
|
|
283512
|
+
"replication",
|
|
283210
283513
|
INIT_STORAGES,
|
|
283211
283514
|
false,
|
|
283212
|
-
() => Promise.resolve("
|
|
283515
|
+
() => Promise.resolve("replication")
|
|
283213
283516
|
);
|
|
283214
283517
|
const storage = storageChoice === "datasource" ? "endpoint" : storageChoice;
|
|
283215
283518
|
if (storage === "direct") {
|
|
283216
283519
|
Me(
|
|
283217
|
-
"`--storage direct` uses the deprecated databaseUrl connector.
|
|
283520
|
+
"`--storage direct` uses the deprecated databaseUrl connector. Logical replication\n(`npx ablo connect`) is the supported path; a signed Data Source endpoint is the fallback.",
|
|
283218
283521
|
import_picocolors20.default.yellow("Deprecated")
|
|
283219
283522
|
);
|
|
283220
283523
|
}
|
|
@@ -283335,7 +283638,11 @@ async function init(args = []) {
|
|
|
283335
283638
|
`Run ${import_picocolors20.default.bold("npx ablo login")} (or add ${import_picocolors20.default.bold("ABLO_API_KEY")} to ${import_picocolors20.default.bold(envFile)})`,
|
|
283336
283639
|
`Set ${import_picocolors20.default.bold("DATABASE_URL")} in ${import_picocolors20.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
|
|
283337
283640
|
`Run ${import_picocolors20.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
|
|
283338
|
-
...storage === "
|
|
283641
|
+
...storage === "replication" ? [
|
|
283642
|
+
`Connect your database \u2014 ${import_picocolors20.default.bold("npx ablo connect")} prints the one-time logical-replication setup SQL to run on your Postgres`,
|
|
283643
|
+
`Verify it \u2014 ${import_picocolors20.default.bold("npx ablo connect --check")} walks wal_level, the publication, the role, and replica identity, with the exact fix for anything missing`,
|
|
283644
|
+
`Register it \u2014 ${import_picocolors20.default.bold("npx ablo connect --register")} tells Ablo to start replicating; your app keeps writing through your own backend while Ablo tails the WAL`
|
|
283645
|
+
] : storage === "direct" ? [
|
|
283339
283646
|
`Provision your DB: ${import_picocolors20.default.bold("npx ablo migrate")} (creates your synced-model tables with row-level security; keep your own migrations for everything else)`
|
|
283340
283647
|
] : [
|
|
283341
283648
|
`Provision your DB: ${import_picocolors20.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors20.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors20.default.bold("/api/ablo/source")}`
|
|
@@ -283440,7 +283747,7 @@ export {};
|
|
|
283440
283747
|
}
|
|
283441
283748
|
function generateEnv(storage, opts = {}) {
|
|
283442
283749
|
const { includeApiKey = true } = opts;
|
|
283443
|
-
const databaseBlock = storage === "direct" ? "# DEPRECATED direct connector. The client registers this connection (sent once\n# over TLS, stored sealed) so Ablo dials into your Postgres. Prefer
|
|
283750
|
+
const databaseBlock = storage === "direct" ? "# DEPRECATED direct connector. The client registers this connection (sent once\n# over TLS, stored sealed) so Ablo dials into your Postgres. Prefer logical\n# replication (`npx ablo connect`); to keep the transaction log in your infra too,\n# self-host the engine. Use a dedicated non-superuser role; the browser never sees this.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : storage === "replication" ? "# Used by `npx ablo connect` to set up + register logical replication \u2014 the\n# DIRECT (un-pooled) endpoint. Ablo TAILS your WAL from here; it never writes.\n# The client never sees it; the browser never sees it. Your DB stays yours.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
|
|
283444
283751
|
const webhookBlock = storage === "endpoint" ? "# Signing secret for the webhook receiver (app/api/ablo/webhooks/route.ts).\n# Ablo mints this when you register the endpoint's URL (POST /v1/webhook_endpoints\n# or the dashboard) and returns it once \u2014 paste it here.\nABLO_WEBHOOK_SECRET=whsec_your_endpoint_secret_here\n" : "";
|
|
283445
283752
|
const apiKeyBlock = includeApiKey ? "# Ablo Sync Engine \u2014 use a sk_test_ key for local dev (`npx ablo push`)\nABLO_API_KEY=sk_test_your_key_here\n" : "";
|
|
283446
283753
|
return `${apiKeyBlock}${webhookBlock}${databaseBlock}`;
|
|
@@ -283707,23 +284014,12 @@ import Ablo from '@abloatai/ablo';
|
|
|
283707
284014
|
import { AbloProvider } from '@abloatai/ablo/react';
|
|
283708
284015
|
import { schema } from '@/ablo/schema';
|
|
283709
284016
|
|
|
283710
|
-
// The browser client holds NO secret. The
|
|
284017
|
+
// The browser client holds NO secret. The endpoint string points at the route
|
|
283711
284018
|
// below, which mints a short-lived session token (already scoped to the org +
|
|
283712
|
-
// user); the
|
|
283713
|
-
//
|
|
283714
|
-
//
|
|
283715
|
-
const ablo = Ablo({
|
|
283716
|
-
schema,
|
|
283717
|
-
apiKey: async () => {
|
|
283718
|
-
const res = await fetch('/api/ablo-session', {
|
|
283719
|
-
method: 'POST',
|
|
283720
|
-
credentials: 'include',
|
|
283721
|
-
});
|
|
283722
|
-
if (!res.ok) return null;
|
|
283723
|
-
const { token } = (await res.json()) as { token: string | null };
|
|
283724
|
-
return token;
|
|
283725
|
-
},
|
|
283726
|
-
});
|
|
284019
|
+
// user); the SDK owns the exchange \u2014 it fetches the route (POST, same-origin
|
|
284020
|
+
// cookies) and keeps the token fresh (refresh timer + wake re-mint). A 401
|
|
284021
|
+
// from the route signs the user out; any other failure is retried.
|
|
284022
|
+
const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
283727
284023
|
|
|
283728
284024
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
283729
284025
|
return <AbloProvider client={ablo}>{children}</AbloProvider>;
|
|
@@ -283744,8 +284040,8 @@ export async function POST(): Promise<Response> {
|
|
|
283744
284040
|
const user = await getCurrentUser();
|
|
283745
284041
|
if (!user) return new Response('Unauthorized', { status: 401 });
|
|
283746
284042
|
|
|
283747
|
-
const { token } = await sync.sessions.create({ user: { id: user.id } });
|
|
283748
|
-
return Response.json({ token });
|
|
284043
|
+
const { token, expiresAt } = await sync.sessions.create({ user: { id: user.id } });
|
|
284044
|
+
return Response.json({ token, expiresAt });
|
|
283749
284045
|
}
|
|
283750
284046
|
|
|
283751
284047
|
// Validate the signed-in user SERVER-SIDE. This ships wired for Better Auth;
|