@snakechat/commskit-node 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/adapters-8ztjEYtz.d.ts +83 -0
  4. package/dist/call.d.ts +34 -0
  5. package/dist/call.js +51 -0
  6. package/dist/call.js.map +1 -0
  7. package/dist/chat.d.ts +64 -0
  8. package/dist/chat.js +30 -0
  9. package/dist/chat.js.map +1 -0
  10. package/dist/chunk-5BJ2SNM5.js +26 -0
  11. package/dist/chunk-5BJ2SNM5.js.map +1 -0
  12. package/dist/chunk-6I27CI32.js +586 -0
  13. package/dist/chunk-6I27CI32.js.map +1 -0
  14. package/dist/chunk-BBFVG7QU.js +52 -0
  15. package/dist/chunk-BBFVG7QU.js.map +1 -0
  16. package/dist/chunk-GR4PMGCS.js +82 -0
  17. package/dist/chunk-GR4PMGCS.js.map +1 -0
  18. package/dist/chunk-KL4WRZKZ.js +799 -0
  19. package/dist/chunk-KL4WRZKZ.js.map +1 -0
  20. package/dist/chunk-KLKRK6OG.js +107 -0
  21. package/dist/chunk-KLKRK6OG.js.map +1 -0
  22. package/dist/chunk-LY2GJWN7.js +708 -0
  23. package/dist/chunk-LY2GJWN7.js.map +1 -0
  24. package/dist/chunk-SEDRF62Q.js +39 -0
  25. package/dist/chunk-SEDRF62Q.js.map +1 -0
  26. package/dist/chunk-U4Z5INIW.js +1192 -0
  27. package/dist/chunk-U4Z5INIW.js.map +1 -0
  28. package/dist/chunk-XOXZZYSW.js +247 -0
  29. package/dist/chunk-XOXZZYSW.js.map +1 -0
  30. package/dist/chunk-XQ6FLRX7.js +179 -0
  31. package/dist/chunk-XQ6FLRX7.js.map +1 -0
  32. package/dist/chunk-XQFQN73U.js +38 -0
  33. package/dist/chunk-XQFQN73U.js.map +1 -0
  34. package/dist/connections-CGEPiZPF.d.ts +128 -0
  35. package/dist/core.d.ts +26 -0
  36. package/dist/core.js +89 -0
  37. package/dist/core.js.map +1 -0
  38. package/dist/devices-oLIB_M19.d.ts +97 -0
  39. package/dist/express.d.ts +19 -0
  40. package/dist/express.js +34 -0
  41. package/dist/express.js.map +1 -0
  42. package/dist/fastify.d.ts +22 -0
  43. package/dist/fastify.js +42 -0
  44. package/dist/fastify.js.map +1 -0
  45. package/dist/feature-B2bEXLZG.d.ts +233 -0
  46. package/dist/feature-DQUbnYE-.d.ts +284 -0
  47. package/dist/feature-DwKkuOBh.d.ts +195 -0
  48. package/dist/hub-CPrWmIvB.d.ts +174 -0
  49. package/dist/index.d.ts +33 -0
  50. package/dist/index.js +138 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/integration.d.ts +97 -0
  53. package/dist/integration.js +151 -0
  54. package/dist/integration.js.map +1 -0
  55. package/dist/models-D1lehXIu.d.ts +31 -0
  56. package/dist/models-DTXEX1Q8.d.ts +34 -0
  57. package/dist/moderation.d.ts +9 -0
  58. package/dist/moderation.js +48 -0
  59. package/dist/moderation.js.map +1 -0
  60. package/dist/push.d.ts +198 -0
  61. package/dist/push.js +475 -0
  62. package/dist/push.js.map +1 -0
  63. package/dist/redis.d.ts +103 -0
  64. package/dist/redis.js +296 -0
  65. package/dist/redis.js.map +1 -0
  66. package/dist/s3.d.ts +75 -0
  67. package/dist/s3.js +315 -0
  68. package/dist/s3.js.map +1 -0
  69. package/dist/util-B32rIBVA.d.ts +22 -0
  70. package/package.json +148 -6
