@x12i/catalox 2.7.0 → 3.1.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.
Files changed (137) hide show
  1. package/README.md +73 -31
  2. package/dist/src/catalox/authorization.js +1 -1
  3. package/dist/src/catalox/authorization.js.map +1 -1
  4. package/dist/src/catalox/catalog-lifecycle.d.ts +29 -0
  5. package/dist/src/catalox/catalog-lifecycle.d.ts.map +1 -0
  6. package/dist/src/catalox/catalog-lifecycle.js +480 -0
  7. package/dist/src/catalox/catalog-lifecycle.js.map +1 -0
  8. package/dist/src/catalox/catalox-bound.d.ts +135 -0
  9. package/dist/src/catalox/catalox-bound.d.ts.map +1 -0
  10. package/dist/src/catalox/catalox-bound.js +190 -0
  11. package/dist/src/catalox/catalox-bound.js.map +1 -0
  12. package/dist/src/catalox/catalox.d.ts +34 -4
  13. package/dist/src/catalox/catalox.d.ts.map +1 -1
  14. package/dist/src/catalox/catalox.js +508 -72
  15. package/dist/src/catalox/catalox.js.map +1 -1
  16. package/dist/src/catalox/context.js +2 -2
  17. package/dist/src/catalox/context.js.map +1 -1
  18. package/dist/src/catalox/create-catalox.d.ts +28 -0
  19. package/dist/src/catalox/create-catalox.d.ts.map +1 -0
  20. package/dist/src/catalox/create-catalox.js +90 -0
  21. package/dist/src/catalox/create-catalox.js.map +1 -0
  22. package/dist/src/catalox/index.d.ts +4 -0
  23. package/dist/src/catalox/index.d.ts.map +1 -1
  24. package/dist/src/catalox/index.js +4 -0
  25. package/dist/src/catalox/index.js.map +1 -1
  26. package/dist/src/catalox/native-catalog-merge.d.ts +12 -0
  27. package/dist/src/catalox/native-catalog-merge.d.ts.map +1 -0
  28. package/dist/src/catalox/native-catalog-merge.js +102 -0
  29. package/dist/src/catalox/native-catalog-merge.js.map +1 -0
  30. package/dist/src/catalox/native-scope.d.ts +28 -0
  31. package/dist/src/catalox/native-scope.d.ts.map +1 -0
  32. package/dist/src/catalox/native-scope.js +184 -0
  33. package/dist/src/catalox/native-scope.js.map +1 -0
  34. package/dist/src/catalox/record-history.d.ts +53 -0
  35. package/dist/src/catalox/record-history.d.ts.map +1 -0
  36. package/dist/src/catalox/record-history.js +158 -0
  37. package/dist/src/catalox/record-history.js.map +1 -0
  38. package/dist/src/cli/index.js +148 -42
  39. package/dist/src/cli/index.js.map +1 -1
  40. package/dist/src/contracts/apps.d.ts +2 -0
  41. package/dist/src/contracts/apps.d.ts.map +1 -1
  42. package/dist/src/contracts/catalog-lifecycle.d.ts +70 -0
  43. package/dist/src/contracts/catalog-lifecycle.d.ts.map +1 -0
  44. package/dist/src/contracts/catalog-lifecycle.js +2 -0
  45. package/dist/src/contracts/catalog-lifecycle.js.map +1 -0
  46. package/dist/src/contracts/catalogs.d.ts +54 -0
  47. package/dist/src/contracts/catalogs.d.ts.map +1 -1
  48. package/dist/src/contracts/catalogs.js.map +1 -1
  49. package/dist/src/contracts/context.d.ts +5 -1
  50. package/dist/src/contracts/context.d.ts.map +1 -1
  51. package/dist/src/contracts/descriptors.d.ts +6 -0
  52. package/dist/src/contracts/descriptors.d.ts.map +1 -1
  53. package/dist/src/contracts/index.d.ts +5 -2
  54. package/dist/src/contracts/index.d.ts.map +1 -1
  55. package/dist/src/contracts/index.js.map +1 -1
  56. package/dist/src/contracts/items.d.ts +19 -0
  57. package/dist/src/contracts/items.d.ts.map +1 -1
  58. package/dist/src/contracts/record-history.d.ts +66 -0
  59. package/dist/src/contracts/record-history.d.ts.map +1 -0
  60. package/dist/src/contracts/record-history.js +2 -0
  61. package/dist/src/contracts/record-history.js.map +1 -0
  62. package/dist/src/embedder.d.ts +17 -0
  63. package/dist/src/embedder.d.ts.map +1 -0
  64. package/dist/src/embedder.js +17 -0
  65. package/dist/src/embedder.js.map +1 -0
  66. package/dist/src/firebase/adapter-store.d.ts +1 -0
  67. package/dist/src/firebase/adapter-store.d.ts.map +1 -1
  68. package/dist/src/firebase/adapter-store.js +3 -0
  69. package/dist/src/firebase/adapter-store.js.map +1 -1
  70. package/dist/src/firebase/binding-store.d.ts +2 -0
  71. package/dist/src/firebase/binding-store.d.ts.map +1 -1
  72. package/dist/src/firebase/binding-store.js +10 -0
  73. package/dist/src/firebase/binding-store.js.map +1 -1
  74. package/dist/src/firebase/catalog-data-index-store.d.ts +1 -0
  75. package/dist/src/firebase/catalog-data-index-store.d.ts.map +1 -1
  76. package/dist/src/firebase/catalog-data-index-store.js +3 -0
  77. package/dist/src/firebase/catalog-data-index-store.js.map +1 -1
  78. package/dist/src/firebase/catalog-item-history-store.d.ts +21 -0
  79. package/dist/src/firebase/catalog-item-history-store.d.ts.map +1 -0
  80. package/dist/src/firebase/catalog-item-history-store.js +61 -0
  81. package/dist/src/firebase/catalog-item-history-store.js.map +1 -0
  82. package/dist/src/firebase/catalog-store.d.ts +1 -0
  83. package/dist/src/firebase/catalog-store.d.ts.map +1 -1
  84. package/dist/src/firebase/catalog-store.js +3 -0
  85. package/dist/src/firebase/catalog-store.js.map +1 -1
  86. package/dist/src/firebase/index.d.ts +1 -0
  87. package/dist/src/firebase/index.d.ts.map +1 -1
  88. package/dist/src/firebase/index.js +1 -0
  89. package/dist/src/firebase/index.js.map +1 -1
  90. package/dist/src/firebase/mapping-store.d.ts +1 -0
  91. package/dist/src/firebase/mapping-store.d.ts.map +1 -1
  92. package/dist/src/firebase/mapping-store.js +3 -0
  93. package/dist/src/firebase/mapping-store.js.map +1 -1
  94. package/dist/src/firebase/native-item-store.d.ts +8 -2
  95. package/dist/src/firebase/native-item-store.d.ts.map +1 -1
  96. package/dist/src/firebase/native-item-store.js +22 -6
  97. package/dist/src/firebase/native-item-store.js.map +1 -1
  98. package/dist/src/firebase/reference-store.d.ts +3 -0
  99. package/dist/src/firebase/reference-store.d.ts.map +1 -1
  100. package/dist/src/firebase/reference-store.js +16 -0
  101. package/dist/src/firebase/reference-store.js.map +1 -1
  102. package/dist/src/firebase/renderer-snippet-store.d.ts +3 -0
  103. package/dist/src/firebase/renderer-snippet-store.d.ts.map +1 -1
  104. package/dist/src/firebase/renderer-snippet-store.js +17 -0
  105. package/dist/src/firebase/renderer-snippet-store.js.map +1 -1
  106. package/dist/src/firebase/snapshot-store.d.ts +1 -0
  107. package/dist/src/firebase/snapshot-store.d.ts.map +1 -1
  108. package/dist/src/firebase/snapshot-store.js +8 -0
  109. package/dist/src/firebase/snapshot-store.js.map +1 -1
  110. package/dist/src/index.d.ts +2 -15
  111. package/dist/src/index.d.ts.map +1 -1
  112. package/dist/src/index.js +2 -15
  113. package/dist/src/index.js.map +1 -1
  114. package/dist/src/operator.d.ts +24 -0
  115. package/dist/src/operator.d.ts.map +1 -0
  116. package/dist/src/operator.js +25 -0
  117. package/dist/src/operator.js.map +1 -0
  118. package/dist/test/integration/firestore.emulator.test.js +16 -29
  119. package/dist/test/integration/firestore.emulator.test.js.map +1 -1
  120. package/dist/test/integration/record-history.live.test.d.ts +2 -0
  121. package/dist/test/integration/record-history.live.test.d.ts.map +1 -0
  122. package/dist/test/integration/record-history.live.test.js +126 -0
  123. package/dist/test/integration/record-history.live.test.js.map +1 -0
  124. package/dist/test/unit/native-catalog-merge.test.d.ts +2 -0
  125. package/dist/test/unit/native-catalog-merge.test.d.ts.map +1 -0
  126. package/dist/test/unit/native-catalog-merge.test.js +33 -0
  127. package/dist/test/unit/native-catalog-merge.test.js.map +1 -0
  128. package/dist/test/unit/native-scope.test.d.ts +2 -0
  129. package/dist/test/unit/native-scope.test.d.ts.map +1 -0
  130. package/dist/test/unit/native-scope.test.js +29 -0
  131. package/dist/test/unit/native-scope.test.js.map +1 -0
  132. package/dist/test/unit/record-history-path.test.d.ts +2 -0
  133. package/dist/test/unit/record-history-path.test.d.ts.map +1 -0
  134. package/dist/test/unit/record-history-path.test.js +24 -0
  135. package/dist/test/unit/record-history-path.test.js.map +1 -0
  136. package/firestore.indexes.json +39 -0
  137. package/package.json +23 -6
