From 80e28a4b046bf4af9fd655eaa2d781c71e96ff1f Mon Sep 17 00:00:00 2001 From: vitaliytv Date: Fri, 17 Jul 2026 20:05:17 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(relay):=20eslint=20=D0=BD?= =?UTF-8?q?=D0=B0=20v4-=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=D1=85=20(main?= =?UTF-8?q?=20=D0=B7=D0=BB=D0=B0=D0=BC=D0=B0=D0=BD=D0=BE=20=D0=BF=D1=96?= =?UTF-8?q?=D1=81=D0=BB=D1=8F=20=D0=BC=D0=B5=D1=80=D0=B4=D0=B6=D1=83=20PR#?= =?UTF-8?q?53=20=D0=B4=D0=BE=20=D1=84=D1=96=D0=BA=D1=81=D1=96=D0=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #53 змерджено на коміті 2893506 — до пуша фіксів eslint (module-scope regex, max-classes-per-file, Uint8Array-API), тому main лишився з чотирма порушеннями. Той самий фікс, реаплайнутий поверх поточного main: - DevPushSink винесено з push.mjs у push-sink.mjs (max-classes-per-file) - signing.mjs: Buffer.from(…, 'base64') → Uint8Array.fromBase64() - тести: Buffer …toString('base64') → …toBase64(), [...map.values()] → map.values().toArray(), regex-літерали в toThrow/toMatch — module-scope константи Co-Authored-By: Claude Fable 5 --- relay/.changes/260717-2005.md | 5 +++++ relay/lib/docs/index.md | 17 +++++++++-------- relay/lib/docs/push-sink.md | 31 +++++++++++++++++++++++++++++++ relay/lib/docs/push.md | 2 +- relay/lib/docs/signing.md | 3 +-- relay/lib/push-sink.mjs | 23 +++++++++++++++++++++++ relay/lib/push.mjs | 22 ++-------------------- relay/lib/signing.mjs | 2 +- relay/lib/tests/relay.test.mjs | 16 +++++++++++----- relay/lib/tests/server.test.mjs | 8 +++++--- 10 files changed, 89 insertions(+), 40 deletions(-) create mode 100644 relay/.changes/260717-2005.md create mode 100644 relay/lib/docs/push-sink.md create mode 100644 relay/lib/push-sink.mjs diff --git a/relay/.changes/260717-2005.md b/relay/.changes/260717-2005.md new file mode 100644 index 0000000..08099e6 --- /dev/null +++ b/relay/.changes/260717-2005.md @@ -0,0 +1,5 @@ +--- +bump: patch +section: Fixed +--- +eslint (unicorn/prefer-uint8array-base64, unicorn/prefer-iterator-to-array, max-classes-per-file) на релей-модулях v4: DevPushSink винесено у push-sink.mjs, Buffer base64 → Uint8Array.fromBase64/toBase64, regex-літерали тестів — у module-scope, iterator.toArray() замість spread diff --git a/relay/lib/docs/index.md b/relay/lib/docs/index.md index a22f527..318138e 100644 --- a/relay/lib/docs/index.md +++ b/relay/lib/docs/index.md @@ -4,11 +4,12 @@ title: relay/lib resource: relay/lib/ --- -| Файл | Тип | -| ------------------------- | --------- | -| [push.mjs](push.md) | JS Module | -| [relay.mjs](relay.md) | JS Module | -| [rooms.mjs](rooms.md) | JS Module | -| [server.mjs](server.md) | JS Module | -| [signing.mjs](signing.md) | JS Module | -| [store.mjs](store.md) | JS Module | +| Файл | Тип | +| ----------------------------- | --------- | +| [push-sink.mjs](push-sink.md) | JS Module | +| [push.mjs](push.md) | JS Module | +| [relay.mjs](relay.md) | JS Module | +| [rooms.mjs](rooms.md) | JS Module | +| [server.mjs](server.md) | JS Module | +| [signing.mjs](signing.md) | JS Module | +| [store.mjs](store.md) | JS Module | diff --git a/relay/lib/docs/push-sink.md b/relay/lib/docs/push-sink.md new file mode 100644 index 0000000..29e1b54 --- /dev/null +++ b/relay/lib/docs/push-sink.md @@ -0,0 +1,31 @@ +--- +type: JS Module +title: push-sink.mjs +resource: relay/lib/push-sink.mjs +docgen: + crc: 1c998579 + model: openai-codex/gpt-5.4-mini + tier: cloud-min + score: 100 + issues: judge:inaccurate:0.98 + judgeModel: openai-codex/gpt-5.4-mini +--- + +## Огляд + +`DevPushSink` існує як dev-накопичувач push-сповіщень: він складає нотифікації в памʼять як `magic tokens` в auth і приймає доставку до акаунта через спільний контракт `deliver`, за яким окремою задачею підключається реальний FCM-transport. + +## Поведінка + +1. `DevPushSink` створює dev-накопичувач для push-сповіщень, щоб окремо від реального транспортного шару зберігати факти доставки в пам’яті. +2. `DevPushSink` приймає повідомлення про доставку до акаунта і фіксує їх як запис про отримувача, причину, кореневу подію та, за наявності, посилання на джерело. +3. `DevPushSink` не виконує реальну доставку push і не звертається до зовнішніх систем; це лише тимчасова точка накопичення для подальшого підключення transport-рівня за тим самим контрактом. +4. `DevPushSink` працює без змін у ФС чи БД, тому підходить для локального dev-сценарію та перевірки бізнес-потоків доставки. + +## Публічний API + +- DevPushSink — dev-варіант sink-а для push-доставки, що приймає і передає push-повідомлення у робочому середовищі розробки. + +## Гарантії поведінки + +- Read-only: не виконує операцій запису (ФС/БД). diff --git a/relay/lib/docs/push.md b/relay/lib/docs/push.md index 4f420e2..d42a9cf 100644 --- a/relay/lib/docs/push.md +++ b/relay/lib/docs/push.md @@ -3,7 +3,7 @@ type: JS Module title: push.mjs resource: relay/lib/push.mjs docgen: - crc: a554bd6d + crc: d6df4c66 model: openai-codex/gpt-5.5 score: 100 issues: judge:inaccurate:0.98 diff --git a/relay/lib/docs/signing.md b/relay/lib/docs/signing.md index 662f1b6..622382d 100644 --- a/relay/lib/docs/signing.md +++ b/relay/lib/docs/signing.md @@ -3,9 +3,8 @@ type: JS Module title: signing.mjs resource: relay/lib/signing.mjs docgen: - crc: 9228feae + crc: 0b7d8ba8 model: openai-codex/gpt-5.5 - tier: cloud-avg score: 100 issues: judge:inaccurate:0.99 judgeModel: openai-codex/gpt-5.4-mini diff --git a/relay/lib/push-sink.mjs b/relay/lib/push-sink.mjs new file mode 100644 index 0000000..4c86fac --- /dev/null +++ b/relay/lib/push-sink.mjs @@ -0,0 +1,23 @@ +/** + * Dev-sink push-доставки: складає нотифікації в памʼять (як magic tokens + * в auth) — реальний FCM-транспорт підключається за тим самим інтерфейсом + * `deliver(accountId, note)` окремою задачею (stack.md, «Push»). + */ + +/** Dev-реалізація sink-а push-доставки. */ +export class DevPushSink { + constructor() { + /** @type {{account_id: string, root: string, reason: string, ref: string | null}[]} */ + this.deliveries = [] + } + + /** + * Доставляє push усім пристроям акаунта. + * @param {string} accountId акаунт-отримувач + * @param {{ root: string, reason: string, ref?: string | null }} note зміст + * @returns {void} + */ + deliver(accountId, note) { + this.deliveries.push({ account_id: accountId, root: note.root, reason: note.reason, ref: note.ref ?? null }) + } +} diff --git a/relay/lib/push.mjs b/relay/lib/push.mjs index 5eb2679..0a2b234 100644 --- a/relay/lib/push.mjs +++ b/relay/lib/push.mjs @@ -1,7 +1,7 @@ /** * Push-нотифікації relay (access.md, «Push-нотифікації»): «вас запрошено» * (тип 2) і «задача потребує уваги» (тип 3). FCM-доставка — окрема задача; - * тут інтерфейс sink-а і dev-реалізація з чергою в памʼяті (як magic tokens + * тут інтерфейс sink-а (dev-реалізація — `push-sink.mjs`, як magic tokens * в auth). Relay не парсить payload далі роутінгових полів — для push * роутінговими є `event.type` і адресний `event.to_account_id`. */ @@ -9,28 +9,10 @@ /** Типи подій Envelope, що означають «задача потребує уваги» (тип 3). */ const ATTENTION_TYPES = new Set(['PlanReview', 'AuditPending', 'Escalation']) -/** Dev-sink: складає доставки в памʼять (реальний FCM — за тим самим інтерфейсом). */ -export class DevPushSink { - constructor() { - /** @type {{account_id: string, root: string, reason: string, ref: string | null}[]} */ - this.deliveries = [] - } - - /** - * Доставляє push усім пристроям акаунта. - * @param {string} accountId акаунт-отримувач - * @param {{ root: string, reason: string, ref?: string | null }} note зміст - * @returns {void} - */ - deliver(accountId, note) { - this.deliveries.push({ account_id: accountId, root: note.root, reason: note.reason, ref: note.ref ?? null }) - } -} - /** Маршрутизатор push поверх store і sink-а. */ export class PushRouter { /** - * @param {{ store: import('./store.mjs').InMemoryStore, sink: DevPushSink }} deps залежності + * @param {{ store: import('./store.mjs').InMemoryStore, sink: import('./push-sink.mjs').DevPushSink }} deps залежності */ constructor({ store, sink }) { this.store = store diff --git a/relay/lib/signing.mjs b/relay/lib/signing.mjs index 481e4b4..09d5e02 100644 --- a/relay/lib/signing.mjs +++ b/relay/lib/signing.mjs @@ -39,7 +39,7 @@ export function verifySignature(pubkeyHex, message, signatureBase64) { if (!PUBKEY_RE.test(pubkeyHex)) return false let signature try { - signature = Buffer.from(signatureBase64, 'base64') + signature = Uint8Array.fromBase64(signatureBase64) } catch { return false } diff --git a/relay/lib/tests/relay.test.mjs b/relay/lib/tests/relay.test.mjs index 39f2a96..01c9f0a 100644 --- a/relay/lib/tests/relay.test.mjs +++ b/relay/lib/tests/relay.test.mjs @@ -3,7 +3,8 @@ import { generateKeyPairSync, sign } from 'node:crypto' import { beforeEach, describe, expect, test } from 'vitest' -import { DevPushSink, PushRouter } from '../push.mjs' +import { DevPushSink } from '../push-sink.mjs' +import { PushRouter } from '../push.mjs' import { RelayCore } from '../relay.mjs' import { Rooms } from '../rooms.mjs' import { transferMessage } from '../signing.mjs' @@ -14,6 +15,8 @@ const RE_VIEWER = /viewer/ const RE_OWNER_ONLY = /owner/ const RE_FOREIGN_ACCOUNT = /іншому акаунту/ const RE_ALREADY_PROCESSED = /оброблене/ +const RE_HEX = /hex/ +const RE_SIGNATURE = /підпис/ /** @type {InMemoryStore} */ let store @@ -183,7 +186,7 @@ describe('pubkeys', () => { test('registerDevice відхиляє pubkey не у hex-32 форматі', () => { expect(() => store.registerDevice(accounts.owner.account_id, { name: 'bad', role: 'client', pubkey: 'pk-bad' }) - ).toThrow(/hex/) + ).toThrow(RE_HEX) }) }) @@ -203,7 +206,7 @@ describe('підписаний transfer ownership', () => { }) return { device: store.deviceByToken(device_token), - signTransfer: payload => sign(null, transferMessage(payload), privateKey).toString('base64') + signTransfer: payload => sign(null, transferMessage(payload), privateKey).toBase64() } } @@ -222,7 +225,7 @@ describe('підписаний transfer ownership', () => { device: signer, signature: bad }) - ).toThrow(/підпис/) + ).toThrow(RE_SIGNATURE) expect(store.memberRole('root-1', accounts.owner.account_id)).toBe('owner') core.transferOwnership('root-1', accounts.owner.account_id, accounts.approver.account_id, { @@ -307,7 +310,10 @@ describe('bootstrapMembers', () => { // Повторний прогін: без нових запрошень і без зміни ролей. const again = core.bootstrapMembers(accounts.owner.account_id, 'root-1', entries) expect(again).toEqual({ added: [], invited: ['ghost@x'], kept: ['olena@x', 'viewer@x'] }) - const pending = [...store.invitations.values()].filter(i => i.to_email === 'ghost@x') + const pending = store.invitations + .values() + .filter(i => i.to_email === 'ghost@x') + .toArray() expect(pending).toHaveLength(1) }) }) diff --git a/relay/lib/tests/server.test.mjs b/relay/lib/tests/server.test.mjs index 8132ad9..f4943cb 100644 --- a/relay/lib/tests/server.test.mjs +++ b/relay/lib/tests/server.test.mjs @@ -21,6 +21,8 @@ function fakeKey(name) { const RE_HELLO = /hello/ const RE_VIEWER = /viewer/ +const RE_HEX_KEY = /^[0-9a-f]{64}$/ +const RE_SIGNATURE = /підпис/ // Тести ходять на локальний loopback без TLS; sdl-правило про insecure-URL // націлене на продакшн-адреси, тому схему складаємо окремо від хоста. const WS_SCHEME = 'ws:' @@ -136,7 +138,7 @@ test('pubkeys-кадр: pubkey-и approver+ пристроїв для перев expect(reply.root).toBe('root-1') // Owner (approver+) — так; viewer — ні. expect(reply.pubkeys.map(k => k.account_id)).toEqual([owner.account_id]) - expect(reply.pubkeys[0].pubkey).toMatch(/^[0-9a-f]{64}$/) + expect(reply.pubkeys[0].pubkey).toMatch(RE_HEX_KEY) socket.close() }) @@ -171,13 +173,13 @@ test('transfer_ownership через WS: без підпису — error, з пі to_account: approver.account_id }) expect(unsigned.kind).toBe('error') - expect(unsigned.message).toMatch(/підпис/) + expect(unsigned.message).toMatch(RE_SIGNATURE) const signature = sign( null, transferMessage({ root: 'root-1', fromAccount: owner.account_id, toAccount: approver.account_id }), ownerPrivateKey - ).toString('base64') + ).toBase64() const transferred = await roundtrip(socket, { kind: 'transfer_ownership', root: 'root-1',