@ttt-productions/ttt-core 0.79.2 → 0.79.4
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/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/registry.d.ts +2 -0
- 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/dist/schemas/admin-dispatch-actions.d.ts +6 -3
- package/dist/schemas/admin-dispatch-actions.d.ts.map +1 -1
- package/dist/schemas/admin-dispatch-actions.js +15 -4
- package/dist/schemas/admin-dispatch-actions.js.map +1 -1
- package/dist/types/content.d.ts +0 -1
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.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"]}
|
|
@@ -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"]}
|
|
@@ -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>;
|
|
@@ -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"]}
|
|
@@ -14,15 +14,18 @@ export declare const UpdateAdminDispatchStatusInputSchema: z.ZodObject<{
|
|
|
14
14
|
}>;
|
|
15
15
|
}, z.core.$strict>;
|
|
16
16
|
export type UpdateAdminDispatchStatusInput = z.infer<typeof UpdateAdminDispatchStatusInputSchema>;
|
|
17
|
-
export declare const UpdateInviteConfirmationInputSchema: z.ZodObject<{
|
|
17
|
+
export declare const UpdateInviteConfirmationInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
18
|
+
guildInviteId: z.ZodString;
|
|
19
|
+
action: z.ZodLiteral<"agree">;
|
|
20
|
+
expectedStakeShares: z.ZodNumber;
|
|
21
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
18
22
|
guildInviteId: z.ZodString;
|
|
19
23
|
action: z.ZodEnum<{
|
|
20
|
-
agree: "agree";
|
|
21
24
|
decline: "decline";
|
|
22
25
|
cancel: "cancel";
|
|
23
26
|
retract: "retract";
|
|
24
27
|
}>;
|
|
25
|
-
}, z.core.$strict>;
|
|
28
|
+
}, z.core.$strict>], "action">;
|
|
26
29
|
export type UpdateInviteConfirmationInput = z.infer<typeof UpdateInviteConfirmationInputSchema>;
|
|
27
30
|
export declare const UpdateGuildInviteStakeSharesInputSchema: z.ZodObject<{
|
|
28
31
|
guildInviteId: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-dispatch-actions.d.ts","sourceRoot":"","sources":["../../src/schemas/admin-dispatch-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;8DAC8D;AAC9D,eAAO,MAAM,8BAA8B;;;EAAmD,CAAC;AAC/F,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAItF,eAAO,MAAM,oCAAoC;;;;;;kBAGtC,CAAC;AACZ,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oCAAoC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"admin-dispatch-actions.d.ts","sourceRoot":"","sources":["../../src/schemas/admin-dispatch-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;8DAC8D;AAC9D,eAAO,MAAM,8BAA8B;;;EAAmD,CAAC;AAC/F,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAItF,eAAO,MAAM,oCAAoC;;;;;;kBAGtC,CAAC;AACZ,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oCAAoC,CAAC,CAAC;AAMlG,eAAO,MAAM,mCAAmC;;;;;;;;;;;8BAU9C,CAAC;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC;AAEhG,eAAO,MAAM,uCAAuC;;;kBAGzC,CAAC;AACZ,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uCAAuC,CAAC,CAAC;AAIxG,eAAO,MAAM,8BAA8B;;iBAEzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAItF,eAAO,MAAM,qCAAqC;;;;;;;;;iBAUhD,CAAC;AACH,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qCAAqC,CAAC,CAAC;AAEpG,eAAO,MAAM,+BAA+B;;;;;iBAQ1C,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC"}
|
|
@@ -9,10 +9,21 @@ export const UpdateAdminDispatchStatusInputSchema = z.object({
|
|
|
9
9
|
adminDispatchId: adminDispatchIdSchema,
|
|
10
10
|
newStatus: AdminDispatchCloseStatusSchema,
|
|
11
11
|
}).strict();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// The invite conversation is the binding agreement, so consent is pinned to specific terms:
|
|
13
|
+
// an `agree` must carry the stake-share offer the member was shown, and the backend rejects
|
|
14
|
+
// the action inside its transaction when the doc's current offer differs (the member re-agrees
|
|
15
|
+
// against the fresh value). The other actions don't assert terms and carry no pin.
|
|
16
|
+
export const UpdateInviteConfirmationInputSchema = z.discriminatedUnion('action', [
|
|
17
|
+
z.object({
|
|
18
|
+
guildInviteId: guildInviteIdSchema,
|
|
19
|
+
action: z.literal('agree'),
|
|
20
|
+
expectedStakeShares: z.number().int().min(1),
|
|
21
|
+
}).strict(),
|
|
22
|
+
z.object({
|
|
23
|
+
guildInviteId: guildInviteIdSchema,
|
|
24
|
+
action: z.enum(['decline', 'cancel', 'retract']),
|
|
25
|
+
}).strict(),
|
|
26
|
+
]);
|
|
16
27
|
export const UpdateGuildInviteStakeSharesInputSchema = z.object({
|
|
17
28
|
guildInviteId: guildInviteIdSchema,
|
|
18
29
|
newStakeShares: z.number().int().min(1),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-dispatch-actions.js","sourceRoot":"","sources":["../../src/schemas/admin-dispatch-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExE;8DAC8D;AAC9D,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAG/F,0FAA0F;AAC1F,6FAA6F;AAC7F,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3D,eAAe,EAAE,qBAAqB;IACtC,SAAS,EAAE,8BAA8B;CAC1C,CAAC,CAAC,MAAM,EAAE,CAAC;AAGZ,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"admin-dispatch-actions.js","sourceRoot":"","sources":["../../src/schemas/admin-dispatch-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExE;8DAC8D;AAC9D,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAG/F,0FAA0F;AAC1F,6FAA6F;AAC7F,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3D,eAAe,EAAE,qBAAqB;IACtC,SAAS,EAAE,8BAA8B;CAC1C,CAAC,CAAC,MAAM,EAAE,CAAC;AAGZ,4FAA4F;AAC5F,4FAA4F;AAC5F,+FAA+F;AAC/F,mFAAmF;AACnF,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC,CAAC,kBAAkB,CAAC,QAAQ,EAAE;IAChF,CAAC,CAAC,MAAM,CAAC;QACP,aAAa,EAAE,mBAAmB;QAClC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QAC1B,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC7C,CAAC,CAAC,MAAM,EAAE;IACX,CAAC,CAAC,MAAM,CAAC;QACP,aAAa,EAAE,mBAAmB;QAClC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;KACjD,CAAC,CAAC,MAAM,EAAE;CACZ,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,uCAAuC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9D,aAAa,EAAE,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACxC,CAAC,CAAC,MAAM,EAAE,CAAC;AAGZ,8FAA8F;AAC9F,0DAA0D;AAC1D,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC;IACrD,eAAe,EAAE,qBAAqB;CACvC,CAAC,CAAC;AAGH,8EAA8E;AAE9E,MAAM,CAAC,MAAM,qCAAqC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5D,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IACxB,eAAe,EAAE,qBAAqB;IACtC,wCAAwC;IACxC,SAAS,EAAE,8BAA8B;IACzC,uFAAuF;IACvF,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B;qGACiG;IACjG,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC3C,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IACtD,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IACxB,eAAe,EAAE,qBAAqB;IACtC,wFAAwF;IACxF,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACnD;qGACiG;IACjG,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC3C,CAAC,CAAC","sourcesContent":["import { z } from 'zod';\nimport { adminDispatchIdSchema, guildInviteIdSchema } from './atoms.js';\n\n/** The two terminal dispatch-thread statuses a close can set (ONE canonical declaration —\n * shared by the close input and its authoritative result). */\nexport const AdminDispatchCloseStatusSchema = z.enum(['closed_resolved', 'closed_unresolved']);\nexport type AdminDispatchCloseStatus = z.infer<typeof AdminDispatchCloseStatusSchema>;\n\n// The close message is composed SERVER-SIDE from the authoritative actor role; the client\n// supplies no message text (a client-supplied value would be a system-message spoof vector).\nexport const UpdateAdminDispatchStatusInputSchema = z.object({\n adminDispatchId: adminDispatchIdSchema,\n newStatus: AdminDispatchCloseStatusSchema,\n}).strict();\nexport type UpdateAdminDispatchStatusInput = z.infer<typeof UpdateAdminDispatchStatusInputSchema>;\n\n// The invite conversation is the binding agreement, so consent is pinned to specific terms:\n// an `agree` must carry the stake-share offer the member was shown, and the backend rejects\n// the action inside its transaction when the doc's current offer differs (the member re-agrees\n// against the fresh value). The other actions don't assert terms and carry no pin.\nexport const UpdateInviteConfirmationInputSchema = z.discriminatedUnion('action', [\n z.object({\n guildInviteId: guildInviteIdSchema,\n action: z.literal('agree'),\n expectedStakeShares: z.number().int().min(1),\n }).strict(),\n z.object({\n guildInviteId: guildInviteIdSchema,\n action: z.enum(['decline', 'cancel', 'retract']),\n }).strict(),\n]);\nexport type UpdateInviteConfirmationInput = z.infer<typeof UpdateInviteConfirmationInputSchema>;\n\nexport const UpdateGuildInviteStakeSharesInputSchema = z.object({\n guildInviteId: guildInviteIdSchema,\n newStakeShares: z.number().int().min(1),\n}).strict();\nexport type UpdateGuildInviteStakeSharesInput = z.infer<typeof UpdateGuildInviteStakeSharesInputSchema>;\n\n// Admin/jr-admin deletes a user's support dispatch thread (`deleteAdminDispatch`). Non-strict\n// posture is carried faithfully from the source callable.\nexport const DeleteAdminDispatchInputSchema = z.object({\n adminDispatchId: adminDispatchIdSchema,\n});\nexport type DeleteAdminDispatchInput = z.infer<typeof DeleteAdminDispatchInputSchema>;\n\n// --- Authoritative mutation RESULTS (non-strict server → client posture) ---\n\nexport const UpdateAdminDispatchStatusResultSchema = z.object({\n success: z.literal(true),\n adminDispatchId: adminDispatchIdSchema,\n /** The thread's status after commit. */\n newStatus: AdminDispatchCloseStatusSchema,\n /** Uid of the closer (the caller) — attribution for the server-composed close line. */\n closedBy: z.string().min(1),\n /** Operation-receipt audit id — the `chat.adminThreadStatusChanged` event written in the same\n * transaction. Optional/additive; the domain id (`adminDispatchId`) is already carried above. */\n auditEventId: z.string().min(1).optional(),\n});\nexport type UpdateAdminDispatchStatusResult = z.infer<typeof UpdateAdminDispatchStatusResultSchema>;\n\nexport const DeleteAdminDispatchResultSchema = z.object({\n success: z.literal(true),\n adminDispatchId: adminDispatchIdSchema,\n /** Conversation messages removed with the thread (counted during the delete itself). */\n deletedMessageCount: z.number().int().nonnegative(),\n /** Operation-receipt audit id — the `admin.dispatchDeleted` event written in the same\n * transaction. Optional/additive; the domain id (`adminDispatchId`) is already carried above. */\n auditEventId: z.string().min(1).optional(),\n});\nexport type DeleteAdminDispatchResult = z.infer<typeof DeleteAdminDispatchResultSchema>;\n\n\n"]}
|
package/dist/types/content.d.ts
CHANGED
|
@@ -12,7 +12,6 @@ export type { ThresholdItem, PublishedHallItem, PublishedTuneTrack, PublishedCha
|
|
|
12
12
|
export type { HallLibraryPreferences, HallLibraryInProgressEntry, HallLibraryRecentlyViewedEntry, HallLibrarySettings, } from '../doc-schemas/content.js';
|
|
13
13
|
export type HallLibrarySortOption = 'newest' | 'oldest' | 'most_followed';
|
|
14
14
|
export type HallLibraryFilters = {
|
|
15
|
-
workGenre?: string;
|
|
16
15
|
hallWingType: HallWingTypeFilter;
|
|
17
16
|
workProjectType: WorkProjectTypeFilter;
|
|
18
17
|
sortBy: HallLibrarySortOption;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,aAAa,EACb,cAAc,EACd,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,mBAAmB,4DAA6D,CAAC;AAC9F,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,6FAA6F;AAC7F,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,KAAK,CAAC;AAEtD,eAAO,MAAM,sBAAsB,2CAA4C,CAAC;AAChF,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,gGAAgG;AAChG,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG,KAAK,CAAC;AAE5D,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAG1D,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EACV,sBAAsB,EACtB,0BAA0B,EAC1B,8BAA8B,EAC9B,mBAAmB,GACpB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,GACR,QAAQ,GACR,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,aAAa,EACb,cAAc,EACd,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,mBAAmB,4DAA6D,CAAC;AAC9F,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,6FAA6F;AAC7F,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,KAAK,CAAC;AAEtD,eAAO,MAAM,sBAAsB,2CAA4C,CAAC;AAChF,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,gGAAgG;AAChG,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG,KAAK,CAAC;AAE5D,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAG1D,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,0BAA0B,EAC1B,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EACV,sBAAsB,EACtB,0BAA0B,EAC1B,8BAA8B,EAC9B,mBAAmB,GACpB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,GACR,QAAQ,GACR,eAAe,CAAC;AAMpB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,kBAAkB,CAAC;IACjC,eAAe,EAAE,qBAAqB,CAAC;IACvC,MAAM,EAAE,qBAAqB,CAAC;CAC/B,CAAC;AAGF,YAAY,EACV,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,YAAY,EACZ,IAAI,EACJ,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAYvD,4BAA4B;AAE5B,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,eAAe,EAAE,aAAa,EAAE,eAAe,CAAU,CAAC;AAM9F,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAU,CAAC","sourcesContent":["// Content types: Tales, Tunes, Television, HallLibrary\n\n// --- WorkProject content sub-docs (shapes in ../doc-schemas/content.ts) ---\nexport type {\n FullTale,\n FullChapter,\n FullTune,\n FullTuneTrack,\n FullTelevision,\n FullTelevisionEpisode,\n} from '../doc-schemas/content.js';\n\n// --- HallLibrary Types ---\n\nexport const HALL_WING_TYPE_KEYS = ['entertainment', 'educational', 'newsPolitical'] as const;\nexport type HallWingType = (typeof HALL_WING_TYPE_KEYS)[number];\n\n/** HallWingType with 'All' filter option — for UI filtering only, not stored in Firestore */\nexport type HallWingTypeFilter = HallWingType | 'All';\n\nexport const WORK_PROJECT_TYPE_KEYS = ['Tales', 'Tunes', 'Television'] as const;\nexport type WorkProjectType = (typeof WORK_PROJECT_TYPE_KEYS)[number];\n\n/** WorkProjectType with 'All' filter option — for UI filtering only, not stored in Firestore */\nexport type WorkProjectTypeFilter = WorkProjectType | 'All';\n\nexport type { ItemsKey } from '../doc-schemas/content.js';\n\n// --- Threshold + Hall library items (shapes in ../doc-schemas/content.ts) ---\nexport type {\n ThresholdItem,\n PublishedHallItem,\n PublishedTuneTrack,\n PublishedChapter,\n PublishedTelevisionEpisode,\n HallContentChangeRequest,\n HallContentTextSurface,\n} from '../doc-schemas/content.js';\n\nexport type {\n HallLibraryPreferences,\n HallLibraryInProgressEntry,\n HallLibraryRecentlyViewedEntry,\n HallLibrarySettings,\n} from '../doc-schemas/content.js';\n\nexport type HallLibrarySortOption =\n | 'newest'\n | 'oldest'\n | 'most_followed';\n\nexport type HallLibraryFilters = {\n
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAYvD,4BAA4B;AAE5B,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,eAAe,EAAE,aAAa,EAAE,eAAe,CAAU,CAAC;AAM9F,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAU,CAAC","sourcesContent":["// Content types: Tales, Tunes, Television, HallLibrary\n\n// --- WorkProject content sub-docs (shapes in ../doc-schemas/content.ts) ---\nexport type {\n FullTale,\n FullChapter,\n FullTune,\n FullTuneTrack,\n FullTelevision,\n FullTelevisionEpisode,\n} from '../doc-schemas/content.js';\n\n// --- HallLibrary Types ---\n\nexport const HALL_WING_TYPE_KEYS = ['entertainment', 'educational', 'newsPolitical'] as const;\nexport type HallWingType = (typeof HALL_WING_TYPE_KEYS)[number];\n\n/** HallWingType with 'All' filter option — for UI filtering only, not stored in Firestore */\nexport type HallWingTypeFilter = HallWingType | 'All';\n\nexport const WORK_PROJECT_TYPE_KEYS = ['Tales', 'Tunes', 'Television'] as const;\nexport type WorkProjectType = (typeof WORK_PROJECT_TYPE_KEYS)[number];\n\n/** WorkProjectType with 'All' filter option — for UI filtering only, not stored in Firestore */\nexport type WorkProjectTypeFilter = WorkProjectType | 'All';\n\nexport type { ItemsKey } from '../doc-schemas/content.js';\n\n// --- Threshold + Hall library items (shapes in ../doc-schemas/content.ts) ---\nexport type {\n ThresholdItem,\n PublishedHallItem,\n PublishedTuneTrack,\n PublishedChapter,\n PublishedTelevisionEpisode,\n HallContentChangeRequest,\n HallContentTextSurface,\n} from '../doc-schemas/content.js';\n\nexport type {\n HallLibraryPreferences,\n HallLibraryInProgressEntry,\n HallLibraryRecentlyViewedEntry,\n HallLibrarySettings,\n} from '../doc-schemas/content.js';\n\nexport type HallLibrarySortOption =\n | 'newest'\n | 'oldest'\n | 'most_followed';\n\n// V1 Hall browse filters on the big categories only (medium coins × wing coins). The genre\n// FILTER was removed for launch (DJ, 2026-07-27) — works still TAG per-medium genres at\n// creation and display them, so a browse genre filter can return post-launch once real\n// content data exists (options recorded in ttt-prod docs/post-launch).\nexport type HallLibraryFilters = {\n hallWingType: HallWingTypeFilter;\n workProjectType: WorkProjectTypeFilter;\n sortBy: HallLibrarySortOption;\n};\n\n// --- _config singleton docs (shapes in ../doc-schemas/content.ts) ---\nexport type {\n FuturePlanItem,\n FuturePlansDocument,\n RuleGroup,\n RuleSubgroup,\n Rule,\n AgreementCategory,\n RulesAndAgreements,\n} from '../doc-schemas/content.js';\n\n\n"]}
|