The Telegram-like group file contract (packages/core shared-files.ts) becomes a working subsystem, built in three parallel tracks: API (NestJS): migration 029 group_files (chat_media_archive metadata-only index with partial replay-unique + RLS, group_file_shares with sha256 token persistence, archive_sync_state/outbox durable-claim seam incl. a unique claim_key index), group-files module (list/search with cursor pagination, direct upload ≤20 MiB streamed+hashed to oCIS WebDAV, download with ACL + retention rechecks, share links with expiry, retention modes incl. legalHold) — RBAC group-membership authz + audit chain events. Web: Files section per group — group picker, drag-drop upload (20 MiB cap), search + MIME/origin filters, cursor pagination, download via authenticated blob fetch, share dialog with expiry + shown-once token, admin retention dialog; bilingual EN/ES (wiki i18n pattern); nav sidebar/palette/bottom-nav wiring; MIME classes aligned to the DB CHECK (document/other). Archive bot (apps/archive-bot): plain sync-loop Matrix bot mirroring attachments from explicitly-mapped unencrypted archive rooms — raw /sync with durable since-cursor, outbox-claimed ingestion (hash-verified stream, no temp files), magic-byte MIME sniffing, safe-name normalization, WebDAV upload, indexed + replied with link, redaction→unavailable; health endpoint; Dockerfile. Tests 74/74, typecheck clean across api/web/bot.
33 lines
1.3 KiB
TypeScript
33 lines
1.3 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { describe, expect, it } from "vitest";
|
|
import { backoffMs, BACKOFF_BASE_MS, BACKOFF_MAX_MS, nextAttemptAt } from "./backoff.js";
|
|
import { claimKey, replyTxnId } from "./keys.js";
|
|
|
|
describe("claimKey", () => {
|
|
it("is the shared attachmentArchiveKey encoding", () => {
|
|
expect(claimKey("w", "!r:x", "$e")).toBe(JSON.stringify(["w", "!r:x", "$e"]));
|
|
});
|
|
it("is collision-free across separators", () => {
|
|
expect(claimKey("a", "b|c", "d")).not.toBe(claimKey("a|b", "c", "d"));
|
|
});
|
|
});
|
|
|
|
describe("replyTxnId", () => {
|
|
it("is the first 24 hex chars of sha256(claim key) and stable", () => {
|
|
const key = claimKey("w", "!r:x", "$e");
|
|
const expected = createHash("sha256").update(key).digest("hex").slice(0, 24);
|
|
expect(replyTxnId(key)).toBe(expected);
|
|
expect(replyTxnId(key)).toMatch(/^[0-9a-f]{24}$/);
|
|
expect(replyTxnId(claimKey("w", "!r:x", "$f"))).not.toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("backoff", () => {
|
|
it("grows exponentially and caps at one hour", () => {
|
|
expect(backoffMs(0)).toBe(0);
|
|
expect(backoffMs(1)).toBe(BACKOFF_BASE_MS);
|
|
expect(backoffMs(3)).toBe(BACKOFF_BASE_MS * 4);
|
|
expect(backoffMs(40)).toBe(BACKOFF_MAX_MS);
|
|
expect(nextAttemptAt(2, new Date(0)).getTime()).toBe(BACKOFF_BASE_MS * 2);
|
|
});
|
|
});
|