@x12i/memorix-service 3.2.1 → 3.4.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 +61 -3
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +8 -0
- package/dist/app.js.map +1 -1
- package/dist/blob-store/config.d.ts +31 -0
- package/dist/blob-store/config.d.ts.map +1 -0
- package/dist/blob-store/config.js +68 -0
- package/dist/blob-store/config.js.map +1 -0
- package/dist/blob-store/disk.d.ts +10 -0
- package/dist/blob-store/disk.d.ts.map +1 -0
- package/dist/blob-store/disk.js +104 -0
- package/dist/blob-store/disk.js.map +1 -0
- package/dist/blob-store/index.d.ts +6 -0
- package/dist/blob-store/index.d.ts.map +1 -0
- package/dist/blob-store/index.js +6 -0
- package/dist/blob-store/index.js.map +1 -0
- package/dist/blob-store/memory.d.ts +4 -0
- package/dist/blob-store/memory.d.ts.map +1 -0
- package/dist/blob-store/memory.js +47 -0
- package/dist/blob-store/memory.js.map +1 -0
- package/dist/blob-store/s3.d.ts +52 -0
- package/dist/blob-store/s3.d.ts.map +1 -0
- package/dist/blob-store/s3.js +151 -0
- package/dist/blob-store/s3.js.map +1 -0
- package/dist/blob-store/types.d.ts +30 -0
- package/dist/blob-store/types.d.ts.map +1 -0
- package/dist/blob-store/types.js +38 -0
- package/dist/blob-store/types.js.map +1 -0
- package/dist/catalog/catalog-service.d.ts +48 -0
- package/dist/catalog/catalog-service.d.ts.map +1 -0
- package/dist/catalog/catalog-service.js +397 -0
- package/dist/catalog/catalog-service.js.map +1 -0
- package/dist/catalog/package-list-source.d.ts +35 -0
- package/dist/catalog/package-list-source.d.ts.map +1 -0
- package/dist/catalog/package-list-source.js +47 -0
- package/dist/catalog/package-list-source.js.map +1 -0
- package/dist/catalog/types.d.ts +65 -0
- package/dist/catalog/types.d.ts.map +1 -0
- package/dist/catalog/types.js +16 -0
- package/dist/catalog/types.js.map +1 -0
- package/dist/catalog-store-cli.d.ts +20 -0
- package/dist/catalog-store-cli.d.ts.map +1 -0
- package/dist/catalog-store-cli.js +237 -0
- package/dist/catalog-store-cli.js.map +1 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +48 -18
- package/dist/cli.js.map +1 -1
- package/dist/connector-ops/attempt-store.d.ts +5 -0
- package/dist/connector-ops/attempt-store.d.ts.map +1 -0
- package/dist/connector-ops/attempt-store.js +186 -0
- package/dist/connector-ops/attempt-store.js.map +1 -0
- package/dist/connector-ops/index.d.ts +5 -0
- package/dist/connector-ops/index.d.ts.map +1 -0
- package/dist/connector-ops/index.js +5 -0
- package/dist/connector-ops/index.js.map +1 -0
- package/dist/connector-ops/override-store.d.ts +7 -0
- package/dist/connector-ops/override-store.d.ts.map +1 -0
- package/dist/connector-ops/override-store.js +123 -0
- package/dist/connector-ops/override-store.js.map +1 -0
- package/dist/connector-ops/sanitize.d.ts +16 -0
- package/dist/connector-ops/sanitize.d.ts.map +1 -0
- package/dist/connector-ops/sanitize.js +48 -0
- package/dist/connector-ops/sanitize.js.map +1 -0
- package/dist/connector-ops/types.d.ts +76 -0
- package/dist/connector-ops/types.d.ts.map +1 -0
- package/dist/connector-ops/types.js +12 -0
- package/dist/connector-ops/types.js.map +1 -0
- package/dist/effective-listing.d.ts +11 -0
- package/dist/effective-listing.d.ts.map +1 -0
- package/dist/effective-listing.js +41 -0
- package/dist/effective-listing.js.map +1 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -3
- package/dist/errors.js.map +1 -1
- package/dist/execution-target.d.ts +16 -0
- package/dist/execution-target.d.ts.map +1 -1
- package/dist/execution-target.js +54 -0
- package/dist/execution-target.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.d.ts +3 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +3 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/instance-store.d.ts +5 -0
- package/dist/integrations/instance-store.d.ts.map +1 -0
- package/dist/integrations/instance-store.js +327 -0
- package/dist/integrations/instance-store.js.map +1 -0
- package/dist/integrations/types.d.ts +92 -0
- package/dist/integrations/types.d.ts.map +1 -0
- package/dist/integrations/types.js +40 -0
- package/dist/integrations/types.js.map +1 -0
- package/dist/magit/magit-pack-service.d.ts +436 -70
- package/dist/magit/magit-pack-service.d.ts.map +1 -1
- package/dist/magit/magit-pack-service.js +949 -202
- package/dist/magit/magit-pack-service.js.map +1 -1
- package/dist/magit/pack-to-patchset.d.ts +21 -0
- package/dist/magit/pack-to-patchset.d.ts.map +1 -1
- package/dist/magit/pack-to-patchset.js +29 -0
- package/dist/magit/pack-to-patchset.js.map +1 -1
- package/dist/magit/package-catalog-bridge.d.ts +4 -14
- package/dist/magit/package-catalog-bridge.d.ts.map +1 -1
- package/dist/magit/package-catalog-bridge.js +2 -21
- package/dist/magit/package-catalog-bridge.js.map +1 -1
- package/dist/mgc-packages.d.ts +4 -3
- package/dist/mgc-packages.d.ts.map +1 -1
- package/dist/mgc-packages.js +4 -3
- package/dist/mgc-packages.js.map +1 -1
- package/dist/openapi/openapi.json +3060 -135
- package/dist/optional-companion-url.d.ts +72 -0
- package/dist/optional-companion-url.d.ts.map +1 -0
- package/dist/optional-companion-url.js +148 -0
- package/dist/optional-companion-url.js.map +1 -0
- package/dist/platform.d.ts +29 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +138 -2
- package/dist/platform.js.map +1 -1
- package/dist/restricted-store/crypto.d.ts +10 -0
- package/dist/restricted-store/crypto.d.ts.map +1 -0
- package/dist/restricted-store/crypto.js +43 -0
- package/dist/restricted-store/crypto.js.map +1 -0
- package/dist/restricted-store/index.d.ts +4 -0
- package/dist/restricted-store/index.d.ts.map +1 -0
- package/dist/restricted-store/index.js +4 -0
- package/dist/restricted-store/index.js.map +1 -0
- package/dist/restricted-store/object-store.d.ts +12 -0
- package/dist/restricted-store/object-store.d.ts.map +1 -0
- package/dist/restricted-store/object-store.js +233 -0
- package/dist/restricted-store/object-store.js.map +1 -0
- package/dist/restricted-store/types.d.ts +78 -0
- package/dist/restricted-store/types.d.ts.map +1 -0
- package/dist/restricted-store/types.js +12 -0
- package/dist/restricted-store/types.js.map +1 -0
- package/dist/routes/catalog.d.ts +4 -0
- package/dist/routes/catalog.d.ts.map +1 -0
- package/dist/routes/catalog.js +103 -0
- package/dist/routes/catalog.js.map +1 -0
- package/dist/routes/connector-ops.d.ts +12 -0
- package/dist/routes/connector-ops.d.ts.map +1 -0
- package/dist/routes/connector-ops.js +160 -0
- package/dist/routes/connector-ops.js.map +1 -0
- package/dist/routes/integrations.d.ts +5 -0
- package/dist/routes/integrations.d.ts.map +1 -0
- package/dist/routes/integrations.js +126 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +9 -2
- package/dist/routes/memory.js.map +1 -1
- package/dist/routes/metadata.d.ts.map +1 -1
- package/dist/routes/metadata.js +459 -186
- package/dist/routes/metadata.js.map +1 -1
- package/dist/routes/pipelines.d.ts.map +1 -1
- package/dist/routes/pipelines.js +3 -2
- package/dist/routes/pipelines.js.map +1 -1
- package/dist/routes/restricted-store.d.ts +5 -0
- package/dist/routes/restricted-store.d.ts.map +1 -0
- package/dist/routes/restricted-store.js +117 -0
- package/dist/routes/restricted-store.js.map +1 -0
- package/openapi/openapi.json +3060 -135
- package/package.json +31 -7
|
@@ -1,32 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MagitPackService — server-only façade over @x12i/magit for pack versioning
|
|
3
|
-
*
|
|
2
|
+
* MagitPackService — server-only façade over @x12i/magit for pack versioning.
|
|
3
|
+
* Phase 0–5: ensureRepo, saveVersion, history, diff, packAtCommit, publish, proposals.
|
|
4
|
+
*
|
|
5
|
+
* Package catalog (Magit 2.10+): flat FR-CPS when store env is unset; store-
|
|
6
|
+
* scoped when both MAGIT_PACKAGE_STORE_SELECTOR + MAGIT_PACKAGE_TYPE_ID are
|
|
7
|
+
* set. Selector is capability-like — never log it. Half-config fails closed.
|
|
8
|
+
* Store packages: pack/list/fetch + pullStore only (no Magit restore).
|
|
4
9
|
*/
|
|
5
10
|
import { createHash } from "node:crypto";
|
|
6
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
12
|
import { join } from "node:path";
|
|
8
13
|
import { MongoClient } from "mongodb";
|
|
9
|
-
import { magit, listProposalLedgerEntries, isPatchsetError,
|
|
14
|
+
import { magit, listProposalLedgerEntries, isPatchsetError, createProgressReporter, labelDiff, redactStoreSelector, } from "@x12i/magit";
|
|
10
15
|
import { checksumMetadataPack } from "@x12i/memorix-metadata-runtime";
|
|
11
|
-
import { ServiceError } from "../errors.js";
|
|
16
|
+
import { ServiceError, redactSecrets } from "../errors.js";
|
|
12
17
|
import { isOrgRuntimeFolderSource, stripOrgRuntimeSourcesFromPack, } from "../org-sources.js";
|
|
13
|
-
import {
|
|
14
|
-
import { MAGIT_AGENTS_COLLECTION, MAGIT_SEED_AGENT_ID, metadataPackToPackagePayload, packagePayloadToMetadataPack, packToPatchset, parseRecordId, parseSaveVersionIdempotencyKey, recordIdForDefinition, recordsToPack, } from "./pack-to-patchset.js";
|
|
18
|
+
import { MAGIT_AGENTS_COLLECTION, MAGIT_SEED_AGENT_ID, packToPackagePayload, packToPatchset, packToDefinitionRecords, parseRecordId, parseSaveVersionIdempotencyKey, recordIdForDefinition, recordsToPack, packagePayloadToPack, } from "./pack-to-patchset.js";
|
|
15
19
|
import { mergeProposalIntoPack, } from "./proposal-to-pack.js";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Central package catalog (FR-CPS). Org-free — kind/id/version only. Distinct
|
|
22
|
+
// from the org-scoped saveVersion/history/diff/packAtCommit API above, which
|
|
23
|
+
// remains this environment's own live-metadata history.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Fixed local working directory for the shared package-catalog repo. Not an
|
|
26
|
+
* org — purely a local disk key; the remote binding is the catalog tenant
|
|
27
|
+
* (resolveMagitCatalogTenant), applied uniformly by ensureRepo. */
|
|
28
|
+
const PACKAGE_CATALOG_LOCAL_DIR = "_package-catalog";
|
|
16
29
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
30
|
+
* Any error surfaced from a catalog-flagged operation that isn't already a
|
|
31
|
+
* ServiceError or PatchsetError (both handled earlier in mapError, and
|
|
32
|
+
* neither indicates a reachability problem) is treated as central-storage
|
|
33
|
+
* unreachable — network/DNS failure, connection refused, and auth failure
|
|
34
|
+
* (missing/expired MAGIT_TOKEN) all leave the catalog equally unusable from
|
|
35
|
+
* an operator's "is Magit healthy" perspective, and the FR (§8's retirement)
|
|
36
|
+
* requires surfacing every one of them rather than silently falling back to
|
|
37
|
+
* local-only behaviour. The first line of the error message is kept as the
|
|
38
|
+
* classified reason for diagnostics.
|
|
19
39
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
/** First line of an error, with Magit store-selector material redacted. */
|
|
41
|
+
export function classifyCatalogUnreachable(err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
const first = message.split("\n")[0] ?? message;
|
|
44
|
+
return String(redactSecrets(redactStoreSelector(first)));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Pure record-level pack compare used for Mode A (immutable store payloads)
|
|
48
|
+
* and unit tests. Keys definitions by kind/id with stable JSON equality.
|
|
49
|
+
*/
|
|
50
|
+
export function diffMetadataPacks(agentId, from, to, fromPack, toPack) {
|
|
51
|
+
const before = new Map();
|
|
52
|
+
const after = new Map();
|
|
53
|
+
for (const r of packToDefinitionRecords(fromPack)) {
|
|
54
|
+
before.set(`${r.kind}/${r.id}`, {
|
|
55
|
+
kind: r.kind,
|
|
56
|
+
id: r.id,
|
|
57
|
+
body: r.definition,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (const r of packToDefinitionRecords(toPack)) {
|
|
61
|
+
after.set(`${r.kind}/${r.id}`, {
|
|
62
|
+
kind: r.kind,
|
|
63
|
+
id: r.id,
|
|
64
|
+
body: r.definition,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
68
|
+
const changes = [];
|
|
69
|
+
for (const key of [...keys].sort()) {
|
|
70
|
+
const b = before.get(key);
|
|
71
|
+
const a = after.get(key);
|
|
72
|
+
if (!b && a) {
|
|
73
|
+
changes.push({ kind: a.kind, id: a.id, change: "added" });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (b && !a) {
|
|
77
|
+
changes.push({ kind: b.kind, id: b.id, change: "removed" });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (b && a) {
|
|
81
|
+
const fields = fieldDiffs(b.body, a.body);
|
|
82
|
+
if (fields?.length) {
|
|
83
|
+
changes.push({
|
|
84
|
+
kind: a.kind,
|
|
85
|
+
id: a.id,
|
|
86
|
+
change: "changed",
|
|
87
|
+
fields,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { agentId, from, to, changes };
|
|
30
93
|
}
|
|
31
94
|
const MEMORIX_LENS = {
|
|
32
95
|
[MAGIT_AGENTS_COLLECTION]: {
|
|
@@ -56,12 +119,99 @@ export function isMetadataMagitEnabled() {
|
|
|
56
119
|
// Default-on when unset or any other truthy-ish value (including "1"/"true"/"on").
|
|
57
120
|
return true;
|
|
58
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* `MAGIT_REPO_ROOT` is the parent of each repo's LOCAL working checkout / cache
|
|
124
|
+
* (per-org live-history repos and the shared package catalog repo alike). It is
|
|
125
|
+
* demoted, not removed, by the central-catalog cutover (X12-65): it no longer
|
|
126
|
+
* doubles as the catalog — that is central storage, bound via `storageTenant`
|
|
127
|
+
* (see `resolveMagitCatalogTenant`). This directory can be wiped and
|
|
128
|
+
* re-bootstrapped from central storage without data loss.
|
|
129
|
+
*/
|
|
59
130
|
export function resolveMagitRepoRootParent() {
|
|
60
131
|
const fromEnv = process.env.MAGIT_REPO_ROOT?.trim();
|
|
61
132
|
if (fromEnv)
|
|
62
133
|
return fromEnv;
|
|
63
134
|
return join(process.cwd(), ".tmp", "magit-repos");
|
|
64
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Central Magit package-catalog tenant (FR-CPS-1). This is a fixed
|
|
138
|
+
* developer/solution scope for Memorix's own package storage — it must NEVER
|
|
139
|
+
* be a runtime `orgId`. Org only appears as a Restore install target, never as
|
|
140
|
+
* the catalog key. Default `"x12i"` (dedicated Memorix catalog workspace).
|
|
141
|
+
*/
|
|
142
|
+
export function resolveMagitCatalogTenant() {
|
|
143
|
+
return process.env.MAGIT_CATALOG_TENANT?.trim() || "x12i";
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Magit storage repo id for the central package catalog (FR-CPS). Defaults to
|
|
147
|
+
* `memorix` (dedicated catalog repo). Override with MAGIT_CATALOG_REPO only when
|
|
148
|
+
* targeting a non-standard host layout.
|
|
149
|
+
*/
|
|
150
|
+
export function resolveMagitCatalogRepoName() {
|
|
151
|
+
return process.env.MAGIT_CATALOG_REPO?.trim() || "memorix";
|
|
152
|
+
}
|
|
153
|
+
/** Write-once on createGlobalStore — kinds omitted can only be fixed with a second store. */
|
|
154
|
+
export const DEFAULT_TYPED_STORE_ALLOWED_KINDS = [
|
|
155
|
+
"agent",
|
|
156
|
+
"connector",
|
|
157
|
+
"service",
|
|
158
|
+
"agent-bundle",
|
|
159
|
+
];
|
|
160
|
+
/**
|
|
161
|
+
* Env var *name* Magit uses to load the central-storage bearer token.
|
|
162
|
+
* Priority: MAGIT_STORAGE_TOKEN_ENV (explicit name) → AUTHIX_TOKEN if set → MAGIT_TOKEN.
|
|
163
|
+
*/
|
|
164
|
+
export function resolveMagitStorageTokenEnv() {
|
|
165
|
+
const explicit = process.env.MAGIT_STORAGE_TOKEN_ENV?.trim();
|
|
166
|
+
if (explicit)
|
|
167
|
+
return explicit;
|
|
168
|
+
if (process.env.AUTHIX_TOKEN?.trim())
|
|
169
|
+
return "AUTHIX_TOKEN";
|
|
170
|
+
return "MAGIT_TOKEN";
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Magit 2.10 typed global store binding.
|
|
174
|
+
* - both MAGIT_PACKAGE_STORE_SELECTOR + MAGIT_PACKAGE_TYPE_ID → store path
|
|
175
|
+
* - neither → flat catalog (2.9 compatible)
|
|
176
|
+
* - only one set → fail closed (never half-use store APIs)
|
|
177
|
+
*/
|
|
178
|
+
export function resolveMagitTypedStoreConfig() {
|
|
179
|
+
const storeSelector = process.env.MAGIT_PACKAGE_STORE_SELECTOR?.trim() || "";
|
|
180
|
+
const packageTypeId = process.env.MAGIT_PACKAGE_TYPE_ID?.trim() || "";
|
|
181
|
+
if (!storeSelector && !packageTypeId)
|
|
182
|
+
return null;
|
|
183
|
+
if (!storeSelector || !packageTypeId) {
|
|
184
|
+
throw new ServiceError("VALIDATION", "MAGIT_PACKAGE_STORE_SELECTOR and MAGIT_PACKAGE_TYPE_ID must both be set for store-scoped packs, or both unset for the flat 2.9 catalog", 400);
|
|
185
|
+
}
|
|
186
|
+
const publisherId = process.env.MAGIT_PACKAGE_PUBLISHER_ID?.trim() || "memorix";
|
|
187
|
+
const publisherAppId = process.env.MAGIT_PACKAGE_PUBLISHER_APP_ID?.trim() || "memorix-service";
|
|
188
|
+
return {
|
|
189
|
+
storeSelector,
|
|
190
|
+
packageTypeId,
|
|
191
|
+
publisher: { id: publisherId, appId: publisherAppId || undefined },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Whether store env is configured (yes/no only — never selector values). */
|
|
195
|
+
export function magitTypedStoreConfigured() {
|
|
196
|
+
try {
|
|
197
|
+
return resolveMagitTypedStoreConfig() != null;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Whether MAGIT_PACKAGE_TYPE_ID is set (safe to report; not a capability secret). */
|
|
204
|
+
export function magitPackageTypeIdConfigured() {
|
|
205
|
+
return Boolean(process.env.MAGIT_PACKAGE_TYPE_ID?.trim());
|
|
206
|
+
}
|
|
207
|
+
/** Whether MAGIT_PACKAGE_STORE_SELECTOR is set (yes/no only — never the value). */
|
|
208
|
+
export function magitStoreSelectorConfigured() {
|
|
209
|
+
return Boolean(process.env.MAGIT_PACKAGE_STORE_SELECTOR?.trim());
|
|
210
|
+
}
|
|
211
|
+
/** Structure language id for diagnostics when set. Never the store selector. */
|
|
212
|
+
export function magitPackageTypeIdForDiagnostics() {
|
|
213
|
+
return process.env.MAGIT_PACKAGE_TYPE_ID?.trim() || null;
|
|
214
|
+
}
|
|
65
215
|
function isLiveRef(ref) {
|
|
66
216
|
return ref === "live" || ref === "LIVE";
|
|
67
217
|
}
|
|
@@ -108,9 +258,14 @@ function unwrapDefinitionBody(body) {
|
|
|
108
258
|
export class MagitPackService {
|
|
109
259
|
cfg;
|
|
110
260
|
ensured = new Set();
|
|
261
|
+
catalogHealth = { reachable: null, reason: null };
|
|
111
262
|
constructor(cfg) {
|
|
112
263
|
this.cfg = cfg;
|
|
113
264
|
}
|
|
265
|
+
/** Last-observed central package-catalog reachability (task 5 diagnostics). */
|
|
266
|
+
catalogHealthStatus() {
|
|
267
|
+
return this.catalogHealth;
|
|
268
|
+
}
|
|
114
269
|
progress() {
|
|
115
270
|
const log = this.cfg.log;
|
|
116
271
|
createProgressReporter({ mode: "json" });
|
|
@@ -120,33 +275,10 @@ export class MagitPackService {
|
|
|
120
275
|
},
|
|
121
276
|
};
|
|
122
277
|
}
|
|
123
|
-
storageTokenEnvName() {
|
|
124
|
-
return this.cfg.storageTokenEnv?.trim() || resolveMagitStorageTokenEnv();
|
|
125
|
-
}
|
|
126
|
-
storageUrl() {
|
|
127
|
-
return (this.cfg.storageUrl ||
|
|
128
|
-
process.env.MAGIT_STORAGE_URL?.trim() ||
|
|
129
|
-
"https://magit-storage.x12i.com");
|
|
130
|
-
}
|
|
131
|
-
/** True when Authix/Magit token is present for remote catalog push/pull. */
|
|
132
|
-
hasRemoteCatalog() {
|
|
133
|
-
const env = this.storageTokenEnvName();
|
|
134
|
-
return Boolean(process.env[env]?.trim());
|
|
135
|
-
}
|
|
136
278
|
repoRootFor(orgId) {
|
|
137
279
|
return join(this.cfg.repoRootParent, orgId);
|
|
138
280
|
}
|
|
139
|
-
catalogRepoRoot() {
|
|
140
|
-
return this.repoRootFor(PACKAGE_CATALOG_ORG);
|
|
141
|
-
}
|
|
142
281
|
async ensureRepo(orgId) {
|
|
143
|
-
await this.ensureRepoInternal(orgId, { catalog: false });
|
|
144
|
-
}
|
|
145
|
-
/** Dedicated Magit root for org-free package catalog (`packages/`). */
|
|
146
|
-
async ensureCatalogRepo() {
|
|
147
|
-
await this.ensureRepoInternal(PACKAGE_CATALOG_ORG, { catalog: true });
|
|
148
|
-
}
|
|
149
|
-
async ensureRepoInternal(orgId, opts) {
|
|
150
282
|
if (this.ensured.has(orgId))
|
|
151
283
|
return;
|
|
152
284
|
const { tick } = this.progress();
|
|
@@ -158,14 +290,13 @@ export class MagitPackService {
|
|
|
158
290
|
const repo = await magit.open({ repoRoot });
|
|
159
291
|
const configPath = join(repoRoot, ".magit", "config.json");
|
|
160
292
|
if (!existsSync(configPath)) {
|
|
161
|
-
tick("ensureRepo.init", { orgId
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
293
|
+
tick("ensureRepo.init", { orgId });
|
|
294
|
+
const isPackageCatalog = orgId === PACKAGE_CATALOG_LOCAL_DIR;
|
|
295
|
+
const repoName = isPackageCatalog
|
|
296
|
+
? resolveMagitCatalogRepoName()
|
|
297
|
+
: `memorix-metadata-${orgId}`;
|
|
165
298
|
await repo.init({
|
|
166
|
-
name:
|
|
167
|
-
? "memorix-package-catalog"
|
|
168
|
-
: `memorix-metadata-${orgId}`,
|
|
299
|
+
name: repoName,
|
|
169
300
|
driver: "transform",
|
|
170
301
|
urlEnv: this.cfg.mongoUrlEnv,
|
|
171
302
|
db: this.cfg.metadataDb,
|
|
@@ -180,11 +311,19 @@ export class MagitPackService {
|
|
|
180
311
|
encodingProfile: "current",
|
|
181
312
|
},
|
|
182
313
|
storageProvider: "x12i-http",
|
|
183
|
-
storageUrl: this.storageUrl
|
|
184
|
-
|
|
185
|
-
|
|
314
|
+
storageUrl: this.cfg.storageUrl ||
|
|
315
|
+
process.env.MAGIT_STORAGE_URL?.trim() ||
|
|
316
|
+
"https://magit-storage.x12i.com",
|
|
317
|
+
storageTokenEnv: this.cfg.storageTokenEnv || "MAGIT_TOKEN",
|
|
318
|
+
// FR-CPS-1: catalog tenant is a fixed developer/solution scope, never
|
|
319
|
+
// the runtime org — `orgId` here only names this repo's LOCAL working
|
|
320
|
+
// directory (see repoRootFor), not its central storage binding.
|
|
321
|
+
storageTenant: resolveMagitCatalogTenant(),
|
|
186
322
|
environmentName: "draft",
|
|
187
|
-
|
|
323
|
+
// FR-CPS-3: bootstrap/sync with central storage instead of staying
|
|
324
|
+
// local-only. The interim §8 bridge (skipRemoteBootstrap: true) is
|
|
325
|
+
// retired — no silent local-history fallback.
|
|
326
|
+
skipRemoteBootstrap: false,
|
|
188
327
|
});
|
|
189
328
|
writeFileSync(join(repoRoot, ".magitignore"), MAGITIGNORE, "utf8");
|
|
190
329
|
await this.seedAgentsCollectionCommit(repo);
|
|
@@ -196,180 +335,249 @@ export class MagitPackService {
|
|
|
196
335
|
}
|
|
197
336
|
}
|
|
198
337
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
338
|
+
* Open the shared central package-catalog repo (org-free). Opening the
|
|
339
|
+
* LOCAL repo directory can succeed even when central storage itself is
|
|
340
|
+
* unreachable (`repo.init` only warns on a failed remote bootstrap by
|
|
341
|
+
* default — see `strictRemote` — and `magit.open` is a local-only call), so
|
|
342
|
+
* this deliberately does NOT mark catalogHealth reachable; only an actual
|
|
343
|
+
* round-trip (pull/pack/list/fetch) that completes should do that.
|
|
201
344
|
*/
|
|
202
|
-
async
|
|
203
|
-
await this.
|
|
204
|
-
|
|
205
|
-
return { remote: false };
|
|
206
|
-
const { tick } = this.progress();
|
|
207
|
-
try {
|
|
208
|
-
this.ensureMongoEnv();
|
|
209
|
-
const repo = await magit.open({ repoRoot: this.catalogRepoRoot() });
|
|
210
|
-
tick("catalog.pull", { full: Boolean(opts?.full) });
|
|
211
|
-
const result = await repo.pull({ full: Boolean(opts?.full) });
|
|
212
|
-
return { remote: true, downloaded: result.downloaded };
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
// Local catalog still usable; surface via log only for soft pull failures.
|
|
216
|
-
this.cfg.log?.("[magit] catalog.pull failed", {
|
|
217
|
-
error: err instanceof Error ? err.message : String(err),
|
|
218
|
-
});
|
|
219
|
-
return { remote: true };
|
|
220
|
-
}
|
|
345
|
+
async ensureCatalogRepo() {
|
|
346
|
+
await this.ensureRepo(PACKAGE_CATALOG_LOCAL_DIR);
|
|
347
|
+
return magit.open({ repoRoot: this.repoRootFor(PACKAGE_CATALOG_LOCAL_DIR) });
|
|
221
348
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
tick("catalog.push", {});
|
|
231
|
-
const result = await repo.push();
|
|
232
|
-
return { remote: true, uploaded: result.uploaded };
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
throw this.mapError(err, "failed to push package catalog");
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
/** Pack a MetadataPack into Magit packages/ (does not change live metadata). */
|
|
239
|
-
async packCatalogPackage(input) {
|
|
240
|
-
const kind = input.kind;
|
|
241
|
-
const id = input.id?.trim();
|
|
242
|
-
const message = input.message?.trim();
|
|
243
|
-
if (!id)
|
|
244
|
-
throw new ServiceError("VALIDATION", "id required", 400);
|
|
245
|
-
if (!message)
|
|
246
|
-
throw new ServiceError("VALIDATION", "message required", 400);
|
|
247
|
-
await this.ensureCatalogRepo();
|
|
349
|
+
/**
|
|
350
|
+
* Pack a payload into the central catalog (FR-CPS-4), or into the typed
|
|
351
|
+
* global store when MAGIT_PACKAGE_STORE_SELECTOR + MAGIT_PACKAGE_TYPE_ID are set.
|
|
352
|
+
*
|
|
353
|
+
* Flat: pull then packPackageFR without selector.
|
|
354
|
+
* Store: packPackageFR with selector/type/kind/publisher; do not full-tree pull.
|
|
355
|
+
*/
|
|
356
|
+
async packPackage(opts) {
|
|
248
357
|
const { tick } = this.progress();
|
|
249
358
|
try {
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
const payload =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
id
|
|
260
|
-
message,
|
|
261
|
-
payload,
|
|
262
|
-
mode,
|
|
263
|
-
associations: input.associations
|
|
359
|
+
const store = resolveMagitTypedStoreConfig();
|
|
360
|
+
const repo = await this.ensureCatalogRepo();
|
|
361
|
+
const { payload } = packToPackagePayload(opts.pack);
|
|
362
|
+
tick("packPackage.start", {
|
|
363
|
+
kind: opts.kind,
|
|
364
|
+
id: opts.id,
|
|
365
|
+
storeScoped: Boolean(store),
|
|
366
|
+
});
|
|
367
|
+
if (store) {
|
|
368
|
+
const publisher = opts.actor?.id
|
|
264
369
|
? {
|
|
265
|
-
|
|
266
|
-
|
|
370
|
+
id: opts.actor.id,
|
|
371
|
+
appId: store.publisher.appId,
|
|
267
372
|
}
|
|
268
|
-
:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
373
|
+
: store.publisher;
|
|
374
|
+
const result = await repo.packPackageFR({
|
|
375
|
+
storeSelector: store.storeSelector,
|
|
376
|
+
packageTypeId: store.packageTypeId,
|
|
377
|
+
kind: opts.kind,
|
|
378
|
+
id: opts.id,
|
|
379
|
+
message: opts.message,
|
|
380
|
+
payload,
|
|
381
|
+
associations: opts.associations,
|
|
382
|
+
mode: opts.mode,
|
|
383
|
+
publisher,
|
|
384
|
+
actor: opts.actor,
|
|
385
|
+
});
|
|
386
|
+
// Magit 2.11+ throws when default publish fails; still bind health to
|
|
387
|
+
// published so clients and diagnostics stay honest if publish:false ever used.
|
|
388
|
+
const published = result.published === true;
|
|
389
|
+
if (published) {
|
|
390
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
this.catalogHealth = {
|
|
394
|
+
reachable: false,
|
|
395
|
+
reason: "pack completed without remote publish (published=false)",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
tick("packPackage.done", {
|
|
399
|
+
version: result.version,
|
|
400
|
+
reused: result.reused,
|
|
401
|
+
published,
|
|
402
|
+
storeScoped: true,
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
version: result.version,
|
|
406
|
+
checksum: result.checksum,
|
|
407
|
+
reused: result.reused,
|
|
408
|
+
published,
|
|
409
|
+
storeId: result.storeId,
|
|
410
|
+
packageTypeId: result.packageTypeId ?? store.packageTypeId,
|
|
411
|
+
};
|
|
277
412
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
413
|
+
await repo.pull();
|
|
414
|
+
const result = await repo.packPackageFR({
|
|
415
|
+
kind: opts.kind,
|
|
416
|
+
id: opts.id,
|
|
417
|
+
message: opts.message,
|
|
418
|
+
payload,
|
|
419
|
+
associations: opts.associations,
|
|
420
|
+
mode: opts.mode,
|
|
421
|
+
pins: opts.pins,
|
|
422
|
+
actor: opts.actor,
|
|
283
423
|
});
|
|
424
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
425
|
+
tick("packPackage.done", { version: result.version, reused: result.reused });
|
|
284
426
|
return {
|
|
285
427
|
version: result.version,
|
|
286
428
|
checksum: result.checksum,
|
|
287
429
|
reused: result.reused,
|
|
288
|
-
remote: push.remote,
|
|
289
430
|
};
|
|
290
431
|
}
|
|
291
432
|
catch (err) {
|
|
292
|
-
if (
|
|
293
|
-
throw
|
|
294
|
-
}
|
|
295
|
-
throw this.mapError(err, "failed to pack package into Magit catalog");
|
|
433
|
+
if (err instanceof ServiceError)
|
|
434
|
+
throw err;
|
|
435
|
+
throw this.mapError(err, "failed to pack package version", { catalog: true });
|
|
296
436
|
}
|
|
297
437
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
438
|
+
/**
|
|
439
|
+
* List packages — flat catalog or store-scoped list (kind required on store path).
|
|
440
|
+
* Org-free: kind + id only; never a store selector from the client.
|
|
441
|
+
*/
|
|
442
|
+
async listPackages(filter) {
|
|
301
443
|
try {
|
|
302
|
-
|
|
303
|
-
const repo = await
|
|
444
|
+
const store = resolveMagitTypedStoreConfig();
|
|
445
|
+
const repo = await this.ensureCatalogRepo();
|
|
446
|
+
if (store) {
|
|
447
|
+
if (!filter?.kind) {
|
|
448
|
+
throw new ServiceError("VALIDATION", "kind is required when Magit typed store is configured", 400);
|
|
449
|
+
}
|
|
450
|
+
await repo.pullStore({ storeSelector: store.storeSelector });
|
|
451
|
+
const items = await repo.listPackagesFR({
|
|
452
|
+
storeSelector: store.storeSelector,
|
|
453
|
+
packageTypeId: store.packageTypeId,
|
|
454
|
+
kind: filter.kind,
|
|
455
|
+
id: filter.id,
|
|
456
|
+
publisher: filter.publisher,
|
|
457
|
+
maxCount: filter.maxCount,
|
|
458
|
+
});
|
|
459
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
460
|
+
return items.map((item) => ({
|
|
461
|
+
kind: item.kind,
|
|
462
|
+
id: item.id,
|
|
463
|
+
version: item.version,
|
|
464
|
+
checksum: item.checksum,
|
|
465
|
+
message: item.message,
|
|
466
|
+
createdAt: item.createdAt,
|
|
467
|
+
associations: item.associations,
|
|
468
|
+
mode: item.mode,
|
|
469
|
+
publisher: item.publisher,
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
await repo.pull();
|
|
304
473
|
const items = await repo.listPackagesFR({
|
|
305
474
|
kind: filter?.kind,
|
|
306
|
-
id: filter?.id
|
|
475
|
+
id: filter?.id,
|
|
307
476
|
maxCount: filter?.maxCount,
|
|
308
477
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const k = row.kind;
|
|
312
|
-
return k === "agent" || k === "connector" || k === "service";
|
|
313
|
-
})
|
|
314
|
-
.map((row) => ({
|
|
315
|
-
kind: row.kind,
|
|
316
|
-
id: row.id,
|
|
317
|
-
version: row.version,
|
|
318
|
-
commit: row.version,
|
|
319
|
-
checksum: row.checksum,
|
|
320
|
-
message: row.message,
|
|
321
|
-
createdAt: row.createdAt,
|
|
322
|
-
mode: row.mode,
|
|
323
|
-
associations: row.associations
|
|
324
|
-
? {
|
|
325
|
-
connectorIds: [...(row.associations.connectorIds ?? [])].sort(),
|
|
326
|
-
serviceIds: [...(row.associations.serviceIds ?? [])].sort(),
|
|
327
|
-
}
|
|
328
|
-
: undefined,
|
|
329
|
-
}));
|
|
330
|
-
return { items: mapped, remote: pull.remote };
|
|
478
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
479
|
+
return items;
|
|
331
480
|
}
|
|
332
481
|
catch (err) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const pull = await this.syncCatalogPull({ full: true });
|
|
346
|
-
const { tick } = this.progress();
|
|
482
|
+
if (err instanceof ServiceError)
|
|
483
|
+
throw err;
|
|
484
|
+
throw this.mapError(err, "failed to list packages", { catalog: true });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Fetch a package version WITH its payload.
|
|
489
|
+
* Flat: full pull + fetchPackageFR(ref).
|
|
490
|
+
* Store: pullStore + fetchPackageFR({ storeSelector, packageTypeId, kind, id, version }).
|
|
491
|
+
* Restore remains product-side (install after fetch) — no Magit store restore.
|
|
492
|
+
*/
|
|
493
|
+
async fetchPackage(ref) {
|
|
347
494
|
try {
|
|
348
|
-
|
|
349
|
-
const repo = await
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
495
|
+
const store = resolveMagitTypedStoreConfig();
|
|
496
|
+
const repo = await this.ensureCatalogRepo();
|
|
497
|
+
if (store) {
|
|
498
|
+
await repo.pullStore({ storeSelector: store.storeSelector });
|
|
499
|
+
const fetched = await repo.fetchPackageFR({
|
|
500
|
+
storeSelector: store.storeSelector,
|
|
501
|
+
packageTypeId: store.packageTypeId,
|
|
502
|
+
kind: ref.kind,
|
|
503
|
+
id: ref.id,
|
|
504
|
+
version: ref.version,
|
|
505
|
+
});
|
|
506
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
507
|
+
return {
|
|
508
|
+
ref: {
|
|
509
|
+
kind: fetched.kind,
|
|
510
|
+
id: fetched.id,
|
|
511
|
+
version: fetched.version,
|
|
512
|
+
},
|
|
513
|
+
checksum: fetched.checksum,
|
|
514
|
+
payload: fetched.payload,
|
|
515
|
+
associations: fetched.associations,
|
|
516
|
+
publisher: fetched.publisher,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
await repo.pull({ full: true });
|
|
520
|
+
const fetched = await repo.fetchPackageFR(ref);
|
|
521
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
522
|
+
return fetched;
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
if (err instanceof ServiceError)
|
|
526
|
+
throw err;
|
|
527
|
+
throw this.mapError(err, "failed to fetch package version", { catalog: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Compare two package versions (or a version vs live/installed).
|
|
532
|
+
*
|
|
533
|
+
* Mode A (typed store): package versions are immutable store payloads, not
|
|
534
|
+
* catalog-repo commits. Fetch both refs (or accept an installed pack for the
|
|
535
|
+
* installed/live sentinel) and produce a record-level PackDiffResult.
|
|
536
|
+
*
|
|
537
|
+
* Mode B (flat FR-CPS): reuses the org-scoped commit diff against the
|
|
538
|
+
* catalog local repo. `sliceAgentId` is the catalog-internal record grouping
|
|
539
|
+
* packPackage embeds, not a runtime org.
|
|
540
|
+
*/
|
|
541
|
+
async diffPackages(sliceAgentId, opts) {
|
|
542
|
+
const from = opts.from?.trim();
|
|
543
|
+
const to = opts.to?.trim();
|
|
544
|
+
if (!from || !to) {
|
|
545
|
+
throw new ServiceError("VALIDATION", "from and to required", 400);
|
|
546
|
+
}
|
|
547
|
+
const store = resolveMagitTypedStoreConfig();
|
|
548
|
+
if (!store) {
|
|
549
|
+
return this.diff(PACKAGE_CATALOG_LOCAL_DIR, sliceAgentId, { from, to });
|
|
550
|
+
}
|
|
551
|
+
const kind = opts.kind;
|
|
552
|
+
const id = opts.id?.trim();
|
|
553
|
+
if (!kind || !id) {
|
|
554
|
+
throw new ServiceError("VALIDATION", "kind and id required for typed-store package compare", 400);
|
|
555
|
+
}
|
|
556
|
+
const resolveSide = async (ref) => {
|
|
557
|
+
if (ref === "live" || ref === "LIVE" || ref === "installed") {
|
|
558
|
+
if (!opts.installedPack) {
|
|
559
|
+
throw new ServiceError("VALIDATION", "installed package payload required when comparing against installed/live", 400);
|
|
560
|
+
}
|
|
561
|
+
return opts.installedPack;
|
|
562
|
+
}
|
|
563
|
+
const fetched = await this.fetchPackage({
|
|
564
|
+
kind: kind,
|
|
353
565
|
id,
|
|
354
|
-
version,
|
|
566
|
+
version: ref,
|
|
355
567
|
});
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
associations: fetched.associations
|
|
363
|
-
? {
|
|
364
|
-
connectorIds: [...(fetched.associations.connectorIds ?? [])].sort(),
|
|
365
|
-
serviceIds: [...(fetched.associations.serviceIds ?? [])].sort(),
|
|
366
|
-
}
|
|
367
|
-
: undefined,
|
|
368
|
-
remote: pull.remote,
|
|
369
|
-
};
|
|
568
|
+
return packagePayloadToPack(sliceAgentId, fetched.payload);
|
|
569
|
+
};
|
|
570
|
+
try {
|
|
571
|
+
const fromPack = await resolveSide(from);
|
|
572
|
+
const toPack = await resolveSide(to);
|
|
573
|
+
return diffMetadataPacks(sliceAgentId, from, to, fromPack, toPack);
|
|
370
574
|
}
|
|
371
575
|
catch (err) {
|
|
372
|
-
|
|
576
|
+
if (err instanceof ServiceError)
|
|
577
|
+
throw err;
|
|
578
|
+
throw this.mapError(err, "failed to compare package versions", {
|
|
579
|
+
catalog: true,
|
|
580
|
+
});
|
|
373
581
|
}
|
|
374
582
|
}
|
|
375
583
|
async saveVersion(orgId, pack, opts) {
|
|
@@ -902,18 +1110,557 @@ export class MagitPackService {
|
|
|
902
1110
|
process.env[this.cfg.mongoUrlEnv] = this.cfg.mongoUri;
|
|
903
1111
|
}
|
|
904
1112
|
}
|
|
905
|
-
mapError(err, fallback) {
|
|
1113
|
+
mapError(err, fallback, opts) {
|
|
906
1114
|
if (err instanceof ServiceError)
|
|
907
1115
|
return err;
|
|
908
1116
|
if (isPatchsetError(err)) {
|
|
909
|
-
|
|
1117
|
+
const safe = String(redactSecrets(redactStoreSelector(err.message)));
|
|
1118
|
+
return new ServiceError("VALIDATION", `${fallback}: ${safe}`, 400);
|
|
910
1119
|
}
|
|
911
|
-
|
|
912
|
-
|
|
1120
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
1121
|
+
const message = String(redactSecrets(redactStoreSelector(raw)));
|
|
1122
|
+
if (opts?.catalog) {
|
|
1123
|
+
const reason = classifyCatalogUnreachable(err);
|
|
1124
|
+
this.catalogHealth = { reachable: false, reason };
|
|
1125
|
+
return new ServiceError("MAGIT_CATALOG_UNREACHABLE", `${fallback}: central package catalog is not reachable (${reason})`, 503);
|
|
913
1126
|
}
|
|
914
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
915
1127
|
return new ServiceError("MAGIT_UNAVAILABLE", `${fallback}: ${message}`, 503);
|
|
916
1128
|
}
|
|
1129
|
+
// -------------------------------------------------------------------------
|
|
1130
|
+
// Catalog store administration (Magit 2.12.1 owner operations)
|
|
1131
|
+
// -------------------------------------------------------------------------
|
|
1132
|
+
/**
|
|
1133
|
+
* Provision a typed global package store (create + pushStore + verify).
|
|
1134
|
+
* Returns the selector EXACTLY ONCE — caller must write it to a secret store.
|
|
1135
|
+
* Duplicate packageTypeId stores abort unless force=true.
|
|
1136
|
+
* allowedKinds is write-once on the Magit store — decide the list before create.
|
|
1137
|
+
*/
|
|
1138
|
+
async provisionTypedPackageStore(opts) {
|
|
1139
|
+
const packageTypeId = opts?.packageTypeId?.trim() ||
|
|
1140
|
+
process.env.MAGIT_PACKAGE_TYPE_ID?.trim() ||
|
|
1141
|
+
"memorix.packages.v1";
|
|
1142
|
+
const allowedKinds = opts?.allowedKinds && opts.allowedKinds.length > 0
|
|
1143
|
+
? opts.allowedKinds.map((k) => k.trim()).filter(Boolean)
|
|
1144
|
+
: [...DEFAULT_TYPED_STORE_ALLOWED_KINDS];
|
|
1145
|
+
if (allowedKinds.length === 0) {
|
|
1146
|
+
throw new ServiceError("VALIDATION", "allowedKinds must list at least one package kind (write-once on create)", 400);
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const repo = await this.ensureCatalogRepo();
|
|
1150
|
+
const existing = await repo.listStores();
|
|
1151
|
+
const match = existing.find((s) => s.typeDescriptor?.packageTypeId === packageTypeId);
|
|
1152
|
+
if (match && !opts?.force) {
|
|
1153
|
+
throw new ServiceError("VALIDATION", `A store for packageTypeId=${packageTypeId} already exists (createdAt=${match.createdAt}). Pass force to create another, or use recover with an explicit storeId.`, 409);
|
|
1154
|
+
}
|
|
1155
|
+
const created = await repo.createGlobalStore({
|
|
1156
|
+
packageTypeId,
|
|
1157
|
+
allowedKinds,
|
|
1158
|
+
});
|
|
1159
|
+
const pushed = await repo.pushStore({
|
|
1160
|
+
storeSelector: created.storeSelector,
|
|
1161
|
+
packageTypeId: created.packageTypeId,
|
|
1162
|
+
});
|
|
1163
|
+
// Metadata-only verify (listStores never returns selectors).
|
|
1164
|
+
await repo.listStores();
|
|
1165
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1166
|
+
return {
|
|
1167
|
+
storeId: created.storeId,
|
|
1168
|
+
storeSelector: created.storeSelector,
|
|
1169
|
+
packageTypeId: created.packageTypeId,
|
|
1170
|
+
allowedKinds: [...allowedKinds],
|
|
1171
|
+
pushed: {
|
|
1172
|
+
uploaded: pushed.uploaded ?? 0,
|
|
1173
|
+
skipped: pushed.skipped ?? 0,
|
|
1174
|
+
},
|
|
1175
|
+
existingStoreCount: existing.length,
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
catch (err) {
|
|
1179
|
+
if (err instanceof ServiceError)
|
|
1180
|
+
throw err;
|
|
1181
|
+
throw this.mapError(err, "failed to provision typed package store", {
|
|
1182
|
+
catalog: true,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/** Mode A required: package / release channels live on the typed store only. */
|
|
1187
|
+
requireTypedStoreConfig() {
|
|
1188
|
+
const store = resolveMagitTypedStoreConfig();
|
|
1189
|
+
if (!store) {
|
|
1190
|
+
throw new ServiceError("VALIDATION", "Package channels and release sets require Mode A (MAGIT_PACKAGE_STORE_SELECTOR + MAGIT_PACKAGE_TYPE_ID)", 400);
|
|
1191
|
+
}
|
|
1192
|
+
return store;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Promote a package channel pointer (Mode A). Packages never move — only the pointer.
|
|
1196
|
+
*/
|
|
1197
|
+
async setPackageChannel(opts) {
|
|
1198
|
+
const store = this.requireTypedStoreConfig();
|
|
1199
|
+
const kind = String(opts.kind ?? "").trim();
|
|
1200
|
+
const id = String(opts.id ?? "").trim();
|
|
1201
|
+
const channel = String(opts.channel ?? "").trim();
|
|
1202
|
+
const version = String(opts.version ?? "").trim();
|
|
1203
|
+
if (!kind || !id || !channel || !version) {
|
|
1204
|
+
throw new ServiceError("VALIDATION", "kind, id, channel, and version are required", 400);
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const repo = await this.ensureCatalogRepo();
|
|
1208
|
+
const result = await repo.setPackageChannel({
|
|
1209
|
+
storeSelector: store.storeSelector,
|
|
1210
|
+
packageTypeId: store.packageTypeId,
|
|
1211
|
+
kind,
|
|
1212
|
+
id,
|
|
1213
|
+
channel,
|
|
1214
|
+
version,
|
|
1215
|
+
message: opts.message?.trim() || undefined,
|
|
1216
|
+
expectedVersion: opts.expectedVersion,
|
|
1217
|
+
publisher: store.publisher,
|
|
1218
|
+
});
|
|
1219
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1220
|
+
return {
|
|
1221
|
+
storeId: result.storeId,
|
|
1222
|
+
kind: result.kind,
|
|
1223
|
+
id: result.id,
|
|
1224
|
+
channel: result.channel,
|
|
1225
|
+
version: result.version,
|
|
1226
|
+
previousVersion: result.previousVersion,
|
|
1227
|
+
seq: result.seq,
|
|
1228
|
+
movedAt: result.movedAt,
|
|
1229
|
+
message: result.message,
|
|
1230
|
+
unchanged: result.unchanged,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
catch (err) {
|
|
1234
|
+
if (err instanceof ServiceError)
|
|
1235
|
+
throw err;
|
|
1236
|
+
throw this.mapError(err, "failed to promote package channel", {
|
|
1237
|
+
catalog: true,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/** Resolve where a package channel points (Mode A; Magit pulls first by default). */
|
|
1242
|
+
async resolvePackageChannel(opts) {
|
|
1243
|
+
const store = this.requireTypedStoreConfig();
|
|
1244
|
+
const kind = String(opts.kind ?? "").trim();
|
|
1245
|
+
const id = String(opts.id ?? "").trim();
|
|
1246
|
+
const channel = String(opts.channel ?? "").trim();
|
|
1247
|
+
if (!kind || !id || !channel) {
|
|
1248
|
+
throw new ServiceError("VALIDATION", "kind, id, and channel are required", 400);
|
|
1249
|
+
}
|
|
1250
|
+
try {
|
|
1251
|
+
const repo = await this.ensureCatalogRepo();
|
|
1252
|
+
const result = await repo.resolvePackageChannel({
|
|
1253
|
+
storeSelector: store.storeSelector,
|
|
1254
|
+
packageTypeId: store.packageTypeId,
|
|
1255
|
+
kind,
|
|
1256
|
+
id,
|
|
1257
|
+
channel,
|
|
1258
|
+
sync: opts.sync,
|
|
1259
|
+
});
|
|
1260
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1261
|
+
return {
|
|
1262
|
+
storeId: result.storeId,
|
|
1263
|
+
kind: result.kind,
|
|
1264
|
+
id: result.id,
|
|
1265
|
+
channel: result.channel,
|
|
1266
|
+
version: result.version,
|
|
1267
|
+
seq: result.seq,
|
|
1268
|
+
movedAt: result.movedAt,
|
|
1269
|
+
message: result.message,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
if (err instanceof ServiceError)
|
|
1274
|
+
throw err;
|
|
1275
|
+
throw this.mapError(err, "failed to resolve package channel", {
|
|
1276
|
+
catalog: true,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/** Cut an immutable release set (content-addressed members). Mode A only. */
|
|
1281
|
+
async packRelease(opts) {
|
|
1282
|
+
const store = this.requireTypedStoreConfig();
|
|
1283
|
+
const members = (opts.members ?? [])
|
|
1284
|
+
.map((m) => ({
|
|
1285
|
+
kind: String(m.kind ?? "").trim(),
|
|
1286
|
+
id: String(m.id ?? "").trim(),
|
|
1287
|
+
version: String(m.version ?? "").trim(),
|
|
1288
|
+
}))
|
|
1289
|
+
.filter((m) => m.kind && m.id && m.version);
|
|
1290
|
+
if (members.length === 0) {
|
|
1291
|
+
throw new ServiceError("VALIDATION", "members must list at least one { kind, id, version }", 400);
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
const repo = await this.ensureCatalogRepo();
|
|
1295
|
+
const result = await repo.packRelease({
|
|
1296
|
+
storeSelector: store.storeSelector,
|
|
1297
|
+
packageTypeId: store.packageTypeId,
|
|
1298
|
+
members,
|
|
1299
|
+
message: opts.message?.trim() || undefined,
|
|
1300
|
+
publisher: store.publisher,
|
|
1301
|
+
});
|
|
1302
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1303
|
+
return {
|
|
1304
|
+
releaseId: result.release.releaseId,
|
|
1305
|
+
storeId: result.release.storeId,
|
|
1306
|
+
members: result.release.members.map((m) => ({
|
|
1307
|
+
kind: m.kind,
|
|
1308
|
+
id: m.id,
|
|
1309
|
+
version: m.version,
|
|
1310
|
+
})),
|
|
1311
|
+
reused: result.reused,
|
|
1312
|
+
published: result.published,
|
|
1313
|
+
message: result.release.message,
|
|
1314
|
+
createdAt: result.release.createdAt,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
if (err instanceof ServiceError)
|
|
1319
|
+
throw err;
|
|
1320
|
+
throw this.mapError(err, "failed to pack release set", { catalog: true });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
/** Point a release channel at a releaseId (atomic promote of a set). Mode A. */
|
|
1324
|
+
async setReleaseChannel(opts) {
|
|
1325
|
+
const store = this.requireTypedStoreConfig();
|
|
1326
|
+
const channel = String(opts.channel ?? "").trim();
|
|
1327
|
+
const releaseId = String(opts.releaseId ?? "").trim();
|
|
1328
|
+
if (!channel || !releaseId) {
|
|
1329
|
+
throw new ServiceError("VALIDATION", "channel and releaseId are required", 400);
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
const repo = await this.ensureCatalogRepo();
|
|
1333
|
+
const result = await repo.setReleaseChannel({
|
|
1334
|
+
storeSelector: store.storeSelector,
|
|
1335
|
+
packageTypeId: store.packageTypeId,
|
|
1336
|
+
channel,
|
|
1337
|
+
releaseId,
|
|
1338
|
+
message: opts.message?.trim() || undefined,
|
|
1339
|
+
expectedReleaseId: opts.expectedReleaseId,
|
|
1340
|
+
publisher: store.publisher,
|
|
1341
|
+
});
|
|
1342
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1343
|
+
return {
|
|
1344
|
+
storeId: result.storeId,
|
|
1345
|
+
channel: result.channel,
|
|
1346
|
+
releaseId: result.releaseId,
|
|
1347
|
+
previousReleaseId: result.previousReleaseId,
|
|
1348
|
+
seq: result.seq,
|
|
1349
|
+
movedAt: result.movedAt,
|
|
1350
|
+
message: result.message,
|
|
1351
|
+
unchanged: result.unchanged,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
catch (err) {
|
|
1355
|
+
if (err instanceof ServiceError)
|
|
1356
|
+
throw err;
|
|
1357
|
+
throw this.mapError(err, "failed to promote release channel", {
|
|
1358
|
+
catalog: true,
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
/** Resolve a release channel with full member set (Magit pulls first). Mode A. */
|
|
1363
|
+
async resolveReleaseChannel(opts) {
|
|
1364
|
+
const store = this.requireTypedStoreConfig();
|
|
1365
|
+
const channel = String(opts.channel ?? "").trim();
|
|
1366
|
+
if (!channel) {
|
|
1367
|
+
throw new ServiceError("VALIDATION", "channel is required", 400);
|
|
1368
|
+
}
|
|
1369
|
+
try {
|
|
1370
|
+
const repo = await this.ensureCatalogRepo();
|
|
1371
|
+
const result = await repo.resolveReleaseChannel({
|
|
1372
|
+
storeSelector: store.storeSelector,
|
|
1373
|
+
packageTypeId: store.packageTypeId,
|
|
1374
|
+
channel,
|
|
1375
|
+
sync: opts.sync,
|
|
1376
|
+
});
|
|
1377
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1378
|
+
return {
|
|
1379
|
+
storeId: result.storeId,
|
|
1380
|
+
channel: result.channel,
|
|
1381
|
+
releaseId: result.releaseId,
|
|
1382
|
+
seq: result.seq,
|
|
1383
|
+
movedAt: result.movedAt,
|
|
1384
|
+
message: result.message,
|
|
1385
|
+
release: {
|
|
1386
|
+
releaseId: result.release.releaseId,
|
|
1387
|
+
members: result.release.members.map((m) => ({
|
|
1388
|
+
kind: m.kind,
|
|
1389
|
+
id: m.id,
|
|
1390
|
+
version: m.version,
|
|
1391
|
+
})),
|
|
1392
|
+
message: result.release.message,
|
|
1393
|
+
createdAt: result.release.createdAt,
|
|
1394
|
+
},
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
catch (err) {
|
|
1398
|
+
if (err instanceof ServiceError)
|
|
1399
|
+
throw err;
|
|
1400
|
+
throw this.mapError(err, "failed to resolve release channel", {
|
|
1401
|
+
catalog: true,
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
/** Rollback list for a release channel (Mode A). */
|
|
1406
|
+
async releaseChannelHistory(opts) {
|
|
1407
|
+
const store = this.requireTypedStoreConfig();
|
|
1408
|
+
const channel = String(opts.channel ?? "").trim();
|
|
1409
|
+
if (!channel) {
|
|
1410
|
+
throw new ServiceError("VALIDATION", "channel is required", 400);
|
|
1411
|
+
}
|
|
1412
|
+
try {
|
|
1413
|
+
const repo = await this.ensureCatalogRepo();
|
|
1414
|
+
const rows = await repo.releaseChannelHistory({
|
|
1415
|
+
storeSelector: store.storeSelector,
|
|
1416
|
+
packageTypeId: store.packageTypeId,
|
|
1417
|
+
channel,
|
|
1418
|
+
sync: opts.sync,
|
|
1419
|
+
});
|
|
1420
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1421
|
+
return rows.map((r) => ({
|
|
1422
|
+
channel: r.channel,
|
|
1423
|
+
seq: r.seq,
|
|
1424
|
+
releaseId: r.releaseId,
|
|
1425
|
+
previousReleaseId: r.previousReleaseId,
|
|
1426
|
+
movedAt: r.movedAt,
|
|
1427
|
+
message: r.message,
|
|
1428
|
+
}));
|
|
1429
|
+
}
|
|
1430
|
+
catch (err) {
|
|
1431
|
+
if (err instanceof ServiceError)
|
|
1432
|
+
throw err;
|
|
1433
|
+
throw this.mapError(err, "failed to list release channel history", {
|
|
1434
|
+
catalog: true,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* List package channel heads (Mode A). Optionally filter by kind/id/channel.
|
|
1440
|
+
* Magit pulls first unless sync:false.
|
|
1441
|
+
*/
|
|
1442
|
+
async listPackageChannels(opts = {}) {
|
|
1443
|
+
const store = this.requireTypedStoreConfig();
|
|
1444
|
+
try {
|
|
1445
|
+
const repo = await this.ensureCatalogRepo();
|
|
1446
|
+
const rows = await repo.listPackageChannels({
|
|
1447
|
+
storeSelector: store.storeSelector,
|
|
1448
|
+
packageTypeId: store.packageTypeId,
|
|
1449
|
+
kind: opts.kind?.trim() || undefined,
|
|
1450
|
+
id: opts.id?.trim() || undefined,
|
|
1451
|
+
channel: opts.channel?.trim() || undefined,
|
|
1452
|
+
sync: opts.sync,
|
|
1453
|
+
});
|
|
1454
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1455
|
+
return rows.map((r) => ({
|
|
1456
|
+
storeId: r.storeId,
|
|
1457
|
+
kind: r.kind,
|
|
1458
|
+
id: r.id,
|
|
1459
|
+
channel: r.channel,
|
|
1460
|
+
version: r.version,
|
|
1461
|
+
seq: r.seq,
|
|
1462
|
+
movedAt: r.movedAt,
|
|
1463
|
+
message: r.message,
|
|
1464
|
+
}));
|
|
1465
|
+
}
|
|
1466
|
+
catch (err) {
|
|
1467
|
+
if (err instanceof ServiceError)
|
|
1468
|
+
throw err;
|
|
1469
|
+
throw this.mapError(err, "failed to list package channels", {
|
|
1470
|
+
catalog: true,
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/** Every promotion ever made on a package channel (Mode A). */
|
|
1475
|
+
async packageChannelHistory(opts) {
|
|
1476
|
+
const store = this.requireTypedStoreConfig();
|
|
1477
|
+
const kind = String(opts.kind ?? "").trim();
|
|
1478
|
+
const id = String(opts.id ?? "").trim();
|
|
1479
|
+
const channel = String(opts.channel ?? "").trim();
|
|
1480
|
+
if (!kind || !id || !channel) {
|
|
1481
|
+
throw new ServiceError("VALIDATION", "kind, id, and channel are required", 400);
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
const repo = await this.ensureCatalogRepo();
|
|
1485
|
+
const rows = await repo.packageChannelHistory({
|
|
1486
|
+
storeSelector: store.storeSelector,
|
|
1487
|
+
packageTypeId: store.packageTypeId,
|
|
1488
|
+
kind,
|
|
1489
|
+
id,
|
|
1490
|
+
channel,
|
|
1491
|
+
sync: opts.sync,
|
|
1492
|
+
});
|
|
1493
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1494
|
+
return rows.map((r) => ({
|
|
1495
|
+
kind: r.kind,
|
|
1496
|
+
id: r.id,
|
|
1497
|
+
channel: r.channel,
|
|
1498
|
+
version: r.version,
|
|
1499
|
+
previousVersion: r.previousVersion,
|
|
1500
|
+
seq: r.seq,
|
|
1501
|
+
movedAt: r.movedAt,
|
|
1502
|
+
message: r.message,
|
|
1503
|
+
}));
|
|
1504
|
+
}
|
|
1505
|
+
catch (err) {
|
|
1506
|
+
if (err instanceof ServiceError)
|
|
1507
|
+
throw err;
|
|
1508
|
+
throw this.mapError(err, "failed to list package channel history", {
|
|
1509
|
+
catalog: true,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/** List all release channel heads in the store (Mode A). */
|
|
1514
|
+
async listReleaseChannels(opts = {}) {
|
|
1515
|
+
const store = this.requireTypedStoreConfig();
|
|
1516
|
+
try {
|
|
1517
|
+
const repo = await this.ensureCatalogRepo();
|
|
1518
|
+
const rows = await repo.listReleaseChannels({
|
|
1519
|
+
storeSelector: store.storeSelector,
|
|
1520
|
+
packageTypeId: store.packageTypeId,
|
|
1521
|
+
sync: opts.sync,
|
|
1522
|
+
});
|
|
1523
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1524
|
+
return rows.map((r) => ({
|
|
1525
|
+
storeId: r.storeId,
|
|
1526
|
+
channel: r.channel,
|
|
1527
|
+
releaseId: r.releaseId,
|
|
1528
|
+
seq: r.seq,
|
|
1529
|
+
movedAt: r.movedAt,
|
|
1530
|
+
message: r.message,
|
|
1531
|
+
release: {
|
|
1532
|
+
releaseId: r.release.releaseId,
|
|
1533
|
+
members: r.release.members.map((m) => ({
|
|
1534
|
+
kind: m.kind,
|
|
1535
|
+
id: m.id,
|
|
1536
|
+
version: m.version,
|
|
1537
|
+
})),
|
|
1538
|
+
message: r.release.message,
|
|
1539
|
+
createdAt: r.release.createdAt,
|
|
1540
|
+
},
|
|
1541
|
+
}));
|
|
1542
|
+
}
|
|
1543
|
+
catch (err) {
|
|
1544
|
+
if (err instanceof ServiceError)
|
|
1545
|
+
throw err;
|
|
1546
|
+
throw this.mapError(err, "failed to list release channels", {
|
|
1547
|
+
catalog: true,
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Safe catalog capability flags for Studio (never include selector/token).
|
|
1553
|
+
*/
|
|
1554
|
+
packageCatalogCapabilities() {
|
|
1555
|
+
const store = resolveMagitTypedStoreConfig();
|
|
1556
|
+
const typed = Boolean(store);
|
|
1557
|
+
return {
|
|
1558
|
+
magitOn: true,
|
|
1559
|
+
typedStoreConfigured: typed,
|
|
1560
|
+
packageTypeId: store?.packageTypeId ?? null,
|
|
1561
|
+
canBrowse: true,
|
|
1562
|
+
canPublish: true,
|
|
1563
|
+
canPromote: typed,
|
|
1564
|
+
canUseChannels: typed,
|
|
1565
|
+
canUseReleases: typed,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Verify configured catalog reachability (typed store via pullStore, or flat pull).
|
|
1570
|
+
* Never prints selector/token values.
|
|
1571
|
+
*/
|
|
1572
|
+
async verifyTypedPackageStore() {
|
|
1573
|
+
try {
|
|
1574
|
+
const store = resolveMagitTypedStoreConfig();
|
|
1575
|
+
const repo = await this.ensureCatalogRepo();
|
|
1576
|
+
if (store) {
|
|
1577
|
+
await repo.pullStore({
|
|
1578
|
+
storeSelector: store.storeSelector,
|
|
1579
|
+
packageTypeId: store.packageTypeId,
|
|
1580
|
+
});
|
|
1581
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1582
|
+
return {
|
|
1583
|
+
ok: true,
|
|
1584
|
+
mode: "typed-store",
|
|
1585
|
+
packageTypeId: store.packageTypeId,
|
|
1586
|
+
reachable: true,
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
await repo.pull();
|
|
1590
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1591
|
+
return {
|
|
1592
|
+
ok: true,
|
|
1593
|
+
mode: "flat",
|
|
1594
|
+
packageTypeId: null,
|
|
1595
|
+
reachable: true,
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
catch (err) {
|
|
1599
|
+
if (err instanceof ServiceError)
|
|
1600
|
+
throw err;
|
|
1601
|
+
throw this.mapError(err, "failed to verify package catalog", {
|
|
1602
|
+
catalog: true,
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Owner recovery: mint a new selector for an existing storeId, then pushStore.
|
|
1608
|
+
* The new selector is returned ONCE. storeId is sensitive — operators only.
|
|
1609
|
+
*/
|
|
1610
|
+
async recoverStoreSelector(opts) {
|
|
1611
|
+
const storeId = opts.storeId?.trim();
|
|
1612
|
+
if (!storeId) {
|
|
1613
|
+
throw new ServiceError("VALIDATION", "storeId required for recovery", 400);
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
const repo = await this.ensureCatalogRepo();
|
|
1617
|
+
const rotated = await repo.rotateStoreSelector({ storeId });
|
|
1618
|
+
const pushed = await repo.pushStore({
|
|
1619
|
+
storeSelector: rotated.storeSelector,
|
|
1620
|
+
packageTypeId: rotated.packageTypeId,
|
|
1621
|
+
});
|
|
1622
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1623
|
+
return {
|
|
1624
|
+
storeId: rotated.storeId,
|
|
1625
|
+
storeSelector: rotated.storeSelector,
|
|
1626
|
+
packageTypeId: rotated.packageTypeId,
|
|
1627
|
+
pushed: {
|
|
1628
|
+
uploaded: pushed.uploaded ?? 0,
|
|
1629
|
+
skipped: pushed.skipped ?? 0,
|
|
1630
|
+
},
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
catch (err) {
|
|
1634
|
+
if (err instanceof ServiceError)
|
|
1635
|
+
throw err;
|
|
1636
|
+
throw this.mapError(err, "failed to recover store selector", {
|
|
1637
|
+
catalog: true,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Owner inventory of stores (metadata only — never selectors).
|
|
1643
|
+
*/
|
|
1644
|
+
async listPackageStores() {
|
|
1645
|
+
try {
|
|
1646
|
+
const repo = await this.ensureCatalogRepo();
|
|
1647
|
+
const stores = await repo.listStores();
|
|
1648
|
+
this.catalogHealth = { reachable: true, reason: null };
|
|
1649
|
+
return stores.map((s) => ({
|
|
1650
|
+
storeId: s.storeId,
|
|
1651
|
+
packageTypeId: s.typeDescriptor.packageTypeId,
|
|
1652
|
+
allowedKinds: [...(s.typeDescriptor.allowedKinds ?? [])],
|
|
1653
|
+
createdAt: s.createdAt,
|
|
1654
|
+
}));
|
|
1655
|
+
}
|
|
1656
|
+
catch (err) {
|
|
1657
|
+
if (err instanceof ServiceError)
|
|
1658
|
+
throw err;
|
|
1659
|
+
throw this.mapError(err, "failed to list package stores", {
|
|
1660
|
+
catalog: true,
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
917
1664
|
}
|
|
918
1665
|
export function createMagitPackService(cfg) {
|
|
919
1666
|
return new MagitPackService(cfg);
|