@@ -6,6 +6,8 @@ import { ApiCatalogAdapter } from "../adapters/api/api-adapter.js";
6
6
  import { MongoCatalogAdapter } from "../adapters/mongo/mongo-adapter.js";
7
7
  import { AuthorizationService } from "./authorization.js";
8
8
  import { resolveCatalogItemId } from "./identity.js";
9
+ import { assertSuperAdminForNonGlobalScope, encodeNativeItemStorageDocId, indexedScopeFields, isGlobalPhysicalRow, normalizeStoredScope, parseWriteScopeInput, scopeFromRecordField, } from "./native-scope.js";
10
+ import { filterPhysicalForTenantFetch, mergeNativePhysicalRows, pickWinningPhysicalRow, } from "./native-catalog-merge.js";
9
11
  import { parseJson, toJson } from "./json-io.js";
10
12
  import { createHash, randomUUID } from "node:crypto";
11
13
  import { renderInventoryReportMarkdown } from "./reporting/render-inventory-report.js";
@@ -19,7 +21,7 @@ import { DefinitionStore } from "../firebase/definition-store.js";
19
21
  import { DescriptorStore } from "../firebase/descriptor-store.js";
20
22
  import { FirestoreStore } from "../firebase/firestore-store.js";
21
23
  import { MappingStore } from "../firebase/mapping-store.js";
22
- import { NativeItemStore } from "../firebase/native-item-store.js";
24
+ import { NativeItemStore, storageDocIdForNativeRecord } from "../firebase/native-item-store.js";
23
25
  import { ReferenceStore } from "../firebase/reference-store.js";
24
26
  import { RendererSnippetStore } from "../firebase/renderer-snippet-store.js";
25
27
  import { SnapshotStore } from "../firebase/snapshot-store.js";
@@ -33,11 +35,45 @@ import { migrateNativeCatalogLayout as runMigrateNativeCatalogLayout, } from "..
33
35
  import { reportNativeCatalogLayoutDiagnostics as runReportNativeCatalogLayoutDiagnostics, } from "./native-catalog-layout-diagnostics.js";
34
36
  import { exportAllFirestoreCollectionsToGcs, exportFirestoreCollectionToGcs, restoreAllFirestoreCollectionsFromGcsManifest, restoreFirestoreCollectionFromGcs, } from "./firestore-gcs-transfer.js";
35
37
  import { compareAllFirestoreCollectionsWithGcsManifest, compareFirestoreCollectionWithGcsNdjson, } from "./firestore-gcs-compare.js";
