@objectstack/runtime 8.0.1 → 9.0.1

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.js CHANGED
@@ -143,601 +143,10 @@ var seed_loader_exports = {};
143
143
  __export(seed_loader_exports, {
144
144
  SeedLoaderService: () => SeedLoaderService
145
145
  });
146
- import { SeedLoaderConfigSchema } from "@objectstack/spec/data";
147
- import { resolveSeedRecord } from "@objectstack/formula";
148
- var DEFAULT_EXTERNAL_ID_FIELD, _SeedLoaderService, SeedLoaderService;
146
+ import { SeedLoaderService } from "@objectstack/objectql";
149
147
  var init_seed_loader = __esm({
150
148
  "src/seed-loader.ts"() {
151
149
  "use strict";
152
- DEFAULT_EXTERNAL_ID_FIELD = "name";
153
- _SeedLoaderService = class _SeedLoaderService {
154
- constructor(engine, metadata, logger) {
155
- this.engine = engine;
156
- this.metadata = metadata;
157
- this.logger = logger;
158
- }
159
- // ==========================================================================
160
- // Public API
161
- // ==========================================================================
162
- async load(request) {
163
- const startTime = Date.now();
164
- const config = request.config;
165
- const allErrors = [];
166
- const allResults = [];
167
- const datasets = this.filterByEnv(request.seeds, config.env);
168
- if (datasets.length === 0) {
169
- return this.buildEmptyResult(config, Date.now() - startTime);
170
- }
171
- const objectNames = datasets.map((d) => d.object);
172
- const graph = await this.buildDependencyGraph(objectNames);
173
- this.logger.info("[SeedLoader] Dependency graph built", {
174
- objects: objectNames.length,
175
- insertOrder: graph.insertOrder,
176
- circularDeps: graph.circularDependencies.length
177
- });
178
- const orderedDatasets = this.orderDatasets(datasets, graph.insertOrder);
179
- const refMap = this.buildReferenceMap(graph);
180
- const insertedRecords = /* @__PURE__ */ new Map();
181
- const deferredUpdates = [];
182
- for (const dataset of orderedDatasets) {
183
- const result = await this.loadDataset(
184
- dataset,
185
- config,
186
- refMap,
187
- insertedRecords,
188
- deferredUpdates,
189
- allErrors
190
- );
191
- allResults.push(result);
192
- if (config.haltOnError && result.errored > 0) {
193
- this.logger.warn("[SeedLoader] Halting on first error", { object: dataset.object });
194
- break;
195
- }
196
- }
197
- if (config.multiPass && deferredUpdates.length > 0 && !config.dryRun) {
198
- this.logger.info("[SeedLoader] Pass 2: resolving deferred references", {
199
- count: deferredUpdates.length
200
- });
201
- await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, config.organizationId);
202
- }
203
- const durationMs = Date.now() - startTime;
204
- return this.buildResult(config, graph, allResults, allErrors, durationMs);
205
- }
206
- async buildDependencyGraph(objectNames) {
207
- const nodes = [];
208
- const objectSet = new Set(objectNames);
209
- for (const objectName of objectNames) {
210
- const objDef = await this.metadata.getObject(objectName);
211
- const dependsOn = [];
212
- const references = [];
213
- if (objDef && objDef.fields) {
214
- const fields = objDef.fields;
215
- for (const [fieldName, fieldDef] of Object.entries(fields)) {
216
- if ((fieldDef.type === "lookup" || fieldDef.type === "master_detail") && fieldDef.reference) {
217
- const targetObject = fieldDef.reference;
218
- if (objectSet.has(targetObject) && !dependsOn.includes(targetObject)) {
219
- dependsOn.push(targetObject);
220
- }
221
- references.push({
222
- field: fieldName,
223
- targetObject,
224
- targetField: DEFAULT_EXTERNAL_ID_FIELD,
225
- fieldType: fieldDef.type
226
- });
227
- }
228
- }
229
- }
230
- nodes.push({ object: objectName, dependsOn, references });
231
- }
232
- const { insertOrder, circularDependencies } = this.topologicalSort(nodes);
233
- return { nodes, insertOrder, circularDependencies };
234
- }
235
- async validate(datasets, config) {
236
- const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
237
- return this.load({ seeds: datasets, config: parsedConfig });
238
- }
239
- // ==========================================================================
240
- // Internal: Seed Loading
241
- // ==========================================================================
242
- async loadDataset(dataset, config, refMap, insertedRecords, deferredUpdates, allErrors) {
243
- const objectName = dataset.object;
244
- const mode = dataset.mode || config.defaultMode;
245
- const externalId = dataset.externalId || "name";
246
- let inserted = 0;
247
- let updated = 0;
248
- let skipped = 0;
249
- let errored = 0;
250
- let referencesResolved = 0;
251
- let referencesDeferred = 0;
252
- const errors = [];
253
- if (!insertedRecords.has(objectName)) {
254
- insertedRecords.set(objectName, /* @__PURE__ */ new Map());
255
- }
256
- let existingRecords;
257
- if ((mode === "upsert" || mode === "update" || mode === "ignore") && !config.dryRun) {
258
- existingRecords = await this.loadExistingRecords(
259
- objectName,
260
- externalId,
261
- config.organizationId
262
- );
263
- }
264
- const objectRefs = refMap.get(objectName) || [];
265
- const seedNow = /* @__PURE__ */ new Date();
266
- const seedIdentity = config.identity;
267
- const baseEvalCtx = {
268
- now: seedNow,
269
- user: seedIdentity?.user,
270
- // Fall back to the per-tenant organizationId so `os.org.id` resolves
271
- // during per-org replay even without an explicit identity.org.
272
- org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
273
- env: config.env
274
- };
275
- for (let i = 0; i < dataset.records.length; i++) {
276
- const seedResult = resolveSeedRecord(
277
- dataset.records[i],
278
- baseEvalCtx
279
- );
280
- if (!seedResult.ok) {
281
- errored++;
282
- const error = {
283
- sourceObject: objectName,
284
- field: "(expression)",
285
- targetObject: objectName,
286
- targetField: "(expression)",
287
- attemptedValue: dataset.records[i],
288
- recordIndex: i,
289
- message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. Records using cel\`os.user.id\` / cel\`os.org.id\` require a seed identity \u2014 ensure a system/admin user exists before seeding (see SeedLoaderConfig.identity).`
290
- };
291
- errors.push(error);
292
- allErrors.push(error);
293
- this.logger.warn(`[SeedLoader] ${error.message}`);
294
- continue;
295
- }
296
- const record = { ...seedResult.value };
297
- if (config.organizationId && record["organization_id"] == null) {
298
- record["organization_id"] = config.organizationId;
299
- }
300
- for (const ref of objectRefs) {
301
- const fieldValue = record[ref.field];
302
- if (fieldValue === void 0 || fieldValue === null) continue;
303
- if (typeof fieldValue === "object") {
304
- const wrapped = fieldValue.externalId;
305
- const hint = wrapped !== void 0 ? ` Pass the natural key directly: ${ref.field}: ${JSON.stringify(wrapped)}.` : ` Pass the target's ${ref.targetField} value as a plain string.`;
306
- const error = {
307
- sourceObject: objectName,
308
- field: ref.field,
309
- targetObject: ref.targetObject,
310
- targetField: ref.targetField,
311
- attemptedValue: fieldValue,
312
- recordIndex: i,
313
- message: `Invalid reference for ${objectName}.${ref.field}: expected a ${ref.targetObject}.${ref.targetField} natural-key string but got an object.${hint}`
314
- };
315
- errors.push(error);
316
- allErrors.push(error);
317
- this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
318
- record[ref.field] = null;
319
- continue;
320
- }
321
- if (typeof fieldValue !== "string" || this.looksLikeInternalId(fieldValue)) continue;
322
- const targetMap = insertedRecords.get(ref.targetObject);
323
- const resolvedId = targetMap?.get(String(fieldValue));
324
- if (resolvedId) {
325
- record[ref.field] = resolvedId;
326
- referencesResolved++;
327
- } else if (!config.dryRun) {
328
- const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue, config.organizationId);
329
- if (dbId) {
330
- record[ref.field] = dbId;
331
- referencesResolved++;
332
- } else if (config.multiPass) {
333
- record[ref.field] = null;
334
- deferredUpdates.push({
335
- objectName,
336
- recordExternalId: String(record[externalId] ?? ""),
337
- field: ref.field,
338
- targetObject: ref.targetObject,
339
- targetField: ref.targetField,
340
- attemptedValue: fieldValue,
341
- recordIndex: i
342
- });
343
- referencesDeferred++;
344
- } else {
345
- const error = {
346
- sourceObject: objectName,
347
- field: ref.field,
348
- targetObject: ref.targetObject,
349
- targetField: ref.targetField,
350
- attemptedValue: fieldValue,
351
- recordIndex: i,
352
- message: `Cannot resolve reference: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField} not found`
353
- };
354
- errors.push(error);
355
- allErrors.push(error);
356
- }
357
- } else {
358
- const targetMap2 = insertedRecords.get(ref.targetObject);
359
- if (!targetMap2?.has(String(fieldValue))) {
360
- const error = {
361
- sourceObject: objectName,
362
- field: ref.field,
363
- targetObject: ref.targetObject,
364
- targetField: ref.targetField,
365
- attemptedValue: fieldValue,
366
- recordIndex: i,
367
- message: `[dry-run] Reference may not resolve: ${objectName}.${ref.field} = '${fieldValue}' \u2192 ${ref.targetObject}.${ref.targetField}`
368
- };
369
- errors.push(error);
370
- allErrors.push(error);
371
- }
372
- }
373
- }
374
- if (!config.dryRun) {
375
- try {
376
- const result = await this.writeRecord(
377
- objectName,
378
- record,
379
- mode,
380
- externalId,
381
- existingRecords
382
- );
383
- if (result.action === "inserted") inserted++;
384
- else if (result.action === "updated") updated++;
385
- else if (result.action === "skipped") skipped++;
386
- const externalIdValue = String(record[externalId] ?? "");
387
- const internalId = result.id;
388
- if (externalIdValue && internalId) {
389
- insertedRecords.get(objectName).set(externalIdValue, String(internalId));
390
- }
391
- } catch (err) {
392
- errored++;
393
- const error = {
394
- sourceObject: objectName,
395
- field: "(write)",
396
- targetObject: objectName,
397
- targetField: externalId,
398
- attemptedValue: record[externalId] ?? null,
399
- recordIndex: i,
400
- message: `Failed to write ${objectName} record #${i} (${externalId}=${String(record[externalId] ?? "")}): ${err.message}`
401
- };
402
- errors.push(error);
403
- allErrors.push(error);
404
- this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
405
- }
406
- } else {
407
- const externalIdValue = String(record[externalId] ?? "");
408
- if (externalIdValue) {
409
- insertedRecords.get(objectName).set(externalIdValue, `dry-run-id-${i}`);
410
- }
411
- inserted++;
412
- }
413
- }
414
- return {
415
- object: objectName,
416
- mode,
417
- inserted,
418
- updated,
419
- skipped,
420
- errored,
421
- total: dataset.records.length,
422
- referencesResolved,
423
- referencesDeferred,
424
- errors
425
- };
426
- }
427
- // ==========================================================================
428
- // Internal: Reference Resolution
429
- // ==========================================================================
430
- async resolveFromDatabase(targetObject, targetField, value, organizationId) {
431
- try {
432
- const where = { [targetField]: value };
433
- if (organizationId) where.organization_id = organizationId;
434
- const records = await this.engine.find(targetObject, {
435
- where,
436
- fields: ["id"],
437
- limit: 1,
438
- context: { isSystem: true }
439
- });
440
- if (records && records.length > 0) {
441
- return String(records[0].id || records[0]._id);
442
- }
443
- } catch {
444
- }
445
- return null;
446
- }
447
- async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, organizationId) {
448
- for (const deferred of deferredUpdates) {
449
- const targetMap = insertedRecords.get(deferred.targetObject);
450
- let resolvedId = targetMap?.get(String(deferred.attemptedValue));
451
- if (!resolvedId) {
452
- resolvedId = await this.resolveFromDatabase(
453
- deferred.targetObject,
454
- deferred.targetField,
455
- deferred.attemptedValue,
456
- organizationId
457
- ) ?? void 0;
458
- }
459
- if (resolvedId) {
460
- const objectRecordMap = insertedRecords.get(deferred.objectName);
461
- const recordId = objectRecordMap?.get(deferred.recordExternalId);
462
- if (recordId) {
463
- try {
464
- await this.engine.update(deferred.objectName, {
465
- id: recordId,
466
- [deferred.field]: resolvedId
467
- }, { context: { isSystem: true } });
468
- const resultEntry = allResults.find((r) => r.object === deferred.objectName);
469
- if (resultEntry) {
470
- resultEntry.referencesResolved++;
471
- resultEntry.referencesDeferred--;
472
- }
473
- } catch (err) {
474
- this.logger.warn("[SeedLoader] Failed to resolve deferred reference", {
475
- object: deferred.objectName,
476
- field: deferred.field,
477
- error: err.message
478
- });
479
- }
480
- }
481
- } else {
482
- const error = {
483
- sourceObject: deferred.objectName,
484
- field: deferred.field,
485
- targetObject: deferred.targetObject,
486
- targetField: deferred.targetField,
487
- attemptedValue: deferred.attemptedValue,
488
- recordIndex: deferred.recordIndex,
489
- message: `Deferred reference unresolved after pass 2: ${deferred.objectName}.${deferred.field} = '${deferred.attemptedValue}' \u2192 ${deferred.targetObject}.${deferred.targetField} not found`
490
- };
491
- const resultEntry = allResults.find((r) => r.object === deferred.objectName);
492
- if (resultEntry) {
493
- resultEntry.errors.push(error);
494
- }
495
- allErrors.push(error);
496
- }
497
- }
498
- }
499
- async writeRecord(objectName, record, mode, externalId, existingRecords) {
500
- const externalIdValue = record[externalId];
501
- const existing = existingRecords?.get(String(externalIdValue ?? ""));
502
- const opts = _SeedLoaderService.SEED_OPTIONS;
503
- switch (mode) {
504
- case "insert": {
505
- const result = await this.engine.insert(objectName, record, opts);
506
- return { action: "inserted", id: this.extractId(result) };
507
- }
508
- case "update": {
509
- if (!existing) {
510
- return { action: "skipped" };
511
- }
512
- const id = this.extractId(existing);
513
- await this.engine.update(objectName, { ...record, id }, opts);
514
- return { action: "updated", id };
515
- }
516
- case "upsert": {
517
- if (existing) {
518
- const id = this.extractId(existing);
519
- await this.engine.update(objectName, { ...record, id }, opts);
520
- return { action: "updated", id };
521
- } else {
522
- const result = await this.engine.insert(objectName, record, opts);
523
- return { action: "inserted", id: this.extractId(result) };
524
- }
525
- }
526
- case "ignore": {
527
- if (existing) {
528
- return { action: "skipped", id: this.extractId(existing) };
529
- }
530
- const result = await this.engine.insert(objectName, record, opts);
531
- return { action: "inserted", id: this.extractId(result) };
532
- }
533
- case "replace": {
534
- const result = await this.engine.insert(objectName, record, opts);
535
- return { action: "inserted", id: this.extractId(result) };
536
- }
537
- default: {
538
- const result = await this.engine.insert(objectName, record, opts);
539
- return { action: "inserted", id: this.extractId(result) };
540
- }
541
- }
542
- }
543
- // ==========================================================================
544
- // Internal: Dependency Graph
545
- // ==========================================================================
546
- /**
547
- * Kahn's algorithm for topological sort with cycle detection.
548
- */
549
- topologicalSort(nodes) {
550
- const inDegree = /* @__PURE__ */ new Map();
551
- const adjacency = /* @__PURE__ */ new Map();
552
- const objectSet = new Set(nodes.map((n) => n.object));
553
- for (const node of nodes) {
554
- inDegree.set(node.object, 0);
555
- adjacency.set(node.object, []);
556
- }
557
- for (const node of nodes) {
558
- for (const dep of node.dependsOn) {
559
- if (objectSet.has(dep) && dep !== node.object) {
560
- adjacency.get(dep).push(node.object);
561
- inDegree.set(node.object, (inDegree.get(node.object) || 0) + 1);
562
- }
563
- }
564
- }
565
- const queue = [];
566
- for (const [obj, degree] of inDegree) {
567
- if (degree === 0) queue.push(obj);
568
- }
569
- const insertOrder = [];
570
- while (queue.length > 0) {
571
- const current = queue.shift();
572
- insertOrder.push(current);
573
- for (const neighbor of adjacency.get(current) || []) {
574
- const newDegree = (inDegree.get(neighbor) || 0) - 1;
575
- inDegree.set(neighbor, newDegree);
576
- if (newDegree === 0) {
577
- queue.push(neighbor);
578
- }
579
- }
580
- }
581
- const circularDependencies = [];
582
- const remaining = nodes.filter((n) => !insertOrder.includes(n.object));
583
- if (remaining.length > 0) {
584
- const cycles = this.findCycles(remaining);
585
- circularDependencies.push(...cycles);
586
- for (const node of remaining) {
587
- if (!insertOrder.includes(node.object)) {
588
- insertOrder.push(node.object);
589
- }
590
- }
591
- }
592
- return { insertOrder, circularDependencies };
593
- }
594
- findCycles(nodes) {
595
- const cycles = [];
596
- const nodeMap = new Map(nodes.map((n) => [n.object, n]));
597
- const visited = /* @__PURE__ */ new Set();
598
- const inStack = /* @__PURE__ */ new Set();
599
- const dfs = (current, path) => {
600
- if (inStack.has(current)) {
601
- const cycleStart = path.indexOf(current);
602
- if (cycleStart !== -1) {
603
- cycles.push([...path.slice(cycleStart), current]);
604
- }
605
- return;
606
- }
607
- if (visited.has(current)) return;
608
- visited.add(current);
609
- inStack.add(current);
610
- path.push(current);
611
- const node = nodeMap.get(current);
612
- if (node) {
613
- for (const dep of node.dependsOn) {
614
- if (nodeMap.has(dep)) {
615
- dfs(dep, [...path]);
616
- }
617
- }
618
- }
619
- inStack.delete(current);
620
- };
621
- for (const node of nodes) {
622
- if (!visited.has(node.object)) {
623
- dfs(node.object, []);
624
- }
625
- }
626
- return cycles;
627
- }
628
- // ==========================================================================
629
- // Internal: Helpers
630
- // ==========================================================================
631
- filterByEnv(datasets, env) {
632
- if (!env) return datasets;
633
- return datasets.filter((d) => d.env.includes(env));
634
- }
635
- orderDatasets(datasets, insertOrder) {
636
- const orderMap = new Map(insertOrder.map((name, i) => [name, i]));
637
- return [...datasets].sort((a, b) => {
638
- const orderA = orderMap.get(a.object) ?? Number.MAX_SAFE_INTEGER;
639
- const orderB = orderMap.get(b.object) ?? Number.MAX_SAFE_INTEGER;
640
- return orderA - orderB;
641
- });
642
- }
643
- buildReferenceMap(graph) {
644
- const map = /* @__PURE__ */ new Map();
645
- for (const node of graph.nodes) {
646
- if (node.references.length > 0) {
647
- map.set(node.object, node.references);
648
- }
649
- }
650
- return map;
651
- }
652
- async loadExistingRecords(objectName, externalId, organizationId) {
653
- const map = /* @__PURE__ */ new Map();
654
- try {
655
- const findArgs = {
656
- fields: ["id", externalId],
657
- context: { isSystem: true }
658
- };
659
- if (organizationId) findArgs.where = { organization_id: organizationId };
660
- const records = await this.engine.find(objectName, findArgs);
661
- for (const record of records || []) {
662
- const key = String(record[externalId] ?? "");
663
- if (key) {
664
- map.set(key, record);
665
- }
666
- }
667
- } catch {
668
- }
669
- return map;
670
- }
671
- looksLikeInternalId(value) {
672
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
673
- return true;
674
- }
675
- if (/^[0-9a-f]{24}$/i.test(value)) {
676
- return true;
677
- }
678
- return false;
679
- }
680
- extractId(record) {
681
- if (!record) return void 0;
682
- return String(record.id || record._id || "");
683
- }
684
- buildEmptyResult(config, durationMs) {
685
- return {
686
- success: true,
687
- dryRun: config.dryRun,
688
- dependencyGraph: { nodes: [], insertOrder: [], circularDependencies: [] },
689
- results: [],
690
- errors: [],
691
- summary: {
692
- objectsProcessed: 0,
693
- totalRecords: 0,
694
- totalInserted: 0,
695
- totalUpdated: 0,
696
- totalSkipped: 0,
697
- totalErrored: 0,
698
- totalReferencesResolved: 0,
699
- totalReferencesDeferred: 0,
700
- circularDependencyCount: 0,
701
- durationMs
702
- }
703
- };
704
- }
705
- buildResult(config, graph, results, errors, durationMs) {
706
- const summary = {
707
- objectsProcessed: results.length,
708
- totalRecords: results.reduce((sum, r) => sum + r.total, 0),
709
- totalInserted: results.reduce((sum, r) => sum + r.inserted, 0),
710
- totalUpdated: results.reduce((sum, r) => sum + r.updated, 0),
711
- totalSkipped: results.reduce((sum, r) => sum + r.skipped, 0),
712
- totalErrored: results.reduce((sum, r) => sum + r.errored, 0),
713
- totalReferencesResolved: results.reduce((sum, r) => sum + r.referencesResolved, 0),
714
- totalReferencesDeferred: results.reduce((sum, r) => sum + r.referencesDeferred, 0),
715
- circularDependencyCount: graph.circularDependencies.length,
716
- durationMs
717
- };
718
- const hasErrors = errors.length > 0 || summary.totalErrored > 0;
719
- return {
720
- success: !hasErrors,
721
- dryRun: config.dryRun,
722
- dependencyGraph: graph,
723
- results,
724
- errors,
725
- summary
726
- };
727
- }
728
- };
729
- // ==========================================================================
730
- // Internal: Write Operations
731
- // ==========================================================================
732
- /**
733
- * Seed writes always run as a privileged system context. This bypasses
734
- * RBAC checks (so seeds can target system tables like `sys_*`) and
735
- * disables the SecurityPlugin's auto-injection of `organization_id` /
736
- * `owner_id` — seeds either declare those fields explicitly per
737
- * record, or are intentionally cross-tenant / global.
738
- */
739
- _SeedLoaderService.SEED_OPTIONS = { context: { isSystem: true } };
740
- SeedLoaderService = _SeedLoaderService;
741
150
  }
