@x12i/catalox 3.4.5 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/dist/src/adapters/api/api-adapter.d.ts.map +1 -1
- package/dist/src/adapters/api/api-adapter.js +3 -0
- package/dist/src/adapters/api/api-adapter.js.map +1 -1
- package/dist/src/adapters/mongo/mongo-adapter.d.ts.map +1 -1
- package/dist/src/adapters/mongo/mongo-adapter.js +3 -0
- package/dist/src/adapters/mongo/mongo-adapter.js.map +1 -1
- package/dist/src/catalox/catalog-lifecycle.d.ts.map +1 -1
- package/dist/src/catalox/catalog-lifecycle.js +15 -0
- package/dist/src/catalox/catalog-lifecycle.js.map +1 -1
- package/dist/src/catalox/catalox.d.ts +1 -0
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +111 -39
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/cli/index.js +2 -21
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/contracts/context.d.ts +8 -1
- package/dist/src/contracts/context.d.ts.map +1 -1
- package/dist/src/firebase/bootstrap.d.ts +95 -0
- package/dist/src/firebase/bootstrap.d.ts.map +1 -0
- package/dist/src/firebase/bootstrap.js +74 -0
- package/dist/src/firebase/bootstrap.js.map +1 -0
- package/dist/src/firebase/index.d.ts +1 -0
- package/dist/src/firebase/index.d.ts.map +1 -1
- package/dist/src/firebase/index.js +1 -0
- package/dist/src/firebase/index.js.map +1 -1
- package/docs/catalox-ui-contract.md +6 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { compactCatalogFilter } from "../contracts/catalogs.js";
|
|
2
|
-
import { CatalogAccessDeniedError, CatalogAdapterError, CatalogNotFoundError } from "../contracts/errors.js";
|
|
2
|
+
import { CatalogAccessDeniedError, CatalogAdapterError, CatalogBindingError, CatalogNotFoundError, } from "../contracts/errors.js";
|
|
3
3
|
import { validateMappingSpec } from "../mapping/validate-mapping.js";
|
|
4
4
|
import { executeMapping } from "../mapping/execute-mapping.js";
|
|
5
5
|
import { ApiCatalogAdapter } from "../adapters/api/api-adapter.js";
|
|
@@ -110,8 +110,41 @@ export class Catalox {
|
|
|
110
110
|
if (active.length)
|
|
111
111
|
return { storeId, appIds: active };
|
|
112
112
|
}
|
|
113
|
+
if (!context.appId) {
|
|
114
|
+
throw new CatalogBindingError({
|
|
115
|
+
reason: "missing_appId",
|
|
116
|
+
message: "App id is required unless storeId/appIds are provided.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
113
119
|
return { ...(storeId ? { storeId } : {}), appIds: [context.appId] };
|
|
114
120
|
}
|
|
121
|
+
async resolveAppIdForCatalogAccess(params) {
|
|
122
|
+
const { context, catalogId, required } = params;
|
|
123
|
+
if (context.appId)
|
|
124
|
+
return context.appId;
|
|
125
|
+
// Super-admin callers may omit appId; access is still evaluated with explicit elevation.
|
|
126
|
+
if (context.superAdmin)
|
|
127
|
+
return "_super";
|
|
128
|
+
// CatalogId-first: infer appId via active bindings if there is exactly one viable binding.
|
|
129
|
+
const bindings = await this.deps.bindings.listByCatalog(catalogId);
|
|
130
|
+
const active = bindings.filter((b) => b.status === "active");
|
|
131
|
+
const viable = active.filter((b) => {
|
|
132
|
+
if (required === "read")
|
|
133
|
+
return Boolean(b.access.canRead);
|
|
134
|
+
if (required === "write")
|
|
135
|
+
return Boolean(b.access.canWrite);
|
|
136
|
+
return Boolean(b.access.canAdmin);
|
|
137
|
+
});
|
|
138
|
+
const distinctApps = [...new Set(viable.map((b) => String(b.appId)))];
|
|
139
|
+
if (distinctApps.length === 1)
|
|
140
|
+
return viable[0].appId;
|
|
141
|
+
throw new CatalogBindingError({
|
|
142
|
+
reason: "cannot_resolve_appId_for_catalog",
|
|
143
|
+
catalogId: String(catalogId),
|
|
144
|
+
required,
|
|
145
|
+
matchingAppIds: distinctApps,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
115
148
|
readPath(obj, path) {
|
|
116
149
|
if (!path)
|
|
117
150
|
return undefined;
|
|
@@ -162,7 +195,12 @@ export class Catalox {
|
|
|
162
195
|
}
|
|
163
196
|
if (source.type === "catalog") {
|
|
164
197
|
// Enforce authz: safest default is to require binding (unless god-mode).
|
|
165
|
-
await this.
|
|
198
|
+
const appId = await this.resolveAppIdForCatalogAccess({
|
|
199
|
+
context: params.context,
|
|
200
|
+
catalogId: source.catalogId,
|
|
201
|
+
required: "read",
|
|
202
|
+
});
|
|
203
|
+
await this.deps.authz.requireBindingAccess(params.context, appId, source.catalogId, "read");
|
|
166
204
|
const filterEq = resolveFilterBy(source.filterBy, params.currentItemData, (o, p) => this.readPath(o, p));
|
|
167
205
|
const sort = source.sortBy
|
|
168
206
|
? { [source.sortBy.field]: source.sortBy.direction === "asc" ? 1 : -1 }
|
|
@@ -421,6 +459,9 @@ export class Catalox {
|
|
|
421
459
|
}
|
|
422
460
|
async listAppCatalogs(context, input) {
|
|
423
461
|
const appId = input?.appId ?? context.appId;
|
|
462
|
+
if (!appId) {
|
|
463
|
+
throw new CatalogBindingError({ reason: "missing_appId", message: "listAppCatalogs requires an appId." });
|
|
464
|
+
}
|
|
424
465
|
const bindings = await this.deps.bindings.listByApp(appId);
|
|
425
466
|
const catalogs = await this.deps.catalogs.listAll();
|
|
426
467
|
const byId = new Map(catalogs.map((c) => [c.catalogId, c]));
|
|
@@ -547,12 +588,14 @@ export class Catalox {
|
|
|
547
588
|
return findCatalogsByAiHelper({ query: input.query, catalogs: all, input });
|
|
548
589
|
}
|
|
549
590
|
async getCatalogDescriptor(context, catalogId) {
|
|
550
|
-
await this.
|
|
591
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
592
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
551
593
|
const rec = await this.deps.descriptors.get(catalogId);
|
|
552
594
|
return rec?.descriptor ?? null;
|
|
553
595
|
}
|
|
554
596
|
async getCatalogRendererSnippet(context, catalogId, role, mode) {
|
|
555
|
-
await this.
|
|
597
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
598
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
556
599
|
if (!this.deps.rendererSnippets) {
|
|
557
600
|
throw new Error("rendererSnippets dependency is not configured");
|
|
558
601
|
}
|
|
@@ -560,6 +603,9 @@ export class Catalox {
|
|
|
560
603
|
}
|
|
561
604
|
async getAppCatalogBootstrap(context, appId) {
|
|
562
605
|
const resolved = appId ?? context.appId;
|
|
606
|
+
if (!resolved) {
|
|
607
|
+
throw new CatalogBindingError({ reason: "missing_appId", message: "getAppCatalogBootstrap requires an appId." });
|
|
608
|
+
}
|
|
563
609
|
const entries = await this.listAppCatalogs(context, { appId: resolved });
|
|
564
610
|
const descriptors = [];
|
|
565
611
|
for (const e of entries) {
|
|
@@ -582,7 +628,9 @@ export class Catalox {
|
|
|
582
628
|
return out;
|
|
583
629
|
}
|
|
584
630
|
async listCatalogItems(context, catalogId, options) {
|
|
585
|
-
await this.
|
|
631
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
632
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
633
|
+
const appCtx = context.appId ? context : { ...context, appId };
|
|
586
634
|
const catalog = await this.deps.catalogs.get(catalogId);
|
|
587
635
|
if (!catalog)
|
|
588
636
|
throw new CatalogNotFoundError({ catalogId });
|
|
@@ -619,7 +667,7 @@ export class Catalox {
|
|
|
619
667
|
else {
|
|
620
668
|
physical = await this.fetchScopedPhysicalRows(catalogId, rawScope, baseListOpts);
|
|
621
669
|
}
|
|
622
|
-
const merged = mergeNativePhysicalRows(physical, descRec.descriptor.identity, String(
|
|
670
|
+
const merged = mergeNativePhysicalRows(physical, descRec.descriptor.identity, String(appId), wantsSuperList);
|
|
623
671
|
const off = listQueryOptions?.offset ?? 0;
|
|
624
672
|
const lim = listQueryOptions?.limit ?? 100;
|
|
625
673
|
const sliced = merged.slice(off, off + lim);
|
|
@@ -652,7 +700,7 @@ export class Catalox {
|
|
|
652
700
|
const adapterConfig = await this.deps.adapters.get(def.adapterId);
|
|
653
701
|
if (!adapterConfig)
|
|
654
702
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
655
|
-
const result = await this.deps.mongoAdapter.listItems(
|
|
703
|
+
const result = await this.deps.mongoAdapter.listItems(appCtx, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
656
704
|
return result.issues?.length
|
|
657
705
|
? { listOutcome: "ok", items: result.items, issues: result.issues }
|
|
658
706
|
: { listOutcome: "ok", items: result.items };
|
|
@@ -663,7 +711,7 @@ export class Catalox {
|
|
|
663
711
|
const adapterConfig = await this.deps.adapters.get(def.adapterId);
|
|
664
712
|
if (!adapterConfig)
|
|
665
713
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
666
|
-
const result = await this.deps.apiAdapter.listItems(
|
|
714
|
+
const result = await this.deps.apiAdapter.listItems(appCtx, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
667
715
|
return {
|
|
668
716
|
listOutcome: "ok",
|
|
669
717
|
items: result.items,
|
|
@@ -697,7 +745,8 @@ export class Catalox {
|
|
|
697
745
|
}
|
|
698
746
|
}
|
|
699
747
|
async getCatalogItem(context, catalogId, itemId, options) {
|
|
700
|
-
await this.
|
|
748
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
749
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
701
750
|
const catalog = await this.deps.catalogs.get(catalogId);
|
|
702
751
|
if (!catalog)
|
|
703
752
|
throw new CatalogNotFoundError({ catalogId });
|
|
@@ -713,7 +762,7 @@ export class Catalox {
|
|
|
713
762
|
const base = {
|
|
714
763
|
itemId: rec.itemId,
|
|
715
764
|
catalogId: rec.catalogId,
|
|
716
|
-
appId
|
|
765
|
+
appId,
|
|
717
766
|
sourceMode: "native",
|
|
718
767
|
sourceType: "firebase",
|
|
719
768
|
data: rec.data,
|
|
@@ -738,7 +787,7 @@ export class Catalox {
|
|
|
738
787
|
const filtered = scope?.accountId || scope?.groupId || scope?.userId || scope?.channelId || scope?.visitorId || scope?.domainId || scope?.agentId
|
|
739
788
|
? filterPhysicalForTenantFetch(rows, scope)
|
|
740
789
|
: rows.filter((r) => isGlobalPhysicalRow(r));
|
|
741
|
-
const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(
|
|
790
|
+
const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(appId), false);
|
|
742
791
|
const hit = merged.find((m) => m.itemId === itemId) ?? merged[0];
|
|
743
792
|
if (!hit)
|
|
744
793
|
return { outcome: "not_found" };
|
|
@@ -784,18 +833,23 @@ export class Catalox {
|
|
|
784
833
|
return { isValid: true, issues: [] };
|
|
785
834
|
}
|
|
786
835
|
async getCatalogItemReferences(context, catalogId, itemId) {
|
|
787
|
-
await this.
|
|
836
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
837
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
788
838
|
const refs = await this.deps.references.listByItem(catalogId, itemId);
|
|
789
839
|
return refs;
|
|
790
840
|
}
|
|
791
841
|
async listCatalogReferences(context, catalogId) {
|
|
792
|
-
await this.
|
|
842
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
843
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
793
844
|
const refs = await this.deps.references.listByCatalog(catalogId);
|
|
794
845
|
return refs;
|
|
795
846
|
}
|
|
796
847
|
// Spec methods below are stubs for now; filled in by later todos.
|
|
797
848
|
async getApp(_context, appId) {
|
|
798
|
-
|
|
849
|
+
const resolved = appId ?? _context.appId;
|
|
850
|
+
if (!resolved)
|
|
851
|
+
throw new CatalogBindingError({ reason: "missing_appId" });
|
|
852
|
+
return this.deps.apps.get(resolved);
|
|
799
853
|
}
|
|
800
854
|
async createCatalog(_context, _input) {
|
|
801
855
|
const now = new Date().toISOString();
|
|
@@ -1004,7 +1058,8 @@ export class Catalox {
|
|
|
1004
1058
|
return { query: input.query, hits, noMatch: Boolean(res.noMatch) };
|
|
1005
1059
|
}
|
|
1006
1060
|
async createCatalogItemByAi(context, catalogId, input) {
|
|
1007
|
-
await this.
|
|
1061
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1062
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1008
1063
|
const descRec = await this.deps.descriptors.get(catalogId);
|
|
1009
1064
|
if (!descRec)
|
|
1010
1065
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
@@ -1056,7 +1111,8 @@ export class Catalox {
|
|
|
1056
1111
|
return out;
|
|
1057
1112
|
}
|
|
1058
1113
|
async modifyCatalogItemByAi(context, catalogId, itemId, input) {
|
|
1059
|
-
await this.
|
|
1114
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1115
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1060
1116
|
const descRec = await this.deps.descriptors.get(catalogId);
|
|
1061
1117
|
if (!descRec)
|
|
1062
1118
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
@@ -1146,7 +1202,7 @@ export class Catalox {
|
|
|
1146
1202
|
async bindAppToStore(context, input) {
|
|
1147
1203
|
if (!this.deps.storeAppBindings)
|
|
1148
1204
|
throw new Error("storeAppBindings dependency is not configured");
|
|
1149
|
-
if (!context.superAdmin && input.appId !== context.appId) {
|
|
1205
|
+
if (!context.superAdmin && (!context.appId || input.appId !== context.appId)) {
|
|
1150
1206
|
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
1151
1207
|
}
|
|
1152
1208
|
const existing = await this.deps.storeAppBindings.findByStoreApp(input.storeId, input.appId);
|
|
@@ -1170,7 +1226,7 @@ export class Catalox {
|
|
|
1170
1226
|
async unbindAppFromStore(context, storeId, appId) {
|
|
1171
1227
|
if (!this.deps.storeAppBindings)
|
|
1172
1228
|
throw new Error("storeAppBindings dependency is not configured");
|
|
1173
|
-
if (!context.superAdmin && appId !== context.appId) {
|
|
1229
|
+
if (!context.superAdmin && (!context.appId || appId !== context.appId)) {
|
|
1174
1230
|
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
1175
1231
|
}
|
|
1176
1232
|
const existing = await this.deps.storeAppBindings.findByStoreApp(storeId, appId);
|
|
@@ -1192,10 +1248,15 @@ export class Catalox {
|
|
|
1192
1248
|
if (context.superAdmin)
|
|
1193
1249
|
return records;
|
|
1194
1250
|
// non-god: only reveal memberships that include the caller's own appId
|
|
1195
|
-
return records.filter((r) => r.appId === context.appId);
|
|
1251
|
+
return context.appId ? records.filter((r) => r.appId === context.appId) : [];
|
|
1196
1252
|
}
|
|
1197
1253
|
async ensureCatalog(context, catalog) {
|
|
1198
|
-
await this.
|
|
1254
|
+
const appId = await this.resolveAppIdForCatalogAccess({
|
|
1255
|
+
context,
|
|
1256
|
+
catalogId: catalog.catalogId,
|
|
1257
|
+
required: "admin",
|
|
1258
|
+
});
|
|
1259
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalog.catalogId, "admin");
|
|
1199
1260
|
const existing = await this.deps.catalogs.get(catalog.catalogId);
|
|
1200
1261
|
const now = new Date().toISOString();
|
|
1201
1262
|
if (existing)
|
|
@@ -1211,7 +1272,7 @@ export class Catalox {
|
|
|
1211
1272
|
}
|
|
1212
1273
|
async ensureBinding(context, input) {
|
|
1213
1274
|
// only super-admin apps can provision cross-app bindings.
|
|
1214
|
-
if (!context.superAdmin && input.appId !== context.appId) {
|
|
1275
|
+
if (!context.superAdmin && (!context.appId || input.appId !== context.appId)) {
|
|
1215
1276
|
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
1216
1277
|
}
|
|
1217
1278
|
const existing = await this.deps.bindings.findByAppCatalog(input.appId, input.catalogId);
|
|
@@ -1236,7 +1297,8 @@ export class Catalox {
|
|
|
1236
1297
|
return this.upsertNativeCatalogItem(_context, _catalogId, _input);
|
|
1237
1298
|
}
|
|
1238
1299
|
async updateNativeCatalogItem(_context, _catalogId, _itemId, _patch, _options) {
|
|
1239
|
-
await this.
|
|
1300
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context: _context, catalogId: _catalogId, required: "write" });
|
|
1301
|
+
await this.deps.authz.requireBindingAccess(_context, appId, _catalogId, "write");
|
|
1240
1302
|
let existing = null;
|
|
1241
1303
|
if (_options?.storageDocId) {
|
|
1242
1304
|
existing = await this.deps.nativeItems.get(_catalogId, _options.storageDocId);
|
|
@@ -1292,7 +1354,7 @@ export class Catalox {
|
|
|
1292
1354
|
const out = {
|
|
1293
1355
|
itemId: nextRec.itemId,
|
|
1294
1356
|
catalogId: _catalogId,
|
|
1295
|
-
appId
|
|
1357
|
+
appId,
|
|
1296
1358
|
sourceMode: "native",
|
|
1297
1359
|
sourceType: "firebase",
|
|
1298
1360
|
data: mergedData,
|
|
@@ -1302,7 +1364,8 @@ export class Catalox {
|
|
|
1302
1364
|
return this.decorateItem(_catalogId, out);
|
|
1303
1365
|
}
|
|
1304
1366
|
async deleteNativeCatalogItem(_context, _catalogId, _itemId, _options) {
|
|
1305
|
-
await this.
|
|
1367
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context: _context, catalogId: _catalogId, required: "write" });
|
|
1368
|
+
await this.deps.authz.requireBindingAccess(_context, appId, _catalogId, "write");
|
|
1306
1369
|
let docId = _options?.storageDocId;
|
|
1307
1370
|
if (!docId) {
|
|
1308
1371
|
let rows = await this.deps.nativeItems.findByLogicalItemId(_catalogId, _itemId);
|
|
@@ -1333,7 +1396,8 @@ export class Catalox {
|
|
|
1333
1396
|
if (!context.superAdmin) {
|
|
1334
1397
|
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
1335
1398
|
}
|
|
1336
|
-
await this.
|
|
1399
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1400
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1337
1401
|
const fromDoc = input.fromStorageDocId ??
|
|
1338
1402
|
encodeNativeItemStorageDocId(String(itemId), normalizeStoredScope(input.fromScope ?? { kind: "global" }));
|
|
1339
1403
|
const rec = await this.deps.nativeItems.get(catalogId, fromDoc);
|
|
@@ -1362,7 +1426,8 @@ export class Catalox {
|
|
|
1362
1426
|
});
|
|
1363
1427
|
}
|
|
1364
1428
|
async upsertNativeCatalogItem(context, catalogId, input) {
|
|
1365
|
-
await this.
|
|
1429
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1430
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1366
1431
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
1367
1432
|
if (!descriptor)
|
|
1368
1433
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
@@ -1381,7 +1446,7 @@ export class Catalox {
|
|
|
1381
1446
|
itemId: logicalItemId,
|
|
1382
1447
|
catalogId,
|
|
1383
1448
|
...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
|
|
1384
|
-
appScopedOwnerId:
|
|
1449
|
+
appScopedOwnerId: appId,
|
|
1385
1450
|
...(indexed != null ? { indexed } : {}),
|
|
1386
1451
|
data,
|
|
1387
1452
|
version: (existing?.version ?? 0) + 1,
|
|
@@ -1402,7 +1467,7 @@ export class Catalox {
|
|
|
1402
1467
|
return {
|
|
1403
1468
|
itemId: logicalItemId,
|
|
1404
1469
|
catalogId,
|
|
1405
|
-
appId
|
|
1470
|
+
appId,
|
|
1406
1471
|
sourceMode: "native",
|
|
1407
1472
|
sourceType: "firebase",
|
|
1408
1473
|
data,
|
|
@@ -1411,7 +1476,8 @@ export class Catalox {
|
|
|
1411
1476
|
};
|
|
1412
1477
|
}
|
|
1413
1478
|
async batchUpsertNativeCatalogItems(context, catalogId, items) {
|
|
1414
|
-
await this.
|
|
1479
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1480
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1415
1481
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
1416
1482
|
if (!descriptor)
|
|
1417
1483
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
@@ -1432,7 +1498,7 @@ export class Catalox {
|
|
|
1432
1498
|
itemId: logicalItemId,
|
|
1433
1499
|
catalogId,
|
|
1434
1500
|
...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
|
|
1435
|
-
appScopedOwnerId:
|
|
1501
|
+
appScopedOwnerId: appId,
|
|
1436
1502
|
...(indexed != null ? { indexed } : {}),
|
|
1437
1503
|
data: stripped.data,
|
|
1438
1504
|
createdAt: now,
|
|
@@ -1803,7 +1869,8 @@ export class Catalox {
|
|
|
1803
1869
|
});
|
|
1804
1870
|
}
|
|
1805
1871
|
async listCatalogItemHistory(context, catalogId, input) {
|
|
1806
|
-
await this.
|
|
1872
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
1873
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
1807
1874
|
const rh = this.deps.recordHistory;
|
|
1808
1875
|
if (!rh)
|
|
1809
1876
|
return { events: [] };
|
|
@@ -1816,7 +1883,8 @@ export class Catalox {
|
|
|
1816
1883
|
const row = await rh.store.get(eventId);
|
|
1817
1884
|
if (!row)
|
|
1818
1885
|
return null;
|
|
1819
|
-
await this.
|
|
1886
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId: row.catalogId, required: "read" });
|
|
1887
|
+
await this.deps.authz.requireBindingAccess(context, appId, row.catalogId, "read");
|
|
1820
1888
|
const payload = await readRecordHistoryEventPayload(rh, row);
|
|
1821
1889
|
return { index: row, payload };
|
|
1822
1890
|
}
|
|
@@ -1827,7 +1895,8 @@ export class Catalox {
|
|
|
1827
1895
|
const row = await rh.store.get(eventId);
|
|
1828
1896
|
if (!row)
|
|
1829
1897
|
throw new Error(`catalogItemHistory event not found: ${eventId}`);
|
|
1830
|
-
await this.
|
|
1898
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId: row.catalogId, required: "write" });
|
|
1899
|
+
await this.deps.authz.requireBindingAccess(context, appId, row.catalogId, "write");
|
|
1831
1900
|
const payload = await readRecordHistoryEventPayload(rh, row);
|
|
1832
1901
|
const rec = input.mode === "before" ? payload.before ?? undefined : payload.after ?? undefined;
|
|
1833
1902
|
if (!rec)
|
|
@@ -1847,7 +1916,7 @@ export class Catalox {
|
|
|
1847
1916
|
return this.decorateItem(row.catalogId, {
|
|
1848
1917
|
itemId: rec.itemId,
|
|
1849
1918
|
catalogId: row.catalogId,
|
|
1850
|
-
appId
|
|
1919
|
+
appId,
|
|
1851
1920
|
sourceMode: "native",
|
|
1852
1921
|
sourceType: "firebase",
|
|
1853
1922
|
data: liveAfter.data,
|
|
@@ -1856,7 +1925,8 @@ export class Catalox {
|
|
|
1856
1925
|
});
|
|
1857
1926
|
}
|
|
1858
1927
|
async replayCatalogToPointInTime(context, catalogId, input) {
|
|
1859
|
-
await this.
|
|
1928
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1929
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1860
1930
|
const rh = this.deps.recordHistory;
|
|
1861
1931
|
if (!rh)
|
|
1862
1932
|
throw new Error("recordHistory is not configured on Catalox");
|
|
@@ -1908,8 +1978,9 @@ export class Catalox {
|
|
|
1908
1978
|
return { upserted, deleted };
|
|
1909
1979
|
}
|
|
1910
1980
|
async deleteCatalog(context, catalogId, input) {
|
|
1911
|
-
await this.
|
|
1912
|
-
|
|
1981
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "admin" });
|
|
1982
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "admin");
|
|
1983
|
+
return runDeleteCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, { ...context, appId }, catalogId, input);
|
|
1913
1984
|
}
|
|
1914
1985
|
async restoreDeletedCatalog(context, input) {
|
|
1915
1986
|
if (!context.superAdmin) {
|
|
@@ -1926,8 +1997,9 @@ export class Catalox {
|
|
|
1926
1997
|
return runRestoreDeletedCatalog(this.catalogLifecycleDeps(), rh, input);
|
|
1927
1998
|
}
|
|
1928
1999
|
async renameCatalog(context, fromCatalogId, toCatalogId, input = {}) {
|
|
1929
|
-
await this.
|
|
1930
|
-
|
|
2000
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId: fromCatalogId, required: "admin" });
|
|
2001
|
+
await this.deps.authz.requireBindingAccess(context, appId, fromCatalogId, "admin");
|
|
2002
|
+
return runRenameCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, { ...context, appId }, fromCatalogId, toCatalogId, input);
|
|
1931
2003
|
}
|
|
1932
2004
|
/**
|
|
1933
2005
|
* Fix {@link CataloxContext} for subsequent calls so embedders omit it on each method (no globals).
|