@objectstack/runtime 9.9.1 → 9.10.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/dist/index.cjs +130 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -24
- package/dist/index.d.ts +0 -24
- package/dist/index.js +100 -70
- package/dist/index.js.map +1 -1
- package/package.json +18 -18
package/dist/index.d.cts
CHANGED
|
@@ -230,30 +230,6 @@ declare class AppPlugin implements Plugin {
|
|
|
230
230
|
init: (ctx: PluginContext) => Promise<void>;
|
|
231
231
|
start: (ctx: PluginContext) => Promise<void>;
|
|
232
232
|
stop: (ctx: PluginContext) => Promise<void>;
|
|
233
|
-
/**
|
|
234
|
-
* Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
|
|
235
|
-
*
|
|
236
|
-
* On a fresh boot there are zero users until the first human sign-up
|
|
237
|
-
* (which the SeedLoader runs *before*), so identity-derived seeds like
|
|
238
|
-
* `owner_id: cel`os.user.id`` had nothing to resolve against and were
|
|
239
|
-
* dropped silently. To make seeds deterministic and self-sufficient we
|
|
240
|
-
* upsert a single non-loginable **system user** (`usr_system`) and bind
|
|
241
|
-
* it as `os.user`.
|
|
242
|
-
*
|
|
243
|
-
* Why a dedicated system user rather than the login admin:
|
|
244
|
-
* - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
|
|
245
|
-
* password lives in `sys_account`, so a *loginable* admin can only be
|
|
246
|
-
* minted through better-auth (the CLI does this via HTTP sign-up after
|
|
247
|
-
* boot). A raw insert here would bypass those invariants.
|
|
248
|
-
* - `usr_system` is an owner identity only (no credential row), analogous
|
|
249
|
-
* to Salesforce's "Automated Process" user. The human admin is created
|
|
250
|
-
* independently and need not be the seed owner.
|
|
251
|
-
*
|
|
252
|
-
* Idempotent: matches by the stable id, inserts once, reuses thereafter.
|
|
253
|
-
* Failures are non-fatal (logged) — records that actually need `os.user`
|
|
254
|
-
* then fail loudly in the loader with an actionable message.
|
|
255
|
-
*/
|
|
256
|
-
private ensureSeedIdentity;
|
|
257
233
|
/**
|
|
258
234
|
* Emit a kernel hook so the control-plane `AppCatalogService` can
|
|
259
235
|
* upsert / delete the corresponding `sys_app` row. Silently no-ops
|
package/dist/index.d.ts
CHANGED
|
@@ -230,30 +230,6 @@ declare class AppPlugin implements Plugin {
|
|
|
230
230
|
init: (ctx: PluginContext) => Promise<void>;
|
|
231
231
|
start: (ctx: PluginContext) => Promise<void>;
|
|
232
232
|
stop: (ctx: PluginContext) => Promise<void>;
|
|
233
|
-
/**
|
|
234
|
-
* Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
|
|
235
|
-
*
|
|
236
|
-
* On a fresh boot there are zero users until the first human sign-up
|
|
237
|
-
* (which the SeedLoader runs *before*), so identity-derived seeds like
|
|
238
|
-
* `owner_id: cel`os.user.id`` had nothing to resolve against and were
|
|
239
|
-
* dropped silently. To make seeds deterministic and self-sufficient we
|
|
240
|
-
* upsert a single non-loginable **system user** (`usr_system`) and bind
|
|
241
|
-
* it as `os.user`.
|
|
242
|
-
*
|
|
243
|
-
* Why a dedicated system user rather than the login admin:
|
|
244
|
-
* - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
|
|
245
|
-
* password lives in `sys_account`, so a *loginable* admin can only be
|
|
246
|
-
* minted through better-auth (the CLI does this via HTTP sign-up after
|
|
247
|
-
* boot). A raw insert here would bypass those invariants.
|
|
248
|
-
* - `usr_system` is an owner identity only (no credential row), analogous
|
|
249
|
-
* to Salesforce's "Automated Process" user. The human admin is created
|
|
250
|
-
* independently and need not be the seed owner.
|
|
251
|
-
*
|
|
252
|
-
* Idempotent: matches by the stable id, inserts once, reuses thereafter.
|
|
253
|
-
* Failures are non-fatal (logged) — records that actually need `os.user`
|
|
254
|
-
* then fail loudly in the loader with an actionable message.
|
|
255
|
-
*/
|
|
256
|
-
private ensureSeedIdentity;
|
|
257
233
|
/**
|
|
258
234
|
* Emit a kernel hook so the control-plane `AppCatalogService` can
|
|
259
235
|
* upsert / delete the corresponding `sys_app` row. Silently no-ops
|
package/dist/index.js
CHANGED
|
@@ -205,7 +205,7 @@ var init_package_state_store = __esm({
|
|
|
205
205
|
import {
|
|
206
206
|
newAsyncContext
|
|
207
207
|
} from "quickjs-emscripten";
|
|
208
|
-
function installApiMethod(vm, parent, method, objectName, ctx, caps, required, origin) {
|
|
208
|
+
function installApiMethod(vm, parent, method, objectName, ctx, caps, required, origin, txState) {
|
|
209
209
|
const fn = vm.newFunction(method, (...argHandles) => {
|
|
210
210
|
if (!caps.has(required)) {
|
|
211
211
|
throw new SandboxError(
|
|
@@ -220,7 +220,8 @@ function installApiMethod(vm, parent, method, objectName, ctx, caps, required, o
|
|
|
220
220
|
const deferred = vm.newPromise();
|
|
221
221
|
void (async () => {
|
|
222
222
|
try {
|
|
223
|
-
const
|
|
223
|
+
const source = txState.api ?? apiAny;
|
|
224
|
+
const proxy = source.object(objectName);
|
|
224
225
|
const m = proxy[method];
|
|
225
226
|
if (typeof m !== "function") {
|
|
226
227
|
throw new SandboxError(`ctx.api.object('${objectName}').${method} not implemented`);
|
|
@@ -365,8 +366,9 @@ var init_quickjs_runner = __esm({
|
|
|
365
366
|
const start = Date.now();
|
|
366
367
|
const deadline = start + args.timeoutMs;
|
|
367
368
|
runtime.setInterruptHandler(() => Date.now() > deadline);
|
|
369
|
+
const txState = { api: null, handle: null, open: false };
|
|
368
370
|
try {
|
|
369
|
-
this.installCtx(vm, args.ctx, new Set(args.capabilities), args.origin);
|
|
371
|
+
this.installCtx(vm, args.ctx, new Set(args.capabilities), args.origin, txState);
|
|
370
372
|
if (args.isExpression) {
|
|
371
373
|
const wrapped2 = `globalThis.__result = JSON.stringify((function(){ return (${args.source}); })());`;
|
|
372
374
|
const result = vm.evalCode(wrapped2);
|
|
@@ -429,6 +431,16 @@ var init_quickjs_runner = __esm({
|
|
|
429
431
|
pumps++;
|
|
430
432
|
}
|
|
431
433
|
} finally {
|
|
434
|
+
if (txState.open && txState.handle != null) {
|
|
435
|
+
const apiTx = args.ctx.api;
|
|
436
|
+
const rollback = apiTx?.rollbackTransaction;
|
|
437
|
+
if (typeof rollback === "function") {
|
|
438
|
+
try {
|
|
439
|
+
await rollback.call(apiTx, txState.handle);
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
432
444
|
vm.dispose();
|
|
433
445
|
}
|
|
434
446
|
}
|
|
@@ -441,7 +453,7 @@ var init_quickjs_runner = __esm({
|
|
|
441
453
|
* {@link installApiMethod}) so they may return Promises (real ObjectQL
|
|
442
454
|
* `find/count/insert/...` are async) without asyncify's single-unwind limit.
|
|
443
455
|
*/
|
|
444
|
-
installCtx(vm, ctx, caps, origin) {
|
|
456
|
+
installCtx(vm, ctx, caps, origin, txState) {
|
|
445
457
|
setGlobalJson(vm, "__input", ctx.input);
|
|
446
458
|
setGlobalJson(vm, "__previous", ctx.previous);
|
|
447
459
|
const ctxObj = vm.newObject();
|
|
@@ -476,12 +488,70 @@ var init_quickjs_runner = __esm({
|
|
|
476
488
|
const wrap = vm.newObject();
|
|
477
489
|
const READ = ["find", "findOne", "count", "aggregate"];
|
|
478
490
|
const WRITE = ["insert", "update", "delete", "updateMany", "deleteMany", "upsert"];
|
|
479
|
-
for (const m of READ) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.read", origin);
|
|
480
|
-
for (const m of WRITE) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.write", origin);
|
|
491
|
+
for (const m of READ) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.read", origin, txState);
|
|
492
|
+
for (const m of WRITE) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.write", origin, txState);
|
|
481
493
|
return wrap;
|
|
482
494
|
});
|
|
483
495
|
vm.setProp(apiObj, "object", objectFn);
|
|
484
496
|
objectFn.dispose();
|
|
497
|
+
const apiTx = ctx.api;
|
|
498
|
+
const installTxLeaf = (name, run) => {
|
|
499
|
+
const fn = vm.newFunction(name, () => {
|
|
500
|
+
if (!caps.has("api.transaction")) {
|
|
501
|
+
throw new SandboxError(
|
|
502
|
+
`capability 'api.transaction' not granted to ${origin.kind} '${origin.name}' (called ctx.api.transaction)`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const deferred = vm.newPromise();
|
|
506
|
+
void (async () => {
|
|
507
|
+
try {
|
|
508
|
+
await run();
|
|
509
|
+
if (!vm.alive) return;
|
|
510
|
+
deferred.resolve(vm.undefined);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
if (!vm.alive) return;
|
|
513
|
+
const errH = err instanceof Error ? vm.newError({ name: err.name || "Error", message: err.message }) : vm.newError({ name: "Error", message: String(err) });
|
|
514
|
+
deferred.reject(errH);
|
|
515
|
+
errH.dispose();
|
|
516
|
+
}
|
|
517
|
+
})();
|
|
518
|
+
return deferred.handle;
|
|
519
|
+
});
|
|
520
|
+
vm.setProp(apiObj, name, fn);
|
|
521
|
+
fn.dispose();
|
|
522
|
+
};
|
|
523
|
+
installTxLeaf("__txBegin", async () => {
|
|
524
|
+
if (txState.open) throw new SandboxError("nested ctx.api.transaction is not supported");
|
|
525
|
+
const begin = apiTx?.beginTransaction;
|
|
526
|
+
if (typeof begin === "function") {
|
|
527
|
+
const r = await begin.call(apiTx) ?? null;
|
|
528
|
+
if (r) {
|
|
529
|
+
txState.api = r.ctx;
|
|
530
|
+
txState.handle = r.handle;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
txState.open = true;
|
|
534
|
+
});
|
|
535
|
+
installTxLeaf("__txCommit", async () => {
|
|
536
|
+
const { handle, open } = txState;
|
|
537
|
+
txState.api = null;
|
|
538
|
+
txState.handle = null;
|
|
539
|
+
txState.open = false;
|
|
540
|
+
const commit = apiTx?.commitTransaction;
|
|
541
|
+
if (open && handle != null && typeof commit === "function") {
|
|
542
|
+
await commit.call(apiTx, handle);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
installTxLeaf("__txRollback", async () => {
|
|
546
|
+
const { handle, open } = txState;
|
|
547
|
+
txState.api = null;
|
|
548
|
+
txState.handle = null;
|
|
549
|
+
txState.open = false;
|
|
550
|
+
const rollback = apiTx?.rollbackTransaction;
|
|
551
|
+
if (open && handle != null && typeof rollback === "function") {
|
|
552
|
+
await rollback.call(apiTx, handle);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
485
555
|
vm.setProp(ctxObj, "api", apiObj);
|
|
486
556
|
apiObj.dispose();
|
|
487
557
|
const logObj = vm.newObject();
|
|
@@ -514,6 +584,25 @@ var init_quickjs_runner = __esm({
|
|
|
514
584
|
cryptoObj.dispose();
|
|
515
585
|
vm.setProp(vm.global, "__ctx", ctxObj);
|
|
516
586
|
ctxObj.dispose();
|
|
587
|
+
const sugar = vm.evalCode(
|
|
588
|
+
`__ctx.api.transaction = async function (fn) {
|
|
589
|
+
await __ctx.api.__txBegin();
|
|
590
|
+
try {
|
|
591
|
+
var r = await fn();
|
|
592
|
+
await __ctx.api.__txCommit();
|
|
593
|
+
return r;
|
|
594
|
+
} catch (e) {
|
|
595
|
+
await __ctx.api.__txRollback();
|
|
596
|
+
throw e;
|
|
597
|
+
}
|
|
598
|
+
};`
|
|
599
|
+
);
|
|
600
|
+
if (sugar.error) {
|
|
601
|
+
const msg = vm.dump(sugar.error);
|
|
602
|
+
sugar.error.dispose();
|
|
603
|
+
throw new SandboxError(`failed to install ctx.api.transaction: ${formatErr(msg)}`);
|
|
604
|
+
}
|
|
605
|
+
sugar.value.dispose();
|
|
517
606
|
}
|
|
518
607
|
};
|
|
519
608
|
SandboxError = class extends Error {
|
|
@@ -714,7 +803,6 @@ __export(app_plugin_exports, {
|
|
|
714
803
|
collectBundleHooks: () => collectBundleHooks
|
|
715
804
|
});
|
|
716
805
|
import { readEnvWithDeprecation } from "@objectstack/types";
|
|
717
|
-
import { SystemUserId } from "@objectstack/spec/system";
|
|
718
806
|
function collectBundleHooks(bundle) {
|
|
719
807
|
const out = [];
|
|
720
808
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1045,7 +1133,7 @@ var init_app_plugin = __esm({
|
|
|
1045
1133
|
...d,
|
|
1046
1134
|
object: d.object
|
|
1047
1135
|
}));
|
|
1048
|
-
const seedIdentity =
|
|
1136
|
+
const seedIdentity = void 0;
|
|
1049
1137
|
try {
|
|
1050
1138
|
const kernel = ctx.kernel;
|
|
1051
1139
|
const existing = (() => {
|
|
@@ -1088,10 +1176,10 @@ var init_app_plugin = __esm({
|
|
|
1088
1176
|
defaultMode: "upsert",
|
|
1089
1177
|
multiPass: true,
|
|
1090
1178
|
organizationId,
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1179
|
+
// `os.org` is derived from organizationId inside
|
|
1180
|
+
// the loader. `seedIdentity` (os.user) is undefined
|
|
1181
|
+
// unless a seed embeds `cel`os.user.id`` — see the
|
|
1182
|
+
// lazy guard where it is resolved.
|
|
1095
1183
|
identity: seedIdentity
|
|
1096
1184
|
}
|
|
1097
1185
|
});
|
|
@@ -1251,64 +1339,6 @@ var init_app_plugin = __esm({
|
|
|
1251
1339
|
this.name = `plugin.app.${appId}`;
|
|
1252
1340
|
this.version = sys?.version;
|
|
1253
1341
|
}
|
|
1254
|
-
/**
|
|
1255
|
-
* Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
|
|
1256
|
-
*
|
|
1257
|
-
* On a fresh boot there are zero users until the first human sign-up
|
|
1258
|
-
* (which the SeedLoader runs *before*), so identity-derived seeds like
|
|
1259
|
-
* `owner_id: cel`os.user.id`` had nothing to resolve against and were
|
|
1260
|
-
* dropped silently. To make seeds deterministic and self-sufficient we
|
|
1261
|
-
* upsert a single non-loginable **system user** (`usr_system`) and bind
|
|
1262
|
-
* it as `os.user`.
|
|
1263
|
-
*
|
|
1264
|
-
* Why a dedicated system user rather than the login admin:
|
|
1265
|
-
* - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
|
|
1266
|
-
* password lives in `sys_account`, so a *loginable* admin can only be
|
|
1267
|
-
* minted through better-auth (the CLI does this via HTTP sign-up after
|
|
1268
|
-
* boot). A raw insert here would bypass those invariants.
|
|
1269
|
-
* - `usr_system` is an owner identity only (no credential row), analogous
|
|
1270
|
-
* to Salesforce's "Automated Process" user. The human admin is created
|
|
1271
|
-
* independently and need not be the seed owner.
|
|
1272
|
-
*
|
|
1273
|
-
* Idempotent: matches by the stable id, inserts once, reuses thereafter.
|
|
1274
|
-
* Failures are non-fatal (logged) — records that actually need `os.user`
|
|
1275
|
-
* then fail loudly in the loader with an actionable message.
|
|
1276
|
-
*/
|
|
1277
|
-
async ensureSeedIdentity(ql, logger) {
|
|
1278
|
-
const SYSTEM_USER_ID = SystemUserId.SYSTEM;
|
|
1279
|
-
const SYSTEM_USER_EMAIL = "system@objectstack.local";
|
|
1280
|
-
const identity = { user: { id: SYSTEM_USER_ID, role: "system", email: SYSTEM_USER_EMAIL } };
|
|
1281
|
-
const opts = { context: { isSystem: true } };
|
|
1282
|
-
try {
|
|
1283
|
-
const existing = await ql.find(
|
|
1284
|
-
"sys_user",
|
|
1285
|
-
{ where: { id: SYSTEM_USER_ID }, limit: 1 },
|
|
1286
|
-
opts
|
|
1287
|
-
);
|
|
1288
|
-
if (Array.isArray(existing) && existing.length > 0) {
|
|
1289
|
-
return identity;
|
|
1290
|
-
}
|
|
1291
|
-
await ql.insert(
|
|
1292
|
-
"sys_user",
|
|
1293
|
-
{
|
|
1294
|
-
id: SYSTEM_USER_ID,
|
|
1295
|
-
name: "System",
|
|
1296
|
-
email: SYSTEM_USER_EMAIL,
|
|
1297
|
-
email_verified: true,
|
|
1298
|
-
role: "system"
|
|
1299
|
-
},
|
|
1300
|
-
opts
|
|
1301
|
-
);
|
|
1302
|
-
logger.info(
|
|
1303
|
-
`[Seeder] Provisioned deterministic system user (${SYSTEM_USER_ID}) as seed owner \u2014 binds os.user for identity-derived seed values`
|
|
1304
|
-
);
|
|
1305
|
-
} catch (err) {
|
|
1306
|
-
logger.warn("[Seeder] Failed to ensure system seed user; os.user-dependent seeds may be dropped", {
|
|
1307
|
-
error: err?.message ?? String(err)
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
return identity;
|
|
1311
|
-
}
|
|
1312
1342
|
/**
|
|
1313
1343
|
* Emit a kernel hook so the control-plane `AppCatalogService` can
|
|
1314
1344
|
* upsert / delete the corresponding `sys_app` row. Silently no-ops
|