@krischoichoi/channel-files 0.6.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 +79 -0
- package/lib/attachment-resolver.d.ts +24 -0
- package/lib/attachment-resolver.d.ts.map +1 -0
- package/lib/attachment-resolver.js +38 -0
- package/lib/attachment-resolver.js.map +1 -0
- package/lib/attachments/extractors/docx.d.ts +6 -0
- package/lib/attachments/extractors/docx.d.ts.map +1 -0
- package/lib/attachments/extractors/docx.js +43 -0
- package/lib/attachments/extractors/docx.js.map +1 -0
- package/lib/attachments/extractors/index.d.ts +10 -0
- package/lib/attachments/extractors/index.d.ts.map +1 -0
- package/lib/attachments/extractors/index.js +10 -0
- package/lib/attachments/extractors/index.js.map +1 -0
- package/lib/attachments/extractors/pdf.d.ts +4 -0
- package/lib/attachments/extractors/pdf.d.ts.map +1 -0
- package/lib/attachments/extractors/pdf.js +44 -0
- package/lib/attachments/extractors/pdf.js.map +1 -0
- package/lib/attachments/extractors/registry.d.ts +43 -0
- package/lib/attachments/extractors/registry.d.ts.map +1 -0
- package/lib/attachments/extractors/registry.js +82 -0
- package/lib/attachments/extractors/registry.js.map +1 -0
- package/lib/attachments/extractors/text.d.ts +17 -0
- package/lib/attachments/extractors/text.d.ts.map +1 -0
- package/lib/attachments/extractors/text.js +0 -0
- package/lib/attachments/extractors/text.js.map +1 -0
- package/lib/attachments/extractors/types.d.ts +62 -0
- package/lib/attachments/extractors/types.d.ts.map +1 -0
- package/lib/attachments/extractors/types.js +13 -0
- package/lib/attachments/extractors/types.js.map +1 -0
- package/lib/attachments/extractors/xlsx.d.ts +6 -0
- package/lib/attachments/extractors/xlsx.d.ts.map +1 -0
- package/lib/attachments/extractors/xlsx.js +83 -0
- package/lib/attachments/extractors/xlsx.js.map +1 -0
- package/lib/attachments/filename.d.ts +23 -0
- package/lib/attachments/filename.d.ts.map +1 -0
- package/lib/attachments/filename.js +62 -0
- package/lib/attachments/filename.js.map +1 -0
- package/lib/attachments/hash.d.ts +5 -0
- package/lib/attachments/hash.d.ts.map +1 -0
- package/lib/attachments/hash.js +13 -0
- package/lib/attachments/hash.js.map +1 -0
- package/lib/attachments/index.d.ts +18 -0
- package/lib/attachments/index.d.ts.map +1 -0
- package/lib/attachments/index.js +18 -0
- package/lib/attachments/index.js.map +1 -0
- package/lib/attachments/mime.d.ts +27 -0
- package/lib/attachments/mime.d.ts.map +1 -0
- package/lib/attachments/mime.js +162 -0
- package/lib/attachments/mime.js.map +1 -0
- package/lib/attachments/pipeline-extractor.d.ts +8 -0
- package/lib/attachments/pipeline-extractor.d.ts.map +1 -0
- package/lib/attachments/pipeline-extractor.js +62 -0
- package/lib/attachments/pipeline-extractor.js.map +1 -0
- package/lib/attachments/pipeline.d.ts +29 -0
- package/lib/attachments/pipeline.d.ts.map +1 -0
- package/lib/attachments/pipeline.js +59 -0
- package/lib/attachments/pipeline.js.map +1 -0
- package/lib/attachments/policy.d.ts +25 -0
- package/lib/attachments/policy.d.ts.map +1 -0
- package/lib/attachments/policy.js +21 -0
- package/lib/attachments/policy.js.map +1 -0
- package/lib/attachments/render.d.ts +16 -0
- package/lib/attachments/render.d.ts.map +1 -0
- package/lib/attachments/render.js +28 -0
- package/lib/attachments/render.js.map +1 -0
- package/lib/attachments/schema.d.ts +58 -0
- package/lib/attachments/schema.d.ts.map +1 -0
- package/lib/attachments/schema.js +50 -0
- package/lib/attachments/schema.js.map +1 -0
- package/lib/attachments/store.d.ts +69 -0
- package/lib/attachments/store.d.ts.map +1 -0
- package/lib/attachments/store.js +254 -0
- package/lib/attachments/store.js.map +1 -0
- package/lib/attachments/tool-read.d.ts +34 -0
- package/lib/attachments/tool-read.d.ts.map +1 -0
- package/lib/attachments/tool-read.js +182 -0
- package/lib/attachments/tool-read.js.map +1 -0
- package/lib/attachments/types.d.ts +96 -0
- package/lib/attachments/types.d.ts.map +1 -0
- package/lib/attachments/types.js +18 -0
- package/lib/attachments/types.js.map +1 -0
- package/lib/backends/harness-native.d.ts +45 -0
- package/lib/backends/harness-native.d.ts.map +1 -0
- package/lib/backends/harness-native.js +56 -0
- package/lib/backends/harness-native.js.map +1 -0
- package/lib/catalog/legacy-backfill.d.ts +34 -0
- package/lib/catalog/legacy-backfill.d.ts.map +1 -0
- package/lib/catalog/legacy-backfill.js +49 -0
- package/lib/catalog/legacy-backfill.js.map +1 -0
- package/lib/catalog/store.d.ts +121 -0
- package/lib/catalog/store.d.ts.map +1 -0
- package/lib/catalog/store.js +227 -0
- package/lib/catalog/store.js.map +1 -0
- package/lib/catalog/types.d.ts +83 -0
- package/lib/catalog/types.d.ts.map +1 -0
- package/lib/catalog/types.js +29 -0
- package/lib/catalog/types.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -0
- package/lib/migration/migrate-on-read.d.ts +61 -0
- package/lib/migration/migrate-on-read.d.ts.map +1 -0
- package/lib/migration/migrate-on-read.js +61 -0
- package/lib/migration/migrate-on-read.js.map +1 -0
- package/lib/migration/verify.d.ts +20 -0
- package/lib/migration/verify.d.ts.map +1 -0
- package/lib/migration/verify.js +35 -0
- package/lib/migration/verify.js.map +1 -0
- package/lib/paths.d.ts +13 -0
- package/lib/paths.d.ts.map +1 -0
- package/lib/paths.js +23 -0
- package/lib/paths.js.map +1 -0
- package/lib/plugin.d.ts +4 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +6 -0
- package/lib/plugin.js.map +1 -0
- package/lib/service.d.ts +27 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +67 -0
- package/lib/service.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type AttachmentCatalogRecordV2 } from './types.js';
|
|
3
|
+
/** Errors raised for programmer misuse (an invalid record on write). */
|
|
4
|
+
export declare class CatalogStoreError extends Error {
|
|
5
|
+
readonly code: 'CATALOG_INVALID_RECORD';
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/** Stable logical IDs are generated as `att-${randomUUID()}` by the pipeline. */
|
|
9
|
+
export declare const attachmentIdSchema: z.ZodString;
|
|
10
|
+
/** Zod schema for `AttachmentCatalogRecordV2` (read-time validation). */
|
|
11
|
+
export declare const attachmentCatalogRecordV2Schema: z.ZodObject<{
|
|
12
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
13
|
+
attachmentId: z.ZodString;
|
|
14
|
+
owner: z.ZodObject<{
|
|
15
|
+
sessionId: z.ZodString;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
provenance: z.ZodObject<{
|
|
18
|
+
channelId: z.ZodString;
|
|
19
|
+
accountId: z.ZodString;
|
|
20
|
+
conversationId: z.ZodString;
|
|
21
|
+
conversationType: z.ZodOptional<z.ZodEnum<{
|
|
22
|
+
dm: "dm";
|
|
23
|
+
group: "group";
|
|
24
|
+
}>>;
|
|
25
|
+
threadId: z.ZodOptional<z.ZodString>;
|
|
26
|
+
messageId: z.ZodString;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
file: z.ZodObject<{
|
|
29
|
+
kind: z.ZodEnum<{
|
|
30
|
+
file: "file";
|
|
31
|
+
audio: "audio";
|
|
32
|
+
video: "video";
|
|
33
|
+
}>;
|
|
34
|
+
name: z.ZodString;
|
|
35
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
36
|
+
bytes: z.ZodNumber;
|
|
37
|
+
sha256: z.ZodString;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
storage: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
40
|
+
backend: z.ZodLiteral<"channel-v1">;
|
|
41
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
42
|
+
backend: z.ZodLiteral<"harness-native">;
|
|
43
|
+
nativeId: z.ZodString;
|
|
44
|
+
}, z.core.$strip>], "backend">;
|
|
45
|
+
migration: z.ZodOptional<z.ZodObject<{
|
|
46
|
+
sourceBackend: z.ZodOptional<z.ZodLiteral<"channel-v1">>;
|
|
47
|
+
migratedAt: z.ZodOptional<z.ZodNumber>;
|
|
48
|
+
verifiedAt: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
legacyRetained: z.ZodOptional<z.ZodBoolean>;
|
|
50
|
+
}, z.core.$strip>>;
|
|
51
|
+
createdAt: z.ZodNumber;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
/** Type guard over the zod schema (never trusts an unparsed object). */
|
|
54
|
+
export declare function isAttachmentCatalogRecordV2(value: unknown): value is AttachmentCatalogRecordV2;
|
|
55
|
+
/**
|
|
56
|
+
* Catalog v2 root — a sibling of the v1 tree under the shared attachments
|
|
57
|
+
* root (`<channelData>/attachments/catalog/v2`). Never collides with
|
|
58
|
+
* `attachments/v1/sessions/...`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveCatalogRoot(): string;
|
|
61
|
+
export interface CatalogStoreOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Catalog v2 root; records live at `<root>/by-id/<attachmentId>.json`.
|
|
64
|
+
* Defaults to `resolveCatalogRoot()`.
|
|
65
|
+
*/
|
|
66
|
+
root?: string;
|
|
67
|
+
/** Corrupt-record diagnostics; defaults to `console.warn`. */
|
|
68
|
+
log?: (message: string) => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Patch accepted by `CatalogStore.update` — deliberately limited to the
|
|
72
|
+
* fields lazy migration transitions: the storage locator and the migration
|
|
73
|
+
* state block.
|
|
74
|
+
*/
|
|
75
|
+
export interface CatalogRecordPatch {
|
|
76
|
+
storage?: AttachmentCatalogRecordV2['storage'];
|
|
77
|
+
migration?: AttachmentCatalogRecordV2['migration'];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Filesystem-backed catalog v2 store. All methods are non-fatal on missing
|
|
81
|
+
* data: `get`/`update` return `undefined`, `list` returns `[]`, and no catalog
|
|
82
|
+
* method breaks the legacy fallback when the catalog is absent or corrupt.
|
|
83
|
+
*/
|
|
84
|
+
export declare class CatalogStore {
|
|
85
|
+
private readonly root;
|
|
86
|
+
private readonly byIdDir;
|
|
87
|
+
private readonly log;
|
|
88
|
+
constructor(options?: CatalogStoreOptions);
|
|
89
|
+
/**
|
|
90
|
+
* Atomically publish one record. The record is validated with zod
|
|
91
|
+
* `safeParse` before anything is written; an invalid record raises
|
|
92
|
+
* `CatalogStoreError` and leaves no file behind.
|
|
93
|
+
*/
|
|
94
|
+
put(record: AttachmentCatalogRecordV2): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Read one record. Returns `undefined` for a missing attachment id AND for
|
|
97
|
+
* a corrupt record (invalid JSON or schema mismatch — logged, never thrown).
|
|
98
|
+
*/
|
|
99
|
+
get(attachmentId: string): Promise<AttachmentCatalogRecordV2 | undefined>;
|
|
100
|
+
private readRecord;
|
|
101
|
+
/**
|
|
102
|
+
* List all readable records, sorted by `attachmentId`. Corrupt and
|
|
103
|
+
* transient (`.tmp`) files are skipped; a missing catalog dir returns `[]`.
|
|
104
|
+
*/
|
|
105
|
+
list(): Promise<AttachmentCatalogRecordV2[]>;
|
|
106
|
+
/**
|
|
107
|
+
* Apply a migration-state patch atomically: read current, merge `storage` /
|
|
108
|
+
* `migration` onto it, write back under a writer lock.
|
|
109
|
+
* Returns the merged record; `undefined` when the attachment id is missing
|
|
110
|
+
* (non-fatal — nothing is written). A merged record that fails zod
|
|
111
|
+
* validation raises `CatalogStoreError` (programmer misuse).
|
|
112
|
+
*/
|
|
113
|
+
update(attachmentId: string, patch: CatalogRecordPatch): Promise<AttachmentCatalogRecordV2 | undefined>;
|
|
114
|
+
private recordFile;
|
|
115
|
+
private writeRecord;
|
|
116
|
+
/** Validate a record before persisting; never writes an unparsed object. */
|
|
117
|
+
private parseRecord;
|
|
118
|
+
/** Parse + validate a record read back from disk (corrupt -> undefined). */
|
|
119
|
+
private parseFileRecord;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/catalog/store.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,YAAY,CAAC;AAEpB,wEAAwE;AACxE,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;gBAC5B,OAAO,EAAE,MAAM;CAK5B;AAED,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,aAG9B,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkC1C,CAAC;AAEH,wEAAwE;AACxE,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,yBAAyB,CAEpC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,yBAAyB,CAAC,SAAS,CAAC,CAAC;IAC/C,SAAS,CAAC,EAAE,yBAAyB,CAAC,WAAW,CAAC,CAAC;CACpD;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA4B;gBAEpC,OAAO,GAAE,mBAAwB;IAM7C;;;;OAIG;IACG,GAAG,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3D;;;OAGG;IACG,GAAG,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC;YAKjE,UAAU;IAaxB;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,yBAAyB,EAAE,CAAC;IAiBlD;;;;;;OAMG;IACG,MAAM,CACV,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC;IAiBjD,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,WAAW;IAUnB,4EAA4E;IAC5E,OAAO,CAAC,WAAW;IAWnB,4EAA4E;IAC5E,OAAO,CAAC,eAAe;CAsBxB"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment Catalog v2 store.
|
|
3
|
+
*
|
|
4
|
+
* Filesystem layout:
|
|
5
|
+
*
|
|
6
|
+
* \`\`\`
|
|
7
|
+
* attachments/
|
|
8
|
+
* v1/ # legacy tree — NEVER touched by this module
|
|
9
|
+
* catalog/
|
|
10
|
+
* v2/
|
|
11
|
+
* by-id/
|
|
12
|
+
* <attachmentId>.json # one atomic JSON file per record
|
|
13
|
+
* \`\`\`
|
|
14
|
+
*
|
|
15
|
+
* Every write uses Harness's public `writeFileAtomic`; read-modify-write
|
|
16
|
+
* updates also use its cross-process writer lock. A reader never observes a
|
|
17
|
+
* half-written record. `get` treats missing AND corrupt records the same way — it returns
|
|
18
|
+
* `undefined` and never throws; corrupt JSON is logged and skipped. This is
|
|
19
|
+
* deliberate: the catalog is an OPTIONAL index — "catalog miss MUST be a
|
|
20
|
+
* non-fatal fallback", nothing may break if the catalog directory is absent.
|
|
21
|
+
* The legacy `attachments/v1` tree remains the authoritative fallback for
|
|
22
|
+
* reads.
|
|
23
|
+
*
|
|
24
|
+
* Records are validated with zod `safeParse` on BOTH write and read
|
|
25
|
+
* (AGENTS.md 5.4 — trusted-boundary payloads must be parsed, never cast).
|
|
26
|
+
* Unknown extra fields are tolerated on read (zod `z.object` strips them);
|
|
27
|
+
* they never make a record corrupt.
|
|
28
|
+
*/
|
|
29
|
+
import { mkdir, readFile, readdir } from 'node:fs/promises';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
32
|
+
import { z } from 'zod';
|
|
33
|
+
import { resolveAttachmentsRoot } from '../paths.js';
|
|
34
|
+
import { CATALOG_SCHEMA_VERSION, } from './types.js';
|
|
35
|
+
/** Errors raised for programmer misuse (an invalid record on write). */
|
|
36
|
+
export class CatalogStoreError extends Error {
|
|
37
|
+
code;
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'CatalogStoreError';
|
|
41
|
+
this.code = 'CATALOG_INVALID_RECORD';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Stable logical IDs are generated as `att-${randomUUID()}` by the pipeline. */
|
|
45
|
+
export const attachmentIdSchema = z.string().regex(/^att-[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, 'attachmentId must use the att-UUID format');
|
|
46
|
+
/** Zod schema for `AttachmentCatalogRecordV2` (read-time validation). */
|
|
47
|
+
export const attachmentCatalogRecordV2Schema = z.object({
|
|
48
|
+
schemaVersion: z.literal(CATALOG_SCHEMA_VERSION),
|
|
49
|
+
attachmentId: attachmentIdSchema,
|
|
50
|
+
owner: z.object({
|
|
51
|
+
sessionId: z.string().min(1),
|
|
52
|
+
}),
|
|
53
|
+
provenance: z.object({
|
|
54
|
+
channelId: z.string().min(1),
|
|
55
|
+
accountId: z.string().min(1),
|
|
56
|
+
conversationId: z.string().min(1),
|
|
57
|
+
conversationType: z.enum(['dm', 'group']).optional(),
|
|
58
|
+
threadId: z.string().optional(),
|
|
59
|
+
messageId: z.string().min(1),
|
|
60
|
+
}),
|
|
61
|
+
file: z.object({
|
|
62
|
+
kind: z.enum(['file', 'audio', 'video']),
|
|
63
|
+
name: z.string(),
|
|
64
|
+
mimeType: z.string().optional(),
|
|
65
|
+
bytes: z.number().int().nonnegative(),
|
|
66
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
67
|
+
}),
|
|
68
|
+
storage: z.discriminatedUnion('backend', [
|
|
69
|
+
z.object({ backend: z.literal('channel-v1') }),
|
|
70
|
+
z.object({ backend: z.literal('harness-native'), nativeId: z.string().min(1) }),
|
|
71
|
+
]),
|
|
72
|
+
migration: z
|
|
73
|
+
.object({
|
|
74
|
+
sourceBackend: z.literal('channel-v1').optional(),
|
|
75
|
+
migratedAt: z.number().optional(),
|
|
76
|
+
verifiedAt: z.number().optional(),
|
|
77
|
+
legacyRetained: z.boolean().optional(),
|
|
78
|
+
})
|
|
79
|
+
.optional(),
|
|
80
|
+
createdAt: z.number(),
|
|
81
|
+
});
|
|
82
|
+
/** Type guard over the zod schema (never trusts an unparsed object). */
|
|
83
|
+
export function isAttachmentCatalogRecordV2(value) {
|
|
84
|
+
return attachmentCatalogRecordV2Schema.safeParse(value).success;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Catalog v2 root — a sibling of the v1 tree under the shared attachments
|
|
88
|
+
* root (`<channelData>/attachments/catalog/v2`). Never collides with
|
|
89
|
+
* `attachments/v1/sessions/...`.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveCatalogRoot() {
|
|
92
|
+
return join(resolveAttachmentsRoot(), '..', 'catalog', 'v2');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Filesystem-backed catalog v2 store. All methods are non-fatal on missing
|
|
96
|
+
* data: `get`/`update` return `undefined`, `list` returns `[]`, and no catalog
|
|
97
|
+
* method breaks the legacy fallback when the catalog is absent or corrupt.
|
|
98
|
+
*/
|
|
99
|
+
export class CatalogStore {
|
|
100
|
+
root;
|
|
101
|
+
byIdDir;
|
|
102
|
+
log;
|
|
103
|
+
constructor(options = {}) {
|
|
104
|
+
this.root = options.root ?? resolveCatalogRoot();
|
|
105
|
+
this.byIdDir = join(this.root, 'by-id');
|
|
106
|
+
this.log = options.log ?? ((message) => console.warn('[channel-files:catalog] ' + message));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Atomically publish one record. The record is validated with zod
|
|
110
|
+
* `safeParse` before anything is written; an invalid record raises
|
|
111
|
+
* `CatalogStoreError` and leaves no file behind.
|
|
112
|
+
*/
|
|
113
|
+
async put(record) {
|
|
114
|
+
const parsed = this.parseRecord(record);
|
|
115
|
+
await mkdir(this.byIdDir, { recursive: true, mode: 0o700 });
|
|
116
|
+
const file = this.recordFile(parsed.attachmentId);
|
|
117
|
+
await withFileLock(file, () => this.writeRecord(file, parsed));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read one record. Returns `undefined` for a missing attachment id AND for
|
|
121
|
+
* a corrupt record (invalid JSON or schema mismatch — logged, never thrown).
|
|
122
|
+
*/
|
|
123
|
+
async get(attachmentId) {
|
|
124
|
+
if (!attachmentIdSchema.safeParse(attachmentId).success)
|
|
125
|
+
return undefined;
|
|
126
|
+
return this.readRecord(attachmentId);
|
|
127
|
+
}
|
|
128
|
+
async readRecord(attachmentId) {
|
|
129
|
+
let text;
|
|
130
|
+
try {
|
|
131
|
+
text = await readFile(this.recordFile(attachmentId), 'utf8');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Missing record (or missing catalog dir) is a normal, non-fatal miss.
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return this.parseFileRecord(text, attachmentId);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* List all readable records, sorted by `attachmentId`. Corrupt and
|
|
141
|
+
* transient (`.tmp`) files are skipped; a missing catalog dir returns `[]`.
|
|
142
|
+
*/
|
|
143
|
+
async list() {
|
|
144
|
+
let entries;
|
|
145
|
+
try {
|
|
146
|
+
entries = await readdir(this.byIdDir);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const records = [];
|
|
152
|
+
for (const name of entries) {
|
|
153
|
+
if (!name.endsWith('.json'))
|
|
154
|
+
continue; // ignore transient .tmp files
|
|
155
|
+
const attachmentId = name.slice(0, -'.json'.length);
|
|
156
|
+
const record = await this.get(attachmentId);
|
|
157
|
+
if (record !== undefined)
|
|
158
|
+
records.push(record);
|
|
159
|
+
}
|
|
160
|
+
return records.sort((a, b) => a.attachmentId.localeCompare(b.attachmentId));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Apply a migration-state patch atomically: read current, merge `storage` /
|
|
164
|
+
* `migration` onto it, write back under a writer lock.
|
|
165
|
+
* Returns the merged record; `undefined` when the attachment id is missing
|
|
166
|
+
* (non-fatal — nothing is written). A merged record that fails zod
|
|
167
|
+
* validation raises `CatalogStoreError` (programmer misuse).
|
|
168
|
+
*/
|
|
169
|
+
async update(attachmentId, patch) {
|
|
170
|
+
if (!attachmentIdSchema.safeParse(attachmentId).success)
|
|
171
|
+
return undefined;
|
|
172
|
+
await mkdir(this.byIdDir, { recursive: true, mode: 0o700 });
|
|
173
|
+
const file = this.recordFile(attachmentId);
|
|
174
|
+
return withFileLock(file, async () => {
|
|
175
|
+
const current = await this.readRecord(attachmentId);
|
|
176
|
+
if (current === undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
const merged = this.parseRecord({
|
|
179
|
+
...current,
|
|
180
|
+
...(patch.storage !== undefined ? { storage: patch.storage } : {}),
|
|
181
|
+
...(patch.migration !== undefined ? { migration: patch.migration } : {}),
|
|
182
|
+
});
|
|
183
|
+
await this.writeRecord(file, merged);
|
|
184
|
+
return merged;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
recordFile(attachmentId) {
|
|
188
|
+
return join(this.byIdDir, attachmentId + '.json');
|
|
189
|
+
}
|
|
190
|
+
writeRecord(file, record) {
|
|
191
|
+
return writeFileAtomic(file, JSON.stringify(record, null, 2), {
|
|
192
|
+
mode: 0o600,
|
|
193
|
+
dirMode: 0o700,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Validate a record before persisting; never writes an unparsed object. */
|
|
197
|
+
parseRecord(record) {
|
|
198
|
+
const result = attachmentCatalogRecordV2Schema.safeParse(record);
|
|
199
|
+
if (!result.success) {
|
|
200
|
+
throw new CatalogStoreError('invalid catalog record for \'' + (record?.attachmentId ?? '?') + '\': '
|
|
201
|
+
+ result.error.message);
|
|
202
|
+
}
|
|
203
|
+
return result.data;
|
|
204
|
+
}
|
|
205
|
+
/** Parse + validate a record read back from disk (corrupt -> undefined). */
|
|
206
|
+
parseFileRecord(text, attachmentId) {
|
|
207
|
+
let value;
|
|
208
|
+
try {
|
|
209
|
+
value = JSON.parse(text);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
this.log('corrupt catalog record (invalid JSON) for ' + attachmentId);
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
const result = attachmentCatalogRecordV2Schema.safeParse(value);
|
|
216
|
+
if (!result.success) {
|
|
217
|
+
this.log('corrupt catalog record (schema mismatch) for ' + attachmentId);
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
if (result.data.attachmentId !== attachmentId) {
|
|
221
|
+
this.log('corrupt catalog record (attachmentId mismatch) for ' + attachmentId);
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return result.data;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/catalog/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,sBAAsB,GAEvB,MAAM,YAAY,CAAC;AAEpB,wEAAwE;AACxE,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,IAAI,CAA2B;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAChD,gFAAgF,EAChF,2CAA2C,CAC5C,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IACtD,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,sBAAsB,CAAC;IAChD,YAAY,EAAE,kBAAkB;IAChC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC7B,CAAC;IACF,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC;QACnB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5B,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACjC,gBAAgB,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE;QACpD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC7B,CAAC;IACF,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACxC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;QACrC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC;KAC3C,CAAC;IACF,OAAO,EAAE,CAAC,CAAC,kBAAkB,CAAC,SAAS,EAAE;QACvC,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9C,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;KAChF,CAAC;IACF,SAAS,EAAE,CAAC;SACT,MAAM,CAAC;QACN,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE;QACjD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;KACvC,CAAC;SACD,QAAQ,EAAE;IACb,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC,CAAC;AAEH,wEAAwE;AACxE,MAAM,UAAU,2BAA2B,CACzC,KAAc;IAEd,OAAO,+BAA+B,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AAClE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,sBAAsB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC;AAsBD;;;;GAIG;AACH,MAAM,OAAO,YAAY;IACN,IAAI,CAAS;IACb,OAAO,CAAS;IAChB,GAAG,CAA4B;IAEhD,YAAY,UAA+B,EAAE;QAC3C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,kBAAkB,EAAE,CAAC;QACjD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,0BAA0B,GAAG,OAAO,CAAC,CAAC,CAAC;IAC9F,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAG,CAAC,MAAiC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACxC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAClD,MAAM,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAC,YAAoB;QAC5B,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAC1E,OAAO,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACvC,CAAC;IAEO,KAAK,CAAC,UAAU,CACtB,YAAoB;QAEpB,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;YACvE,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAClD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI;QACR,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,OAAO,GAAgC,EAAE,CAAC;QAChD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,SAAS,CAAC,8BAA8B;YACrE,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACpD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IAC9E,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,MAAM,CACV,YAAoB,EACpB,KAAyB;QAEzB,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAC1E,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;QAC3C,OAAO,YAAY,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;YACnC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;YACpD,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;gBAC9B,GAAG,OAAO;gBACV,GAAG,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClE,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACzE,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACrC,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,UAAU,CAAC,YAAoB;QACrC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC;IACpD,CAAC;IAEO,WAAW,CACjB,IAAY,EACZ,MAAiC;QAEjC,OAAO,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE;YAC5D,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;IACL,CAAC;IAED,4EAA4E;IACpE,WAAW,CAAC,MAAiC;QACnD,MAAM,MAAM,GAAG,+BAA+B,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACjE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,iBAAiB,CACzB,+BAA+B,GAAG,CAAC,MAAM,EAAE,YAAY,IAAI,GAAG,CAAC,GAAG,MAAM;kBACpE,MAAM,CAAC,KAAK,CAAC,OAAO,CACzB,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,4EAA4E;IACpE,eAAe,CACrB,IAAY,EACZ,YAAoB;QAEpB,IAAI,KAAc,CAAC;QACnB,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,GAAG,CAAC,4CAA4C,GAAG,YAAY,CAAC,CAAC;YACtE,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,MAAM,GAAG,+BAA+B,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,IAAI,CAAC,GAAG,CAAC,+CAA+C,GAAG,YAAY,CAAC,CAAC;YACzE,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,YAAY,EAAE,CAAC;YAC9C,IAAI,CAAC,GAAG,CAAC,qDAAqD,GAAG,YAAY,CAAC,CAAC;YAC/E,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment Catalog v2.
|
|
3
|
+
*
|
|
4
|
+
* The catalog is a NEW logical-id -> backend-locator map kept SEPARATE from
|
|
5
|
+
* the legacy `attachments/v1` tree, which is never rewritten in place. One
|
|
6
|
+
* record is written per attachment at:
|
|
7
|
+
*
|
|
8
|
+
* \`\`\`
|
|
9
|
+
* attachments/
|
|
10
|
+
* v1/ # legacy tree — fully untouched
|
|
11
|
+
* catalog/
|
|
12
|
+
* v2/
|
|
13
|
+
* by-id/
|
|
14
|
+
* <attachmentId>.json # AttachmentCatalogRecordV2
|
|
15
|
+
* \`\`\`
|
|
16
|
+
*
|
|
17
|
+
* The record deliberately carries NO transient platform state — no
|
|
18
|
+
* `resourceRef`, no provider URL, no token, no `downloadCode` (same rule as
|
|
19
|
+
* `StoredChannelAsset`). It exists so the resolve flow can later pick a
|
|
20
|
+
* backend (Native-first + legacy fallback) without parsing v1 metadata, and
|
|
21
|
+
* so lazy migration can record the channel-v1 -> harness-native transition
|
|
22
|
+
* explicitly.
|
|
23
|
+
*
|
|
24
|
+
* `attachmentId` is the stable logical id (`att-<uuid>`) and is NEVER a path
|
|
25
|
+
* or a Harness native id.
|
|
26
|
+
*/
|
|
27
|
+
/** Schema version of `AttachmentCatalogRecordV2` (mirrors the v2 directory). */
|
|
28
|
+
export declare const CATALOG_SCHEMA_VERSION: 2;
|
|
29
|
+
/** Binary kinds a generic (non-image) attachment record may describe. */
|
|
30
|
+
export type CatalogFileKind = 'file' | 'audio' | 'video';
|
|
31
|
+
/** Storage backends a catalog record may point at. */
|
|
32
|
+
export type CatalogStorageBackend = 'channel-v1' | 'harness-native';
|
|
33
|
+
/**
|
|
34
|
+
* One catalog v2 record. `storage` discriminates where the real bytes live;
|
|
35
|
+
* `migration` (when present) records that a copy + verify transition happened
|
|
36
|
+
* and that the legacy backend was retained.
|
|
37
|
+
*/
|
|
38
|
+
export interface AttachmentCatalogRecordV2 {
|
|
39
|
+
schemaVersion: typeof CATALOG_SCHEMA_VERSION;
|
|
40
|
+
/** Stable logical attachment id (`att-<uuid>`); never a path or native id. */
|
|
41
|
+
attachmentId: string;
|
|
42
|
+
/** Session ACL owner — the ONLY identity that gates read access. */
|
|
43
|
+
owner: {
|
|
44
|
+
sessionId: string;
|
|
45
|
+
};
|
|
46
|
+
/** De-identified platform provenance (debug / migration only). */
|
|
47
|
+
provenance: {
|
|
48
|
+
channelId: string;
|
|
49
|
+
accountId: string;
|
|
50
|
+
conversationId: string;
|
|
51
|
+
conversationType?: 'dm' | 'group';
|
|
52
|
+
threadId?: string;
|
|
53
|
+
messageId: string;
|
|
54
|
+
};
|
|
55
|
+
file: {
|
|
56
|
+
kind: CatalogFileKind;
|
|
57
|
+
name: string;
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
/** Final byte length (computed by the store that wrote the bytes). */
|
|
60
|
+
bytes: number;
|
|
61
|
+
sha256: string;
|
|
62
|
+
};
|
|
63
|
+
storage: {
|
|
64
|
+
backend: 'channel-v1';
|
|
65
|
+
} | {
|
|
66
|
+
backend: 'harness-native';
|
|
67
|
+
nativeId: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Lazy-migration state. Present once a legacy record has
|
|
71
|
+
* been copied to the native backend, read back and hash-verified.
|
|
72
|
+
* `legacyRetained` must stay `true` — migration is copy + verify,
|
|
73
|
+
* never move/delete.
|
|
74
|
+
*/
|
|
75
|
+
migration?: {
|
|
76
|
+
sourceBackend?: 'channel-v1';
|
|
77
|
+
migratedAt?: number;
|
|
78
|
+
verifiedAt?: number;
|
|
79
|
+
legacyRetained?: boolean;
|
|
80
|
+
};
|
|
81
|
+
createdAt: number;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/catalog/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,EAAG,CAAU,CAAC;AAEjD,yEAAyE;AACzE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAEzD,sDAAsD;AACtD,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAEpE;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,OAAO,sBAAsB,CAAC;IAC7C,8EAA8E;IAC9E,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,kEAAkE;IAClE,UAAU,EAAE;QACV,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC;QAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,IAAI,EAAE;QACJ,IAAI,EAAE,eAAe,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,sEAAsE;QACtE,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,OAAO,EACH;QAAE,OAAO,EAAE,YAAY,CAAA;KAAE,GACzB;QAAE,OAAO,EAAE,gBAAgB,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD;;;;;OAKG;IACH,SAAS,CAAC,EAAE;QACV,aAAa,CAAC,EAAE,YAAY,CAAC;QAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment Catalog v2.
|
|
3
|
+
*
|
|
4
|
+
* The catalog is a NEW logical-id -> backend-locator map kept SEPARATE from
|
|
5
|
+
* the legacy `attachments/v1` tree, which is never rewritten in place. One
|
|
6
|
+
* record is written per attachment at:
|
|
7
|
+
*
|
|
8
|
+
* \`\`\`
|
|
9
|
+
* attachments/
|
|
10
|
+
* v1/ # legacy tree — fully untouched
|
|
11
|
+
* catalog/
|
|
12
|
+
* v2/
|
|
13
|
+
* by-id/
|
|
14
|
+
* <attachmentId>.json # AttachmentCatalogRecordV2
|
|
15
|
+
* \`\`\`
|
|
16
|
+
*
|
|
17
|
+
* The record deliberately carries NO transient platform state — no
|
|
18
|
+
* `resourceRef`, no provider URL, no token, no `downloadCode` (same rule as
|
|
19
|
+
* `StoredChannelAsset`). It exists so the resolve flow can later pick a
|
|
20
|
+
* backend (Native-first + legacy fallback) without parsing v1 metadata, and
|
|
21
|
+
* so lazy migration can record the channel-v1 -> harness-native transition
|
|
22
|
+
* explicitly.
|
|
23
|
+
*
|
|
24
|
+
* `attachmentId` is the stable logical id (`att-<uuid>`) and is NEVER a path
|
|
25
|
+
* or a Harness native id.
|
|
26
|
+
*/
|
|
27
|
+
/** Schema version of `AttachmentCatalogRecordV2` (mirrors the v2 directory). */
|
|
28
|
+
export const CATALOG_SCHEMA_VERSION = 2;
|
|
29
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/catalog/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAU,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './attachments/index.js';
|
|
2
|
+
export * from './attachment-resolver.js';
|
|
3
|
+
export * from './paths.js';
|
|
4
|
+
export * from './service.js';
|
|
5
|
+
export * from './catalog/types.js';
|
|
6
|
+
export * from './catalog/store.js';
|
|
7
|
+
export * from './catalog/legacy-backfill.js';
|
|
8
|
+
export * from './backends/harness-native.js';
|
|
9
|
+
export * from './migration/verify.js';
|
|
10
|
+
export * from './migration/migrate-on-read.js';
|
|
11
|
+
export { name, apply } from './plugin.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,gCAAgC,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './attachments/index.js';
|
|
2
|
+
export * from './attachment-resolver.js';
|
|
3
|
+
export * from './paths.js';
|
|
4
|
+
export * from './service.js';
|
|
5
|
+
export * from './catalog/types.js';
|
|
6
|
+
export * from './catalog/store.js';
|
|
7
|
+
export * from './catalog/legacy-backfill.js';
|
|
8
|
+
export * from './backends/harness-native.js';
|
|
9
|
+
export * from './migration/verify.js';
|
|
10
|
+
export * from './migration/migrate-on-read.js';
|
|
11
|
+
export { name, apply } from './plugin.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,gCAAgC,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy migration ON READ.
|
|
3
|
+
*
|
|
4
|
+
* NOT YET WIRED into the resolve flow. This module is standalone, default-off
|
|
5
|
+
* infrastructure: the first release ships with `migration.nativeOnRead =
|
|
6
|
+
* false` and the resolver turn-on is a later wave. Only when `enabled` is true
|
|
7
|
+
* AND the running Harness host reports a generic attachment capability does a
|
|
8
|
+
* read of a legacy record trigger a copy.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
*
|
|
12
|
+
* - never MOVE or DELETE legacy bytes — migration is copy + verify + retain
|
|
13
|
+
* (`legacyRetained: true`); deleting legacy bytes is GC's job, separate from
|
|
14
|
+
* the migration transaction;
|
|
15
|
+
* - copy to native -> read back -> verify sha256 + bytes -> catalog switch;
|
|
16
|
+
* - ANY failure (copy fail, read-back fail, hash mismatch, length mismatch,
|
|
17
|
+
* catalog get/update fail or throw, missing catalog record) ->
|
|
18
|
+
* `{ outcome: 'legacy-authoritative' }`: legacy remains authoritative and
|
|
19
|
+
* readable, the catalog is unchanged;
|
|
20
|
+
* - a record already at `harness-native` is an idempotent no-op
|
|
21
|
+
* (`{ outcome: 'migrated' }`, nothing copied).
|
|
22
|
+
*/
|
|
23
|
+
import type { NativeGenericAttachmentCapability } from '../backends/harness-native.js';
|
|
24
|
+
import type { CatalogStore } from '../catalog/store.js';
|
|
25
|
+
import type { AttachmentCatalogRecordV2 } from '../catalog/types.js';
|
|
26
|
+
export interface MigrationOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Master switch; defaults to `false` in release ('migration.nativeOnRead'
|
|
29
|
+
* policy). Migration never runs unless explicitly enabled.
|
|
30
|
+
*/
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
/** Capability probe result of the running Harness host. */
|
|
33
|
+
native: NativeGenericAttachmentCapability;
|
|
34
|
+
/**
|
|
35
|
+
* Copy the record's bytes into the native backend; resolves to the native
|
|
36
|
+
* id. Must never move or delete the legacy bytes.
|
|
37
|
+
*/
|
|
38
|
+
copyToNative: (record: AttachmentCatalogRecordV2) => Promise<string>;
|
|
39
|
+
/** Read the copied bytes back from the native backend for verification. */
|
|
40
|
+
readBackNative: (nativeId: string) => Promise<Uint8Array>;
|
|
41
|
+
/** Catalog v2 used for the locator transition (get + update only). */
|
|
42
|
+
catalog: Pick<CatalogStore, 'get' | 'update'>;
|
|
43
|
+
}
|
|
44
|
+
export type MigrateOnReadOutcome = 'skipped-legacy' | 'migrated' | 'legacy-authoritative';
|
|
45
|
+
export interface MigrateOnReadResult {
|
|
46
|
+
outcome: MigrateOnReadOutcome;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Attempt a lazy copy+verify migration of one legacy catalog record.
|
|
50
|
+
*
|
|
51
|
+
* @returns
|
|
52
|
+
* - `skipped-legacy` — migration disabled or native capability unavailable;
|
|
53
|
+
* the caller keeps serving from the legacy backend untouched.
|
|
54
|
+
* - `migrated` — native copy verified (sha256 + bytes) and the catalog now
|
|
55
|
+
* points at `harness-native` with the migration block recorded; legacy
|
|
56
|
+
* bytes retained.
|
|
57
|
+
* - `legacy-authoritative` — any failure; legacy backend stays authoritative
|
|
58
|
+
* and the catalog is unchanged.
|
|
59
|
+
*/
|
|
60
|
+
export declare function migrateOnRead(options: MigrationOptions, record: AttachmentCatalogRecordV2): Promise<MigrateOnReadResult>;
|
|
61
|
+
//# sourceMappingURL=migrate-on-read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-on-read.d.ts","sourceRoot":"","sources":["../../src/migration/migrate-on-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAGrE,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,2DAA2D;IAC3D,MAAM,EAAE,iCAAiC,CAAC;IAC1C;;;OAGG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,yBAAyB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE,2EAA2E;IAC3E,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D,sEAAsE;IACtE,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,KAAK,GAAG,QAAQ,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,oBAAoB,GAC5B,gBAAgB,GAChB,UAAU,GACV,sBAAsB,CAAC;AAE3B,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,oBAAoB,CAAC;CAC/B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,mBAAmB,CAAC,CA6C9B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readBackVerified } from './verify.js';
|
|
2
|
+
/**
|
|
3
|
+
* Attempt a lazy copy+verify migration of one legacy catalog record.
|
|
4
|
+
*
|
|
5
|
+
* @returns
|
|
6
|
+
* - `skipped-legacy` — migration disabled or native capability unavailable;
|
|
7
|
+
* the caller keeps serving from the legacy backend untouched.
|
|
8
|
+
* - `migrated` — native copy verified (sha256 + bytes) and the catalog now
|
|
9
|
+
* points at `harness-native` with the migration block recorded; legacy
|
|
10
|
+
* bytes retained.
|
|
11
|
+
* - `legacy-authoritative` — any failure; legacy backend stays authoritative
|
|
12
|
+
* and the catalog is unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export async function migrateOnRead(options, record) {
|
|
15
|
+
if (!options.enabled || !options.native.available) {
|
|
16
|
+
return { outcome: 'skipped-legacy' };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
// The catalog is the authoritative state: re-read it so the transition
|
|
20
|
+
// cannot clobber a concurrent one, and so a missing catalog record fails
|
|
21
|
+
// safe to legacy-authoritative (catalog miss is never fatal).
|
|
22
|
+
const current = await options.catalog.get(record.attachmentId);
|
|
23
|
+
if (current === undefined)
|
|
24
|
+
return { outcome: 'legacy-authoritative' };
|
|
25
|
+
if (current.storage.backend === 'harness-native') {
|
|
26
|
+
// Already migrated — idempotent no-op, nothing copied.
|
|
27
|
+
return { outcome: 'migrated' };
|
|
28
|
+
}
|
|
29
|
+
// 1) COPY (never move/delete the legacy bytes).
|
|
30
|
+
const nativeId = await options.copyToNative(current);
|
|
31
|
+
// 2) READ BACK + VERIFY sha256 and byte length.
|
|
32
|
+
const verified = await readBackVerified({
|
|
33
|
+
readBack: () => options.readBackNative(nativeId),
|
|
34
|
+
expectedSha256: current.file.sha256,
|
|
35
|
+
expectedBytes: current.file.bytes,
|
|
36
|
+
});
|
|
37
|
+
if (!verified)
|
|
38
|
+
return { outcome: 'legacy-authoritative' };
|
|
39
|
+
// 3) CATALOG SWITCH to harness-native + record the migration state
|
|
40
|
+
// (sourceBackend channel-v1, legacyRetained: true).
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const updated = await options.catalog.update(current.attachmentId, {
|
|
43
|
+
storage: { backend: 'harness-native', nativeId },
|
|
44
|
+
migration: {
|
|
45
|
+
sourceBackend: 'channel-v1',
|
|
46
|
+
migratedAt: now,
|
|
47
|
+
verifiedAt: now,
|
|
48
|
+
legacyRetained: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if (updated === undefined)
|
|
52
|
+
return { outcome: 'legacy-authoritative' };
|
|
53
|
+
return { outcome: 'migrated' };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Copy fail, read-back fail, catalog get/update fail: legacy stays
|
|
57
|
+
// authoritative, catalog unchanged, legacy bytes untouched.
|
|
58
|
+
return { outcome: 'legacy-authoritative' };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=migrate-on-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-on-read.js","sourceRoot":"","sources":["../../src/migration/migrate-on-read.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA8B/C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAyB,EACzB,MAAiC;IAEjC,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAClD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;IACvC,CAAC;IACD,IAAI,CAAC;QACH,uEAAuE;QACvE,yEAAyE;QACzE,8DAA8D;QAC9D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC/D,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;QACtE,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,KAAK,gBAAgB,EAAE,CAAC;YACjD,uDAAuD;YACvD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;QACjC,CAAC;QAED,gDAAgD;QAChD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAErD,gDAAgD;QAChD,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC;YACtC,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC;YAChD,cAAc,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM;YACnC,aAAa,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK;SAClC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;QAE1D,mEAAmE;QACnE,uDAAuD;QACvD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE;YACjE,OAAO,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE;YAChD,SAAS,EAAE;gBACT,aAAa,EAAE,YAAY;gBAC3B,UAAU,EAAE,GAAG;gBACf,UAAU,EAAE,GAAG;gBACf,cAAc,EAAE,IAAI;aACrB;SACF,CAAC,CAAC;QACH,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;QACtE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;QACnE,4DAA4D;QAC5D,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;IAC7C,CAAC;AACH,CAAC"}
|