@ttt-productions/ttt-core 0.79.1 → 0.79.3
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/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/constants/media-download.d.ts +16 -0
- package/dist/constants/media-download.d.ts.map +1 -0
- package/dist/constants/media-download.js +23 -0
- package/dist/constants/media-download.js.map +1 -0
- package/dist/doc-schemas/chat-sync.d.ts +4 -0
- package/dist/doc-schemas/chat-sync.d.ts.map +1 -1
- package/dist/doc-schemas/chat-sync.js +12 -1
- package/dist/doc-schemas/chat-sync.js.map +1 -1
- package/dist/doc-schemas/media-activation-jobs.d.ts +1 -0
- package/dist/doc-schemas/media-activation-jobs.d.ts.map +1 -1
- package/dist/doc-schemas/media-assets.d.ts +3 -0
- package/dist/doc-schemas/media-assets.d.ts.map +1 -1
- package/dist/doc-schemas/media-assets.js +19 -0
- package/dist/doc-schemas/media-assets.js.map +1 -1
- package/dist/doc-schemas/messaging.d.ts +2 -2
- package/dist/doc-schemas/messaging.d.ts.map +1 -1
- package/dist/doc-schemas/messaging.js +9 -7
- package/dist/doc-schemas/messaging.js.map +1 -1
- package/dist/doc-schemas/registry.d.ts +8 -2
- package/dist/doc-schemas/registry.d.ts.map +1 -1
- package/dist/media/download-filename.d.ts +57 -0
- package/dist/media/download-filename.d.ts.map +1 -0
- package/dist/media/download-filename.js +267 -0
- package/dist/media/download-filename.js.map +1 -0
- package/dist/media/edge-serving-contract.d.ts +10 -5
- package/dist/media/edge-serving-contract.d.ts.map +1 -1
- package/dist/media/edge-serving-contract.js.map +1 -1
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -0
- package/dist/media/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ export * from './pagination.js';
|
|
|
13
13
|
export * from './release-flags.js';
|
|
14
14
|
export * from './retention.js';
|
|
15
15
|
export * from './scheduled-jobs.js';
|
|
16
|
+
export * from './media-download.js';
|
|
16
17
|
export * from './media-processing.js';
|
|
17
18
|
export * from './storage-keys.js';
|
|
18
19
|
export * from './company-mascots.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC"}
|
package/dist/constants/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export * from './pagination.js';
|
|
|
13
13
|
export * from './release-flags.js';
|
|
14
14
|
export * from './retention.js';
|
|
15
15
|
export * from './scheduled-jobs.js';
|
|
16
|
+
export * from './media-download.js';
|
|
16
17
|
export * from './media-processing.js';
|
|
17
18
|
export * from './storage-keys.js';
|
|
18
19
|
export * from './company-mascots.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC","sourcesContent":["export * from './app-mode.js';\nexport * from './business.js';\nexport * from './chat.js';\nexport * from './conversation-files.js';\nexport * from './moderation.js';\nexport * from './options.js';\nexport * from './admin-labels.js';\nexport * from './craft-skill-statements.js';\nexport * from './publish-attestations.js';\nexport * from './safety-confirmation-phrases.js';\nexport * from './safety-telemetry-patterns.js';\nexport * from './pagination.js';\nexport * from './release-flags.js';\nexport * from './retention.js';\nexport * from './scheduled-jobs.js';\nexport * from './media-processing.js';\nexport * from './storage-keys.js';\nexport * from './company-mascots.js';\nexport * from './center-stage-rooms.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC","sourcesContent":["export * from './app-mode.js';\nexport * from './business.js';\nexport * from './chat.js';\nexport * from './conversation-files.js';\nexport * from './moderation.js';\nexport * from './options.js';\nexport * from './admin-labels.js';\nexport * from './craft-skill-statements.js';\nexport * from './publish-attestations.js';\nexport * from './safety-confirmation-phrases.js';\nexport * from './safety-telemetry-patterns.js';\nexport * from './pagination.js';\nexport * from './release-flags.js';\nexport * from './retention.js';\nexport * from './scheduled-jobs.js';\nexport * from './media-download.js';\nexport * from './media-processing.js';\nexport * from './storage-keys.js';\nexport * from './company-mascots.js';\nexport * from './center-stage-rooms.js';\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard cap on a normalized download filename, measured in UTF-8 BYTES (not code
|
|
3
|
+
* points) — the unit that actually matters, because the RFC 8187 `filename*`
|
|
4
|
+
* form percent-encodes each non-attr byte to three characters, so a byte cap is
|
|
5
|
+
* what bounds the emitted header. A name over the cap is truncated on a code-point
|
|
6
|
+
* boundary; the extension is always preserved.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_DOWNLOAD_FILENAME_BYTES = 200;
|
|
9
|
+
/**
|
|
10
|
+
* The nonempty fallback stem used when sanitization leaves nothing usable
|
|
11
|
+
* (empty input, an all-punctuation name, a name that was entirely control
|
|
12
|
+
* characters or non-ASCII once the ASCII fallback form is derived). Deliberately
|
|
13
|
+
* neutral: it must never leak anything about the original private filename.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DOWNLOAD_FILENAME_FALLBACK_STEM = "download";
|
|
16
|
+
//# sourceMappingURL=media-download.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-download.d.ts","sourceRoot":"","sources":["../../src/constants/media-download.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,aAAa,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Download-filename policy for gateway `?download=1` responses.
|
|
2
|
+
//
|
|
3
|
+
// The media Worker emits `Content-Disposition: attachment; filename="…";
|
|
4
|
+
// filename*=UTF-8''…` from the SERVED VARIANT's `downloadFilename`
|
|
5
|
+
// (doc-schemas/media-assets.ts). These are the two policy values the canonical
|
|
6
|
+
// normalizer (`src/media/download-filename.ts`) derives from — every enforcement
|
|
7
|
+
// point imports them, never a literal (ARCH-102).
|
|
8
|
+
/**
|
|
9
|
+
* Hard cap on a normalized download filename, measured in UTF-8 BYTES (not code
|
|
10
|
+
* points) — the unit that actually matters, because the RFC 8187 `filename*`
|
|
11
|
+
* form percent-encodes each non-attr byte to three characters, so a byte cap is
|
|
12
|
+
* what bounds the emitted header. A name over the cap is truncated on a code-point
|
|
13
|
+
* boundary; the extension is always preserved.
|
|
14
|
+
*/
|
|
15
|
+
export const MAX_DOWNLOAD_FILENAME_BYTES = 200;
|
|
16
|
+
/**
|
|
17
|
+
* The nonempty fallback stem used when sanitization leaves nothing usable
|
|
18
|
+
* (empty input, an all-punctuation name, a name that was entirely control
|
|
19
|
+
* characters or non-ASCII once the ASCII fallback form is derived). Deliberately
|
|
20
|
+
* neutral: it must never leak anything about the original private filename.
|
|
21
|
+
*/
|
|
22
|
+
export const DOWNLOAD_FILENAME_FALLBACK_STEM = 'download';
|
|
23
|
+
//# sourceMappingURL=media-download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-download.js","sourceRoot":"","sources":["../../src/constants/media-download.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,EAAE;AACF,yEAAyE;AACzE,mEAAmE;AACnE,+EAA+E;AAC/E,iFAAiF;AACjF,kDAAkD;AAElD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,UAAU,CAAC","sourcesContent":["// Download-filename policy for gateway `?download=1` responses.\n//\n// The media Worker emits `Content-Disposition: attachment; filename=\"…\";\n// filename*=UTF-8''…` from the SERVED VARIANT's `downloadFilename`\n// (doc-schemas/media-assets.ts). These are the two policy values the canonical\n// normalizer (`src/media/download-filename.ts`) derives from — every enforcement\n// point imports them, never a literal (ARCH-102).\n\n/**\n * Hard cap on a normalized download filename, measured in UTF-8 BYTES (not code\n * points) — the unit that actually matters, because the RFC 8187 `filename*`\n * form percent-encodes each non-attr byte to three characters, so a byte cap is\n * what bounds the emitted header. A name over the cap is truncated on a code-point\n * boundary; the extension is always preserved.\n */\nexport const MAX_DOWNLOAD_FILENAME_BYTES = 200;\n\n/**\n * The nonempty fallback stem used when sanitization leaves nothing usable\n * (empty input, an all-punctuation name, a name that was entirely control\n * characters or non-ASCII once the ASCII fallback form is derived). Deliberately\n * neutral: it must never leak anything about the original private filename.\n */\nexport const DOWNLOAD_FILENAME_FALLBACK_STEM = 'download';\n"]}
|
|
@@ -152,6 +152,7 @@ export declare const ChatAdminActionCommandSchema: z.ZodObject<{
|
|
|
152
152
|
deadLetter: "deadLetter";
|
|
153
153
|
queued: "queued";
|
|
154
154
|
delivering: "delivering";
|
|
155
|
+
safetyRepair: "safetyRepair";
|
|
155
156
|
}>;
|
|
156
157
|
attemptCount: z.ZodNumber;
|
|
157
158
|
nextAttemptAt: z.ZodNumber;
|
|
@@ -165,6 +166,9 @@ export declare const ChatAdminActionCommandSchema: z.ZodObject<{
|
|
|
165
166
|
deadLetteredAt: z.ZodNullable<z.ZodNumber>;
|
|
166
167
|
reconciled: z.ZodBoolean;
|
|
167
168
|
reconciledAt: z.ZodNullable<z.ZodNumber>;
|
|
169
|
+
safetyRepair: z.ZodBoolean;
|
|
170
|
+
safetyRepairGeneration: z.ZodNumber;
|
|
171
|
+
reconcileAttemptCount: z.ZodNumber;
|
|
168
172
|
expireAt: z.ZodOptional<z.ZodUnknown>;
|
|
169
173
|
}, z.core.$strip>;
|
|
170
174
|
export type ChatAdminActionCommand = z.infer<typeof ChatAdminActionCommandSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-sync.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/chat-sync.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;iBAW1C,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAIxF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;iBAUlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,4BAA4B;;;;;;;;iBAKvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAGlF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiB9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmBlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAGxE,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;iBAclC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAKxE,eAAO,MAAM,4BAA4B
|
|
1
|
+
{"version":3,"file":"chat-sync.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/chat-sync.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;iBAW1C,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAIxF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;iBAUlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,4BAA4B;;;;;;;;iBAKvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAGlF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiB9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmBlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAGxE,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;iBAclC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAKxE,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqCvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAgBlF,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsB5C,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAK5F,eAAO,MAAM,2CAA2C;;;;;;;;;;;;;;iBAQtD,CAAC;AACH,MAAM,MAAM,qCAAqC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2CAA2C,CAAC,CAAC"}
|
|
@@ -113,7 +113,10 @@ export const ChatAdminActionCommandSchema = z.object({
|
|
|
113
113
|
reason: z.string(),
|
|
114
114
|
expectedMessageRevision: z.number(),
|
|
115
115
|
payloadHash: z.string(),
|
|
116
|
-
|
|
116
|
+
// `safetyRepair` is the NONTERMINAL stall loop: a RESERVED command whose reconcile
|
|
117
|
+
// leg (PROBE → FINALIZE) failed past ADMIN_COMMAND_MAX_ATTEMPTS. Drained like
|
|
118
|
+
// `delivering`; never dead-letters.
|
|
119
|
+
state: z.enum(['queued', 'delivering', 'safetyRepair', 'applied', 'failed', 'deadLetter']),
|
|
117
120
|
attemptCount: z.number(),
|
|
118
121
|
nextAttemptAt: z.number(),
|
|
119
122
|
lastError: z.string().nullable(),
|
|
@@ -126,6 +129,14 @@ export const ChatAdminActionCommandSchema = z.object({
|
|
|
126
129
|
deadLetteredAt: z.number().nullable(),
|
|
127
130
|
reconciled: z.boolean(),
|
|
128
131
|
reconciledAt: z.number().nullable(),
|
|
132
|
+
safetyRepair: z.boolean(),
|
|
133
|
+
// Monotonic counter of distinct entries into the safety-repair loop — keys the
|
|
134
|
+
// per-entry idempotent failure audit.
|
|
135
|
+
safetyRepairGeneration: z.number(),
|
|
136
|
+
// Failed reconcile passes on an already-reserved command. Separate from
|
|
137
|
+
// `attemptCount` (delivery attempts, which dead-letter); at the cap it escalates
|
|
138
|
+
// once into `safetyRepair` instead of dead-lettering.
|
|
139
|
+
reconcileAttemptCount: z.number(),
|
|
129
140
|
expireAt: expireAtField,
|
|
130
141
|
});
|
|
131
142
|
// ── chatHistoryAnonymizationJobs/{jobId} ────────────────────────────────────
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-sync.js","sourceRoot":"","sources":["../../src/doc-schemas/chat-sync.ts"],"names":[],"mappings":"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","sourcesContent":["// Chat realtime SYNC / projection / command Firestore schemas (chat-edge-rebuild P1).\n// The chat sync / projection / command (Firestore-side) field lists are FROZEN\n// here — this file is their single source; the durable design owner is ttt-prod\n// docs/design/chat-realtime-system.md. All of these collections are Admin-SDK-only\n// (`allow read, write: if false`). Per the TTT timestamp convention every time\n// field is epoch-ms `number`; the SINGLE exception is the native-TTL `expireAt`,\n// which is a real Firestore `Timestamp` (modeled as `unknown` so the drift-check\n// does not false-flag a Timestamp object against `z.number()`).\n\nimport { z } from 'zod';\n\n/** Native-TTL field — a real Firestore Timestamp (NOT epoch-ms). Absent until the terminal that sets TTL. */\nconst expireAtField = z.unknown().optional();\n\n// ── chatChannelAuthProjections/{authPairKey} ────────────────────────────────\n// Per-(user, channel) authorization FACT. Absent ⇒ removed/deny (fail-closed).\nexport const ChatChannelAuthProjectionSchema = z.object({\n channelRefScope: z.enum(['channel', 'invite']),\n workProjectId: z.string().nullable(),\n guildChatChannelId: z.string().nullable(),\n guildInviteId: z.string().nullable(),\n uid: z.string(),\n channelKey: z.string(),\n channelAuthState: z.enum(['authorized', 'removed']),\n channelAuthVersion: z.number(),\n inputFingerprint: z.string(),\n updatedAt: z.number(),\n});\nexport type ChatChannelAuthProjection = z.infer<typeof ChatChannelAuthProjectionSchema>;\n\n// ── chatScopeDegraded/{scopeKey} (+ causes/{causeId}) ───────────────────────\n// Exists IFF unresolvedCauseCount > 0. Grant mints fail closed while it exists.\nexport const ChatScopeDegradedSchema = z.object({\n scopeKey: z.string(),\n scopeKind: z.enum(['channel', 'memberInWork']),\n // channel scope carries the channelRef tuple; memberInWork carries {uid, workProjectId}.\n workProjectId: z.string().nullable(),\n guildChatChannelId: z.string().nullable(),\n guildInviteId: z.string().nullable(),\n uid: z.string().nullable(),\n unresolvedCauseCount: z.number(),\n updatedAt: z.number(),\n});\nexport type ChatScopeDegraded = z.infer<typeof ChatScopeDegradedSchema>;\n\nexport const ChatScopeDegradedCauseSchema = z.object({\n causeId: z.string(),\n kind: z.enum(['syncEvent', 'fanoutJob']),\n ref: z.string(),\n deadLetteredAt: z.number(),\n});\nexport type ChatScopeDegradedCause = z.infer<typeof ChatScopeDegradedCauseSchema>;\n\n// ── chatSyncEvents/{eventId} ────────────────────────────────────────────────\nexport const ChatSyncEventSchema = z.object({\n eventId: z.string(),\n targetDo: z.string(),\n kind: z.enum(['channelAuth', 'accountAccess', 'config', 'serverMessage']),\n version: z.number(),\n payload: z.record(z.string(), z.unknown()).nullable(),\n tombstone: z.boolean().optional(),\n payloadHash: z.string(),\n status: z.enum(['pending', 'delivered', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n deliveredResult: z.record(z.string(), z.unknown()).nullable(),\n deliveredAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n expireAt: expireAtField,\n});\nexport type ChatSyncEvent = z.infer<typeof ChatSyncEventSchema>;\n\n// ── chatSyncFanoutJobs/{jobId} ──────────────────────────────────────────────\nexport const ChatSyncFanoutJobSchema = z.object({\n jobId: z.string(),\n selectorKind: z.enum(['channelMembers', 'workChannelsForUser', 'policyEditAffectedUsers']),\n selectorArgs: z.object({\n workProjectId: z.string().optional(),\n channelKey: z.string().optional(),\n uid: z.string().optional(),\n }),\n causeVersion: z.number(),\n cursor: z.object({ pageIndex: z.number(), lastDocId: z.string().nullable() }),\n revision: z.number(),\n status: z.enum(['pending', 'complete', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n completedAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n expireAt: expireAtField,\n});\nexport type ChatSyncFanoutJob = z.infer<typeof ChatSyncFanoutJobSchema>;\n\n// ── chatMessageOutbox/{commandId} ───────────────────────────────────────────\nexport const ChatMessageOutboxSchema = z.object({\n commandId: z.string(),\n kind: z.enum(['userMsg', 'systemMsg']),\n threadRef: z.string(),\n payload: z.record(z.string(), z.unknown()),\n payloadVersion: z.number(),\n status: z.enum(['pending', 'applied', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n appliedAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n expireAt: expireAtField,\n});\nexport type ChatMessageOutbox = z.infer<typeof ChatMessageOutboxSchema>;\n\n// ── chatAdminActionCommands/{requestId} ─────────────────────────────────────\n// `requestId` IS the doc id + DO dedup key. `delivering` is NOT a lease (it sets\n// nextAttemptAt = now + DELIVERY_TIMEOUT). terminalFailureGeneration default 0.\nexport const ChatAdminActionCommandSchema = z.object({\n requestId: z.string(),\n actorUid: z.string(),\n actorMode: z.enum(['adminReview', 'adminOverride']),\n systemRole: z.enum(['admin', 'jrAdmin']),\n channelRef: z.record(z.string(), z.unknown()),\n messageSeq: z.number(),\n action: z.enum(['hide', 'delete']),\n caseId: z.string(),\n reason: z.string(),\n expectedMessageRevision: z.number(),\n payloadHash: z.string(),\n state: z.enum(['queued', 'delivering', 'applied', 'failed', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n result: z.record(z.string(), z.unknown()).nullable(),\n doRevisionId: z.string().nullable(),\n terminalFailureGeneration: z.number(),\n createdAt: z.number(),\n appliedAt: z.number().nullable(),\n failedAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n reconciled: z.boolean(),\n reconciledAt: z.number().nullable(),\n expireAt: expireAtField,\n});\nexport type ChatAdminActionCommand = z.infer<typeof ChatAdminActionCommandSchema>;\n\n// ── chatHistoryAnonymizationJobs/{jobId} ────────────────────────────────────\n// Account-deletion history anonymization (Contract F; P8). Resumable + crash-safe:\n// the recorded source-manifest identity (key/hash/generation) + the three cursors\n// (scan/rewrite/delete) make every boundary restartable. jobId =\n// hash('chat-anonymize', channelDoId, epoch, anonymizedUid). `expireAt` (native TTL)\n// is set ONLY when the job reaches `done`.\n//\n// C-B1: the replacement generation is DO-OWNED — the Channel DO (single-threaded per\n// channel) allocates a DISTINCT monotonic generation per erasure off the epoch row and\n// rewrites against the CURRENT live manifest, so two erasures on one epoch chain instead\n// of colliding on a shared `anon-1` keyspace. These four fields are therefore RECORDED AT\n// SCAN (null until the DO reports/allocates them), not pinned at enqueue; if a concurrent\n// anonymization supersedes the source between scan and swap, the job resets to `scanning`\n// to re-derive against the new current manifest (keeping its own allocated generation).\nexport const ChatHistoryAnonymizationJobSchema = z.object({\n jobId: z.string(),\n channelDoId: z.string(),\n epoch: z.number(),\n anonymizedUid: z.string(),\n sourceManifestKey: z.string().nullable(),\n sourceManifestHash: z.string().nullable(),\n sourceGeneration: z.number().nullable(),\n replacementGeneration: z.number().nullable(),\n newManifestKey: z.string().nullable(),\n scanCursor: z.number(),\n rewriteCursor: z.number(),\n deleteCursor: z.number(),\n swappedAt: z.number().nullable(),\n state: z.enum(['scanning', 'rewriting', 'swapping', 'deleting', 'done', 'failed']),\n legalHoldPaused: z.boolean(),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n updatedAt: z.number(),\n expireAt: expireAtField,\n});\nexport type ChatHistoryAnonymizationJob = z.infer<typeof ChatHistoryAnonymizationJobSchema>;\n\n// ── chatHistoryAnonymizationJobs/{jobId}/affectedChunks/{chunkOrdinal} ───────\n// The non-contiguous affected-chunk set (round-16 finding 7) — a crash after the\n// manifest swap resumes deletion from these rows + `deleteCursor` without re-listing R2.\nexport const ChatHistoryAnonymizationAffectedChunkSchema = z.object({\n chunkOrdinal: z.number(),\n sourceChunkKey: z.string(),\n sourceSha256: z.string(),\n replacementChunkKey: z.string().nullable(),\n replacementSha256: z.string().nullable(),\n rewriteState: z.enum(['pending', 'done']),\n deleteState: z.enum(['pending', 'done']),\n});\nexport type ChatHistoryAnonymizationAffectedChunk = z.infer<typeof ChatHistoryAnonymizationAffectedChunkSchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"chat-sync.js","sourceRoot":"","sources":["../../src/doc-schemas/chat-sync.ts"],"names":[],"mappings":"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","sourcesContent":["// Chat realtime SYNC / projection / command Firestore schemas (chat-edge-rebuild P1).\n// The chat sync / projection / command (Firestore-side) field lists are FROZEN\n// here — this file is their single source; the durable design owner is ttt-prod\n// docs/design/chat-realtime-system.md. All of these collections are Admin-SDK-only\n// (`allow read, write: if false`). Per the TTT timestamp convention every time\n// field is epoch-ms `number`; the SINGLE exception is the native-TTL `expireAt`,\n// which is a real Firestore `Timestamp` (modeled as `unknown` so the drift-check\n// does not false-flag a Timestamp object against `z.number()`).\n\nimport { z } from 'zod';\n\n/** Native-TTL field — a real Firestore Timestamp (NOT epoch-ms). Absent until the terminal that sets TTL. */\nconst expireAtField = z.unknown().optional();\n\n// ── chatChannelAuthProjections/{authPairKey} ────────────────────────────────\n// Per-(user, channel) authorization FACT. Absent ⇒ removed/deny (fail-closed).\nexport const ChatChannelAuthProjectionSchema = z.object({\n channelRefScope: z.enum(['channel', 'invite']),\n workProjectId: z.string().nullable(),\n guildChatChannelId: z.string().nullable(),\n guildInviteId: z.string().nullable(),\n uid: z.string(),\n channelKey: z.string(),\n channelAuthState: z.enum(['authorized', 'removed']),\n channelAuthVersion: z.number(),\n inputFingerprint: z.string(),\n updatedAt: z.number(),\n});\nexport type ChatChannelAuthProjection = z.infer<typeof ChatChannelAuthProjectionSchema>;\n\n// ── chatScopeDegraded/{scopeKey} (+ causes/{causeId}) ───────────────────────\n// Exists IFF unresolvedCauseCount > 0. Grant mints fail closed while it exists.\nexport const ChatScopeDegradedSchema = z.object({\n scopeKey: z.string(),\n scopeKind: z.enum(['channel', 'memberInWork']),\n // channel scope carries the channelRef tuple; memberInWork carries {uid, workProjectId}.\n workProjectId: z.string().nullable(),\n guildChatChannelId: z.string().nullable(),\n guildInviteId: z.string().nullable(),\n uid: z.string().nullable(),\n unresolvedCauseCount: z.number(),\n updatedAt: z.number(),\n});\nexport type ChatScopeDegraded = z.infer<typeof ChatScopeDegradedSchema>;\n\nexport const ChatScopeDegradedCauseSchema = z.object({\n causeId: z.string(),\n kind: z.enum(['syncEvent', 'fanoutJob']),\n ref: z.string(),\n deadLetteredAt: z.number(),\n});\nexport type ChatScopeDegradedCause = z.infer<typeof ChatScopeDegradedCauseSchema>;\n\n// ── chatSyncEvents/{eventId} ────────────────────────────────────────────────\nexport const ChatSyncEventSchema = z.object({\n eventId: z.string(),\n targetDo: z.string(),\n kind: z.enum(['channelAuth', 'accountAccess', 'config', 'serverMessage']),\n version: z.number(),\n payload: z.record(z.string(), z.unknown()).nullable(),\n tombstone: z.boolean().optional(),\n payloadHash: z.string(),\n status: z.enum(['pending', 'delivered', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n deliveredResult: z.record(z.string(), z.unknown()).nullable(),\n deliveredAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n expireAt: expireAtField,\n});\nexport type ChatSyncEvent = z.infer<typeof ChatSyncEventSchema>;\n\n// ── chatSyncFanoutJobs/{jobId} ──────────────────────────────────────────────\nexport const ChatSyncFanoutJobSchema = z.object({\n jobId: z.string(),\n selectorKind: z.enum(['channelMembers', 'workChannelsForUser', 'policyEditAffectedUsers']),\n selectorArgs: z.object({\n workProjectId: z.string().optional(),\n channelKey: z.string().optional(),\n uid: z.string().optional(),\n }),\n causeVersion: z.number(),\n cursor: z.object({ pageIndex: z.number(), lastDocId: z.string().nullable() }),\n revision: z.number(),\n status: z.enum(['pending', 'complete', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n completedAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n expireAt: expireAtField,\n});\nexport type ChatSyncFanoutJob = z.infer<typeof ChatSyncFanoutJobSchema>;\n\n// ── chatMessageOutbox/{commandId} ───────────────────────────────────────────\nexport const ChatMessageOutboxSchema = z.object({\n commandId: z.string(),\n kind: z.enum(['userMsg', 'systemMsg']),\n threadRef: z.string(),\n payload: z.record(z.string(), z.unknown()),\n payloadVersion: z.number(),\n status: z.enum(['pending', 'applied', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n appliedAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n expireAt: expireAtField,\n});\nexport type ChatMessageOutbox = z.infer<typeof ChatMessageOutboxSchema>;\n\n// ── chatAdminActionCommands/{requestId} ─────────────────────────────────────\n// `requestId` IS the doc id + DO dedup key. `delivering` is NOT a lease (it sets\n// nextAttemptAt = now + DELIVERY_TIMEOUT). terminalFailureGeneration default 0.\nexport const ChatAdminActionCommandSchema = z.object({\n requestId: z.string(),\n actorUid: z.string(),\n actorMode: z.enum(['adminReview', 'adminOverride']),\n systemRole: z.enum(['admin', 'jrAdmin']),\n channelRef: z.record(z.string(), z.unknown()),\n messageSeq: z.number(),\n action: z.enum(['hide', 'delete']),\n caseId: z.string(),\n reason: z.string(),\n expectedMessageRevision: z.number(),\n payloadHash: z.string(),\n // `safetyRepair` is the NONTERMINAL stall loop: a RESERVED command whose reconcile\n // leg (PROBE → FINALIZE) failed past ADMIN_COMMAND_MAX_ATTEMPTS. Drained like\n // `delivering`; never dead-letters.\n state: z.enum(['queued', 'delivering', 'safetyRepair', 'applied', 'failed', 'deadLetter']),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n result: z.record(z.string(), z.unknown()).nullable(),\n doRevisionId: z.string().nullable(),\n terminalFailureGeneration: z.number(),\n createdAt: z.number(),\n appliedAt: z.number().nullable(),\n failedAt: z.number().nullable(),\n deadLetteredAt: z.number().nullable(),\n reconciled: z.boolean(),\n reconciledAt: z.number().nullable(),\n safetyRepair: z.boolean(),\n // Monotonic counter of distinct entries into the safety-repair loop — keys the\n // per-entry idempotent failure audit.\n safetyRepairGeneration: z.number(),\n // Failed reconcile passes on an already-reserved command. Separate from\n // `attemptCount` (delivery attempts, which dead-letter); at the cap it escalates\n // once into `safetyRepair` instead of dead-lettering.\n reconcileAttemptCount: z.number(),\n expireAt: expireAtField,\n});\nexport type ChatAdminActionCommand = z.infer<typeof ChatAdminActionCommandSchema>;\n\n// ── chatHistoryAnonymizationJobs/{jobId} ────────────────────────────────────\n// Account-deletion history anonymization (Contract F; P8). Resumable + crash-safe:\n// the recorded source-manifest identity (key/hash/generation) + the three cursors\n// (scan/rewrite/delete) make every boundary restartable. jobId =\n// hash('chat-anonymize', channelDoId, epoch, anonymizedUid). `expireAt` (native TTL)\n// is set ONLY when the job reaches `done`.\n//\n// C-B1: the replacement generation is DO-OWNED — the Channel DO (single-threaded per\n// channel) allocates a DISTINCT monotonic generation per erasure off the epoch row and\n// rewrites against the CURRENT live manifest, so two erasures on one epoch chain instead\n// of colliding on a shared `anon-1` keyspace. These four fields are therefore RECORDED AT\n// SCAN (null until the DO reports/allocates them), not pinned at enqueue; if a concurrent\n// anonymization supersedes the source between scan and swap, the job resets to `scanning`\n// to re-derive against the new current manifest (keeping its own allocated generation).\nexport const ChatHistoryAnonymizationJobSchema = z.object({\n jobId: z.string(),\n channelDoId: z.string(),\n epoch: z.number(),\n anonymizedUid: z.string(),\n sourceManifestKey: z.string().nullable(),\n sourceManifestHash: z.string().nullable(),\n sourceGeneration: z.number().nullable(),\n replacementGeneration: z.number().nullable(),\n newManifestKey: z.string().nullable(),\n scanCursor: z.number(),\n rewriteCursor: z.number(),\n deleteCursor: z.number(),\n swappedAt: z.number().nullable(),\n state: z.enum(['scanning', 'rewriting', 'swapping', 'deleting', 'done', 'failed']),\n legalHoldPaused: z.boolean(),\n attemptCount: z.number(),\n nextAttemptAt: z.number(),\n lastError: z.string().nullable(),\n createdAt: z.number(),\n updatedAt: z.number(),\n expireAt: expireAtField,\n});\nexport type ChatHistoryAnonymizationJob = z.infer<typeof ChatHistoryAnonymizationJobSchema>;\n\n// ── chatHistoryAnonymizationJobs/{jobId}/affectedChunks/{chunkOrdinal} ───────\n// The non-contiguous affected-chunk set (round-16 finding 7) — a crash after the\n// manifest swap resumes deletion from these rows + `deleteCursor` without re-listing R2.\nexport const ChatHistoryAnonymizationAffectedChunkSchema = z.object({\n chunkOrdinal: z.number(),\n sourceChunkKey: z.string(),\n sourceSha256: z.string(),\n replacementChunkKey: z.string().nullable(),\n replacementSha256: z.string().nullable(),\n rewriteState: z.enum(['pending', 'done']),\n deleteState: z.enum(['pending', 'done']),\n});\nexport type ChatHistoryAnonymizationAffectedChunk = z.infer<typeof ChatHistoryAnonymizationAffectedChunkSchema>;\n"]}
|
|
@@ -89,6 +89,7 @@ export declare const MediaActivationJobSchema: z.ZodObject<{
|
|
|
89
89
|
width: z.ZodOptional<z.ZodNumber>;
|
|
90
90
|
height: z.ZodOptional<z.ZodNumber>;
|
|
91
91
|
durationSec: z.ZodOptional<z.ZodNumber>;
|
|
92
|
+
downloadFilename: z.ZodOptional<z.ZodString>;
|
|
92
93
|
}, z.core.$strict>>;
|
|
93
94
|
updatedAtMs: z.ZodNumber;
|
|
94
95
|
}, z.core.$strict>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-activation-jobs.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/media-activation-jobs.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,eAAO,MAAM,8BAA8B;;;;;EAKzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF,eAAO,MAAM,wBAAwB
|
|
1
|
+
{"version":3,"file":"media-activation-jobs.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/media-activation-jobs.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,eAAO,MAAM,8BAA8B;;;;;EAKzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkC1B,CAAC;AACZ,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
|
@@ -150,6 +150,7 @@ export declare const MediaAssetVariantSchema: z.ZodObject<{
|
|
|
150
150
|
width: z.ZodOptional<z.ZodNumber>;
|
|
151
151
|
height: z.ZodOptional<z.ZodNumber>;
|
|
152
152
|
durationSec: z.ZodOptional<z.ZodNumber>;
|
|
153
|
+
downloadFilename: z.ZodOptional<z.ZodString>;
|
|
153
154
|
}, z.core.$strict>;
|
|
154
155
|
export type MediaAssetVariant = z.infer<typeof MediaAssetVariantSchema>;
|
|
155
156
|
export declare const MediaRetentionPolicySchema: z.ZodEnum<{
|
|
@@ -298,6 +299,7 @@ export declare const MediaServingAuthorityRecordSchema: z.ZodObject<{
|
|
|
298
299
|
width: z.ZodOptional<z.ZodNumber>;
|
|
299
300
|
height: z.ZodOptional<z.ZodNumber>;
|
|
300
301
|
durationSec: z.ZodOptional<z.ZodNumber>;
|
|
302
|
+
downloadFilename: z.ZodOptional<z.ZodString>;
|
|
301
303
|
}, z.core.$strict>>;
|
|
302
304
|
updatedAtMs: z.ZodNumber;
|
|
303
305
|
}, z.core.$strict>;
|
|
@@ -368,6 +370,7 @@ export declare const MediaAssetSchema: z.ZodObject<{
|
|
|
368
370
|
width: z.ZodOptional<z.ZodNumber>;
|
|
369
371
|
height: z.ZodOptional<z.ZodNumber>;
|
|
370
372
|
durationSec: z.ZodOptional<z.ZodNumber>;
|
|
373
|
+
downloadFilename: z.ZodOptional<z.ZodString>;
|
|
371
374
|
}, z.core.$strict>>;
|
|
372
375
|
moderationStatus: z.ZodEnum<{
|
|
373
376
|
approved: "approved";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-assets.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/media-assets.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,6DAA6D;AAC7D,eAAO,MAAM,qBAAqB;;;;;EAAsD,CAAC;AACzF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;4DAC4D;AAC5D,eAAO,MAAM,wBAAwB;;;;;EAA2D,CAAC;AACjG,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;uCAEuC;AACvC,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;EAiBpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAM5E;;uFAEuF;AACvF,eAAO,MAAM,sBAAsB;;;;EAAsC,CAAC;AAC1E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,8CAA+C,CAAC;AAC/E,eAAO,MAAM,qBAAqB;;;;;EAA6B,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;iCAKiC;AACjC,eAAO,MAAM,qBAAqB;;;;;;;;EAQhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0B5B,CAAC;AACZ,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,eAAO,MAAM,uBAAuB
|
|
1
|
+
{"version":3,"file":"media-assets.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/media-assets.ts"],"names":[],"mappings":"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"}
|
|
@@ -106,6 +106,25 @@ export const MediaAssetVariantSchema = z.object({
|
|
|
106
106
|
width: z.number().int().positive().optional(),
|
|
107
107
|
height: z.number().int().positive().optional(),
|
|
108
108
|
durationSec: z.number().positive().optional(),
|
|
109
|
+
/**
|
|
110
|
+
* Server-owned filename the gateway puts in `Content-Disposition` for a
|
|
111
|
+
* `?download=1` request. VARIANT-specific on purpose: the downloadable bytes are
|
|
112
|
+
* a PROCESSED variant, so the extension must match THIS variant's actual
|
|
113
|
+
* `contentType` — a transcoded JPEG/WebP/MP4 is never labelled with the original
|
|
114
|
+
* upload's incompatible extension.
|
|
115
|
+
*
|
|
116
|
+
* Written ONLY by backend publishers, and ONLY for surfaces that intentionally
|
|
117
|
+
* offer a Download action — Work Files (including the same asset shared to a
|
|
118
|
+
* Realm), Conversation Files, and downloadable Hall media. Profiles, posts,
|
|
119
|
+
* covers, and every other surface leave it ABSENT so no private original filename
|
|
120
|
+
* is ever exposed; an absent value serves the safe bare `attachment` disposition.
|
|
121
|
+
*
|
|
122
|
+
* Always produced by `normalizeDownloadFilename` (media/download-filename.ts) —
|
|
123
|
+
* that helper owns the sanitization rules and the UTF-8 byte cap, so no bound is
|
|
124
|
+
* restated here. A client query parameter is NEVER the filename authority.
|
|
125
|
+
* Filenames are user content: never log this value.
|
|
126
|
+
*/
|
|
127
|
+
downloadFilename: z.string().min(1).optional(),
|
|
109
128
|
}).strict();
|
|
110
129
|
export const MediaRetentionPolicySchema = z.enum([
|
|
111
130
|
'standard', // staging original deleted ~24h after verified mirror
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-assets.js","sourceRoot":"","sources":["../../src/doc-schemas/media-assets.ts"],"names":[],"mappings":"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","sourcesContent":["// mediaAssets/{mediaAssetId} — the canonical media registry. One doc per\n// logical uploaded media item; all variants inside. Content docs reference\n// media ONLY by `*AssetId` fields — never URLs. Serving authority is the\n// gateway Worker (cookie/grant + KV blocklist); this doc is the server-side\n// truth the edge records derive from. NO client reads, NO client writes.\n// See ttt-prod docs/design/media-assets-and-protected-serving.md.\n\nimport { z } from 'zod';\nimport { StructuredErrorSchema } from '@ttt-productions/edge-protocol-core';\nimport { FileOriginSchema } from '../media/file-origin.js';\n\n/** Who may be served the bytes (the Worker's tier check). */\nexport const MediaAccessTierSchema = z.enum(['broad', 'scoped', 'artisan', 'adminOnly']);\nexport type MediaAccessTier = z.infer<typeof MediaAccessTierSchema>;\n\n/** Canonical serving state. The edge record derives from this; hide/restore/\n * quarantine flows write it together with the blocklist. */\nexport const MediaServingStatusSchema = z.enum(['servable', 'hidden', 'quarantined', 'deleted']);\nexport type MediaServingStatus = z.infer<typeof MediaServingStatusSchema>;\n\n/** The surface that owns this asset. One asset never serves two scopes —\n * publishing to the Hall (or future realm promotion) creates a NEW asset\n * owned by the destination surface. */\nexport const MediaAssetOwnerTypeSchema = z.enum([\n 'userProfile',\n 'craftSkill',\n 'squareStreetzPost',\n 'workProject',\n 'workContent', // tale chapter / tune track / television episode media (pre-publish)\n 'hallItem',\n 'commissionListing',\n 'commissionProposal',\n 'audition',\n 'auditionEntry',\n // A file in a conversation's flat Conversation Files list (guild-invite or\n // admin-support). Replaces the removed `guildChatAttachment` owner type — chat\n // messages no longer own media.\n 'conversationFile',\n 'realmFile', // reserved seam — realm shared files are post-launch\n 'safetyEvidence', // inert system sentinel — synthetic NCII-evidence assets (never an account surface)\n]);\nexport type MediaAssetOwnerType = z.infer<typeof MediaAssetOwnerTypeSchema>;\n\n// ===== Media origin lineage (Appendix A0) =====\n// One canonical lineage used by media, CSAM, and safety. Embedded on every\n// media asset + every copy.\n\n/** The ONE canonical stored content media kind (what the pipeline produces and the\n * asset doc records). Distinct from the 4-value display/transport MediaTypeSchema\n * (doc-schemas/social.ts, adds 'other') and media-schemas' generic MediaKindSchema. */\nexport const ContentMediaKindSchema = z.enum(['image', 'video', 'audio']);\nexport type ContentMediaKind = z.infer<typeof ContentMediaKindSchema>;\n\n/**\n * The named-variant key set for `variantSha256s` — declared ONCE here; every declared\n * `image.variants[].key` in `TTT_MEDIA_SPECS` is compile-checked against this union\n * (the specs type their variant keys as `MediaVariantKey`), so an undeclared variant\n * name fails the BUILD — no sync test needed. Never an open `string`.\n */\nexport const MEDIA_VARIANT_KEYS = ['full', 'medium', 'small', 'main'] as const;\nexport const MediaVariantKeySchema = z.enum(MEDIA_VARIANT_KEYS);\nexport type MediaVariantKey = z.infer<typeof MediaVariantKeySchema>;\n\n/** Why this asset was created from its source (A0).\n *\n * `chat_derivative` was REMOVED with the chat-attachment architecture: chat is\n * text-only and owns no media, so no copy can be made \"for a chat message\". A\n * Conversation File is an ORIGINAL (`original`), never a chat derivative; nothing\n * ever set the retired value. */\nexport const MediaCopyReasonSchema = z.enum([\n 'original',\n 'variant',\n 'hall_publish',\n 'profile_derivative',\n 'moderation_copy',\n 'evidence_copy',\n 'other',\n]);\nexport type MediaCopyReason = z.infer<typeof MediaCopyReasonSchema>;\n\n/**\n * Canonical media origin lineage (Appendix A0). A copy INHERITS\n * rootIngestId/rootAssetId/originatingUploaderUid/originatingUploadEventId/\n * originalUploadCreatedAt/rootSha256/originalSha256 unchanged; it sets its own\n * sourceAssetId/variantSha256s/copyReason/copyActorUid/currentOwner*. Client\n * requests can NEVER set or override any lineage field. Automatic account\n * action targets ONLY `originatingUploaderUid`; incident-wide blocking\n * enumerates by `rootIngestId`. Missing/corrupt lineage → safetyLocked +\n * manual decision, never a guessed ban.\n */\nexport const MediaOriginLineageV1Schema = z.object({\n lineageVersion: z.literal(1),\n rootIngestId: z.string().min(1), // first-accepted ingest event id; stable across all copies\n rootAssetId: z.string().min(1), // first asset created from that ingest\n sourceAssetId: z.string().min(1).optional(), // immediate parent copied from (absent on the root)\n originatingUploaderUid: z.string().min(1), // set server-side at first accepted ingest; IMMUTABLE; inherited\n // May be '' for a legacy copy synthesized from a pre-rollout source with no known upload event.\n originatingUploadEventId: z.string(),\n originalUploadCreatedAt: z.number(),\n // '' until content hashing backfills the real sha256 (Phase 5 PhotoDNA / evidence capture).\n // NOT z.string().min(1): the media pipeline does not compute a content hash at finalize yet,\n // so a freshly-ingested asset legitimately carries an empty rootSha256 until it is hashed.\n rootSha256: z.string(),\n originalSha256: z.string().min(1).optional(),\n // PARTIAL map: one hash per PRODUCED variant (an asset produces only the variants its\n // origin declares — e.g. full/medium/small for a profile picture, 'main' for others —\n // never all four). Typed keys, never an open string. `z.partialRecord` (NOT `z.record`):\n // in Zod 4 `z.record(enum,…)` is EXHAUSTIVE (demands every enum key), which would reject\n // every real asset's partial variant set; `partialRecord` makes the keys optional.\n variantSha256s: z.partialRecord(MediaVariantKeySchema, z.string().min(1)),\n copyReason: MediaCopyReasonSchema,\n createdFromOwnerType: MediaAssetOwnerTypeSchema.optional(), // never a bare string\n createdFromOwnerId: z.string().min(1).optional(),\n copyActorUid: z.string().min(1).optional(), // who triggered the copy (distinct from the original uploader)\n currentOwnerType: MediaAssetOwnerTypeSchema, // mutable; the asset's present owner\n currentOwnerId: z.string().min(1),\n}).strict();\nexport type MediaOriginLineageV1 = z.infer<typeof MediaOriginLineageV1Schema>;\n\nexport const MediaAssetVariantSchema = z.object({\n contentType: z.string().min(1),\n sizeBytes: z.number().int().nonnegative(),\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n durationSec: z.number().positive().optional(),\n}).strict();\nexport type MediaAssetVariant = z.infer<typeof MediaAssetVariantSchema>;\n\nexport const MediaRetentionPolicySchema = z.enum([\n 'standard', // staging original deleted ~24h after verified mirror\n 'preserveForTrustAndSafety',\n 'legalHold',\n]);\nexport type MediaRetentionPolicy = z.infer<typeof MediaRetentionPolicySchema>;\n\n// Realm shared-files: a work file's standing in its realm's shared pool.\n// none — not shared to the realm (default)\n// nonCanon — shared to the realm's pool, non-canon\n// canon — shared and marked canon (realm steward/owner toggles canon ↔ nonCanon)\n// Moderation/takedown is NOT a value here — a hidden file is hidden via the shared\n// media path (`servingStatus: 'hidden'`), so the gallery filters on servingStatus.\nexport const RealmFileCanonStatusSchema = z.enum(['none', 'nonCanon', 'canon']);\nexport type RealmFileCanonStatus = z.infer<typeof RealmFileCanonStatusSchema>;\n\n// ===== Serving authority (Durable Object) + publication gating =====\n// See ttt-prod docs/design/media-assets-and-protected-serving.md (the design owner\n// for the Durable Object serving authority and publication gating).\n\n/** Whether the canonical serving record has been applied to the DO authority. */\nexport const MediaAuthoritySyncStateSchema = z.enum(['pending', 'applied', 'deadLetter']);\nexport type MediaAuthoritySyncState = z.infer<typeof MediaAuthoritySyncStateSchema>;\n\n/** Whether the asset is referenced by a visible owner doc. Orthogonal to the\n * upload's processing outcome (`pendingMedia.status`) and to `servingStatus`. */\nexport const MediaAssetPublicationStateSchema = z.enum(['unpublished', 'published', 'retired']);\nexport type MediaAssetPublicationState = z.infer<typeof MediaAssetPublicationStateSchema>;\n\n/**\n * Typed immutable serving scope for scoped-tier assets. The gateway Worker matches\n * a grant against the scope by `kind` — so a whole-Work `{w}` grant matches ONLY a\n * `workProject` scope, never a conversation scope, and a conversation grant is never\n * reachable by a Work asset.\n *\n * - `workProject` — pre-publish content media (hall covers / sub-item media during\n * authoring), scoped to project read membership. Carries the matchable\n * `workProjectId`. This is the typed home for work scope on the strongly-consistent\n * serving record so the DO-fallback path can authorize it (the DO persists scope\n * only as `scopeJson`, no separate column).\n * - `workFileFolder` — a work-project FILE living in a folder. The EXACT folder scope,\n * NOT the whole-Work `{w}` grant: a custom folder's bytes must be unreachable by a\n * guildmate outside that folder's view trade-professions, so the grant callable runs\n * the per-folder view check and the Worker EXACT-matches `{workProjectId,\n * workFileFolderId}` — a plain Work `{w}` grant NEVER matches a folder file, and a\n * folder grant NEVER matches a `workProject`-scoped content asset.\n * - `guildInvite` / `adminSupport` — the two Conversation Files scopes; an EXACT\n * conversation scope, matching the `ConversationFileRef` that owns the file. There is\n * NO guildChannel scope: guild chat channels have no Conversation Files (guildmates\n * share files through Work Files, which keeps its own workFileFolder scope).\n *\n * `null` scope = no scope match required beyond the access tier (e.g. broad).\n */\nexport const MediaServingScopeSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('workProject'),\n workProjectId: z.string().min(1),\n }).strict(),\n z.object({\n kind: z.literal('workFileFolder'),\n workProjectId: z.string().min(1),\n workFileFolderId: z.string().min(1),\n }).strict(),\n z.object({\n kind: z.literal('guildInvite'),\n guildInviteId: z.string().min(1),\n }).strict(),\n z.object({\n kind: z.literal('adminSupport'),\n adminDispatchId: z.string().min(1),\n }).strict(),\n]);\nexport type MediaServingScope = z.infer<typeof MediaServingScopeSchema>;\n\n/**\n * The typed owner adapter a publication goes through. The activation job\n * carries this kind; a server-side registry validates per-kind `publicationArgs`\n * and performs the idempotent owner write. Every kind is a Firestore-owner adapter.\n */\nexport const MediaPublicationKindSchema = z.enum([\n 'profilePicture',\n 'squarePostMedia',\n 'workAsset',\n 'hallCover',\n // Realm cover — Firestore-owner adapter writes `realmCoverAssetId` onto\n // workRealms/{workRealmId} (the 'realm-cover' upload origin).\n 'realmCover',\n 'workContentMedia', // sub-item media (tale chapter / tune track / television episode) — attaches the *AssetId field on the workContent doc; scoped to the work project\n 'auditionMedia',\n 'commissionListingMedia',\n 'commissionProposalMedia',\n 'craftSkillMedia',\n // Conversation File: the ONE adapter that creates a visible Conversation File owner\n // record ({conversationParent}/conversationFiles/{conversationFileId}) and transfers\n // the quota reservation to published counters in the same transaction. Replaces the\n // removed `chatAttachment` (Channel-DO flip) and `adminSupportAttachment` (message\n // placeholder flip) kinds. Carries the typed `guildInvite` / `adminSupport`\n // MediaServingScope of the owning conversation.\n 'conversationFile',\n]);\nexport type MediaPublicationKind = z.infer<typeof MediaPublicationKindSchema>;\n\n/**\n * The normalized serving record — the SAME contract used to generate both\n * the DO authority row and the KV cache payload (one function, so they can't\n * drift). The R2 object key stays deterministic (`mediaAssets/{assetId}/{key}`)\n * and is NOT persisted here. `payloadHash` is computed over the canonical\n * payload EXCLUDING the hash itself (see edge-protocol-core `hashPayload`).\n */\nexport const MediaServingAuthorityRecordSchema = z.object({\n schemaVersion: z.number().int().positive(),\n assetId: z.string().min(1),\n authorityVersion: z.number().int().nonnegative(),\n operationId: z.string().min(1),\n payloadHash: z.string().min(1),\n\n servingStatus: MediaServingStatusSchema,\n accessTier: MediaAccessTierSchema,\n\n ownerType: MediaAssetOwnerTypeSchema,\n ownerId: z.string().min(1),\n scope: MediaServingScopeSchema.nullable(),\n\n variants: z.record(z.string(), MediaAssetVariantSchema),\n\n updatedAtMs: z.number(),\n}).strict();\nexport type MediaServingAuthorityRecord = z.infer<typeof MediaServingAuthorityRecordSchema>;\n\nexport const MediaAssetSchema = z.object({\n mediaAssetId: z.string().min(1),\n mediaKind: ContentMediaKindSchema,\n fileOrigin: FileOriginSchema,\n\n // Ownership / scope. ownerId is the owning doc's id within ownerType's\n // surface; scope ids let the Worker/grant layer match scoped access.\n ownerType: MediaAssetOwnerTypeSchema,\n ownerId: z.string().min(1),\n workProjectId: z.string().optional(),\n realmId: z.string().optional(),\n createdByUid: z.string().min(1),\n\n // Access + serving (canonical; edge records derive from these).\n accessTier: MediaAccessTierSchema,\n servingStatus: MediaServingStatusSchema,\n\n // Variants by key. The object key in R2 (and the Storage emulator) is\n // deterministic — mediaAssets/{mediaAssetId}/{variantKey} — never stored.\n variants: z.record(z.string(), MediaAssetVariantSchema),\n\n // Lifecycle / trust-and-safety.\n moderationStatus: z.enum(['approved', 'quarantined']),\n retentionPolicy: MediaRetentionPolicySchema,\n legalHold: z.boolean(),\n originalDeletedAt: z.number().optional(),\n\n // T&S provenance lineage (Appendix A0). Additive/optional so assets written\n // before the lineage rollout still parse; set server-side at first accepted\n // ingest, inherited unchanged by every copy/variant. Never client-supplied.\n originLineage: MediaOriginLineageV1Schema.optional(),\n\n // Realm shared-files seam. A file's standing in its realm's shared pool; set at\n // creation to 'none' and moved by the realm shared-files callables. Attribution\n // fields (who set canon, when) are added by that feature when it ships.\n realmFileCanonStatus: RealmFileCanonStatusSchema,\n\n // Serving authority + publication gating. Additive/optional so assets\n // written before the authority build still parse: absent authorityVersion ⇒\n // 0/never-applied, absent publicationState ⇒ 'unpublished'. Every\n // serving-record mutation increments authorityVersion in an authoritative txn.\n authorityVersion: z.number().int().nonnegative().optional(),\n authorityPayloadHash: z.string().optional(),\n authoritySyncState: MediaAuthoritySyncStateSchema.optional(),\n publicationState: MediaAssetPublicationStateSchema.optional(),\n scope: MediaServingScopeSchema.optional(),\n authorityAppliedAt: z.number().optional(),\n publishedAt: z.number().optional(),\n lastAuthorityError: StructuredErrorSchema.optional(),\n // Authority-sync retry ledger (M1): the scheduled worker bumps attemptCount + sets a backoff\n // nextAttemptAt on a transient apply failure, and dead-letters (authoritySyncState='deadLetter')\n // past a threshold. Absent ⇒ 0 attempts / immediately due.\n authoritySyncAttemptCount: z.number().int().nonnegative().optional(),\n authoritySyncNextAttemptAt: z.number().optional(),\n\n createdAt: z.number(),\n updatedAt: z.number(),\n}).strict();\nexport type MediaAsset = z.infer<typeof MediaAssetSchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"media-assets.js","sourceRoot":"","sources":["../../src/doc-schemas/media-assets.ts"],"names":[],"mappings":"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","sourcesContent":["// mediaAssets/{mediaAssetId} — the canonical media registry. One doc per\n// logical uploaded media item; all variants inside. Content docs reference\n// media ONLY by `*AssetId` fields — never URLs. Serving authority is the\n// gateway Worker (cookie/grant + KV blocklist); this doc is the server-side\n// truth the edge records derive from. NO client reads, NO client writes.\n// See ttt-prod docs/design/media-assets-and-protected-serving.md.\n\nimport { z } from 'zod';\nimport { StructuredErrorSchema } from '@ttt-productions/edge-protocol-core';\nimport { FileOriginSchema } from '../media/file-origin.js';\n\n/** Who may be served the bytes (the Worker's tier check). */\nexport const MediaAccessTierSchema = z.enum(['broad', 'scoped', 'artisan', 'adminOnly']);\nexport type MediaAccessTier = z.infer<typeof MediaAccessTierSchema>;\n\n/** Canonical serving state. The edge record derives from this; hide/restore/\n * quarantine flows write it together with the blocklist. */\nexport const MediaServingStatusSchema = z.enum(['servable', 'hidden', 'quarantined', 'deleted']);\nexport type MediaServingStatus = z.infer<typeof MediaServingStatusSchema>;\n\n/** The surface that owns this asset. One asset never serves two scopes —\n * publishing to the Hall (or future realm promotion) creates a NEW asset\n * owned by the destination surface. */\nexport const MediaAssetOwnerTypeSchema = z.enum([\n 'userProfile',\n 'craftSkill',\n 'squareStreetzPost',\n 'workProject',\n 'workContent', // tale chapter / tune track / television episode media (pre-publish)\n 'hallItem',\n 'commissionListing',\n 'commissionProposal',\n 'audition',\n 'auditionEntry',\n // A file in a conversation's flat Conversation Files list (guild-invite or\n // admin-support). Replaces the removed `guildChatAttachment` owner type — chat\n // messages no longer own media.\n 'conversationFile',\n 'realmFile', // reserved seam — realm shared files are post-launch\n 'safetyEvidence', // inert system sentinel — synthetic NCII-evidence assets (never an account surface)\n]);\nexport type MediaAssetOwnerType = z.infer<typeof MediaAssetOwnerTypeSchema>;\n\n// ===== Media origin lineage (Appendix A0) =====\n// One canonical lineage used by media, CSAM, and safety. Embedded on every\n// media asset + every copy.\n\n/** The ONE canonical stored content media kind (what the pipeline produces and the\n * asset doc records). Distinct from the 4-value display/transport MediaTypeSchema\n * (doc-schemas/social.ts, adds 'other') and media-schemas' generic MediaKindSchema. */\nexport const ContentMediaKindSchema = z.enum(['image', 'video', 'audio']);\nexport type ContentMediaKind = z.infer<typeof ContentMediaKindSchema>;\n\n/**\n * The named-variant key set for `variantSha256s` — declared ONCE here; every declared\n * `image.variants[].key` in `TTT_MEDIA_SPECS` is compile-checked against this union\n * (the specs type their variant keys as `MediaVariantKey`), so an undeclared variant\n * name fails the BUILD — no sync test needed. Never an open `string`.\n */\nexport const MEDIA_VARIANT_KEYS = ['full', 'medium', 'small', 'main'] as const;\nexport const MediaVariantKeySchema = z.enum(MEDIA_VARIANT_KEYS);\nexport type MediaVariantKey = z.infer<typeof MediaVariantKeySchema>;\n\n/** Why this asset was created from its source (A0).\n *\n * `chat_derivative` was REMOVED with the chat-attachment architecture: chat is\n * text-only and owns no media, so no copy can be made \"for a chat message\". A\n * Conversation File is an ORIGINAL (`original`), never a chat derivative; nothing\n * ever set the retired value. */\nexport const MediaCopyReasonSchema = z.enum([\n 'original',\n 'variant',\n 'hall_publish',\n 'profile_derivative',\n 'moderation_copy',\n 'evidence_copy',\n 'other',\n]);\nexport type MediaCopyReason = z.infer<typeof MediaCopyReasonSchema>;\n\n/**\n * Canonical media origin lineage (Appendix A0). A copy INHERITS\n * rootIngestId/rootAssetId/originatingUploaderUid/originatingUploadEventId/\n * originalUploadCreatedAt/rootSha256/originalSha256 unchanged; it sets its own\n * sourceAssetId/variantSha256s/copyReason/copyActorUid/currentOwner*. Client\n * requests can NEVER set or override any lineage field. Automatic account\n * action targets ONLY `originatingUploaderUid`; incident-wide blocking\n * enumerates by `rootIngestId`. Missing/corrupt lineage → safetyLocked +\n * manual decision, never a guessed ban.\n */\nexport const MediaOriginLineageV1Schema = z.object({\n lineageVersion: z.literal(1),\n rootIngestId: z.string().min(1), // first-accepted ingest event id; stable across all copies\n rootAssetId: z.string().min(1), // first asset created from that ingest\n sourceAssetId: z.string().min(1).optional(), // immediate parent copied from (absent on the root)\n originatingUploaderUid: z.string().min(1), // set server-side at first accepted ingest; IMMUTABLE; inherited\n // May be '' for a legacy copy synthesized from a pre-rollout source with no known upload event.\n originatingUploadEventId: z.string(),\n originalUploadCreatedAt: z.number(),\n // '' until content hashing backfills the real sha256 (Phase 5 PhotoDNA / evidence capture).\n // NOT z.string().min(1): the media pipeline does not compute a content hash at finalize yet,\n // so a freshly-ingested asset legitimately carries an empty rootSha256 until it is hashed.\n rootSha256: z.string(),\n originalSha256: z.string().min(1).optional(),\n // PARTIAL map: one hash per PRODUCED variant (an asset produces only the variants its\n // origin declares — e.g. full/medium/small for a profile picture, 'main' for others —\n // never all four). Typed keys, never an open string. `z.partialRecord` (NOT `z.record`):\n // in Zod 4 `z.record(enum,…)` is EXHAUSTIVE (demands every enum key), which would reject\n // every real asset's partial variant set; `partialRecord` makes the keys optional.\n variantSha256s: z.partialRecord(MediaVariantKeySchema, z.string().min(1)),\n copyReason: MediaCopyReasonSchema,\n createdFromOwnerType: MediaAssetOwnerTypeSchema.optional(), // never a bare string\n createdFromOwnerId: z.string().min(1).optional(),\n copyActorUid: z.string().min(1).optional(), // who triggered the copy (distinct from the original uploader)\n currentOwnerType: MediaAssetOwnerTypeSchema, // mutable; the asset's present owner\n currentOwnerId: z.string().min(1),\n}).strict();\nexport type MediaOriginLineageV1 = z.infer<typeof MediaOriginLineageV1Schema>;\n\nexport const MediaAssetVariantSchema = z.object({\n contentType: z.string().min(1),\n sizeBytes: z.number().int().nonnegative(),\n width: z.number().int().positive().optional(),\n height: z.number().int().positive().optional(),\n durationSec: z.number().positive().optional(),\n /**\n * Server-owned filename the gateway puts in `Content-Disposition` for a\n * `?download=1` request. VARIANT-specific on purpose: the downloadable bytes are\n * a PROCESSED variant, so the extension must match THIS variant's actual\n * `contentType` — a transcoded JPEG/WebP/MP4 is never labelled with the original\n * upload's incompatible extension.\n *\n * Written ONLY by backend publishers, and ONLY for surfaces that intentionally\n * offer a Download action — Work Files (including the same asset shared to a\n * Realm), Conversation Files, and downloadable Hall media. Profiles, posts,\n * covers, and every other surface leave it ABSENT so no private original filename\n * is ever exposed; an absent value serves the safe bare `attachment` disposition.\n *\n * Always produced by `normalizeDownloadFilename` (media/download-filename.ts) —\n * that helper owns the sanitization rules and the UTF-8 byte cap, so no bound is\n * restated here. A client query parameter is NEVER the filename authority.\n * Filenames are user content: never log this value.\n */\n downloadFilename: z.string().min(1).optional(),\n}).strict();\nexport type MediaAssetVariant = z.infer<typeof MediaAssetVariantSchema>;\n\nexport const MediaRetentionPolicySchema = z.enum([\n 'standard', // staging original deleted ~24h after verified mirror\n 'preserveForTrustAndSafety',\n 'legalHold',\n]);\nexport type MediaRetentionPolicy = z.infer<typeof MediaRetentionPolicySchema>;\n\n// Realm shared-files: a work file's standing in its realm's shared pool.\n// none — not shared to the realm (default)\n// nonCanon — shared to the realm's pool, non-canon\n// canon — shared and marked canon (realm steward/owner toggles canon ↔ nonCanon)\n// Moderation/takedown is NOT a value here — a hidden file is hidden via the shared\n// media path (`servingStatus: 'hidden'`), so the gallery filters on servingStatus.\nexport const RealmFileCanonStatusSchema = z.enum(['none', 'nonCanon', 'canon']);\nexport type RealmFileCanonStatus = z.infer<typeof RealmFileCanonStatusSchema>;\n\n// ===== Serving authority (Durable Object) + publication gating =====\n// See ttt-prod docs/design/media-assets-and-protected-serving.md (the design owner\n// for the Durable Object serving authority and publication gating).\n\n/** Whether the canonical serving record has been applied to the DO authority. */\nexport const MediaAuthoritySyncStateSchema = z.enum(['pending', 'applied', 'deadLetter']);\nexport type MediaAuthoritySyncState = z.infer<typeof MediaAuthoritySyncStateSchema>;\n\n/** Whether the asset is referenced by a visible owner doc. Orthogonal to the\n * upload's processing outcome (`pendingMedia.status`) and to `servingStatus`. */\nexport const MediaAssetPublicationStateSchema = z.enum(['unpublished', 'published', 'retired']);\nexport type MediaAssetPublicationState = z.infer<typeof MediaAssetPublicationStateSchema>;\n\n/**\n * Typed immutable serving scope for scoped-tier assets. The gateway Worker matches\n * a grant against the scope by `kind` — so a whole-Work `{w}` grant matches ONLY a\n * `workProject` scope, never a conversation scope, and a conversation grant is never\n * reachable by a Work asset.\n *\n * - `workProject` — pre-publish content media (hall covers / sub-item media during\n * authoring), scoped to project read membership. Carries the matchable\n * `workProjectId`. This is the typed home for work scope on the strongly-consistent\n * serving record so the DO-fallback path can authorize it (the DO persists scope\n * only as `scopeJson`, no separate column).\n * - `workFileFolder` — a work-project FILE living in a folder. The EXACT folder scope,\n * NOT the whole-Work `{w}` grant: a custom folder's bytes must be unreachable by a\n * guildmate outside that folder's view trade-professions, so the grant callable runs\n * the per-folder view check and the Worker EXACT-matches `{workProjectId,\n * workFileFolderId}` — a plain Work `{w}` grant NEVER matches a folder file, and a\n * folder grant NEVER matches a `workProject`-scoped content asset.\n * - `guildInvite` / `adminSupport` — the two Conversation Files scopes; an EXACT\n * conversation scope, matching the `ConversationFileRef` that owns the file. There is\n * NO guildChannel scope: guild chat channels have no Conversation Files (guildmates\n * share files through Work Files, which keeps its own workFileFolder scope).\n *\n * `null` scope = no scope match required beyond the access tier (e.g. broad).\n */\nexport const MediaServingScopeSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('workProject'),\n workProjectId: z.string().min(1),\n }).strict(),\n z.object({\n kind: z.literal('workFileFolder'),\n workProjectId: z.string().min(1),\n workFileFolderId: z.string().min(1),\n }).strict(),\n z.object({\n kind: z.literal('guildInvite'),\n guildInviteId: z.string().min(1),\n }).strict(),\n z.object({\n kind: z.literal('adminSupport'),\n adminDispatchId: z.string().min(1),\n }).strict(),\n]);\nexport type MediaServingScope = z.infer<typeof MediaServingScopeSchema>;\n\n/**\n * The typed owner adapter a publication goes through. The activation job\n * carries this kind; a server-side registry validates per-kind `publicationArgs`\n * and performs the idempotent owner write. Every kind is a Firestore-owner adapter.\n */\nexport const MediaPublicationKindSchema = z.enum([\n 'profilePicture',\n 'squarePostMedia',\n 'workAsset',\n 'hallCover',\n // Realm cover — Firestore-owner adapter writes `realmCoverAssetId` onto\n // workRealms/{workRealmId} (the 'realm-cover' upload origin).\n 'realmCover',\n 'workContentMedia', // sub-item media (tale chapter / tune track / television episode) — attaches the *AssetId field on the workContent doc; scoped to the work project\n 'auditionMedia',\n 'commissionListingMedia',\n 'commissionProposalMedia',\n 'craftSkillMedia',\n // Conversation File: the ONE adapter that creates a visible Conversation File owner\n // record ({conversationParent}/conversationFiles/{conversationFileId}) and transfers\n // the quota reservation to published counters in the same transaction. Replaces the\n // removed `chatAttachment` (Channel-DO flip) and `adminSupportAttachment` (message\n // placeholder flip) kinds. Carries the typed `guildInvite` / `adminSupport`\n // MediaServingScope of the owning conversation.\n 'conversationFile',\n]);\nexport type MediaPublicationKind = z.infer<typeof MediaPublicationKindSchema>;\n\n/**\n * The normalized serving record — the SAME contract used to generate both\n * the DO authority row and the KV cache payload (one function, so they can't\n * drift). The R2 object key stays deterministic (`mediaAssets/{assetId}/{key}`)\n * and is NOT persisted here. `payloadHash` is computed over the canonical\n * payload EXCLUDING the hash itself (see edge-protocol-core `hashPayload`).\n */\nexport const MediaServingAuthorityRecordSchema = z.object({\n schemaVersion: z.number().int().positive(),\n assetId: z.string().min(1),\n authorityVersion: z.number().int().nonnegative(),\n operationId: z.string().min(1),\n payloadHash: z.string().min(1),\n\n servingStatus: MediaServingStatusSchema,\n accessTier: MediaAccessTierSchema,\n\n ownerType: MediaAssetOwnerTypeSchema,\n ownerId: z.string().min(1),\n scope: MediaServingScopeSchema.nullable(),\n\n variants: z.record(z.string(), MediaAssetVariantSchema),\n\n updatedAtMs: z.number(),\n}).strict();\nexport type MediaServingAuthorityRecord = z.infer<typeof MediaServingAuthorityRecordSchema>;\n\nexport const MediaAssetSchema = z.object({\n mediaAssetId: z.string().min(1),\n mediaKind: ContentMediaKindSchema,\n fileOrigin: FileOriginSchema,\n\n // Ownership / scope. ownerId is the owning doc's id within ownerType's\n // surface; scope ids let the Worker/grant layer match scoped access.\n ownerType: MediaAssetOwnerTypeSchema,\n ownerId: z.string().min(1),\n workProjectId: z.string().optional(),\n realmId: z.string().optional(),\n createdByUid: z.string().min(1),\n\n // Access + serving (canonical; edge records derive from these).\n accessTier: MediaAccessTierSchema,\n servingStatus: MediaServingStatusSchema,\n\n // Variants by key. The object key in R2 (and the Storage emulator) is\n // deterministic — mediaAssets/{mediaAssetId}/{variantKey} — never stored.\n variants: z.record(z.string(), MediaAssetVariantSchema),\n\n // Lifecycle / trust-and-safety.\n moderationStatus: z.enum(['approved', 'quarantined']),\n retentionPolicy: MediaRetentionPolicySchema,\n legalHold: z.boolean(),\n originalDeletedAt: z.number().optional(),\n\n // T&S provenance lineage (Appendix A0). Additive/optional so assets written\n // before the lineage rollout still parse; set server-side at first accepted\n // ingest, inherited unchanged by every copy/variant. Never client-supplied.\n originLineage: MediaOriginLineageV1Schema.optional(),\n\n // Realm shared-files seam. A file's standing in its realm's shared pool; set at\n // creation to 'none' and moved by the realm shared-files callables. Attribution\n // fields (who set canon, when) are added by that feature when it ships.\n realmFileCanonStatus: RealmFileCanonStatusSchema,\n\n // Serving authority + publication gating. Additive/optional so assets\n // written before the authority build still parse: absent authorityVersion ⇒\n // 0/never-applied, absent publicationState ⇒ 'unpublished'. Every\n // serving-record mutation increments authorityVersion in an authoritative txn.\n authorityVersion: z.number().int().nonnegative().optional(),\n authorityPayloadHash: z.string().optional(),\n authoritySyncState: MediaAuthoritySyncStateSchema.optional(),\n publicationState: MediaAssetPublicationStateSchema.optional(),\n scope: MediaServingScopeSchema.optional(),\n authorityAppliedAt: z.number().optional(),\n publishedAt: z.number().optional(),\n lastAuthorityError: StructuredErrorSchema.optional(),\n // Authority-sync retry ledger (M1): the scheduled worker bumps attemptCount + sets a backoff\n // nextAttemptAt on a transient apply failure, and dead-letters (authoritySyncState='deadLetter')\n // past a threshold. Absent ⇒ 0 attempts / immediately due.\n authoritySyncAttemptCount: z.number().int().nonnegative().optional(),\n authoritySyncNextAttemptAt: z.number().optional(),\n\n createdAt: z.number(),\n updatedAt: z.number(),\n}).strict();\nexport type MediaAsset = z.infer<typeof MediaAssetSchema>;\n"]}
|
|
@@ -137,10 +137,10 @@ export declare const AdminDispatchSchema: z.ZodObject<{
|
|
|
137
137
|
conversationFileUploadCount: z.ZodOptional<z.ZodNumber>;
|
|
138
138
|
conversationFileBytesReserved: z.ZodOptional<z.ZodNumber>;
|
|
139
139
|
adminDispatchId: z.ZodString;
|
|
140
|
-
partyKind: z.
|
|
140
|
+
partyKind: z.ZodEnum<{
|
|
141
141
|
workProject: "workProject";
|
|
142
142
|
user: "user";
|
|
143
|
-
}
|
|
143
|
+
}>;
|
|
144
144
|
workProjectId: z.ZodOptional<z.ZodString>;
|
|
145
145
|
contextRef: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
146
146
|
kind: z.ZodLiteral<"thresholdItem">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messaging.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/messaging.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA8BxB;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;iBAWjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;iBAwBjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAQtE,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAMpF,eAAO,MAAM,6BAA6B;;;;;;;;;;2BASxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,eAAO,MAAM,4BAA4B;;;EAAkC,CAAC;AAC5E,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"messaging.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/messaging.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA8BxB;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;iBAWjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;iBAwBjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAQtE,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAMpF,eAAO,MAAM,6BAA6B;;;;;;;;;;2BASxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,eAAO,MAAM,4BAA4B;;;EAAkC,CAAC;AAC5E,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2B9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAWhE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;iBAoB9B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
@@ -143,13 +143,15 @@ export const AdminDispatchPartyKindSchema = z.enum(['user', 'workProject']);
|
|
|
143
143
|
export const AdminDispatchSchema = z.object({
|
|
144
144
|
adminDispatchId: z.string(),
|
|
145
145
|
// Party-generic dispatch: the platform corresponds with a PARTY — a user or a
|
|
146
|
-
// workProject.
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
146
|
+
// workProject. REQUIRED and written explicitly at creation: the personal-threads
|
|
147
|
+
// client query filters `partyKind == 'user'` and the Firestore read rules branch
|
|
148
|
+
// on it party-aware, so an absent field is unqueryable and unauthorizable. For
|
|
149
|
+
// 'user' threads `userId` is the thread owner (the sole member-side authority).
|
|
150
|
+
// For 'workProject' threads `workProjectId` is set, read/reply authority is
|
|
151
|
+
// ACTIVE GUILDMATE standing evaluated server-side (never a stored member list),
|
|
152
|
+
// and `userId` holds the INITIATING member's uid for attribution only — it is
|
|
153
|
+
// never a work-thread authority.
|
|
154
|
+
partyKind: AdminDispatchPartyKindSchema,
|
|
153
155
|
workProjectId: z.string().optional(),
|
|
154
156
|
contextRef: AdminDispatchContextRefSchema.optional(),
|
|
155
157
|
userId: z.string(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messaging.js","sourceRoot":"","sources":["../../src/doc-schemas/messaging.ts"],"names":[],"mappings":"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","sourcesContent":["// Messaging parent-thread Firestore document SCHEMAS — guildChatChannels, the\n// guildInviteConversations shell, and pendingAdminDispatches. Per-message body shapes\n// live in @ttt-productions/chat-core (ChatMessageV1); ttt-core owns the thread docs.\n// Types inferred via z.infer.\n\nimport { z } from 'zod';\nimport { ReplyToSchema } from '@ttt-productions/chat-schemas';\nimport { InviteSourceSchema } from '../schemas/work-project-management.js';\nimport { guildInviteConversationStatusSchema } from '../schemas/atoms.js';\nimport { ContentMediaKindSchema } from './media-assets.js';\n\nconst userRefSchema = z.object({ uid: z.string() });\n\n// ===========================================================================\n// CONVERSATION FILES — the flat, Firestore-owned file list on a conversation.\n// Replaces inline chat attachments: a file is associated WITH a conversation but\n// never embedded in, sequenced with, or mutated through a chat message.\n// See ttt-prod docs/design/upload-and-media-pipeline.md.\n// ===========================================================================\n\n/** Backend-owned Conversation Files counters carried by EVERY conversation parent\n * (guildInviteConversations/{id} and pendingAdminDispatches/{id}). All four are\n * optional — an absent field reads as 0, so no pre-existing conversation needs a\n * backfill. Clients can never write them (rules + callable-only writes). */\nconst conversationFileCounterFields = {\n /** Published ACTIVE files in this conversation. */\n conversationFileCount: z.number().optional(),\n /** Published STORED-OUTPUT bytes in this conversation. */\n conversationFileBytesUsed: z.number().optional(),\n /** Active quota RESERVATIONS (uploads in flight). */\n conversationFileUploadCount: z.number().optional(),\n /** Bytes reserved by active uploads. */\n conversationFileBytesReserved: z.number().optional(),\n} as const;\n\n/**\n * `{conversationParent}/conversationFiles/{conversationFileId}` — the conversation's\n * lightweight ownership/list projection, analogous to `WorkFileSchema`.\n *\n * DELIBERATELY ABSENT (owned elsewhere, never duplicated here):\n * - no `status` / `ready` / `publicationState` / processing progress — `pendingMedia`\n * and `mediaAssets` own the lifecycle; the mere EXISTENCE of this doc means the\n * asset is published;\n * - no Firebase / R2 / gateway / signed / download URL — display URLs are built at\n * render time through the protected media gateway;\n * - no Firebase staging `storagePath`;\n * - no username / display name / avatar / Work name snapshot (ARCH-103) — resolve\n * `uploadedByUid` at render time.\n *\n * `conversationFileId` is deterministic: it IS the `pendingMediaId`. The publication\n * adapter creates this doc with `transaction.create` (never `set`), so an id\n * collision cannot overwrite and double-count an existing file.\n */\nexport const ConversationFileSchema = z.object({\n conversationFileId: z.string(),\n mediaAssetId: z.string(),\n name: z.string(),\n // The canonical stored content media kind — never a re-declared union (ARCH-102).\n mediaKind: ContentMediaKindSchema,\n contentType: z.string(),\n /** Sum of the PUBLISHED variant bytes (stored output, not raw upload bytes). */\n sizeBytes: z.number(),\n uploadedByUid: z.string(),\n createdAt: z.number(),\n});\nexport type ConversationFile = z.infer<typeof ConversationFileSchema>;\n\nexport const GuildChatChannelSchema = z.object({\n guildChatChannelId: z.string(),\n workProjectId: z.string(),\n channelName: z.string(),\n description: z.string().optional(),\n requiredGuildStandings: z.array(z.string()),\n allowedUserIds: z.array(z.string()),\n createdAt: z.number(),\n // NOTE: createdBy here is a flat uid string (unlike the `{ uid }` object form on\n // most other docs). Preserved as-is; flagged in the schema-registry recon.\n createdBy: z.string(),\n lastMessageAt: z.string().optional(),\n lastMessage: z.string().optional(),\n messageCount: z.number(),\n isArchived: z.boolean(),\n // Delete/tombstone marker (ttt-prod docs/design/chat-realtime-system.md \"Channel lifecycle semantics\").\n // Independent of `isArchived`: archive = visible-under-a-toggle, delete = gone from every user's\n // UI with grants revoked and sends rejected DO-side; storage is RETAINED (never a physical purge).\n // Optional, absent ⇒ false, so existing/seeded channels need no backfill. Backend-only-writable.\n isDeleted: z.boolean().optional(),\n // Monotonic config version for the chat-realtime sync layer (Contract B). Bumped by\n // each channel create / edit / archive / delete in the authoritative txn; the\n // `config` chatSyncEvents + channel-scoped fanout key on it. Absent ⇒ 0.\n configVersion: z.number().optional(),\n});\nexport type GuildChatChannel = z.infer<typeof GuildChatChannelSchema>;\n\n// The guildInviteConversations/{guildInviteId} doc as actually written by inviteUserToGuild\n// (and mutated by the accept/decline/finalize/cancel callables). This is the single source of\n// truth — the former duplicate `GuildInviteSchema` in ./work-project.ts was a stale, narrower\n// shape that the callable bypassed with an `as unknown as` cast. The inviter is `createdBy`\n// (== `sender`); there is no separate `workSteward`/`workProjectTitle` field (the title lives in\n// `workProject.workingTitle`).\nexport const GuildInviteConversationSchema = z.object({\n guildInviteId: z.string(),\n workProjectId: z.string(),\n relatedUserIds: z.array(z.string()),\n // Title/description are NOT snapshotted (Display Identity Invariant — resolve at render\n // from publicWorkProjects by id). `type` (Tales/Tunes/Television) is an immutable\n // classification, not display text, and IS consumed by an invites-list type filter\n // (guild-invites-section.tsx), so it stays. Triggers read only `workProjectId`.\n workProject: z.object({\n workProjectId: z.string(),\n type: z.string(),\n }),\n createdBy: userRefSchema,\n sender: userRefSchema,\n recipient: userRefSchema,\n stakeSharesOffered: z.number(),\n source: InviteSourceSchema,\n // State machine: pending → accepted (transient, trigger-consumed) → finalized (terminal\n // success), or pending → declined / cancelled (terminal failure). No 'error' state is ever\n // written (dead state removed 2026-07-03). See ttt-prod\n // docs/design/work-guild-membership-and-stake.md for the invite lifecycle.\n status: guildInviteConversationStatusSchema,\n createdAt: z.number(),\n updatedAt: z.number(),\n lastUpdatedAt: z.number(),\n finalizedAt: z.number().optional(),\n senderConfirmed: z.boolean(),\n recipientConfirmed: z.boolean(),\n lastMessage: z.string().optional(),\n lastMessageAt: z.string().optional(),\n // Conversation Files quota counters for THIS invite conversation (per-conversation\n // caps: MAX_CONVERSATION_FILES / MAX_CONVERSATION_FILE_STORAGE_BYTES). Reserved at\n // startUpload, transferred to used at publication, released on every terminal path,\n // decremented on delete. Backend-only-writable; absent ⇒ 0.\n ...conversationFileCounterFields,\n});\nexport type GuildInviteConversation = z.infer<typeof GuildInviteConversationSchema>;\n\n// Typed context ref carried by a dispatch thread born from a review send-back, a\n// moderation placeholder, or a published change request — lets the admin queue card\n// say what the thread is about without parsing the subject line. Extensible union;\n// server-validated against the thread's party (never trusted raw from the client).\nexport const AdminDispatchContextRefSchema = z.discriminatedUnion('kind', [\n z.object({ kind: z.literal('thresholdItem'), thresholdItemId: z.string() }),\n z.object({\n kind: z.literal('hallContent'),\n hallItemId: z.string(),\n // null ⇒ the hall parent DETAIL; set ⇒ a chapter/track/episode sub-item.\n subItemId: z.string().nullable(),\n }),\n z.object({ kind: z.literal('hallContentChangeRequest'), changeRequestId: z.string() }),\n]);\nexport type AdminDispatchContextRef = z.infer<typeof AdminDispatchContextRefSchema>;\n\nexport const AdminDispatchPartyKindSchema = z.enum(['user', 'workProject']);\nexport type AdminDispatchPartyKind = z.infer<typeof AdminDispatchPartyKindSchema>;\n\nexport const AdminDispatchSchema = z.object({\n adminDispatchId: z.string(),\n // Party-generic dispatch: the platform corresponds with a PARTY — a user or a\n // workProject. Absent ⇒ 'user' (every pre-generalization thread). For 'user'\n // threads `userId` is the thread owner (the sole member-side authority). For\n // 'workProject' threads `workProjectId` is set, read/reply authority is ACTIVE\n // GUILDMATE standing evaluated server-side (never a stored member list), and\n // `userId` holds the INITIATING member's uid for attribution only — it is never\n // a work-thread authority.\n partyKind: AdminDispatchPartyKindSchema.optional(),\n workProjectId: z.string().optional(),\n contextRef: AdminDispatchContextRefSchema.optional(),\n userId: z.string(),\n initiatorUserId: z.string(),\n initiatedBy: z.enum(['user', 'admin']),\n subject: z.string(),\n status: z.enum(['open', 'user_reply', 'admin_reply', 'closed_resolved', 'closed_unresolved']),\n createdAt: z.number(),\n lastUpdatedAt: z.number(),\n readByAdmin: z.boolean(),\n readByUser: z.boolean(),\n closedBy: z.string().optional(),\n // Conversation Files quota counters for THIS admin-support thread — same\n // per-conversation caps and same backend-only ownership as the invite conversation.\n ...conversationFileCounterFields,\n});\nexport type AdminDispatch = z.infer<typeof AdminDispatchSchema>;\n\n// Per-message body for admin-support `conversationMessages` — the ONE chat surface still\n// transported through Firestore (guild channel + invite messages are realtime-only, served\n// by the chat Worker Durable Object). Written by runSendGuildChatMessage (senderId/text/\n// createdAt + optional replyTo); the admin-dispatch INITIAL message\n// (runStartAdminSupportThread) additionally stores `messageId`. This is the STORED shape — a relaxed\n// @ttt-productions/chat-core ChatMessageV1: `messageId` is the doc id (only sometimes persisted) and\n// `threadId` is not stored. Reuses the chat-schemas ReplyTo Zod shape.\n// A chat message carries NO file reference — files live in the conversation's\n// `conversationFiles` subcollection (ConversationFileSchema above).\nexport const ChatMessageV1Schema = z.object({\n senderId: z.string(),\n text: z.string(),\n createdAt: z.number(),\n messageId: z.string().optional(),\n threadId: z.string().optional(),\n type: z.string().optional(),\n // Rides on the realtime invite-thread envelope (chat Worker invite Durable Object);\n // guild-invite messages are realtime-only — there is no Firestore inviteMessages\n // subcollection. Optional so the same body schema still fits the guildChat /\n // admin-dispatch messages that don't carry it.\n guildInviteId: z.string().optional(),\n replyTo: ReplyToSchema.optional(),\n isSystemMessage: z.boolean().optional(),\n meta: z.record(z.string(), z.unknown()).optional(),\n // Moderation tombstone flag — the single-doc `hidden` flip written by the admin\n // moderateReportedContent pipeline (the admin-work-message itemType). Backend-only\n // writable (conversationMessages is callable-only in rules); absent ⇒ visible.\n // Thread views render a hidden message as a moderation tombstone.\n hidden: z.boolean().optional(),\n});\nexport type ChatMessageV1Doc = z.infer<typeof ChatMessageV1Schema>;\n"]}
|
|
1
|
+
{"version":3,"file":"messaging.js","sourceRoot":"","sources":["../../src/doc-schemas/messaging.ts"],"names":[],"mappings":"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","sourcesContent":["// Messaging parent-thread Firestore document SCHEMAS — guildChatChannels, the\n// guildInviteConversations shell, and pendingAdminDispatches. Per-message body shapes\n// live in @ttt-productions/chat-core (ChatMessageV1); ttt-core owns the thread docs.\n// Types inferred via z.infer.\n\nimport { z } from 'zod';\nimport { ReplyToSchema } from '@ttt-productions/chat-schemas';\nimport { InviteSourceSchema } from '../schemas/work-project-management.js';\nimport { guildInviteConversationStatusSchema } from '../schemas/atoms.js';\nimport { ContentMediaKindSchema } from './media-assets.js';\n\nconst userRefSchema = z.object({ uid: z.string() });\n\n// ===========================================================================\n// CONVERSATION FILES — the flat, Firestore-owned file list on a conversation.\n// Replaces inline chat attachments: a file is associated WITH a conversation but\n// never embedded in, sequenced with, or mutated through a chat message.\n// See ttt-prod docs/design/upload-and-media-pipeline.md.\n// ===========================================================================\n\n/** Backend-owned Conversation Files counters carried by EVERY conversation parent\n * (guildInviteConversations/{id} and pendingAdminDispatches/{id}). All four are\n * optional — an absent field reads as 0, so no pre-existing conversation needs a\n * backfill. Clients can never write them (rules + callable-only writes). */\nconst conversationFileCounterFields = {\n /** Published ACTIVE files in this conversation. */\n conversationFileCount: z.number().optional(),\n /** Published STORED-OUTPUT bytes in this conversation. */\n conversationFileBytesUsed: z.number().optional(),\n /** Active quota RESERVATIONS (uploads in flight). */\n conversationFileUploadCount: z.number().optional(),\n /** Bytes reserved by active uploads. */\n conversationFileBytesReserved: z.number().optional(),\n} as const;\n\n/**\n * `{conversationParent}/conversationFiles/{conversationFileId}` — the conversation's\n * lightweight ownership/list projection, analogous to `WorkFileSchema`.\n *\n * DELIBERATELY ABSENT (owned elsewhere, never duplicated here):\n * - no `status` / `ready` / `publicationState` / processing progress — `pendingMedia`\n * and `mediaAssets` own the lifecycle; the mere EXISTENCE of this doc means the\n * asset is published;\n * - no Firebase / R2 / gateway / signed / download URL — display URLs are built at\n * render time through the protected media gateway;\n * - no Firebase staging `storagePath`;\n * - no username / display name / avatar / Work name snapshot (ARCH-103) — resolve\n * `uploadedByUid` at render time.\n *\n * `conversationFileId` is deterministic: it IS the `pendingMediaId`. The publication\n * adapter creates this doc with `transaction.create` (never `set`), so an id\n * collision cannot overwrite and double-count an existing file.\n */\nexport const ConversationFileSchema = z.object({\n conversationFileId: z.string(),\n mediaAssetId: z.string(),\n name: z.string(),\n // The canonical stored content media kind — never a re-declared union (ARCH-102).\n mediaKind: ContentMediaKindSchema,\n contentType: z.string(),\n /** Sum of the PUBLISHED variant bytes (stored output, not raw upload bytes). */\n sizeBytes: z.number(),\n uploadedByUid: z.string(),\n createdAt: z.number(),\n});\nexport type ConversationFile = z.infer<typeof ConversationFileSchema>;\n\nexport const GuildChatChannelSchema = z.object({\n guildChatChannelId: z.string(),\n workProjectId: z.string(),\n channelName: z.string(),\n description: z.string().optional(),\n requiredGuildStandings: z.array(z.string()),\n allowedUserIds: z.array(z.string()),\n createdAt: z.number(),\n // NOTE: createdBy here is a flat uid string (unlike the `{ uid }` object form on\n // most other docs). Preserved as-is; flagged in the schema-registry recon.\n createdBy: z.string(),\n lastMessageAt: z.string().optional(),\n lastMessage: z.string().optional(),\n messageCount: z.number(),\n isArchived: z.boolean(),\n // Delete/tombstone marker (ttt-prod docs/design/chat-realtime-system.md \"Channel lifecycle semantics\").\n // Independent of `isArchived`: archive = visible-under-a-toggle, delete = gone from every user's\n // UI with grants revoked and sends rejected DO-side; storage is RETAINED (never a physical purge).\n // Optional, absent ⇒ false, so existing/seeded channels need no backfill. Backend-only-writable.\n isDeleted: z.boolean().optional(),\n // Monotonic config version for the chat-realtime sync layer (Contract B). Bumped by\n // each channel create / edit / archive / delete in the authoritative txn; the\n // `config` chatSyncEvents + channel-scoped fanout key on it. Absent ⇒ 0.\n configVersion: z.number().optional(),\n});\nexport type GuildChatChannel = z.infer<typeof GuildChatChannelSchema>;\n\n// The guildInviteConversations/{guildInviteId} doc as actually written by inviteUserToGuild\n// (and mutated by the accept/decline/finalize/cancel callables). This is the single source of\n// truth — the former duplicate `GuildInviteSchema` in ./work-project.ts was a stale, narrower\n// shape that the callable bypassed with an `as unknown as` cast. The inviter is `createdBy`\n// (== `sender`); there is no separate `workSteward`/`workProjectTitle` field (the title lives in\n// `workProject.workingTitle`).\nexport const GuildInviteConversationSchema = z.object({\n guildInviteId: z.string(),\n workProjectId: z.string(),\n relatedUserIds: z.array(z.string()),\n // Title/description are NOT snapshotted (Display Identity Invariant — resolve at render\n // from publicWorkProjects by id). `type` (Tales/Tunes/Television) is an immutable\n // classification, not display text, and IS consumed by an invites-list type filter\n // (guild-invites-section.tsx), so it stays. Triggers read only `workProjectId`.\n workProject: z.object({\n workProjectId: z.string(),\n type: z.string(),\n }),\n createdBy: userRefSchema,\n sender: userRefSchema,\n recipient: userRefSchema,\n stakeSharesOffered: z.number(),\n source: InviteSourceSchema,\n // State machine: pending → accepted (transient, trigger-consumed) → finalized (terminal\n // success), or pending → declined / cancelled (terminal failure). No 'error' state is ever\n // written (dead state removed 2026-07-03). See ttt-prod\n // docs/design/work-guild-membership-and-stake.md for the invite lifecycle.\n status: guildInviteConversationStatusSchema,\n createdAt: z.number(),\n updatedAt: z.number(),\n lastUpdatedAt: z.number(),\n finalizedAt: z.number().optional(),\n senderConfirmed: z.boolean(),\n recipientConfirmed: z.boolean(),\n lastMessage: z.string().optional(),\n lastMessageAt: z.string().optional(),\n // Conversation Files quota counters for THIS invite conversation (per-conversation\n // caps: MAX_CONVERSATION_FILES / MAX_CONVERSATION_FILE_STORAGE_BYTES). Reserved at\n // startUpload, transferred to used at publication, released on every terminal path,\n // decremented on delete. Backend-only-writable; absent ⇒ 0.\n ...conversationFileCounterFields,\n});\nexport type GuildInviteConversation = z.infer<typeof GuildInviteConversationSchema>;\n\n// Typed context ref carried by a dispatch thread born from a review send-back, a\n// moderation placeholder, or a published change request — lets the admin queue card\n// say what the thread is about without parsing the subject line. Extensible union;\n// server-validated against the thread's party (never trusted raw from the client).\nexport const AdminDispatchContextRefSchema = z.discriminatedUnion('kind', [\n z.object({ kind: z.literal('thresholdItem'), thresholdItemId: z.string() }),\n z.object({\n kind: z.literal('hallContent'),\n hallItemId: z.string(),\n // null ⇒ the hall parent DETAIL; set ⇒ a chapter/track/episode sub-item.\n subItemId: z.string().nullable(),\n }),\n z.object({ kind: z.literal('hallContentChangeRequest'), changeRequestId: z.string() }),\n]);\nexport type AdminDispatchContextRef = z.infer<typeof AdminDispatchContextRefSchema>;\n\nexport const AdminDispatchPartyKindSchema = z.enum(['user', 'workProject']);\nexport type AdminDispatchPartyKind = z.infer<typeof AdminDispatchPartyKindSchema>;\n\nexport const AdminDispatchSchema = z.object({\n adminDispatchId: z.string(),\n // Party-generic dispatch: the platform corresponds with a PARTY — a user or a\n // workProject. REQUIRED and written explicitly at creation: the personal-threads\n // client query filters `partyKind == 'user'` and the Firestore read rules branch\n // on it party-aware, so an absent field is unqueryable and unauthorizable. For\n // 'user' threads `userId` is the thread owner (the sole member-side authority).\n // For 'workProject' threads `workProjectId` is set, read/reply authority is\n // ACTIVE GUILDMATE standing evaluated server-side (never a stored member list),\n // and `userId` holds the INITIATING member's uid for attribution only — it is\n // never a work-thread authority.\n partyKind: AdminDispatchPartyKindSchema,\n workProjectId: z.string().optional(),\n contextRef: AdminDispatchContextRefSchema.optional(),\n userId: z.string(),\n initiatorUserId: z.string(),\n initiatedBy: z.enum(['user', 'admin']),\n subject: z.string(),\n status: z.enum(['open', 'user_reply', 'admin_reply', 'closed_resolved', 'closed_unresolved']),\n createdAt: z.number(),\n lastUpdatedAt: z.number(),\n readByAdmin: z.boolean(),\n readByUser: z.boolean(),\n closedBy: z.string().optional(),\n // Conversation Files quota counters for THIS admin-support thread — same\n // per-conversation caps and same backend-only ownership as the invite conversation.\n ...conversationFileCounterFields,\n});\nexport type AdminDispatch = z.infer<typeof AdminDispatchSchema>;\n\n// Per-message body for admin-support `conversationMessages` — the ONE chat surface still\n// transported through Firestore (guild channel + invite messages are realtime-only, served\n// by the chat Worker Durable Object). Written by runSendGuildChatMessage (senderId/text/\n// createdAt + optional replyTo); the admin-dispatch INITIAL message\n// (runStartAdminSupportThread) additionally stores `messageId`. This is the STORED shape — a relaxed\n// @ttt-productions/chat-core ChatMessageV1: `messageId` is the doc id (only sometimes persisted) and\n// `threadId` is not stored. Reuses the chat-schemas ReplyTo Zod shape.\n// A chat message carries NO file reference — files live in the conversation's\n// `conversationFiles` subcollection (ConversationFileSchema above).\nexport const ChatMessageV1Schema = z.object({\n senderId: z.string(),\n text: z.string(),\n createdAt: z.number(),\n messageId: z.string().optional(),\n threadId: z.string().optional(),\n type: z.string().optional(),\n // Rides on the realtime invite-thread envelope (chat Worker invite Durable Object);\n // guild-invite messages are realtime-only — there is no Firestore inviteMessages\n // subcollection. Optional so the same body schema still fits the guildChat /\n // admin-dispatch messages that don't carry it.\n guildInviteId: z.string().optional(),\n replyTo: ReplyToSchema.optional(),\n isSystemMessage: z.boolean().optional(),\n meta: z.record(z.string(), z.unknown()).optional(),\n // Moderation tombstone flag — the single-doc `hidden` flip written by the admin\n // moderateReportedContent pipeline (the admin-work-message itemType). Backend-only\n // writable (conversationMessages is callable-only in rules); absent ⇒ visible.\n // Thread views render a hidden message as a moderation tombstone.\n hidden: z.boolean().optional(),\n});\nexport type ChatMessageV1Doc = z.infer<typeof ChatMessageV1Schema>;\n"]}
|
|
@@ -1712,10 +1712,10 @@ export declare const COLLECTION_SCHEMAS: {
|
|
|
1712
1712
|
conversationFileUploadCount: z.ZodOptional<z.ZodNumber>;
|
|
1713
1713
|
conversationFileBytesReserved: z.ZodOptional<z.ZodNumber>;
|
|
1714
1714
|
adminDispatchId: z.ZodString;
|
|
1715
|
-
partyKind: z.
|
|
1715
|
+
partyKind: z.ZodEnum<{
|
|
1716
1716
|
workProject: "workProject";
|
|
1717
1717
|
user: "user";
|
|
1718
|
-
}
|
|
1718
|
+
}>;
|
|
1719
1719
|
workProjectId: z.ZodOptional<z.ZodString>;
|
|
1720
1720
|
contextRef: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1721
1721
|
kind: z.ZodLiteral<"thresholdItem">;
|
|
@@ -3530,6 +3530,7 @@ export declare const COLLECTION_SCHEMAS: {
|
|
|
3530
3530
|
width: z.ZodOptional<z.ZodNumber>;
|
|
3531
3531
|
height: z.ZodOptional<z.ZodNumber>;
|
|
3532
3532
|
durationSec: z.ZodOptional<z.ZodNumber>;
|
|
3533
|
+
downloadFilename: z.ZodOptional<z.ZodString>;
|
|
3533
3534
|
}, z.core.$strict>>;
|
|
3534
3535
|
moderationStatus: z.ZodEnum<{
|
|
3535
3536
|
approved: "approved";
|
|
@@ -3729,6 +3730,7 @@ export declare const COLLECTION_SCHEMAS: {
|
|
|
3729
3730
|
width: z.ZodOptional<z.ZodNumber>;
|
|
3730
3731
|
height: z.ZodOptional<z.ZodNumber>;
|
|
3731
3732
|
durationSec: z.ZodOptional<z.ZodNumber>;
|
|
3733
|
+
downloadFilename: z.ZodOptional<z.ZodString>;
|
|
3732
3734
|
}, z.core.$strict>>;
|
|
3733
3735
|
updatedAtMs: z.ZodNumber;
|
|
3734
3736
|
}, z.core.$strict>;
|
|
@@ -4052,6 +4054,7 @@ export declare const COLLECTION_SCHEMAS: {
|
|
|
4052
4054
|
deadLetter: "deadLetter";
|
|
4053
4055
|
queued: "queued";
|
|
4054
4056
|
delivering: "delivering";
|
|
4057
|
+
safetyRepair: "safetyRepair";
|
|
4055
4058
|
}>;
|
|
4056
4059
|
attemptCount: z.ZodNumber;
|
|
4057
4060
|
nextAttemptAt: z.ZodNumber;
|
|
@@ -4065,6 +4068,9 @@ export declare const COLLECTION_SCHEMAS: {
|
|
|
4065
4068
|
deadLetteredAt: z.ZodNullable<z.ZodNumber>;
|
|
4066
4069
|
reconciled: z.ZodBoolean;
|
|
4067
4070
|
reconciledAt: z.ZodNullable<z.ZodNumber>;
|
|
4071
|
+
safetyRepair: z.ZodBoolean;
|
|
4072
|
+
safetyRepairGeneration: z.ZodNumber;
|
|
4073
|
+
reconcileAttemptCount: z.ZodNumber;
|
|
4068
4074
|
expireAt: z.ZodOptional<z.ZodUnknown>;
|
|
4069
4075
|
}, z.core.$strip>;
|
|
4070
4076
|
readonly 'chatHistoryAnonymizationJobs/{jobId}': z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/registry.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiM7B,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/doc-schemas/registry.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiM7B,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6NkB,CAAC;AAElD,MAAM,MAAM,wBAAwB,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAEvE;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;CAmBiC,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAOvC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical file extension (no leading dot) for a served contentType, or
|
|
3
|
+
* `null` when the type is unknown/absent. Exported because the same mapping is
|
|
4
|
+
* the honest answer to "what extension may this variant's bytes carry".
|
|
5
|
+
*/
|
|
6
|
+
export declare function extensionForContentType(contentType: string | null | undefined): string | null;
|
|
7
|
+
export interface NormalizeDownloadFilenameOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The contentType of the variant whose bytes are actually served. When it maps
|
|
10
|
+
* to a known extension, the returned name carries that extension (or an accepted
|
|
11
|
+
* alias the input already had) — never an incompatible original extension.
|
|
12
|
+
*/
|
|
13
|
+
contentType?: string | null;
|
|
14
|
+
/** UTF-8 byte cap. Defaults to `MAX_DOWNLOAD_FILENAME_BYTES`. */
|
|
15
|
+
maxBytes?: number;
|
|
16
|
+
/** Neutral stem used when sanitization leaves nothing. Defaults to `DOWNLOAD_FILENAME_FALLBACK_STEM`. */
|
|
17
|
+
fallbackStem?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The canonical download-filename normalizer. Deterministic, idempotent, and
|
|
21
|
+
* ALWAYS returns a nonempty name.
|
|
22
|
+
*
|
|
23
|
+
* In order: basename only → Unicode NFC → strip control/CR-LF/bidi/path-separator/
|
|
24
|
+
* quote/unsafe-platform characters → collapse whitespace → trim trailing spaces and
|
|
25
|
+
* dots → resolve the extension against the served contentType → replace an empty,
|
|
26
|
+
* dots-only, or Windows-reserved stem → cap by UTF-8 BYTE length on a code-point
|
|
27
|
+
* boundary, keeping the extension.
|
|
28
|
+
*
|
|
29
|
+
* Unicode letters and emoji SURVIVE — they are legal filename content and the
|
|
30
|
+
* RFC 8187 `filename*` form carries them faithfully. The ASCII-only degradation
|
|
31
|
+
* happens later, in `buildContentDispositionFilenameForms`, and only for the
|
|
32
|
+
* RFC 6266 `filename=` fallback.
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalizeDownloadFilename(rawName: string | null | undefined, options?: NormalizeDownloadFilenameOptions): string;
|
|
35
|
+
export interface ContentDispositionFilenameForms {
|
|
36
|
+
/**
|
|
37
|
+
* RFC 6266 `filename=` value: safe printable ASCII only, already free of quotes,
|
|
38
|
+
* backslashes, and header-structural characters, so the Worker may place it inside
|
|
39
|
+
* `filename="…"` verbatim with no further escaping.
|
|
40
|
+
*/
|
|
41
|
+
asciiFallback: string;
|
|
42
|
+
/**
|
|
43
|
+
* RFC 8187 ext-value INCLUDING its charset prefix — e.g. `UTF-8''caf%C3%A9.jpg`.
|
|
44
|
+
* Goes straight after `filename*=` UNQUOTED (an ext-value is never quoted).
|
|
45
|
+
*/
|
|
46
|
+
extendedValue: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build both `Content-Disposition` filename forms from a normalized name.
|
|
50
|
+
*
|
|
51
|
+
* The Worker assembles the header itself (emitting `filename` first, `filename*`
|
|
52
|
+
* second per RFC 6266), but the ESCAPING is owned here so the app, the Worker, and
|
|
53
|
+
* the tests can never disagree about it. Defensively re-normalizes, so passing a
|
|
54
|
+
* raw record value can still not produce a CR/LF or quote in a header.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildContentDispositionFilenameForms(normalizedFilename: string, options?: NormalizeDownloadFilenameOptions): ContentDispositionFilenameForms;
|
|
57
|
+
//# sourceMappingURL=download-filename.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-filename.d.ts","sourceRoot":"","sources":["../../src/media/download-filename.ts"],"names":[],"mappings":"AAqFA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAG7F;AA+FD,MAAM,WAAW,gCAAgC;IAC/C;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yGAAyG;IACzG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,OAAO,GAAE,gCAAqC,GAC7C,MAAM,CA2CR;AAqCD,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,oCAAoC,CAClD,kBAAkB,EAAE,MAAM,EAC1B,OAAO,GAAE,gCAAqC,GAC7C,+BAA+B,CAUjC"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// DOWNLOAD FILENAME — the ONE canonical normalizer for the server-owned filename
|
|
3
|
+
// the media gateway puts in `Content-Disposition` for a `?download=1` request,
|
|
4
|
+
// plus the pure RFC 6266 / RFC 8187 string forms that header is assembled from.
|
|
5
|
+
//
|
|
6
|
+
// Ownership (ENG-002 / ARCH-101): the escaping and sanitization logic lives HERE,
|
|
7
|
+
// once. Backend publishers (Work Files, Conversation Files, downloadable Hall
|
|
8
|
+
// media) run `normalizeDownloadFilename` when they write a variant's
|
|
9
|
+
// `downloadFilename`; the media Worker defensively runs it AGAIN before building
|
|
10
|
+
// the header (a record is backend-authored, but the header is the trust boundary),
|
|
11
|
+
// then assembles the header from `buildContentDispositionFilenameForms`. Header
|
|
12
|
+
// ASSEMBLY stays worker-side — this module only produces the two string forms.
|
|
13
|
+
//
|
|
14
|
+
// Everything here is PURE: no env, no I/O, no clock, no crypto. It runs unchanged
|
|
15
|
+
// in Cloud Functions (Node 22) and in the Cloudflare Worker.
|
|
16
|
+
//
|
|
17
|
+
// Filenames are USER CONTENT. Never log them — not the raw input, not the output.
|
|
18
|
+
//
|
|
19
|
+
// See ttt-prod docs/design/media-assets-and-protected-serving.md and
|
|
20
|
+
// docs/code_changes_needed/CODE_CHANGE_invite_finalization_conversation_files_and_downloads.md
|
|
21
|
+
// (§3 "Gateway downloads").
|
|
22
|
+
// ============================================================================
|
|
23
|
+
import { DOWNLOAD_FILENAME_FALLBACK_STEM, MAX_DOWNLOAD_FILENAME_BYTES, } from '../constants/media-download.js';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// contentType → extension
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Extensions per media type. The FIRST entry is canonical (what a corrected name
|
|
29
|
+
* gets); the rest are accepted aliases that are left alone when the input already
|
|
30
|
+
* carries one (so `photo.jpeg` stays `.jpeg` for an `image/jpeg` variant instead
|
|
31
|
+
* of being churned to `.jpg`).
|
|
32
|
+
*
|
|
33
|
+
* The served variant's contentType is authoritative: a transcoded JPEG/WebP/MP4
|
|
34
|
+
* is never labelled with the original upload's incompatible extension.
|
|
35
|
+
*/
|
|
36
|
+
const CONTENT_TYPE_EXTENSIONS = {
|
|
37
|
+
'image/jpeg': ['jpg', 'jpeg'],
|
|
38
|
+
'image/png': ['png'],
|
|
39
|
+
'image/webp': ['webp'],
|
|
40
|
+
'image/gif': ['gif'],
|
|
41
|
+
'image/avif': ['avif'],
|
|
42
|
+
'image/heic': ['heic'],
|
|
43
|
+
'image/heif': ['heif'],
|
|
44
|
+
'image/bmp': ['bmp'],
|
|
45
|
+
'image/tiff': ['tiff', 'tif'],
|
|
46
|
+
'image/svg+xml': ['svg'],
|
|
47
|
+
'video/mp4': ['mp4', 'm4v'],
|
|
48
|
+
'video/webm': ['webm'],
|
|
49
|
+
'video/quicktime': ['mov'],
|
|
50
|
+
'video/x-matroska': ['mkv'],
|
|
51
|
+
'video/ogg': ['ogv'],
|
|
52
|
+
'audio/mpeg': ['mp3'],
|
|
53
|
+
'audio/mp4': ['m4a'],
|
|
54
|
+
'audio/aac': ['aac'],
|
|
55
|
+
'audio/flac': ['flac'],
|
|
56
|
+
'audio/x-flac': ['flac'],
|
|
57
|
+
'audio/wav': ['wav'],
|
|
58
|
+
'audio/x-wav': ['wav'],
|
|
59
|
+
'audio/webm': ['weba'],
|
|
60
|
+
'audio/ogg': ['ogg', 'oga'],
|
|
61
|
+
'application/pdf': ['pdf'],
|
|
62
|
+
};
|
|
63
|
+
/** Non-canonical spellings seen in the wild, mapped onto the canonical essence. */
|
|
64
|
+
const CONTENT_TYPE_ALIASES = {
|
|
65
|
+
'image/jpg': 'image/jpeg',
|
|
66
|
+
'image/pjpeg': 'image/jpeg',
|
|
67
|
+
'audio/mp3': 'audio/mpeg',
|
|
68
|
+
'audio/x-m4a': 'audio/mp4',
|
|
69
|
+
'video/x-m4v': 'video/mp4',
|
|
70
|
+
};
|
|
71
|
+
/** `image/JPEG; charset=binary` → `image/jpeg`. Empty string when unusable. */
|
|
72
|
+
function contentTypeEssence(contentType) {
|
|
73
|
+
if (typeof contentType !== 'string')
|
|
74
|
+
return '';
|
|
75
|
+
const essence = contentType.split(';')[0].trim().toLowerCase();
|
|
76
|
+
return CONTENT_TYPE_ALIASES[essence] ?? essence;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The canonical file extension (no leading dot) for a served contentType, or
|
|
80
|
+
* `null` when the type is unknown/absent. Exported because the same mapping is
|
|
81
|
+
* the honest answer to "what extension may this variant's bytes carry".
|
|
82
|
+
*/
|
|
83
|
+
export function extensionForContentType(contentType) {
|
|
84
|
+
const known = CONTENT_TYPE_EXTENSIONS[contentTypeEssence(contentType)];
|
|
85
|
+
return known ? known[0] : null;
|
|
86
|
+
}
|
|
87
|
+
/** Every extension accepted as already-correct for a contentType (canonical first). */
|
|
88
|
+
function acceptedExtensions(contentType) {
|
|
89
|
+
return CONTENT_TYPE_EXTENSIONS[contentTypeEssence(contentType)] ?? [];
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// sanitization primitives
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/** C0 + DEL + C1 controls (CR/LF included — header-injection primitives). */
|
|
95
|
+
// eslint-disable-next-line no-control-regex -- matching control characters IS the point here: this class is exactly what must be stripped (CR/LF header injection).
|
|
96
|
+
const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
97
|
+
/** Bidi overrides/isolates — invisible reordering used to disguise an extension. */
|
|
98
|
+
const BIDI_CONTROLS = /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g;
|
|
99
|
+
/** Path separators + the Windows-reserved set + every quote form. */
|
|
100
|
+
const UNSAFE_FILENAME_CHARS = /[/\\<>:"|?*'\u2018\u2019\u201C\u201D\u0060]/g;
|
|
101
|
+
/** Windows reserved device names — unusable as a filename stem on that platform. */
|
|
102
|
+
const RESERVED_DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
103
|
+
/** A plausible extension: short and alphanumeric. Anything else is part of the stem. */
|
|
104
|
+
const EXTENSION_SHAPE = /^[A-Za-z0-9]{1,10}$/;
|
|
105
|
+
const utf8 = new TextEncoder();
|
|
106
|
+
/** UTF-8 byte length of a string. */
|
|
107
|
+
function utf8ByteLength(value) {
|
|
108
|
+
return utf8.encode(value).length;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Truncate to at most `maxBytes` UTF-8 bytes, never splitting a code point.
|
|
112
|
+
* Iterating with `for…of` walks CODE POINTS (surrogate pairs stay intact), so an
|
|
113
|
+
* emoji is either wholly kept or wholly dropped — never cut into lone surrogates.
|
|
114
|
+
*/
|
|
115
|
+
function truncateToUtf8Bytes(value, maxBytes) {
|
|
116
|
+
if (maxBytes <= 0)
|
|
117
|
+
return '';
|
|
118
|
+
if (utf8ByteLength(value) <= maxBytes)
|
|
119
|
+
return value;
|
|
120
|
+
let out = '';
|
|
121
|
+
let used = 0;
|
|
122
|
+
for (const codePoint of value) {
|
|
123
|
+
const size = utf8ByteLength(codePoint);
|
|
124
|
+
if (used + size > maxBytes)
|
|
125
|
+
break;
|
|
126
|
+
out += codePoint;
|
|
127
|
+
used += size;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** Strip the leading path, whatever separator style produced it. */
|
|
132
|
+
function takeBasename(value) {
|
|
133
|
+
const segments = value.split(/[/\\]/);
|
|
134
|
+
return segments[segments.length - 1] ?? '';
|
|
135
|
+
}
|
|
136
|
+
/** Remove everything hostile, collapse whitespace, trim edges + trailing dots. */
|
|
137
|
+
function sanitizeSegment(value) {
|
|
138
|
+
return value
|
|
139
|
+
.replace(CONTROL_CHARS, '')
|
|
140
|
+
.replace(BIDI_CONTROLS, '')
|
|
141
|
+
.replace(UNSAFE_FILENAME_CHARS, '')
|
|
142
|
+
.replace(/\s+/g, ' ')
|
|
143
|
+
.trim();
|
|
144
|
+
}
|
|
145
|
+
/** Trailing spaces and dots are silently dropped by Windows — drop them ourselves. */
|
|
146
|
+
function trimTrailingDotsAndSpaces(value) {
|
|
147
|
+
return value.replace(/[\s.]+$/, '');
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Split on the last dot. A leading dot is NOT an extension separator (`.gitignore`
|
|
151
|
+
* is a stem), and a long or non-alphanumeric tail is treated as part of the stem
|
|
152
|
+
* (`Version 2.5 notes` keeps its `.5 notes`).
|
|
153
|
+
*/
|
|
154
|
+
function splitExtension(value) {
|
|
155
|
+
const dot = value.lastIndexOf('.');
|
|
156
|
+
if (dot <= 0 || dot === value.length - 1)
|
|
157
|
+
return { stem: value, ext: '' };
|
|
158
|
+
const ext = value.slice(dot + 1);
|
|
159
|
+
if (!EXTENSION_SHAPE.test(ext))
|
|
160
|
+
return { stem: value, ext: '' };
|
|
161
|
+
return { stem: value.slice(0, dot), ext };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The canonical download-filename normalizer. Deterministic, idempotent, and
|
|
165
|
+
* ALWAYS returns a nonempty name.
|
|
166
|
+
*
|
|
167
|
+
* In order: basename only → Unicode NFC → strip control/CR-LF/bidi/path-separator/
|
|
168
|
+
* quote/unsafe-platform characters → collapse whitespace → trim trailing spaces and
|
|
169
|
+
* dots → resolve the extension against the served contentType → replace an empty,
|
|
170
|
+
* dots-only, or Windows-reserved stem → cap by UTF-8 BYTE length on a code-point
|
|
171
|
+
* boundary, keeping the extension.
|
|
172
|
+
*
|
|
173
|
+
* Unicode letters and emoji SURVIVE — they are legal filename content and the
|
|
174
|
+
* RFC 8187 `filename*` form carries them faithfully. The ASCII-only degradation
|
|
175
|
+
* happens later, in `buildContentDispositionFilenameForms`, and only for the
|
|
176
|
+
* RFC 6266 `filename=` fallback.
|
|
177
|
+
*/
|
|
178
|
+
export function normalizeDownloadFilename(rawName, options = {}) {
|
|
179
|
+
const maxBytes = options.maxBytes ?? MAX_DOWNLOAD_FILENAME_BYTES;
|
|
180
|
+
const fallbackStem = trimTrailingDotsAndSpaces(sanitizeSegment(options.fallbackStem ?? '')) ||
|
|
181
|
+
DOWNLOAD_FILENAME_FALLBACK_STEM;
|
|
182
|
+
const source = typeof rawName === 'string' ? rawName : '';
|
|
183
|
+
const sanitized = trimTrailingDotsAndSpaces(sanitizeSegment(takeBasename(source.normalize('NFC'))));
|
|
184
|
+
const split = splitExtension(sanitized);
|
|
185
|
+
// --- extension: the served contentType is authoritative ---
|
|
186
|
+
const canonicalExt = extensionForContentType(options.contentType);
|
|
187
|
+
const inputExt = split.ext.toLowerCase();
|
|
188
|
+
let ext;
|
|
189
|
+
if (canonicalExt) {
|
|
190
|
+
ext = acceptedExtensions(options.contentType).includes(inputExt) ? inputExt : canonicalExt;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
ext = inputExt;
|
|
194
|
+
}
|
|
195
|
+
const suffix = ext ? `.${ext}` : '';
|
|
196
|
+
// --- stem: nonempty, not a reserved device name ---
|
|
197
|
+
let stem = trimTrailingDotsAndSpaces(split.stem);
|
|
198
|
+
if (stem.length === 0)
|
|
199
|
+
stem = fallbackStem;
|
|
200
|
+
// --- byte cap (extension preserved; code points never split) ---
|
|
201
|
+
const budget = maxBytes - utf8ByteLength(suffix);
|
|
202
|
+
if (budget <= 0) {
|
|
203
|
+
// Pathological: the extension alone exceeds the cap. Drop it rather than
|
|
204
|
+
// emit an over-length header value, and keep a usable stem.
|
|
205
|
+
return (truncateToUtf8Bytes(stem, maxBytes) ||
|
|
206
|
+
truncateToUtf8Bytes(fallbackStem, maxBytes) ||
|
|
207
|
+
fallbackStem);
|
|
208
|
+
}
|
|
209
|
+
if (RESERVED_DEVICE_NAME.test(stem))
|
|
210
|
+
stem = `_${stem}`;
|
|
211
|
+
stem = trimTrailingDotsAndSpaces(truncateToUtf8Bytes(stem, budget));
|
|
212
|
+
if (stem.length === 0)
|
|
213
|
+
stem = trimTrailingDotsAndSpaces(truncateToUtf8Bytes(fallbackStem, budget));
|
|
214
|
+
if (stem.length === 0)
|
|
215
|
+
return truncateToUtf8Bytes(fallbackStem, maxBytes) || fallbackStem;
|
|
216
|
+
return `${stem}${suffix}`;
|
|
217
|
+
}
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// RFC 6266 / RFC 8187 string forms
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
/** Combining marks — removed when degrading to ASCII so `café` reads `cafe`. */
|
|
222
|
+
const COMBINING_MARKS = /\p{M}+/gu;
|
|
223
|
+
/** Printable ASCII that is safe inside an RFC 6266 quoted-string. */
|
|
224
|
+
const SAFE_ASCII = /[\u0020-\u007E]/;
|
|
225
|
+
/** Header-structural characters kept out of the fallback even though they are ASCII. */
|
|
226
|
+
const ASCII_HEADER_HOSTILE = new Set(['"', '\\', ';', ',']);
|
|
227
|
+
/** RFC 8187 attr-char: the only bytes that may appear unencoded in an ext-value. */
|
|
228
|
+
const ATTR_CHAR = /[A-Za-z0-9!#$&+\-.^_`|~]/;
|
|
229
|
+
/** Best-effort ASCII degradation of one name component. */
|
|
230
|
+
function toAsciiComponent(value) {
|
|
231
|
+
const stripped = value.normalize('NFD').replace(COMBINING_MARKS, '');
|
|
232
|
+
let out = '';
|
|
233
|
+
for (const ch of stripped) {
|
|
234
|
+
out += ch.length === 1 && SAFE_ASCII.test(ch) && !ASCII_HEADER_HOSTILE.has(ch) ? ch : '_';
|
|
235
|
+
}
|
|
236
|
+
return out
|
|
237
|
+
.replace(/_+/g, '_')
|
|
238
|
+
.replace(/^[_\s]+|[_\s.]+$/g, '');
|
|
239
|
+
}
|
|
240
|
+
/** Percent-encode every non-`attr-char` byte of the UTF-8 encoding (RFC 8187 §3.2). */
|
|
241
|
+
function encodeExtValue(value) {
|
|
242
|
+
let out = '';
|
|
243
|
+
for (const byte of utf8.encode(value)) {
|
|
244
|
+
const ch = String.fromCharCode(byte);
|
|
245
|
+
out += byte < 0x80 && ATTR_CHAR.test(ch) ? ch : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Build both `Content-Disposition` filename forms from a normalized name.
|
|
251
|
+
*
|
|
252
|
+
* The Worker assembles the header itself (emitting `filename` first, `filename*`
|
|
253
|
+
* second per RFC 6266), but the ESCAPING is owned here so the app, the Worker, and
|
|
254
|
+
* the tests can never disagree about it. Defensively re-normalizes, so passing a
|
|
255
|
+
* raw record value can still not produce a CR/LF or quote in a header.
|
|
256
|
+
*/
|
|
257
|
+
export function buildContentDispositionFilenameForms(normalizedFilename, options = {}) {
|
|
258
|
+
const safeName = normalizeDownloadFilename(normalizedFilename, options);
|
|
259
|
+
const { stem, ext } = splitExtension(safeName);
|
|
260
|
+
const asciiStem = toAsciiComponent(stem) || DOWNLOAD_FILENAME_FALLBACK_STEM;
|
|
261
|
+
const asciiExt = ext.replace(/[^A-Za-z0-9]/g, '').toLowerCase();
|
|
262
|
+
return {
|
|
263
|
+
asciiFallback: asciiExt ? `${asciiStem}.${asciiExt}` : asciiStem,
|
|
264
|
+
extendedValue: `UTF-8''${encodeExtValue(safeName)}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=download-filename.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-filename.js","sourceRoot":"","sources":["../../src/media/download-filename.ts"],"names":[],"mappings":"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","sourcesContent":["// ============================================================================\n// DOWNLOAD FILENAME — the ONE canonical normalizer for the server-owned filename\n// the media gateway puts in `Content-Disposition` for a `?download=1` request,\n// plus the pure RFC 6266 / RFC 8187 string forms that header is assembled from.\n//\n// Ownership (ENG-002 / ARCH-101): the escaping and sanitization logic lives HERE,\n// once. Backend publishers (Work Files, Conversation Files, downloadable Hall\n// media) run `normalizeDownloadFilename` when they write a variant's\n// `downloadFilename`; the media Worker defensively runs it AGAIN before building\n// the header (a record is backend-authored, but the header is the trust boundary),\n// then assembles the header from `buildContentDispositionFilenameForms`. Header\n// ASSEMBLY stays worker-side — this module only produces the two string forms.\n//\n// Everything here is PURE: no env, no I/O, no clock, no crypto. It runs unchanged\n// in Cloud Functions (Node 22) and in the Cloudflare Worker.\n//\n// Filenames are USER CONTENT. Never log them — not the raw input, not the output.\n//\n// See ttt-prod docs/design/media-assets-and-protected-serving.md and\n// docs/code_changes_needed/CODE_CHANGE_invite_finalization_conversation_files_and_downloads.md\n// (§3 \"Gateway downloads\").\n// ============================================================================\n\nimport {\n DOWNLOAD_FILENAME_FALLBACK_STEM,\n MAX_DOWNLOAD_FILENAME_BYTES,\n} from '../constants/media-download.js';\n\n// ---------------------------------------------------------------------------\n// contentType → extension\n// ---------------------------------------------------------------------------\n\n/**\n * Extensions per media type. The FIRST entry is canonical (what a corrected name\n * gets); the rest are accepted aliases that are left alone when the input already\n * carries one (so `photo.jpeg` stays `.jpeg` for an `image/jpeg` variant instead\n * of being churned to `.jpg`).\n *\n * The served variant's contentType is authoritative: a transcoded JPEG/WebP/MP4\n * is never labelled with the original upload's incompatible extension.\n */\nconst CONTENT_TYPE_EXTENSIONS: Readonly<Record<string, readonly string[]>> = {\n 'image/jpeg': ['jpg', 'jpeg'],\n 'image/png': ['png'],\n 'image/webp': ['webp'],\n 'image/gif': ['gif'],\n 'image/avif': ['avif'],\n 'image/heic': ['heic'],\n 'image/heif': ['heif'],\n 'image/bmp': ['bmp'],\n 'image/tiff': ['tiff', 'tif'],\n 'image/svg+xml': ['svg'],\n 'video/mp4': ['mp4', 'm4v'],\n 'video/webm': ['webm'],\n 'video/quicktime': ['mov'],\n 'video/x-matroska': ['mkv'],\n 'video/ogg': ['ogv'],\n 'audio/mpeg': ['mp3'],\n 'audio/mp4': ['m4a'],\n 'audio/aac': ['aac'],\n 'audio/flac': ['flac'],\n 'audio/x-flac': ['flac'],\n 'audio/wav': ['wav'],\n 'audio/x-wav': ['wav'],\n 'audio/webm': ['weba'],\n 'audio/ogg': ['ogg', 'oga'],\n 'application/pdf': ['pdf'],\n};\n\n/** Non-canonical spellings seen in the wild, mapped onto the canonical essence. */\nconst CONTENT_TYPE_ALIASES: Readonly<Record<string, string>> = {\n 'image/jpg': 'image/jpeg',\n 'image/pjpeg': 'image/jpeg',\n 'audio/mp3': 'audio/mpeg',\n 'audio/x-m4a': 'audio/mp4',\n 'video/x-m4v': 'video/mp4',\n};\n\n/** `image/JPEG; charset=binary` → `image/jpeg`. Empty string when unusable. */\nfunction contentTypeEssence(contentType: string | null | undefined): string {\n if (typeof contentType !== 'string') return '';\n const essence = contentType.split(';')[0].trim().toLowerCase();\n return CONTENT_TYPE_ALIASES[essence] ?? essence;\n}\n\n/**\n * The canonical file extension (no leading dot) for a served contentType, or\n * `null` when the type is unknown/absent. Exported because the same mapping is\n * the honest answer to \"what extension may this variant's bytes carry\".\n */\nexport function extensionForContentType(contentType: string | null | undefined): string | null {\n const known = CONTENT_TYPE_EXTENSIONS[contentTypeEssence(contentType)];\n return known ? known[0] : null;\n}\n\n/** Every extension accepted as already-correct for a contentType (canonical first). */\nfunction acceptedExtensions(contentType: string | null | undefined): readonly string[] {\n return CONTENT_TYPE_EXTENSIONS[contentTypeEssence(contentType)] ?? [];\n}\n\n// ---------------------------------------------------------------------------\n// sanitization primitives\n// ---------------------------------------------------------------------------\n\n/** C0 + DEL + C1 controls (CR/LF included — header-injection primitives). */\n// eslint-disable-next-line no-control-regex -- matching control characters IS the point here: this class is exactly what must be stripped (CR/LF header injection).\nconst CONTROL_CHARS = /[\\u0000-\\u001F\\u007F-\\u009F]/g;\n/** Bidi overrides/isolates — invisible reordering used to disguise an extension. */\nconst BIDI_CONTROLS = /[\\u200E\\u200F\\u202A-\\u202E\\u2066-\\u2069]/g;\n/** Path separators + the Windows-reserved set + every quote form. */\nconst UNSAFE_FILENAME_CHARS = /[/\\\\<>:\"|?*'\\u2018\\u2019\\u201C\\u201D\\u0060]/g;\n\n/** Windows reserved device names — unusable as a filename stem on that platform. */\nconst RESERVED_DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;\n\n/** A plausible extension: short and alphanumeric. Anything else is part of the stem. */\nconst EXTENSION_SHAPE = /^[A-Za-z0-9]{1,10}$/;\n\nconst utf8 = new TextEncoder();\n\n/** UTF-8 byte length of a string. */\nfunction utf8ByteLength(value: string): number {\n return utf8.encode(value).length;\n}\n\n/**\n * Truncate to at most `maxBytes` UTF-8 bytes, never splitting a code point.\n * Iterating with `for…of` walks CODE POINTS (surrogate pairs stay intact), so an\n * emoji is either wholly kept or wholly dropped — never cut into lone surrogates.\n */\nfunction truncateToUtf8Bytes(value: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (utf8ByteLength(value) <= maxBytes) return value;\n let out = '';\n let used = 0;\n for (const codePoint of value) {\n const size = utf8ByteLength(codePoint);\n if (used + size > maxBytes) break;\n out += codePoint;\n used += size;\n }\n return out;\n}\n\n/** Strip the leading path, whatever separator style produced it. */\nfunction takeBasename(value: string): string {\n const segments = value.split(/[/\\\\]/);\n return segments[segments.length - 1] ?? '';\n}\n\n/** Remove everything hostile, collapse whitespace, trim edges + trailing dots. */\nfunction sanitizeSegment(value: string): string {\n return value\n .replace(CONTROL_CHARS, '')\n .replace(BIDI_CONTROLS, '')\n .replace(UNSAFE_FILENAME_CHARS, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n/** Trailing spaces and dots are silently dropped by Windows — drop them ourselves. */\nfunction trimTrailingDotsAndSpaces(value: string): string {\n return value.replace(/[\\s.]+$/, '');\n}\n\ninterface SplitName {\n stem: string;\n /** Extension WITHOUT its dot; '' when the name carries none. */\n ext: string;\n}\n\n/**\n * Split on the last dot. A leading dot is NOT an extension separator (`.gitignore`\n * is a stem), and a long or non-alphanumeric tail is treated as part of the stem\n * (`Version 2.5 notes` keeps its `.5 notes`).\n */\nfunction splitExtension(value: string): SplitName {\n const dot = value.lastIndexOf('.');\n if (dot <= 0 || dot === value.length - 1) return { stem: value, ext: '' };\n const ext = value.slice(dot + 1);\n if (!EXTENSION_SHAPE.test(ext)) return { stem: value, ext: '' };\n return { stem: value.slice(0, dot), ext };\n}\n\n// ---------------------------------------------------------------------------\n// the normalizer\n// ---------------------------------------------------------------------------\n\nexport interface NormalizeDownloadFilenameOptions {\n /**\n * The contentType of the variant whose bytes are actually served. When it maps\n * to a known extension, the returned name carries that extension (or an accepted\n * alias the input already had) — never an incompatible original extension.\n */\n contentType?: string | null;\n /** UTF-8 byte cap. Defaults to `MAX_DOWNLOAD_FILENAME_BYTES`. */\n maxBytes?: number;\n /** Neutral stem used when sanitization leaves nothing. Defaults to `DOWNLOAD_FILENAME_FALLBACK_STEM`. */\n fallbackStem?: string;\n}\n\n/**\n * The canonical download-filename normalizer. Deterministic, idempotent, and\n * ALWAYS returns a nonempty name.\n *\n * In order: basename only → Unicode NFC → strip control/CR-LF/bidi/path-separator/\n * quote/unsafe-platform characters → collapse whitespace → trim trailing spaces and\n * dots → resolve the extension against the served contentType → replace an empty,\n * dots-only, or Windows-reserved stem → cap by UTF-8 BYTE length on a code-point\n * boundary, keeping the extension.\n *\n * Unicode letters and emoji SURVIVE — they are legal filename content and the\n * RFC 8187 `filename*` form carries them faithfully. The ASCII-only degradation\n * happens later, in `buildContentDispositionFilenameForms`, and only for the\n * RFC 6266 `filename=` fallback.\n */\nexport function normalizeDownloadFilename(\n rawName: string | null | undefined,\n options: NormalizeDownloadFilenameOptions = {},\n): string {\n const maxBytes = options.maxBytes ?? MAX_DOWNLOAD_FILENAME_BYTES;\n const fallbackStem =\n trimTrailingDotsAndSpaces(sanitizeSegment(options.fallbackStem ?? '')) ||\n DOWNLOAD_FILENAME_FALLBACK_STEM;\n\n const source = typeof rawName === 'string' ? rawName : '';\n const sanitized = trimTrailingDotsAndSpaces(sanitizeSegment(takeBasename(source.normalize('NFC'))));\n const split = splitExtension(sanitized);\n\n // --- extension: the served contentType is authoritative ---\n const canonicalExt = extensionForContentType(options.contentType);\n const inputExt = split.ext.toLowerCase();\n let ext: string;\n if (canonicalExt) {\n ext = acceptedExtensions(options.contentType).includes(inputExt) ? inputExt : canonicalExt;\n } else {\n ext = inputExt;\n }\n const suffix = ext ? `.${ext}` : '';\n\n // --- stem: nonempty, not a reserved device name ---\n let stem = trimTrailingDotsAndSpaces(split.stem);\n if (stem.length === 0) stem = fallbackStem;\n\n // --- byte cap (extension preserved; code points never split) ---\n const budget = maxBytes - utf8ByteLength(suffix);\n if (budget <= 0) {\n // Pathological: the extension alone exceeds the cap. Drop it rather than\n // emit an over-length header value, and keep a usable stem.\n return (\n truncateToUtf8Bytes(stem, maxBytes) ||\n truncateToUtf8Bytes(fallbackStem, maxBytes) ||\n fallbackStem\n );\n }\n\n if (RESERVED_DEVICE_NAME.test(stem)) stem = `_${stem}`;\n stem = trimTrailingDotsAndSpaces(truncateToUtf8Bytes(stem, budget));\n if (stem.length === 0) stem = trimTrailingDotsAndSpaces(truncateToUtf8Bytes(fallbackStem, budget));\n if (stem.length === 0) return truncateToUtf8Bytes(fallbackStem, maxBytes) || fallbackStem;\n\n return `${stem}${suffix}`;\n}\n\n// ---------------------------------------------------------------------------\n// RFC 6266 / RFC 8187 string forms\n// ---------------------------------------------------------------------------\n\n/** Combining marks — removed when degrading to ASCII so `café` reads `cafe`. */\nconst COMBINING_MARKS = /\\p{M}+/gu;\n/** Printable ASCII that is safe inside an RFC 6266 quoted-string. */\nconst SAFE_ASCII = /[\\u0020-\\u007E]/;\n/** Header-structural characters kept out of the fallback even though they are ASCII. */\nconst ASCII_HEADER_HOSTILE = new Set(['\"', '\\\\', ';', ',']);\n/** RFC 8187 attr-char: the only bytes that may appear unencoded in an ext-value. */\nconst ATTR_CHAR = /[A-Za-z0-9!#$&+\\-.^_`|~]/;\n\n/** Best-effort ASCII degradation of one name component. */\nfunction toAsciiComponent(value: string): string {\n const stripped = value.normalize('NFD').replace(COMBINING_MARKS, '');\n let out = '';\n for (const ch of stripped) {\n out += ch.length === 1 && SAFE_ASCII.test(ch) && !ASCII_HEADER_HOSTILE.has(ch) ? ch : '_';\n }\n return out\n .replace(/_+/g, '_')\n .replace(/^[_\\s]+|[_\\s.]+$/g, '');\n}\n\n/** Percent-encode every non-`attr-char` byte of the UTF-8 encoding (RFC 8187 §3.2). */\nfunction encodeExtValue(value: string): string {\n let out = '';\n for (const byte of utf8.encode(value)) {\n const ch = String.fromCharCode(byte);\n out += byte < 0x80 && ATTR_CHAR.test(ch) ? ch : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;\n }\n return out;\n}\n\nexport interface ContentDispositionFilenameForms {\n /**\n * RFC 6266 `filename=` value: safe printable ASCII only, already free of quotes,\n * backslashes, and header-structural characters, so the Worker may place it inside\n * `filename=\"…\"` verbatim with no further escaping.\n */\n asciiFallback: string;\n /**\n * RFC 8187 ext-value INCLUDING its charset prefix — e.g. `UTF-8''caf%C3%A9.jpg`.\n * Goes straight after `filename*=` UNQUOTED (an ext-value is never quoted).\n */\n extendedValue: string;\n}\n\n/**\n * Build both `Content-Disposition` filename forms from a normalized name.\n *\n * The Worker assembles the header itself (emitting `filename` first, `filename*`\n * second per RFC 6266), but the ESCAPING is owned here so the app, the Worker, and\n * the tests can never disagree about it. Defensively re-normalizes, so passing a\n * raw record value can still not produce a CR/LF or quote in a header.\n */\nexport function buildContentDispositionFilenameForms(\n normalizedFilename: string,\n options: NormalizeDownloadFilenameOptions = {},\n): ContentDispositionFilenameForms {\n const safeName = normalizeDownloadFilename(normalizedFilename, options);\n const { stem, ext } = splitExtension(safeName);\n const asciiStem = toAsciiComponent(stem) || DOWNLOAD_FILENAME_FALLBACK_STEM;\n const asciiExt = ext.replace(/[^A-Za-z0-9]/g, '').toLowerCase();\n\n return {\n asciiFallback: asciiExt ? `${asciiStem}.${asciiExt}` : asciiStem,\n extendedValue: `UTF-8''${encodeExtValue(safeName)}`,\n };\n}\n"]}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import type { MediaAccessTier, MediaServingStatus, MediaServingScope } from '../doc-schemas/media-assets.js';
|
|
1
|
+
import type { MediaAccessTier, MediaAssetVariant, MediaServingStatus, MediaServingScope } from '../doc-schemas/media-assets.js';
|
|
2
2
|
/** The signed internal apply route — one declaration; the functions client posts to
|
|
3
3
|
* it and the worker routes on it. */
|
|
4
4
|
export declare const MEDIA_AUTHORITY_APPLY_PATH = "/internal/media-authority/apply";
|
|
5
|
+
/**
|
|
6
|
+
* The serving-relevant projection of a variant carried on the edge record. DERIVED
|
|
7
|
+
* from the canonical `MediaAssetVariant` (ARCH-102) — the edge needs the served
|
|
8
|
+
* contentType, the recorded size for the range pre-gate, and the server-owned
|
|
9
|
+
* `downloadFilename` the Worker builds `Content-Disposition` from. Pixel dimensions
|
|
10
|
+
* and duration are deliberately NOT projected; the edge has no use for them.
|
|
11
|
+
*/
|
|
12
|
+
export type EdgeServingVariant = Pick<MediaAssetVariant, 'contentType' | 'sizeBytes' | 'downloadFilename'>;
|
|
5
13
|
/** The derived edge serving record (built by functions edge-sync
|
|
6
14
|
* `buildEdgeServingRecord`, stored in the shard DO + KV cache, read by the worker's
|
|
7
15
|
* serving path). A projection of MediaServingAuthorityRecord — same enums. */
|
|
@@ -16,10 +24,7 @@ export interface EdgeServingRecord {
|
|
|
16
24
|
/** Typed scope for scoped-tier assets; absent/null for everything else. The
|
|
17
25
|
* serving path EXACT-matches a grant against it. */
|
|
18
26
|
scope?: MediaServingScope | null;
|
|
19
|
-
variants: Record<string,
|
|
20
|
-
contentType: string;
|
|
21
|
-
sizeBytes: number;
|
|
22
|
-
}>;
|
|
27
|
+
variants: Record<string, EdgeServingVariant>;
|
|
23
28
|
}
|
|
24
29
|
/** The apply endpoint's authoritative ack (the worker's MediaAuthorityApplyResult;
|
|
25
30
|
* the functions client verifies the exact version + hash off it). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edge-serving-contract.d.ts","sourceRoot":"","sources":["../../src/media/edge-serving-contract.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,gCAAgC,CAAC;AAExC;qCACqC;AACrC,eAAO,MAAM,0BAA0B,oCAAoC,CAAC;AAE5E;;8EAE8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,kBAAkB,CAAC;IAClC,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB;gFAC4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;wDACoD;IACpD,KAAK,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"edge-serving-contract.d.ts","sourceRoot":"","sources":["../../src/media/edge-serving-contract.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,gCAAgC,CAAC;AAExC;qCACqC;AACrC,eAAO,MAAM,0BAA0B,oCAAoC,CAAC;AAE5E;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,iBAAiB,EACjB,aAAa,GAAG,WAAW,GAAG,kBAAkB,CACjD,CAAC;AAEF;;8EAE8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,kBAAkB,CAAC;IAClC,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB;gFAC4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;wDACoD;IACpD,KAAK,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC9C;AAED;qEACqE;AACrE,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,kBAAkB,CAAC;IAClC,OAAO,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;CAC5C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,CAAC,EAAE,CAAC,CAAC;IACL,GAAG,EAAE,SAAS,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,wDAAwD;IACxD,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IACX,gDAAgD;IAChD,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,CAAC,EAAE,CAAC,CAAC;IACL,GAAG,EAAE,OAAO,CAAC;IACb,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QACL,sDAAsD;QACtD,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,4EAA4E;QAC5E,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,CAAC,CAAC,EAAE,MAAM,CAAC;QACX;oFAC4E;QAC5E,EAAE,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC9B;+EACuE;QACvE,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ;mFAC2E;QAC3E,EAAE,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACnB;;oFAE4E;QAC5E,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,CAAC;CACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edge-serving-contract.js","sourceRoot":"","sources":["../../src/media/edge-serving-contract.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,iFAAiF;AACjF,+EAA+E;AAC/E,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,iFAAiF;AACjF,8EAA8E;AAC9E,8EAA8E;AAC9E,yEAAyE;AACzE,+EAA+E;
|
|
1
|
+
{"version":3,"file":"edge-serving-contract.js","sourceRoot":"","sources":["../../src/media/edge-serving-contract.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,iFAAiF;AACjF,+EAA+E;AAC/E,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,iFAAiF;AACjF,8EAA8E;AAC9E,8EAA8E;AAC9E,yEAAyE;AACzE,+EAA+E;AAS/E;qCACqC;AACrC,MAAM,CAAC,MAAM,0BAA0B,GAAG,iCAAiC,CAAC","sourcesContent":["// ============================================================================\n// MEDIA EDGE-SERVING CONTRACT — the ONE declaration of the wire contracts shared\n// by the backend (functions media-authority client / edge-sync / media-session\n// route / createMediaGrant) and the Cloudflare media-worker. Consolidated here\n// 2026-07-13 from the former hand-mirrored pair (media-worker/src/types.ts +\n// apply-endpoint.ts ↔ functions media-authority-client.ts / edge-sync.ts); the\n// worker's \"deliberately dependency-free\" rationale no longer holds — it already\n// imports @ttt-productions/edge-protocol-core, and these shapes are TTT media\n// business truth, so they live in ttt-core. Tier/status/scope derive from the\n// canonical doc-schemas enums — the worker never re-declares them again.\n// ============================================================================\n\nimport type {\n MediaAccessTier,\n MediaAssetVariant,\n MediaServingStatus,\n MediaServingScope,\n} from '../doc-schemas/media-assets.js';\n\n/** The signed internal apply route — one declaration; the functions client posts to\n * it and the worker routes on it. */\nexport const MEDIA_AUTHORITY_APPLY_PATH = '/internal/media-authority/apply';\n\n/**\n * The serving-relevant projection of a variant carried on the edge record. DERIVED\n * from the canonical `MediaAssetVariant` (ARCH-102) — the edge needs the served\n * contentType, the recorded size for the range pre-gate, and the server-owned\n * `downloadFilename` the Worker builds `Content-Disposition` from. Pixel dimensions\n * and duration are deliberately NOT projected; the edge has no use for them.\n */\nexport type EdgeServingVariant = Pick<\n MediaAssetVariant,\n 'contentType' | 'sizeBytes' | 'downloadFilename'\n>;\n\n/** The derived edge serving record (built by functions edge-sync\n * `buildEdgeServingRecord`, stored in the shard DO + KV cache, read by the worker's\n * serving path). A projection of MediaServingAuthorityRecord — same enums. */\nexport interface EdgeServingRecord {\n servingStatus: MediaServingStatus;\n accessTier: MediaAccessTier;\n ownerType: string;\n ownerId: string;\n /** Work-project scoped media (work files, pre-publish content). A conversation-file\n * asset never sets this — a conversation scope carries no workProjectId. */\n workProjectId?: string;\n /** Typed scope for scoped-tier assets; absent/null for everything else. The\n * serving path EXACT-matches a grant against it. */\n scope?: MediaServingScope | null;\n variants: Record<string, EdgeServingVariant>;\n}\n\n/** The apply endpoint's authoritative ack (the worker's MediaAuthorityApplyResult;\n * the functions client verifies the exact version + hash off it). */\nexport interface MediaAuthorityApplyAck {\n applied: boolean;\n idempotent: boolean;\n stale: boolean;\n authorityVersion: number;\n payloadHash: string;\n servingStatus: MediaServingStatus;\n kvCache: 'warmed' | 'deleted' | 'deferred';\n}\n\n/**\n * Signed-token payloads (cookie + grant). Wire format (worker token.ts /\n * edge-protocol-core signToken):\n * v1.{base64url(JSON payload)}.{base64url(HMAC-SHA256(secret, \"v1.\" + payloadB64))}\n * Timestamps are SECONDS. The app side (Next media-session route, the\n * createMediaGrant callable) signs; the worker verifies — same shapes, ONE home.\n */\nexport interface SessionTokenPayload {\n v: 1;\n typ: 'session';\n uid: string;\n /** 1 = artisan creator (unlocks the 'artisan' tier). */\n art: 0 | 1;\n /** 1 = admin (unlocks the 'adminOnly' tier). */\n adm: 0 | 1;\n iat: number;\n exp: number;\n}\n\nexport interface GrantTokenPayload {\n v: 1;\n typ: 'grant';\n /** Must equal the session cookie's uid — grants are user-bound, never bearer. */\n uid: string;\n exp: number;\n scope: {\n /** Grants every scoped asset of this work project. */\n w?: string;\n /** Or one exact owner (e.g. a commission proposal): ownerType + ownerId. */\n t?: string;\n o?: string;\n /** Work-project FILE FOLDER scope — the SAME ids the asset's typed `scope`\n * carries; EXACT-matched (a {w} Work grant never reaches a folder file). */\n wf?: { w: string; f: string };\n /** Guild-INVITE conversation scope (guildInviteId) — a Conversation File in that\n * conversation; EXACT-matched (never a {w} Work grant cross-match). */\n gi?: string;\n /** Admin-SUPPORT thread scope (adminDispatchId) — the SAME id the asset's typed\n * `scope` carries; EXACT-matched (one grant covers the thread's files). */\n as?: { d: string };\n /** Admin-REVIEW reveal scope — bound to exactly ONE mediaAssetId (minted only for\n * a full/jr admin against a report group's server-derived target). EXACT-matched\n * against the REQUESTED asset id, regardless of the record's scope kind. */\n ar?: string;\n };\n}\n"]}
|
package/dist/media/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./hall-library-target-fields.js";
|
|
|
9
9
|
export * from "./pending-media.js";
|
|
10
10
|
export * from "./upload-tray-display.js";
|
|
11
11
|
export * from "./media-asset-url.js";
|
|
12
|
+
export * from "./download-filename.js";
|
|
12
13
|
export * from "./photodna-coverage.js";
|
|
13
14
|
export * from "./edge-serving-contract.js";
|
|
14
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kCAAkC,EAClC,KAAK,OAAO,EACZ,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AACpB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kCAAkC,EAClC,KAAK,OAAO,EACZ,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AACpB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC"}
|
package/dist/media/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./hall-library-target-fields.js";
|
|
|
9
9
|
export * from "./pending-media.js";
|
|
10
10
|
export * from "./upload-tray-display.js";
|
|
11
11
|
export * from "./media-asset-url.js";
|
|
12
|
+
export * from "./download-filename.js";
|
|
12
13
|
export * from "./photodna-coverage.js";
|
|
13
14
|
export * from "./edge-serving-contract.js";
|
|
14
15
|
//# sourceMappingURL=index.js.map
|
package/dist/media/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAmB,MAAM,kBAAkB,CAAC;AACrE,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kCAAkC,GAGnC,MAAM,YAAY,CAAC;AACpB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC","sourcesContent":["export { FileOriginSchema, type FileOrigin } from \"./file-origin.js\";\nexport {\n MentionSchema,\n MentionTypeSchema,\n rejectDuplicateMentionPlaceholders,\n type Mention,\n type MentionType,\n} from \"./atoms.js\";\nexport * from \"./conversation-file-ref.js\";\nexport * from \"./domain-events.js\";\nexport {\n TTT_MEDIA_SPECS,\n} from \"./ttt-media-specs.js\";\nexport * from \"./start-upload.js\";\nexport * from \"./target-info.js\";\nexport * from \"./hall-library-target-fields.js\";\nexport * from \"./pending-media.js\";\nexport * from \"./upload-tray-display.js\";\nexport * from \"./media-asset-url.js\";\nexport * from \"./photodna-coverage.js\";\nexport * from \"./edge-serving-contract.js\";\n\n\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAmB,MAAM,kBAAkB,CAAC;AACrE,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kCAAkC,GAGnC,MAAM,YAAY,CAAC;AACpB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC","sourcesContent":["export { FileOriginSchema, type FileOrigin } from \"./file-origin.js\";\nexport {\n MentionSchema,\n MentionTypeSchema,\n rejectDuplicateMentionPlaceholders,\n type Mention,\n type MentionType,\n} from \"./atoms.js\";\nexport * from \"./conversation-file-ref.js\";\nexport * from \"./domain-events.js\";\nexport {\n TTT_MEDIA_SPECS,\n} from \"./ttt-media-specs.js\";\nexport * from \"./start-upload.js\";\nexport * from \"./target-info.js\";\nexport * from \"./hall-library-target-fields.js\";\nexport * from \"./pending-media.js\";\nexport * from \"./upload-tray-display.js\";\nexport * from \"./media-asset-url.js\";\nexport * from \"./download-filename.js\";\nexport * from \"./photodna-coverage.js\";\nexport * from \"./edge-serving-contract.js\";\n\n\n"]}
|