742
151
  });
743
152
 
@@ -972,7 +381,7 @@ var init_quickjs_runner = __esm({
972
381
  evalRes.value.dispose();
973
382
  let pumps = 0;
974
383
  while (pumps < 1e3) {
975
- await new Promise((resolve2) => setImmediate(resolve2));
384
+ await new Promise((resolve) => setImmediate(resolve));
976
385
  const pending = runtime.executePendingJobs();
977
386
  if (pending.error) {
978
387
  const err = vm.dump(pending.error);
@@ -1751,8 +1160,8 @@ var init_app_plugin = __esm({
1751
1160
  }
1752
1161
  })();
1753
1162
  let timer;
1754
- const budget = new Promise((resolve2) => {
1755
- timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
1163
+ const budget = new Promise((resolve) => {
1164
+ timer = setTimeout(() => resolve("budget"), seedBudgetMs);
1756
1165
  });
1757
1166
  const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
1758
1167
  if (timer) clearTimeout(timer);
@@ -2644,7 +2053,13 @@ function randomUUID() {
2644
2053
  });
2645
2054
  }
2646
2055
  var _HttpDispatcher = class _HttpDispatcher {
2647
- constructor(kernel, envRegistry, options) {
2056
+ /**
2057
+ * @param _envRegistryIgnored — RETIRED (ADR-0006 Phase 5). Environment
2058
+ * resolution moved behind the host's {@link KernelResolver}; the
2059
+ * positional parameter is kept so existing 3-arg callers keep compiling,
2060
+ * but its value is ignored.
2061
+ */
2062
+ constructor(kernel, _envRegistryIgnored, options) {
2648
2063
  /**
2649
2064
  * In-memory cache of positive membership checks, keyed by
2650
2065
  * `${environmentId}:${userId}`. Entries expire 60 seconds after insertion
@@ -2661,9 +2076,8 @@ var _HttpDispatcher = class _HttpDispatcher {
2661
2076
  return void 0;
2662
2077
  }
2663
2078
  };
2664
- this.envRegistry = envRegistry ?? resolveService("env-registry");
2665
2079
  this.enforceMembership = options?.enforceProjectMembership ?? true;
2666
- this.kernelManager = options?.kernelManager ?? resolveService("kernel-manager");
2080
+ this.kernelResolver = options?.kernelResolver ?? resolveService("kernel-resolver");
2667
2081
  this.scopeManager = options?.scopeManager ?? resolveService("scope-manager");
2668
2082
  }
2669
2083
  resolveDefaultProject() {
@@ -3051,130 +2465,20 @@ var _HttpDispatcher = class _HttpDispatcher {
3051
2465
  return candidate;
3052
2466
  }
3053
2467
  /**
3054
- * Resolve environment context for incoming request.
2468
+ * Attach the dispatcher's parsing hints for the host's
2469
+ * {@link KernelResolver} (ADR-0006 Phase 5).
3055
2470
  *
3056
- * Precedence:
3057
- * 0. URL path matches `/environments/:environmentId/...` OR request.params.environmentId set by router
3058
- * envRegistry.resolveById(id)
3059
- * 1. request.headers.host envRegistry.resolveByHostname(host)
3060
- * 2. request.headers['x-environment-id'] envRegistry.resolveById(id)
3061
- * 3. session.activeEnvironmentId envRegistry.resolveById(id)
3062
- * 4. session.activeOrganizationId → find default project → envRegistry.resolveById(id)
3063
- * 5. single-environment default (registered by `createSingleEnvironmentPlugin`)
3064
- * → envRegistry.resolveById(defaultProject.environmentId). Lets bare
3065
- * `/api/v1/data/...` URLs resolve to the lone project in
3066
- * `cloudUrl: 'local'` deployments.
3067
- *
3068
- * Skip for paths: /auth, /cloud, /health, /discovery (NOT /meta when scoped,
3069
- * so project-scoped meta routes can resolve their project).
2471
+ * Environment RESOLUTION (hostname / x-environment-id / session /
2472
+ * org-default / single-env-default environment + driver) is owned by
2473
+ * the host's resolver — the dispatcher no longer touches an environment
2474
+ * registry. What stays here is pure URL parsing (the dispatcher's own
2475
+ * routing convention): the scoped-path environment-id candidate and the
2476
+ * cleaned route path, both UNVALIDATED.
3070
2477
  */
3071
- async resolveEnvironmentContext(context, path) {
3072
- const skipPaths = ["/cloud", "/health", "/discovery"];
3073
- if (skipPaths.some((p) => path.startsWith(p))) {
3074
- return;
3075
- }
3076
- if (!this.envRegistry) {
3077
- return;
3078
- }
3079
- const headers = context.request?.headers;
3080
- const getHeader = (name) => {
3081
- if (!headers) return void 0;
3082
- const h = headers;
3083
- if (typeof h.get === "function") {
3084
- const v = h.get(name);
3085
- return v == null ? void 0 : String(v);
3086
- }
3087
- const lower = name.toLowerCase();
3088
- for (const k of Object.keys(h)) {
3089
- if (k.toLowerCase() === lower) {
3090
- const v = h[k];
3091
- return Array.isArray(v) ? v[0] : v == null ? void 0 : String(v);
3092
- }
3093
- }
3094
- return void 0;
3095
- };
3096
- try {
3097
- const urlEnvironmentId = this.extractEnvironmentIdFromPath(path) ?? context.request?.params?.environmentId;
3098
- if (urlEnvironmentId) {
3099
- const driver = await this.envRegistry.resolveById(urlEnvironmentId);
3100
- if (driver) {
3101
- context.environmentId = urlEnvironmentId;
3102
- context.dataDriver = driver;
3103
- return;
3104
- }
3105
- }
3106
- const host = getHeader("host");
3107
- if (host) {
3108
- const hostname = host.split(":")[0];
3109
- const result = await this.envRegistry.resolveByHostname(hostname);
3110
- if (result) {
3111
- context.environmentId = result.environmentId;
3112
- context.dataDriver = result.driver;
3113
- return;
3114
- }
3115
- }
3116
- const envIdHeader = getHeader("x-environment-id");
3117
- if (envIdHeader) {
3118
- const driver = await this.envRegistry.resolveById(envIdHeader);
3119
- if (driver) {
3120
- context.environmentId = envIdHeader;
3121
- context.dataDriver = driver;
3122
- return;
3123
- }
3124
- }
3125
- try {
3126
- const authService = await this.getService(CoreServiceName.enum.auth);
3127
- const sessionData = await authService?.api?.getSession?.({
3128
- headers: context.request?.headers
3129
- });
3130
- const activeEnvironmentId = sessionData?.session?.activeEnvironmentId ?? sessionData?.session?.activeEnvironmentId;
3131
- if (activeEnvironmentId) {
3132
- const driver = await this.envRegistry.resolveById(activeEnvironmentId);
3133
- if (driver) {
3134
- context.environmentId = activeEnvironmentId;
3135
- context.dataDriver = driver;
3136
- return;
3137
- }
3138
- }
3139
- const activeOrganizationId = sessionData?.session?.activeOrganizationId;
3140
- if (activeOrganizationId) {
3141
- const qlService = await this.getObjectQLService();
3142
- const ql = qlService ?? await this.resolveService("objectql");
3143
- if (ql) {
3144
- let rows = await ql.find("sys_environment", {
3145
- where: {
3146
- organization_id: activeOrganizationId,
3147
- is_default: true
3148
- },
3149
- limit: 1
3150
- });
3151
- if (rows && rows.value) rows = rows.value;
3152
- if (Array.isArray(rows) && rows[0]) {
3153
- const defaultEnv = rows[0];
3154
- const driver = await this.envRegistry.resolveById(defaultEnv.id);
3155
- if (driver) {
3156
- context.environmentId = defaultEnv.id;
3157
- context.dataDriver = driver;
3158
- return;
3159
- }
3160
- }
3161
- }
3162
- }
3163
- } catch (sessionError) {
3164
- console.debug("[HttpDispatcher] Session resolution failed:", sessionError);
3165
- }
3166
- if (this.defaultProject?.environmentId || this.resolveDefaultProject()) {
3167
- const def = this.defaultProject;
3168
- const driver = await this.envRegistry.resolveById(def.environmentId);
3169
- if (driver) {
3170
- context.environmentId = def.environmentId;
3171
- context.dataDriver = driver;
3172
- return;
3173
- }
3174
- }
3175
- } catch (error) {
3176
- console.error("[HttpDispatcher] Environment resolution failed:", error);
3177
- }
2478
+ prepareResolverHints(context, path) {
2479
+ context.routePath = path;
2480
+ const urlEnvironmentId = this.extractEnvironmentIdFromPath(path) ?? context.request?.params?.environmentId;
2481
+ if (urlEnvironmentId) context.urlEnvironmentId = String(urlEnvironmentId);
3178
2482
  }
3179
2483
  /**
3180
2484
  * Check whether the authenticated user is a member of
@@ -3697,7 +3001,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3697
3001
  if (!objectName) {
3698
3002
  return { handled: true, response: this.error("Object name required", 400) };
3699
3003
  }
3700
- if (!_context.dataDriver && this.envRegistry) {
3004
+ if (!_context.dataDriver && this.kernelResolver) {
3701
3005
  return {
3702
3006
  handled: true,
3703
3007
  response: this.error("Project not resolved. Please specify X-Environment-Id header or ensure hostname maps to a project.", 428)
@@ -3976,17 +3280,19 @@ var _HttpDispatcher = class _HttpDispatcher {
3976
3280
  ...organizationId ? { organizationId } : {},
3977
3281
  ...body?.actor ? { actor: body.actor } : {}
3978
3282
  });
3979
- try {
3980
- const seedNames = (result?.published ?? []).filter((p) => p?.type === "seed").map((p) => p.name);
3981
- if (seedNames.length > 0) {
3982
- result.seedApplied = await this.applyPublishedSeeds(
3983
- seedNames,
3984
- organizationId,
3985
- _context
3986
- );
3283
+ if (result?.seedApplied === void 0) {
3284
+ try {
3285
+ const seedNames = (result?.published ?? []).filter((p) => p?.type === "seed").map((p) => p.name);
3286
+ if (seedNames.length > 0) {
3287
+ result.seedApplied = await this.applyPublishedSeeds(
3288
+ seedNames,
3289
+ organizationId,
3290
+ _context
3291
+ );
3292
+ }
3293
+ } catch (e) {
3294
+ result.seedApplied = { success: false, error: e?.message ?? "seed apply failed" };
3987
3295
  }
3988
- } catch (e) {
3989
- result.seedApplied = { success: false, error: e?.message ?? "seed apply failed" };
3990
3296
  }
3991
3297
  return { handled: true, response: this.success(result) };
3992
3298
  } catch (e) {
@@ -4596,9 +3902,9 @@ var _HttpDispatcher = class _HttpDispatcher {
4596
3902
  if (def?.environmentId) _context.environmentId = def.environmentId;
4597
3903
  }
4598
3904
  let projectQl = null;
4599
- if (this.kernelManager && _context.environmentId && _context.environmentId !== "platform") {
3905
+ if (this.kernelResolver && _context.environmentId && _context.environmentId !== "platform") {
4600
3906
  try {
4601
- const projectKernel = await this.kernelManager.getOrCreate(_context.environmentId);
3907
+ const projectKernel = await this.kernelResolver.resolveKernel(_context, this.defaultKernel);
4602
3908
  if (projectKernel) {
4603
3909
  this.kernel = projectKernel;
4604
3910
  if (typeof projectKernel.getServiceAsync === "function") {
@@ -4771,9 +4077,9 @@ var _HttpDispatcher = class _HttpDispatcher {
4771
4077
  */
4772
4078
  async dispatch(method, path, body, query, context, prefix) {
4773
4079
  let cleanPath = path.replace(/\/$/, "");
4774
- await this.resolveEnvironmentContext(context, cleanPath);
4775
- if (this.kernelManager && context.environmentId && context.environmentId !== "platform") {
4776
- this.kernel = await this.kernelManager.getOrCreate(context.environmentId);
4080
+ this.prepareResolverHints(context, cleanPath);
4081
+ if (this.kernelResolver) {
4082
+ this.kernel = await this.kernelResolver.resolveKernel(context, this.defaultKernel) ?? this.defaultKernel;
4777
4083
  } else {
4778
4084
  this.kernel = this.defaultKernel;
4779
4085
  }
@@ -6209,1088 +5515,6 @@ var MiddlewareManager = class {
6209
5515
  // src/index.ts
6210
5516
  init_load_artifact_bundle();
6211
5517
 
6212
- // src/cloud/cloud-url.ts
6213
- var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
6214
- function resolveCloudUrl(explicit) {
6215
- const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
6216
- const lower = raw.toLowerCase();
6217
- if (lower === "off" || lower === "none" || lower === "local" || lower === "disabled") {
6218
- return "";
6219
- }
6220
- const picked = raw || DEFAULT_CLOUD_URL;
6221
- return picked.replace(/\/+$/, "");
6222
- }
6223
-
6224
- // src/cloud/marketplace-public-url.ts
6225
- function resolveMarketplacePublicBaseUrl(explicit) {
6226
- const raw = (explicit ?? process.env.OS_MARKETPLACE_PUBLIC_BASE_URL ?? "").trim();
6227
- const lower = raw.toLowerCase();
6228
- if (!raw || lower === "off" || lower === "none" || lower === "disabled" || lower === "false") {
6229
- return "";
6230
- }
6231
- return raw.replace(/\/+$/, "");
6232
- }
6233
- function publicMarketplaceKeyForApiPath(pathname) {
6234
- const prefix = "/api/v1/marketplace/packages";
6235
- if (pathname === prefix) return "packages.json";
6236
- if (!pathname.startsWith(`${prefix}/`)) return null;
6237
- const tail = pathname.slice(prefix.length + 1);
6238
- if (!tail) return null;
6239
- const parts = tail.split("/");
6240
- if (parts.length === 1) {
6241
- const id = decodeURIComponent(parts[0] ?? "");
6242
- if (!id) return null;
6243
- return `packages/${encodeURIComponent(id)}.json`;
6244
- }
6245
- if (parts.length === 4 && parts[1] === "versions" && parts[3] === "manifest") {
6246
- const id = decodeURIComponent(parts[0] ?? "");
6247
- const versionId = decodeURIComponent(parts[2] ?? "");
6248
- if (!id || !versionId) return null;
6249
- return `packages/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}/manifest.json`;
6250
- }
6251
- return null;
6252
- }
6253
-
6254
- // src/cloud/marketplace-proxy-plugin.ts
6255
- var MARKETPLACE_PREFIX = "/api/v1/marketplace";
6256
- var DEFAULT_LRU_MAX = 200;
6257
- var LIST_TTL_MS = 30 * 60 * 1e3;
6258
- var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
6259
- var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
6260
- function ttlForPath(pathname) {
6261
- if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
6262
- if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
6263
- return LIST_TTL_MS;
6264
- }
6265
- var LruTtlCache = class {
6266
- constructor(max) {
6267
- this.max = max;
6268
- this.map = /* @__PURE__ */ new Map();
6269
- }
6270
- get(key) {
6271
- const entry = this.map.get(key);
6272
- if (!entry) return void 0;
6273
- this.map.delete(key);
6274
- this.map.set(key, entry);
6275
- return entry;
6276
- }
6277
- set(key, entry) {
6278
- if (this.map.has(key)) this.map.delete(key);
6279
- this.map.set(key, entry);
6280
- while (this.map.size > this.max) {
6281
- const oldest = this.map.keys().next().value;
6282
- if (oldest === void 0) break;
6283
- this.map.delete(oldest);
6284
- }
6285
- }
6286
- clear() {
6287
- this.map.clear();
6288
- }
6289
- };
6290
- var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
6291
- constructor(config = {}) {
6292
- this.name = "com.objectstack.runtime.marketplace-proxy";
6293
- this.version = "1.1.0";
6294
- this.init = async (_ctx) => {
6295
- };
6296
- this.start = async (ctx) => {
6297
- ctx.hook("kernel:ready", async () => {
6298
- let httpServer;
6299
- try {
6300
- httpServer = ctx.getService("http-server");
6301
- } catch {
6302
- ctx.logger?.warn?.("[MarketplaceProxyPlugin] http-server not available \u2014 marketplace routes not mounted");
6303
- return;
6304
- }
6305
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
6306
- ctx.logger?.warn?.("[MarketplaceProxyPlugin] http-server missing getRawApp() \u2014 marketplace routes not mounted");
6307
- return;
6308
- }
6309
- const rawApp = httpServer.getRawApp();
6310
- const cloudUrl = this.cloudUrl;
6311
- const publicBaseUrl = this.publicBaseUrl;
6312
- const cache = this.cache;
6313
- if (publicBaseUrl) {
6314
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] public R2 fast-path enabled \u2192 ${publicBaseUrl}`);
6315
- }
6316
- const handler = async (c, next) => {
6317
- if (!cloudUrl) {
6318
- return c.json({
6319
- success: false,
6320
- error: {
6321
- code: "marketplace_unavailable",
6322
- message: "No control-plane URL configured for this runtime (OS_CLOUD_URL)."
6323
- }
6324
- }, 503);
6325
- }
6326
- try {
6327
- const incomingUrl = new URL(c.req.url);
6328
- if (incomingUrl.pathname.startsWith(`${MARKETPLACE_PREFIX}/install-local`)) {
6329
- return next();
6330
- }
6331
- const method = String(c.req.method ?? "GET").toUpperCase();
6332
- if (publicBaseUrl && (method === "GET" || method === "HEAD")) {
6333
- const r2Resp = await tryPublicMarketplaceFetch(
6334
- publicBaseUrl,
6335
- incomingUrl,
6336
- method,
6337
- c.req.header("accept"),
6338
- ctx.logger
6339
- );
6340
- if (r2Resp) return r2Resp;
6341
- }
6342
- const target = `${cloudUrl}${incomingUrl.pathname}${incomingUrl.search}`;
6343
- if (method !== "GET" && method !== "HEAD") {
6344
- return next();
6345
- }
6346
- const accept = c.req.header("accept") ?? "application/json";
6347
- const acceptLang = c.req.header("accept-language") ?? "";
6348
- const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
6349
- const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
6350
- const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
6351
- const now = Date.now();
6352
- if (cache && !bypass) {
6353
- const hit = cache.get(cacheKey);
6354
- if (hit && hit.expiresAt > now) {
6355
- return buildCachedResponse(hit, method, "HIT");
6356
- }
6357
- if (hit) {
6358
- const revalHeaders = {
6359
- "Accept": accept,
6360
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
6361
- };
6362
- if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
6363
- if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
6364
- if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
6365
- const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
6366
- if (revalResp.status === 304) {
6367
- hit.expiresAt = now + hit.ttlMs;
6368
- const newEtag = revalResp.headers.get("etag");
6369
- const newLm = revalResp.headers.get("last-modified");
6370
- if (newEtag) hit.etag = newEtag;
6371
- if (newLm) hit.lastModified = newLm;
6372
- cache.set(cacheKey, hit);
6373
- return buildCachedResponse(hit, method, "REVALIDATED");
6374
- }
6375
- return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
6376
- }
6377
- }
6378
- const reqHeaders = {
6379
- // Strip the inbound Host header — fetch will set
6380
- // it to the cloud host. Forward only the
6381
- // identifying headers cloud might log.
6382
- "Accept": accept,
6383
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
6384
- };
6385
- if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
6386
- const resp = await fetch(target, { method: "GET", headers: reqHeaders });
6387
- if (bypass || !cache) {
6388
- return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
6389
- }
6390
- return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
6391
- } catch (err) {
6392
- const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
6393
- ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
6394
- return c.json({
6395
- success: false,
6396
- error: {
6397
- code: "marketplace_proxy_failed",
6398
- message: err?.message ?? String(err)
6399
- }
6400
- }, 502);
6401
- }
6402
- };
6403
- if (typeof rawApp.all === "function") {
6404
- rawApp.all(`${MARKETPLACE_PREFIX}/*`, handler);
6405
- } else {
6406
- for (const m of ["get", "head"]) {
6407
- try {
6408
- rawApp[m]?.(`${MARKETPLACE_PREFIX}/*`, handler);
6409
- } catch {
6410
- }
6411
- }
6412
- }
6413
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
6414
- });
6415
- };
6416
- this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
6417
- this.publicBaseUrl = resolveMarketplacePublicBaseUrl(config.publicMarketplaceBaseUrl);
6418
- const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
6419
- const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
6420
- const disabled = config.cacheDisabled ?? envDisabled;
6421
- this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
6422
- }
6423
- };
6424
- async function tryPublicMarketplaceFetch(publicBaseUrl, incomingUrl, method, acceptHeader, logger) {
6425
- const key = publicMarketplaceKeyForApiPath(incomingUrl.pathname);
6426
- if (!key) return null;
6427
- const target = `${publicBaseUrl}/${key}`;
6428
- let resp;
6429
- try {
6430
- resp = await fetch(target, {
6431
- method: "GET",
6432
- headers: {
6433
- "Accept": acceptHeader || "application/json",
6434
- "User-Agent": `objectos-marketplace-proxy/public-r2`
6435
- }
6436
- });
6437
- } catch (err) {
6438
- logger?.warn?.(`[MarketplaceProxyPlugin] public R2 fetch failed (${target}): ${err?.message ?? err}`);
6439
- return null;
6440
- }
6441
- if (resp.status === 404) return null;
6442
- if (!resp.ok) {
6443
- logger?.warn?.(`[MarketplaceProxyPlugin] public R2 ${target} returned ${resp.status} \u2014 falling back to cloud`);
6444
- return null;
6445
- }
6446
- const isList = key === "packages.json";
6447
- const hasFilters = isList && (incomingUrl.searchParams.has("q") || incomingUrl.searchParams.has("category") || incomingUrl.searchParams.has("limit") || incomingUrl.searchParams.has("offset"));
6448
- if (!hasFilters) {
6449
- const headers2 = new Headers();
6450
- const ct = resp.headers.get("content-type") ?? "application/json; charset=utf-8";
6451
- headers2.set("content-type", ct);
6452
- const cc = resp.headers.get("cache-control");
6453
- if (cc) headers2.set("cache-control", cc);
6454
- const etag = resp.headers.get("etag");
6455
- if (etag) headers2.set("etag", etag);
6456
- headers2.set("x-cache", "PUBLIC-R2");
6457
- const body2 = method === "HEAD" ? null : resp.body;
6458
- return new Response(body2, { status: 200, headers: headers2 });
6459
- }
6460
- let snapshot;
6461
- try {
6462
- snapshot = await resp.json();
6463
- } catch (err) {
6464
- logger?.warn?.(`[MarketplaceProxyPlugin] public R2 list snapshot parse failed: ${err?.message ?? err}`);
6465
- return null;
6466
- }
6467
- const items = Array.isArray(snapshot?.data?.items) ? snapshot.data.items : [];
6468
- const q = (incomingUrl.searchParams.get("q") ?? "").trim().toLowerCase();
6469
- const category = (incomingUrl.searchParams.get("category") ?? "").trim();
6470
- const limit = Math.min(Math.max(Number(incomingUrl.searchParams.get("limit") ?? 50), 1), 100);
6471
- const offset = Math.max(Number(incomingUrl.searchParams.get("offset") ?? 0), 0);
6472
- let filtered = items;
6473
- if (q) {
6474
- filtered = filtered.filter((r) => {
6475
- const dn = String(r?.display_name ?? "").toLowerCase();
6476
- const mid = String(r?.manifest_id ?? "").toLowerCase();
6477
- return dn.includes(q) || mid.includes(q);
6478
- });
6479
- }
6480
- if (category) {
6481
- filtered = filtered.filter((r) => String(r?.category ?? "") === category);
6482
- }
6483
- const total = filtered.length;
6484
- const page = filtered.slice(offset, offset + limit);
6485
- const body = JSON.stringify({ success: true, data: { items: page, total, limit, offset } });
6486
- const headers = new Headers({
6487
- "content-type": "application/json; charset=utf-8",
6488
- "cache-control": "public, max-age=30",
6489
- "x-cache": "PUBLIC-R2-FILTERED"
6490
- });
6491
- return new Response(method === "HEAD" ? null : body, { status: 200, headers });
6492
- }
6493
- var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
6494
- function collectHeaders(src) {
6495
- const out = {};
6496
- for (const h of PASSTHROUGH_HEADERS) {
6497
- const v = src.headers.get(h);
6498
- if (v) out[h] = v;
6499
- }
6500
- return out;
6501
- }
6502
- function buildCachedResponse(entry, method, xCache) {
6503
- const headers = new Headers(entry.headers);
6504
- headers.set("X-Cache", xCache);
6505
- const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
6506
- headers.set("Age", String(Math.max(0, ageSec)));
6507
- const body = method === "HEAD" ? null : entry.body;
6508
- return new Response(body, { status: entry.status, headers });
6509
- }
6510
- async function passthroughResponse(resp, method, xCache) {
6511
- const headers = new Headers(collectHeaders(resp));
6512
- headers.set("X-Cache", xCache);
6513
- if (method === "HEAD") {
6514
- try {
6515
- await resp.arrayBuffer();
6516
- } catch {
6517
- }
6518
- return new Response(null, { status: resp.status, headers });
6519
- }
6520
- const body = await resp.arrayBuffer();
6521
- return new Response(body, { status: resp.status, headers });
6522
- }
6523
- async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
6524
- const body = await resp.arrayBuffer();
6525
- const headers = collectHeaders(resp);
6526
- if (resp.status >= 200 && resp.status < 300) {
6527
- const ttlMs = ttlForPath(pathname);
6528
- const entry = {
6529
- status: resp.status,
6530
- body,
6531
- headers,
6532
- etag: resp.headers.get("etag") ?? void 0,
6533
- lastModified: resp.headers.get("last-modified") ?? void 0,
6534
- expiresAt: Date.now() + ttlMs,
6535
- ttlMs
6536
- };
6537
- cache.set(key, entry);
6538
- }
6539
- const respHeaders = new Headers(headers);
6540
- respHeaders.set("X-Cache", "MISS");
6541
- const outBody = method === "HEAD" ? null : body;
6542
- return new Response(outBody, { status: resp.status, headers: respHeaders });
6543
- }
6544
-
6545
- // src/cloud/marketplace-install-local-plugin.ts
6546
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
6547
- import { join as join2, resolve } from "path";
6548
- import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
6549
- var ROUTE_BASE = "/api/v1/marketplace/install-local";
6550
- var DEFAULT_DIR = ".objectstack/installed-packages";
6551
- function safeFilename(manifestId) {
6552
- return manifestId.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json";
6553
- }
6554
- var MarketplaceInstallLocalPlugin = class {
6555
- constructor(config = {}) {
6556
- this.name = "com.objectstack.runtime.marketplace-install-local";
6557
- this.version = "1.0.0";
6558
- this.init = async (_ctx) => {
6559
- };
6560
- this.start = async (ctx) => {
6561
- ctx.hook("kernel:ready", async () => {
6562
- await this.rehydrate(ctx);
6563
- let httpServer;
6564
- try {
6565
- httpServer = ctx.getService("http-server");
6566
- } catch {
6567
- ctx.logger?.warn?.("[MarketplaceInstallLocal] http-server not available \u2014 install endpoints not mounted");
6568
- return;
6569
- }
6570
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
6571
- ctx.logger?.warn?.("[MarketplaceInstallLocal] http-server missing getRawApp() \u2014 install endpoints not mounted");
6572
- return;
6573
- }
6574
- const rawApp = httpServer.getRawApp();
6575
- const postHandler = async (c) => this.handleInstall(c, ctx);
6576
- const getHandler = async (c) => this.handleList(c);
6577
- const deleteHandler = async (c) => this.handleUninstall(c, ctx);
6578
- const reseedHandler = async (c) => this.handleReseed(c, ctx);
6579
- const purgeHandler = async (c) => this.handlePurge(c, ctx);
6580
- if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
6581
- if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
6582
- if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
6583
- if (typeof rawApp.post === "function") {
6584
- rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
6585
- rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
6586
- }
6587
- ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
6588
- });
6589
- };
6590
- /**
6591
- * Re-register every cached manifest with the kernel's manifest service.
6592
- * Safe to call on a kernel that already has the same manifest_id (the
6593
- * underlying ObjectQL registry overwrites by id, but we still warn so
6594
- * a developer can spot the dev-time clash between their config.ts and
6595
- * a marketplace package).
6596
- */
6597
- this.rehydrate = async (ctx) => {
6598
- const entries = this.readAll();
6599
- if (entries.length === 0) return;
6600
- let manifestService = null;
6601
- try {
6602
- manifestService = ctx.getService("manifest");
6603
- } catch {
6604
- ctx.logger?.warn?.("[MarketplaceInstallLocal] no `manifest` service \u2014 rehydrate skipped");
6605
- return;
6606
- }
6607
- for (const entry of entries) {
6608
- try {
6609
- manifestService.register(entry.manifest);
6610
- try {
6611
- const ql = ctx.getService("objectql");
6612
- if (ql && typeof ql.syncSchemas === "function") await ql.syncSchemas();
6613
- } catch {
6614
- }
6615
- await this.applySideEffects(ctx, entry.manifest, { seedNow: false });
6616
- ctx.logger?.info?.(`[MarketplaceInstallLocal] rehydrated ${entry.manifestId}@${entry.version}`);
6617
- } catch (err) {
6618
- ctx.logger?.error?.(`[MarketplaceInstallLocal] rehydrate failed for ${entry.manifestId}`, err instanceof Error ? err : new Error(String(err)));
6619
- }
6620
- }
6621
- };
6622
- this.handleInstall = async (c, ctx) => {
6623
- const userId = await this.requireAuthenticatedUser(c, ctx);
6624
- if (!userId) {
6625
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required to install packages." } }, 401);
6626
- }
6627
- let body = {};
6628
- try {
6629
- body = await c.req.json();
6630
- } catch {
6631
- }
6632
- const inlineManifest = body?.manifest && typeof body.manifest === "object" ? body.manifest : null;
6633
- let manifest;
6634
- let resolvedVersionId;
6635
- let version;
6636
- let packageId;
6637
- if (inlineManifest) {
6638
- manifest = inlineManifest;
6639
- packageId = String(manifest.id ?? manifest.name ?? "").trim();
6640
- version = String(manifest.version ?? "unknown");
6641
- resolvedVersionId = String(body?.versionId ?? version);
6642
- if (!packageId) {
6643
- return c.json({ success: false, error: { code: "invalid_manifest", message: 'Inline manifest must have an "id" or "name".' } }, 400);
6644
- }
6645
- } else {
6646
- if (!this.cloudUrl) {
6647
- return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
6648
- }
6649
- packageId = String(body?.packageId ?? "").trim();
6650
- const versionId = String(body?.versionId ?? "latest").trim() || "latest";
6651
- if (!packageId) {
6652
- return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
6653
- }
6654
- let payload;
6655
- const publicBase = resolveMarketplacePublicBaseUrl();
6656
- const fetchAttempts = [];
6657
- if (publicBase) {
6658
- fetchAttempts.push({
6659
- label: "public-r2",
6660
- url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
6661
- });
6662
- }
6663
- fetchAttempts.push({
6664
- label: "cloud",
6665
- url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
6666
- });
6667
- let lastErrStatus = 0;
6668
- let lastErrText = "";
6669
- for (const attempt of fetchAttempts) {
6670
- try {
6671
- const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
6672
- if (!resp.ok) {
6673
- lastErrStatus = resp.status;
6674
- lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
6675
- if (attempt.label === "public-r2" && resp.status === 404) {
6676
- ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
6677
- continue;
6678
- }
6679
- if (attempt.label === "public-r2" && resp.status >= 500) {
6680
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
6681
- continue;
6682
- }
6683
- break;
6684
- }
6685
- payload = await resp.json();
6686
- lastErrStatus = 0;
6687
- break;
6688
- } catch (err) {
6689
- if (attempt.label === "public-r2") {
6690
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
6691
- continue;
6692
- }
6693
- return c.json({
6694
- success: false,
6695
- error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
6696
- }, 502);
6697
- }
6698
- }
6699
- if (!payload) {
6700
- return c.json({
6701
- success: false,
6702
- error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
6703
- }, lastErrStatus === 404 ? 404 : 502);
6704
- }
6705
- const data = payload?.data ?? payload;
6706
- manifest = data?.manifest;
6707
- resolvedVersionId = String(data?.version_id ?? versionId);
6708
- version = String(data?.version ?? "unknown");
6709
- }
6710
- const manifestId = String(manifest?.id ?? manifest?.name ?? "");
6711
- if (!manifest || !manifestId) {
6712
- return c.json({ success: false, error: { code: "invalid_manifest", message: "Invalid manifest payload." } }, inlineManifest ? 400 : 502);
6713
- }
6714
- const conflict = this.findConflict(ctx, manifestId);
6715
- if (conflict === "user-code") {
6716
- return c.json({
6717
- success: false,
6718
- error: {
6719
- code: "manifest_conflict",
6720
- message: `manifest_id "${manifestId}" is already defined by this runtime's local code. Refusing to overwrite. Uninstall the local definition first.`
6721
- }
6722
- }, 409);
6723
- }
6724
- try {
6725
- const manifestService = ctx.getService("manifest");
6726
- manifestService.register(manifest);
6727
- } catch (err) {
6728
- if (inlineManifest) {
6729
- return c.json({
6730
- success: false,
6731
- error: { code: "register_failed", message: `Failed to register imported manifest: ${err?.message ?? err}` }
6732
- }, 422);
6733
- }
6734
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
6735
- }
6736
- const entry = {
6737
- packageId,
6738
- versionId: resolvedVersionId,
6739
- manifestId,
6740
- version,
6741
- manifest,
6742
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
6743
- installedBy: userId,
6744
- withSampleData: false
6745
- };
6746
- try {
6747
- mkdirSync4(this.storageDir, { recursive: true });
6748
- writeFileSync3(join2(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
6749
- } catch (err) {
6750
- return c.json({
6751
- success: false,
6752
- error: { code: "storage_failed", message: `Failed to persist manifest: ${err?.message ?? err}` }
6753
- }, 500);
6754
- }
6755
- try {
6756
- const ql = ctx.getService("objectql");
6757
- if (ql && typeof ql.syncSchemas === "function") {
6758
- await ql.syncSchemas();
6759
- ctx.logger?.info?.(`[MarketplaceInstallLocal] syncSchemas() ran after registering ${manifestId}`);
6760
- }
6761
- } catch (err) {
6762
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
6763
- }
6764
- const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
6765
- if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
6766
- entry.withSampleData = true;
6767
- try {
6768
- writeFileSync3(join2(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
6769
- } catch {
6770
- }
6771
- }
6772
- return c.json({
6773
- success: true,
6774
- data: {
6775
- manifestId,
6776
- version,
6777
- versionId: resolvedVersionId,
6778
- installedAt: entry.installedAt,
6779
- hotLoaded: true,
6780
- upgradedFrom: conflict === "marketplace" ? "previous-marketplace-version" : null,
6781
- translationsLoaded: seededSummary.translationsLoaded,
6782
- seeded: seededSummary.seeded,
6783
- note: "App is now available in this runtime. Refresh the console to see it in the app switcher."
6784
- }
6785
- }, 200);
6786
- };
6787
- this.handleList = async (c) => {
6788
- const entries = this.readAll();
6789
- return c.json({
6790
- success: true,
6791
- data: {
6792
- items: entries.map((e) => ({
6793
- packageId: e.packageId,
6794
- versionId: e.versionId,
6795
- manifestId: e.manifestId,
6796
- version: e.version,
6797
- installedAt: e.installedAt,
6798
- installedBy: e.installedBy,
6799
- withSampleData: e.withSampleData ?? false
6800
- })),
6801
- total: entries.length,
6802
- storageDir: this.storageDir
6803
- }
6804
- }, 200);
6805
- };
6806
- this.handleUninstall = async (c, ctx) => {
6807
- const userId = await this.requireAuthenticatedUser(c, ctx);
6808
- if (!userId) {
6809
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
6810
- }
6811
- const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
6812
- if (!manifestId) {
6813
- return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
6814
- }
6815
- const file = join2(this.storageDir, safeFilename(manifestId));
6816
- if (!existsSync3(file)) {
6817
- return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
6818
- }
6819
- try {
6820
- unlinkSync(file);
6821
- } catch (err) {
6822
- return c.json({ success: false, error: { code: "storage_failed", message: err?.message ?? String(err) } }, 500);
6823
- }
6824
- ctx.logger?.info?.(`[MarketplaceInstallLocal] uninstalled ${manifestId} (cached manifest removed; restart runtime to unload from running kernel)`);
6825
- return c.json({
6826
- success: true,
6827
- data: {
6828
- manifestId,
6829
- note: "Cached manifest removed. The app remains loaded in the running kernel until the next restart (the kernel API does not support unregistering apps in-place)."
6830
- }
6831
- }, 200);
6832
- };
6833
- /**
6834
- * Detect whether `manifestId` is already known to the kernel and classify
6835
- * the source so we can refuse vs upgrade gracefully.
6836
- *
6837
- * 'none' — fresh install
6838
- * 'marketplace' — previously installed by this plugin (allow upgrade)
6839
- * 'user-code' — defined by AppPlugin from objectstack.config.ts
6840
- * (refuse to avoid silently overwriting authored code)
6841
- */
6842
- this.findConflict = (ctx, manifestId) => {
6843
- if (existsSync3(join2(this.storageDir, safeFilename(manifestId)))) {
6844
- return "marketplace";
6845
- }
6846
- try {
6847
- const ql = ctx.getService("objectql");
6848
- const packages = ql?.registry?.getAllPackages?.() ?? [];
6849
- const hit = packages.find(
6850
- (p) => (p?.manifest?.id ?? p?.id ?? p?.manifest?.name) === manifestId
6851
- );
6852
- if (hit) return "user-code";
6853
- } catch {
6854
- }
6855
- return "none";
6856
- };
6857
- /**
6858
- * Pull a userId out of the request's better-auth session, if any.
6859
- * Returns null when there is no signed-in user. v1 does not check
6860
- * admin role — UI gating + the auth requirement is sufficient for
6861
- * dev / single-tenant runtimes. Stricter checks can be layered on
6862
- * via a middleware in cloud-hosted multi-tenant deployments.
6863
- */
6864
- /**
6865
- * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
6866
- *
6867
- * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
6868
- * Idempotent (upsert by id). Useful when:
6869
- * • The user installed an app and skipped sample data
6870
- * • A purge was undone
6871
- * • The user wants a clean baseline back after editing demo rows
6872
- *
6873
- * Multi-tenant: requires an active organization on the session (same
6874
- * rule as install seed path).
6875
- */
6876
- this.handleReseed = async (c, ctx) => {
6877
- const userId = await this.requireAuthenticatedUser(c, ctx);
6878
- if (!userId) {
6879
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
6880
- }
6881
- const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
6882
- if (!manifestId) {
6883
- return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
6884
- }
6885
- const file = join2(this.storageDir, safeFilename(manifestId));
6886
- if (!existsSync3(file)) {
6887
- return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
6888
- }
6889
- let entry;
6890
- try {
6891
- entry = JSON.parse(readFileSync2(file, "utf8"));
6892
- } catch (err) {
6893
- return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
6894
- }
6895
- const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
6896
- if (summary.seeded.mode === "skipped") {
6897
- return c.json({
6898
- success: false,
6899
- error: {
6900
- code: "reseed_skipped",
6901
- message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
6902
- }
6903
- }, 400);
6904
- }
6905
- try {
6906
- entry.withSampleData = true;
6907
- writeFileSync3(file, JSON.stringify(entry, null, 2), "utf8");
6908
- } catch {
6909
- }
6910
- return c.json({
6911
- success: true,
6912
- data: {
6913
- manifestId,
6914
- inserted: summary.seeded.inserted ?? 0,
6915
- updated: summary.seeded.updated ?? 0,
6916
- errors: summary.seeded.errors ?? 0,
6917
- withSampleData: true
6918
- }
6919
- }, 200);
6920
- };
6921
- /**
6922
- * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
6923
- *
6924
- * Deletes every record whose id is declared in the cached manifest's
6925
- * seed datasets. Uses the `driver` service directly to bypass ACL /
6926
- * lifecycle hooks (same pattern as cloud purge). User-created records
6927
- * are never touched — only ids declared in the package's bundled
6928
- * datasets are removed. Already-deleted rows count as `skipped`.
6929
- */
6930
- this.handlePurge = async (c, ctx) => {
6931
- const userId = await this.requireAuthenticatedUser(c, ctx);
6932
- if (!userId) {
6933
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
6934
- }
6935
- const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
6936
- if (!manifestId) {
6937
- return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
6938
- }
6939
- const file = join2(this.storageDir, safeFilename(manifestId));
6940
- if (!existsSync3(file)) {
6941
- return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
6942
- }
6943
- let entry;
6944
- try {
6945
- entry = JSON.parse(readFileSync2(file, "utf8"));
6946
- } catch (err) {
6947
- return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
6948
- }
6949
- const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
6950
- if (datasets.length === 0) {
6951
- return c.json({
6952
- success: false,
6953
- error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
6954
- }, 400);
6955
- }
6956
- let driver;
6957
- try {
6958
- driver = ctx.getService("driver");
6959
- } catch {
6960
- }
6961
- if (!driver || typeof driver.delete !== "function") {
6962
- return c.json({
6963
- success: false,
6964
- error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
6965
- }, 500);
6966
- }
6967
- let deleted = 0;
6968
- let skipped = 0;
6969
- let errors = 0;
6970
- for (const ds of datasets) {
6971
- const object = String(ds.object);
6972
- for (const rec of ds.records) {
6973
- const id = rec?.id;
6974
- if (id === void 0 || id === null || id === "") {
6975
- skipped++;
6976
- continue;
6977
- }
6978
- try {
6979
- const r = await driver.delete(object, id);
6980
- if (r === false || r === 0 || r?.deleted === 0) skipped++;
6981
- else deleted++;
6982
- } catch (err) {
6983
- const msg = String(err?.message ?? err);
6984
- if (/not.?found|no row/i.test(msg)) skipped++;
6985
- else {
6986
- errors++;
6987
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
6988
- }
6989
- }
6990
- }
6991
- }
6992
- try {
6993
- entry.withSampleData = false;
6994
- writeFileSync3(file, JSON.stringify(entry, null, 2), "utf8");
6995
- } catch {
6996
- }
6997
- ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
6998
- return c.json({
6999
- success: true,
7000
- data: { manifestId, deleted, skipped, errors, withSampleData: false }
7001
- }, 200);
7002
- };
7003
- /**
7004
- * Replicate the start-time side-effects that AppPlugin runs for
7005
- * statically-declared apps but the `manifest` service does NOT:
7006
- *
7007
- * 1. Load `manifest.translations` (array of `Record<locale, data>`)
7008
- * into the i18n service — auto-creating an in-memory fallback if
7009
- * none is registered, matching AppPlugin's behaviour.
7010
- *
7011
- * 2. Merge `manifest.data` (an array of seed datasets) into the
7012
- * kernel's `seed-datasets` service so SecurityPlugin's per-org
7013
- * replay middleware picks them up on every future
7014
- * sys_organization insert.
7015
- *
7016
- * 3. When `seedNow=true`, also run the seed immediately so the user
7017
- * sees demo data without having to create a new org:
7018
- * • single-tenant: run SeedLoaderService inline (mirrors
7019
- * AppPlugin single-tenant branch)
7020
- * • multi-tenant: invoke `seed-replayer` for the caller's
7021
- * active org (resolved from the request session)
7022
- *
7023
- * Errors are logged but never thrown — install succeeds even if
7024
- * post-register side-effects partially fail (the manifest itself is
7025
- * already registered + cached). Returns a small summary for the
7026
- * response envelope.
7027
- */
7028
- this.applySideEffects = async (ctx, manifest, opts) => {
7029
- const appId = String(manifest?.id ?? "unknown");
7030
- let translationsLoaded = 0;
7031
- let seedSummary = { mode: "skipped", reason: "no-datasets" };
7032
- try {
7033
- const bundles = [];
7034
- if (Array.isArray(manifest?.translations)) bundles.push(...manifest.translations);
7035
- if (Array.isArray(manifest?.i18n)) bundles.push(...manifest.i18n);
7036
- if (bundles.length > 0) {
7037
- let i18nService;
7038
- try {
7039
- i18nService = ctx.getService("i18n");
7040
- } catch {
7041
- }
7042
- if (!i18nService) {
7043
- try {
7044
- const mod = await import("@objectstack/core");
7045
- const createMemoryI18n = mod.createMemoryI18n;
7046
- if (typeof createMemoryI18n === "function") {
7047
- i18nService = createMemoryI18n();
7048
- ctx.registerService?.("i18n", i18nService);
7049
- ctx.logger?.info?.(`[MarketplaceInstallLocal] auto-registered in-memory i18n fallback for "${appId}"`);
7050
- }
7051
- } catch {
7052
- }
7053
- }
7054
- if (i18nService?.loadTranslations) {
7055
- for (const bundle of bundles) {
7056
- for (const [locale, data] of Object.entries(bundle)) {
7057
- if (data && typeof data === "object") {
7058
- try {
7059
- i18nService.loadTranslations(locale, data);
7060
- translationsLoaded++;
7061
- } catch (err) {
7062
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] failed to load ${appId} translations for ${locale}: ${err?.message ?? err}`);
7063
- }
7064
- }
7065
- }
7066
- }
7067
- ctx.logger?.info?.(`[MarketplaceInstallLocal] loaded ${translationsLoaded} locale bundle(s) for ${appId}`);
7068
- }
7069
- }
7070
- } catch (err) {
7071
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] i18n side-effect failed for ${appId}: ${err?.message ?? err}`);
7072
- }
7073
- const datasets = Array.isArray(manifest?.data) ? manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
7074
- if (datasets.length > 0) {
7075
- try {
7076
- const kernel = ctx.kernel;
7077
- let existing = [];
7078
- try {
7079
- const v = kernel?.getService?.("seed-datasets");
7080
- if (Array.isArray(v)) existing = v;
7081
- } catch {
7082
- }
7083
- const merged = [...existing, ...datasets];
7084
- if (kernel?.registerService) kernel.registerService("seed-datasets", merged);
7085
- else ctx.registerService?.("seed-datasets", merged);
7086
- ctx.logger?.info?.(`[MarketplaceInstallLocal] merged ${datasets.length} seed dataset(s) into kernel (total: ${merged.length})`);
7087
- } catch (err) {
7088
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] failed to merge seed-datasets: ${err?.message ?? err}`);
7089
- }
7090
- }
7091
- if (opts.seedNow && datasets.length > 0) {
7092
- const multiTenant = String(readEnvWithDeprecation3("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
7093
- try {
7094
- const ql = ctx.getService("objectql");
7095
- let metadata;
7096
- try {
7097
- metadata = ctx.getService("metadata");
7098
- } catch {
7099
- }
7100
- if (!ql || !metadata) {
7101
- seedSummary = { mode: "skipped", reason: "objectql-or-metadata-missing" };
7102
- } else {
7103
- let organizationId;
7104
- if (multiTenant) {
7105
- const resolved = await this.resolveActiveOrgId(opts.c, ctx);
7106
- if (resolved) organizationId = resolved;
7107
- else {
7108
- seedSummary = { mode: "skipped", reason: "multi-tenant-no-active-org" };
7109
- ctx.logger?.warn?.("[MarketplaceInstallLocal] multi-tenant: no active org on request \u2014 data not seeded");
7110
- }
7111
- }
7112
- if (!multiTenant || organizationId) {
7113
- const [{ SeedLoaderService: SeedLoaderService2 }, { SeedLoaderRequestSchema }] = await Promise.all([
7114
- Promise.resolve().then(() => (init_seed_loader(), seed_loader_exports)),
7115
- import("@objectstack/spec/data")
7116
- ]);
7117
- const seedLoader = new SeedLoaderService2(ql, metadata, ctx.logger);
7118
- const request = SeedLoaderRequestSchema.parse({
7119
- datasets,
7120
- config: {
7121
- defaultMode: "upsert",
7122
- multiPass: true,
7123
- ...organizationId ? { organizationId } : {}
7124
- }
7125
- });
7126
- const result = await seedLoader.load(request);
7127
- seedSummary = {
7128
- mode: "inline",
7129
- inserted: result.summary.totalInserted,
7130
- updated: result.summary.totalUpdated,
7131
- errors: result.errors.length
7132
- };
7133
- ctx.logger?.info?.(`[MarketplaceInstallLocal] inline seed for ${appId}${organizationId ? ` (org=${organizationId})` : ""}: inserted=${seedSummary.inserted} updated=${seedSummary.updated} errors=${seedSummary.errors}`);
7134
- }
7135
- }
7136
- } catch (err) {
7137
- seedSummary = { mode: "skipped", reason: `seed-error: ${err?.message ?? err}` };
7138
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] seed run failed for ${appId}: ${err?.message ?? err}`);
7139
- }
7140
- }
7141
- return { translationsLoaded, seeded: seedSummary };
7142
- };
7143
- /**
7144
- * Best-effort active-org resolution. Reads the better-auth session
7145
- * (same path as requireAuthenticatedUser) and returns
7146
- * `session.activeOrganizationId`, falling back to the user's first
7147
- * org membership.
7148
- */
7149
- this.resolveActiveOrgId = async (c, ctx) => {
7150
- if (!c?.req?.raw?.headers) return null;
7151
- try {
7152
- const authService = ctx.getService("auth");
7153
- let api = authService?.api;
7154
- if (!api && typeof authService?.getApi === "function") api = await authService.getApi();
7155
- if (!api?.getSession) return null;
7156
- const session = await api.getSession({ headers: c.req.raw.headers });
7157
- const direct = session?.session?.activeOrganizationId ?? session?.activeOrganizationId ?? null;
7158
- if (direct) return String(direct);
7159
- const userId = session?.user?.id;
7160
- if (!userId) return null;
7161
- try {
7162
- const ql = ctx.getService("objectql");
7163
- if (ql?.find) {
7164
- const rows = await ql.find("sys_organization_member", { where: { user_id: userId }, limit: 1, context: { isSystem: true } });
7165
- const row = Array.isArray(rows) ? rows[0] : rows?.items?.[0] ?? null;
7166
- return row?.organization_id ? String(row.organization_id) : null;
7167
- }
7168
- } catch {
7169
- }
7170
- } catch {
7171
- }
7172
- return null;
7173
- };
7174
- this.requireAuthenticatedUser = async (c, ctx) => {
7175
- try {
7176
- const authService = ctx.getService("auth");
7177
- let api = authService?.api;
7178
- if (!api && typeof authService?.getApi === "function") {
7179
- api = await authService.getApi();
7180
- }
7181
- if (api?.getSession && c?.req?.raw?.headers) {
7182
- const session = await api.getSession({ headers: c.req.raw.headers });
7183
- const userId = session?.user?.id ?? null;
7184
- if (userId) return String(userId);
7185
- }
7186
- } catch {
7187
- }
7188
- const xUserId = c?.req?.header?.("x-user-id");
7189
- if (xUserId) return String(xUserId);
7190
- return null;
7191
- };
7192
- this.readAll = () => {
7193
- if (!existsSync3(this.storageDir)) return [];
7194
- const out = [];
7195
- for (const name of readdirSync(this.storageDir)) {
7196
- if (!name.endsWith(".json")) continue;
7197
- try {
7198
- const raw = readFileSync2(join2(this.storageDir, name), "utf8");
7199
- out.push(JSON.parse(raw));
7200
- } catch {
7201
- }
7202
- }
7203
- return out;
7204
- };
7205
- this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
7206
- this.storageDir = config.storageDir ? resolve(config.storageDir) : resolve(process.cwd(), DEFAULT_DIR);
7207
- }
7208
- };
7209
-
7210
- // src/cloud/runtime-config-plugin.ts
7211
- var RuntimeConfigPlugin = class {
7212
- constructor(config = {}) {
7213
- this.name = "com.objectstack.runtime.runtime-config";
7214
- this.version = "1.0.0";
7215
- this.init = async (_ctx) => {
7216
- };
7217
- this.start = async (ctx) => {
7218
- ctx.hook("kernel:ready", async () => {
7219
- let httpServer;
7220
- try {
7221
- httpServer = ctx.getService("http-server");
7222
- } catch {
7223
- ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server not available \u2014 runtime/config not mounted");
7224
- return;
7225
- }
7226
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
7227
- ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server missing getRawApp() \u2014 runtime/config not mounted");
7228
- return;
7229
- }
7230
- const rawApp = httpServer.getRawApp();
7231
- const features = {
7232
- installLocal: this.installLocal,
7233
- marketplace: true,
7234
- aiStudio: this.aiStudio
7235
- };
7236
- let envRegistry = null;
7237
- try {
7238
- envRegistry = ctx.getService("env-registry");
7239
- } catch {
7240
- }
7241
- const handler = async (c) => {
7242
- const rawHost = c.req.header("host") ?? "";
7243
- const host = rawHost.split(":")[0].toLowerCase().trim();
7244
- let defaultEnvironmentId;
7245
- let defaultOrgId;
7246
- let resolvedSingleEnv = this.singleEnvironment;
7247
- const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
7248
- if (resolveFn && host) {
7249
- try {
7250
- const resolved = await resolveFn(host);
7251
- if (resolved?.environmentId) {
7252
- defaultEnvironmentId = String(resolved.environmentId);
7253
- const orgId = resolved.organizationId ?? resolved.organization_id;
7254
- if (orgId) defaultOrgId = String(orgId);
7255
- resolvedSingleEnv = true;
7256
- }
7257
- } catch {
7258
- }
7259
- }
7260
- return c.json({
7261
- cloudUrl: this.cloudUrl,
7262
- singleEnvironment: resolvedSingleEnv,
7263
- defaultOrgId,
7264
- defaultEnvironmentId,
7265
- features,
7266
- branding: {
7267
- productName: this.productName,
7268
- productShortName: this.productShortName
7269
- }
7270
- });
7271
- };
7272
- rawApp.get("/api/v1/runtime/config", handler);
7273
- rawApp.get("/api/v1/studio/runtime-config", handler);
7274
- ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
7275
- cloudUrl: this.cloudUrl || "(empty)",
7276
- installLocal: this.installLocal,
7277
- perHostEnvResolution: !!envRegistry
7278
- });
7279
- });
7280
- };
7281
- this.destroy = async () => {
7282
- };
7283
- this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
7284
- this.installLocal = !!config.installLocal;
7285
- this.aiStudio = config.aiStudio !== false;
7286
- this.singleEnvironment = !!config.singleEnvironment;
7287
- const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
7288
- const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
7289
- this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
7290
- this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
7291
- }
7292
- };
7293
-
7294
5518
  // src/sandbox/script-runner.ts
7295
5519
  var UnimplementedScriptRunner = class {
7296
5520
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -7321,10 +5545,9 @@ import {
7321
5545
  createRestApiPlugin
7322
5546
  } from "@objectstack/rest";
7323
5547
  export * from "@objectstack/core";
7324
- import { readEnvWithDeprecation as readEnvWithDeprecation4, _resetEnvDeprecationWarnings } from "@objectstack/types";
5548
+ import { readEnvWithDeprecation as readEnvWithDeprecation3, _resetEnvDeprecationWarnings } from "@objectstack/types";
7325
5549
  export {
7326
5550
  AppPlugin,
7327
- DEFAULT_CLOUD_URL,
7328
5551
  DEFAULT_RATE_LIMITS,
7329
5552
  DriverPlugin,
7330
5553
  ExternalValidationPlugin,
@@ -7332,8 +5555,6 @@ export {
7332
5555
  HttpServer,
7333
5556
  InMemoryErrorReporter,
7334
5557
  InMemoryMetricsRegistry,
7335
- MarketplaceInstallLocalPlugin,
7336
- MarketplaceProxyPlugin,
7337
5558
  MiddlewareManager,
7338
5559
  NoopErrorReporter,
7339
5560
  NoopMetricsRegistry,
@@ -7348,7 +5569,6 @@ export {
7348
5569
  RouteGroupBuilder,
7349
5570
  RouteManager,
7350
5571
  Runtime,
7351
- RuntimeConfigPlugin,
7352
5572
  SYSTEM_ENVIRONMENT_ID,
7353
5573
  SandboxError,
7354
5574
  SeedLoaderService,
@@ -7374,8 +5594,7 @@ export {
7374
5594
  mergeRuntimeModule,
7375
5595
  parseTraceparent,
7376
5596
  readArtifactSource,
7377
- readEnvWithDeprecation4 as readEnvWithDeprecation,
7378
- resolveCloudUrl,
5597
+ readEnvWithDeprecation3 as readEnvWithDeprecation,
7379
5598
  resolveDefaultArtifactPath,
7380
5599
  resolveErrorReporter,
7381
5600
  resolveMetrics,