@uns-kit/core 3.0.45 → 3.0.47

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 (30) hide show
  1. package/MIGRATIONS.md +31 -0
  2. package/dist/runtime/catalog-publication-client.d.ts +21 -0
  3. package/dist/runtime/catalog-publication-client.d.ts.map +1 -0
  4. package/dist/runtime/catalog-publication-client.js +92 -0
  5. package/dist/runtime/catalog-publication-client.js.map +1 -0
  6. package/dist/runtime/catalog-publication.d.ts +97 -0
  7. package/dist/runtime/catalog-publication.d.ts.map +1 -0
  8. package/dist/runtime/catalog-publication.js +185 -0
  9. package/dist/runtime/catalog-publication.js.map +1 -0
  10. package/dist/runtime/retained-store-copy.d.mts +3 -0
  11. package/dist/runtime/retained-store-copy.mjs +133 -0
  12. package/dist/runtime/retained-store-guide.md +44 -2
  13. package/dist/runtime/retained-store-job.d.mts +1 -0
  14. package/dist/runtime/retained-store-job.mjs +28 -0
  15. package/dist/runtime/retained-store-storage.d.mts +7 -0
  16. package/dist/runtime/retained-store-storage.mjs +66 -21
  17. package/dist/runtime/retained-store.d.ts +12 -3
  18. package/dist/runtime/retained-store.d.ts.map +1 -1
  19. package/dist/runtime/retained-store.js +74 -19
  20. package/dist/runtime/retained-store.js.map +1 -1
  21. package/dist/runtime/service-access-guide.md +130 -0
  22. package/dist/runtime/service-access.d.ts +23 -0
  23. package/dist/runtime/service-access.d.ts.map +1 -0
  24. package/dist/runtime/service-access.js +62 -0
  25. package/dist/runtime/service-access.js.map +1 -0
  26. package/dist/runtime/service-access.test.d.ts +2 -0
  27. package/dist/runtime/service-access.test.d.ts.map +1 -0
  28. package/dist/runtime/service-access.test.js +57 -0
  29. package/dist/runtime/service-access.test.js.map +1 -0
  30. package/package.json +14 -1
package/MIGRATIONS.md CHANGED
@@ -9,6 +9,37 @@ Agents must inspect the application's existing ownership and shutdown flow
9
9
  before editing it. The examples below describe the intended behavior, not a
10
10
  mechanical search-and-replace operation.
11
11
 
12
+ ## 3.0.47 - Common Service access and publications
13
+
14
+ When crossing `<3.0.47` to `>=3.0.47`, adoption is explicit; existing endpoint authorization and caller forwarding do
15
+ not change automatically. Ship `service-access.json` referenced by
16
+ `package.json` `unsDatahub.serviceAccess`, retaining the data file in the build.
17
+ Use `parseServiceAccessDeclaration` and review the exact installed release in
18
+ Controller Service access. Background services need no Catalog/API. Optional
19
+ `publications` reuse the catalog manifest, `CatalogPublicationClient`, and API
20
+ `catalogPublicationHandler`; call `api.registerCatalogRoute()` for signed launch
21
+ route publication. Source approval remains separate from raw workload rights.
22
+ The unpublished earlier `catalogAccess` marker is a compatibility reader only;
23
+ never declare both. Read the [bundled Service access guide](dist/runtime/service-access-guide.md).
24
+
25
+ Do not remove existing credentials until the new path passes deployed
26
+ acceptance. The matching Controller capability must be installed and reviewed
27
+ before enabling the declaration. SDK publication and consumer frozen-lock
28
+ updates remain separate steps. This bounded adapter covers UNS history; it
29
+ does not create SQL, broker or arbitrary external-resource permissions.
30
+
31
+ ## 3.0.46 - Verified retained-store copy and recovery
32
+
33
+ When crossing `<3.0.46` to `>=3.0.46`, existing rename and unbound consumers
34
+ remain compatible. An explicit
35
+ `transfer: "copy-verified"` adds stopped-source review and bounded asynchronous
36
+ opaque copying between local mounts. Use the same operation/checksum after
37
+ interruption; return current work and retain the original copy. Full Runtime
38
+ recovery requires the matching verified offline retained root before RTT owners
39
+ can be activated. See `docs/retained-store-adoption.md`, bundled as the core
40
+ retained-store guide. No application-specific Controller logic, queue conversion,
41
+ new default handover protocol or automatic disk persistence is introduced.
42
+
12
43
  ## 3.0.45 - Opt-in retained-store storage protocol
13
44
 
14
45
  When crossing `<3.0.45` to `>=3.0.45`, ordinary applications keep their current
