@abloatai/ablo 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/cli.cjs +234 -79
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.js +1 -0
- package/docs/quickstart.md +20 -1
- package/llms-full.txt +9 -0
- package/llms.txt +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Scoped-role automation + tenant-routing fix. `ablo migrate` now auto-creates the RLS-gated scoped role (zero SQL) with a log-safe SCRAM-SHA-256 password verifier, plus a Neon/Supabase scoped-role `databaseUrl` recipe. Fix a jsonb double-encode that corrupted per-tenant routing and silently fell back to the shared pool.
|
|
8
|
+
|
|
3
9
|
## 0.9.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/cli.cjs
CHANGED
|
@@ -125060,7 +125060,7 @@ ${lanes.join("\n")}
|
|
|
125060
125060
|
}
|
|
125061
125061
|
}
|
|
125062
125062
|
function createImportCallExpressionAMD(arg, containsLexicalThis) {
|
|
125063
|
-
const
|
|
125063
|
+
const resolve6 = factory2.createUniqueName("resolve");
|
|
125064
125064
|
const reject = factory2.createUniqueName("reject");
|
|
125065
125065
|
const parameters = [
|
|
125066
125066
|
factory2.createParameterDeclaration(
|
|
@@ -125069,7 +125069,7 @@ ${lanes.join("\n")}
|
|
|
125069
125069
|
/*dotDotDotToken*/
|
|
125070
125070
|
void 0,
|
|
125071
125071
|
/*name*/
|
|
125072
|
-
|
|
125072
|
+
resolve6
|
|
125073
125073
|
),
|
|
125074
125074
|
factory2.createParameterDeclaration(
|
|
125075
125075
|
/*modifiers*/
|
|
@@ -125086,7 +125086,7 @@ ${lanes.join("\n")}
|
|
|
125086
125086
|
factory2.createIdentifier("require"),
|
|
125087
125087
|
/*typeArguments*/
|
|
125088
125088
|
void 0,
|
|
125089
|
-
[factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]),
|
|
125089
|
+
[factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve6, reject]
|
|
125090
125090
|
)
|
|
125091
125091
|
)
|
|
125092
125092
|
]);
|
|
@@ -211695,8 +211695,8 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|
|
211695
211695
|
installPackage(options) {
|
|
211696
211696
|
this.packageInstallId++;
|
|
211697
211697
|
const request = { kind: "installPackage", ...options, id: this.packageInstallId };
|
|
211698
|
-
const promise = new Promise((
|
|
211699
|
-
(this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve:
|
|
211698
|
+
const promise = new Promise((resolve6, reject) => {
|
|
211699
|
+
(this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve6, reject });
|
|
211700
211700
|
});
|
|
211701
211701
|
this.installer.send(request);
|
|
211702
211702
|
return promise;
|
|
@@ -213965,7 +213965,7 @@ var require_path_browserify = __commonJS({
|
|
|
213965
213965
|
}
|
|
213966
213966
|
var posix = {
|
|
213967
213967
|
// path.resolve([from ...], to)
|
|
213968
|
-
resolve: function
|
|
213968
|
+
resolve: function resolve6() {
|
|
213969
213969
|
var resolvedPath = "";
|
|
213970
213970
|
var resolvedAbsolute = false;
|
|
213971
213971
|
var cwd;
|
|
@@ -218865,41 +218865,41 @@ var require_queue = __commonJS({
|
|
|
218865
218865
|
queue.drained = drained;
|
|
218866
218866
|
return queue;
|
|
218867
218867
|
function push2(value) {
|
|
218868
|
-
var p2 = new Promise(function(
|
|
218868
|
+
var p2 = new Promise(function(resolve6, reject) {
|
|
218869
218869
|
pushCb(value, function(err, result) {
|
|
218870
218870
|
if (err) {
|
|
218871
218871
|
reject(err);
|
|
218872
218872
|
return;
|
|
218873
218873
|
}
|
|
218874
|
-
|
|
218874
|
+
resolve6(result);
|
|
218875
218875
|
});
|
|
218876
218876
|
});
|
|
218877
218877
|
p2.catch(noop3);
|
|
218878
218878
|
return p2;
|
|
218879
218879
|
}
|
|
218880
218880
|
function unshift(value) {
|
|
218881
|
-
var p2 = new Promise(function(
|
|
218881
|
+
var p2 = new Promise(function(resolve6, reject) {
|
|
218882
218882
|
unshiftCb(value, function(err, result) {
|
|
218883
218883
|
if (err) {
|
|
218884
218884
|
reject(err);
|
|
218885
218885
|
return;
|
|
218886
218886
|
}
|
|
218887
|
-
|
|
218887
|
+
resolve6(result);
|
|
218888
218888
|
});
|
|
218889
218889
|
});
|
|
218890
218890
|
p2.catch(noop3);
|
|
218891
218891
|
return p2;
|
|
218892
218892
|
}
|
|
218893
218893
|
function drained() {
|
|
218894
|
-
var p2 = new Promise(function(
|
|
218894
|
+
var p2 = new Promise(function(resolve6) {
|
|
218895
218895
|
process.nextTick(function() {
|
|
218896
218896
|
if (queue.idle()) {
|
|
218897
|
-
|
|
218897
|
+
resolve6();
|
|
218898
218898
|
} else {
|
|
218899
218899
|
var previousDrain = queue.drain;
|
|
218900
218900
|
queue.drain = function() {
|
|
218901
218901
|
if (typeof previousDrain === "function") previousDrain();
|
|
218902
|
-
|
|
218902
|
+
resolve6();
|
|
218903
218903
|
queue.drain = previousDrain;
|
|
218904
218904
|
};
|
|
218905
218905
|
}
|
|
@@ -219396,9 +219396,9 @@ var require_stream3 = __commonJS({
|
|
|
219396
219396
|
});
|
|
219397
219397
|
}
|
|
219398
219398
|
_getStat(filepath) {
|
|
219399
|
-
return new Promise((
|
|
219399
|
+
return new Promise((resolve6, reject) => {
|
|
219400
219400
|
this._stat(filepath, this._fsStatSettings, (error, stats) => {
|
|
219401
|
-
return error === null ?
|
|
219401
|
+
return error === null ? resolve6(stats) : reject(error);
|
|
219402
219402
|
});
|
|
219403
219403
|
});
|
|
219404
219404
|
}
|
|
@@ -219423,10 +219423,10 @@ var require_async5 = __commonJS({
|
|
|
219423
219423
|
this._readerStream = new stream_1.default(this._settings);
|
|
219424
219424
|
}
|
|
219425
219425
|
dynamic(root, options) {
|
|
219426
|
-
return new Promise((
|
|
219426
|
+
return new Promise((resolve6, reject) => {
|
|
219427
219427
|
this._walkAsync(root, options, (error, entries) => {
|
|
219428
219428
|
if (error === null) {
|
|
219429
|
-
|
|
219429
|
+
resolve6(entries);
|
|
219430
219430
|
} else {
|
|
219431
219431
|
reject(error);
|
|
219432
219432
|
}
|
|
@@ -219436,10 +219436,10 @@ var require_async5 = __commonJS({
|
|
|
219436
219436
|
async static(patterns, options) {
|
|
219437
219437
|
const entries = [];
|
|
219438
219438
|
const stream = this._readerStream.static(patterns, options);
|
|
219439
|
-
return new Promise((
|
|
219439
|
+
return new Promise((resolve6, reject) => {
|
|
219440
219440
|
stream.once("error", reject);
|
|
219441
219441
|
stream.on("data", (entry) => entries.push(entry));
|
|
219442
|
-
stream.once("end", () =>
|
|
219442
|
+
stream.once("end", () => resolve6(entries));
|
|
219443
219443
|
});
|
|
219444
219444
|
}
|
|
219445
219445
|
};
|
|
@@ -252345,12 +252345,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
252345
252345
|
};
|
|
252346
252346
|
var NodeRuntimeFileSystem = class {
|
|
252347
252347
|
delete(path2) {
|
|
252348
|
-
return new Promise((
|
|
252348
|
+
return new Promise((resolve6, reject) => {
|
|
252349
252349
|
fs__namespace.rm(path2, { recursive: true }, (err) => {
|
|
252350
252350
|
if (err)
|
|
252351
252351
|
reject(err);
|
|
252352
252352
|
else
|
|
252353
|
-
|
|
252353
|
+
resolve6();
|
|
252354
252354
|
});
|
|
252355
252355
|
});
|
|
252356
252356
|
}
|
|
@@ -252369,12 +252369,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
252369
252369
|
}));
|
|
252370
252370
|
}
|
|
252371
252371
|
readFile(filePath, encoding = "utf-8") {
|
|
252372
|
-
return new Promise((
|
|
252372
|
+
return new Promise((resolve6, reject) => {
|
|
252373
252373
|
fs__namespace.readFile(filePath, encoding, (err, data) => {
|
|
252374
252374
|
if (err)
|
|
252375
252375
|
reject(err);
|
|
252376
252376
|
else
|
|
252377
|
-
|
|
252377
|
+
resolve6(data);
|
|
252378
252378
|
});
|
|
252379
252379
|
});
|
|
252380
252380
|
}
|
|
@@ -252382,12 +252382,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
252382
252382
|
return fs__namespace.readFileSync(filePath, encoding);
|
|
252383
252383
|
}
|
|
252384
252384
|
async writeFile(filePath, fileText) {
|
|
252385
|
-
await new Promise((
|
|
252385
|
+
await new Promise((resolve6, reject) => {
|
|
252386
252386
|
fs__namespace.writeFile(filePath, fileText, (err) => {
|
|
252387
252387
|
if (err)
|
|
252388
252388
|
reject(err);
|
|
252389
252389
|
else
|
|
252390
|
-
|
|
252390
|
+
resolve6();
|
|
252391
252391
|
});
|
|
252392
252392
|
});
|
|
252393
252393
|
}
|
|
@@ -252401,12 +252401,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
252401
252401
|
fs__namespace.mkdirSync(dirPath, { recursive: true });
|
|
252402
252402
|
}
|
|
252403
252403
|
move(srcPath, destPath) {
|
|
252404
|
-
return new Promise((
|
|
252404
|
+
return new Promise((resolve6, reject) => {
|
|
252405
252405
|
fs__namespace.rename(srcPath, destPath, (err) => {
|
|
252406
252406
|
if (err)
|
|
252407
252407
|
reject(err);
|
|
252408
252408
|
else
|
|
252409
|
-
|
|
252409
|
+
resolve6();
|
|
252410
252410
|
});
|
|
252411
252411
|
});
|
|
252412
252412
|
}
|
|
@@ -252414,12 +252414,12 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
252414
252414
|
fs__namespace.renameSync(srcPath, destPath);
|
|
252415
252415
|
}
|
|
252416
252416
|
copy(srcPath, destPath) {
|
|
252417
|
-
return new Promise((
|
|
252417
|
+
return new Promise((resolve6, reject) => {
|
|
252418
252418
|
fs__namespace.copyFile(srcPath, destPath, (err) => {
|
|
252419
252419
|
if (err)
|
|
252420
252420
|
reject(err);
|
|
252421
252421
|
else
|
|
252422
|
-
|
|
252422
|
+
resolve6();
|
|
252423
252423
|
});
|
|
252424
252424
|
});
|
|
252425
252425
|
}
|
|
@@ -252427,15 +252427,15 @@ type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "js
|
|
|
252427
252427
|
fs__namespace.copyFileSync(srcPath, destPath);
|
|
252428
252428
|
}
|
|
252429
252429
|
stat(path2) {
|
|
252430
|
-
return new Promise((
|
|
252430
|
+
return new Promise((resolve6, reject) => {
|
|
252431
252431
|
fs__namespace.stat(path2, (err, stat) => {
|
|
252432
252432
|
if (err) {
|
|
252433
252433
|
if (err.code === "ENOENT" || err.code === "ENOTDIR")
|
|
252434
|
-
|
|
252434
|
+
resolve6(void 0);
|
|
252435
252435
|
else
|
|
252436
252436
|
reject(err);
|
|
252437
252437
|
} else {
|
|
252438
|
-
|
|
252438
|
+
resolve6(stat);
|
|
252439
252439
|
}
|
|
252440
252440
|
});
|
|
252441
252441
|
});
|
|
@@ -276702,7 +276702,7 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
|
276702
276702
|
// src/cli/index.ts
|
|
276703
276703
|
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
276704
276704
|
var import_fs11 = require("fs");
|
|
276705
|
-
var
|
|
276705
|
+
var import_path7 = require("path");
|
|
276706
276706
|
var import_child_process2 = require("child_process");
|
|
276707
276707
|
|
|
276708
276708
|
// src/cli/migrate.ts
|
|
@@ -276775,6 +276775,7 @@ var ERROR_CODES = {
|
|
|
276775
276775
|
issuer_register_forbidden: wire("permission", 403, false, "Registering a trusted issuer requires a secret (sk_) API key."),
|
|
276776
276776
|
capability_invalid: wire("capability", 403, false, "The capability is unknown, revoked, or expired."),
|
|
276777
276777
|
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."),
|
|
276778
|
+
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."),
|
|
276778
276779
|
database_role_cannot_enforce_rls: wire("permission", 403, false, "The connected database role cannot enforce row-level security (superuser or BYPASSRLS)."),
|
|
276779
276780
|
database_role_unreadable: wire("permission", 403, false, "The connected database role could not be introspected."),
|
|
276780
276781
|
database_tables_unforced_rls: wire("permission", 403, false, "Synced tables in the connected database do not have FORCE ROW LEVEL SECURITY applied."),
|
|
@@ -277089,6 +277090,7 @@ var ErrorBodyShapeSchema = import_zod2.z.object({
|
|
|
277089
277090
|
// src/cli/migrate.ts
|
|
277090
277091
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
277091
277092
|
var import_fs4 = require("fs");
|
|
277093
|
+
var import_path3 = require("path");
|
|
277092
277094
|
|
|
277093
277095
|
// node_modules/postgres/src/index.js
|
|
277094
277096
|
init_cjs_shims();
|
|
@@ -277106,9 +277108,9 @@ var originError = /* @__PURE__ */ Symbol("OriginError");
|
|
|
277106
277108
|
var CLOSE = {};
|
|
277107
277109
|
var Query = class extends Promise {
|
|
277108
277110
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
277109
|
-
let
|
|
277111
|
+
let resolve6, reject;
|
|
277110
277112
|
super((a, b4) => {
|
|
277111
|
-
|
|
277113
|
+
resolve6 = a;
|
|
277112
277114
|
reject = b4;
|
|
277113
277115
|
});
|
|
277114
277116
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -277119,7 +277121,7 @@ var Query = class extends Promise {
|
|
|
277119
277121
|
this.options = options;
|
|
277120
277122
|
this.state = null;
|
|
277121
277123
|
this.statement = null;
|
|
277122
|
-
this.resolve = (x2) => (this.active = false,
|
|
277124
|
+
this.resolve = (x2) => (this.active = false, resolve6(x2));
|
|
277123
277125
|
this.reject = (x2) => (this.active = false, reject(x2));
|
|
277124
277126
|
this.active = false;
|
|
277125
277127
|
this.cancelled = null;
|
|
@@ -277167,12 +277169,12 @@ var Query = class extends Promise {
|
|
|
277167
277169
|
if (this.executed && !this.active)
|
|
277168
277170
|
return { done: true };
|
|
277169
277171
|
prev && prev();
|
|
277170
|
-
const promise = new Promise((
|
|
277172
|
+
const promise = new Promise((resolve6, reject) => {
|
|
277171
277173
|
this.cursorFn = (value) => {
|
|
277172
|
-
|
|
277174
|
+
resolve6({ value, done: false });
|
|
277173
277175
|
return new Promise((r2) => prev = r2);
|
|
277174
277176
|
};
|
|
277175
|
-
this.resolve = () => (this.active = false,
|
|
277177
|
+
this.resolve = () => (this.active = false, resolve6({ done: true }));
|
|
277176
277178
|
this.reject = (x2) => (this.active = false, reject(x2));
|
|
277177
277179
|
});
|
|
277178
277180
|
this.execute();
|
|
@@ -277804,12 +277806,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
277804
277806
|
x2.on("drain", drain);
|
|
277805
277807
|
return x2;
|
|
277806
277808
|
}
|
|
277807
|
-
async function cancel({ pid, secret },
|
|
277809
|
+
async function cancel({ pid, secret }, resolve6, reject) {
|
|
277808
277810
|
try {
|
|
277809
277811
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
277810
277812
|
await connect();
|
|
277811
277813
|
socket.once("error", reject);
|
|
277812
|
-
socket.once("close",
|
|
277814
|
+
socket.once("close", resolve6);
|
|
277813
277815
|
} catch (error2) {
|
|
277814
277816
|
reject(error2);
|
|
277815
277817
|
}
|
|
@@ -278758,7 +278760,7 @@ function parseEvent(x2) {
|
|
|
278758
278760
|
init_cjs_shims();
|
|
278759
278761
|
var import_stream2 = __toESM(require("stream"), 1);
|
|
278760
278762
|
function largeObject(sql, oid, mode2 = 131072 | 262144) {
|
|
278761
|
-
return new Promise(async (
|
|
278763
|
+
return new Promise(async (resolve6, reject) => {
|
|
278762
278764
|
await sql.begin(async (sql2) => {
|
|
278763
278765
|
let finish;
|
|
278764
278766
|
!oid && ([{ oid }] = await sql2`select lo_creat(-1) as oid`);
|
|
@@ -278784,7 +278786,7 @@ function largeObject(sql, oid, mode2 = 131072 | 262144) {
|
|
|
278784
278786
|
) seek
|
|
278785
278787
|
`
|
|
278786
278788
|
};
|
|
278787
|
-
|
|
278789
|
+
resolve6(lo);
|
|
278788
278790
|
return new Promise(async (r2) => finish = r2);
|
|
278789
278791
|
async function readable({
|
|
278790
278792
|
highWaterMark = 2048 * 8,
|
|
@@ -278958,8 +278960,8 @@ function Postgres(a, b4) {
|
|
|
278958
278960
|
}
|
|
278959
278961
|
async function reserve() {
|
|
278960
278962
|
const queue = queue_default();
|
|
278961
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
278962
|
-
const query = { reserve:
|
|
278963
|
+
const c = open.length ? open.shift() : await new Promise((resolve6, reject) => {
|
|
278964
|
+
const query = { reserve: resolve6, reject };
|
|
278963
278965
|
queries.push(query);
|
|
278964
278966
|
closed.length && connect(closed.shift(), query);
|
|
278965
278967
|
});
|
|
@@ -278996,9 +278998,9 @@ function Postgres(a, b4) {
|
|
|
278996
278998
|
let uncaughtError, result;
|
|
278997
278999
|
name && await sql2`savepoint ${sql2(name)}`;
|
|
278998
279000
|
try {
|
|
278999
|
-
result = await new Promise((
|
|
279001
|
+
result = await new Promise((resolve6, reject) => {
|
|
279000
279002
|
const x2 = fn2(sql2);
|
|
279001
|
-
Promise.resolve(Array.isArray(x2) ? Promise.all(x2) : x2).then(
|
|
279003
|
+
Promise.resolve(Array.isArray(x2) ? Promise.all(x2) : x2).then(resolve6, reject);
|
|
279002
279004
|
});
|
|
279003
279005
|
if (uncaughtError)
|
|
279004
279006
|
throw uncaughtError;
|
|
@@ -279055,8 +279057,8 @@ function Postgres(a, b4) {
|
|
|
279055
279057
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
279056
279058
|
}
|
|
279057
279059
|
function cancel(query) {
|
|
279058
|
-
return new Promise((
|
|
279059
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
279060
|
+
return new Promise((resolve6, reject) => {
|
|
279061
|
+
query.state ? query.active ? connection_default(options).cancel(query.state, resolve6, reject) : query.cancelled = { resolve: resolve6, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve6());
|
|
279060
279062
|
});
|
|
279061
279063
|
}
|
|
279062
279064
|
async function end({ timeout = null } = {}) {
|
|
@@ -279075,11 +279077,11 @@ function Postgres(a, b4) {
|
|
|
279075
279077
|
async function close() {
|
|
279076
279078
|
await Promise.all(connections.map((c) => c.end()));
|
|
279077
279079
|
}
|
|
279078
|
-
async function destroy(
|
|
279080
|
+
async function destroy(resolve6) {
|
|
279079
279081
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
279080
279082
|
while (queries.length)
|
|
279081
279083
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
279082
|
-
|
|
279084
|
+
resolve6();
|
|
279083
279085
|
}
|
|
279084
279086
|
function connect(c, query) {
|
|
279085
279087
|
move(c, connecting);
|
|
@@ -279224,6 +279226,81 @@ function osUsername() {
|
|
|
279224
279226
|
}
|
|
279225
279227
|
}
|
|
279226
279228
|
|
|
279229
|
+
// src/cli/dbRole.ts
|
|
279230
|
+
init_cjs_shims();
|
|
279231
|
+
var import_crypto2 = require("crypto");
|
|
279232
|
+
var DEFAULT_SCOPED_ROLE = "ablo_app";
|
|
279233
|
+
async function detectRoleSafety(sql) {
|
|
279234
|
+
const rows = await sql`SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user`;
|
|
279235
|
+
const row = rows[0];
|
|
279236
|
+
if (!row) return { role: "unknown", superuser: false, bypassRls: false, unsafe: false };
|
|
279237
|
+
return {
|
|
279238
|
+
role: row.rolname,
|
|
279239
|
+
superuser: row.rolsuper,
|
|
279240
|
+
bypassRls: row.rolbypassrls,
|
|
279241
|
+
unsafe: row.rolsuper || row.rolbypassrls
|
|
279242
|
+
};
|
|
279243
|
+
}
|
|
279244
|
+
function generateRolePassword() {
|
|
279245
|
+
return (0, import_crypto2.randomBytes)(24).toString("base64url");
|
|
279246
|
+
}
|
|
279247
|
+
function scramSha256Verifier(password, iterations = 4096) {
|
|
279248
|
+
const salt = (0, import_crypto2.randomBytes)(16);
|
|
279249
|
+
const saltedPassword = (0, import_crypto2.pbkdf2Sync)(password, salt, iterations, 32, "sha256");
|
|
279250
|
+
const clientKey = (0, import_crypto2.createHmac)("sha256", saltedPassword).update("Client Key").digest();
|
|
279251
|
+
const storedKey = (0, import_crypto2.createHash)("sha256").update(clientKey).digest();
|
|
279252
|
+
const serverKey = (0, import_crypto2.createHmac)("sha256", saltedPassword).update("Server Key").digest();
|
|
279253
|
+
return `SCRAM-SHA-256$${iterations}:${salt.toString("base64")}$${storedKey.toString("base64")}:${serverKey.toString("base64")}`;
|
|
279254
|
+
}
|
|
279255
|
+
function scopedRoleStatements(input) {
|
|
279256
|
+
const role = input.role ?? DEFAULT_SCOPED_ROLE;
|
|
279257
|
+
const q2 = (id) => `"${id.replace(/"/g, '""')}"`;
|
|
279258
|
+
const pw = (input.passwordMode ?? "scram-verifier") === "scram-verifier" ? scramSha256Verifier(input.password) : input.password.replace(/'/g, "''");
|
|
279259
|
+
return [
|
|
279260
|
+
`DO $$ BEGIN
|
|
279261
|
+
CREATE ROLE ${q2(role)} LOGIN PASSWORD '${pw}'
|
|
279262
|
+
NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;
|
|
279263
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
279264
|
+
-- Rerun: rotate ONLY the password. Re-asserting attributes here trips
|
|
279265
|
+
-- managed-Postgres permission walls (Neon: "permission denied to alter
|
|
279266
|
+
-- role" for attribute changes by non-superusers); the attributes were set
|
|
279267
|
+
-- at creation, and the server-side probe still audits the live role.
|
|
279268
|
+
ALTER ROLE ${q2(role)} WITH LOGIN PASSWORD '${pw}';
|
|
279269
|
+
END $$;`,
|
|
279270
|
+
`GRANT CREATE, CONNECT ON DATABASE ${q2(input.database)} TO ${q2(role)};`,
|
|
279271
|
+
`GRANT CREATE, USAGE ON SCHEMA public TO ${q2(role)};`
|
|
279272
|
+
];
|
|
279273
|
+
}
|
|
279274
|
+
function rewriteDatabaseUrl(ownerUrl, role, password) {
|
|
279275
|
+
const url = new URL(ownerUrl);
|
|
279276
|
+
url.username = role;
|
|
279277
|
+
url.password = password;
|
|
279278
|
+
return url.toString();
|
|
279279
|
+
}
|
|
279280
|
+
async function createScopedRole(ownerUrl, options) {
|
|
279281
|
+
const role = options?.role ?? DEFAULT_SCOPED_ROLE;
|
|
279282
|
+
const password = generateRolePassword();
|
|
279283
|
+
const database = new URL(ownerUrl).pathname.replace(/^\//, "") || "postgres";
|
|
279284
|
+
const sql = src_default(ownerUrl, { max: 1, prepare: false, onnotice: () => {
|
|
279285
|
+
} });
|
|
279286
|
+
try {
|
|
279287
|
+
try {
|
|
279288
|
+
for (const statement of scopedRoleStatements({ database, role, password })) {
|
|
279289
|
+
await sql.unsafe(statement);
|
|
279290
|
+
}
|
|
279291
|
+
} catch (err) {
|
|
279292
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
279293
|
+
if (!/plaintext password/i.test(message)) throw err;
|
|
279294
|
+
for (const statement of scopedRoleStatements({ database, role, password, passwordMode: "plaintext" })) {
|
|
279295
|
+
await sql.unsafe(statement);
|
|
279296
|
+
}
|
|
279297
|
+
}
|
|
279298
|
+
} finally {
|
|
279299
|
+
await sql.end({ timeout: 5 });
|
|
279300
|
+
}
|
|
279301
|
+
return { role, databaseUrl: rewriteDatabaseUrl(ownerUrl, role, password) };
|
|
279302
|
+
}
|
|
279303
|
+
|
|
279227
279304
|
// src/cli/migrate.ts
|
|
279228
279305
|
var import_schema2 = require("@abloatai/ablo/schema");
|
|
279229
279306
|
var import_source = require("@abloatai/ablo/source");
|
|
@@ -279500,9 +279577,9 @@ async function push(argv) {
|
|
|
279500
279577
|
}
|
|
279501
279578
|
console.error(import_picocolors2.default.dim(` Re-push with ${import_picocolors2.default.bold("--force")} to override, or use ${import_picocolors2.default.bold("--rename old:new")} if you renamed a model.`));
|
|
279502
279579
|
} else if (status2 === 403) {
|
|
279503
|
-
console.error(import_picocolors2.default.red(` Forbidden: ${body.reason ?? "key lacks schema:push scope"}
|
|
279580
|
+
console.error(import_picocolors2.default.red(` Forbidden: ${body.message ?? body.reason ?? "key lacks schema:push scope"}`));
|
|
279504
279581
|
} else {
|
|
279505
|
-
console.error(import_picocolors2.default.red(` Push failed (${status2}): ${body.reason ?? bodyText}`));
|
|
279582
|
+
console.error(import_picocolors2.default.red(` Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}`));
|
|
279506
279583
|
}
|
|
279507
279584
|
process.exit(1);
|
|
279508
279585
|
}
|
|
@@ -279595,7 +279672,7 @@ async function applyStatements(dbUrl, targetSchema, statements, concurrent = [])
|
|
|
279595
279672
|
if (pg.code === PG_LOCK_NOT_AVAILABLE && attempt < MAX_LOCK_ATTEMPTS) {
|
|
279596
279673
|
const backoffMs = Math.min(6e4, 10 * 2 ** attempt) + Math.floor(Math.random() * 50);
|
|
279597
279674
|
log.warn("schema change blocked by a lock; backing off and retrying", { targetSchema, attempt, backoffMs });
|
|
279598
|
-
await new Promise((
|
|
279675
|
+
await new Promise((resolve6) => setTimeout(resolve6, backoffMs));
|
|
279599
279676
|
continue;
|
|
279600
279677
|
}
|
|
279601
279678
|
throw err;
|
|
@@ -279653,18 +279730,96 @@ async function migrate(argv) {
|
|
|
279653
279730
|
console.error(import_picocolors3.default.red(" Set DATABASE_URL (or ABLO_DATABASE_URL) to apply, or use --dry-run to preview."));
|
|
279654
279731
|
process.exit(1);
|
|
279655
279732
|
}
|
|
279733
|
+
const effectiveUrl = await ensureScopedRole(dbUrl);
|
|
279656
279734
|
try {
|
|
279657
|
-
await applyStatements(
|
|
279735
|
+
await applyStatements(effectiveUrl, args.targetSchema, plan.statements, plan.concurrent);
|
|
279658
279736
|
console.log(` ${import_picocolors3.default.green("\u2713")} Migration complete`);
|
|
279659
279737
|
} catch {
|
|
279660
279738
|
process.exit(1);
|
|
279661
279739
|
}
|
|
279662
279740
|
}
|
|
279741
|
+
async function ensureScopedRole(dbUrl) {
|
|
279742
|
+
let safety;
|
|
279743
|
+
try {
|
|
279744
|
+
const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
|
|
279745
|
+
} });
|
|
279746
|
+
try {
|
|
279747
|
+
safety = await detectRoleSafety(sql);
|
|
279748
|
+
} finally {
|
|
279749
|
+
await sql.end({ timeout: 5 });
|
|
279750
|
+
}
|
|
279751
|
+
} catch {
|
|
279752
|
+
return dbUrl;
|
|
279753
|
+
}
|
|
279754
|
+
if (!safety.unsafe) return dbUrl;
|
|
279755
|
+
const why = safety.superuser ? "a superuser" : "BYPASSRLS";
|
|
279756
|
+
console.log(
|
|
279757
|
+
`
|
|
279758
|
+
${import_picocolors3.default.yellow("!")} DATABASE_URL connects as ${import_picocolors3.default.bold(safety.role)} \u2014 ${why}, so row-level security can't be enforced.
|
|
279759
|
+
Ablo's server will refuse this connection (${import_picocolors3.default.bold("database_role_cannot_enforce_rls")}).`
|
|
279760
|
+
);
|
|
279761
|
+
if (!process.stdout.isTTY) {
|
|
279762
|
+
console.log(
|
|
279763
|
+
import_picocolors3.default.dim(
|
|
279764
|
+
` Create a scoped role and update DATABASE_URL \u2014 run \`npx ablo migrate\` interactively
|
|
279765
|
+
to do it automatically, or see https://docs.abloatai.com/quickstart#scoped-role`
|
|
279766
|
+
)
|
|
279767
|
+
);
|
|
279768
|
+
return dbUrl;
|
|
279769
|
+
}
|
|
279770
|
+
const proceed = await ye({
|
|
279771
|
+
message: `Create a scoped role ${DEFAULT_SCOPED_ROLE} (NOSUPERUSER, NOBYPASSRLS) and update DATABASE_URL?`,
|
|
279772
|
+
initialValue: true
|
|
279773
|
+
});
|
|
279774
|
+
if (pD(proceed) || !proceed) {
|
|
279775
|
+
console.log(import_picocolors3.default.dim(" Skipped \u2014 see https://docs.abloatai.com/quickstart#scoped-role for the manual recipe."));
|
|
279776
|
+
return dbUrl;
|
|
279777
|
+
}
|
|
279778
|
+
const { role, databaseUrl } = await createScopedRole(dbUrl);
|
|
279779
|
+
const where = persistDatabaseUrl(databaseUrl);
|
|
279780
|
+
console.log(
|
|
279781
|
+
` ${import_picocolors3.default.green("\u2713")} Created role ${import_picocolors3.default.bold(role)} and updated ${import_picocolors3.default.bold("DATABASE_URL")} in ${import_picocolors3.default.bold(where)}.
|
|
279782
|
+
` + import_picocolors3.default.dim(` The owner credential never left this machine; the new password was written, not printed.`)
|
|
279783
|
+
);
|
|
279784
|
+
return databaseUrl;
|
|
279785
|
+
}
|
|
279786
|
+
function persistDatabaseUrl(databaseUrl, cwd = process.cwd()) {
|
|
279787
|
+
const line = `DATABASE_URL=${databaseUrl}`;
|
|
279788
|
+
for (const name of [".env.local", ".env"]) {
|
|
279789
|
+
const path = (0, import_path3.resolve)(cwd, name);
|
|
279790
|
+
if (!(0, import_fs4.existsSync)(path)) continue;
|
|
279791
|
+
const content = (0, import_fs4.readFileSync)(path, "utf8");
|
|
279792
|
+
if (/^DATABASE_URL=/m.test(content)) {
|
|
279793
|
+
(0, import_fs4.writeFileSync)(path, content.replace(/^DATABASE_URL=.*$/m, line));
|
|
279794
|
+
return name;
|
|
279795
|
+
}
|
|
279796
|
+
}
|
|
279797
|
+
const envLocal = (0, import_path3.resolve)(cwd, ".env.local");
|
|
279798
|
+
if ((0, import_fs4.existsSync)(envLocal)) {
|
|
279799
|
+
const content = (0, import_fs4.readFileSync)(envLocal, "utf8");
|
|
279800
|
+
(0, import_fs4.appendFileSync)(envLocal, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
|
|
279801
|
+
`);
|
|
279802
|
+
} else {
|
|
279803
|
+
(0, import_fs4.writeFileSync)(envLocal, `${line}
|
|
279804
|
+
`, { mode: 384 });
|
|
279805
|
+
}
|
|
279806
|
+
const gitignorePath = (0, import_path3.resolve)(cwd, ".gitignore");
|
|
279807
|
+
const gitignore = (0, import_fs4.existsSync)(gitignorePath) ? (0, import_fs4.readFileSync)(gitignorePath, "utf8") : "";
|
|
279808
|
+
if (!/^(\.env\.local|\.env\*|\.env\.\*|\.env.*)$/m.test(gitignore)) {
|
|
279809
|
+
(0, import_fs4.writeFileSync)(
|
|
279810
|
+
gitignorePath,
|
|
279811
|
+
`${gitignore.endsWith("\n") || gitignore.length === 0 ? gitignore : `${gitignore}
|
|
279812
|
+
`}.env.local
|
|
279813
|
+
`
|
|
279814
|
+
);
|
|
279815
|
+
}
|
|
279816
|
+
return ".env.local";
|
|
279817
|
+
}
|
|
279663
279818
|
|
|
279664
279819
|
// src/cli/generate.ts
|
|
279665
279820
|
init_cjs_shims();
|
|
279666
279821
|
var import_fs5 = require("fs");
|
|
279667
|
-
var
|
|
279822
|
+
var import_path4 = require("path");
|
|
279668
279823
|
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
279669
279824
|
var import_schema3 = require("@abloatai/ablo/schema");
|
|
279670
279825
|
var DEFAULT_SCHEMA_PATH3 = "ablo/schema.ts";
|
|
@@ -279709,8 +279864,8 @@ async function generate(argv) {
|
|
|
279709
279864
|
console.error(import_picocolors4.default.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
279710
279865
|
process.exit(1);
|
|
279711
279866
|
}
|
|
279712
|
-
const abs = (0,
|
|
279713
|
-
(0, import_fs5.mkdirSync)((0,
|
|
279867
|
+
const abs = (0, import_path4.resolve)(process.cwd(), args.out);
|
|
279868
|
+
(0, import_fs5.mkdirSync)((0, import_path4.dirname)(abs), { recursive: true });
|
|
279714
279869
|
(0, import_fs5.writeFileSync)(abs, source);
|
|
279715
279870
|
console.log(` ${import_picocolors4.default.green("\u2713")} Generated types \u2192 ${import_picocolors4.default.bold(args.out)}`);
|
|
279716
279871
|
}
|
|
@@ -279719,7 +279874,7 @@ async function generate(argv) {
|
|
|
279719
279874
|
init_cjs_shims();
|
|
279720
279875
|
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
279721
279876
|
var import_fs6 = require("fs");
|
|
279722
|
-
var
|
|
279877
|
+
var import_path5 = require("path");
|
|
279723
279878
|
var import_schema4 = require("@abloatai/ablo/schema");
|
|
279724
279879
|
|
|
279725
279880
|
// src/cli/theme.ts
|
|
@@ -279786,7 +279941,7 @@ function classifyKey(apiKey) {
|
|
|
279786
279941
|
return { ok: false, reason: `${import_picocolors5.default.bold("ABLO_API_KEY")} is not an Ablo key (expected ${import_picocolors5.default.bold("sk_test_\u2026")}).` };
|
|
279787
279942
|
}
|
|
279788
279943
|
function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
279789
|
-
const envPath = (0,
|
|
279944
|
+
const envPath = (0, import_path5.resolve)(cwd, ".env.local");
|
|
279790
279945
|
const line = `ABLO_API_KEY=${apiKey}`;
|
|
279791
279946
|
let action;
|
|
279792
279947
|
if (!(0, import_fs6.existsSync)(envPath)) {
|
|
@@ -279807,7 +279962,7 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
|
|
|
279807
279962
|
action = `Updated ${import_picocolors5.default.bold("ABLO_API_KEY")} in ${import_picocolors5.default.bold(".env.local")} ${import_picocolors5.default.dim(`(was ${match[1].slice(0, 12)}\u2026)`)}`;
|
|
279808
279963
|
}
|
|
279809
279964
|
}
|
|
279810
|
-
const gitignorePath = (0,
|
|
279965
|
+
const gitignorePath = (0, import_path5.resolve)(cwd, ".gitignore");
|
|
279811
279966
|
const gitignore = (0, import_fs6.existsSync)(gitignorePath) ? (0, import_fs6.readFileSync)(gitignorePath, "utf8") : "";
|
|
279812
279967
|
const ignored = /^(\.env\.local|\.env\*|\.env\.\*|\.env.*)$/m.test(gitignore);
|
|
279813
279968
|
let gitignoreNote = "";
|
|
@@ -279848,15 +280003,15 @@ async function runPush(schema, args) {
|
|
|
279848
280003
|
return { ok: false, message: lines.join("\n") };
|
|
279849
280004
|
}
|
|
279850
280005
|
if (status2 === 403) {
|
|
280006
|
+
const serverSays = body.message ?? body.reason;
|
|
280007
|
+
const hint = body.code === "database_role_cannot_enforce_rls" ? `Run ${import_picocolors5.default.bold("npx ablo migrate")} \u2014 it creates the scoped role for you (your DB credential never leaves this machine).` : `Schema authoring needs a ${import_picocolors5.default.bold("sandbox")} key with ${import_picocolors5.default.bold("schema:push")} \u2014 manage keys at ${import_picocolors5.default.cyan("https://abloatai.com")}.`;
|
|
279851
280008
|
return {
|
|
279852
280009
|
ok: false,
|
|
279853
|
-
message:
|
|
279854
|
-
` + import_picocolors5.default.dim(
|
|
279855
|
-
`Use a ${import_picocolors5.default.bold("sandbox")} key, or one with ${import_picocolors5.default.bold("schema authoring")} enabled at ${import_picocolors5.default.cyan("https://abloatai.com")}.`
|
|
279856
|
-
)
|
|
280010
|
+
message: `${serverSays ?? "This key can't author schema (missing schema:push scope)."}
|
|
280011
|
+
` + import_picocolors5.default.dim(hint)
|
|
279857
280012
|
};
|
|
279858
280013
|
}
|
|
279859
|
-
return { ok: false, message: `Push failed (${status2}): ${body.reason ?? bodyText}` };
|
|
280014
|
+
return { ok: false, message: `Push failed (${status2}): ${body.message ?? body.reason ?? bodyText}` };
|
|
279860
280015
|
}
|
|
279861
280016
|
async function dev(argv) {
|
|
279862
280017
|
let args;
|
|
@@ -279898,7 +280053,7 @@ async function dev(argv) {
|
|
|
279898
280053
|
}
|
|
279899
280054
|
console.log(` Your app is wired for the sandbox.`);
|
|
279900
280055
|
if (!args.watch) return;
|
|
279901
|
-
const abs = (0,
|
|
280056
|
+
const abs = (0, import_path5.resolve)(process.cwd(), args.schemaPath);
|
|
279902
280057
|
console.log(` ${import_picocolors5.default.dim(`watching ${args.schemaPath} \u2026 (Ctrl-C to stop)`)}
|
|
279903
280058
|
`);
|
|
279904
280059
|
let timer2 = null;
|
|
@@ -281297,7 +281452,7 @@ async function prismaPull(argv) {
|
|
|
281297
281452
|
init_cjs_shims();
|
|
281298
281453
|
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
281299
281454
|
var import_fs10 = require("fs");
|
|
281300
|
-
var
|
|
281455
|
+
var import_path6 = require("path");
|
|
281301
281456
|
var DEFAULT_OUT4 = "ablo/schema.ts";
|
|
281302
281457
|
var DEFAULT_IMPORT3 = "@abloatai/ablo/schema";
|
|
281303
281458
|
var BASE_FIELD_NAMES2 = /* @__PURE__ */ new Set(["id", "organizationId", "createdBy", "createdAt", "updatedAt"]);
|
|
@@ -281423,7 +281578,7 @@ function parseDrizzlePullArgs(argv) {
|
|
|
281423
281578
|
async function loadModule(path) {
|
|
281424
281579
|
const { createJiti } = await import("jiti");
|
|
281425
281580
|
const jiti = createJiti(process.cwd());
|
|
281426
|
-
const mod = await jiti.import((0,
|
|
281581
|
+
const mod = await jiti.import((0, import_path6.resolve)(path));
|
|
281427
281582
|
return mod;
|
|
281428
281583
|
}
|
|
281429
281584
|
async function drizzlePull(argv) {
|
|
@@ -281731,13 +281886,13 @@ async function init(args = []) {
|
|
|
281731
281886
|
}
|
|
281732
281887
|
}
|
|
281733
281888
|
}
|
|
281734
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281889
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)(abloDir, "schema.ts"), schemaSource);
|
|
281735
281890
|
created.push(`${abloDir}/schema.ts${schemaNote}`);
|
|
281736
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281891
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)(abloDir, "index.ts"), generateSyncConfig(auth, storage));
|
|
281737
281892
|
created.push(`${abloDir}/index.ts`);
|
|
281738
281893
|
const orm = detectOrm(opts.orm);
|
|
281739
281894
|
if (storage === "endpoint") {
|
|
281740
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281895
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)(abloDir, "data-source.ts"), generateDataSource(orm));
|
|
281741
281896
|
created.push(`${abloDir}/data-source.ts${orm === "drizzle" ? " (Drizzle)" : " (Prisma)"}`);
|
|
281742
281897
|
}
|
|
281743
281898
|
const envFile = framework === "nextjs" ? ".env.local" : ".env";
|
|
@@ -281754,25 +281909,25 @@ async function init(args = []) {
|
|
|
281754
281909
|
}
|
|
281755
281910
|
}
|
|
281756
281911
|
if (agent) {
|
|
281757
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281912
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
|
|
281758
281913
|
created.push(`${abloDir}/agent.ts`);
|
|
281759
281914
|
}
|
|
281760
281915
|
if (framework === "nextjs") {
|
|
281761
281916
|
if (storage === "endpoint") {
|
|
281762
|
-
const webhookDir = (0,
|
|
281917
|
+
const webhookDir = (0, import_path7.join)("app", "api", "ablo", "webhooks");
|
|
281763
281918
|
(0, import_fs11.mkdirSync)(webhookDir, { recursive: true });
|
|
281764
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281919
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)(webhookDir, "route.ts"), generateWebhookRoute(orm));
|
|
281765
281920
|
created.push(`${webhookDir}/route.ts${orm === "prisma" ? " (Prisma mirror)" : " (add your database write)"}`);
|
|
281766
281921
|
}
|
|
281767
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281922
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)("app", "providers.tsx"), generateProviders());
|
|
281768
281923
|
created.push(`app/providers.tsx ${import_picocolors16.default.dim("(wrap app/layout.tsx in <Providers>)")}`);
|
|
281769
|
-
const sessionDir = (0,
|
|
281924
|
+
const sessionDir = (0, import_path7.join)("app", "api", "ablo-session");
|
|
281770
281925
|
(0, import_fs11.mkdirSync)(sessionDir, { recursive: true });
|
|
281771
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281926
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
|
|
281772
281927
|
created.push(`app/api/ablo-session/route.ts ${import_picocolors16.default.dim("(wire your auth)")}`);
|
|
281773
281928
|
}
|
|
281774
281929
|
if (framework !== "vanilla") {
|
|
281775
|
-
(0, import_fs11.writeFileSync)((0,
|
|
281930
|
+
(0, import_fs11.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
|
|
281776
281931
|
created.push(`${abloDir}/TaskList.tsx`);
|
|
281777
281932
|
}
|
|
281778
281933
|
Me(created.map((f) => `${import_picocolors16.default.green("\u2713")} ${f}`).join("\n"), "Created");
|
package/dist/errorCodes.d.ts
CHANGED
|
@@ -139,6 +139,7 @@ export declare const ERROR_CODES: {
|
|
|
139
139
|
readonly issuer_register_forbidden: ErrorCodeSpec;
|
|
140
140
|
readonly capability_invalid: ErrorCodeSpec;
|
|
141
141
|
readonly test_database_not_registered: ErrorCodeSpec;
|
|
142
|
+
readonly tenant_routing_failed: ErrorCodeSpec;
|
|
142
143
|
readonly database_role_cannot_enforce_rls: ErrorCodeSpec;
|
|
143
144
|
readonly database_role_unreadable: ErrorCodeSpec;
|
|
144
145
|
readonly database_tables_unforced_rls: ErrorCodeSpec;
|
package/dist/errorCodes.js
CHANGED
|
@@ -130,6 +130,7 @@ export const ERROR_CODES = {
|
|
|
130
130
|
issuer_register_forbidden: wire('permission', 403, false, 'Registering a trusted issuer requires a secret (sk_) API key.'),
|
|
131
131
|
capability_invalid: wire('capability', 403, false, 'The capability is unknown, revoked, or expired.'),
|
|
132
132
|
test_database_not_registered: wire('permission', 403, false, 'Test mode requires a registered dev database for this org — run `npx ablo init`, or construct the client with `databaseUrl` using your test key.'),
|
|
133
|
+
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 — retry, and check the datasource status if it persists."),
|
|
133
134
|
database_role_cannot_enforce_rls: wire('permission', 403, false, 'The connected database role cannot enforce row-level security (superuser or BYPASSRLS).'),
|
|
134
135
|
database_role_unreadable: wire('permission', 403, false, 'The connected database role could not be introspected.'),
|
|
135
136
|
database_tables_unforced_rls: wire('permission', 403, false, 'Synced tables in the connected database do not have FORCE ROW LEVEL SECURITY applied.'),
|
package/docs/quickstart.md
CHANGED
|
@@ -74,7 +74,26 @@ export const ablo = Ablo({
|
|
|
74
74
|
|
|
75
75
|
Use a dedicated **non-superuser role** for the connection — Ablo enforces
|
|
76
76
|
tenant isolation with row-level security, so the server rejects superuser or
|
|
77
|
-
`BYPASSRLS` roles outright.
|
|
77
|
+
`BYPASSRLS` roles outright (`database_role_cannot_enforce_rls`).
|
|
78
|
+
|
|
79
|
+
> **Neon / Supabase note:** the connection string those dashboards hand you
|
|
80
|
+
> uses the database OWNER role (e.g. `neondb_owner`), which is `BYPASSRLS` —
|
|
81
|
+
> Ablo will reject it. You don't have to fix that by hand: `npx ablo migrate`
|
|
82
|
+
> detects the unsafe role and offers to create the scoped one for you — from
|
|
83
|
+
> your machine, so the owner credential never reaches Ablo. It writes the new
|
|
84
|
+
> `DATABASE_URL` into your env file (the generated password is never printed).
|
|
85
|
+
>
|
|
86
|
+
> Prefer to do it manually? The equivalent SQL:
|
|
87
|
+
>
|
|
88
|
+
> ```sql
|
|
89
|
+
> CREATE ROLE ablo_app LOGIN PASSWORD '<strong password>'
|
|
90
|
+
> NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;
|
|
91
|
+
> GRANT CREATE, CONNECT ON DATABASE <your_db> TO ablo_app;
|
|
92
|
+
> GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;
|
|
93
|
+
> ```
|
|
94
|
+
>
|
|
95
|
+
> Then swap the user/password in the dashboard's string:
|
|
96
|
+
> `postgres://ablo_app:<password>@<same-host>/<same-db>?sslmode=require`.
|
|
78
97
|
|
|
79
98
|
Don't want a connection string to leave your infrastructure? Keep
|
|
80
99
|
`DATABASE_URL` in your app only and expose one signed **Data Source endpoint**
|
package/llms-full.txt
CHANGED
|
@@ -142,6 +142,15 @@ This applies to any API-backed app: Python, Rails, Go, or Node. The backend keep
|
|
|
142
142
|
its service layer and DB credentials. Ablo gets a Data Source endpoint and uses
|
|
143
143
|
`ABLO_API_KEY`, not a database URL.
|
|
144
144
|
|
|
145
|
+
When the user DOES use the connection-string path (`Ablo({ databaseUrl })` /
|
|
146
|
+
`DATABASE_URL` at init): the role must be NON-superuser and NON-BYPASSRLS —
|
|
147
|
+
Ablo enforces row-level security and rejects owner roles with
|
|
148
|
+
`database_role_cannot_enforce_rls`. Neon's and Supabase's default dashboard
|
|
149
|
+
strings use the database OWNER (e.g. `neondb_owner`) and ARE rejected. EASIEST: have the user run `npx ablo migrate` — it detects the unsafe role and creates the scoped one automatically from their machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first (`CREATE ROLE ablo_app LOGIN PASSWORD '...'
|
|
150
|
+
NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app;
|
|
151
|
+
GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;`), then swap user/password
|
|
152
|
+
into the same host/db string.
|
|
153
|
+
|
|
145
154
|
## Client Behavior
|
|
146
155
|
|
|
147
156
|
The options that matter: `schema` and `apiKey`. Everything else
|
package/llms.txt
CHANGED
|
@@ -120,6 +120,8 @@ Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_AP
|
|
|
120
120
|
|
|
121
121
|
Every schema model is backed by a database, and the default is YOUR OWN. Keep your rows in your Postgres and expose a Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle is the default; Prisma and Kysely are also supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass (no hand-written `commit`/`events`). Your `DATABASE_URL` lives in your app, never in `Ablo(...)`; run `npx ablo migrate` to provision the synced-model tables in your DB.
|
|
122
122
|
|
|
123
|
+
GOTCHA the user WILL hit: `DATABASE_URL` must use a NON-superuser, NON-BYPASSRLS role (Ablo enforces row-level security; owner roles are rejected with `database_role_cannot_enforce_rls`). Neon's and Supabase's default dashboard connection strings use the database OWNER (e.g. `neondb_owner`) and are rejected. EASIEST: `npx ablo migrate` detects the unsafe role and creates the scoped one automatically from the user's machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first: `CREATE ROLE ablo_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app; GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;` — then swap user/password into the same host/db string.
|
|
124
|
+
|
|
123
125
|
```ts
|
|
124
126
|
// app/api/ablo/source/route.ts
|
|
125
127
|
import { dataSourceNext } from '@abloatai/ablo/source/next';
|