@objectstack/runtime 9.9.0 → 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 CHANGED
@@ -227,7 +227,7 @@ var init_package_state_store = __esm({
227
227
  });
228
228
 
229
229
  // src/sandbox/quickjs-runner.ts
230
- function installApiMethod(vm, parent, method, objectName, ctx, caps, required, origin) {
230
+ function installApiMethod(vm, parent, method, objectName, ctx, caps, required, origin, txState) {
231
231
  const fn = vm.newFunction(method, (...argHandles) => {
232
232
  if (!caps.has(required)) {
233
233
  throw new SandboxError(
@@ -242,7 +242,8 @@ function installApiMethod(vm, parent, method, objectName, ctx, caps, required, o
242
242
  const deferred = vm.newPromise();
243
243
  void (async () => {
244
244
  try {
245
- const proxy = apiAny.object(objectName);
245
+ const source = txState.api ?? apiAny;
246
+ const proxy = source.object(objectName);
246
247
  const m = proxy[method];
247
248
  if (typeof m !== "function") {
248
249
  throw new SandboxError(`ctx.api.object('${objectName}').${method} not implemented`);
@@ -388,8 +389,9 @@ var init_quickjs_runner = __esm({
388
389
  const start = Date.now();
389
390
  const deadline = start + args.timeoutMs;
390
391
  runtime.setInterruptHandler(() => Date.now() > deadline);
392
+ const txState = { api: null, handle: null, open: false };
391
393
  try {
392
- this.installCtx(vm, args.ctx, new Set(args.capabilities), args.origin);
394
+ this.installCtx(vm, args.ctx, new Set(args.capabilities), args.origin, txState);
393
395
  if (args.isExpression) {
394
396
  const wrapped2 = `globalThis.__result = JSON.stringify((function(){ return (${args.source}); })());`;
395
397
  const result = vm.evalCode(wrapped2);
@@ -452,6 +454,16 @@ var init_quickjs_runner = __esm({
452
454
  pumps++;
453
455
  }
454
456
  } finally {
457
+ if (txState.open && txState.handle != null) {
458
+ const apiTx = args.ctx.api;
459
+ const rollback = apiTx?.rollbackTransaction;
460
+ if (typeof rollback === "function") {
461
+ try {
462
+ await rollback.call(apiTx, txState.handle);
463
+ } catch {
464
+ }
465
+ }
466
+ }
455
467
  vm.dispose();
456
468
  }
457
469
  }
@@ -464,7 +476,7 @@ var init_quickjs_runner = __esm({
464
476
  * {@link installApiMethod}) so they may return Promises (real ObjectQL
465
477
  * `find/count/insert/...` are async) without asyncify's single-unwind limit.
466
478
  */
467
- installCtx(vm, ctx, caps, origin) {
479
+ installCtx(vm, ctx, caps, origin, txState) {
468
480
  setGlobalJson(vm, "__input", ctx.input);
469
481
  setGlobalJson(vm, "__previous", ctx.previous);
470
482
  const ctxObj = vm.newObject();
@@ -499,12 +511,70 @@ var init_quickjs_runner = __esm({
499
511
  const wrap = vm.newObject();
500
512
  const READ = ["find", "findOne", "count", "aggregate"];
501
513
  const WRITE = ["insert", "update", "delete", "updateMany", "deleteMany", "upsert"];
502
- for (const m of READ) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.read", origin);
503
- for (const m of WRITE) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.write", origin);
514
+ for (const m of READ) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.read", origin, txState);
515
+ for (const m of WRITE) installApiMethod(vm, wrap, m, objectName, ctx, caps, "api.write", origin, txState);
504
516
  return wrap;
505
517
  });
506
518
  vm.setProp(apiObj, "object", objectFn);
507
519
  objectFn.dispose();
520
+ const apiTx = ctx.api;
521
+ const installTxLeaf = (name, run) => {
522
+ const fn = vm.newFunction(name, () => {
523
+ if (!caps.has("api.transaction")) {
524
+ throw new SandboxError(
525
+ `capability 'api.transaction' not granted to ${origin.kind} '${origin.name}' (called ctx.api.transaction)`
526
+ );
527
+ }
528
+ const deferred = vm.newPromise();
529
+ void (async () => {
530
+ try {
531
+ await run();
532
+ if (!vm.alive) return;
533
+ deferred.resolve(vm.undefined);
534
+ } catch (err) {
535
+ if (!vm.alive) return;
536
+ const errH = err instanceof Error ? vm.newError({ name: err.name || "Error", message: err.message }) : vm.newError({ name: "Error", message: String(err) });
537
+ deferred.reject(errH);
538
+ errH.dispose();
539
+ }
540
+ })();
541
+ return deferred.handle;
542
+ });
543
+ vm.setProp(apiObj, name, fn);
544
+ fn.dispose();
545
+ };
546
+ installTxLeaf("__txBegin", async () => {
547
+ if (txState.open) throw new SandboxError("nested ctx.api.transaction is not supported");
548
+ const begin = apiTx?.beginTransaction;
549
+ if (typeof begin === "function") {
550
+ const r = await begin.call(apiTx) ?? null;
551
+ if (r) {
552
+ txState.api = r.ctx;
553
+ txState.handle = r.handle;
554
+ }
555
+ }
556
+ txState.open = true;
557
+ });
558
+ installTxLeaf("__txCommit", async () => {
559
+ const { handle, open } = txState;
560
+ txState.api = null;
561
+ txState.handle = null;
562
+ txState.open = false;
563
+ const commit = apiTx?.commitTransaction;
564
+ if (open && handle != null && typeof commit === "function") {
565
+ await commit.call(apiTx, handle);
566
+ }
567
+ });
568
+ installTxLeaf("__txRollback", async () => {
569
+ const { handle, open } = txState;
570
+ txState.api = null;
571
+ txState.handle = null;
572
+ txState.open = false;
573
+ const rollback = apiTx?.rollbackTransaction;
574
+ if (open && handle != null && typeof rollback === "function") {
575
+ await rollback.call(apiTx, handle);
576
+ }
577
+ });
508
578
  vm.setProp(ctxObj, "api", apiObj);
509
579
  apiObj.dispose();
510
580
  const logObj = vm.newObject();
@@ -537,6 +607,25 @@ var init_quickjs_runner = __esm({
537
607
  cryptoObj.dispose();
538
608
  vm.setProp(vm.global, "__ctx", ctxObj);
539
609
  ctxObj.dispose();
610
+ const sugar = vm.evalCode(
611
+ `__ctx.api.transaction = async function (fn) {
612
+ await __ctx.api.__txBegin();
613
+ try {
614
+ var r = await fn();
615
+ await __ctx.api.__txCommit();
616
+ return r;
617
+ } catch (e) {
618
+ await __ctx.api.__txRollback();
619
+ throw e;
620
+ }
621
+ };`
622
+ );
623
+ if (sugar.error) {
624
+ const msg = vm.dump(sugar.error);
625
+ sugar.error.dispose();
626
+ throw new SandboxError(`failed to install ctx.api.transaction: ${formatErr(msg)}`);
627
+ }
628
+ sugar.value.dispose();
540
629
  }
541
630
  };
542
631
  SandboxError = class extends Error {
@@ -796,14 +885,13 @@ function collectBundleFunctions(bundle) {
796
885
  merge(bundle?.manifest?.functions);
797
886
  return out;
798
887
  }
799
- var import_types, import_system, AppPlugin;
888
+ var import_types, AppPlugin;
800
889
  var init_app_plugin = __esm({
801
890
  "src/app-plugin.ts"() {
802
891
  "use strict";
803
892
  import_types = require("@objectstack/types");
804
893
  init_seed_loader();
805
894
  init_package_state_store();
806
- import_system = require("@objectstack/spec/system");
807
895
  init_quickjs_runner();
808
896
  init_body_runner();
809
897
  AppPlugin = class {
@@ -1069,7 +1157,7 @@ var init_app_plugin = __esm({
1069
1157
  ...d,
1070
1158
  object: d.object
1071
1159
  }));
1072
- const seedIdentity = await this.ensureSeedIdentity(ql, ctx.logger);
1160
+ const seedIdentity = void 0;
1073
1161
  try {
1074
1162
  const kernel = ctx.kernel;
1075
1163
  const existing = (() => {
@@ -1112,10 +1200,10 @@ var init_app_plugin = __esm({
1112
1200
  defaultMode: "upsert",
1113
1201
  multiPass: true,
1114
1202
  organizationId,
1115
- // Bind os.user (system identity) and os.org (this
1116
- // tenant) so identity-derived seed values resolve
1117
- // per-org. org.id falls back to organizationId
1118
- // inside the loader when identity.org is absent.
1203
+ // `os.org` is derived from organizationId inside
1204
+ // the loader. `seedIdentity` (os.user) is undefined
1205
+ // unless a seed embeds `cel`os.user.id`` see the
1206
+ // lazy guard where it is resolved.
1119
1207
  identity: seedIdentity
1120
1208
  }
1121
1209
  });
@@ -1275,64 +1363,6 @@ var init_app_plugin = __esm({
1275
1363
  this.name = `plugin.app.${appId}`;
1276
1364
  this.version = sys?.version;
1277
1365
  }
1278
- /**
1279
- * Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
1280
- *
1281
- * On a fresh boot there are zero users until the first human sign-up
1282
- * (which the SeedLoader runs *before*), so identity-derived seeds like
1283
- * `owner_id: cel`os.user.id`` had nothing to resolve against and were
1284
- * dropped silently. To make seeds deterministic and self-sufficient we
1285
- * upsert a single non-loginable **system user** (`usr_system`) and bind
1286
- * it as `os.user`.
1287
- *
1288
- * Why a dedicated system user rather than the login admin:
1289
- * - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
1290
- * password lives in `sys_account`, so a *loginable* admin can only be
1291
- * minted through better-auth (the CLI does this via HTTP sign-up after
1292
- * boot). A raw insert here would bypass those invariants.
1293
- * - `usr_system` is an owner identity only (no credential row), analogous
1294
- * to Salesforce's "Automated Process" user. The human admin is created
1295
- * independently and need not be the seed owner.
1296
- *
1297
- * Idempotent: matches by the stable id, inserts once, reuses thereafter.
1298
- * Failures are non-fatal (logged) — records that actually need `os.user`
1299
- * then fail loudly in the loader with an actionable message.
1300
- */
1301
- async ensureSeedIdentity(ql, logger) {
1302
- const SYSTEM_USER_ID = import_system.SystemUserId.SYSTEM;
1303
- const SYSTEM_USER_EMAIL = "system@objectstack.local";
1304
- const identity = { user: { id: SYSTEM_USER_ID, role: "system", email: SYSTEM_USER_EMAIL } };
1305
- const opts = { context: { isSystem: true } };
1306
- try {
1307
- const existing = await ql.find(
1308
- "sys_user",
1309
- { where: { id: SYSTEM_USER_ID }, limit: 1 },
1310
- opts
1311
- );
1312
- if (Array.isArray(existing) && existing.length > 0) {
1313
- return identity;
1314
- }
1315
- await ql.insert(
1316
- "sys_user",
1317
- {
1318
- id: SYSTEM_USER_ID,
1319
- name: "System",
1320
- email: SYSTEM_USER_EMAIL,
1321
- email_verified: true,
1322
- role: "system"
1323
- },
1324
- opts
1325
- );
1326
- logger.info(
1327
- `[Seeder] Provisioned deterministic system user (${SYSTEM_USER_ID}) as seed owner \u2014 binds os.user for identity-derived seed values`
1328
- );
1329
- } catch (err) {
1330
- logger.warn("[Seeder] Failed to ensure system seed user; os.user-dependent seeds may be dropped", {
1331
- error: err?.message ?? String(err)
1332
- });
1333
- }
1334
- return identity;
1335
- }
1336
1366
  /**
1337
1367
  * Emit a kernel hook so the control-plane `AppCatalogService` can
1338
1368
  * upsert / delete the corresponding `sys_app` row. Silently no-ops
@@ -1950,7 +1980,7 @@ function safeGet(ctx, name) {
1950
1980
 
1951
1981
  // src/http-dispatcher.ts
1952
1982
  var import_core3 = require("@objectstack/core");
1953
- var import_system2 = require("@objectstack/spec/system");
1983
+ var import_system = require("@objectstack/spec/system");
1954
1984
  var import_shared2 = require("@objectstack/spec/shared");
1955
1985
  init_package_state_store();
1956
1986
 
@@ -2695,7 +2725,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2695
2725
  let userId;
2696
2726
  let activeOrganizationId;
2697
2727
  try {
2698
- const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
2728
+ const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
2699
2729
  const sessionData = await authService?.api?.getSession?.({
2700
2730
  headers: context.request?.headers
2701
2731
  });
@@ -2764,21 +2794,21 @@ var _HttpDispatcher = class _HttpDispatcher {
2764
2794
  queueSvc,
2765
2795
  jobSvc
2766
2796
  ] = await Promise.all([
2767
- this.resolveService(import_system2.CoreServiceName.enum.auth),
2768
- this.resolveService(import_system2.CoreServiceName.enum.graphql),
2769
- this.resolveService(import_system2.CoreServiceName.enum.search),
2770
- this.resolveService(import_system2.CoreServiceName.enum.realtime),
2771
- this.resolveService(import_system2.CoreServiceName.enum["file-storage"]),
2772
- this.resolveService(import_system2.CoreServiceName.enum.analytics),
2773
- this.resolveService(import_system2.CoreServiceName.enum.workflow),
2774
- this.resolveService(import_system2.CoreServiceName.enum.ai),
2775
- this.resolveService(import_system2.CoreServiceName.enum.notification),
2776
- this.resolveService(import_system2.CoreServiceName.enum.i18n),
2777
- this.resolveService(import_system2.CoreServiceName.enum.ui),
2778
- this.resolveService(import_system2.CoreServiceName.enum.automation),
2779
- this.resolveService(import_system2.CoreServiceName.enum.cache),
2780
- this.resolveService(import_system2.CoreServiceName.enum.queue),
2781
- this.resolveService(import_system2.CoreServiceName.enum.job)
2797
+ this.resolveService(import_system.CoreServiceName.enum.auth),
2798
+ this.resolveService(import_system.CoreServiceName.enum.graphql),
2799
+ this.resolveService(import_system.CoreServiceName.enum.search),
2800
+ this.resolveService(import_system.CoreServiceName.enum.realtime),
2801
+ this.resolveService(import_system.CoreServiceName.enum["file-storage"]),
2802
+ this.resolveService(import_system.CoreServiceName.enum.analytics),
2803
+ this.resolveService(import_system.CoreServiceName.enum.workflow),
2804
+ this.resolveService(import_system.CoreServiceName.enum.ai),
2805
+ this.resolveService(import_system.CoreServiceName.enum.notification),
2806
+ this.resolveService(import_system.CoreServiceName.enum.i18n),
2807
+ this.resolveService(import_system.CoreServiceName.enum.ui),
2808
+ this.resolveService(import_system.CoreServiceName.enum.automation),
2809
+ this.resolveService(import_system.CoreServiceName.enum.cache),
2810
+ this.resolveService(import_system.CoreServiceName.enum.queue),
2811
+ this.resolveService(import_system.CoreServiceName.enum.job)
2782
2812
  ]);
2783
2813
  const hasAuth = !!authSvc;
2784
2814
  const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
@@ -2899,7 +2929,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2899
2929
  * path: sub-path after /auth/
2900
2930
  */
2901
2931
  async handleAuth(path, method, body, context) {
2902
- const authService = await this.getService(import_system2.CoreServiceName.enum.auth);
2932
+ const authService = await this.getService(import_system.CoreServiceName.enum.auth);
2903
2933
  if (authService && typeof authService.handler === "function") {
2904
2934
  const response = await authService.handler(context.request, context.response);
2905
2935
  return { handled: true, result: response };
@@ -2997,7 +3027,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2997
3027
  if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
2998
3028
  const type = parts[0];
2999
3029
  const name = parts.slice(1, -1).join("/");
3000
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3030
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3001
3031
  if (metadataService && typeof metadataService.getPublished === "function") {
3002
3032
  const data = await metadataService.getPublished(type, name);
3003
3033
  if (data === void 0) return { handled: true, response: this.error("Not found", 404) };
@@ -3127,7 +3157,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3127
3157
  } catch {
3128
3158
  }
3129
3159
  }
3130
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3160
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3131
3161
  if (metadataService && typeof metadataService.list === "function") {
3132
3162
  try {
3133
3163
  let items = await metadataService.list(typeOrName);
@@ -3261,7 +3291,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3261
3291
  * path: sub-path after /analytics/
3262
3292
  */
3263
3293
  async handleAnalytics(path, method, body, _context) {
3264
- const analyticsService = await this.getService(import_system2.CoreServiceName.enum.analytics);
3294
+ const analyticsService = await this.getService(import_system.CoreServiceName.enum.analytics);
3265
3295
  if (!analyticsService) return { handled: false };
3266
3296
  const m = method.toUpperCase();
3267
3297
  const subPath = path.replace(/^\/+/, "");
@@ -3293,7 +3323,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3293
3323
  * POST /read/all → markAllRead
3294
3324
  */
3295
3325
  async handleNotification(path, method, body, query, context) {
3296
- const service = await this.resolveService(import_system2.CoreServiceName.enum.notification, context.environmentId);
3326
+ const service = await this.resolveService(import_system.CoreServiceName.enum.notification, context.environmentId);
3297
3327
  if (!service || typeof service.listInbox !== "function") return { handled: false };
3298
3328
  const userId = context.executionContext?.userId;
3299
3329
  if (!userId) {
@@ -3331,7 +3361,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3331
3361
  * GET /labels/:object?locale=xx → getFieldLabels (locale from query)
3332
3362
  */
3333
3363
  async handleI18n(path, method, query, _context) {
3334
- const i18nService = await this.getService(import_system2.CoreServiceName.enum.i18n);
3364
+ const i18nService = await this.getService(import_system.CoreServiceName.enum.i18n);
3335
3365
  if (!i18nService) return { handled: true, response: this.error("i18n service not available", 501) };
3336
3366
  const m = method.toUpperCase();
3337
3367
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -3449,7 +3479,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3449
3479
  }
3450
3480
  if (parts.length === 2 && parts[1] === "publish" && m === "POST") {
3451
3481
  const id = decodeURIComponent(parts[0]);
3452
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3482
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3453
3483
  if (metadataService && typeof metadataService.publishPackage === "function") {
3454
3484
  const result = await metadataService.publishPackage(id, body || {});
3455
3485
  return { handled: true, response: this.success(result) };
@@ -3535,7 +3565,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3535
3565
  }
3536
3566
  if (parts.length === 2 && parts[1] === "revert" && m === "POST") {
3537
3567
  const id = decodeURIComponent(parts[0]);
3538
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3568
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3539
3569
  if (metadataService && typeof metadataService.revertPackage === "function") {
3540
3570
  await metadataService.revertPackage(id);
3541
3571
  return { handled: true, response: this.success({ success: true }) };
@@ -3713,7 +3743,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3713
3743
  */
3714
3744
  async applyPublishedSeeds(names, organizationId, _context) {
3715
3745
  const protocol = await this.resolveService("protocol");
3716
- const metadata = await this.getService(import_system2.CoreServiceName.enum.metadata);
3746
+ const metadata = await this.getService(import_system.CoreServiceName.enum.metadata);
3717
3747
  const ql = await this.resolveService("objectql");
3718
3748
  if (!protocol || typeof protocol.getMetaItem !== "function" || !ql || !metadata) {
3719
3749
  return { success: false, error: "seed apply: required services unavailable" };
@@ -3766,7 +3796,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3766
3796
  */
3767
3797
  async resolveActiveOrganizationId(context) {
3768
3798
  try {
3769
- const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
3799
+ const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
3770
3800
  const rawHeaders = context.request?.headers;
3771
3801
  let headers = rawHeaders;
3772
3802
  if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
@@ -3794,7 +3824,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3794
3824
  * path: sub-path after /storage/
3795
3825
  */
3796
3826
  async handleStorage(path, method, file, context) {
3797
- const storageService = await this.getService(import_system2.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
3827
+ const storageService = await this.getService(import_system.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
3798
3828
  if (!storageService) {
3799
3829
  return { handled: true, response: this.error("File storage not configured", 501) };
3800
3830
  }
@@ -3873,7 +3903,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3873
3903
  * GET /:name/runs/:runId/screen → the screen a paused run awaits
3874
3904
  */
3875
3905
  async handleAutomation(path, method, body, context, query) {
3876
- const automationService = await this.getService(import_system2.CoreServiceName.enum.automation);
3906
+ const automationService = await this.getService(import_system.CoreServiceName.enum.automation);
3877
3907
  if (!automationService) return { handled: false };
3878
3908
  const m = method.toUpperCase();
3879
3909
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);