@objectstack/runtime 9.9.1 → 9.11.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
 
@@ -2086,6 +2116,7 @@ async function resolveExecutionContext(opts) {
2086
2116
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
2087
2117
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
2088
2118
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
2119
+ if (sessionData?.user?.email) ctx.email = String(sessionData.user.email);
2089
2120
  } catch {
2090
2121
  }
2091
2122
  }
@@ -2094,6 +2125,10 @@ async function resolveExecutionContext(opts) {
2094
2125
  if (!userId) return ctx;
2095
2126
  const ql = await opts.getQl();
2096
2127
  if (!ql) return ctx;
2128
+ if (!ctx.email) {
2129
+ const userRows = await tryFind(ql, "sys_user", { id: userId }, 1);
2130
+ if (userRows[0]?.email) ctx.email = String(userRows[0].email);
2131
+ }
2097
2132
  const memberWhere = tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId };
2098
2133
  const members = await tryFind(ql, "sys_member", memberWhere, 50);
2099
2134
  for (const m of members) {
@@ -2695,7 +2730,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2695
2730
  let userId;
2696
2731
  let activeOrganizationId;
2697
2732
  try {
2698
- const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
2733
+ const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
2699
2734
  const sessionData = await authService?.api?.getSession?.({
2700
2735
  headers: context.request?.headers
2701
2736
  });
@@ -2764,21 +2799,21 @@ var _HttpDispatcher = class _HttpDispatcher {
2764
2799
  queueSvc,
2765
2800
  jobSvc
2766
2801
  ] = 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)
2802
+ this.resolveService(import_system.CoreServiceName.enum.auth),
2803
+ this.resolveService(import_system.CoreServiceName.enum.graphql),
2804
+ this.resolveService(import_system.CoreServiceName.enum.search),
2805
+ this.resolveService(import_system.CoreServiceName.enum.realtime),
2806
+ this.resolveService(import_system.CoreServiceName.enum["file-storage"]),
2807
+ this.resolveService(import_system.CoreServiceName.enum.analytics),
2808
+ this.resolveService(import_system.CoreServiceName.enum.workflow),
2809
+ this.resolveService(import_system.CoreServiceName.enum.ai),
2810
+ this.resolveService(import_system.CoreServiceName.enum.notification),
2811
+ this.resolveService(import_system.CoreServiceName.enum.i18n),
2812
+ this.resolveService(import_system.CoreServiceName.enum.ui),
2813
+ this.resolveService(import_system.CoreServiceName.enum.automation),
2814
+ this.resolveService(import_system.CoreServiceName.enum.cache),
2815
+ this.resolveService(import_system.CoreServiceName.enum.queue),
2816
+ this.resolveService(import_system.CoreServiceName.enum.job)
2782
2817
  ]);
2783
2818
  const hasAuth = !!authSvc;
2784
2819
  const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
@@ -2899,7 +2934,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2899
2934
  * path: sub-path after /auth/
2900
2935
  */