38
+ import { CataloxBound } from "./catalox-bound.js";
39
+ import { emitRecordHistoryEvent, readRecordHistoryEventPayload, } from "./record-history.js";
40
+ import { runDeleteCatalog, runRenameCatalog, runRestoreDeletedCatalog, } from "./catalog-lifecycle.js";
36
41
  export class Catalox {
37
42
  deps;
38
43
  constructor(deps) {
39
44
  this.deps = deps;
40
45
  }
46
+ catalogLifecycleDeps() {
47
+ return {
48
+ firestoreStore: this.deps.firestoreStore,
49
+ catalogs: this.deps.catalogs,
50
+ bindings: this.deps.bindings,
51
+ definitions: this.deps.definitions,
52
+ descriptors: this.deps.descriptors,
53
+ mappings: this.deps.mappings,
54
+ adapters: this.deps.adapters,
55
+ references: this.deps.references,
56
+ nativeItems: this.deps.nativeItems,
57
+ snapshots: this.deps.snapshots,
58
+ catalogDataIndex: this.deps.catalogDataIndex,
59
+ ...(this.deps.rendererSnippets ? { rendererSnippets: this.deps.rendererSnippets } : {}),
60
+ };
61
+ }
62
+ async emitNativeItemHistory(context, params) {
63
+ const rh = this.deps.recordHistory;
64
+ if (!rh)
65
+ return;
66
+ const res = await emitRecordHistoryEvent(rh, context, {
67
+ catalogId: params.catalogId,
68
+ itemId: params.itemId,
69
+ ...(params.storageDocId != null ? { storageDocId: params.storageDocId } : {}),
70
+ op: params.op,
71
+ ...(params.before !== undefined ? { before: params.before } : {}),
72
+ ...(params.after !== undefined ? { after: params.after } : {}),
73
+ });
74
+ if (!res.ok && rh.failClosed)
75
+ throw new Error(`recordHistory: ${res.error}`);
76
+ }
41
77
  resolveActorId(context) {
42
78
  return context.userId ?? context.actor?.id;
43
79
  }
@@ -194,9 +230,13 @@ export class Catalox {
194
230
  }
195
231
  async buildCatalogItemRenderMap(context, catalogId, itemId, options) {
196
232
  const descriptor = await this.getCatalogDescriptor(context, catalogId);
197
- const item = await this.getCatalogItem(context, catalogId, itemId);
198
- if (!item)
233
+ const got = await this.getCatalogItem(context, catalogId, itemId);
234
+ if (got.outcome === "mapping_blocked") {
235
+ throw new CatalogAdapterError({ catalogId, reason: "mapping_validation_failed", issues: got.issues });
236
+ }
237
+ if (got.outcome === "not_found")
199
238
  throw new CatalogNotFoundError({ catalogId, itemId });
239
+ const item = got.item;
200
240
  const sources = this.gatherDescriptorSources(descriptor);
201
241
  const resolvedSources = {};
202
242
  if (options?.resolveSources !== false) {
@@ -262,8 +302,83 @@ export class Catalox {
262
302
  return Object.keys(out).length ? out : undefined;
263
303
  }
264
304
  stripReservedWriteFields(input) {
265
- const { indexed, ...rest } = input;
266
- return { data: rest, ...(indexed != null ? { indexed: indexed } : {}) };
305
+ const { indexed, scope, ...rest } = input;
306
+ const parsedScope = scope != null ? parseWriteScopeInput(scope) : undefined;
307
+ return {
308
+ data: rest,
309
+ ...(indexed != null ? { indexed: indexed } : {}),
310
+ ...(parsedScope != null ? { scope: parsedScope } : {}),
311
+ };
312
+ }
313
+ normalizeListFetchScope(scope) {
314
+ if (!scope)
315
+ return undefined;
316
+ if (!scope.accountId && !scope.agentId && !scope.userId && scope.superAdmin !== true)
317
+ return undefined;
318
+ return scope;
319
+ }
320
+ buildAppliedScope(scope, effectiveSuperList) {
321
+ return {
322
+ ...(scope?.accountId != null ? { accountId: scope.accountId } : {}),
323
+ ...(scope?.agentId != null ? { agentId: scope.agentId } : {}),
324
+ ...(scope?.userId != null ? { userId: scope.userId } : {}),
325
+ superAdmin: effectiveSuperList,
326
+ };
327
+ }
328
+ async fetchGlobalPhysicalRows(catalogId, listOpts) {
329
+ const userFe = { ...(listOpts.filterEq ?? {}) };
330
+ const withGlobal = { ...userFe, scopeLayer: "global" };
331
+ let recs = await this.deps.nativeItems.list(catalogId, { ...listOpts, filterEq: withGlobal });
332
+ if (recs.length === 0 && Object.keys(userFe).length === 0) {
333
+ const broad = await this.deps.nativeItems.list(catalogId, { ...listOpts, filterEq: {} });
334
+ recs = broad.filter((r) => isGlobalPhysicalRow(r));
335
+ }
336
+ return recs;
337
+ }
338
+ async fetchTenantPhysicalRows(catalogId, fetch, listOpts) {
339
+ const userFe = { ...(listOpts.filterEq ?? {}) };
340
+ const cap = { ...listOpts, limit: 500, offset: 0 };
341
+ const seen = new Set();
342
+ const out = [];
343
+ const take = (rows) => {
344
+ for (const r of rows) {
345
+ const id = storageDocIdForNativeRecord(r);
346
+ if (seen.has(id))
347
+ continue;
348
+ seen.add(id);
349
+ out.push(r);
350
+ }
351
+ };
352
+ take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, scopeLayer: "global" } }));
353
+ if (fetch.accountId) {
354
+ take(await this.deps.nativeItems.list(catalogId, {
355
+ ...cap,
356
+ filterEq: { ...userFe, scopeLayer: "account", scopeAccountId: fetch.accountId },
357
+ }));
358
+ if (fetch.agentId) {
359
+ take(await this.deps.nativeItems.list(catalogId, {
360
+ ...cap,
361
+ filterEq: {
362
+ ...userFe,
363
+ scopeLayer: "agent",
364
+ scopeAccountId: fetch.accountId,
365
+ scopeAgentId: fetch.agentId,
366
+ },
367
+ }));
368
+ }
369
+ if (fetch.userId) {
370
+ take(await this.deps.nativeItems.list(catalogId, {
371
+ ...cap,
372
+ filterEq: {
373
+ ...userFe,
374
+ scopeLayer: "user",
375
+ scopeAccountId: fetch.accountId,
376
+ scopeUserId: fetch.userId,
377
+ },
378
+ }));
379
+ }
380
+ }
381
+ return filterPhysicalForTenantFetch(out, fetch);
267
382
  }
