@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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/moderation/service.ts","../src/moderation/handlers.ts","../src/moderation/policy.ts","../src/moderation/router.ts","../src/moderation/feature.ts"],"sourcesContent":["/** ModerationService: block list and reports (port of commskit.moderation.service). Never runs a query inside another's transaction. */\nimport { type Db, type Dialect, dbTimestamp, detectDialect } from \"../core/db.js\";\nimport { iso } from \"../core/util.js\";\n\n// Suggested; free text is allowed.\nexport const REPORT_CATEGORIES = [\"spam\", \"harassment\", \"inappropriate\", \"fake_profile\", \"scam\", \"underage\", \"other\"] as const;\nexport const REPORT_STATUSES = [\"open\", \"resolved\", \"dismissed\"] as const;\nexport const MAX_NOTE_CHARS = 4000;\nexport const MAX_REASON_CHARS = 2000;\nexport const MAX_CATEGORY_CHARS = 64;\n\n/** What `moderation.reported` listeners and `onReport` receive. */\nexport interface ReportInfo {\n id: number;\n reporter_id: string;\n reported_id: string;\n reason: string | null;\n category: string | null;\n message_id: number | null;\n status: string;\n created_at: Date;\n /** The reporter also blocked the user. */\n blocked: boolean;\n}\n\n/** Admin wire shape (PROTOCOL.md section 9). */\nexport interface ReportDict {\n report_id: number;\n reporter_id: string | null;\n reported_id: string | null;\n reason: string | null;\n category: string | null;\n message_id: number | null;\n status: string;\n created_at: string | null;\n resolved_at: string | null;\n resolved_by: string | null;\n note: string | null;\n}\n\nexport function reportDict(r: any): ReportDict {\n return {\n report_id: Number(r.id),\n reporter_id: r.reporter_id ?? null,\n reported_id: r.reported_id ?? null,\n reason: r.reason ?? null,\n category: r.category ?? null,\n message_id: r.message_id === null || r.message_id === undefined ? null : Number(r.message_id),\n status: r.status,\n created_at: iso(r.created_at),\n resolved_at: iso(r.resolved_at),\n resolved_by: r.resolved_by ?? null,\n note: r.note ?? null,\n };\n}\n\nconst isUniqueViolation = (err: unknown): boolean => /unique|duplicate/i.test(String((err as Error)?.message ?? err)) || (err as any)?.code === \"23505\";\n\nexport class ModerationService {\n readonly dialect: Dialect;\n\n constructor(\n private db: Db,\n dialect?: Dialect,\n ) {\n this.dialect = dialect ?? detectDialect(db);\n }\n\n // --------------------------------------------------------------- blocks\n\n /** True if a new block was stored (blocking twice is a no-op). */\n async block(blockerId: string, blockedId: string): Promise<boolean> {\n if (await this.hasBlocked(blockerId, blockedId)) return false;\n try {\n await this.db.insertInto(\"comms_blocks\").values({ blocker_id: blockerId, blocked_id: blockedId, created_at: dbTimestamp(this.dialect) }).execute();\n } catch (err) {\n if (isUniqueViolation(err)) return false; // concurrent duplicate\n throw err;\n }\n return true;\n }\n\n async unblock(blockerId: string, blockedId: string): Promise<boolean> {\n const r = await this.db.deleteFrom(\"comms_blocks\").where(\"blocker_id\", \"=\", blockerId).where(\"blocked_id\", \"=\", blockedId).executeTakeFirst();\n return Number(r.numDeletedRows) > 0;\n }\n\n async hasBlocked(blockerId: string, blockedId: string): Promise<boolean> {\n const r = await this.db.selectFrom(\"comms_blocks\").select(\"id\").where(\"blocker_id\", \"=\", blockerId).where(\"blocked_id\", \"=\", blockedId).limit(1).executeTakeFirst();\n return r !== undefined;\n }\n\n /** Either direction. */\n async isBlockedBetween(userA: string, userB: string): Promise<boolean> {\n const r = await this.db\n .selectFrom(\"comms_blocks\")\n .select(\"id\")\n .where((eb) =>\n eb.or([\n eb.and([eb(\"blocker_id\", \"=\", userA), eb(\"blocked_id\", \"=\", userB)]),\n eb.and([eb(\"blocker_id\", \"=\", userB), eb(\"blocked_id\", \"=\", userA)]),\n ]),\n )\n .limit(1)\n .executeTakeFirst();\n return r !== undefined;\n }\n\n /** `{otherId: [isBlocked, blockedByOther]}` for ids with any block. */\n async blockStatus(viewerId: string, otherIds: readonly string[]): Promise<Record<string, [boolean, boolean]>> {\n const ids = [...new Set(otherIds)].filter(Boolean);\n if (!ids.length) return {};\n const rows = await this.db\n .selectFrom(\"comms_blocks\")\n .select([\"blocker_id\", \"blocked_id\"])\n .where((eb) =>\n eb.or([\n eb.and([eb(\"blocker_id\", \"=\", viewerId), eb(\"blocked_id\", \"in\", ids)]),\n eb.and([eb(\"blocked_id\", \"=\", viewerId), eb(\"blocker_id\", \"in\", ids)]),\n ]),\n )\n .execute();\n const status: Record<string, [boolean, boolean]> = {};\n for (const { blocker_id: blocker, blocked_id: blocked } of rows) {\n if (blocker === viewerId) {\n const cur = status[blocked] ?? [false, false];\n status[blocked] = [true, cur[1]];\n } else {\n const cur = status[blocker] ?? [false, false];\n status[blocker] = [cur[0], true];\n }\n }\n return status;\n }\n\n /** `[blockedId, blockedAt]`, newest first. */\n async listBlocked(blockerId: string): Promise<Array<[string, unknown]>> {\n const rows = await this.db.selectFrom(\"comms_blocks\").select([\"blocked_id\", \"created_at\"]).where(\"blocker_id\", \"=\", blockerId)\n .orderBy(\"created_at\", \"desc\").orderBy(\"id\", \"desc\").execute();\n return rows.map((r) => [r.blocked_id as string, r.created_at]);\n }\n\n // -------------------------------------------------------------- reports\n\n async createReport(\n reporterId: string,\n reportedId: string,\n opts: { reason?: string | null; category?: string | null; messageId?: number | null } = {},\n ): Promise<ReportInfo> {\n const reason = opts.reason === null || opts.reason === undefined ? null : String(opts.reason).trim().slice(0, MAX_REASON_CHARS) || null;\n const category = opts.category === null || opts.category === undefined ? null : String(opts.category).trim().toLowerCase().slice(0, MAX_CATEGORY_CHARS) || null;\n const messageId = opts.messageId ?? null;\n const now = new Date();\n const r = await this.db\n .insertInto(\"comms_reports\")\n .values({ reporter_id: reporterId, reported_id: reportedId, reason, category, message_id: messageId, status: \"open\", created_at: dbTimestamp(this.dialect, now) })\n .returning(\"id\")\n .executeTakeFirstOrThrow();\n return { id: Number(r.id), reporter_id: reporterId, reported_id: reportedId, reason, category, message_id: messageId, status: \"open\", created_at: now, blocked: false };\n }\n\n // --------------------------------------------------------- admin review\n\n async getReport(reportId: number): Promise<ReportDict | null> {\n const row = await this.db.selectFrom(\"comms_reports\").selectAll().where(\"id\", \"=\", reportId).executeTakeFirst();\n return row ? reportDict(row) : null;\n }\n\n /** Newest first; `status` filters (open | resolved | dismissed). */\n async listReports(status?: string | null, limit = 50, offset = 0): Promise<ReportDict[]> {\n let q = this.db.selectFrom(\"comms_reports\").selectAll();\n if (status) q = q.where(\"status\", \"=\", status);\n const rows = await q.orderBy(\"created_at\", \"desc\").orderBy(\"id\", \"desc\").limit(limit).offset(offset).execute();\n return rows.map(reportDict);\n }\n\n /** Set a report to resolved / dismissed (or back to open). Null if it doesn't exist; throws Error on a bad status. */\n async resolveReport(reportId: number, status: string, resolvedBy: string | null = null, note: string | null = null): Promise<ReportDict | null> {\n if (!(REPORT_STATUSES as readonly string[]).includes(status)) throw new Error(`status must be one of: ${REPORT_STATUSES.join(\", \")}`);\n const row = await this.db.selectFrom(\"comms_reports\").selectAll().where(\"id\", \"=\", reportId).executeTakeFirst();\n if (!row) return null;\n const patch: Record<string, unknown> = { status };\n if (status === \"open\") {\n patch.resolved_at = null;\n patch.resolved_by = null;\n } else {\n patch.resolved_at = dbTimestamp(this.dialect);\n patch.resolved_by = resolvedBy === null || resolvedBy === undefined ? null : String(resolvedBy).slice(0, 50) || null;\n }\n if (note !== null && note !== undefined) patch.note = String(note).trim().slice(0, MAX_NOTE_CHARS) || null;\n await this.db.updateTable(\"comms_reports\").set(patch).where(\"id\", \"=\", reportId).execute();\n return reportDict({ ...row, ...patch });\n }\n\n // ------------------------------------------------------------- deletion\n\n /**\n * Account deletion: drop every block involving `userId`; keep reports (moderation history) but replace the user's id\n * with NULL as reporter and as reported user. Use a transaction handle (`trx`) for `db` to make it atomic.\n */\n async deleteUserData(userId: string): Promise<{ blocks: number; reports_anonymized: number }> {\n const blocks = await this.db.deleteFrom(\"comms_blocks\").where((eb) => eb.or([eb(\"blocker_id\", \"=\", userId), eb(\"blocked_id\", \"=\", userId)])).executeTakeFirst();\n const asReporter = await this.db.updateTable(\"comms_reports\").set({ reporter_id: null }).where(\"reporter_id\", \"=\", userId).executeTakeFirst();\n const asReported = await this.db.updateTable(\"comms_reports\").set({ reported_id: null }).where(\"reported_id\", \"=\", userId).executeTakeFirst();\n return { blocks: Number(blocks.numDeletedRows), reports_anonymized: Number(asReporter.numUpdatedRows) + Number(asReported.numUpdatedRows) };\n }\n}\n","/** Moderation WebSocket handlers and the shared block/report actions (also used by REST). Port of commskit.moderation.handlers. */\nimport type { Db } from \"../core/db.js\";\nimport { logger } from \"../core/logger.js\";\nimport type { ModerationFeature } from \"./feature.js\";\nimport { ModerationService, type ReportInfo } from \"./service.js\";\n\nexport const BLOCKED_TEXT = \"User blocked successfully\";\nexport const BLOCKED_BY_TEXT = \"You have been blocked by this user\";\nexport const UNBLOCKED_TEXT = \"User unblocked successfully\";\nexport const UNBLOCKED_BY_TEXT = \"You have been unblocked by this user\";\n\nexport const moderationError = (error: string, extra: Record<string, unknown> = {}): Record<string, unknown> => ({\n type: \"error\",\n error,\n ...extra,\n context: \"moderation\",\n});\n\n/** Bad request (missing / self target ...). */\nexport class ModerationError extends Error {}\n\nexport function targetId(userId: string, value: unknown): string {\n if (value === null || value === undefined || String(value).trim() === \"\") {\n throw new ModerationError(\"Missing required fields: candidate_user_id\");\n }\n const target = String(value).trim();\n if (target === userId) throw new ModerationError(\"You cannot block or report yourself.\");\n return target;\n}\n\nfunction messageIdOf(value: unknown): number | null {\n if (value === null || value === undefined || value === \"\") return null;\n const s = String(value).trim();\n if (!/^[+-]?\\d+$/.test(s)) throw new ModerationError(\"message_id must be an integer\");\n return Number(s);\n}\n\n/** Block / unblock / report + their frames and events. */\nexport class ModerationActions {\n constructor(private feature: ModerationFeature) {}\n\n private get hub() {\n return this.feature.hub;\n }\n\n async block(db: Db, userId: string, target: string): Promise<boolean> {\n const created = await new ModerationService(db, this.hub.dialect).block(userId, target);\n await this.hub.send(userId, { type: \"user_blocked\", candidate_user_id: target, message: BLOCKED_TEXT });\n if (this.feature.notifyBlockedUser) {\n await this.hub.send(target, { type: \"blocked_by_user\", candidate_user_id: userId, message: BLOCKED_BY_TEXT });\n }\n // Always emitted (also for a repeated block): listeners such as linkModeration end live calls between the pair,\n // which must happen even if a call slipped through.\n await this.hub.events.emit(\"moderation.blocked\", userId, target);\n return created;\n }\n\n async unblock(db: Db, userId: string, target: string): Promise<boolean> {\n const removed = await new ModerationService(db, this.hub.dialect).unblock(userId, target);\n await this.hub.send(userId, { type: \"user_unblocked\", candidate_user_id: target, message: UNBLOCKED_TEXT });\n if (this.feature.notifyBlockedUser && removed) {\n await this.hub.send(target, { type: \"unblocked_by_user\", candidate_user_id: userId, message: UNBLOCKED_BY_TEXT });\n }\n if (removed) await this.hub.events.emit(\"moderation.unblocked\", userId, target);\n return removed;\n }\n\n async report(\n db: Db,\n userId: string,\n target: string,\n opts: { reason?: unknown; category?: unknown; messageId?: unknown; block?: boolean } = {},\n ): Promise<ReportInfo> {\n let info = await new ModerationService(db, this.hub.dialect).createReport(userId, target, {\n reason: opts.reason === null || opts.reason === undefined ? null : String(opts.reason),\n category: opts.category === null || opts.category === undefined ? null : String(opts.category),\n messageId: messageIdOf(opts.messageId),\n });\n if (opts.block ?? true) {\n await this.block(db, userId, target);\n info = { ...info, blocked: true };\n }\n await this.hub.send(userId, { type: \"report_received\", report_id: info.id, candidate_user_id: target });\n await this.hub.events.emit(\"moderation.reported\", info);\n const hook = this.feature.onReport;\n if (hook) {\n const reported = info;\n this.hub.spawn(async () => {\n try {\n await hook(reported);\n } catch (err) {\n logger.error(`onReport hook failed for report ${reported.id}`, err);\n }\n });\n }\n return info;\n }\n}\n\nexport class ModerationHandlers {\n constructor(private feature: ModerationFeature) {}\n\n private get hub() {\n return this.feature.hub;\n }\n\n register(): void {\n this.hub.on(\"report_and_block\", this.guard((u, m, db) => this.reportAndBlock(u, m, db), \"Failed to report user\"));\n this.hub.on(\"block\", this.guard((u, m, db) => this.block(u, m, db), \"Failed to block user\"));\n this.hub.on(\"unblock\", this.guard((u, m, db) => this.unblock(u, m, db), \"Failed to unblock user\"));\n }\n\n private guard(fn: (userId: string, message: Record<string, any>, db: Db) => Promise<void>, label: string) {\n return async (userId: string, message: Record<string, any>, db: Db): Promise<void> => {\n try {\n await fn(userId, message, db);\n } catch (err) {\n if (err instanceof ModerationError) {\n await this.hub.send(userId, moderationError(err.message));\n return;\n }\n logger.error(`${message.type} handler failed`, err);\n await this.hub.reportHandlerError(userId, message, err);\n await this.hub.send(userId, moderationError(`${label}: ${(err as Error)?.message ?? err}`));\n }\n };\n }\n\n async reportAndBlock(userId: string, message: Record<string, any>, db: Db): Promise<void> {\n const target = targetId(userId, message.candidate_user_id);\n await this.feature.actions.report(db, userId, target, {\n reason: message.reason,\n category: message.category,\n messageId: message.message_id,\n block: message.block !== false,\n });\n }\n\n async block(userId: string, message: Record<string, any>, db: Db): Promise<void> {\n await this.feature.actions.block(db, userId, targetId(userId, message.candidate_user_id));\n }\n\n async unblock(userId: string, message: Record<string, any>, db: Db): Promise<void> {\n await this.feature.actions.unblock(db, userId, targetId(userId, message.candidate_user_id));\n }\n}\n","/**\n * Chat / call policy wrappers that put the block list in front of the host's own policy (port of commskit.moderation.policy).\n *\n * They match the chat `ChatPolicy` and the call `CallPolicy` structurally, so this package never imports chat or call:\n *\n * new ChatFeature({ policy: moderation.chatPolicy(myChatPolicy) })\n * new CallFeature({ policy: moderation.callPolicy(myCreditPolicy) })\n *\n * or let `linkModeration` (src/integration) wrap the installed features' policies. A blocked pair (either direction) is refused\n * BEFORE the inner policy runs, so e.g. a credit-charging call policy never charges for a call that would be refused anyway.\n */\nimport type { Db } from \"../core/db.js\";\nimport { ModerationService } from \"./service.js\";\n\nexport const CHAT_REFUSAL = \"Cannot send message. User is blocked.\";\nexport const CALL_REFUSAL = \"Cannot call. User is blocked.\";\n\n/** Same shape as the call feature's decision (`allowed`, `error`). */\nexport interface ModerationDecision {\n allowed: boolean;\n error?: Record<string, unknown> | null;\n}\n\n/** Structural view of a host chat policy. */\nexport interface ChatPolicyLike {\n refusalMessage?: string;\n canMessage(db: Db, senderId: string, recipientId: string): Promise<boolean>;\n blockStatus(db: Db, viewerId: string, otherIds: readonly string[]): Promise<Record<string, [boolean, boolean]>>;\n extraConversationInfo(db: Db, viewerId: string, otherId: string): Promise<Record<string, unknown>>;\n}\n\n/** Structural view of a host call policy. */\nexport interface CallPolicyLike {\n authorize(db: Db, callerId: string, receiverId: string, message: Record<string, any>): Promise<any> | any;\n}\n\nexport class ModerationChatPolicy implements ChatPolicyLike {\n /**\n * The chat feature reads `refusalMessage` right after `canMessage` returned false; `lastRefusal` remembers whose refusal\n * it was (the block list or the inner policy). Both messages are constant per policy, so concurrent frames cannot mix\n * up different texts except when the host policy's message differs from the block text.\n */\n private lastRefusal: string | null = null;\n\n constructor(\n public inner: ChatPolicyLike | null = null,\n public blockedMessage: string = CHAT_REFUSAL,\n ) {}\n\n get refusalMessage(): string {\n return this.lastRefusal ?? this.inner?.refusalMessage ?? this.blockedMessage;\n }\n\n async canMessage(db: Db, senderId: string, recipientId: string): Promise<boolean> {\n if (await new ModerationService(db).isBlockedBetween(senderId, recipientId)) {\n this.lastRefusal = this.blockedMessage;\n return false;\n }\n if (this.inner && !(await this.inner.canMessage(db, senderId, recipientId))) {\n this.lastRefusal = this.inner.refusalMessage || this.blockedMessage;\n return false;\n }\n this.lastRefusal = null;\n return true;\n }\n\n /** Kit block list OR the inner policy's (e.g. a legacy host block table). */\n async blockStatus(db: Db, viewerId: string, otherIds: readonly string[]): Promise<Record<string, [boolean, boolean]>> {\n const merged: Record<string, [boolean, boolean]> = { ...(await new ModerationService(db).blockStatus(viewerId, otherIds)) };\n if (this.inner) {\n for (const [other, [blocked, byOther]] of Object.entries(await this.inner.blockStatus(db, viewerId, otherIds))) {\n const mine = merged[other] ?? [false, false];\n merged[other] = [mine[0] || Boolean(blocked), mine[1] || Boolean(byOther)];\n }\n }\n return merged;\n }\n\n async extraConversationInfo(db: Db, viewerId: string, otherId: string): Promise<Record<string, unknown>> {\n if (this.inner) return this.inner.extraConversationInfo(db, viewerId, otherId);\n return { chat_consent_given: true };\n }\n}\n\nexport class ModerationCallPolicy implements CallPolicyLike {\n constructor(\n public inner: CallPolicyLike | null = null,\n public blockedMessage: string = CALL_REFUSAL,\n ) {}\n\n async authorize(db: Db, callerId: string, receiverId: string, message: Record<string, any>): Promise<ModerationDecision> {\n if (await new ModerationService(db).isBlockedBetween(callerId, receiverId)) {\n return { allowed: false, error: { error: this.blockedMessage, reason: \"blocked\" } };\n }\n if (this.inner) return this.inner.authorize(db, callerId, receiverId, message);\n return { allowed: true };\n }\n}\n","/** Moderation REST routes (PROTOCOL.md sections 7 and 9). Port of commskit.moderation.router. */\nimport { HttpError, type RestContext, type RestRoute, firstQuery, ok, validationError } from \"../core/rest.js\";\nimport { iso, pyBool } from \"../core/util.js\";\nimport type { ModerationFeature } from \"./feature.js\";\nimport { ModerationError, targetId } from \"./handlers.js\";\nimport { REPORT_STATUSES, ModerationService } from \"./service.js\";\n\nfunction bodyObject(ctx: RestContext): Record<string, any> {\n const b = ctx.body;\n if (b === null || typeof b !== \"object\" || Array.isArray(b)) {\n throw new HttpError(422, [{ type: \"missing\", loc: [\"body\"], msg: \"Field required\", input: null }]);\n }\n return b as Record<string, any>;\n}\n\nfunction intParam(where: \"path\" | \"query\", name: string, raw: string | undefined, def?: number, min?: number, max?: number): number {\n if (raw === undefined) {\n if (def === undefined) throw validationError(where, name, \"missing\", \"Field required\");\n return def;\n }\n if (!/^\\s*[+-]?\\d+\\s*$/.test(raw)) {\n throw validationError(where, name, \"int_parsing\", \"Input should be a valid integer, unable to parse string as an integer\", raw);\n }\n const n = Number(raw);\n if (min !== undefined && n < min) throw validationError(where, name, \"greater_than_equal\", `Input should be greater than or equal to ${min}`, raw);\n if (max !== undefined && n > max) throw validationError(where, name, \"less_than_equal\", `Input should be less than or equal to ${max}`, raw);\n return n;\n}\n\nexport function buildModerationRoutes(feature: ModerationFeature): RestRoute[] {\n const hub = feature.hub;\n const actions = feature.actions;\n\n const targetOr422 = (userId: string, value: unknown): string => {\n try {\n return targetId(userId, value);\n } catch (err) {\n if (err instanceof ModerationError) throw new HttpError(422, err.message);\n throw err;\n }\n };\n\n const routes: RestRoute[] = [\n {\n method: \"GET\",\n path: \"/moderation/blocks\",\n handler: async (ctx) => {\n const rows = await new ModerationService(hub.db, hub.dialect).listBlocked(ctx.userId!);\n const profiles = await hub.profiles(rows.map((r) => r[0]));\n return ok(\n rows.map(([blockedId, blockedAt]) => {\n const p = profiles[blockedId];\n return { user_id: blockedId, username: p?.name ? p.name : blockedId, avatar: p ? (p.avatar ?? null) : null, blocked_at: iso(blockedAt) };\n }),\n );\n },\n },\n {\n method: \"POST\",\n path: \"/moderation/blocks\",\n handler: async (ctx) => {\n const target = targetOr422(ctx.userId!, bodyObject(ctx).user_id);\n await actions.block(hub.db, ctx.userId!, target);\n return ok({ status: true });\n },\n },\n {\n method: \"DELETE\",\n path: \"/moderation/blocks/:blockedId\",\n handler: async (ctx) => {\n await actions.unblock(hub.db, ctx.userId!, targetOr422(ctx.userId!, ctx.params.blockedId));\n return ok({ status: true });\n },\n },\n {\n method: \"POST\",\n path: \"/moderation/reports\",\n handler: async (ctx) => {\n const p = bodyObject(ctx);\n const target = targetOr422(ctx.userId!, p.user_id);\n if (p.reason === null || p.reason === undefined || !String(p.reason).trim()) throw new HttpError(422, \"reason is required\");\n try {\n const info = await actions.report(hub.db, ctx.userId!, target, {\n reason: p.reason,\n category: p.category,\n messageId: p.message_id,\n block: pyBool(p.block ?? false),\n });\n return ok({ report_id: info.id });\n } catch (err) {\n if (err instanceof ModerationError) throw new HttpError(422, err.message);\n throw err;\n }\n },\n },\n ];\n\n const admin = feature.adminDependency;\n if (admin) {\n // The host's dependency authenticates the moderator itself (throw HttpError 401/403), hence auth: false.\n routes.push(\n {\n method: \"GET\",\n path: \"/moderation/admin/reports\",\n auth: false,\n handler: async (ctx) => {\n await admin(ctx);\n const status = firstQuery(ctx.query, \"status\");\n if (status !== undefined && !(REPORT_STATUSES as readonly string[]).includes(status)) {\n throw new HttpError(422, `status must be one of: ${REPORT_STATUSES.join(\", \")}`);\n }\n const limit = intParam(\"query\", \"limit\", firstQuery(ctx.query, \"limit\"), 50, 1, 200);\n const offset = intParam(\"query\", \"offset\", firstQuery(ctx.query, \"offset\"), 0, 0);\n return ok(await feature.listReports(status ?? null, { limit, offset }));\n },\n },\n {\n method: \"POST\",\n path: \"/moderation/admin/reports/:reportId\",\n auth: false,\n handler: async (ctx) => {\n const adminId = await admin(ctx);\n const reportId = intParam(\"path\", \"report_id\", ctx.params.reportId);\n const p = bodyObject(ctx);\n if (![\"resolved\", \"dismissed\", \"open\"].includes(p.status)) throw new HttpError(422, \"status must be resolved or dismissed\");\n const report = await feature.resolveReport(reportId, p.status, {\n resolvedBy: adminId === null || adminId === undefined ? null : String(adminId),\n note: p.note === null || p.note === undefined ? null : String(p.note),\n });\n if (!report) throw new HttpError(404, \"Report not found\");\n return ok(report);\n },\n },\n {\n method: \"GET\",\n path: \"/moderation/admin/reports/:reportId/context\",\n auth: false,\n handler: async (ctx) => {\n await admin(ctx);\n const reportId = intParam(\"path\", \"report_id\", ctx.params.reportId);\n const before = intParam(\"query\", \"before\", firstQuery(ctx.query, \"before\"), 20, 0, 200);\n const after = intParam(\"query\", \"after\", firstQuery(ctx.query, \"after\"), 20, 0, 200);\n const context = await feature.reportContext(reportId, { before, after });\n if (!context) throw new HttpError(404, \"Report not found\");\n return ok(context);\n },\n },\n );\n }\n return routes;\n}\n","/** `hub.install(new ModerationFeature({...}))` (port of commskit.moderation.feature). Imports only core. */\nimport type { Db } from \"../core/db.js\";\nimport type { CommsHub, Feature } from \"../core/hub.js\";\nimport type { RestContext } from \"../core/rest.js\";\nimport { ModerationActions, ModerationHandlers } from \"./handlers.js\";\nimport { type CallPolicyLike, type ChatPolicyLike, ModerationCallPolicy, ModerationChatPolicy } from \"./policy.js\";\nimport { buildModerationRoutes } from \"./router.js\";\nimport { ModerationService, type ReportDict, type ReportInfo } from \"./service.js\";\n\nexport type ReportHook = (report: ReportInfo) => Promise<void> | void;\n/** `contextProvider(reporterId, reportedId, messageId, before, after)` -> MessageDto list (newest first). Set by linkModeration. */\nexport type ContextProvider = (\n reporterId: string | null,\n reportedId: string | null,\n messageId: number | null,\n before: number,\n after: number,\n) => Promise<Array<Record<string, any>>>;\n/** Authenticates a moderator for the admin routes: return their id, or throw an `HttpError` (401 / 403). */\nexport type AdminDependency = (ctx: RestContext) => Promise<string | number | null | undefined> | string | number | null | undefined;\n\nexport interface ModerationFeatureOptions {\n onReport?: ReportHook;\n /** Send `blocked_by_user` / `unblocked_by_user` to the other user (default true, for compatibility). */\n notifyBlockedUser?: boolean;\n /** When set, mounts `/moderation/admin/reports...`. */\n adminDependency?: AdminDependency;\n contextProvider?: ContextProvider;\n}\n\n/**\n * Block list + reports. Installs WS handlers `report_and_block` / `block` / `unblock` and `/moderation/...` REST routes.\n * It does not touch chat or call by itself: wrap your policies (`chatPolicy(inner)` / `callPolicy(inner)`) or use\n * `linkModeration(moderation, { chat, call })`, which also ends a live call between the pair on block.\n *\n * Events on `hub.events`: `moderation.blocked(blockerId, blockedId)`, `moderation.unblocked(blockerId, blockedId)`,\n * `moderation.reported(ReportInfo)`, `moderation.report_resolved(report)`.\n */\nexport class ModerationFeature implements Feature {\n readonly name = \"moderation\";\n onReport?: ReportHook;\n notifyBlockedUser: boolean;\n adminDependency?: AdminDependency;\n contextProvider?: ContextProvider;\n actions!: ModerationActions;\n handlers!: ModerationHandlers;\n /** Set by linkModeration. */\n link: unknown = null;\n private _hub: CommsHub | null = null;\n\n constructor(opts: ModerationFeatureOptions = {}) {\n this.onReport = opts.onReport;\n this.notifyBlockedUser = opts.notifyBlockedUser ?? true;\n this.adminDependency = opts.adminDependency;\n this.contextProvider = opts.contextProvider;\n }\n\n get hub(): CommsHub {\n if (!this._hub) throw new Error(\"ModerationFeature is not installed; call hub.install(feature) first\");\n return this._hub;\n }\n\n install(hub: CommsHub): this {\n hub.registerFeature(this.name, this);\n this._hub = hub;\n this.actions = new ModerationActions(this);\n this.handlers = new ModerationHandlers(this);\n this.handlers.register();\n hub.includeRoutes(buildModerationRoutes(this));\n return this;\n }\n\n private service(db?: Db): ModerationService {\n return new ModerationService(db ?? this.hub.db, this.hub.dialect);\n }\n\n // ------------------------------------------------------------ policies\n\n /** ChatPolicy: refuses a blocked pair, otherwise asks `inner` (allow-all if null). */\n chatPolicy(inner: ChatPolicyLike | null = null): ModerationChatPolicy {\n return new ModerationChatPolicy(inner);\n }\n\n /** CallPolicy: denies a blocked pair (`reason: \"blocked\"`), otherwise `inner`. */\n callPolicy(inner: CallPolicyLike | null = null): ModerationCallPolicy {\n return new ModerationCallPolicy(inner);\n }\n\n // -------------------------------------------------------------- queries\n\n /** `{otherId: [isBlocked, blockedByOther]}` for `res_users` / `res_messages.info`. */\n blockStatus(viewerId: string, otherIds: readonly string[], db?: Db): Promise<Record<string, [boolean, boolean]>> {\n return this.service(db).blockStatus(viewerId, otherIds);\n }\n\n isBlockedBetween(userA: string, userB: string, db?: Db): Promise<boolean> {\n return this.service(db).isBlockedBetween(userA, userB);\n }\n\n // --------------------------------------------------------- admin review\n\n listReports(status: string | null = null, opts: { limit?: number; offset?: number } = {}): Promise<ReportDict[]> {\n return this.service().listReports(status, opts.limit ?? 50, opts.offset ?? 0);\n }\n\n /** `status`: resolved | dismissed (| open). Null if the report doesn't exist. */\n async resolveReport(reportId: number, status: string, opts: { resolvedBy?: string | null; note?: string | null } = {}): Promise<ReportDict | null> {\n const report = await this.service().resolveReport(reportId, status, opts.resolvedBy ?? null, opts.note ?? null);\n if (report) await this.hub.events.emit(\"moderation.report_resolved\", report);\n return report;\n }\n\n /**\n * `{report, messages}`: messages around the reported message (or the latest between the pair), newest first, from\n * `contextProvider` (empty without one, e.g. call-only apps).\n */\n async reportContext(reportId: number, opts: { before?: number; after?: number } = {}): Promise<{ report: ReportDict; messages: Array<Record<string, any>> } | null> {\n const report = await this.service().getReport(reportId);\n if (!report) return null;\n let messages: Array<Record<string, any>> = [];\n if (this.contextProvider && report.reporter_id && report.reported_id) {\n messages = [...(await this.contextProvider(report.reporter_id, report.reported_id, report.message_id, Math.max(0, opts.before ?? 20), Math.max(0, opts.after ?? 20)))];\n }\n return { report, messages };\n }\n\n // ------------------------------------------------------------- deletion\n\n /**\n * Account deletion: blocks involving the user are deleted; reports are kept for the moderation history with the user's\n * id set to NULL (as reporter and as reported user).\n */\n async deleteUserData(userId: string): Promise<{ blocks: number; reports_anonymized: number }> {\n const hub = this.hub;\n return hub.db.transaction().execute((trx) => new ModerationService(trx, hub.dialect).deleteUserData(String(userId)));\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,52 @@
1
+ import {
2
+ LRU
3
+ } from "./chunk-KLKRK6OG.js";
4
+
5
+ // src/core/ratelimit.ts
6
+ var RateLimit = class {
7
+ constructor(count, seconds) {
8
+ this.count = count;
9
+ this.seconds = seconds;
10
+ if (count < 1 || seconds <= 0) throw new Error("RateLimit needs count >= 1 and seconds > 0");
11
+ }
12
+ count;
13
+ seconds;
14
+ get rate() {
15
+ return this.count / this.seconds;
16
+ }
17
+ };
18
+ var retryAfterSeconds = (wait) => Math.max(1, Math.ceil(wait));
19
+ var RateLimiter = class {
20
+ constructor(maxKeys = 1e5, clock = () => performance.now() / 1e3) {
21
+ this.clock = clock;
22
+ this.buckets = new LRU(maxKeys);
23
+ }
24
+ clock;
25
+ buckets;
26
+ async hit(key, limit, cost = 1) {
27
+ return this.hitSync(key, limit, cost);
28
+ }
29
+ hitSync(key, limit, cost = 1) {
30
+ if (!limit) return 0;
31
+ const now = this.clock();
32
+ const [prev, last] = this.buckets.get(key) ?? [limit.count, now];
33
+ const tokens = Math.min(limit.count, prev + (now - last) * limit.rate);
34
+ if (tokens >= cost) {
35
+ this.buckets.put(key, [tokens - cost, now]);
36
+ return 0;
37
+ }
38
+ this.buckets.put(key, [tokens, now]);
39
+ return (cost - tokens) / limit.rate;
40
+ }
41
+ reset(key) {
42
+ if (key === void 0) this.buckets.clear();
43
+ else this.buckets.pop(key);
44
+ }
45
+ };
46
+
47
+ export {
48
+ RateLimit,
49
+ retryAfterSeconds,
50
+ RateLimiter
51
+ };
52
+ //# sourceMappingURL=chunk-BBFVG7QU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/ratelimit.ts"],"sourcesContent":["/**\n * Per-user rate limits (token bucket), PROTOCOL.md section 9 \"Limits\".\n * Keys are \"<action>:<userId>\", e.g. chat.message:U1, chat.upload:U1, chat.typing:U1, call.initiate:U1.\n * `RedisRateLimiter` (see `redis.ts`) shares buckets across workers.\n */\nimport { LRU } from \"./util.js\";\n\nexport class RateLimit {\n constructor(\n public readonly count: number,\n public readonly seconds: number,\n ) {\n if (count < 1 || seconds <= 0) throw new Error(\"RateLimit needs count >= 1 and seconds > 0\");\n }\n get rate(): number {\n return this.count / this.seconds;\n }\n}\n\nexport interface Limiter {\n /** Take `cost` tokens. Returns 0 when allowed, else seconds until it would be. */\n hit(key: string, limit: RateLimit | null | undefined, cost?: number): Promise<number>;\n}\n\n/** Whole seconds for the wire (`retry_after`), at least 1. */\nexport const retryAfterSeconds = (wait: number): number => Math.max(1, Math.ceil(wait));\n\nexport class RateLimiter implements Limiter {\n private buckets: LRU<string, [number, number]>;\n constructor(\n maxKeys = 100_000,\n private clock: () => number = () => performance.now() / 1000,\n ) {\n this.buckets = new LRU(maxKeys);\n }\n\n async hit(key: string, limit: RateLimit | null | undefined, cost = 1): Promise<number> {\n return this.hitSync(key, limit, cost);\n }\n\n hitSync(key: string, limit: RateLimit | null | undefined, cost = 1): number {\n if (!limit) return 0;\n const now = this.clock();\n const [prev, last] = this.buckets.get(key) ?? [limit.count, now];\n const tokens = Math.min(limit.count, prev + (now - last) * limit.rate);\n if (tokens >= cost) {\n this.buckets.put(key, [tokens - cost, now]);\n return 0;\n }\n this.buckets.put(key, [tokens, now]);\n return (cost - tokens) / limit.rate;\n }\n\n reset(key?: string): void {\n if (key === undefined) this.buckets.clear();\n else this.buckets.pop(key);\n }\n}\n"],"mappings":";;;;;AAOO,IAAM,YAAN,MAAgB;AAAA,EACrB,YACkB,OACA,SAChB;AAFgB;AACA;AAEhB,QAAI,QAAQ,KAAK,WAAW,EAAG,OAAM,IAAI,MAAM,4CAA4C;AAAA,EAC7F;AAAA,EAJkB;AAAA,EACA;AAAA,EAIlB,IAAI,OAAe;AACjB,WAAO,KAAK,QAAQ,KAAK;AAAA,EAC3B;AACF;AAQO,IAAM,oBAAoB,CAAC,SAAyB,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC;AAE/E,IAAM,cAAN,MAAqC;AAAA,EAE1C,YACE,UAAU,KACF,QAAsB,MAAM,YAAY,IAAI,IAAI,KACxD;AADQ;AAER,SAAK,UAAU,IAAI,IAAI,OAAO;AAAA,EAChC;AAAA,EAHU;AAAA,EAHF;AAAA,EAQR,MAAM,IAAI,KAAa,OAAqC,OAAO,GAAoB;AACrF,WAAO,KAAK,QAAQ,KAAK,OAAO,IAAI;AAAA,EACtC;AAAA,EAEA,QAAQ,KAAa,OAAqC,OAAO,GAAW;AAC1E,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,MAAM,KAAK,MAAM;AACvB,UAAM,CAAC,MAAM,IAAI,IAAI,KAAK,QAAQ,IAAI,GAAG,KAAK,CAAC,MAAM,OAAO,GAAG;AAC/D,UAAM,SAAS,KAAK,IAAI,MAAM,OAAO,QAAQ,MAAM,QAAQ,MAAM,IAAI;AACrE,QAAI,UAAU,MAAM;AAClB,WAAK,QAAQ,IAAI,KAAK,CAAC,SAAS,MAAM,GAAG,CAAC;AAC1C,aAAO;AAAA,IACT;AACA,SAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC;AACnC,YAAQ,OAAO,UAAU,MAAM;AAAA,EACjC;AAAA,EAEA,MAAM,KAAoB;AACxB,QAAI,QAAQ,OAAW,MAAK,QAAQ,MAAM;AAAA,QACrC,MAAK,QAAQ,IAAI,GAAG;AAAA,EAC3B;AACF;","names":[]}
@@ -0,0 +1,82 @@
1
+ // src/core/db.ts
2
+ function detectDialect(db) {
3
+ const name = db.getExecutor().adapter?.constructor?.name ?? "";
4
+ if (/postgres/i.test(name)) return "postgres";
5
+ if (/sqlite/i.test(name)) return "sqlite";
6
+ throw new Error(`Cannot detect the SQL dialect (adapter ${name}); pass { dialect } to the hub`);
7
+ }
8
+ var p2 = (n, w = 2) => String(n).padStart(w, "0");
9
+ function dbTimestamp(dialect, d = /* @__PURE__ */ new Date()) {
10
+ if (dialect === "postgres") return d;
11
+ return `${d.getUTCFullYear()}-${p2(d.getUTCMonth() + 1)}-${p2(d.getUTCDate())} ${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}:${p2(d.getUTCSeconds())}.${p2(d.getUTCMilliseconds(), 3)}000`;
12
+ }
13
+ var dbBool = (dialect, v) => dialect === "sqlite" ? v ? 1 : 0 : v;
14
+ var dbNow = () => /* @__PURE__ */ new Date();
15
+
16
+ // src/core/rest.ts
17
+ var HttpError = class extends Error {
18
+ constructor(status, detail, headers) {
19
+ super(typeof detail === "string" ? detail : JSON.stringify(detail));
20
+ this.status = status;
21
+ this.detail = detail;
22
+ this.headers = headers;
23
+ }
24
+ status;
25
+ detail;
26
+ headers;
27
+ };
28
+ function validationError(where, name, type, msg, input = null) {
29
+ return new HttpError(422, [{ type, loc: [where, name], msg, input }]);
30
+ }
31
+ var ok = (body, status = 200) => ({ status, body });
32
+ function firstQuery(q, name) {
33
+ const v = q[name];
34
+ return Array.isArray(v) ? v[0] : v;
35
+ }
36
+ function matchPath(pattern, actual) {
37
+ const a = pattern.split("/");
38
+ const b = actual.split("/");
39
+ if (a.length !== b.length) return null;
40
+ const params = {};
41
+ for (let i = 0; i < a.length; i++) {
42
+ if (a[i].startsWith(":")) {
43
+ try {
44
+ params[a[i].slice(1)] = decodeURIComponent(b[i]);
45
+ } catch {
46
+ return null;
47
+ }
48
+ } else if (a[i] !== b[i]) return null;
49
+ }
50
+ return params;
51
+ }
52
+
53
+ // src/core/ddl.ts
54
+ function ddlTypes(dialect) {
55
+ return dialect === "sqlite" ? {
56
+ pk: "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT",
57
+ ts: "DATETIME",
58
+ tsDefaultNow: "DATETIME DEFAULT (CURRENT_TIMESTAMP)",
59
+ bool: "BOOLEAN",
60
+ big: "BIGINT"
61
+ } : {
62
+ pk: "SERIAL PRIMARY KEY",
63
+ ts: "TIMESTAMP WITH TIME ZONE",
64
+ tsDefaultNow: "TIMESTAMP WITH TIME ZONE DEFAULT now()",
65
+ bool: "BOOLEAN",
66
+ big: "BIGINT"
67
+ };
68
+ }
69
+
70
+ export {
71
+ detectDialect,
72
+ dbTimestamp,
73
+ dbBool,
74
+ dbNow,
75
+ ddlTypes,
76
+ HttpError,
77
+ validationError,
78
+ ok,
79
+ firstQuery,
80
+ matchPath
81
+ };
82
+ //# sourceMappingURL=chunk-GR4PMGCS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/db.ts","../src/core/rest.ts","../src/core/ddl.ts"],"sourcesContent":["/** Database helpers shared by all features (Kysely; sqlite for tests/demo, postgres for production). */\nimport type { Kysely } from \"kysely\";\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n/** The per-frame \"session\": the hub's Kysely instance (queries autocommit; use `db.transaction()`). */\nexport type Db = Kysely<any>;\n\nexport function detectDialect(db: Db): Dialect {\n const name: string = (db.getExecutor() as any).adapter?.constructor?.name ?? \"\";\n if (/postgres/i.test(name)) return \"postgres\";\n if (/sqlite/i.test(name)) return \"sqlite\";\n throw new Error(`Cannot detect the SQL dialect (adapter ${name}); pass { dialect } to the hub`);\n}\n\nconst p2 = (n: number, w = 2): string => String(n).padStart(w, \"0\");\n\n/** A UTC timestamp value to write: \"YYYY-MM-DD HH:MM:SS.ffffff\" (sqlite, as SQLAlchemy stores it) or a Date (pg). */\nexport function dbTimestamp(dialect: Dialect, d: Date = new Date()): string | Date {\n if (dialect === \"postgres\") return d;\n return (\n `${d.getUTCFullYear()}-${p2(d.getUTCMonth() + 1)}-${p2(d.getUTCDate())} ` +\n `${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}:${p2(d.getUTCSeconds())}.${p2(d.getUTCMilliseconds(), 3)}000`\n );\n}\n\n/** Boolean column value: sqlite stores 0/1, postgres wants real booleans. */\nexport const dbBool = (dialect: Dialect, v: boolean): boolean | number => (dialect === \"sqlite\" ? (v ? 1 : 0) : v);\n\n/** Current time (the app clock; the Python kit reads the database clock). */\nexport const dbNow = (): Date => new Date();\n","/** Framework-neutral REST routes (adapters: fastify, express, or the built-in node:http listener). */\n\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\nexport interface RestContext {\n method: string;\n /** Route pattern with `:param` segments, e.g. `/chats/:chatId/messages`. */\n path: string;\n params: Record<string, string>;\n query: Record<string, string | string[] | undefined>;\n /** Lower-cased header names. */\n headers: Record<string, string | string[] | undefined>;\n /** Parsed JSON body (undefined when there is none). */\n body: unknown;\n /** Set by the hub for routes with `auth !== false` (Bearer token through the AuthAdapter). */\n userId: string | null;\n}\n\nexport interface RestResult {\n status: number;\n headers?: Record<string, string>;\n body: unknown;\n}\n\nexport interface RestRoute {\n method: HttpMethod;\n path: string;\n /** Require `Authorization: Bearer <token>` (default true). */\n auth?: boolean;\n handler(ctx: RestContext): Promise<RestResult> | RestResult;\n}\n\n/** Thrown by handlers to answer with FastAPI's error shape `{\"detail\": ...}`. */\nexport class HttpError extends Error {\n constructor(\n public status: number,\n public detail: unknown,\n public headers?: Record<string, string>,\n ) {\n super(typeof detail === \"string\" ? detail : JSON.stringify(detail));\n }\n}\n\n/** FastAPI-style 422 body for a bad parameter. */\nexport function validationError(where: \"query\" | \"path\" | \"body\", name: string, type: string, msg: string, input: unknown = null): HttpError {\n return new HttpError(422, [{ type, loc: [where, name], msg, input }]);\n}\n\nexport const ok = (body: unknown, status = 200): RestResult => ({ status, body });\n\nexport function firstQuery(q: RestContext[\"query\"], name: string): string | undefined {\n const v = q[name];\n return Array.isArray(v) ? v[0] : v;\n}\n\n/** Match a request path against `:param` patterns. Returns the params or null. */\nexport function matchPath(pattern: string, actual: string): Record<string, string> | null {\n const a = pattern.split(\"/\");\n const b = actual.split(\"/\");\n if (a.length !== b.length) return null;\n const params: Record<string, string> = {};\n for (let i = 0; i < a.length; i++) {\n if (a[i]!.startsWith(\":\")) {\n try {\n params[a[i]!.slice(1)] = decodeURIComponent(b[i]!);\n } catch {\n return null;\n }\n } else if (a[i] !== b[i]) return null;\n }\n return params;\n}\n","import type { Dialect } from \"./db.js\";\n\n/** Portable column type fragments (same logical schema SQLAlchemy `create_all` produces). */\nexport function ddlTypes(dialect: Dialect): { pk: string; ts: string; tsDefaultNow: string; bool: string; big: string } {\n return dialect === \"sqlite\"\n ? {\n pk: \"INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT\",\n ts: \"DATETIME\",\n tsDefaultNow: \"DATETIME DEFAULT (CURRENT_TIMESTAMP)\",\n bool: \"BOOLEAN\",\n big: \"BIGINT\",\n }\n : {\n pk: \"SERIAL PRIMARY KEY\",\n ts: \"TIMESTAMP WITH TIME ZONE\",\n tsDefaultNow: \"TIMESTAMP WITH TIME ZONE DEFAULT now()\",\n bool: \"BOOLEAN\",\n big: \"BIGINT\",\n };\n}\n"],"mappings":";AAOO,SAAS,cAAc,IAAiB;AAC7C,QAAM,OAAgB,GAAG,YAAY,EAAU,SAAS,aAAa,QAAQ;AAC7E,MAAI,YAAY,KAAK,IAAI,EAAG,QAAO;AACnC,MAAI,UAAU,KAAK,IAAI,EAAG,QAAO;AACjC,QAAM,IAAI,MAAM,0CAA0C,IAAI,gCAAgC;AAChG;AAEA,IAAM,KAAK,CAAC,GAAW,IAAI,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAG3D,SAAS,YAAY,SAAkB,IAAU,oBAAI,KAAK,GAAkB;AACjF,MAAI,YAAY,WAAY,QAAO;AACnC,SACE,GAAG,EAAE,eAAe,CAAC,IAAI,GAAG,EAAE,YAAY,IAAI,CAAC,CAAC,IAAI,GAAG,EAAE,WAAW,CAAC,CAAC,IACnE,GAAG,EAAE,YAAY,CAAC,CAAC,IAAI,GAAG,EAAE,cAAc,CAAC,CAAC,IAAI,GAAG,EAAE,cAAc,CAAC,CAAC,IAAI,GAAG,EAAE,mBAAmB,GAAG,CAAC,CAAC;AAE7G;AAGO,IAAM,SAAS,CAAC,SAAkB,MAAkC,YAAY,WAAY,IAAI,IAAI,IAAK;AAGzG,IAAM,QAAQ,MAAY,oBAAI,KAAK;;;ACInC,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YACS,QACA,QACA,SACP;AACA,UAAM,OAAO,WAAW,WAAW,SAAS,KAAK,UAAU,MAAM,CAAC;AAJ3D;AACA;AACA;AAAA,EAGT;AAAA,EALS;AAAA,EACA;AAAA,EACA;AAIX;AAGO,SAAS,gBAAgB,OAAkC,MAAc,MAAc,KAAa,QAAiB,MAAiB;AAC3I,SAAO,IAAI,UAAU,KAAK,CAAC,EAAE,MAAM,KAAK,CAAC,OAAO,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC;AACtE;AAEO,IAAM,KAAK,CAAC,MAAe,SAAS,SAAqB,EAAE,QAAQ,KAAK;AAExE,SAAS,WAAW,GAAyB,MAAkC;AACpF,QAAM,IAAI,EAAE,IAAI;AAChB,SAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI;AACnC;AAGO,SAAS,UAAU,SAAiB,QAA+C;AACxF,QAAM,IAAI,QAAQ,MAAM,GAAG;AAC3B,QAAM,IAAI,OAAO,MAAM,GAAG;AAC1B,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,QAAM,SAAiC,CAAC;AACxC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,QAAI,EAAE,CAAC,EAAG,WAAW,GAAG,GAAG;AACzB,UAAI;AACF,eAAO,EAAE,CAAC,EAAG,MAAM,CAAC,CAAC,IAAI,mBAAmB,EAAE,CAAC,CAAE;AAAA,MACnD,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,WAAW,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAAA,EACnC;AACA,SAAO;AACT;;;ACpEO,SAAS,SAAS,SAA+F;AACtH,SAAO,YAAY,WACf;AAAA,IACE,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,cAAc;AAAA,IACd,MAAM;AAAA,IACN,KAAK;AAAA,EACP,IACA;AAAA,IACE,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,cAAc;AAAA,IACd,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AACN;","names":[]}