2901
2936
  async handleAuth(path, method, body, context) {
2902
- const authService = await this.getService(import_system2.CoreServiceName.enum.auth);
2937
+ const authService = await this.getService(import_system.CoreServiceName.enum.auth);
2903
2938
  if (authService && typeof authService.handler === "function") {
2904
2939
  const response = await authService.handler(context.request, context.response);
2905
2940
  return { handled: true, result: response };
@@ -2997,7 +3032,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2997
3032
  if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
2998
3033
  const type = parts[0];
2999
3034
  const name = parts.slice(1, -1).join("/");
3000
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3035
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3001
3036
  if (metadataService && typeof metadataService.getPublished === "function") {
3002
3037
  const data = await metadataService.getPublished(type, name);
3003
3038
  if (data === void 0) return { handled: true, response: this.error("Not found", 404) };
@@ -3127,7 +3162,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3127
3162
  } catch {
3128
3163
  }
3129
3164
  }
3130
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3165
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3131
3166
  if (metadataService && typeof metadataService.list === "function") {
3132
3167
  try {
3133
3168
  let items = await metadataService.list(typeOrName);
@@ -3261,7 +3296,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3261
3296
  * path: sub-path after /analytics/
3262
3297
  */
3263
3298
  async handleAnalytics(path, method, body, _context) {
3264
- const analyticsService = await this.getService(import_system2.CoreServiceName.enum.analytics);
3299
+ const analyticsService = await this.getService(import_system.CoreServiceName.enum.analytics);
3265
3300
  if (!analyticsService) return { handled: false };
3266
3301
  const m = method.toUpperCase();
3267
3302
  const subPath = path.replace(/^\/+/, "");
@@ -3293,7 +3328,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3293
3328
  * POST /read/all → markAllRead
3294
3329
  */
3295
3330
  async handleNotification(path, method, body, query, context) {
3296
- const service = await this.resolveService(import_system2.CoreServiceName.enum.notification, context.environmentId);
3331
+ const service = await this.resolveService(import_system.CoreServiceName.enum.notification, context.environmentId);
3297
3332
  if (!service || typeof service.listInbox !== "function") return { handled: false };
3298
3333
  const userId = context.executionContext?.userId;
3299
3334
  if (!userId) {
@@ -3331,7 +3366,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3331
3366
  * GET /labels/:object?locale=xx → getFieldLabels (locale from query)
3332
3367
  */
3333
3368
  async handleI18n(path, method, query, _context) {
3334
- const i18nService = await this.getService(import_system2.CoreServiceName.enum.i18n);
3369
+ const i18nService = await this.getService(import_system.CoreServiceName.enum.i18n);
3335
3370
  if (!i18nService) return { handled: true, response: this.error("i18n service not available", 501) };
3336
3371
  const m = method.toUpperCase();
3337
3372
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -3449,7 +3484,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3449
3484
  }
3450
3485
  if (parts.length === 2 && parts[1] === "publish" && m === "POST") {
3451
3486
  const id = decodeURIComponent(parts[0]);
3452
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3487
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3453
3488
  if (metadataService && typeof metadataService.publishPackage === "function") {
3454
3489
  const result = await metadataService.publishPackage(id, body || {});
3455
3490
  return { handled: true, response: this.success(result) };
@@ -3535,7 +3570,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3535
3570
  }
3536
3571
  if (parts.length === 2 && parts[1] === "revert" && m === "POST") {
3537
3572
  const id = decodeURIComponent(parts[0]);
3538
- const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3573
+ const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3539
3574
  if (metadataService && typeof metadataService.revertPackage === "function") {
3540
3575
  await metadataService.revertPackage(id);
3541
3576
  return { handled: true, response: this.success({ success: true }) };
@@ -3713,7 +3748,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3713
3748
  */
3714
3749
  async applyPublishedSeeds(names, organizationId, _context) {
3715
3750
  const protocol = await this.resolveService("protocol");
3716
- const metadata = await this.getService(import_system2.CoreServiceName.enum.metadata);
3751
+ const metadata = await this.getService(import_system.CoreServiceName.enum.metadata);
3717
3752
  const ql = await this.resolveService("objectql");
3718
3753
  if (!protocol || typeof protocol.getMetaItem !== "function" || !ql || !metadata) {
3719
3754
  return { success: false, error: "seed apply: required services unavailable" };
@@ -3766,7 +3801,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3766
3801
  */
3767
3802
  async resolveActiveOrganizationId(context) {
3768
3803
  try {
3769
- const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
3804
+ const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
3770
3805
  const rawHeaders = context.request?.headers;
3771
3806
  let headers = rawHeaders;
3772
3807
  if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
@@ -3794,7 +3829,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3794
3829
  * path: sub-path after /storage/
3795
3830
  */
3796
3831
  async handleStorage(path, method, file, context) {
3797
- const storageService = await this.getService(import_system2.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
3832
+ const storageService = await this.getService(import_system.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
3798
3833
  if (!storageService) {
3799
3834
  return { handled: true, response: this.error("File storage not configured", 501) };
3800
3835
  }
@@ -3873,7 +3908,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3873
3908
  * GET /:name/runs/:runId/screen → the screen a paused run awaits
3874
3909
  */
3875
3910
  async handleAutomation(path, method, body, context, query) {
3876
- const automationService = await this.getService(import_system2.CoreServiceName.enum.automation);
3911
+ const automationService = await this.getService(import_system.CoreServiceName.enum.automation);
3877
3912
  if (!automationService) return { handled: false };
3878
3913
  const m = method.toUpperCase();
3879
3914
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);