@@ -0,0 +1,21 @@
1
+ import { type AccessTokenProvider } from "../tools/auth/service-token-provider.js";
2
+ import { type CatalogPublicationManifest } from "./catalog-publication.js";
3
+ /** No caller-token fallback and no arbitrary source URL or topic. */
4
+ export declare class CatalogPublicationClient {
5
+ private readonly options;
6
+ constructor(options: {
7
+ controllerBase: string;
8
+ rttNode: string;
9
+ version: string;
10
+ instanceId: string;
11
+ manifest: CatalogPublicationManifest;
12
+ tokenProvider?: AccessTokenProvider;
13
+ fetch?: typeof fetch;
14
+ });
15
+ private call;
16
+ execute<T>(operationId: string, input: Record<string, unknown>, callerToken: string, transform: (context: {
17
+ signal: AbortSignal;
18
+ history: (sourceId: string) => Promise<unknown>;
19
+ }) => Promise<T>): Promise<T>;
20
+ }
21
+ //# sourceMappingURL=catalog-publication-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-publication-client.d.ts","sourceRoot":"","sources":["../../src/runtime/catalog-publication-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAwB,MAAM,yCAAyC,CAAC;AACzG,OAAO,EAIL,KAAK,0BAA0B,EAIhC,MAAM,0BAA0B,CAAC;AAElC,qEAAqE;AACrE,qBAAa,wBAAwB;IAEjC,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,0BAA0B,CAAC;QACrC,aAAa,CAAC,EAAE,mBAAmB,CAAC;QACpC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;KACtB;YAEW,IAAI;IAmDZ,OAAO,CAAC,CAAC,EACb,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3G,OAAO,CAAC,CAAC,CAAC;CA6Bd"}
@@ -0,0 +1,92 @@
1
+ import { ServiceTokenProvider } from "../tools/auth/service-token-provider.js";
2
+ import { CatalogAccessError, assertCatalogRequest, assertCatalogResponse, catalogAccessDigest, } from "./catalog-publication.js";
3
+ /** No caller-token fallback and no arbitrary source URL or topic. */
4
+ export class CatalogPublicationClient {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ async call(action, operation, input, callerToken, sourceId, signal) {
10
+ assertCatalogRequest(operation, input);
11
+ const token = await (this.options.tokenProvider ?? new ServiceTokenProvider()).getAccessToken();
12
+ if (!token)
13
+ throw Error("Catalog service credential unavailable.");
14
+ const body = {
15
+ rttNode: this.options.rttNode,
16
+ version: this.options.version,
17
+ instanceId: this.options.instanceId,
18
+ operationId: operation.id,
19
+ manifestDigest: catalogAccessDigest(this.options.manifest),
20
+ input,
21
+ callerToken,
22
+ };
23
+ const response = await (this.options.fetch ?? fetch)(new URL(`/api/catalog-publications/${action}`, this.options.controllerBase), {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
26
+ body: JSON.stringify({ ...body, ...(sourceId ? { sourceId } : {}) }),
27
+ signal: signal ?? AbortSignal.timeout(operation.timeoutMs),
28
+ redirect: "error",
29
+ });
30
+ if (!response.ok)
31
+ throw new CatalogAccessError("catalog_access_denied", [400, 401, 403, 409, 429].includes(response.status) ? response.status : 503);
32
+ if (!response.body)
33
+ throw new CatalogAccessError("catalog_source_unavailable", 503);
34
+ const reader = response.body.getReader();
35
+ let size = 0, text = "";
36
+ const decoder = new TextDecoder();
37
+ try {
38
+ while (true) {
39
+ const r = await reader.read();
40
+ if (r.done)
41
+ break;
42
+ size += r.value.byteLength;
43
+ if (size > 1048576) {
44
+ await reader.cancel();
45
+ throw new CatalogAccessError("catalog_source_budget", 503);
46
+ }
47
+ text += decoder.decode(r.value, { stream: true });
48
+ }
49
+ text += decoder.decode();
50
+ return JSON.parse(text);
51
+ }
52
+ finally {
53
+ reader.releaseLock();
54
+ }
55
+ }
56
+ async execute(operationId, input, callerToken, transform) {
57
+ const operation = this.options.manifest.operations.find((op) => op.id === operationId);
58
+ if (!operation)
59
+ throw Error("Catalog operation not declared.");
60
+ const signal = AbortSignal.timeout(operation.timeoutMs);
61
+ await this.call("authorize", operation, input, callerToken, undefined, signal);
62
+ const work = transform({
63
+ signal,
64
+ history: async (sourceId) => {
65
+ if (!operation.sources.some((source) => source.id === sourceId))
66
+ throw Error("Catalog source not declared.");
67
+ return this.call("history", operation, input, callerToken, sourceId, signal);
68
+ },
69
+ });
70
+ let stop;
71
+ const deadline = new Promise((_resolve, reject) => {
72
+ stop = () => reject(new CatalogAccessError("catalog_execution_timeout", 504));
73
+ if (signal.aborted)
74
+ stop();
75
+ else
76
+ signal.addEventListener("abort", stop, { once: true });
77
+ });
78
+ let result;
79
+ try {
80
+ result = await Promise.race([work, deadline]);
81
+ }
82
+ finally {
83
+ if (stop)
84
+ signal.removeEventListener("abort", stop);
85
+ }
86
+ // Recheck after asynchronous processing, including a cached application result.
87
+ await this.call("authorize", operation, input, callerToken, undefined, signal);
88
+ assertCatalogResponse(operation, result);
89
+ return result;
90
+ }
91
+ }
92
+ //# sourceMappingURL=catalog-publication-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-publication-client.js","sourceRoot":"","sources":["../../src/runtime/catalog-publication-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { type AccessTokenProvider, ServiceTokenProvider } from \"../tools/auth/service-token-provider.js\";\nimport {\n CatalogAccessError,\n type CatalogInvocation,\n type CatalogOperation,\n type CatalogPublicationManifest,\n assertCatalogRequest,\n assertCatalogResponse,\n catalogAccessDigest,\n} from \"./catalog-publication.js\";\n\n/** No caller-token fallback and no arbitrary source URL or topic. */\nexport class CatalogPublicationClient {\n constructor(\n private readonly options: {\n controllerBase: string;\n rttNode: string;\n version: string;\n instanceId: string;\n manifest: CatalogPublicationManifest;\n tokenProvider?: AccessTokenProvider;\n fetch?: typeof fetch;\n },\n ) {}\n private async call(\n action: \"authorize\" | \"history\",\n operation: CatalogOperation,\n input: Record<string, unknown>,\n callerToken: string,\n sourceId?: string,\n signal?: AbortSignal,\n ): Promise<unknown> {\n assertCatalogRequest(operation, input);\n const token = await (this.options.tokenProvider ?? new ServiceTokenProvider()).getAccessToken();\n if (!token) throw Error(\"Catalog service credential unavailable.\");\n const body: CatalogInvocation = {\n rttNode: this.options.rttNode,\n version: this.options.version,\n instanceId: this.options.instanceId,\n operationId: operation.id,\n manifestDigest: catalogAccessDigest(this.options.manifest),\n input,\n callerToken,\n };\n const response = await (this.options.fetch ?? fetch)(new URL(`/api/catalog-publications/${action}`, this.options.controllerBase), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${token}` },\n body: JSON.stringify({ ...body, ...(sourceId ? { sourceId } : {}) }),\n signal: signal ?? AbortSignal.timeout(operation.timeoutMs),\n redirect: \"error\",\n });\n if (!response.ok)\n throw new CatalogAccessError(\"catalog_access_denied\", [400, 401, 403, 409, 429].includes(response.status) ? response.status : 503);\n if (!response.body) throw new CatalogAccessError(\"catalog_source_unavailable\", 503);\n const reader = response.body.getReader();\n let size = 0,\n text = \"\";\n const decoder = new TextDecoder();\n try {\n while (true) {\n const r = await reader.read();\n if (r.done) break;\n size += r.value.byteLength;\n if (size > 1048576) {\n await reader.cancel();\n throw new CatalogAccessError(\"catalog_source_budget\", 503);\n }\n text += decoder.decode(r.value, { stream: true });\n }\n text += decoder.decode();\n return JSON.parse(text);\n } finally {\n reader.releaseLock();\n }\n }\n async execute<T>(\n operationId: string,\n input: Record<string, unknown>,\n callerToken: string,\n transform: (context: { signal: AbortSignal; history: (sourceId: string) => Promise<unknown> }) => Promise<T>,\n ): Promise<T> {\n const operation = this.options.manifest.operations.find((op) => op.id === operationId);\n if (!operation) throw Error(\"Catalog operation not declared.\");\n const signal = AbortSignal.timeout(operation.timeoutMs);\n await this.call(\"authorize\", operation, input, callerToken, undefined, signal);\n const work = transform({\n signal,\n history: async (sourceId) => {\n if (!operation.sources.some((source) => source.id === sourceId)) throw Error(\"Catalog source not declared.\");\n return this.call(\"history\", operation, input, callerToken, sourceId, signal);\n },\n });\n let stop: (() => void) | undefined;\n const deadline = new Promise<never>((_resolve, reject) => {\n stop = () => reject(new CatalogAccessError(\"catalog_execution_timeout\", 504));\n if (signal.aborted) stop();\n else signal.addEventListener(\"abort\", stop, { once: true });\n });\n let result: T;\n try {\n result = await Promise.race([work, deadline]);\n } finally {\n if (stop) signal.removeEventListener(\"abort\", stop);\n }\n // Recheck after asynchronous processing, including a cached application result.\n await this.call(\"authorize\", operation, input, callerToken, undefined, signal);\n assertCatalogResponse(operation, result);\n return result;\n }\n}\n"]}
@@ -0,0 +1,97 @@
1
+ import { z } from "zod";
2
+ export declare const CATALOG_PUBLICATION_PROTOCOL = 1;
3
+ export declare const CATALOG_SOURCE_SCOPE = "catalog:sources:read";
4
+ export declare const CATALOG_MANIFEST_FIELD = "catalogAccess";
5
+ export declare class CatalogAccessError extends Error {
6
+ readonly code: string;
7
+ readonly status: number;
8
+ constructor(code: string, status?: number);
9
+ }
10
+ declare const historySource: z.ZodObject<{
11
+ id: z.ZodString;
12
+ kind: z.ZodLiteral<"history">;
13
+ topic: z.ZodString;
14
+ maxWindowMs: z.ZodDefault<z.ZodNumber>;
15
+ maxRows: z.ZodDefault<z.ZodNumber>;
16
+ fromParameter: z.ZodDefault<z.ZodString>;
17
+ toParameter: z.ZodDefault<z.ZodString>;
18
+ }, z.core.$strict>;
19
+ declare const operation: z.ZodObject<{
20
+ id: z.ZodString;
21
+ title: z.ZodString;
22
+ method: z.ZodLiteral<"GET">;
23
+ path: z.ZodString;
24
+ mode: z.ZodEnum<{
25
+ caller: "caller";
26
+ "approved-service": "approved-service";
27
+ }>;
28
+ requestSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
29
+ responseSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
30
+ sources: z.ZodArray<z.ZodObject<{
31
+ id: z.ZodString;
32
+ kind: z.ZodLiteral<"history">;
33
+ topic: z.ZodString;
34
+ maxWindowMs: z.ZodDefault<z.ZodNumber>;
35
+ maxRows: z.ZodDefault<z.ZodNumber>;
36
+ fromParameter: z.ZodDefault<z.ZodString>;
37
+ toParameter: z.ZodDefault<z.ZodString>;
38
+ }, z.core.$strict>>;
39
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
40
+ maxResponseBytes: z.ZodDefault<z.ZodNumber>;
41
+ }, z.core.$strict>;
42
+ declare const manifest: z.ZodObject<{
43
+ protocol: z.ZodLiteral<1>;
44
+ operations: z.ZodArray<z.ZodObject<{
45
+ id: z.ZodString;
46
+ title: z.ZodString;
47
+ method: z.ZodLiteral<"GET">;
48
+ path: z.ZodString;
49
+ mode: z.ZodEnum<{
50
+ caller: "caller";
51
+ "approved-service": "approved-service";
52
+ }>;
53
+ requestSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
54
+ responseSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
55
+ sources: z.ZodArray<z.ZodObject<{
56
+ id: z.ZodString;
57
+ kind: z.ZodLiteral<"history">;
58
+ topic: z.ZodString;
59
+ maxWindowMs: z.ZodDefault<z.ZodNumber>;
60
+ maxRows: z.ZodDefault<z.ZodNumber>;
61
+ fromParameter: z.ZodDefault<z.ZodString>;
62
+ toParameter: z.ZodDefault<z.ZodString>;
63
+ }, z.core.$strict>>;
64
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
65
+ maxResponseBytes: z.ZodDefault<z.ZodNumber>;
66
+ }, z.core.$strict>>;
67
+ }, z.core.$strict>;
68
+ export type CatalogOperationInput = z.input<typeof operation>;
69
+ export type CatalogOperation = z.output<typeof operation>;
70
+ export type CatalogHistorySource = z.output<typeof historySource>;
71
+ export type CatalogPublicationManifest = z.output<typeof manifest>;
72
+ export declare const catalogAccessDigest: (value: unknown) => string;
73
+ /** Catalog routes use the same UNS resource path as SDK endpoint guards. */
74
+ export declare const catalogOperationResource: (path: string) => string;
75
+ /** Inspection consumes bounded JSON data, never application JavaScript. */
76
+ export declare function parseCatalogPublicationManifest(input: unknown): CatalogPublicationManifest;
77
+ export declare function assertCatalogRequest(op: CatalogOperation, input: unknown): void;
78
+ export declare function assertCatalogResponse(op: CatalogOperation, value: unknown): void;
79
+ export declare function catalogHistoryQuery(source: CatalogHistorySource, input: unknown): {
80
+ topics: string[];
81
+ from: string;
82
+ to: string;
83
+ limit: number;
84
+ dedupe: false;
85
+ };
86
+ export declare const catalogInvocationSchema: z.ZodObject<{
87
+ rttNode: z.ZodString;
88
+ version: z.ZodString;
89
+ instanceId: z.ZodString;
90
+ operationId: z.ZodString;
91
+ manifestDigest: z.ZodString;
92
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
93
+ callerToken: z.ZodString;
94
+ }, z.core.$strict>;
95
+ export type CatalogInvocation = z.infer<typeof catalogInvocationSchema>;
96
+ export {};
97
+ //# sourceMappingURL=catalog-publication.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-publication.d.ts","sourceRoot":"","sources":["../../src/runtime/catalog-publication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAC9C,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAC3D,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAkBtD,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,IAAI,EAAE,MAAM;aACZ,MAAM;gBADN,IAAI,EAAE,MAAM,EACZ,MAAM,SAAM;CAI/B;AACD,QAAA,MAAM,aAAa;;;;;;;;kBAeR,CAAC;AACZ,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;kBAaJ,CAAC;AACZ,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;kBAA+F,CAAC;AAC9G,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAClE,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AACnE,eAAO,MAAM,mBAAmB,4BAAiB,CAAC;AAClD,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,MAAsC,CAAC;AAE/F,2EAA2E;AAC3E,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAmC1F;AAiDD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAI/E;AACD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAEhF;AACD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,OAAO,GACb;IACD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;CACf,CAWA;AAED,eAAO,MAAM,uBAAuB;;;;;;;;kBAUzB,CAAC;AACZ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,185 @@
1
+ import { z } from "zod";
2
+ import { operatorDigest, operatorJson, operatorRequestSchema } from "./operator-api.js";
3
+ export const CATALOG_PUBLICATION_PROTOCOL = 1;
4
+ export const CATALOG_SOURCE_SCOPE = "catalog:sources:read";
5
+ export const CATALOG_MANIFEST_FIELD = "catalogAccess";
6
+ const id = z
7
+ .string()
8
+ .min(1)
9
+ .max(120)
10
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/);
11
+ const topic = z
12
+ .string()
13
+ .min(1)
14
+ .max(512)
15
+ .regex(/^[A-Za-z0-9-]+(?:\/[A-Za-z0-9-]+)*$/)
16
+ .refine((value) => !value.split("/").some((part) => part === "." || part === ".."));
17
+ const localPath = z
18
+ .string()
19
+ .min(1)
20
+ .max(1024)
21
+ .regex(/^\/api\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)*$/)
22
+ .refine((value) => !value.split("/").some((part) => part === "." || part === ".."));
23
+ export class CatalogAccessError extends Error {
24
+ code;
25
+ status;
26
+ constructor(code, status = 400) {
27
+ super(code);
28
+ this.code = code;
29
+ this.status = status;
30
+ }
31
+ }
32
+ const historySource = z
33
+ .object({
34
+ id,
35
+ kind: z.literal("history"),
36
+ topic,
37
+ maxWindowMs: z
38
+ .number()
39
+ .int()
40
+ .min(1000)
41
+ .max(31 * 86400000)
42
+ .default(7 * 86400000),
43
+ maxRows: z.number().int().min(1).max(27000).default(1000),
44
+ fromParameter: id.default("from"),
45
+ toParameter: id.default("to"),
46
+ })
47
+ .strict();
48
+ const operation = z
49
+ .object({
50
+ id,
51
+ title: z.string().min(1).max(160),
52
+ method: z.literal("GET"),
53
+ path: localPath,
54
+ mode: z.enum(["caller", "approved-service"]),
55
+ requestSchema: z.record(z.string(), z.unknown()),
56
+ responseSchema: z.record(z.string(), z.unknown()),
57
+ sources: z.array(historySource).min(1).max(16),
58
+ timeoutMs: z.number().int().min(500).max(30000).default(10000),
59
+ maxResponseBytes: z.number().int().min(256).max(1048576).default(262144),
60
+ })
61
+ .strict();
62
+ const manifest = z.object({ protocol: z.literal(1), operations: z.array(operation).min(1).max(32) }).strict();
63
+ export const catalogAccessDigest = operatorDigest;
64
+ /** Catalog routes use the same UNS resource path as SDK endpoint guards. */
65
+ export const catalogOperationResource = (path) => path.replace(/^\/api\//, "");
66
+ /** Inspection consumes bounded JSON data, never application JavaScript. */
67
+ export function parseCatalogPublicationManifest(input) {
68
+ try {
69
+ operatorJson(input);
70
+ const parsed = manifest.parse(input), ids = new Set(), paths = new Set();
71
+ for (const op of parsed.operations) {
72
+ if (ids.has(op.id) || paths.has(op.path))
73
+ throw Error("duplicate operation");
74
+ ids.add(op.id);
75
+ paths.add(op.path);
76
+ op.requestSchema = catalogSchema(op.requestSchema);
77
+ op.responseSchema = catalogSchema(op.responseSchema);
78
+ if (op.requestSchema["type"] !== "object")
79
+ throw Error("request must be an object");
80
+ if (Object.values(op.requestSchema["properties"] ?? {}).some((p) => p["type"] !== "string"))
81
+ throw Error("GET parameters must be strings");
82
+ const sources = new Set();
83
+ for (const source of op.sources) {
84
+ if (sources.has(source.id) || source.fromParameter === source.toParameter)
85
+ throw Error("source binding");
86
+ sources.add(source.id);
87
+ const properties = op.requestSchema["properties"];
88
+ for (const key of [source.fromParameter, source.toParameter]) {
89
+ const parameter = properties?.[key];
90
+ if (parameter?.["type"] !== "string" ||
91
+ parameter["format"] !== "date-time" ||
92
+ !op.requestSchema["required"]?.includes(key))
93
+ throw Error("time parameter binding");
94
+ }
95
+ }
96
+ }
97
+ return parsed;
98
+ }
99
+ catch {
100
+ throw new CatalogAccessError("catalog_manifest_invalid");
101
+ }
102
+ }
103
+ function catalogSchema(value) {
104
+ const schema = operatorRequestSchema(value);
105
+ const visit = (node) => {
106
+ if (!["object", "array", "string", "integer", "number", "boolean", "null"].includes(String(node["type"])) ||
107
+ ["oneOf", "anyOf", "allOf", "default"].some((key) => key in node))
108
+ throw Error("unsupported catalog schema");
109
+ if (node["properties"])
110
+ for (const child of Object.values(node["properties"]))
111
+ visit(child);
112
+ if (node["items"])
113
+ visit(node["items"]);
114
+ };
115
+ visit(schema);
116
+ return schema;
117
+ }
118
+ /** Zod's JSON Schema importer limits date-time to UTC. JSON Schema permits
119
+ * explicit offsets. Check these formats separately without altering input. */
120
+ function offsetSchema(schema, value) {
121
+ const copy = { ...schema };
122
+ if (copy["format"] === "date-time") {
123
+ if (value !== undefined && !z.iso.datetime({ offset: true }).safeParse(value).success)
124
+ throw Error("date-time");
125
+ delete copy["format"];
126
+ }
127
+ if (copy["properties"])
128
+ copy["properties"] = Object.fromEntries(Object.entries(copy["properties"]).map(([key, child]) => [
129
+ key,
130
+ offsetSchema(child, value && typeof value === "object" ? value[key] : undefined),
131
+ ]));
132
+ if (copy["items"]) {
133
+ const child = copy["items"];
134
+ if (Array.isArray(value))
135
+ for (const item of value)
136
+ offsetSchema(child, item);
137
+ copy["items"] = offsetSchema(child, undefined);
138
+ }
139
+ return copy;
140
+ }
141
+ function assertSchema(schema, value, code, maxBytes) {
142
+ try {
143
+ const json = JSON.stringify(value);
144
+ if (json === undefined || Buffer.byteLength(json, "utf8") > maxBytes)
145
+ throw Error("size");
146
+ // fromJSONSchema strips unknown keys by default, so validate the unchanged
147
+ // value against a strict schema rather than returning parsed/transformed data.
148
+ if (!z.fromJSONSchema(offsetSchema(schema, value)).safeParse(value).success)
149
+ throw Error("schema");
150
+ }
151
+ catch {
152
+ throw new CatalogAccessError(code, code === "catalog_response_invalid" ? 502 : 400);
153
+ }
154
+ }
155
+ export function assertCatalogRequest(op, input) {
156
+ operatorJson(input);
157
+ assertSchema(op.requestSchema, input, "catalog_request_invalid", 65536);
158
+ for (const source of op.sources)
159
+ catalogHistoryQuery(source, input);
160
+ }
161
+ export function assertCatalogResponse(op, value) {
162
+ assertSchema(op.responseSchema, value, "catalog_response_invalid", op.maxResponseBytes);
163
+ }
164
+ export function catalogHistoryQuery(source, input) {
165
+ const values = input;
166
+ const from = values?.[source.fromParameter], to = values?.[source.toParameter];
167
+ if (!z.iso.datetime({ offset: true }).safeParse(from).success || !z.iso.datetime({ offset: true }).safeParse(to).success)
168
+ throw new CatalogAccessError("catalog_window_invalid");
169
+ const start = Date.parse(from), end = Date.parse(to);
170
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || end - start > source.maxWindowMs)
171
+ throw new CatalogAccessError("catalog_window_invalid");
172
+ return { topics: [source.topic], from: new Date(start).toISOString(), to: new Date(end).toISOString(), limit: source.maxRows, dedupe: false };
173
+ }
174
+ export const catalogInvocationSchema = z
175
+ .object({
176
+ rttNode: id,
177
+ version: id,
178
+ instanceId: id,
179
+ operationId: id,
180
+ manifestDigest: z.string().regex(/^[a-f0-9]{64}$/),
181
+ input: z.record(z.string(), z.unknown()),
182
+ callerToken: z.string().min(1).max(16384),
183
+ })
184
+ .strict();
185
+ //# sourceMappingURL=catalog-publication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-publication.js","sourceRoot":"","sources":["../../src/runtime/catalog-publication.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\n\nimport { operatorDigest, operatorJson, operatorRequestSchema } from \"./operator-api.js\";\n\nexport const CATALOG_PUBLICATION_PROTOCOL = 1;\nexport const CATALOG_SOURCE_SCOPE = \"catalog:sources:read\";\nexport const CATALOG_MANIFEST_FIELD = \"catalogAccess\";\nconst id = z\n .string()\n .min(1)\n .max(120)\n .regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/);\nconst topic = z\n .string()\n .min(1)\n .max(512)\n .regex(/^[A-Za-z0-9-]+(?:\\/[A-Za-z0-9-]+)*$/)\n .refine((value) => !value.split(\"/\").some((part) => part === \".\" || part === \"..\"));\nconst localPath = z\n .string()\n .min(1)\n .max(1024)\n .regex(/^\\/api\\/[A-Za-z0-9_-]+(?:\\/[A-Za-z0-9_.-]+)*$/)\n .refine((value) => !value.split(\"/\").some((part) => part === \".\" || part === \"..\"));\nexport class CatalogAccessError extends Error {\n constructor(\n public readonly code: string,\n public readonly status = 400,\n ) {\n super(code);\n }\n}\nconst historySource = z\n .object({\n id,\n kind: z.literal(\"history\"),\n topic,\n maxWindowMs: z\n .number()\n .int()\n .min(1000)\n .max(31 * 86400000)\n .default(7 * 86400000),\n maxRows: z.number().int().min(1).max(27000).default(1000),\n fromParameter: id.default(\"from\"),\n toParameter: id.default(\"to\"),\n })\n .strict();\nconst operation = z\n .object({\n id,\n title: z.string().min(1).max(160),\n method: z.literal(\"GET\"),\n path: localPath,\n mode: z.enum([\"caller\", \"approved-service\"]),\n requestSchema: z.record(z.string(), z.unknown()),\n responseSchema: z.record(z.string(), z.unknown()),\n sources: z.array(historySource).min(1).max(16),\n timeoutMs: z.number().int().min(500).max(30000).default(10000),\n maxResponseBytes: z.number().int().min(256).max(1048576).default(262144),\n })\n .strict();\nconst manifest = z.object({ protocol: z.literal(1), operations: z.array(operation).min(1).max(32) }).strict();\nexport type CatalogOperationInput = z.input<typeof operation>;\nexport type CatalogOperation = z.output<typeof operation>;\nexport type CatalogHistorySource = z.output<typeof historySource>;\nexport type CatalogPublicationManifest = z.output<typeof manifest>;\nexport const catalogAccessDigest = operatorDigest;\n/** Catalog routes use the same UNS resource path as SDK endpoint guards. */\nexport const catalogOperationResource = (path: string): string => path.replace(/^\\/api\\//, \"\");\n\n/** Inspection consumes bounded JSON data, never application JavaScript. */\nexport function parseCatalogPublicationManifest(input: unknown): CatalogPublicationManifest {\n try {\n operatorJson(input);\n const parsed = manifest.parse(input),\n ids = new Set<string>(),\n paths = new Set<string>();\n for (const op of parsed.operations) {\n if (ids.has(op.id) || paths.has(op.path)) throw Error(\"duplicate operation\");\n ids.add(op.id);\n paths.add(op.path);\n op.requestSchema = catalogSchema(op.requestSchema);\n op.responseSchema = catalogSchema(op.responseSchema);\n if (op.requestSchema[\"type\"] !== \"object\") throw Error(\"request must be an object\");\n if (Object.values((op.requestSchema[\"properties\"] as Record<string, Record<string, unknown>>) ?? {}).some((p) => p[\"type\"] !== \"string\"))\n throw Error(\"GET parameters must be strings\");\n const sources = new Set<string>();\n for (const source of op.sources) {\n if (sources.has(source.id) || source.fromParameter === source.toParameter) throw Error(\"source binding\");\n sources.add(source.id);\n const properties = op.requestSchema[\"properties\"] as Record<string, unknown> | undefined;\n for (const key of [source.fromParameter, source.toParameter]) {\n const parameter = properties?.[key] as Record<string, unknown> | undefined;\n if (\n parameter?.[\"type\"] !== \"string\" ||\n parameter[\"format\"] !== \"date-time\" ||\n !(op.requestSchema[\"required\"] as string[] | undefined)?.includes(key)\n )\n throw Error(\"time parameter binding\");\n }\n }\n }\n return parsed;\n } catch {\n throw new CatalogAccessError(\"catalog_manifest_invalid\");\n }\n}\n\nfunction catalogSchema(value: unknown): Record<string, unknown> {\n const schema = operatorRequestSchema(value);\n const visit = (node: Record<string, unknown>): void => {\n if (\n ![\"object\", \"array\", \"string\", \"integer\", \"number\", \"boolean\", \"null\"].includes(String(node[\"type\"])) ||\n [\"oneOf\", \"anyOf\", \"allOf\", \"default\"].some((key) => key in node)\n )\n throw Error(\"unsupported catalog schema\");\n if (node[\"properties\"]) for (const child of Object.values(node[\"properties\"] as Record<string, Record<string, unknown>>)) visit(child);\n if (node[\"items\"]) visit(node[\"items\"] as Record<string, unknown>);\n };\n visit(schema);\n return schema;\n}\n/** Zod's JSON Schema importer limits date-time to UTC. JSON Schema permits\n * explicit offsets. Check these formats separately without altering input. */\nfunction offsetSchema(schema: Record<string, unknown>, value: unknown): Record<string, unknown> {\n const copy = { ...schema };\n if (copy[\"format\"] === \"date-time\") {\n if (value !== undefined && !z.iso.datetime({ offset: true }).safeParse(value).success) throw Error(\"date-time\");\n delete copy[\"format\"];\n }\n if (copy[\"properties\"])\n copy[\"properties\"] = Object.fromEntries(\n Object.entries(copy[\"properties\"] as Record<string, Record<string, unknown>>).map(([key, child]) => [\n key,\n offsetSchema(child, value && typeof value === \"object\" ? (value as Record<string, unknown>)[key] : undefined),\n ]),\n );\n if (copy[\"items\"]) {\n const child = copy[\"items\"] as Record<string, unknown>;\n if (Array.isArray(value)) for (const item of value) offsetSchema(child, item);\n copy[\"items\"] = offsetSchema(child, undefined);\n }\n return copy;\n}\nfunction assertSchema(schema: Record<string, unknown>, value: unknown, code: string, maxBytes: number): void {\n try {\n const json = JSON.stringify(value);\n if (json === undefined || Buffer.byteLength(json, \"utf8\") > maxBytes) throw Error(\"size\");\n // fromJSONSchema strips unknown keys by default, so validate the unchanged\n // value against a strict schema rather than returning parsed/transformed data.\n if (!z.fromJSONSchema(offsetSchema(schema, value) as never).safeParse(value).success) throw Error(\"schema\");\n } catch {\n throw new CatalogAccessError(code, code === \"catalog_response_invalid\" ? 502 : 400);\n }\n}\nexport function assertCatalogRequest(op: CatalogOperation, input: unknown): void {\n operatorJson(input);\n assertSchema(op.requestSchema, input, \"catalog_request_invalid\", 65536);\n for (const source of op.sources) catalogHistoryQuery(source, input);\n}\nexport function assertCatalogResponse(op: CatalogOperation, value: unknown): void {\n assertSchema(op.responseSchema, value, \"catalog_response_invalid\", op.maxResponseBytes);\n}\nexport function catalogHistoryQuery(\n source: CatalogHistorySource,\n input: unknown,\n): {\n topics: string[];\n from: string;\n to: string;\n limit: number;\n dedupe: false;\n} {\n const values = input as Record<string, unknown> | null;\n const from = values?.[source.fromParameter],\n to = values?.[source.toParameter];\n if (!z.iso.datetime({ offset: true }).safeParse(from).success || !z.iso.datetime({ offset: true }).safeParse(to).success)\n throw new CatalogAccessError(\"catalog_window_invalid\");\n const start = Date.parse(from as string),\n end = Date.parse(to as string);\n if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || end - start > source.maxWindowMs)\n throw new CatalogAccessError(\"catalog_window_invalid\");\n return { topics: [source.topic], from: new Date(start).toISOString(), to: new Date(end).toISOString(), limit: source.maxRows, dedupe: false };\n}\n\nexport const catalogInvocationSchema = z\n .object({\n rttNode: id,\n version: id,\n instanceId: id,\n operationId: id,\n manifestDigest: z.string().regex(/^[a-f0-9]{64}$/),\n input: z.record(z.string(), z.unknown()),\n callerToken: z.string().min(1).max(16384),\n })\n .strict();\nexport type CatalogInvocation = z.infer<typeof catalogInvocationSchema>;\n"]}
@@ -0,0 +1,3 @@
1
+ export type RetainedCopyFingerprint = { digest: string; bytes: number; entries: number };
2
+ export function inspectRetainedCopy(root: string, kind: "file" | "directory", limits: unknown): RetainedCopyFingerprint;
3
+ export function applyRetainedCopy(plan: unknown, direction: "adopt" | "return", limits: unknown, afterMove?: (id: string) => void, onFile?: (file: string) => void): void;
@@ -0,0 +1,133 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { safePath, owned, sha, sync, entry, relative, checkLimit } from "./retained-store-storage.mjs";
5
+
6
+ function inventory(root, kind, limits) {
7
+ checkLimit(limits);
8
+ const entries = [];
9
+ let bytes = 0;
10
+ function visit(file, rel, depth) {
11
+ if (depth > limits.maximumDepth || (rel && !relative(rel)) || entries.length >= limits.maximumEntries) throw Error("Retained copy path/count limit");
12
+ const stat = entry(file);
13
+ if (!stat) throw Error("Retained copy source is missing");
14
+ if (stat.isDirectory()) {
15
+ owned(file, true);
16
+ entries.push({ path: rel, type: "directory" });
17
+ for (const name of fs.readdirSync(file).sort()) visit(path.join(file, name), rel ? rel + "/" + name : name, depth + 1);
18
+ } else {
19
+ const value = owned(file);
20
+ bytes += value.size;
21
+ if (bytes > limits.maximumBytes || value.size > limits.maximumFileBytes) throw Error("Retained copy byte limit");
22
+ entries.push({ path: rel, type: "file", size: value.size, sha256: sha(file) });
23
+ }
24
+ }
25
+ safePath(root);
26
+ if ((kind === "directory") !== !!entry(root)?.isDirectory()) throw Error("Retained copy resource kind differs");
27
+ visit(root, "", 0);
28
+ return { entries, fingerprint: { digest: createHash("sha256").update(JSON.stringify(entries)).digest("hex"), bytes, entries: entries.length } };
29
+ }
30
+ export function inspectRetainedCopy(root, kind, limits) { return inventory(root, kind, limits).fingerprint; }
31
+ function same(a, b) { return a?.digest === b?.digest && a?.bytes === b?.bytes && a?.entries === b?.entries; }
32
+ function assertFingerprint(file, move, limits) {
33
+ const stat = entry(file);
34
+ if (!stat || stat.dev !== move.device || stat.ino !== move.inode || !same(inspectRetainedCopy(file, move.kind, limits), move.content)) throw Error("Retained copy source changed after review");
35
+ }
36
+ function privateStage(stage) {
37
+ if (!entry(stage)) { fs.mkdirSync(stage, { mode: 0o700 }); sync(path.dirname(stage)); }
38
+ if (owned(stage, true).mode & 0o077) throw Error("Retained copy staging must be private");
39
+ }
40
+ function checkStage(stage, entries) {
41
+ const allowed = new Set(["payload", ...entries.map(e => e.path ? "payload/" + e.path : "payload")]);
42
+ function visit(dir, prefix) {
43
+ for (const name of fs.readdirSync(dir)) {
44
+ const rel = prefix ? prefix + "/" + name : name, file = path.join(dir, name);
45
+ if (!allowed.has(rel)) throw Error("Unexpected retained copy staging entry");
46
+ const stat = entry(file);
47
+ owned(file, !!stat?.isDirectory());
48
+ if (stat.isDirectory()) visit(file, rel);
49
+ }
50
+ }
51
+ visit(stage, "");
52
+ }
53
+ function copyFile(source, destination, expected) {
54
+ if (entry(destination)) owned(destination);
55
+ const input = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
56
+ let output;
57
+ try {
58
+ output = fs.openSync(destination, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_NOFOLLOW, 0o600);
59
+ const h = createHash("sha256"), buffer = Buffer.alloc(65536);
60
+ let count = 0, size;
61
+ while ((size = fs.readSync(input, buffer, 0, buffer.length, null))) {
62
+ count += size;
63
+ if (count > expected.size) throw Error("Retained copy source grew");
64
+ h.update(buffer.subarray(0, size));
65
+ let offset = 0;
66
+ while (offset < size) offset += fs.writeSync(output, buffer, offset, size - offset);
67
+ }
68
+ fs.fchmodSync(output, 0o600); fs.fsyncSync(output);
69
+ if (count !== expected.size || h.digest("hex") !== expected.sha256) throw Error("Retained copy source changed while reading");
70
+ } finally { fs.closeSync(input); if(output!==undefined)fs.closeSync(output); }
71
+ }
72
+ function copyClosed(source, destination, stage, move, limits, onFile) {
73
+ assertFingerprint(source, move, limits);
74
+ const snapshot = inventory(source, move.kind, limits);
75
+ if (entry(destination)) {
76
+ if (!same(inspectRetainedCopy(destination, move.kind, limits), move.content)) throw Error("Published retained copy differs; preserve for review");
77
+ if (entry(stage)) { checkStage(stage, snapshot.entries); fs.rmSync(stage, { recursive: true }); sync(path.dirname(stage)); }
78
+ return;
79
+ }
80
+ privateStage(stage); checkStage(stage, snapshot.entries);
81
+ const payload = path.join(stage, "payload");
82
+ for (const item of snapshot.entries) {
83
+ const from = item.path ? path.join(source, item.path) : source, to = item.path ? path.join(payload, item.path) : payload;
84
+ if (item.type === "directory") {
85
+ if (!entry(to)) fs.mkdirSync(to, { mode: 0o700 });
86
+ if (owned(to, true).mode & 0o077) throw Error("Retained stage directory must be private");
87
+ } else {
88
+ if (entry(to) && owned(to).size === item.size && sha(to) === item.sha256) continue;
89
+ copyFile(from, to, item); onFile?.(item.path);
90
+ }
91
+ }
92
+ assertFingerprint(source, move, limits);
93
+ if (!same(inspectRetainedCopy(payload, move.kind, limits), move.content)) throw Error("Retained staged copy differs");
94
+ for (const item of [...snapshot.entries].reverse()) if (item.type === "directory") sync(item.path ? path.join(payload, item.path) : payload);
95
+ fs.renameSync(payload, destination); sync(stage); sync(path.dirname(destination));
96
+ fs.rmdirSync(stage); sync(path.dirname(stage));
97
+ }
98
+ export function applyRetainedCopy(plan, direction, limits, afterMove, onFile) {
99
+ checkLimit(limits); safePath(plan.source); safePath(plan.directory);
100
+ for (const move of plan.moves) {
101
+ const legacy = path.join(plan.source, move.relative), stable = path.join(plan.directory, move.id);
102
+ const stage = direction === "adopt" ? path.join(plan.directory, ".retained-copy-" + move.id) : path.join(plan.source, ".retained-return-" + plan.storeId + "-" + move.id);
103
+ if (direction === "adopt") copyClosed(legacy, stable, stage, move, limits, onFile);
104
+ else {
105
+ assertFingerprint(stable, move, limits);
106
+ const original = plan.originalMoves?.find(m => m.id === move.id), saved = path.join(plan.source, ".retained-original-" + plan.storeId + "-" + move.id);
107
+ if (entry(legacy) && !same(inspectRetainedCopy(legacy, move.kind, limits), move.content)) {
108
+ if (!original || entry(saved)) throw Error("Return would overwrite unreviewed legacy work");
109
+ assertFingerprint(legacy, original, limits);
110
+ fs.renameSync(legacy, saved); sync(path.dirname(legacy));
111
+ }
112
+ if (entry(saved)) {
113
+ if (!original) throw Error("Unreviewed original retained copy");
114
+ assertFingerprint(saved, original, limits);
115
+ }
116
+ copyClosed(stable, legacy, stage, move, limits, onFile);
117
+ }
118
+ afterMove?.(move.id);
119
+ }
120
+ if (direction === "return") {
121
+ for (const original of plan.originalMoves ?? []) {
122
+ const stage = path.join(plan.directory, ".retained-copy-" + original.id);
123
+ if (!entry(stage)) continue;
124
+ const legacy = path.join(plan.source, original.relative);
125
+ if (!plan.moves.some(m => m.id === original.id)) assertFingerprint(legacy, original, limits);
126
+ const reference = plan.moves.some(m => m.id === original.id) ? path.join(plan.directory, original.id) : legacy;
127
+ const snapshot = inventory(reference, original.kind, limits);
128
+ checkStage(stage, snapshot.entries);
129
+ // Only incomplete scratch is discarded; all original bytes remain proven.
130
+ fs.rmSync(stage, { recursive: true }); sync(plan.directory);
131
+ }
132
+ }
133
+ }