268
383
  async decorateItem(catalogId, item) {
269
384
  const descriptor = await this.deps.descriptors.get(catalogId);
@@ -356,26 +471,38 @@ export class Catalox {
356
471
  };
357
472
  })();
358
473
  if (catalog.sourceMode === "native") {
474
+ const descRec = await this.deps.descriptors.get(catalogId);
475
+ if (!descRec)
476
+ throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
359
477
  const filterEq = (listQueryOptions?.filter ?? {});
360
- const listOpts = {
478
+ const baseListOpts = {
361
479
  ...(listQueryOptions?.limit != null ? { limit: listQueryOptions.limit } : {}),
362
480
  ...(listQueryOptions?.offset != null ? { offset: listQueryOptions.offset } : {}),
363
481
  ...(Object.keys(filterEq).length ? { filterEq } : {}),
364
482
  ...(listQueryOptions?.sort ? { sort: listQueryOptions.sort } : {}),
365
483
  };
366
- const records = await this.deps.nativeItems.list(catalogId, listOpts);
367
- const items = records.map((r) => ({
368
- itemId: r.itemId,
369
- catalogId: r.catalogId,
370
- appId: context.appId,
371
- sourceMode: "native",
372
- sourceType: "firebase",
373
- data: r.data,
374
- ...(r.metadata != null ? { metadata: r.metadata } : {}),
375
- createdAt: r.createdAt,
376
- updatedAt: r.updatedAt,
377
- }));
378
- return { items };
484
+ const rawScope = this.normalizeListFetchScope(listQueryOptions?.scope);
485
+ const wantsSuperList = rawScope?.superAdmin === true && context.superAdmin === true;
486
+ let physical = [];
487
+ if (wantsSuperList) {
488
+ physical = await this.deps.nativeItems.list(catalogId, baseListOpts);
489
+ }
490
+ else if (!rawScope?.accountId && !rawScope?.agentId && !rawScope?.userId) {
491
+ physical = await this.fetchGlobalPhysicalRows(catalogId, baseListOpts);
492
+ }
493
+ else {
494
+ physical = await this.fetchTenantPhysicalRows(catalogId, rawScope, baseListOpts);
495
+ }
496
+ const merged = mergeNativePhysicalRows(physical, descRec.descriptor.identity, String(context.appId), wantsSuperList);
497
+ const off = listQueryOptions?.offset ?? 0;
498
+ const lim = listQueryOptions?.limit ?? 100;
499
+ const sliced = merged.slice(off, off + lim);
500
+ const decorated = await Promise.all(sliced.map((it) => this.decorateItem(catalogId, it)));
501
+ return {
502
+ listOutcome: "ok",
503
+ items: decorated,
504
+ appliedScope: this.buildAppliedScope(rawScope, wantsSuperList),
505
+ };
379
506
  }
380
507
  const def = await this.deps.definitions.get(catalogId);
381
508
  if (!def || def.type !== "mapped")
@@ -385,16 +512,24 @@ export class Catalox {
385
512
  throw new CatalogAdapterError({ catalogId, reason: "missing_mapping" });
386
513
  const mappingIssues = validateMappingSpec(mapping.mapping);
387
514
  if (mappingIssues.some((i) => i.severity === "error")) {
388
- return { items: [], issues: mappingIssues };
515
+ return { listOutcome: "mapping_blocked", items: [], issues: mappingIssues };
389
516
  }
517
+ const mappedListQueryOptions = listQueryOptions != null
518
+ ? (() => {
519
+ const { scope: _omitScope, ...rest } = listQueryOptions;
520
+ return rest;
521
+ })()
522
+ : undefined;
390
523
  if (def.adapterType === "mongo") {
391
524
  if (!this.deps.mongoAdapter)
392
525
  throw new CatalogAdapterError({ catalogId, reason: "mongo_adapter_unconfigured" });
393
526
  const adapterConfig = await this.deps.adapters.get(def.adapterId);
394
527
  if (!adapterConfig)
395
528
  throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
396
- const result = await this.deps.mongoAdapter.listItems(context, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, listQueryOptions);
397
- return result.issues ? { items: result.items, issues: result.issues } : { items: result.items };
529
+ const result = await this.deps.mongoAdapter.listItems(context, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
530
+ return result.issues?.length
531
+ ? { listOutcome: "ok", items: result.items, issues: result.issues }
532
+ : { listOutcome: "ok", items: result.items };
398
533
  }
399
534
  if (def.adapterType === "api") {
400
535
  if (!this.deps.apiAdapter)
@@ -402,8 +537,9 @@ export class Catalox {
402
537
  const adapterConfig = await this.deps.adapters.get(def.adapterId);
403
538
  if (!adapterConfig)
404
539
  throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
405
- const result = await this.deps.apiAdapter.listItems(context, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, listQueryOptions);
540
+ const result = await this.deps.apiAdapter.listItems(context, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
406
541
  return {
542
+ listOutcome: "ok",
407
543
  items: result.items,
408
544
  ...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
409
545
  ...(result.issues ? { issues: result.issues } : {}),
@@ -411,35 +547,66 @@ export class Catalox {
411
547
  }
412
548
  throw new CatalogAdapterError({ catalogId, reason: "unknown_adapter_type" });
413
549
  }
414
- async getCatalogItem(context, catalogId, itemId) {
550
+ async getCatalogItem(context, catalogId, itemId, options) {
415
551
  await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "read");
416
552
  const catalog = await this.deps.catalogs.get(catalogId);
417
553
  if (!catalog)
418
554
  throw new CatalogNotFoundError({ catalogId });
419
555
  if (catalog.sourceMode === "native") {
420
- const rec = await this.deps.nativeItems.get(catalogId, itemId);
421
- if (!rec)
422
- return null;
423
- const base = {
424
- itemId: rec.itemId,
425
- catalogId: rec.catalogId,
426
- appId: context.appId,
427
- sourceMode: "native",
428
- sourceType: "firebase",
429
- data: rec.data,
430
- ...(rec.metadata != null ? { metadata: rec.metadata } : {}),
431
- createdAt: rec.createdAt,
432
- updatedAt: rec.updatedAt,
433
- };
434
- return this.decorateItem(catalogId, base);
556
+ if (options?.storageDocId) {
557
+ const rec = await this.deps.nativeItems.get(catalogId, options.storageDocId);
558
+ if (!rec)
559
+ return { outcome: "not_found" };
560
+ const meta = {
561
+ ...(rec.metadata && typeof rec.metadata === "object" ? rec.metadata : {}),
562
+ scope: rec.scope ?? { kind: "global" },
563
+ };
564
+ const base = {
565
+ itemId: rec.itemId,
566
+ catalogId: rec.catalogId,
567
+ appId: context.appId,
568
+ sourceMode: "native",
569
+ sourceType: "firebase",
570
+ data: rec.data,
571
+ metadata: meta,
572
+ createdAt: rec.createdAt,
573
+ updatedAt: rec.updatedAt,
574
+ };
575
+ return { outcome: "found", item: await this.decorateItem(catalogId, base) };
576
+ }
577
+ const descRec = await this.deps.descriptors.get(catalogId);
578
+ if (!descRec)
579
+ throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
580
+ let rows = await this.deps.nativeItems.findByLogicalItemId(catalogId, itemId);
581
+ if (!rows.length) {
582
+ const legacy = await this.deps.nativeItems.get(catalogId, itemId);
583
+ if (legacy)
584
+ rows = [legacy];
585
+ }
586
+ if (!rows.length)
587
+ return { outcome: "not_found" };
588
+ const scope = options?.scope;
589
+ const filtered = scope?.accountId || scope?.agentId || scope?.userId
590
+ ? filterPhysicalForTenantFetch(rows, scope)
591
+ : rows.filter((r) => isGlobalPhysicalRow(r));
592
+ const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(context.appId), false);
593
+ const hit = merged.find((m) => m.itemId === itemId) ?? merged[0];
594
+ if (!hit)
595
+ return { outcome: "not_found" };
596
+ return { outcome: "found", item: await this.decorateItem(catalogId, hit) };
435
597
  }
436
598
  // For mapped catalogs, use list path with filter if adapter supports.
437
599
  const result = await this.listCatalogItems(context, catalogId, {
438
600
  limit: 1,
439
601
  filter: { itemId },
440
602
  });
603
+ if (result.listOutcome === "mapping_blocked") {
604
+ return { outcome: "mapping_blocked", issues: result.issues ?? [] };
605
+ }
441
606
  const first = result.items[0];
442
- return first ? this.decorateItem(catalogId, first) : null;
607
+ if (!first)
608
+ return { outcome: "not_found" };
609
+ return { outcome: "found", item: await this.decorateItem(catalogId, first) };
443
610
  }
444
611
  async validateCatalog(_context, _catalogId) {
445
612
  return { isValid: true, issues: [] };
@@ -619,8 +786,8 @@ export class Catalox {
619
786
  async bindAppToStore(context, input) {
620
787
  if (!this.deps.storeAppBindings)
621
788
  throw new Error("storeAppBindings dependency is not configured");
622
- if (!context.isGodMode && input.appId !== context.appId) {
623
- throw new CatalogAccessDeniedError({ reason: "not_god_mode" });
789
+ if (!context.superAdmin && input.appId !== context.appId) {
790
+ throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
624
791
  }
625
792
  const existing = await this.deps.storeAppBindings.findByStoreApp(input.storeId, input.appId);
626
793
  if (existing)
@@ -643,8 +810,8 @@ export class Catalox {
643
810
  async unbindAppFromStore(context, storeId, appId) {
644
811
  if (!this.deps.storeAppBindings)
645
812
  throw new Error("storeAppBindings dependency is not configured");
646
- if (!context.isGodMode && appId !== context.appId) {
647
- throw new CatalogAccessDeniedError({ reason: "not_god_mode" });
813
+ if (!context.superAdmin && appId !== context.appId) {
814
+ throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
648
815
  }
649
816
  const existing = await this.deps.storeAppBindings.findByStoreApp(storeId, appId);
650
817
  if (!existing)
@@ -660,9 +827,9 @@ export class Catalox {
660
827
  async listAppsForStore(context, storeId) {
661
828
  if (!this.deps.storeAppBindings)
662
829
  throw new Error("storeAppBindings dependency is not configured");
663
- // listing is allowed for any caller; enforce god-mode only if you want to hide cross-app membership.
830
+ // listing is allowed for any caller; super-admin sees all store memberships.
664
831
  const records = await this.deps.storeAppBindings.listAppsByStore(storeId);
665
- if (context.isGodMode)
832
+ if (context.superAdmin)
666
833
  return records;
667
834
  // non-god: only reveal memberships that include the caller's own appId
668
835
  return records.filter((r) => r.appId === context.appId);
@@ -683,9 +850,9 @@ export class Catalox {
683
850
  });
684
851
  }
685
852
  async ensureBinding(context, input) {
686
- // only god-mode apps can provision cross-app bindings.
687
- if (!context.isGodMode && input.appId !== context.appId) {
688
- throw new CatalogAccessDeniedError({ reason: "not_god_mode" });
853
+ // only super-admin apps can provision cross-app bindings.
854
+ if (!context.superAdmin && input.appId !== context.appId) {
855
+ throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
689
856
  }
690
857
  const existing = await this.deps.bindings.findByAppCatalog(input.appId, input.catalogId);
691
858
  if (existing)
@@ -708,51 +875,152 @@ export class Catalox {
708
875
  async createNativeCatalogItem(_context, _catalogId, _input) {
709
876
  return this.upsertNativeCatalogItem(_context, _catalogId, _input);
710
877
  }
711
- async updateNativeCatalogItem(_context, _catalogId, _itemId, _patch) {
878
+ async updateNativeCatalogItem(_context, _catalogId, _itemId, _patch, _options) {
712
879
  await this.deps.authz.requireBindingAccess(_context, _context.appId, _catalogId, "write");
713
- const existing = await this.deps.nativeItems.get(_catalogId, _itemId);
880
+ let existing = null;
881
+ if (_options?.storageDocId) {
882
+ existing = await this.deps.nativeItems.get(_catalogId, _options.storageDocId);
883
+ }
884
+ else {
885
+ let rows = await this.deps.nativeItems.findByLogicalItemId(_catalogId, _itemId);
886
+ if (!rows.length) {
887
+ const legacy = await this.deps.nativeItems.get(_catalogId, _itemId);
888
+ if (legacy)
889
+ rows = [legacy];
890
+ }
891
+ existing = pickWinningPhysicalRow(rows, _options?.scope);
892
+ }
714
893
  if (!existing)
715
894
  throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
895
+ const { data: patchData, indexed: patchIndexed, scope: patchScope } = this.stripReservedWriteFields(_patch);
896
+ let nextScope = normalizeStoredScope(scopeFromRecordField(existing.scope));
897
+ if (patchScope != null) {
898
+ nextScope = normalizeStoredScope(patchScope);
899
+ assertSuperAdminForNonGlobalScope(_context.superAdmin, nextScope);
900
+ }
716
901
  const updatedAt = new Date().toISOString();
717
- const merged = { ...(existing.data ?? {}), ..._patch };
718
- const actorId = this.resolveActorId(_context);
719
- await this.deps.nativeItems.upsert(_catalogId, {
720
- ...existing,
721
- data: merged,
902
+ const mergedData = { ...(existing.data ?? {}), ...patchData };
903
+ const idx = {
904
+ ...(existing.indexed ?? {}),
905
+ ...(patchIndexed ?? {}),
906
+ ...indexedScopeFields(nextScope),
907
+ };
908
+ const oldDocId = storageDocIdForNativeRecord(existing);
909
+ const { scope: _dropScope, ...existingRest } = existing;
910
+ const nextRec = {
911
+ ...existingRest,
912
+ data: mergedData,
913
+ indexed: idx,
914
+ ...(nextScope.kind !== "global" ? { scope: nextScope } : {}),
722
915
  updatedAt,
723
- ...(actorId ? { updatedBy: actorId } : {}),
724
916
  version: (existing.version ?? 0) + 1,
917
+ ...(this.resolveActorId(_context) ? { updatedBy: this.resolveActorId(_context) } : {}),
918
+ };
919
+ const newDocId = storageDocIdForNativeRecord(nextRec);
920
+ await this.deps.nativeItems.upsert(_catalogId, nextRec);
921
+ if (newDocId !== oldDocId)
922
+ await this.deps.nativeItems.delete(_catalogId, oldDocId);
923
+ const persisted = (await this.deps.nativeItems.get(_catalogId, newDocId)) ?? nextRec;
924
+ await this.emitNativeItemHistory(_context, {
925
+ catalogId: _catalogId,
926
+ itemId: String(persisted.itemId),
927
+ storageDocId: newDocId,
928
+ op: "update",
929
+ before: existing,
930
+ after: persisted,
725
931
  });
726
932
  const out = {
727
- itemId: _itemId,
933
+ itemId: nextRec.itemId,
728
934
  catalogId: _catalogId,
729
935
  appId: _context.appId,
730
936
  sourceMode: "native",
731
937
  sourceType: "firebase",
732
- data: merged,
938
+ data: mergedData,
733
939
  createdAt: existing.createdAt,
734
940
  updatedAt,
735
941
  };
736
942
  return this.decorateItem(_catalogId, out);
737
943
  }
738
- async deleteNativeCatalogItem(_context, _catalogId, _itemId) {
944
+ async deleteNativeCatalogItem(_context, _catalogId, _itemId, _options) {
739
945
  await this.deps.authz.requireBindingAccess(_context, _context.appId, _catalogId, "write");
740
- await this.deps.nativeItems.delete(_catalogId, _itemId);
946
+ let docId = _options?.storageDocId;
947
+ if (!docId) {
948
+ let rows = await this.deps.nativeItems.findByLogicalItemId(_catalogId, _itemId);
949
+ if (!rows.length) {
950
+ const legacy = await this.deps.nativeItems.get(_catalogId, _itemId);
951
+ if (legacy)
952
+ rows = [legacy];
953
+ }
954
+ const winner = pickWinningPhysicalRow(rows, _options?.scope);
955
+ if (!winner)
956
+ throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
957
+ docId = storageDocIdForNativeRecord(winner);
958
+ }
959
+ const beforeDel = await this.deps.nativeItems.get(_catalogId, docId);
960
+ await this.deps.nativeItems.delete(_catalogId, docId);
961
+ if (beforeDel) {
962
+ await this.emitNativeItemHistory(_context, {
963
+ catalogId: _catalogId,
964
+ itemId: String(beforeDel.itemId),
965
+ storageDocId: docId,
966
+ op: "delete",
967
+ before: beforeDel,
968
+ after: null,
969
+ });
970
+ }
971
+ }
972
+ async moveNativeCatalogItemScope(context, catalogId, itemId, input) {
973
+ if (!context.superAdmin) {
974
+ throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
975
+ }
976
+ await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
977
+ const fromDoc = input.fromStorageDocId ??
978
+ encodeNativeItemStorageDocId(String(itemId), normalizeStoredScope(input.fromScope ?? { kind: "global" }));
979
+ const rec = await this.deps.nativeItems.get(catalogId, fromDoc);
980
+ if (!rec)
981
+ throw new CatalogNotFoundError({ catalogId, itemId });
982
+ const toScope = normalizeStoredScope(input.toScope);
983
+ const { scope: _dropScope, ...recRest } = rec;
984
+ const nextRec = {
985
+ ...recRest,
986
+ ...(toScope.kind !== "global" ? { scope: toScope } : {}),
987
+ indexed: { ...(rec.indexed ?? {}), ...indexedScopeFields(toScope) },
988
+ updatedAt: new Date().toISOString(),
989
+ };
990
+ const toDoc = storageDocIdForNativeRecord(nextRec);
991
+ await this.deps.nativeItems.upsert(catalogId, nextRec);
992
+ if (toDoc !== fromDoc)
993
+ await this.deps.nativeItems.delete(catalogId, fromDoc);
994
+ const persisted = (await this.deps.nativeItems.get(catalogId, toDoc)) ?? nextRec;
995
+ await this.emitNativeItemHistory(context, {
996
+ catalogId,
997
+ itemId: String(persisted.itemId),
998
+ storageDocId: toDoc,
999
+ op: "update",
1000
+ before: rec,
1001
+ after: persisted,
1002
+ });
741
1003
  }
742
1004
  async upsertNativeCatalogItem(context, catalogId, input) {
743
1005
  await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
744
1006
  const descriptor = await this.deps.descriptors.get(catalogId);
745
1007
  if (!descriptor)
746
1008
  throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
747
- const { data, indexed: callerIndexed } = this.stripReservedWriteFields(input);
748
- const indexed = callerIndexed ?? this.deriveIndexed(descriptor.descriptor, data);
749
- const itemId = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data });
1009
+ const { data, indexed: callerIndexed, scope: inputScope } = this.stripReservedWriteFields(input);
1010
+ const storedScope = normalizeStoredScope(inputScope ?? { kind: "global" });
1011
+ assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
1012
+ const derived = this.deriveIndexed(descriptor.descriptor, data);
1013
+ const scopeIdx = indexedScopeFields(storedScope);
1014
+ const indexed = derived != null ? { ...derived, ...scopeIdx } : (Object.keys(scopeIdx).length ? scopeIdx : undefined);
1015
+ const logicalItemId = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data });
1016
+ const storageDocId = encodeNativeItemStorageDocId(String(logicalItemId), storedScope);
750
1017
  const now = new Date().toISOString();
751
- const existing = await this.deps.nativeItems.get(catalogId, itemId);
1018
+ const existing = await this.deps.nativeItems.get(catalogId, storageDocId);
752
1019
  const actorId = this.resolveActorId(context);
753
1020
  await this.deps.nativeItems.upsert(catalogId, {
754
- itemId,
1021
+ itemId: logicalItemId,
755
1022
  catalogId,
1023
+ ...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
756
1024
  appScopedOwnerId: context.appId,
757
1025
  ...(indexed != null ? { indexed } : {}),
758
1026
  data,
@@ -762,8 +1030,17 @@ export class Catalox {
762
1030
  ...(existing?.createdBy ? { createdBy: existing.createdBy } : actorId ? { createdBy: actorId } : {}),
763
1031
  ...(actorId ? { updatedBy: actorId } : {}),
764
1032
  });
1033
+ const persisted = (await this.deps.nativeItems.get(catalogId, storageDocId));
1034
+ await this.emitNativeItemHistory(context, {
1035
+ catalogId,
1036
+ itemId: String(logicalItemId),
1037
+ storageDocId,
1038
+ op: "update",
1039
+ before: existing ?? null,
1040
+ after: persisted,
1041
+ });
765
1042
  return {
766
- itemId,
1043
+ itemId: logicalItemId,
767
1044
  catalogId,
768
1045
  appId: context.appId,
769
1046
  sourceMode: "native",
@@ -780,13 +1057,21 @@ export class Catalox {
780
1057
  throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
781
1058
  const now = new Date().toISOString();
782
1059
  const actorId = this.resolveActorId(context);
783
- const records = items.map((input) => {
784
- const stripped = this.stripReservedWriteFields(input);
785
- const indexed = stripped.indexed ?? this.deriveIndexed(descriptor.descriptor, stripped.data);
786
- const itemId = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data: stripped.data });
1060
+ const records = items.map((row) => {
1061
+ const stripped = this.stripReservedWriteFields(row);
1062
+ const storedScope = normalizeStoredScope(stripped.scope ?? { kind: "global" });
1063
+ assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
1064
+ const derived = this.deriveIndexed(descriptor.descriptor, stripped.data);
1065
+ const scopeIdx = indexedScopeFields(storedScope);
1066
+ const indexed = derived != null ? { ...derived, ...scopeIdx } : (Object.keys(scopeIdx).length ? scopeIdx : undefined);
1067
+ const logicalItemId = resolveCatalogItemId({
1068
+ identity: descriptor.descriptor.identity,
1069
+ data: stripped.data,
1070
+ });
787
1071
  return {
788
- itemId,
1072
+ itemId: logicalItemId,
789
1073
  catalogId,
1074
+ ...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
790
1075
  appScopedOwnerId: context.appId,
791
1076
  ...(indexed != null ? { indexed } : {}),
792
1077
  data: stripped.data,
@@ -795,7 +1080,25 @@ export class Catalox {
795
1080
  ...(actorId ? { updatedBy: actorId } : {}),
796
1081
  };
797
1082
  });
1083
+ const beforeRows = [];
1084
+ for (const r of records) {
1085
+ const docId = storageDocIdForNativeRecord(r);
1086
+ const ex = await this.deps.nativeItems.get(catalogId, docId);
1087
+ beforeRows.push({ docId, rec: ex });
1088
+ }
798
1089
  await this.deps.nativeItems.batchUpsert(catalogId, records);
1090
+ for (let i = 0; i < records.length; i++) {
1091
+ const { docId, rec: before } = beforeRows[i];
1092
+ const after = (await this.deps.nativeItems.get(catalogId, docId));
1093
+ await this.emitNativeItemHistory(context, {
1094
+ catalogId,
1095
+ itemId: String(after.itemId),
1096
+ storageDocId: docId,
1097
+ op: "update",
1098
+ before,
1099
+ after,
1100
+ });
1101
+ }
799
1102
  }
800
1103
  importCatalogItemsFromJson(json) {
801
1104
  return parseJson(json);
@@ -1153,6 +1456,139 @@ export class Catalox {
1153
1456
  firestore: this.deps.firestoreStore.firestore,
1154
1457
  });
1155
1458
  }
1459
+ async listCatalogItemHistory(context, catalogId, input) {
1460
+ await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "read");
1461
+ const rh = this.deps.recordHistory;
1462
+ if (!rh)
1463
+ return { events: [] };
1464
+ return rh.store.listByCatalog(catalogId, input);
1465
+ }
1466
+ async getCatalogItemHistoryEvent(context, eventId) {
1467
+ const rh = this.deps.recordHistory;
1468
+ if (!rh)
1469
+ return null;
1470
+ const row = await rh.store.get(eventId);
1471
+ if (!row)
1472
+ return null;
1473
+ await this.deps.authz.requireBindingAccess(context, context.appId, row.catalogId, "read");
1474
+ const payload = await readRecordHistoryEventPayload(rh, row);
1475
+ return { index: row, payload };
1476
+ }
1477
+ async restoreCatalogItemFromHistory(context, eventId, input) {
1478
+ const rh = this.deps.recordHistory;
1479
+ if (!rh)
1480
+ throw new Error("recordHistory is not configured on Catalox");
1481
+ const row = await rh.store.get(eventId);
1482
+ if (!row)
1483
+ throw new Error(`catalogItemHistory event not found: ${eventId}`);
1484
+ await this.deps.authz.requireBindingAccess(context, context.appId, row.catalogId, "write");
1485
+ const payload = await readRecordHistoryEventPayload(rh, row);
1486
+ const rec = input.mode === "before" ? payload.before ?? undefined : payload.after ?? undefined;
1487
+ if (!rec)
1488
+ throw new CatalogNotFoundError({ catalogId: row.catalogId, itemId: row.itemId });
1489
+ const docId = storageDocIdForNativeRecord(rec);
1490
+ const liveBefore = await this.deps.nativeItems.get(row.catalogId, docId);
1491
+ await this.deps.nativeItems.upsert(row.catalogId, rec);
1492
+ const liveAfter = (await this.deps.nativeItems.get(row.catalogId, docId));
1493
+ await this.emitNativeItemHistory(context, {
1494
+ catalogId: row.catalogId,
1495
+ itemId: String(rec.itemId),
1496
+ storageDocId: docId,
1497
+ op: "restore",
1498
+ before: liveBefore ?? null,
1499
+ after: liveAfter,
1500
+ });
1501
+ return this.decorateItem(row.catalogId, {
1502
+ itemId: rec.itemId,
1503
+ catalogId: row.catalogId,
1504
+ appId: context.appId,
1505
+ sourceMode: "native",
1506
+ sourceType: "firebase",
1507
+ data: liveAfter.data,
1508
+ createdAt: liveAfter.createdAt,
1509
+ updatedAt: liveAfter.updatedAt,
1510
+ });
1511
+ }
1512
+ async replayCatalogToPointInTime(context, catalogId, input) {
1513
+ await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
1514
+ const rh = this.deps.recordHistory;
1515
+ if (!rh)
1516
+ throw new Error("recordHistory is not configured on Catalox");
1517
+ const rows = await rh.store.listByCatalogChronologicalUntil(catalogId, input.asOf);
1518
+ const state = new Map();
1519
+ for (const idx of rows) {
1520
+ const full = await readRecordHistoryEventPayload(rh, idx);
1521
+ const key = String(full.storageDocId ?? full.itemId);
1522
+ if (full.op === "catalog_delete_bulk") {
1523
+ state.clear();
1524
+ continue;
1525
+ }
1526
+ if (full.op === "catalog_rename")
1527
+ continue;
1528
+ if (full.op === "delete") {
1529
+ state.delete(key);
1530
+ continue;
1531
+ }
1532
+ if (full.after)
1533
+ state.set(key, full.after);
1534
+ else
1535
+ state.delete(key);
1536
+ }
1537
+ const live = [];
1538
+ let off = 0;
1539
+ const page = 400;
1540
+ while (true) {
1541
+ const chunk = await this.deps.nativeItems.list(catalogId, { limit: page, offset: off });
1542
+ if (!chunk.length)
1543
+ break;
1544
+ live.push(...chunk);
1545
+ off += chunk.length;
1546
+ if (chunk.length < page)
1547
+ break;
1548
+ }
1549
+ let upserted = 0;
1550
+ let deleted = 0;
1551
+ for (const rec of state.values()) {
1552
+ await this.deps.nativeItems.upsert(catalogId, rec);
1553
+ upserted += 1;
1554
+ }
1555
+ for (const r of live) {
1556
+ const k = storageDocIdForNativeRecord(r);
1557
+ if (!state.has(k)) {
1558
+ await this.deps.nativeItems.delete(catalogId, k);
1559
+ deleted += 1;
1560
+ }
1561
+ }
1562
+ return { upserted, deleted };
1563
+ }
1564
+ async deleteCatalog(context, catalogId, input) {
1565
+ await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "admin");
1566
+ return runDeleteCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, context, catalogId, input);
1567
+ }
1568
+ async restoreDeletedCatalog(context, input) {
1569
+ if (!context.superAdmin) {
1570
+ throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
1571
+ }
1572
+ const rh = this.deps.recordHistory;
1573
+ if (!rh) {
1574
+ return {
1575
+ ok: false,
1576
+ catalogId: "",
1577
+ issues: [{ code: "record_history_required", message: "recordHistory is not configured on Catalox" }],
1578
+ };
1579
+ }
1580
+ return runRestoreDeletedCatalog(this.catalogLifecycleDeps(), rh, input);
1581
+ }
1582
+ async renameCatalog(context, fromCatalogId, toCatalogId, input = {}) {
1583
+ await this.deps.authz.requireBindingAccess(context, context.appId, fromCatalogId, "admin");
1584
+ return runRenameCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, context, fromCatalogId, toCatalogId, input);
1585
+ }
1586
+ /**
1587
+ * Fix {@link CataloxContext} for subsequent calls so embedders omit it on each method (no globals).
1588
+ */
1589
+ withContext(context) {
1590
+ return new CataloxBound(this, context);
1591
+ }
1156
1592
  }
1157
1593
  // (moved to `src/contracts/bootstrap.ts`)
1158
1594
  //# sourceMappingURL=catalox.js.map