@mevdragon/vidfarm-devcli 0.5.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/SKILL.director.md +28 -7
- package/SKILL.platform.md +9 -5
- package/demo/README.md +28 -0
- package/demo/dist/app.css +1 -0
- package/demo/dist/app.js +1184 -0
- package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
- package/demo/dist/chunks/chunk-S7OWAJDS.js +36 -0
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +1 -0
- package/demo/dist/chunks/dist-ADSJKBVE.js +332 -0
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +1 -0
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +425 -0
- package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
- package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +7 -0
- package/demo/dist/icons/timeline/captions.svg +5 -0
- package/demo/dist/icons/timeline/composition.svg +12 -0
- package/demo/dist/icons/timeline/image.svg +18 -0
- package/demo/dist/icons/timeline/music.svg +10 -0
- package/demo/dist/icons/timeline/text.svg +3 -0
- package/demo/dist/index.html +15 -0
- package/dist/src/account-pages-legacy.js +9396 -0
- package/dist/src/account-pages.js +61 -0
- package/dist/src/app.js +14663 -0
- package/dist/src/cli.js +60 -97
- package/dist/src/composition-runtime.js +613 -0
- package/dist/src/config.js +173 -0
- package/dist/src/context.js +447 -0
- package/dist/src/dev-app-legacy.js +739 -0
- package/dist/src/dev-app.js +6 -0
- package/dist/src/domain.js +2 -0
- package/dist/src/editor-chat-history.js +82 -0
- package/dist/src/editor-chat.js +449 -0
- package/dist/src/editor-dark-theme.js +1128 -0
- package/dist/src/frontend/debug.js +71 -0
- package/dist/src/frontend/flockposter-cache-store.js +124 -0
- package/dist/src/frontend/homepage-client.js +182 -0
- package/dist/src/frontend/homepage-shared.js +4 -0
- package/dist/src/frontend/homepage-store.js +28 -0
- package/dist/src/frontend/homepage-view.js +547 -0
- package/dist/src/frontend/page-runtime-client.js +132 -0
- package/dist/src/frontend/page-runtime-store.js +9 -0
- package/dist/src/frontend/sentry.js +42 -0
- package/dist/src/frontend/template-editor-chat.js +3960 -0
- package/dist/src/help-page.js +346 -0
- package/dist/src/homepage.js +1235 -0
- package/dist/src/hyperframes/composition.js +180 -0
- package/dist/src/index.js +16 -0
- package/dist/src/instrument.js +30 -0
- package/dist/src/lib/crypto.js +45 -0
- package/dist/src/lib/dev-log.js +54 -0
- package/dist/src/lib/display-name.js +11 -0
- package/dist/src/lib/ids.js +24 -0
- package/dist/src/lib/images.js +19 -0
- package/dist/src/lib/json.js +15 -0
- package/dist/src/lib/package-root.js +47 -0
- package/dist/src/lib/template-paths.js +28 -0
- package/dist/src/lib/time.js +7 -0
- package/dist/src/lib/url-clean.js +85 -0
- package/dist/src/page-runtime.js +2 -0
- package/dist/src/page-shell.js +1381 -0
- package/dist/src/primitive-context.js +357 -0
- package/dist/src/primitive-registry.js +2561 -0
- package/dist/src/primitive-sdk.js +4 -0
- package/dist/src/primitives/hyperframes-media.js +108 -0
- package/dist/src/react-page-shell.js +35 -0
- package/dist/src/ready-post-schedule-component.js +1540 -0
- package/dist/src/registry.js +296 -0
- package/dist/src/runtime.js +35 -0
- package/dist/src/services/api-call-history.js +249 -0
- package/dist/src/services/auth.js +152 -0
- package/dist/src/services/billing-pricing.js +39 -0
- package/dist/src/services/billing.js +228 -0
- package/dist/src/services/cast.js +127 -0
- package/dist/src/services/chat-threads.js +92 -0
- package/dist/src/services/composition-sanitize.js +124 -0
- package/dist/src/services/composition-watch.js +79 -0
- package/dist/src/services/fork-access.js +93 -0
- package/dist/src/services/fork-manifest.js +42 -0
- package/dist/src/services/ghostcut.js +179 -0
- package/dist/src/services/hyperframes.js +2307 -0
- package/dist/src/services/job-capacity.js +14 -0
- package/dist/src/services/job-logs.js +197 -0
- package/dist/src/services/jobs.js +136 -0
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +766 -0
- package/dist/src/services/primitive-media-lambda.js +280 -0
- package/dist/src/services/providers.js +2926 -0
- package/dist/src/services/rate-limits.js +262 -0
- package/dist/src/services/serverless-auth.js +382 -0
- package/dist/src/services/serverless-jobs.js +1082 -0
- package/dist/src/services/serverless-provider-keys.js +409 -0
- package/dist/src/services/serverless-records.js +1385 -0
- package/dist/src/services/serverless-template-configs.js +75 -0
- package/dist/src/services/storage.js +383 -0
- package/dist/src/services/template-certification.js +413 -0
- package/dist/src/services/template-loader.js +99 -0
- package/dist/src/services/template-runtime-bundles.js +217 -0
- package/dist/src/services/template-sources.js +1017 -0
- package/dist/src/services/upstream.js +248 -0
- package/dist/src/services/video-normalization.js +2 -0
- package/dist/src/services/webhooks.js +62 -0
- package/dist/src/template-editor-pages.js +2576 -0
- package/dist/src/template-editor-shell.js +2840 -0
- package/dist/src/template-sdk.js +4 -0
- package/dist/src/worker.js +17 -0
- package/package.json +6 -4
- package/public/assets/homepage-app.js +54 -0
- package/public/assets/homepage-client-app.js +80 -0
- package/public/assets/page-runtime-client-app.js +94 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
- package/src/assets/SELLING_WITH_HOOKS.md +377 -0
- package/src/assets/SELLING_WITH_VSLS.md +606 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
2
|
+
import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
|
|
3
|
+
import { config } from "../config.js";
|
|
4
|
+
import { devLog } from "../lib/dev-log.js";
|
|
5
|
+
import { nowIso } from "../lib/time.js";
|
|
6
|
+
import { createLocalDynamoClient } from "./local-dynamo.js";
|
|
7
|
+
// `vidfarm serve` (RECORDS_DRIVER=local) swaps in the disk-backed shim so
|
|
8
|
+
// template configs live on disk; deployed environments keep the real client.
|
|
9
|
+
const dynamodb = config.RECORDS_DRIVER === "local"
|
|
10
|
+
? createLocalDynamoClient()
|
|
11
|
+
: DynamoDBDocumentClient.from(new DynamoDBClient({}), {
|
|
12
|
+
marshallOptions: {
|
|
13
|
+
removeUndefinedValues: true
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
export class ServerlessTemplateConfigService {
|
|
17
|
+
async getTemplateConfig(customerId, templateId) {
|
|
18
|
+
const response = await dynamodb.send(new GetCommand({
|
|
19
|
+
TableName: requireMainTableName(),
|
|
20
|
+
Key: templateConfigKey(customerId, templateId)
|
|
21
|
+
}));
|
|
22
|
+
const item = response.Item;
|
|
23
|
+
const configJson = item && isRecord(item.config_json) ? item.config_json : {};
|
|
24
|
+
devLog("template_config.get", {
|
|
25
|
+
customer_id: customerId,
|
|
26
|
+
template_id: templateId,
|
|
27
|
+
found: Boolean(item),
|
|
28
|
+
key_count: Object.keys(configJson).length
|
|
29
|
+
});
|
|
30
|
+
return configJson;
|
|
31
|
+
}
|
|
32
|
+
async upsertTemplateConfig(input) {
|
|
33
|
+
const timestamp = nowIso();
|
|
34
|
+
await dynamodb.send(new PutCommand({
|
|
35
|
+
TableName: requireMainTableName(),
|
|
36
|
+
Item: {
|
|
37
|
+
...templateConfigKey(input.customerId, input.templateId),
|
|
38
|
+
entity_type: "TemplateConfig",
|
|
39
|
+
gsi1pk: `CUSTOMER#${input.customerId}#TEMPLATE_CONFIGS`,
|
|
40
|
+
gsi1sk: `TEMPLATE#${input.templateId}`,
|
|
41
|
+
config_id: input.id,
|
|
42
|
+
customer_id: input.customerId,
|
|
43
|
+
template_id: input.templateId,
|
|
44
|
+
config_json: input.config,
|
|
45
|
+
created_at: timestamp,
|
|
46
|
+
updated_at: timestamp
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
devLog("template_config.upserted", {
|
|
50
|
+
config_id: input.id,
|
|
51
|
+
customer_id: input.customerId,
|
|
52
|
+
template_id: input.templateId,
|
|
53
|
+
key_count: Object.keys(input.config).length
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function requireMainTableName() {
|
|
58
|
+
if (config.RECORDS_DRIVER === "local") {
|
|
59
|
+
return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME || "vidfarm-local-main";
|
|
60
|
+
}
|
|
61
|
+
if (!config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME) {
|
|
62
|
+
throw new Error("Serverless template config store requires VIDFARM_SERVERLESS_MAIN_TABLE_NAME.");
|
|
63
|
+
}
|
|
64
|
+
return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME;
|
|
65
|
+
}
|
|
66
|
+
function templateConfigKey(customerId, templateId) {
|
|
67
|
+
return {
|
|
68
|
+
pk: `CUSTOMER#${customerId}#TEMPLATE#${templateId}`,
|
|
69
|
+
sk: "CONFIG"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function isRecord(value) {
|
|
73
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=serverless-template-configs.js.map
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
4
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
5
|
+
import { config } from "../config.js";
|
|
6
|
+
function isS3NotFoundError(error) {
|
|
7
|
+
if (!error || typeof error !== "object")
|
|
8
|
+
return false;
|
|
9
|
+
const record = error;
|
|
10
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
11
|
+
const code = typeof record.Code === "string" ? record.Code : "";
|
|
12
|
+
const metadata = record.$metadata;
|
|
13
|
+
const httpStatusCode = typeof metadata?.httpStatusCode === "number" ? metadata.httpStatusCode : undefined;
|
|
14
|
+
return name === "NoSuchKey" || code === "NoSuchKey" || name === "NotFound" || httpStatusCode === 404;
|
|
15
|
+
}
|
|
16
|
+
export class StorageService {
|
|
17
|
+
localRoot = path.join(config.VIDFARM_DATA_DIR, "storage");
|
|
18
|
+
s3 = config.STORAGE_DRIVER === "s3"
|
|
19
|
+
? new S3Client({
|
|
20
|
+
region: config.AWS_REGION,
|
|
21
|
+
endpoint: config.AWS_S3_ENDPOINT || undefined
|
|
22
|
+
})
|
|
23
|
+
: null;
|
|
24
|
+
constructor() {
|
|
25
|
+
mkdirSync(this.localRoot, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
userAttachmentKey(customerId, attachmentId, fileName, folderPath) {
|
|
28
|
+
return joinStorageKey("users", customerId, "files", folderPath || "", attachmentId, fileName);
|
|
29
|
+
}
|
|
30
|
+
legacyUserAttachmentKey(customerId, attachmentId, fileName) {
|
|
31
|
+
return joinStorageKey("users", customerId, attachmentId, fileName);
|
|
32
|
+
}
|
|
33
|
+
userTemporaryFileKey(customerId, fileId, fileName, folderPath) {
|
|
34
|
+
return joinStorageKey("users", customerId, "temporary", folderPath || "", fileId, fileName);
|
|
35
|
+
}
|
|
36
|
+
developerAttachmentKey(customerId, attachmentId, fileName) {
|
|
37
|
+
return joinStorageKey("developer", customerId, "attachments", attachmentId, fileName);
|
|
38
|
+
}
|
|
39
|
+
developerScopedKey(customerId, ...parts) {
|
|
40
|
+
return joinStorageKey("developer", customerId, ...parts);
|
|
41
|
+
}
|
|
42
|
+
templateJobPrefix(templateId, customerId, jobId) {
|
|
43
|
+
return joinStorageKey("templates", templateId, "users", customerId, "jobs", jobId);
|
|
44
|
+
}
|
|
45
|
+
templateJobKey(templateId, customerId, jobId, key) {
|
|
46
|
+
return joinStorageKey(this.templateJobPrefix(templateId, customerId, jobId), key);
|
|
47
|
+
}
|
|
48
|
+
primitiveJobPrefix(primitiveId, customerId, jobId) {
|
|
49
|
+
return joinStorageKey("primitives", primitiveId, "users", customerId, "jobs", jobId);
|
|
50
|
+
}
|
|
51
|
+
primitiveJobKey(primitiveId, customerId, jobId, key) {
|
|
52
|
+
return joinStorageKey(this.primitiveJobPrefix(primitiveId, customerId, jobId), key);
|
|
53
|
+
}
|
|
54
|
+
templateAboutKey(templateId, assetPath) {
|
|
55
|
+
return joinStorageKey("templates", templateId, "about", assetPath);
|
|
56
|
+
}
|
|
57
|
+
compositionForkRoot(forkId) {
|
|
58
|
+
return joinStorageKey("compositions", "forks", forkId);
|
|
59
|
+
}
|
|
60
|
+
compositionForkWorkingPrefix(forkId) {
|
|
61
|
+
return joinStorageKey(this.compositionForkRoot(forkId), "working");
|
|
62
|
+
}
|
|
63
|
+
compositionForkWorkingKey(forkId, filename) {
|
|
64
|
+
return joinStorageKey(this.compositionForkWorkingPrefix(forkId), filename);
|
|
65
|
+
}
|
|
66
|
+
compositionForkVersionPrefix(forkId, version) {
|
|
67
|
+
return joinStorageKey(this.compositionForkRoot(forkId), "versions", String(version));
|
|
68
|
+
}
|
|
69
|
+
compositionForkVersionKey(forkId, version, filename) {
|
|
70
|
+
return joinStorageKey(this.compositionForkVersionPrefix(forkId, version), filename);
|
|
71
|
+
}
|
|
72
|
+
submittedVideoPrefix(videoId) {
|
|
73
|
+
return joinStorageKey("videos", videoId);
|
|
74
|
+
}
|
|
75
|
+
submittedVideoKey(videoId, filename) {
|
|
76
|
+
return joinStorageKey(this.submittedVideoPrefix(videoId), filename);
|
|
77
|
+
}
|
|
78
|
+
async putJson(key, value) {
|
|
79
|
+
const body = JSON.stringify(value, null, 2);
|
|
80
|
+
return this.putBuffer(key, Buffer.from(body, "utf8"), "application/json");
|
|
81
|
+
}
|
|
82
|
+
async putText(key, value, contentType = "text/plain; charset=utf-8") {
|
|
83
|
+
return this.putBuffer(key, Buffer.from(value, "utf8"), contentType);
|
|
84
|
+
}
|
|
85
|
+
// Uploads to S3 (or the local dev filesystem) and returns the key plus a
|
|
86
|
+
// public URL when the key is in a public-policy prefix. The old
|
|
87
|
+
// `publicRead` argument was a lie — bucket-level policy decides what's
|
|
88
|
+
// public, not per-call flags — so it's been removed.
|
|
89
|
+
async putBuffer(key, value, contentType = "application/octet-stream") {
|
|
90
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
91
|
+
await this.s3.send(new PutObjectCommand({
|
|
92
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
93
|
+
Key: key,
|
|
94
|
+
Body: value,
|
|
95
|
+
ContentType: contentType
|
|
96
|
+
}));
|
|
97
|
+
return { key, url: this.getPublicUrl(key) };
|
|
98
|
+
}
|
|
99
|
+
const filePath = this.resolveLocalPath(key);
|
|
100
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
101
|
+
writeFileSync(filePath, value);
|
|
102
|
+
return { key, url: this.getPublicUrl(key) };
|
|
103
|
+
}
|
|
104
|
+
// Returns a directly-fetchable URL for a storage key when the key is
|
|
105
|
+
// stored under a prefix the bucket resource policy grants public read to.
|
|
106
|
+
// Callers get a real S3 URL that browsers/CDNs/GhostCut/etc. can hit
|
|
107
|
+
// without any Lambda proxy hop. Returns null for keys under private
|
|
108
|
+
// prefixes — callers who need to serve those must do access-checked
|
|
109
|
+
// reads through their own endpoint.
|
|
110
|
+
getPublicUrl(key) {
|
|
111
|
+
let normalizedKey;
|
|
112
|
+
try {
|
|
113
|
+
normalizedKey = normalizeStorageKey(key);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
119
|
+
if (!isPublicStorageKey(normalizedKey)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const encodedKey = normalizedKey.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
123
|
+
if (config.AWS_S3_ENDPOINT) {
|
|
124
|
+
return `${config.AWS_S3_ENDPOINT.replace(/\/$/, "")}/${config.AWS_S3_BUCKET}/${encodedKey}`;
|
|
125
|
+
}
|
|
126
|
+
return `https://${config.AWS_S3_BUCKET}.s3.${config.AWS_REGION}.amazonaws.com/${encodedKey}`;
|
|
127
|
+
}
|
|
128
|
+
return `${config.PUBLIC_BASE_URL}/storage/${encodeURIComponent(normalizedKey)}`;
|
|
129
|
+
}
|
|
130
|
+
readLocalJson(key) {
|
|
131
|
+
return JSON.parse(readFileSync(this.resolveLocalPath(key), "utf8"));
|
|
132
|
+
}
|
|
133
|
+
readLocalObject(key) {
|
|
134
|
+
const filePath = this.resolveLocalPath(key);
|
|
135
|
+
const body = readFileSync(filePath);
|
|
136
|
+
return {
|
|
137
|
+
body,
|
|
138
|
+
contentType: inferContentType(filePath)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async getReadUrl(key) {
|
|
142
|
+
return this.createReadUrl(key);
|
|
143
|
+
}
|
|
144
|
+
async createWriteUrl(key, input) {
|
|
145
|
+
if (!this.s3 || !config.AWS_S3_BUCKET) {
|
|
146
|
+
throw new Error("Presigned uploads require S3 storage.");
|
|
147
|
+
}
|
|
148
|
+
const expiresIn = input.expiresIn ?? 3600;
|
|
149
|
+
const command = new PutObjectCommand({
|
|
150
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
151
|
+
Key: key,
|
|
152
|
+
ContentType: input.contentType
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
url: await getSignedUrl(this.s3, command, { expiresIn }),
|
|
156
|
+
method: "PUT",
|
|
157
|
+
headers: {
|
|
158
|
+
"content-type": input.contentType
|
|
159
|
+
},
|
|
160
|
+
expiresIn
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async readText(key) {
|
|
164
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
165
|
+
try {
|
|
166
|
+
const response = await this.s3.send(new GetObjectCommand({
|
|
167
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
168
|
+
Key: key
|
|
169
|
+
}));
|
|
170
|
+
if (!response.Body) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return response.Body.transformToString();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (isS3NotFoundError(error))
|
|
177
|
+
return null;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
return readFileSync(this.resolveLocalPath(key), "utf8");
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error?.code === "ENOENT")
|
|
186
|
+
return null;
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async readBuffer(key) {
|
|
191
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
192
|
+
try {
|
|
193
|
+
const response = await this.s3.send(new GetObjectCommand({
|
|
194
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
195
|
+
Key: key
|
|
196
|
+
}));
|
|
197
|
+
if (!response.Body) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return Buffer.from(await response.Body.transformToByteArray());
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (isS3NotFoundError(error))
|
|
204
|
+
return null;
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return readFileSync(this.resolveLocalPath(key));
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error?.code === "ENOENT")
|
|
213
|
+
return null;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async listKeys(prefix) {
|
|
218
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
219
|
+
const keys = [];
|
|
220
|
+
let continuationToken;
|
|
221
|
+
do {
|
|
222
|
+
const response = await this.s3.send(new ListObjectsV2Command({
|
|
223
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
224
|
+
Prefix: prefix,
|
|
225
|
+
ContinuationToken: continuationToken
|
|
226
|
+
}));
|
|
227
|
+
for (const object of response.Contents ?? []) {
|
|
228
|
+
if (object.Key) {
|
|
229
|
+
keys.push(object.Key);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
233
|
+
} while (continuationToken);
|
|
234
|
+
return keys;
|
|
235
|
+
}
|
|
236
|
+
const root = path.join(this.localRoot, prefix);
|
|
237
|
+
const keys = [];
|
|
238
|
+
walkLocalTree(root, (filePath) => {
|
|
239
|
+
keys.push(path.relative(this.localRoot, filePath).split(path.sep).join("/"));
|
|
240
|
+
});
|
|
241
|
+
return keys;
|
|
242
|
+
}
|
|
243
|
+
async objectExists(key) {
|
|
244
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
245
|
+
try {
|
|
246
|
+
await this.s3.send(new HeadObjectCommand({
|
|
247
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
248
|
+
Key: key
|
|
249
|
+
}));
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
readFileSync(this.resolveLocalPath(key));
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async deleteObject(key) {
|
|
265
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
266
|
+
await this.s3.send(new DeleteObjectCommand({
|
|
267
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
268
|
+
Key: key
|
|
269
|
+
}));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
rmSync(this.resolveLocalPath(key), { force: true });
|
|
273
|
+
}
|
|
274
|
+
resolveLocalPath(key) {
|
|
275
|
+
const normalizedKey = normalizeStorageKey(key);
|
|
276
|
+
const root = path.resolve(this.localRoot);
|
|
277
|
+
const filePath = path.resolve(root, normalizedKey);
|
|
278
|
+
if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) {
|
|
279
|
+
throw new Error("Invalid storage key.");
|
|
280
|
+
}
|
|
281
|
+
return filePath;
|
|
282
|
+
}
|
|
283
|
+
async createReadUrl(key) {
|
|
284
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
285
|
+
const command = new GetObjectCommand({
|
|
286
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
287
|
+
Key: key
|
|
288
|
+
});
|
|
289
|
+
return getSignedUrl(this.s3, command, { expiresIn: 86400 });
|
|
290
|
+
}
|
|
291
|
+
return this.getPublicUrl(key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export function joinStorageKey(...parts) {
|
|
295
|
+
return parts
|
|
296
|
+
.flatMap((part) => part.split("/"))
|
|
297
|
+
.map((part) => part.trim())
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join("/");
|
|
300
|
+
}
|
|
301
|
+
function normalizeStorageKey(key) {
|
|
302
|
+
const normalized = key.replace(/^\/+/, "");
|
|
303
|
+
if (!normalized ||
|
|
304
|
+
normalized.includes("\\") ||
|
|
305
|
+
path.isAbsolute(normalized) ||
|
|
306
|
+
normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
|
|
307
|
+
throw new Error("Invalid storage key.");
|
|
308
|
+
}
|
|
309
|
+
return normalized;
|
|
310
|
+
}
|
|
311
|
+
// MUST match the bucket resource policy in the CDK stacks. Adding a prefix
|
|
312
|
+
// here without also granting it public read in the S3 bucket policy will
|
|
313
|
+
// produce 403s for anonymous readers. Kept alphabetized for grepability.
|
|
314
|
+
function isPublicStorageKey(key) {
|
|
315
|
+
return [
|
|
316
|
+
"assets/channels/",
|
|
317
|
+
"compositions/forks/",
|
|
318
|
+
"developer/",
|
|
319
|
+
"primitives/",
|
|
320
|
+
"templates/",
|
|
321
|
+
"user/",
|
|
322
|
+
"users/"
|
|
323
|
+
].some((prefix) => key.startsWith(prefix));
|
|
324
|
+
}
|
|
325
|
+
function inferContentType(filePath) {
|
|
326
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
327
|
+
case ".json":
|
|
328
|
+
return "application/json";
|
|
329
|
+
case ".png":
|
|
330
|
+
return "image/png";
|
|
331
|
+
case ".gif":
|
|
332
|
+
return "image/gif";
|
|
333
|
+
case ".jpg":
|
|
334
|
+
case ".jpeg":
|
|
335
|
+
return "image/jpeg";
|
|
336
|
+
case ".webp":
|
|
337
|
+
return "image/webp";
|
|
338
|
+
case ".svg":
|
|
339
|
+
return "image/svg+xml";
|
|
340
|
+
case ".mp4":
|
|
341
|
+
return "video/mp4";
|
|
342
|
+
case ".webm":
|
|
343
|
+
return "video/webm";
|
|
344
|
+
case ".mov":
|
|
345
|
+
return "video/quicktime";
|
|
346
|
+
case ".mp3":
|
|
347
|
+
return "audio/mpeg";
|
|
348
|
+
case ".wav":
|
|
349
|
+
return "audio/wav";
|
|
350
|
+
case ".m4a":
|
|
351
|
+
return "audio/mp4";
|
|
352
|
+
case ".ogg":
|
|
353
|
+
return "audio/ogg";
|
|
354
|
+
case ".pdf":
|
|
355
|
+
return "application/pdf";
|
|
356
|
+
case ".md":
|
|
357
|
+
return "text/markdown; charset=utf-8";
|
|
358
|
+
case ".txt":
|
|
359
|
+
return "text/plain; charset=utf-8";
|
|
360
|
+
default:
|
|
361
|
+
return "application/octet-stream";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function walkLocalTree(root, onFile) {
|
|
365
|
+
try {
|
|
366
|
+
const entries = readdirSync(root, { withFileTypes: true });
|
|
367
|
+
for (const entry of entries) {
|
|
368
|
+
const childPath = path.join(root, entry.name);
|
|
369
|
+
if (entry.isDirectory()) {
|
|
370
|
+
walkLocalTree(childPath, onFile);
|
|
371
|
+
}
|
|
372
|
+
else if (entry.isFile()) {
|
|
373
|
+
onFile(childPath);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error.code !== "ENOENT") {
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=storage.js.map
|