@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.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/adapters-8ztjEYtz.d.ts +83 -0
- package/dist/call.d.ts +34 -0
- package/dist/call.js +51 -0
- package/dist/call.js.map +1 -0
- package/dist/chat.d.ts +64 -0
- package/dist/chat.js +30 -0
- package/dist/chat.js.map +1 -0
- package/dist/chunk-5BJ2SNM5.js +26 -0
- package/dist/chunk-5BJ2SNM5.js.map +1 -0
- package/dist/chunk-6I27CI32.js +586 -0
- package/dist/chunk-6I27CI32.js.map +1 -0
- package/dist/chunk-BBFVG7QU.js +52 -0
- package/dist/chunk-BBFVG7QU.js.map +1 -0
- package/dist/chunk-GR4PMGCS.js +82 -0
- package/dist/chunk-GR4PMGCS.js.map +1 -0
- package/dist/chunk-KL4WRZKZ.js +799 -0
- package/dist/chunk-KL4WRZKZ.js.map +1 -0
- package/dist/chunk-KLKRK6OG.js +107 -0
- package/dist/chunk-KLKRK6OG.js.map +1 -0
- package/dist/chunk-LY2GJWN7.js +708 -0
- package/dist/chunk-LY2GJWN7.js.map +1 -0
- package/dist/chunk-SEDRF62Q.js +39 -0
- package/dist/chunk-SEDRF62Q.js.map +1 -0
- package/dist/chunk-U4Z5INIW.js +1192 -0
- package/dist/chunk-U4Z5INIW.js.map +1 -0
- package/dist/chunk-XOXZZYSW.js +247 -0
- package/dist/chunk-XOXZZYSW.js.map +1 -0
- package/dist/chunk-XQ6FLRX7.js +179 -0
- package/dist/chunk-XQ6FLRX7.js.map +1 -0
- package/dist/chunk-XQFQN73U.js +38 -0
- package/dist/chunk-XQFQN73U.js.map +1 -0
- package/dist/connections-CGEPiZPF.d.ts +128 -0
- package/dist/core.d.ts +26 -0
- package/dist/core.js +89 -0
- package/dist/core.js.map +1 -0
- package/dist/devices-oLIB_M19.d.ts +97 -0
- package/dist/express.d.ts +19 -0
- package/dist/express.js +34 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.d.ts +22 -0
- package/dist/fastify.js +42 -0
- package/dist/fastify.js.map +1 -0
- package/dist/feature-B2bEXLZG.d.ts +233 -0
- package/dist/feature-DQUbnYE-.d.ts +284 -0
- package/dist/feature-DwKkuOBh.d.ts +195 -0
- package/dist/hub-CPrWmIvB.d.ts +174 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +97 -0
- package/dist/integration.js +151 -0
- package/dist/integration.js.map +1 -0
- package/dist/models-D1lehXIu.d.ts +31 -0
- package/dist/models-DTXEX1Q8.d.ts +34 -0
- package/dist/moderation.d.ts +9 -0
- package/dist/moderation.js +48 -0
- package/dist/moderation.js.map +1 -0
- package/dist/push.d.ts +198 -0
- package/dist/push.js +475 -0
- package/dist/push.js.map +1 -0
- package/dist/redis.d.ts +103 -0
- package/dist/redis.js +296 -0
- package/dist/redis.js.map +1 -0
- package/dist/s3.d.ts +75 -0
- package/dist/s3.js +315 -0
- package/dist/s3.js.map +1 -0
- package/dist/util-B32rIBVA.d.ts +22 -0
- package/package.json +148 -6
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-5BJ2SNM5.js";
|
|
4
|
+
|
|
5
|
+
// src/core/adapters.ts
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import fs from "fs/promises";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { jwtVerify } from "jose";
|
|
10
|
+
var AuthError = class extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "AuthError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var JwtAuth = class {
|
|
17
|
+
key;
|
|
18
|
+
algorithm;
|
|
19
|
+
claim;
|
|
20
|
+
constructor(secret, options = {}) {
|
|
21
|
+
if (!secret) throw new Error("JwtAuth needs a non-empty secret");
|
|
22
|
+
const opts = typeof options === "string" ? { algorithm: options } : options;
|
|
23
|
+
this.key = new TextEncoder().encode(secret);
|
|
24
|
+
this.algorithm = opts.algorithm ?? "HS256";
|
|
25
|
+
this.claim = opts.claim ?? "uid";
|
|
26
|
+
}
|
|
27
|
+
async userIdFromToken(token) {
|
|
28
|
+
let payload;
|
|
29
|
+
try {
|
|
30
|
+
({ payload } = await jwtVerify(token, this.key, { algorithms: [this.algorithm] }));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new AuthError(err instanceof Error ? err.message : String(err));
|
|
33
|
+
}
|
|
34
|
+
const uid = payload[this.claim];
|
|
35
|
+
if (uid === null || uid === void 0 || uid === "") throw new AuthError(`Token has no '${this.claim}' claim`);
|
|
36
|
+
return String(uid);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
async function resolveUserId(auth, token) {
|
|
40
|
+
return String(await auth.userIdFromToken(token));
|
|
41
|
+
}
|
|
42
|
+
var NullUserDirectory = class {
|
|
43
|
+
async getProfiles() {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var StaticUserDirectory = class {
|
|
48
|
+
profiles;
|
|
49
|
+
constructor(profiles = {}) {
|
|
50
|
+
this.profiles = { ...profiles };
|
|
51
|
+
}
|
|
52
|
+
async getProfiles(userIds) {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const id of userIds) if (Object.hasOwn(this.profiles, id)) out[id] = this.profiles[id];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var NoopPush = class {
|
|
59
|
+
async send() {
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var UploadRejected = class extends Error {
|
|
63
|
+
};
|
|
64
|
+
async function deleteMedia(storage, ...fileUrls) {
|
|
65
|
+
const del = storage?.delete;
|
|
66
|
+
if (typeof del !== "function") return 0;
|
|
67
|
+
let done = 0;
|
|
68
|
+
for (const url of new Set(fileUrls.filter((u) => !!u))) {
|
|
69
|
+
try {
|
|
70
|
+
await del.call(storage, url);
|
|
71
|
+
done += 1;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error(`Deleting media ${url} failed`, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return done;
|
|
77
|
+
}
|
|
78
|
+
var MIME = {
|
|
79
|
+
".jpg": "image/jpeg",
|
|
80
|
+
".jpeg": "image/jpeg",
|
|
81
|
+
".png": "image/png",
|
|
82
|
+
".gif": "image/gif",
|
|
83
|
+
".webp": "image/webp",
|
|
84
|
+
".bmp": "image/bmp",
|
|
85
|
+
".mp4": "video/mp4",
|
|
86
|
+
".avi": "video/x-msvideo",
|
|
87
|
+
".mov": "video/quicktime",
|
|
88
|
+
".mkv": "video/x-matroska",
|
|
89
|
+
".3gp": "video/3gpp",
|
|
90
|
+
".webm": "video/webm",
|
|
91
|
+
".mp3": "audio/mpeg",
|
|
92
|
+
".wav": "audio/x-wav",
|
|
93
|
+
".m4a": "audio/mp4",
|
|
94
|
+
".aac": "audio/aac",
|
|
95
|
+
".ogg": "audio/ogg",
|
|
96
|
+
".opus": "audio/opus",
|
|
97
|
+
".pdf": "application/pdf",
|
|
98
|
+
".doc": "application/msword",
|
|
99
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
100
|
+
".xls": "application/vnd.ms-excel",
|
|
101
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
102
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
103
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
104
|
+
".txt": "text/plain",
|
|
105
|
+
".zip": "application/zip",
|
|
106
|
+
".rar": "application/vnd.rar"
|
|
107
|
+
};
|
|
108
|
+
var LocalDiskStorage = class {
|
|
109
|
+
root;
|
|
110
|
+
baseUrl;
|
|
111
|
+
constructor(root = "comms_media", baseUrl = "/media") {
|
|
112
|
+
this.root = root;
|
|
113
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
114
|
+
}
|
|
115
|
+
async upload(chatId, fileName, data, mediaType) {
|
|
116
|
+
const safeName = path.basename(fileName.replace(/\\/g, "/")).replace(/ /g, "_") || "file";
|
|
117
|
+
const rel = `chat/${chatId}/${mediaType}/${randomBytes(6).toString("hex")}_${safeName}`;
|
|
118
|
+
const file = path.join(this.root, ...rel.split("/"));
|
|
119
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
120
|
+
await fs.writeFile(file, data);
|
|
121
|
+
return {
|
|
122
|
+
file_url: `${this.baseUrl}/${rel}`,
|
|
123
|
+
file_name: fileName,
|
|
124
|
+
file_size: data.length,
|
|
125
|
+
file_type: MIME[path.extname(fileName).toLowerCase()] ?? "application/octet-stream",
|
|
126
|
+
media_type: mediaType
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Local path of a URL this storage produced; null for anything else (incl. `..` tricks). */
|
|
130
|
+
pathFor(fileUrl) {
|
|
131
|
+
const prefix = `${this.baseUrl}/`;
|
|
132
|
+
const url = fileUrl.split("?", 1)[0];
|
|
133
|
+
if (!url.startsWith(prefix)) return null;
|
|
134
|
+
const rel = url.slice(prefix.length);
|
|
135
|
+
const parts = rel.split("/");
|
|
136
|
+
if (!rel || parts.some((p) => p === "" || p === "." || p === "..")) return null;
|
|
137
|
+
const root = path.resolve(this.root);
|
|
138
|
+
const file = path.resolve(root, ...parts);
|
|
139
|
+
return file.startsWith(root + path.sep) ? file : null;
|
|
140
|
+
}
|
|
141
|
+
async delete(fileUrl) {
|
|
142
|
+
const file = this.pathFor(fileUrl);
|
|
143
|
+
if (file === null) return;
|
|
144
|
+
await fs.rm(file, { force: true });
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/core/media.ts
|
|
149
|
+
import path2 from "path";
|
|
150
|
+
var MEDIA_EXTENSIONS = {
|
|
151
|
+
image: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"],
|
|
152
|
+
video: [".mp4", ".avi", ".mov", ".mkv", ".3gp"],
|
|
153
|
+
audio: [".mp3", ".wav", ".m4a", ".aac", ".ogg", ".opus"],
|
|
154
|
+
voice: [".mp3", ".wav", ".m4a", ".aac", ".ogg", ".opus"],
|
|
155
|
+
document: [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", ".zip", ".rar"],
|
|
156
|
+
webm: [".webm"]
|
|
157
|
+
};
|
|
158
|
+
var MEDIA_TYPES = new Set(Object.keys(MEDIA_EXTENSIONS));
|
|
159
|
+
function detectMediaType(fileName, requested) {
|
|
160
|
+
if (typeof requested === "string" && MEDIA_TYPES.has(requested)) return requested;
|
|
161
|
+
const ext = path2.extname(fileName.toLowerCase());
|
|
162
|
+
for (const [type, exts] of Object.entries(MEDIA_EXTENSIONS)) if (exts.includes(ext)) return type;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
AuthError,
|
|
168
|
+
JwtAuth,
|
|
169
|
+
resolveUserId,
|
|
170
|
+
NullUserDirectory,
|
|
171
|
+
StaticUserDirectory,
|
|
172
|
+
NoopPush,
|
|
173
|
+
UploadRejected,
|
|
174
|
+
deleteMedia,
|
|
175
|
+
LocalDiskStorage,
|
|
176
|
+
MEDIA_TYPES,
|
|
177
|
+
detectMediaType
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=chunk-XQ6FLRX7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/adapters.ts","../src/core/media.ts"],"sourcesContent":["/**\n * Interfaces the host app implements, with safe defaults (port of commskit.core.adapters).\n * Every adapter is a plain interface: any object with matching methods works.\n */\nimport { randomBytes } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { jwtVerify } from \"jose\";\nimport { logger } from \"./logger.js\";\n\n// --------------------------------------------------------------------- auth\n\n/** Token is missing, invalid or expired. */\nexport class AuthError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AuthError\";\n }\n}\n\nexport interface AuthAdapter {\n /** Return the user id for `token` or throw (any exception = invalid). */\n userIdFromToken(token: string): string | Promise<string>;\n}\n\n/** HS256 (by default) JWT auth reading the user id from `claim`. */\nexport class JwtAuth implements AuthAdapter {\n private key: Uint8Array;\n private algorithm: string;\n private claim: string;\n\n constructor(secret: string, options: { algorithm?: string; claim?: string } | string = {}) {\n if (!secret) throw new Error(\"JwtAuth needs a non-empty secret\");\n const opts = typeof options === \"string\" ? { algorithm: options } : options;\n this.key = new TextEncoder().encode(secret);\n this.algorithm = opts.algorithm ?? \"HS256\";\n this.claim = opts.claim ?? \"uid\";\n }\n\n async userIdFromToken(token: string): Promise<string> {\n let payload: Record<string, unknown>;\n try {\n ({ payload } = await jwtVerify(token, this.key, { algorithms: [this.algorithm] }));\n } catch (err) {\n throw new AuthError(err instanceof Error ? err.message : String(err));\n }\n const uid = payload[this.claim];\n if (uid === null || uid === undefined || uid === \"\") throw new AuthError(`Token has no '${this.claim}' claim`);\n return String(uid);\n }\n}\n\n/** Call the adapter, supporting both sync and async implementations. */\nexport async function resolveUserId(auth: AuthAdapter, token: string): Promise<string> {\n return String(await auth.userIdFromToken(token));\n}\n\n// ---------------------------------------------------------------- directory\n\nexport interface UserProfile {\n name?: string | null;\n avatar?: string | null;\n}\n\nexport interface UserDirectory {\n /** Display info for the ids it knows; unknown ids may be omitted. */\n getProfiles(userIds: readonly string[]): Promise<Record<string, UserProfile>>;\n}\n\n/** Knows nobody: names fall back to user ids / \"Someone\". */\nexport class NullUserDirectory implements UserDirectory {\n async getProfiles(): Promise<Record<string, UserProfile>> {\n return {};\n }\n}\n\n/** In-memory directory (tests, demos). */\nexport class StaticUserDirectory implements UserDirectory {\n profiles: Record<string, UserProfile>;\n constructor(profiles: Record<string, UserProfile> = {}) {\n this.profiles = { ...profiles };\n }\n async getProfiles(userIds: readonly string[]): Promise<Record<string, UserProfile>> {\n const out: Record<string, UserProfile> = {};\n for (const id of userIds) if (Object.hasOwn(this.profiles, id)) out[id] = this.profiles[id]!;\n return out;\n }\n}\n\n// --------------------------------------------------------------------- push\n\nexport interface PushSender {\n /** Push to every device of `userId`. Must not throw for \"no devices\". */\n send(\n userId: string,\n title: string,\n body: string,\n data: Record<string, unknown>,\n channel?: string,\n ttl?: number | null,\n ): Promise<void>;\n}\n\nexport class NoopPush implements PushSender {\n async send(): Promise<void> {}\n}\n\n/** Optional: backs the `/chats/expo/push/token` routes (`SqlDeviceRegistry` is the SQL implementation). */\nexport interface DeviceRegistry {\n register(userId: string, payload: Record<string, unknown>): Promise<unknown>;\n remove(userId: string, payload: Record<string, unknown>): Promise<unknown>;\n}\n\n// -------------------------------------------------------------------- media\n\nexport interface MediaInfo {\n file_url: string;\n file_name: string;\n file_size: number;\n file_type: string;\n media_type: string;\n thumbnail_url?: string | null;\n duration?: number | null;\n}\n\nexport interface MediaStorage {\n /** Store the file and return where it lives. Throw `UploadRejected` to reject it with a message. */\n upload(chatId: number, fileName: string, data: Buffer, mediaType: string): Promise<MediaInfo>;\n /** Optional: remove a stored file (unsend, user deletion). Must not throw for files already gone. */\n delete?(fileUrl: string): Promise<void>;\n}\n\n/** Throw from `MediaStorage.upload` to refuse a file; the message goes to the client (Python: ValueError). */\nexport class UploadRejected extends Error {}\n\n/** Delete files through `storage.delete` when the storage has it. Never throws; returns how many succeeded. */\nexport async function deleteMedia(storage: unknown, ...fileUrls: (string | null | undefined)[]): Promise<number> {\n const del = (storage as MediaStorage | undefined)?.delete;\n if (typeof del !== \"function\") return 0;\n let done = 0;\n for (const url of new Set(fileUrls.filter((u): u is string => !!u))) {\n try {\n await del.call(storage, url);\n done += 1;\n } catch (err) {\n logger.error(`Deleting media ${url} failed`, err);\n }\n }\n return done;\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\n/** Writes files under `root/chat/<chatId>/<mediaType>/`; `file_url` is `baseUrl` + relative path (serve `root` yourself). */\nexport class LocalDiskStorage implements MediaStorage {\n root: string;\n baseUrl: string;\n\n constructor(root = \"comms_media\", baseUrl = \"/media\") {\n this.root = root;\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n }\n\n async upload(chatId: number, fileName: string, data: Buffer, mediaType: string): Promise<MediaInfo> {\n const safeName = path.basename(fileName.replace(/\\\\/g, \"/\")).replace(/ /g, \"_\") || \"file\";\n const rel = `chat/${chatId}/${mediaType}/${randomBytes(6).toString(\"hex\")}_${safeName}`;\n const file = path.join(this.root, ...rel.split(\"/\"));\n await fs.mkdir(path.dirname(file), { recursive: true });\n await fs.writeFile(file, data);\n return {\n file_url: `${this.baseUrl}/${rel}`,\n file_name: fileName,\n file_size: data.length,\n file_type: MIME[path.extname(fileName).toLowerCase()] ?? \"application/octet-stream\",\n media_type: mediaType,\n };\n }\n\n /** Local path of a URL this storage produced; null for anything else (incl. `..` tricks). */\n pathFor(fileUrl: string): string | null {\n const prefix = `${this.baseUrl}/`;\n const url = fileUrl.split(\"?\", 1)[0]!;\n if (!url.startsWith(prefix)) return null;\n const rel = url.slice(prefix.length);\n const parts = rel.split(\"/\");\n if (!rel || parts.some((p) => p === \"\" || p === \".\" || p === \"..\")) return null;\n const root = path.resolve(this.root);\n const file = path.resolve(root, ...parts);\n return file.startsWith(root + path.sep) ? file : null;\n }\n\n async delete(fileUrl: string): Promise<void> {\n const file = this.pathFor(fileUrl);\n if (file === null) return;\n await fs.rm(file, { force: true });\n }\n}\n","/** Media type detection by file extension (port of commskit.core.media). */\nimport path from \"node:path\";\n\n// Order matters: the first type whose extensions match wins (\"voice\" shares the audio extensions,\n// so it is only chosen when the client asks for it).\nexport const MEDIA_EXTENSIONS: Record<string, readonly string[]> = {\n image: [\".jpg\", \".jpeg\", \".png\", \".gif\", \".webp\", \".bmp\"],\n video: [\".mp4\", \".avi\", \".mov\", \".mkv\", \".3gp\"],\n audio: [\".mp3\", \".wav\", \".m4a\", \".aac\", \".ogg\", \".opus\"],\n voice: [\".mp3\", \".wav\", \".m4a\", \".aac\", \".ogg\", \".opus\"],\n document: [\".pdf\", \".doc\", \".docx\", \".xls\", \".xlsx\", \".ppt\", \".pptx\", \".txt\", \".zip\", \".rar\"],\n webm: [\".webm\"],\n};\nexport const MEDIA_TYPES: ReadonlySet<string> = new Set(Object.keys(MEDIA_EXTENSIONS));\n\nexport function detectMediaType(fileName: string, requested?: unknown): string | null {\n if (typeof requested === \"string\" && MEDIA_TYPES.has(requested)) return requested;\n const ext = path.extname(fileName.toLowerCase());\n for (const [type, exts] of Object.entries(MEDIA_EXTENSIONS)) if (exts.includes(ext)) return type;\n return null;\n}\n"],"mappings":"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","names":["path"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ddlTypes
|
|
3
|
+
} from "./chunk-GR4PMGCS.js";
|
|
4
|
+
|
|
5
|
+
// src/call/models.ts
|
|
6
|
+
function callDdl(dialect) {
|
|
7
|
+
const t = ddlTypes(dialect);
|
|
8
|
+
return [
|
|
9
|
+
`CREATE TABLE IF NOT EXISTS calls (
|
|
10
|
+
id ${t.pk},
|
|
11
|
+
call_id VARCHAR(100) NOT NULL,
|
|
12
|
+
chat_id INTEGER,
|
|
13
|
+
caller_id VARCHAR(50) NOT NULL,
|
|
14
|
+
receiver_id VARCHAR(50) NOT NULL,
|
|
15
|
+
recording_url TEXT,
|
|
16
|
+
call_status VARCHAR(20) NOT NULL,
|
|
17
|
+
is_video_call ${t.bool},
|
|
18
|
+
offer_sdp TEXT,
|
|
19
|
+
answer_sdp TEXT,
|
|
20
|
+
ice_candidates TEXT,
|
|
21
|
+
started_at ${t.ts} NOT NULL,
|
|
22
|
+
answered_at ${t.ts},
|
|
23
|
+
ended_at ${t.ts},
|
|
24
|
+
created_at ${t.tsDefaultNow},
|
|
25
|
+
updated_at ${t.ts},
|
|
26
|
+
duration INTEGER,
|
|
27
|
+
missed_at ${t.ts},
|
|
28
|
+
end_reason VARCHAR(255),
|
|
29
|
+
UNIQUE (call_id)
|
|
30
|
+
)`,
|
|
31
|
+
"CREATE INDEX IF NOT EXISTS ix_calls_id ON calls (id)"
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
callDdl
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=chunk-XQFQN73U.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/call/models.ts"],"sourcesContent":["/**\n * Call table (`calls`), same columns as the Python `commskit.call.models.Call`.\n * `chat_id` is nullable so calls work without the chat feature.\n * CallFeature / CallService / lifecycle build on this.\n */\nimport type { Generated } from \"kysely\";\nimport type { Dialect } from \"../core/db.js\";\nimport { ddlTypes } from \"../core/ddl.js\";\n\ntype Ts = string | Date;\n\nexport interface CallsTable {\n id: Generated<number>;\n call_id: string;\n chat_id: number | null;\n caller_id: string;\n receiver_id: string;\n recording_url: string | null;\n call_status: string; // ringing | answered | ended | missed | rejected\n is_video_call: boolean | number | null;\n offer_sdp: string | null;\n answer_sdp: string | null;\n ice_candidates: string | null;\n started_at: Ts;\n answered_at: Ts | null;\n ended_at: Ts | null;\n created_at: Ts | null;\n updated_at: Ts | null;\n duration: number | null;\n missed_at: Ts | null;\n end_reason: string | null;\n}\n\nexport function callDdl(dialect: Dialect): string[] {\n const t = ddlTypes(dialect);\n return [\n `CREATE TABLE IF NOT EXISTS calls (\n id ${t.pk},\n call_id VARCHAR(100) NOT NULL,\n chat_id INTEGER,\n caller_id VARCHAR(50) NOT NULL,\n receiver_id VARCHAR(50) NOT NULL,\n recording_url TEXT,\n call_status VARCHAR(20) NOT NULL,\n is_video_call ${t.bool},\n offer_sdp TEXT,\n answer_sdp TEXT,\n ice_candidates TEXT,\n started_at ${t.ts} NOT NULL,\n answered_at ${t.ts},\n ended_at ${t.ts},\n created_at ${t.tsDefaultNow},\n updated_at ${t.ts},\n duration INTEGER,\n missed_at ${t.ts},\n end_reason VARCHAR(255),\n UNIQUE (call_id)\n)`,\n \"CREATE INDEX IF NOT EXISTS ix_calls_id ON calls (id)\",\n ];\n}\n"],"mappings":";;;;;AAiCO,SAAS,QAAQ,SAA4B;AAClD,QAAM,IAAI,SAAS,OAAO;AAC1B,SAAO;AAAA,IACL;AAAA,OACG,EAAE,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAOO,EAAE,IAAI;AAAA;AAAA;AAAA;AAAA,eAIT,EAAE,EAAE;AAAA,gBACH,EAAE,EAAE;AAAA,aACP,EAAE,EAAE;AAAA,eACF,EAAE,YAAY;AAAA,eACd,EAAE,EAAE;AAAA;AAAA,cAEL,EAAE,EAAE;AAAA;AAAA;AAAA;AAAA,IAId;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
declare class RateLimit {
|
|
2
|
+
readonly count: number;
|
|
3
|
+
readonly seconds: number;
|
|
4
|
+
constructor(count: number, seconds: number);
|
|
5
|
+
get rate(): number;
|
|
6
|
+
}
|
|
7
|
+
interface Limiter {
|
|
8
|
+
/** Take `cost` tokens. Returns 0 when allowed, else seconds until it would be. */
|
|
9
|
+
hit(key: string, limit: RateLimit | null | undefined, cost?: number): Promise<number>;
|
|
10
|
+
}
|
|
11
|
+
/** Whole seconds for the wire (`retry_after`), at least 1. */
|
|
12
|
+
declare const retryAfterSeconds: (wait: number) => number;
|
|
13
|
+
declare class RateLimiter implements Limiter {
|
|
14
|
+
private clock;
|
|
15
|
+
private buckets;
|
|
16
|
+
constructor(maxKeys?: number, clock?: () => number);
|
|
17
|
+
hit(key: string, limit: RateLimit | null | undefined, cost?: number): Promise<number>;
|
|
18
|
+
hitSync(key: string, limit: RateLimit | null | undefined, cost?: number): number;
|
|
19
|
+
reset(key?: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare const USER_ROLE = "user";
|
|
23
|
+
/** The minimal socket surface the kit needs (a `ws` WebSocket satisfies it). */
|
|
24
|
+
interface SocketLike {
|
|
25
|
+
readyState: number;
|
|
26
|
+
send(data: string, cb?: (err?: Error) => void): void;
|
|
27
|
+
close(code?: number, reason?: string): void;
|
|
28
|
+
}
|
|
29
|
+
type OfflineHook = (userId: string) => Promise<void> | void;
|
|
30
|
+
type PartnersLookup = (userId: string) => Promise<Iterable<string>>;
|
|
31
|
+
type Deliver = (userId: string, envelope: any) => Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Cross-worker presence + delivery. `RedisBridge` implements this.
|
|
34
|
+
* Optional members: setRoomDeliver(fn), publishRoom(room, payload, origin),
|
|
35
|
+
* claimCameOnline(userId, ttlSeconds) -> boolean, releaseCameOnline(userId).
|
|
36
|
+
*/
|
|
37
|
+
interface PresenceBridge {
|
|
38
|
+
heartbeatSeconds: number;
|
|
39
|
+
start(deliver: Deliver, serverId: string): Promise<void>;
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
setOnline(userId: string, serverId: string): Promise<void>;
|
|
42
|
+
setOffline(userId: string, serverId: string): Promise<void>;
|
|
43
|
+
setAway(userId: string, away: boolean): Promise<void>;
|
|
44
|
+
getUserServers(userId: string): Promise<string[]>;
|
|
45
|
+
isOnline(userId: string): Promise<boolean>;
|
|
46
|
+
getOnlineUsers(userIds: string[]): Promise<Set<string>>;
|
|
47
|
+
publish(userId: string, payload: unknown, origin: string): Promise<void>;
|
|
48
|
+
setRoomDeliver?(fn: (room: string, envelope: any) => Promise<void>): void;
|
|
49
|
+
publishRoom?(room: string, payload: unknown, origin: string): Promise<void>;
|
|
50
|
+
claimCameOnline?(userId: string, ttlSeconds: number): Promise<boolean>;
|
|
51
|
+
releaseCameOnline?(userId: string): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
interface UserConnection {
|
|
54
|
+
userId: string;
|
|
55
|
+
status: string;
|
|
56
|
+
lastSeen: Date;
|
|
57
|
+
}
|
|
58
|
+
declare class ConnectionManager {
|
|
59
|
+
bridge: PresenceBridge | null;
|
|
60
|
+
serverId: string;
|
|
61
|
+
/** A user may hold several sockets (devices / screens); all get every frame. */
|
|
62
|
+
userSockets: Map<string, Set<SocketLike>>;
|
|
63
|
+
activeConnections: Map<string, UserConnection>;
|
|
64
|
+
roles: Map<string, string>;
|
|
65
|
+
rooms: Map<string, Set<string>>;
|
|
66
|
+
private userRooms;
|
|
67
|
+
/** user.came_online: 0 = only dedupe concurrent connects; > 0 = at most once per cooldown (seconds). */
|
|
68
|
+
cameOnlineCooldown: number;
|
|
69
|
+
cameOnlineDedupeSeconds: number;
|
|
70
|
+
private cameOnlineAt;
|
|
71
|
+
private offlineHooks;
|
|
72
|
+
private partnersLookup;
|
|
73
|
+
private tasks;
|
|
74
|
+
constructor(opts?: {
|
|
75
|
+
bridge?: PresenceBridge | null;
|
|
76
|
+
serverId?: string;
|
|
77
|
+
cameOnlineCooldown?: number;
|
|
78
|
+
cameOnlineDedupeSeconds?: number;
|
|
79
|
+
});
|
|
80
|
+
/** Start the bridge consumer (call at app start). */
|
|
81
|
+
start(): Promise<void>;
|
|
82
|
+
stop(): Promise<void>;
|
|
83
|
+
/** `hook(userId)` runs when the user's last socket on this worker closes (users only unless `allRoles`). */
|
|
84
|
+
addOfflineHook(hook: OfflineHook, allRoles?: boolean): void;
|
|
85
|
+
/** Who should receive this user's `user_status` (chat installs this). */
|
|
86
|
+
setPartnersLookup(lookup: PartnersLookup | null): void;
|
|
87
|
+
roleOf(userId: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Register an already-accepted socket. Returns true when this connect is the user's
|
|
90
|
+
* offline -> online transition across all workers; always false for non-user roles.
|
|
91
|
+
*/
|
|
92
|
+
register(userId: string, ws: SocketLike, role?: string): Promise<boolean>;
|
|
93
|
+
private claimCameOnline;
|
|
94
|
+
heartbeat(userId: string): Promise<void>;
|
|
95
|
+
/** Remove one socket (or all). The user stays online while any socket remains. Idempotent. */
|
|
96
|
+
disconnect(userId: string, ws?: SocketLike): void;
|
|
97
|
+
private wentOffline;
|
|
98
|
+
/** Client-reported foreground ("online") / background (anything else). */
|
|
99
|
+
setStatus(userId: string, status: string): Promise<void>;
|
|
100
|
+
/** Local only: foreground socket on this worker. */
|
|
101
|
+
isUserOnline(userId: string): boolean;
|
|
102
|
+
/** Cluster-wide: connected somewhere with the app in the foreground. */
|
|
103
|
+
isOnline(userId: string): Promise<boolean>;
|
|
104
|
+
/** Cluster-wide: any open socket (foreground or background). */
|
|
105
|
+
isConnected(userId: string): Promise<boolean>;
|
|
106
|
+
getOnlineUsers(userIds: string[]): Promise<Set<string>>;
|
|
107
|
+
private sendLocal;
|
|
108
|
+
/** Deliver to every socket of the user on any worker. True if at least one got it. */
|
|
109
|
+
send(userId: string, payload: unknown): Promise<boolean>;
|
|
110
|
+
sendToSocket(ws: SocketLike, payload: unknown): Promise<void>;
|
|
111
|
+
/** Every socket on this worker (e.g. host community events). */
|
|
112
|
+
broadcast(payload: unknown): Promise<void>;
|
|
113
|
+
private deliverFromBridge;
|
|
114
|
+
/** Add the user's sockets on this worker to `room` (left automatically on disconnect). False when none. */
|
|
115
|
+
joinRoom(userId: string, room: string): boolean;
|
|
116
|
+
leaveRoom(userId: string, room: string): void;
|
|
117
|
+
/** Members on this worker. */
|
|
118
|
+
roomMembers(room: string): Set<string>;
|
|
119
|
+
private sendRoomLocal;
|
|
120
|
+
/** Send `payload` unchanged to every member on every worker. Returns how many local members got it. */
|
|
121
|
+
broadcastRoom(room: string, payload: unknown): Promise<number>;
|
|
122
|
+
private deliverRoomFromBridge;
|
|
123
|
+
/** Presence goes only to the user's conversation partners (never for non-user roles). */
|
|
124
|
+
broadcastUserStatus(userId: string, status: string, timestamp?: Date): Promise<void>;
|
|
125
|
+
private broadcastStatus;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { ConnectionManager as C, type Deliver as D, type Limiter as L, type PresenceBridge as P, RateLimit as R, type SocketLike as S, USER_ROLE as U, RateLimiter as a, type UserConnection as b, retryAfterSeconds as r };
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { A as AuthAdapter, a as AuthError, D as DeviceRegistry, J as JwtAuth, L as LocalDiskStorage, M as MediaInfo, b as MediaStorage, N as NoopPush, c as NullUserDirectory, P as PushSender, S as StaticUserDirectory, U as UploadRejected, d as UserDirectory, e as UserProfile, f as deleteMedia, r as resolveUserId } from './adapters-8ztjEYtz.js';
|
|
2
|
+
export { C as ConnectionManager, L as Limiter, P as PresenceBridge, R as RateLimit, a as RateLimiter, S as SocketLike, U as USER_ROLE, b as UserConnection, r as retryAfterSeconds } from './connections-CGEPiZPF.js';
|
|
3
|
+
import { C as CommsHub } from './hub-CPrWmIvB.js';
|
|
4
|
+
export { b as CommsHubOptions, c as ConnectHook, D as Db, a as Dialect, d as DisconnectHook, E as EventBus, F as Feature, H as Handler, e as HandlerErrorHook, f as HttpError, g as HttpMethod, L as Listener, h as Logger, R as RestContext, i as RestResult, j as RestRoute, k as RoleResolver, l as dbBool, m as dbNow, n as dbTimestamp, o as detectDialect, p as firstQuery, q as matchPath, r as ok, s as setLogger, t as silentLogger, v as validationError } from './hub-CPrWmIvB.js';
|
|
5
|
+
export { D as DevicesTable, d as devicesDdl } from './devices-oLIB_M19.js';
|
|
6
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
7
|
+
export { f as formatIso, i as iso, n as nowIso, u as utcnow } from './util-B32rIBVA.js';
|
|
8
|
+
import 'node:stream';
|
|
9
|
+
import 'ws';
|
|
10
|
+
import 'kysely';
|
|
11
|
+
|
|
12
|
+
/** Legacy wire quirks kept for installed app versions (PROTOCOL.md section 6). */
|
|
13
|
+
declare const PONG_TYPE = "PONG";
|
|
14
|
+
|
|
15
|
+
/** Built-in `node:http` request listener for the hub's REST routes (no framework needed; used by tests/demo). */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns `(req, res) => Promise<boolean>`: true when a route matched and the response was written,
|
|
19
|
+
* false when nothing matched (the caller may serve other things or answer 404).
|
|
20
|
+
*/
|
|
21
|
+
declare function restListener(hub: CommsHub): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
22
|
+
|
|
23
|
+
declare const MEDIA_TYPES: ReadonlySet<string>;
|
|
24
|
+
declare function detectMediaType(fileName: string, requested?: unknown): string | null;
|
|
25
|
+
|
|
26
|
+
export { CommsHub, MEDIA_TYPES, PONG_TYPE, detectMediaType, restListener };
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CommsHub,
|
|
3
|
+
ConnectionManager,
|
|
4
|
+
EventBus,
|
|
5
|
+
PONG_TYPE,
|
|
6
|
+
USER_ROLE,
|
|
7
|
+
restListener
|
|
8
|
+
} from "./chunk-LY2GJWN7.js";
|
|
9
|
+
import {
|
|
10
|
+
AuthError,
|
|
11
|
+
JwtAuth,
|
|
12
|
+
LocalDiskStorage,
|
|
13
|
+
MEDIA_TYPES,
|
|
14
|
+
NoopPush,
|
|
15
|
+
NullUserDirectory,
|
|
16
|
+
StaticUserDirectory,
|
|
17
|
+
UploadRejected,
|
|
18
|
+
deleteMedia,
|
|
19
|
+
detectMediaType,
|
|
20
|
+
resolveUserId
|
|
21
|
+
} from "./chunk-XQ6FLRX7.js";
|
|
22
|
+
import {
|
|
23
|
+
devicesDdl
|
|
24
|
+
} from "./chunk-XOXZZYSW.js";
|
|
25
|
+
import {
|
|
26
|
+
HttpError,
|
|
27
|
+
dbBool,
|
|
28
|
+
dbNow,
|
|
29
|
+
dbTimestamp,
|
|
30
|
+
detectDialect,
|
|
31
|
+
firstQuery,
|
|
32
|
+
matchPath,
|
|
33
|
+
ok,
|
|
34
|
+
validationError
|
|
35
|
+
} from "./chunk-GR4PMGCS.js";
|
|
36
|
+
import {
|
|
37
|
+
RateLimit,
|
|
38
|
+
RateLimiter,
|
|
39
|
+
retryAfterSeconds
|
|
40
|
+
} from "./chunk-BBFVG7QU.js";
|
|
41
|
+
import {
|
|
42
|
+
formatIso,
|
|
43
|
+
iso,
|
|
44
|
+
nowIso,
|
|
45
|
+
utcnow
|
|
46
|
+
} from "./chunk-KLKRK6OG.js";
|
|
47
|
+
import {
|
|
48
|
+
setLogger,
|
|
49
|
+
silentLogger
|
|
50
|
+
} from "./chunk-5BJ2SNM5.js";
|
|
51
|
+
export {
|
|
52
|
+
AuthError,
|
|
53
|
+
CommsHub,
|
|
54
|
+
ConnectionManager,
|
|
55
|
+
EventBus,
|
|
56
|
+
HttpError,
|
|
57
|
+
JwtAuth,
|
|
58
|
+
LocalDiskStorage,
|
|
59
|
+
MEDIA_TYPES,
|
|
60
|
+
NoopPush,
|
|
61
|
+
NullUserDirectory,
|
|
62
|
+
PONG_TYPE,
|
|
63
|
+
RateLimit,
|
|
64
|
+
RateLimiter,
|
|
65
|
+
StaticUserDirectory,
|
|
66
|
+
USER_ROLE,
|
|
67
|
+
UploadRejected,
|
|
68
|
+
dbBool,
|
|
69
|
+
dbNow,
|
|
70
|
+
dbTimestamp,
|
|
71
|
+
deleteMedia,
|
|
72
|
+
detectDialect,
|
|
73
|
+
detectMediaType,
|
|
74
|
+
devicesDdl,
|
|
75
|
+
firstQuery,
|
|
76
|
+
formatIso,
|
|
77
|
+
iso,
|
|
78
|
+
matchPath,
|
|
79
|
+
nowIso,
|
|
80
|
+
ok,
|
|
81
|
+
resolveUserId,
|
|
82
|
+
restListener,
|
|
83
|
+
retryAfterSeconds,
|
|
84
|
+
setLogger,
|
|
85
|
+
silentLogger,
|
|
86
|
+
utcnow,
|
|
87
|
+
validationError
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=core.js.map
|
package/dist/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Generated } from 'kysely';
|
|
2
|
+
import { D as DeviceRegistry } from './adapters-8ztjEYtz.js';
|
|
3
|
+
import { a as Dialect, F as Feature, D as Db, C as CommsHub, j as RestRoute } from './hub-CPrWmIvB.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Devices: the `comms_devices` table, `SqlDeviceRegistry` and the device REST routes
|
|
7
|
+
* (port of commskit.core.devices). `SqlDeviceRegistry` implements `DeviceRegistry` (legacy routes) and
|
|
8
|
+
* `DeviceStore` (used by the push senders).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface DevicesTable {
|
|
12
|
+
id: Generated<number>;
|
|
13
|
+
user_id: string;
|
|
14
|
+
device_id: string;
|
|
15
|
+
platform: string;
|
|
16
|
+
expo_push_token: string | null;
|
|
17
|
+
voip_token: string | null;
|
|
18
|
+
app_version: string | null;
|
|
19
|
+
os_version: string | null;
|
|
20
|
+
model: string | null;
|
|
21
|
+
created_at: string | Date;
|
|
22
|
+
updated_at: string | Date;
|
|
23
|
+
last_seen_at: string | Date;
|
|
24
|
+
}
|
|
25
|
+
declare function devicesDdl(dialect: Dialect): string[];
|
|
26
|
+
declare const PLATFORMS: readonly ["ios", "android", "web"];
|
|
27
|
+
type TokenKind = "expo" | "voip";
|
|
28
|
+
/** Plain view of a device row. */
|
|
29
|
+
interface DeviceInfo {
|
|
30
|
+
user_id: string;
|
|
31
|
+
device_id: string;
|
|
32
|
+
platform: string;
|
|
33
|
+
expo_push_token: string | null;
|
|
34
|
+
voip_token: string | null;
|
|
35
|
+
app_version: string | null;
|
|
36
|
+
os_version: string | null;
|
|
37
|
+
model: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** What the push senders need from a registry. */
|
|
40
|
+
interface DeviceStore {
|
|
41
|
+
devicesFor(userId: string): Promise<DeviceInfo[]>;
|
|
42
|
+
/** Forget a token the push service reported invalid. Returns rows changed. */
|
|
43
|
+
removeToken(token: string, kind?: TokenKind): Promise<number>;
|
|
44
|
+
}
|
|
45
|
+
interface DeviceFields {
|
|
46
|
+
expo_push_token?: unknown;
|
|
47
|
+
voip_token?: unknown;
|
|
48
|
+
app_version?: unknown;
|
|
49
|
+
os_version?: unknown;
|
|
50
|
+
model?: unknown;
|
|
51
|
+
}
|
|
52
|
+
/** Devices in `comms_devices` (one row per app install, keyed by `device_id`). */
|
|
53
|
+
declare class SqlDeviceRegistry implements DeviceRegistry, DeviceStore {
|
|
54
|
+
private db;
|
|
55
|
+
readonly dialect: Dialect;
|
|
56
|
+
constructor(db: Db, dialect?: Dialect);
|
|
57
|
+
/**
|
|
58
|
+
* Create or update the device. A device that changes hands moves to `userId` (and drops tokens it did not
|
|
59
|
+
* send again). A token lives on one device only: other rows carrying it lose it. Throws Error on bad input.
|
|
60
|
+
*/
|
|
61
|
+
upsert(userId: string, deviceId: string, platform: string, fields?: DeviceFields): Promise<DeviceInfo>;
|
|
62
|
+
private upsertOnce;
|
|
63
|
+
delete(userId: string, deviceId: string): Promise<boolean>;
|
|
64
|
+
/** Forget every device of `userId` (account deletion). Returns rows deleted. */
|
|
65
|
+
deleteUser(userId: string): Promise<number>;
|
|
66
|
+
removeToken(token: string, kind?: TokenKind): Promise<number>;
|
|
67
|
+
devicesFor(userId: string): Promise<DeviceInfo[]>;
|
|
68
|
+
get(deviceId: string): Promise<DeviceInfo | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Legacy `POST /chats/expo/push/token` body. Accepted keys: `expo_push_token` | `token` | `push_token` |
|
|
71
|
+
* `expoPushToken`, `device_id` | `deviceId`, `platform` | `device_type` | `os`, and the `/devices` fields.
|
|
72
|
+
* Without a device id the token itself identifies the install (`expo:<token>`).
|
|
73
|
+
*/
|
|
74
|
+
register(userId: string, payload: Record<string, any>): Promise<Record<string, unknown>>;
|
|
75
|
+
/** Legacy `POST /chats/expo/push/token/remove`: by device id or token (own devices only). */
|
|
76
|
+
remove(userId: string, payload: Record<string, any>): Promise<Record<string, unknown>>;
|
|
77
|
+
}
|
|
78
|
+
declare function buildDevicesRoutes(registry: SqlDeviceRegistry, legacyRoutes?: boolean): RestRoute[];
|
|
79
|
+
/**
|
|
80
|
+
* Mounts the device routes on the hub (independent of chat / call). `legacyRoutes` also mounts
|
|
81
|
+
* `/chats/expo/push/token[/remove]`; ChatFeature skips its own copy when this is installed BEFORE it.
|
|
82
|
+
*/
|
|
83
|
+
declare class DevicesFeature implements Feature {
|
|
84
|
+
readonly registry: SqlDeviceRegistry;
|
|
85
|
+
private opts;
|
|
86
|
+
readonly name = "devices";
|
|
87
|
+
constructor(registry: SqlDeviceRegistry, opts?: {
|
|
88
|
+
legacyRoutes?: boolean;
|
|
89
|
+
});
|
|
90
|
+
install(hub: CommsHub): this;
|
|
91
|
+
/** Account deletion: drop all of the user's devices and tokens. */
|
|
92
|
+
deleteUserData(userId: string): Promise<{
|
|
93
|
+
devices: number;
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { type DevicesTable as D, PLATFORMS as P, SqlDeviceRegistry as S, type TokenKind as T, type DeviceStore as a, type DeviceFields as b, type DeviceInfo as c, devicesDdl as d, DevicesFeature as e, buildDevicesRoutes as f };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import { C as CommsHub } from './hub-CPrWmIvB.js';
|
|
4
|
+
import 'node:stream';
|
|
5
|
+
import 'ws';
|
|
6
|
+
import './adapters-8ztjEYtz.js';
|
|
7
|
+
import './connections-CGEPiZPF.js';
|
|
8
|
+
import 'kysely';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Express adapter: `app.use(commskitRouter(hub))` for the REST routes, `attach(server, hub)` for `/ws/:token`.
|
|
12
|
+
* `express` is an optional peer dependency.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare function commskitRouter(hub: CommsHub): Router;
|
|
16
|
+
/** Handle `/ws/:token` upgrades on the http.Server that runs the Express app. */
|
|
17
|
+
declare function attach(server: Server, hub: CommsHub): void;
|
|
18
|
+
|
|
19
|
+
export { attach, commskitRouter };
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/express/index.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
function commskitRouter(hub) {
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
router.use(express.json({ limit: "20mb" }));
|
|
6
|
+
for (const route of hub.restRoutes()) {
|
|
7
|
+
const method = route.method.toLowerCase();
|
|
8
|
+
router[method](route.path, async (req, res, next) => {
|
|
9
|
+
try {
|
|
10
|
+
const result = await route.handler({
|
|
11
|
+
method: req.method,
|
|
12
|
+
path: route.path,
|
|
13
|
+
params: req.params,
|
|
14
|
+
query: req.query,
|
|
15
|
+
headers: req.headers,
|
|
16
|
+
body: req.body && Object.keys(req.body).length === 0 && req.method === "GET" ? void 0 : req.body,
|
|
17
|
+
userId: null
|
|
18
|
+
});
|
|
19
|
+
res.status(result.status).set(result.headers ?? {}).json(result.body);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
next(err);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return router;
|
|
26
|
+
}
|
|
27
|
+
function attach(server, hub) {
|
|
28
|
+
hub.attach(server);
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
attach,
|
|
32
|
+
commskitRouter
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/express/index.ts"],"sourcesContent":["/**\n * Express adapter: `app.use(commskitRouter(hub))` for the REST routes, `attach(server, hub)` for `/ws/:token`.\n * `express` is an optional peer dependency.\n */\nimport type { Server } from \"node:http\";\nimport express, { type Router } from \"express\";\nimport type { CommsHub } from \"../core/hub.js\";\n\nexport function commskitRouter(hub: CommsHub): Router {\n const router = express.Router();\n router.use(express.json({ limit: \"20mb\" }));\n for (const route of hub.restRoutes()) {\n const method = route.method.toLowerCase() as \"get\" | \"post\" | \"put\" | \"patch\" | \"delete\";\n router[method](route.path, async (req, res, next) => {\n try {\n const result = await route.handler({\n method: req.method,\n path: route.path,\n params: req.params,\n query: req.query as Record<string, string | string[] | undefined>,\n headers: req.headers,\n body: req.body && Object.keys(req.body).length === 0 && req.method === \"GET\" ? undefined : req.body,\n userId: null,\n });\n res.status(result.status).set(result.headers ?? {}).json(result.body);\n } catch (err) {\n next(err);\n }\n });\n }\n return router;\n}\n\n/** Handle `/ws/:token` upgrades on the http.Server that runs the Express app. */\nexport function attach(server: Server, hub: CommsHub): void {\n hub.attach(server);\n}\n"],"mappings":";AAKA,OAAO,aAA8B;AAG9B,SAAS,eAAe,KAAuB;AACpD,QAAM,SAAS,QAAQ,OAAO;AAC9B,SAAO,IAAI,QAAQ,KAAK,EAAE,OAAO,OAAO,CAAC,CAAC;AAC1C,aAAW,SAAS,IAAI,WAAW,GAAG;AACpC,UAAM,SAAS,MAAM,OAAO,YAAY;AACxC,WAAO,MAAM,EAAE,MAAM,MAAM,OAAO,KAAK,KAAK,SAAS;AACnD,UAAI;AACF,cAAM,SAAS,MAAM,MAAM,QAAQ;AAAA,UACjC,QAAQ,IAAI;AAAA,UACZ,MAAM,MAAM;AAAA,UACZ,QAAQ,IAAI;AAAA,UACZ,OAAO,IAAI;AAAA,UACX,SAAS,IAAI;AAAA,UACb,MAAM,IAAI,QAAQ,OAAO,KAAK,IAAI,IAAI,EAAE,WAAW,KAAK,IAAI,WAAW,QAAQ,SAAY,IAAI;AAAA,UAC/F,QAAQ;AAAA,QACV,CAAC;AACD,YAAI,OAAO,OAAO,MAAM,EAAE,IAAI,OAAO,WAAW,CAAC,CAAC,EAAE,KAAK,OAAO,IAAI;AAAA,MACtE,SAAS,KAAK;AACZ,aAAK,GAAG;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAGO,SAAS,OAAO,QAAgB,KAAqB;AAC1D,MAAI,OAAO,MAAM;AACnB;","names":[]}
|