@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.
Files changed (122) hide show
  1. package/README.md +79 -0
  2. package/lib/attachment-resolver.d.ts +24 -0
  3. package/lib/attachment-resolver.d.ts.map +1 -0
  4. package/lib/attachment-resolver.js +38 -0
  5. package/lib/attachment-resolver.js.map +1 -0
  6. package/lib/attachments/extractors/docx.d.ts +6 -0
  7. package/lib/attachments/extractors/docx.d.ts.map +1 -0
  8. package/lib/attachments/extractors/docx.js +43 -0
  9. package/lib/attachments/extractors/docx.js.map +1 -0
  10. package/lib/attachments/extractors/index.d.ts +10 -0
  11. package/lib/attachments/extractors/index.d.ts.map +1 -0
  12. package/lib/attachments/extractors/index.js +10 -0
  13. package/lib/attachments/extractors/index.js.map +1 -0
  14. package/lib/attachments/extractors/pdf.d.ts +4 -0
  15. package/lib/attachments/extractors/pdf.d.ts.map +1 -0
  16. package/lib/attachments/extractors/pdf.js +44 -0
  17. package/lib/attachments/extractors/pdf.js.map +1 -0
  18. package/lib/attachments/extractors/registry.d.ts +43 -0
  19. package/lib/attachments/extractors/registry.d.ts.map +1 -0
  20. package/lib/attachments/extractors/registry.js +82 -0
  21. package/lib/attachments/extractors/registry.js.map +1 -0
  22. package/lib/attachments/extractors/text.d.ts +17 -0
  23. package/lib/attachments/extractors/text.d.ts.map +1 -0
  24. package/lib/attachments/extractors/text.js +0 -0
  25. package/lib/attachments/extractors/text.js.map +1 -0
  26. package/lib/attachments/extractors/types.d.ts +62 -0
  27. package/lib/attachments/extractors/types.d.ts.map +1 -0
  28. package/lib/attachments/extractors/types.js +13 -0
  29. package/lib/attachments/extractors/types.js.map +1 -0
  30. package/lib/attachments/extractors/xlsx.d.ts +6 -0
  31. package/lib/attachments/extractors/xlsx.d.ts.map +1 -0
  32. package/lib/attachments/extractors/xlsx.js +83 -0
  33. package/lib/attachments/extractors/xlsx.js.map +1 -0
  34. package/lib/attachments/filename.d.ts +23 -0
  35. package/lib/attachments/filename.d.ts.map +1 -0
  36. package/lib/attachments/filename.js +62 -0
  37. package/lib/attachments/filename.js.map +1 -0
  38. package/lib/attachments/hash.d.ts +5 -0
  39. package/lib/attachments/hash.d.ts.map +1 -0
  40. package/lib/attachments/hash.js +13 -0
  41. package/lib/attachments/hash.js.map +1 -0
  42. package/lib/attachments/index.d.ts +18 -0
  43. package/lib/attachments/index.d.ts.map +1 -0
  44. package/lib/attachments/index.js +18 -0
  45. package/lib/attachments/index.js.map +1 -0
  46. package/lib/attachments/mime.d.ts +27 -0
  47. package/lib/attachments/mime.d.ts.map +1 -0
  48. package/lib/attachments/mime.js +162 -0
  49. package/lib/attachments/mime.js.map +1 -0
  50. package/lib/attachments/pipeline-extractor.d.ts +8 -0
  51. package/lib/attachments/pipeline-extractor.d.ts.map +1 -0
  52. package/lib/attachments/pipeline-extractor.js +62 -0
  53. package/lib/attachments/pipeline-extractor.js.map +1 -0
  54. package/lib/attachments/pipeline.d.ts +29 -0
  55. package/lib/attachments/pipeline.d.ts.map +1 -0
  56. package/lib/attachments/pipeline.js +59 -0
  57. package/lib/attachments/pipeline.js.map +1 -0
  58. package/lib/attachments/policy.d.ts +25 -0
  59. package/lib/attachments/policy.d.ts.map +1 -0
  60. package/lib/attachments/policy.js +21 -0
  61. package/lib/attachments/policy.js.map +1 -0
  62. package/lib/attachments/render.d.ts +16 -0
  63. package/lib/attachments/render.d.ts.map +1 -0
  64. package/lib/attachments/render.js +28 -0
  65. package/lib/attachments/render.js.map +1 -0
  66. package/lib/attachments/schema.d.ts +58 -0
  67. package/lib/attachments/schema.d.ts.map +1 -0
  68. package/lib/attachments/schema.js +50 -0
  69. package/lib/attachments/schema.js.map +1 -0
  70. package/lib/attachments/store.d.ts +69 -0
  71. package/lib/attachments/store.d.ts.map +1 -0
  72. package/lib/attachments/store.js +254 -0
  73. package/lib/attachments/store.js.map +1 -0
  74. package/lib/attachments/tool-read.d.ts +34 -0
  75. package/lib/attachments/tool-read.d.ts.map +1 -0
  76. package/lib/attachments/tool-read.js +182 -0
  77. package/lib/attachments/tool-read.js.map +1 -0
  78. package/lib/attachments/types.d.ts +96 -0
  79. package/lib/attachments/types.d.ts.map +1 -0
  80. package/lib/attachments/types.js +18 -0
  81. package/lib/attachments/types.js.map +1 -0
  82. package/lib/backends/harness-native.d.ts +45 -0
  83. package/lib/backends/harness-native.d.ts.map +1 -0
  84. package/lib/backends/harness-native.js +56 -0
  85. package/lib/backends/harness-native.js.map +1 -0
  86. package/lib/catalog/legacy-backfill.d.ts +34 -0
  87. package/lib/catalog/legacy-backfill.d.ts.map +1 -0
  88. package/lib/catalog/legacy-backfill.js +49 -0
  89. package/lib/catalog/legacy-backfill.js.map +1 -0
  90. package/lib/catalog/store.d.ts +121 -0
  91. package/lib/catalog/store.d.ts.map +1 -0
  92. package/lib/catalog/store.js +227 -0
  93. package/lib/catalog/store.js.map +1 -0
  94. package/lib/catalog/types.d.ts +83 -0
  95. package/lib/catalog/types.d.ts.map +1 -0
  96. package/lib/catalog/types.js +29 -0
  97. package/lib/catalog/types.js.map +1 -0
  98. package/lib/index.d.ts +12 -0
  99. package/lib/index.d.ts.map +1 -0
  100. package/lib/index.js +12 -0
  101. package/lib/index.js.map +1 -0
  102. package/lib/migration/migrate-on-read.d.ts +61 -0
  103. package/lib/migration/migrate-on-read.d.ts.map +1 -0
  104. package/lib/migration/migrate-on-read.js +61 -0
  105. package/lib/migration/migrate-on-read.js.map +1 -0
  106. package/lib/migration/verify.d.ts +20 -0
  107. package/lib/migration/verify.d.ts.map +1 -0
  108. package/lib/migration/verify.js +35 -0
  109. package/lib/migration/verify.js.map +1 -0
  110. package/lib/paths.d.ts +13 -0
  111. package/lib/paths.d.ts.map +1 -0
  112. package/lib/paths.js +23 -0
  113. package/lib/paths.js.map +1 -0
  114. package/lib/plugin.d.ts +4 -0
  115. package/lib/plugin.d.ts.map +1 -0
  116. package/lib/plugin.js +6 -0
  117. package/lib/plugin.js.map +1 -0
  118. package/lib/service.d.ts +27 -0
  119. package/lib/service.d.ts.map +1 -0
  120. package/lib/service.js +67 -0
  121. package/lib/service.js.map +1 -0
  122. 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":"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"}
@@ -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
@@ -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"}