package/dist/s3.js ADDED
@@ -0,0 +1,315 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-5BJ2SNM5.js";
4
+
5
+ // src/core/s3.ts
6
+ import { randomBytes } from "crypto";
7
+ import path2 from "path";
8
+
9
+ // src/core/mediaTools.ts
10
+ import { execFile } from "child_process";
11
+ import { existsSync, statSync } from "fs";
12
+ import { mkdtemp, rm, writeFile } from "fs/promises";
13
+ import { tmpdir } from "os";
14
+ import path from "path";
15
+ var THUMBNAIL_SIZE = [300, 300];
16
+ var DURATION_MEDIA_TYPES = /* @__PURE__ */ new Set(["audio", "voice", "video", "webm"]);
17
+ var VIDEO_MEDIA_TYPES = /* @__PURE__ */ new Set(["video", "webm"]);
18
+ var isFile = (p) => {
19
+ try {
20
+ return existsSync(p) && statSync(p).isFile();
21
+ } catch {
22
+ return false;
23
+ }
24
+ };
25
+ function findTool(name, env = process.env) {
26
+ const configured = env[`${name.toUpperCase()}_PATH`];
27
+ if (configured && isFile(configured)) return configured;
28
+ const exts = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
29
+ for (const dir of (env.PATH ?? env.Path ?? "").split(path.delimiter)) {
30
+ if (!dir) continue;
31
+ for (const ext of exts) {
32
+ const candidate = path.join(dir, name + ext);
33
+ if (isFile(candidate)) return candidate;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ async function loadSharp() {
39
+ const name = "sharp";
40
+ try {
41
+ const mod = await import(
42
+ /* @vite-ignore */
43
+ name
44
+ );
45
+ return mod.default ?? mod;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ async function makeImageThumbnail(data, size = THUMBNAIL_SIZE, quality = 80) {
51
+ const sharp = await loadSharp();
52
+ if (!sharp) {
53
+ logger.warn("sharp is not installed (npm install sharp); no image thumbnail");
54
+ return null;
55
+ }
56
+ try {
57
+ return await sharp(data).rotate().resize(size[0], size[1], { fit: "inside", withoutEnlargement: true }).flatten({ background: "#ffffff" }).jpeg({ quality }).toBuffer();
58
+ } catch (err) {
59
+ logger.warn(`Image thumbnail failed: ${err?.message ?? err}`);
60
+ return null;
61
+ }
62
+ }
63
+ async function withTempFile(data, suffix, fn) {
64
+ const dir = await mkdtemp(path.join(tmpdir(), "commskit-media-"));
65
+ try {
66
+ const file = path.join(dir, `input${suffix || ".bin"}`);
67
+ await writeFile(file, data);
68
+ return await fn(file);
69
+ } finally {
70
+ await rm(dir, { recursive: true, force: true }).catch(() => void 0);
71
+ }
72
+ }
73
+ function run(cmd, args, timeoutMs) {
74
+ return new Promise((resolve, reject) => {
75
+ execFile(cmd, args, { timeout: timeoutMs, encoding: "buffer", maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
76
+ if (err && err.killed) return reject(err);
77
+ if (err && typeof err.code !== "number") return reject(err);
78
+ resolve({ code: err ? err.code : 0, stdout });
79
+ });
80
+ });
81
+ }
82
+ async function probeDuration(data, suffix = "", timeoutSeconds = 20) {
83
+ const ffprobe = findTool("ffprobe");
84
+ if (!ffprobe) return null;
85
+ try {
86
+ return await withTempFile(data, suffix, async (file) => {
87
+ const r = await run(ffprobe, ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file], timeoutSeconds * 1e3);
88
+ const first = r.stdout.toString("utf8").trim().split(/\r?\n/)[0];
89
+ const seconds = first ? Number(first) : NaN;
90
+ return Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds) : null;
91
+ });
92
+ } catch (err) {
93
+ logger.warn(`ffprobe failed: ${err?.message ?? err}`);
94
+ return null;
95
+ }
96
+ }
97
+ async function makeVideoThumbnail(data, suffix = ".mp4", width = THUMBNAIL_SIZE[0], timeoutSeconds = 30) {
98
+ const ffmpeg = findTool("ffmpeg");
99
+ if (!ffmpeg) return null;
100
+ try {
101
+ return await withTempFile(data, suffix, async (file) => {
102
+ for (const seek of ["1", "0"]) {
103
+ const r = await run(ffmpeg, ["-v", "error", "-ss", seek, "-i", file, "-frames:v", "1", "-vf", `scale=${Math.trunc(width)}:-2`, "-f", "image2", "-c:v", "mjpeg", "pipe:1"], timeoutSeconds * 1e3);
104
+ if (r.code === 0 && r.stdout.length) return r.stdout;
105
+ }
106
+ return null;
107
+ });
108
+ } catch (err) {
109
+ logger.warn(`ffmpeg thumbnail failed: ${err?.message ?? err}`);
110
+ return null;
111
+ }
112
+ }
113
+
114
+ // src/core/s3.ts
115
+ var PRESIGN_MAX_SECONDS = 7 * 24 * 3600;
116
+ var MIME = {
117
+ ".jpg": "image/jpeg",
118
+ ".jpeg": "image/jpeg",
119
+ ".png": "image/png",
120
+ ".gif": "image/gif",
121
+ ".webp": "image/webp",
122
+ ".bmp": "image/bmp",
123
+ ".mp4": "video/mp4",
124
+ ".avi": "video/x-msvideo",
125
+ ".mov": "video/quicktime",
126
+ ".mkv": "video/x-matroska",
127
+ ".3gp": "video/3gpp",
128
+ ".webm": "video/webm",
129
+ ".mp3": "audio/mpeg",
130
+ ".wav": "audio/x-wav",
131
+ ".m4a": "audio/mp4",
132
+ ".aac": "audio/aac",
133
+ ".ogg": "audio/ogg",
134
+ ".opus": "audio/opus",
135
+ ".pdf": "application/pdf",
136
+ ".doc": "application/msword",
137
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
138
+ ".xls": "application/vnd.ms-excel",
139
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
140
+ ".ppt": "application/vnd.ms-powerpoint",
141
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
142
+ ".txt": "text/plain",
143
+ ".zip": "application/zip",
144
+ ".rar": "application/vnd.rar"
145
+ };
146
+ function safeName(fileName) {
147
+ const base = path2.posix.basename(fileName.replace(/\\/g, "/")).trim();
148
+ const name = base.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._]+|[._]+$/g, "") || "file";
149
+ return name.slice(-120);
150
+ }
151
+ function thumbnailKey(key) {
152
+ const last = key.slice(key.lastIndexOf("/") + 1);
153
+ const base = last.includes(".") ? key.slice(0, key.lastIndexOf(".")) : key;
154
+ return `${base}_thumb.jpg`;
155
+ }
156
+ function awsOperations(region) {
157
+ let ops = null;
158
+ return async () => {
159
+ if (ops) return ops;
160
+ let sdk;
161
+ try {
162
+ const name = "@aws-sdk/client-s3";
163
+ sdk = await import(
164
+ /* @vite-ignore */
165
+ name
166
+ );
167
+ } catch {
168
+ throw new Error("S3Storage needs @aws-sdk/client-s3: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner");
169
+ }
170
+ const s3 = new sdk.S3Client(region ? { region } : {});
171
+ ops = {
172
+ putObject: (p) => s3.send(new sdk.PutObjectCommand(p)),
173
+ deleteObject: (p) => s3.send(new sdk.DeleteObjectCommand(p)),
174
+ presignGet: async (p, expiresIn) => {
175
+ let presigner;
176
+ try {
177
+ const name = "@aws-sdk/s3-request-presigner";
178
+ presigner = await import(
179
+ /* @vite-ignore */
180
+ name
181
+ );
182
+ } catch {
183
+ throw new Error("Presigned URLs need @aws-sdk/s3-request-presigner");
184
+ }
185
+ return presigner.getSignedUrl(s3, new sdk.GetObjectCommand(p), { expiresIn });
186
+ }
187
+ };
188
+ return ops;
189
+ };
190
+ }
191
+ var S3Storage = class {
192
+ bucket;
193
+ region;
194
+ prefix;
195
+ publicBaseUrl;
196
+ private;
197
+ presignExpires;
198
+ thumbnails;
199
+ durations;
200
+ extraPutArgs;
201
+ injected;
202
+ lazyOps;
203
+ constructor(opts) {
204
+ if (!opts.bucket) throw new Error("S3Storage needs a bucket");
205
+ const expires = opts.presignExpires ?? PRESIGN_MAX_SECONDS;
206
+ if (!(expires >= 1 && expires <= PRESIGN_MAX_SECONDS)) throw new Error("presignExpires must be between 1 s and 7 days");
207
+ this.bucket = opts.bucket;
208
+ this.region = opts.region;
209
+ const p = (opts.prefix ?? "").replace(/^\/+|\/+$/g, "");
210
+ this.prefix = p ? `${p}/` : "";
211
+ this.publicBaseUrl = opts.publicBaseUrl ? opts.publicBaseUrl.replace(/\/+$/, "") : opts.region ? `https://${opts.bucket}.s3.${opts.region}.amazonaws.com` : `https://${opts.bucket}.s3.amazonaws.com`;
212
+ this.private = opts.private ?? false;
213
+ this.presignExpires = expires;
214
+ this.thumbnails = opts.thumbnails ?? true;
215
+ this.durations = opts.durations ?? true;
216
+ this.extraPutArgs = { ...opts.extraPutArgs ?? {} };
217
+ this.injected = opts.client;
218
+ this.lazyOps = awsOperations(opts.region);
219
+ }
220
+ ops() {
221
+ return this.injected ? Promise.resolve(this.injected) : this.lazyOps();
222
+ }
223
+ // ------------------------------------------------------------------ keys
224
+ makeKey(chatId, mediaType, fileName) {
225
+ return `${this.prefix}chat/${chatId}/${mediaType}/${Math.floor(Date.now() / 1e3)}_${randomBytes(4).toString("hex")}_${safeName(fileName)}`;
226
+ }
227
+ async urlFor(key) {
228
+ if (this.private) return (await this.ops()).presignGet({ Bucket: this.bucket, Key: key }, this.presignExpires);
229
+ return `${this.publicBaseUrl}/${key.split("/").map(encodeURIComponent).join("/")}`;
230
+ }
231
+ /** Object key of a URL this storage produced (public or presigned); null otherwise. */
232
+ keyFor(fileUrl) {
233
+ if (!fileUrl) return null;
234
+ const base = `${this.publicBaseUrl}/`;
235
+ const plain = fileUrl.split("?", 1)[0];
236
+ let key;
237
+ try {
238
+ if (plain.startsWith(base)) key = decodeURIComponent(plain.slice(base.length));
239
+ else {
240
+ const u = new URL(plain);
241
+ const p = decodeURIComponent(u.pathname.replace(/^\/+/, ""));
242
+ if (u.hostname.toLowerCase().startsWith(`${this.bucket.toLowerCase()}.`)) key = p;
243
+ else if (p.startsWith(`${this.bucket}/`)) key = p.slice(this.bucket.length + 1);
244
+ else return null;
245
+ }
246
+ } catch {
247
+ return null;
248
+ }
249
+ if (!key.startsWith(`${this.prefix}chat/`) || key.split("/").includes("..")) return null;
250
+ return key;
251
+ }
252
+ /** Fresh presigned GET URL for a stored file; null for URLs this storage did not produce. */
253
+ async presign(fileUrl, expires) {
254
+ const key = this.keyFor(fileUrl);
255
+ if (key === null) return null;
256
+ return (await this.ops()).presignGet({ Bucket: this.bucket, Key: key }, Math.min(expires || this.presignExpires, PRESIGN_MAX_SECONDS));
257
+ }
258
+ // ---------------------------------------------------------------- upload
259
+ async put(key, body, contentType) {
260
+ await (await this.ops()).putObject({ Bucket: this.bucket, Key: key, Body: body, ContentType: contentType, ...this.extraPutArgs });
261
+ }
262
+ async upload(chatId, fileName, data, mediaType) {
263
+ const suffix = path2.posix.extname(fileName.replace(/\\/g, "/")).toLowerCase();
264
+ const contentType = MIME[suffix] ?? "application/octet-stream";
265
+ const key = this.makeKey(chatId, mediaType, fileName);
266
+ await this.put(key, data, contentType);
267
+ const info = {
268
+ file_url: await this.urlFor(key),
269
+ file_name: fileName,
270
+ file_size: data.length,
271
+ file_type: contentType,
272
+ media_type: mediaType
273
+ };
274
+ let thumb = null;
275
+ if (this.thumbnails && mediaType === "image") thumb = await makeImageThumbnail(data);
276
+ else if (this.thumbnails && VIDEO_MEDIA_TYPES.has(mediaType)) thumb = await makeVideoThumbnail(data, suffix || ".mp4");
277
+ if (thumb) {
278
+ try {
279
+ const tkey = thumbnailKey(key);
280
+ await this.put(tkey, thumb, "image/jpeg");
281
+ info.thumbnail_url = await this.urlFor(tkey);
282
+ } catch (err) {
283
+ logger.error(`Thumbnail upload failed for ${key}`, err);
284
+ }
285
+ }
286
+ if (this.durations && DURATION_MEDIA_TYPES.has(mediaType)) info.duration = await probeDuration(data, suffix);
287
+ return info;
288
+ }
289
+ // ---------------------------------------------------------------- delete
290
+ /** Delete the object (and its thumbnail). Unknown URLs are ignored; S3 deletes are idempotent. */
291
+ async delete(fileUrl) {
292
+ const key = this.keyFor(fileUrl);
293
+ if (key === null) return;
294
+ const ops = await this.ops();
295
+ await ops.deleteObject({ Bucket: this.bucket, Key: key });
296
+ const rel = key.slice(this.prefix.length).split("/");
297
+ const mediaType = rel.length >= 4 ? rel[2] : "";
298
+ if ((mediaType === "image" || VIDEO_MEDIA_TYPES.has(mediaType)) && !key.endsWith("_thumb.jpg")) {
299
+ await ops.deleteObject({ Bucket: this.bucket, Key: thumbnailKey(key) });
300
+ }
301
+ }
302
+ };
303
+ export {
304
+ DURATION_MEDIA_TYPES,
305
+ PRESIGN_MAX_SECONDS,
306
+ S3Storage,
307
+ THUMBNAIL_SIZE,
308
+ VIDEO_MEDIA_TYPES,
309
+ findTool,
310
+ makeImageThumbnail,
311
+ makeVideoThumbnail,
312
+ probeDuration,
313
+ thumbnailKey
314
+ };
315
+ //# sourceMappingURL=s3.js.map
package/dist/s3.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/s3.ts","../src/core/mediaTools.ts"],"sourcesContent":["/**\n * `S3Storage`: a reference `MediaStorage` on Amazon S3 (port of commskit.core.s3). Needs `@aws-sdk/client-s3`\n * (and `@aws-sdk/s3-request-presigner` for presigned URLs), both optional and imported lazily, unless you inject `client`.\n *\n * Key layout: `{prefix}chat/{chat_id}/{media_type}/{unix_ts}_{uuid8}_{name}`; thumbnails sit next to the file as\n * `..._thumb.jpg`. Images get a sharp thumbnail, videos an ffmpeg one (when installed, see mediaTools), audio / voice /\n * video a duration (ffprobe). `private: true` stores presigned GET URLs (`presignExpires`, at most 7 days) instead of\n * public URLs; refresh old ones with `storage.presign(fileUrl)`.\n *\n * new ChatFeature({ storage: new S3Storage({ bucket: \"my-bucket\", region: \"ap-south-1\" }) })\n */\nimport { randomBytes } from \"node:crypto\";\nimport path from \"node:path\";\nimport type { MediaInfo, MediaStorage } from \"./adapters.js\";\nimport { logger } from \"./logger.js\";\nimport { DURATION_MEDIA_TYPES, VIDEO_MEDIA_TYPES, makeImageThumbnail, makeVideoThumbnail, probeDuration } from \"./mediaTools.js\";\n\nexport const PRESIGN_MAX_SECONDS = 7 * 24 * 3600; // SigV4 ceiling\n\n/** The S3 operations the storage needs; the default implementation wraps @aws-sdk/client-s3 (inject a fake in tests). */\nexport interface S3Operations {\n putObject(params: { Bucket: string; Key: string; Body: Buffer; ContentType: string; [k: string]: unknown }): Promise<unknown>;\n deleteObject(params: { Bucket: string; Key: string }): Promise<unknown>;\n presignGet(params: { Bucket: string; Key: string }, expiresIn: number): Promise<string>;\n}\n\nconst MIME: Record<string, string> = {\n \".jpg\": \"image/jpeg\", \".jpeg\": \"image/jpeg\", \".png\": \"image/png\", \".gif\": \"image/gif\", \".webp\": \"image/webp\",\n \".bmp\": \"image/bmp\", \".mp4\": \"video/mp4\", \".avi\": \"video/x-msvideo\", \".mov\": \"video/quicktime\",\n \".mkv\": \"video/x-matroska\", \".3gp\": \"video/3gpp\", \".webm\": \"video/webm\", \".mp3\": \"audio/mpeg\",\n \".wav\": \"audio/x-wav\", \".m4a\": \"audio/mp4\", \".aac\": \"audio/aac\", \".ogg\": \"audio/ogg\", \".opus\": \"audio/opus\",\n \".pdf\": \"application/pdf\", \".doc\": \"application/msword\",\n \".docx\": \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \".xls\": \"application/vnd.ms-excel\",\n \".xlsx\": \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \".ppt\": \"application/vnd.ms-powerpoint\",\n \".pptx\": \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \".txt\": \"text/plain\", \".zip\": \"application/zip\", \".rar\": \"application/vnd.rar\",\n};\n\nfunction safeName(fileName: string): string {\n const base = path.posix.basename(fileName.replace(/\\\\/g, \"/\")).trim();\n const name = base.replace(/[^A-Za-z0-9._-]+/g, \"_\").replace(/^[._]+|[._]+$/g, \"\") || \"file\";\n return name.slice(-120);\n}\n\nexport function thumbnailKey(key: string): string {\n const last = key.slice(key.lastIndexOf(\"/\") + 1);\n const base = last.includes(\".\") ? key.slice(0, key.lastIndexOf(\".\")) : key;\n return `${base}_thumb.jpg`;\n}\n\nexport interface S3StorageOptions {\n bucket: string;\n region?: string;\n /** An S3Operations implementation (default: built lazily from @aws-sdk/client-s3). */\n client?: S3Operations;\n prefix?: string;\n publicBaseUrl?: string;\n private?: boolean;\n presignExpires?: number;\n thumbnails?: boolean;\n durations?: boolean;\n /** Extra PutObject parameters (e.g. ServerSideEncryption, CacheControl). */\n extraPutArgs?: Record<string, unknown>;\n}\n\n/** Lazily build S3Operations on top of the AWS SDK v3. */\nfunction awsOperations(region: string | undefined): () => Promise<S3Operations> {\n let ops: S3Operations | null = null;\n return async () => {\n if (ops) return ops;\n let sdk: any;\n try {\n const name = \"@aws-sdk/client-s3\";\n sdk = await import(/* @vite-ignore */ name);\n } catch {\n throw new Error(\"S3Storage needs @aws-sdk/client-s3: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\");\n }\n const s3 = new sdk.S3Client(region ? { region } : {});\n ops = {\n putObject: (p) => s3.send(new sdk.PutObjectCommand(p)),\n deleteObject: (p) => s3.send(new sdk.DeleteObjectCommand(p)),\n presignGet: async (p, expiresIn) => {\n let presigner: any;\n try {\n const name = \"@aws-sdk/s3-request-presigner\";\n presigner = await import(/* @vite-ignore */ name);\n } catch {\n throw new Error(\"Presigned URLs need @aws-sdk/s3-request-presigner\");\n }\n return presigner.getSignedUrl(s3, new sdk.GetObjectCommand(p), { expiresIn });\n },\n };\n return ops;\n };\n}\n\nexport class S3Storage implements MediaStorage {\n readonly bucket: string;\n readonly region?: string;\n readonly prefix: string;\n readonly publicBaseUrl: string;\n readonly private: boolean;\n readonly presignExpires: number;\n readonly thumbnails: boolean;\n readonly durations: boolean;\n readonly extraPutArgs: Record<string, unknown>;\n private injected?: S3Operations;\n private lazyOps: () => Promise<S3Operations>;\n\n constructor(opts: S3StorageOptions) {\n if (!opts.bucket) throw new Error(\"S3Storage needs a bucket\");\n const expires = opts.presignExpires ?? PRESIGN_MAX_SECONDS;\n if (!(expires >= 1 && expires <= PRESIGN_MAX_SECONDS)) throw new Error(\"presignExpires must be between 1 s and 7 days\");\n this.bucket = opts.bucket;\n this.region = opts.region;\n const p = (opts.prefix ?? \"\").replace(/^\\/+|\\/+$/g, \"\");\n this.prefix = p ? `${p}/` : \"\";\n this.publicBaseUrl = opts.publicBaseUrl\n ? opts.publicBaseUrl.replace(/\\/+$/, \"\")\n : opts.region\n ? `https://${opts.bucket}.s3.${opts.region}.amazonaws.com`\n : `https://${opts.bucket}.s3.amazonaws.com`;\n this.private = opts.private ?? false;\n this.presignExpires = expires;\n this.thumbnails = opts.thumbnails ?? true;\n this.durations = opts.durations ?? true;\n this.extraPutArgs = { ...(opts.extraPutArgs ?? {}) };\n this.injected = opts.client;\n this.lazyOps = awsOperations(opts.region);\n }\n\n private ops(): Promise<S3Operations> {\n return this.injected ? Promise.resolve(this.injected) : this.lazyOps();\n }\n\n // ------------------------------------------------------------------ keys\n\n makeKey(chatId: number, mediaType: string, fileName: string): string {\n return `${this.prefix}chat/${chatId}/${mediaType}/${Math.floor(Date.now() / 1000)}_${randomBytes(4).toString(\"hex\")}_${safeName(fileName)}`;\n }\n\n async urlFor(key: string): Promise<string> {\n if (this.private) return (await this.ops()).presignGet({ Bucket: this.bucket, Key: key }, this.presignExpires);\n return `${this.publicBaseUrl}/${key.split(\"/\").map(encodeURIComponent).join(\"/\")}`;\n }\n\n /** Object key of a URL this storage produced (public or presigned); null otherwise. */\n keyFor(fileUrl: string): string | null {\n if (!fileUrl) return null;\n const base = `${this.publicBaseUrl}/`;\n const plain = fileUrl.split(\"?\", 1)[0]!;\n let key: string;\n try {\n if (plain.startsWith(base)) key = decodeURIComponent(plain.slice(base.length));\n else {\n const u = new URL(plain);\n const p = decodeURIComponent(u.pathname.replace(/^\\/+/, \"\"));\n if (u.hostname.toLowerCase().startsWith(`${this.bucket.toLowerCase()}.`)) key = p; // virtual-hosted style\n else if (p.startsWith(`${this.bucket}/`)) key = p.slice(this.bucket.length + 1); // path style\n else return null;\n }\n } catch {\n return null;\n }\n if (!key.startsWith(`${this.prefix}chat/`) || key.split(\"/\").includes(\"..\")) return null;\n return key;\n }\n\n /** Fresh presigned GET URL for a stored file; null for URLs this storage did not produce. */\n async presign(fileUrl: string, expires?: number): Promise<string | null> {\n const key = this.keyFor(fileUrl);\n if (key === null) return null;\n return (await this.ops()).presignGet({ Bucket: this.bucket, Key: key }, Math.min(expires || this.presignExpires, PRESIGN_MAX_SECONDS));\n }\n\n // ---------------------------------------------------------------- upload\n\n private async put(key: string, body: Buffer, contentType: string): Promise<void> {\n await (await this.ops()).putObject({ Bucket: this.bucket, Key: key, Body: body, ContentType: contentType, ...this.extraPutArgs });\n }\n\n async upload(chatId: number, fileName: string, data: Buffer, mediaType: string): Promise<MediaInfo> {\n const suffix = path.posix.extname(fileName.replace(/\\\\/g, \"/\")).toLowerCase();\n const contentType = MIME[suffix] ?? \"application/octet-stream\";\n const key = this.makeKey(chatId, mediaType, fileName);\n await this.put(key, data, contentType);\n const info: MediaInfo = {\n file_url: await this.urlFor(key),\n file_name: fileName,\n file_size: data.length,\n file_type: contentType,\n media_type: mediaType,\n };\n let thumb: Buffer | null = null;\n if (this.thumbnails && mediaType === \"image\") thumb = await makeImageThumbnail(data);\n else if (this.thumbnails && VIDEO_MEDIA_TYPES.has(mediaType)) thumb = await makeVideoThumbnail(data, suffix || \".mp4\");\n if (thumb) {\n try {\n const tkey = thumbnailKey(key);\n await this.put(tkey, thumb, \"image/jpeg\");\n info.thumbnail_url = await this.urlFor(tkey);\n } catch (err) {\n logger.error(`Thumbnail upload failed for ${key}`, err);\n }\n }\n if (this.durations && DURATION_MEDIA_TYPES.has(mediaType)) info.duration = await probeDuration(data, suffix);\n return info;\n }\n\n // ---------------------------------------------------------------- delete\n\n /** Delete the object (and its thumbnail). Unknown URLs are ignored; S3 deletes are idempotent. */\n async delete(fileUrl: string): Promise<void> {\n const key = this.keyFor(fileUrl);\n if (key === null) return;\n const ops = await this.ops();\n await ops.deleteObject({ Bucket: this.bucket, Key: key });\n const rel = key.slice(this.prefix.length).split(\"/\");\n const mediaType = rel.length >= 4 ? rel[2]! : \"\";\n if ((mediaType === \"image\" || VIDEO_MEDIA_TYPES.has(mediaType)) && !key.endsWith(\"_thumb.jpg\")) {\n await ops.deleteObject({ Bucket: this.bucket, Key: thumbnailKey(key) });\n }\n }\n}\n","/**\n * Optional media helpers (port of commskit.core.media_tools): image / video thumbnails and audio / video duration.\n *\n * - `makeImageThumbnail` needs `sharp` (optional peer dependency, imported lazily).\n * - `probeDuration` / `makeVideoThumbnail` run `ffprobe` / `ffmpeg` when they are on PATH (or at `FFPROBE_PATH` / `FFMPEG_PATH`).\n *\n * All helpers are async (work runs off the event loop) and resolve to `null` when the tool is missing or the\n * input can't be read, so an upload never fails because of a thumbnail.\n */\nimport { execFile } from \"node:child_process\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { logger } from \"./logger.js\";\n\nexport const THUMBNAIL_SIZE: readonly [number, number] = [300, 300];\nexport const DURATION_MEDIA_TYPES: ReadonlySet<string> = new Set([\"audio\", \"voice\", \"video\", \"webm\"]);\nexport const VIDEO_MEDIA_TYPES: ReadonlySet<string> = new Set([\"video\", \"webm\"]);\n\nconst isFile = (p: string): boolean => {\n try {\n return existsSync(p) && statSync(p).isFile();\n } catch {\n return false;\n }\n};\n\n/** `<NAME>_PATH` env var if it points at a file, else the first match on PATH; null when not found. */\nexport function findTool(name: string, env: NodeJS.ProcessEnv = process.env): string | null {\n const configured = env[`${name.toUpperCase()}_PATH`];\n if (configured && isFile(configured)) return configured;\n const exts = process.platform === \"win32\" ? [\"\", ...(env.PATHEXT ?? \".EXE;.CMD;.BAT\").split(\";\")] : [\"\"];\n for (const dir of (env.PATH ?? env.Path ?? \"\").split(path.delimiter)) {\n if (!dir) continue;\n for (const ext of exts) {\n const candidate = path.join(dir, name + ext);\n if (isFile(candidate)) return candidate;\n }\n }\n return null;\n}\n\nasync function loadSharp(): Promise<any | null> {\n const name = \"sharp\"; // variable specifier: optional, no compile-time dependency\n try {\n const mod: any = await import(/* @vite-ignore */ name);\n return mod.default ?? mod;\n } catch {\n return null;\n }\n}\n\n/** JPEG thumbnail that fits in `size` (aspect kept, never enlarged); null without sharp or for a bad image. */\nexport async function makeImageThumbnail(data: Buffer, size: readonly [number, number] = THUMBNAIL_SIZE, quality = 80): Promise<Buffer | null> {\n const sharp = await loadSharp();\n if (!sharp) {\n logger.warn(\"sharp is not installed (npm install sharp); no image thumbnail\");\n return null;\n }\n try {\n return await sharp(data)\n .rotate()\n .resize(size[0], size[1], { fit: \"inside\", withoutEnlargement: true })\n .flatten({ background: \"#ffffff\" })\n .jpeg({ quality })\n .toBuffer();\n } catch (err) {\n logger.warn(`Image thumbnail failed: ${(err as Error)?.message ?? err}`);\n return null;\n }\n}\n\nasync function withTempFile<T>(data: Buffer, suffix: string, fn: (file: string) => Promise<T>): Promise<T> {\n const dir = await mkdtemp(path.join(tmpdir(), \"commskit-media-\"));\n try {\n const file = path.join(dir, `input${suffix || \".bin\"}`);\n await writeFile(file, data);\n return await fn(file);\n } finally {\n await rm(dir, { recursive: true, force: true }).catch(() => undefined);\n }\n}\n\nfunction run(cmd: string, args: string[], timeoutMs: number): Promise<{ code: number; stdout: Buffer }> {\n return new Promise((resolve, reject) => {\n execFile(cmd, args, { timeout: timeoutMs, encoding: \"buffer\", maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {\n if (err && (err as any).killed) return reject(err);\n if (err && typeof (err as any).code !== \"number\") return reject(err);\n resolve({ code: err ? ((err as any).code as number) : 0, stdout: stdout as Buffer });\n });\n });\n}\n\n/** Duration in whole seconds via `ffprobe`; null when ffprobe is missing or fails. */\nexport async function probeDuration(data: Buffer, suffix = \"\", timeoutSeconds = 20): Promise<number | null> {\n const ffprobe = findTool(\"ffprobe\");\n if (!ffprobe) return null;\n try {\n return await withTempFile(data, suffix, async (file) => {\n const r = await run(ffprobe, [\"-v\", \"error\", \"-show_entries\", \"format=duration\", \"-of\", \"default=noprint_wrappers=1:nokey=1\", file], timeoutSeconds * 1000);\n const first = r.stdout.toString(\"utf8\").trim().split(/\\r?\\n/)[0];\n const seconds = first ? Number(first) : NaN;\n return Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds) : null;\n });\n } catch (err) {\n logger.warn(`ffprobe failed: ${(err as Error)?.message ?? err}`);\n return null;\n }\n}\n\n/** JPEG of a frame near the start via `ffmpeg`; null when ffmpeg is missing or fails. */\nexport async function makeVideoThumbnail(data: Buffer, suffix = \".mp4\", width = THUMBNAIL_SIZE[0], timeoutSeconds = 30): Promise<Buffer | null> {\n const ffmpeg = findTool(\"ffmpeg\");\n if (!ffmpeg) return null;\n try {\n return await withTempFile(data, suffix, async (file) => {\n for (const seek of [\"1\", \"0\"]) {\n // very short clips have no frame at 1 s\n const r = await run(ffmpeg, [\"-v\", \"error\", \"-ss\", seek, \"-i\", file, \"-frames:v\", \"1\", \"-vf\", `scale=${Math.trunc(width)}:-2`, \"-f\", \"image2\", \"-c:v\", \"mjpeg\", \"pipe:1\"], timeoutSeconds * 1000);\n if (r.code === 0 && r.stdout.length) return r.stdout;\n }\n return null;\n });\n } catch (err) {\n logger.warn(`ffmpeg thumbnail failed: ${(err as Error)?.message ?? err}`);\n return null;\n }\n}\n"],"mappings":"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","names":["path","path"]}
@@ -0,0 +1,22 @@
1
+ /** Python-style naive UTC isoformat: no offset suffix, fraction only when non-zero (6 digits). */
2
+ declare function formatIso(d: Date): string;
3
+ declare function utcnow(): Date;
4
+ declare function nowIso(): string;
5
+ /**
6
+ * ISO string for a value read from the database: a Date (pg), or a string
7
+ * ("YYYY-MM-DD HH:MM:SS[.ffffff]" from sqlite, or an ISO string). Mirrors Python's `iso()`.
8
+ */
9
+ declare function iso(value: unknown): string | null;
10
+ /** Bounded map; oldest entries are evicted first. */
11
+ declare class LRU<K, V> {
12
+ private limit;
13
+ private data;
14
+ constructor(limit: number);
15
+ get(key: K): V | undefined;
16
+ put(key: K, value: V): void;
17
+ pop(key: K): V | undefined;
18
+ clear(): void;
19
+ discardWhere(predicate: (key: K) => boolean): void;
20
+ }
21
+
22
+ export { LRU as L, formatIso as f, iso as i, nowIso as n, utcnow as u };
package/package.json CHANGED
@@ -1,6 +1,148 @@
1
- {
2
- "name": "@snakechat/commskit-node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@snakechat/commskit-node",
3
+ "version": "0.1.0",
4
+ "description": "Node.js/TypeScript server for the commskit wire protocol: chat, calls, moderation, push. Sibling of the Python `commskit` backend.",
5
+ "keywords": [
6
+ "chat",
7
+ "voice-call",
8
+ "video-call",
9
+ "webrtc",
10
+ "websocket",
11
+ "realtime",
12
+ "signaling",
13
+ "fastify",
14
+ "express",
15
+ "commskit"
16
+ ],
17
+ "type": "module",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/prateek0128/chat-call-module.git",
22
+ "directory": "backend/backend-node"
23
+ },
24
+ "homepage": "https://github.com/prateek0128/chat-call-module/tree/main/backend/backend-node#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/prateek0128/chat-call-module/issues"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js"
40
+ },
41
+ "./core": {
42
+ "types": "./dist/core.d.ts",
43
+ "import": "./dist/core.js"
44
+ },
45
+ "./chat": {
46
+ "types": "./dist/chat.d.ts",
47
+ "import": "./dist/chat.js"
48
+ },
49
+ "./call": {
50
+ "types": "./dist/call.d.ts",
51
+ "import": "./dist/call.js"
52
+ },
53
+ "./moderation": {
54
+ "types": "./dist/moderation.d.ts",
55
+ "import": "./dist/moderation.js"
56
+ },
57
+ "./integration": {
58
+ "types": "./dist/integration.d.ts",
59
+ "import": "./dist/integration.js"
60
+ },
61
+ "./push": {
62
+ "types": "./dist/push.d.ts",
63
+ "import": "./dist/push.js"
64
+ },
65
+ "./redis": {
66
+ "types": "./dist/redis.d.ts",
67
+ "import": "./dist/redis.js"
68
+ },
69
+ "./s3": {
70
+ "types": "./dist/s3.d.ts",
71
+ "import": "./dist/s3.js"
72
+ },
73
+ "./fastify": {
74
+ "types": "./dist/fastify.d.ts",
75
+ "import": "./dist/fastify.js"
76
+ },
77
+ "./express": {
78
+ "types": "./dist/express.d.ts",
79
+ "import": "./dist/express.js"
80
+ },
81
+ "./package.json": "./package.json"
82
+ },
83
+ "files": [
84
+ "dist",
85
+ "LICENSE"
86
+ ],
87
+ "scripts": {
88
+ "build": "tsup",
89
+ "test": "vitest run",
90
+ "typecheck": "tsc --noEmit",
91
+ "smoke": "node scripts/dist-smoke.mjs"
92
+ },
93
+ "dependencies": {
94
+ "jose": "^5.9.6",
95
+ "kysely": "^0.27.4",
96
+ "ws": "^8.18.0"
97
+ },
98
+ "peerDependencies": {
99
+ "better-sqlite3": "^11.0.0",
100
+ "express": "^4.19.0 || ^5.0.0",
101
+ "fastify": "^4.28.0 || ^5.0.0",
102
+ "pg": "^8.11.0",
103
+ "@aws-sdk/client-s3": "^3.600.0",
104
+ "@aws-sdk/s3-request-presigner": "^3.600.0",
105
+ "ioredis": "^5.4.0",
106
+ "sharp": "^0.33.0"
107
+ },
108
+ "peerDependenciesMeta": {
109
+ "better-sqlite3": {
110
+ "optional": true
111
+ },
112
+ "express": {
113
+ "optional": true
114
+ },
115
+ "fastify": {
116
+ "optional": true
117
+ },
118
+ "pg": {
119
+ "optional": true
120
+ },
121
+ "@aws-sdk/client-s3": {
122
+ "optional": true
123
+ },
124
+ "@aws-sdk/s3-request-presigner": {
125
+ "optional": true
126
+ },
127
+ "ioredis": {
128
+ "optional": true
129
+ },
130
+ "sharp": {
131
+ "optional": true
132
+ }
133
+ },
134
+ "devDependencies": {
135
+ "@types/better-sqlite3": "^7.6.11",
136
+ "@types/express": "^4.17.21",
137
+ "@types/node": "^20.14.0",
138
+ "@types/pg": "^8.11.10",
139
+ "@types/ws": "^8.5.12",
140
+ "better-sqlite3": "^11.5.0",
141
+ "express": "^4.21.0",
142
+ "fastify": "^4.28.1",
143
+ "pg": "^8.13.0",
144
+ "tsup": "^8.3.0",
145
+ "typescript": "^5.6.0",
146
+ "vitest": "^2.1.0"
147
+ }
148
+ }