From 9ca4222c0db9c30cb1f9640452a9635f9b0a731d Mon Sep 17 00:00:00 2001 From: autocommit Date: Thu, 14 May 2026 22:58:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(collector):=20=E2=9C=A8=20Add=20Corp,=20Do?= =?UTF-8?q?main,=20and=20VisitorSalt=20entity=20models=20for=20cross-domai?= =?UTF-8?q?n=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../collector/src/entities/corp.entity.ts | 25 ++++++++++++ .../collector/src/entities/domain.entity.ts | 39 +++++++++++++++++++ services/collector/src/entities/index.ts | 4 ++ .../src/entities/visitor-salt.entity.ts | 17 ++++++++ 4 files changed, 85 insertions(+) create mode 100644 services/collector/src/entities/corp.entity.ts create mode 100644 services/collector/src/entities/domain.entity.ts create mode 100644 services/collector/src/entities/visitor-salt.entity.ts diff --git a/services/collector/src/entities/corp.entity.ts b/services/collector/src/entities/corp.entity.ts new file mode 100644 index 0000000..0f0862d --- /dev/null +++ b/services/collector/src/entities/corp.entity.ts @@ -0,0 +1,25 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +/** + * Corp — top-level brand/legal-entity grouping for cross-domain analytics. + * One corp owns 1..N domains. Cross-corp flow queries pivot on this. + */ +@Entity('corps') +export class Corp { + @PrimaryGeneratedColumn({ type: 'smallint' }) + id!: number; + + @Column({ type: 'varchar', length: 64, unique: true }) + slug!: string; + + @Column({ type: 'varchar', length: 255, name: 'legal_name' }) + legalName!: string; + + @CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) + createdAt!: Date; +} diff --git a/services/collector/src/entities/domain.entity.ts b/services/collector/src/entities/domain.entity.ts new file mode 100644 index 0000000..3ea1137 --- /dev/null +++ b/services/collector/src/entities/domain.entity.ts @@ -0,0 +1,39 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, + Index, +} from 'typeorm'; +import { Corp } from './corp.entity'; + +export type DomainRole = 'canonical' | 'alias' | 'seo_bait' | 'preview'; + +/** + * Domain — hostname registered against a corp. Used to dimension every event + * with corp_id + domain_id derived from the request Origin/Referer/Host. + */ +@Entity('domains') +@Index(['corpId']) +export class Domain { + @PrimaryGeneratedColumn() + id!: number; + + @Column({ type: 'smallint', name: 'corp_id' }) + corpId!: number; + + @ManyToOne(() => Corp) + @JoinColumn({ name: 'corp_id' }) + corp!: Corp; + + @Column({ type: 'varchar', length: 255, unique: true }) + hostname!: string; + + @Column({ type: 'varchar', length: 16 }) + role!: DomainRole; + + @CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) + createdAt!: Date; +} diff --git a/services/collector/src/entities/index.ts b/services/collector/src/entities/index.ts index cb0680b..230d756 100644 --- a/services/collector/src/entities/index.ts +++ b/services/collector/src/entities/index.ts @@ -1,2 +1,6 @@ export { RawEvent } from './raw-event.entity'; export { SessionFingerprint } from './session-fingerprint.entity'; +export { Corp } from "./corp.entity"; +export { Domain } from "./domain.entity"; +export type { DomainRole } from "./domain.entity"; +export { VisitorSalt } from "./visitor-salt.entity"; diff --git a/services/collector/src/entities/visitor-salt.entity.ts b/services/collector/src/entities/visitor-salt.entity.ts new file mode 100644 index 0000000..d88e354 --- /dev/null +++ b/services/collector/src/entities/visitor-salt.entity.ts @@ -0,0 +1,17 @@ +import { Entity, PrimaryColumn, Column } from 'typeorm'; + +/** + * Daily-rotating salt for visitor_id_daily. One row per UTC day. + * Salts older than ~7 days are purged to make historical re-identification + * mathematically impossible (cookie-free, GDPR-clean). + */ +@Entity('visitor_salts') +export class VisitorSalt { + /** UTC date (YYYY-MM-DD). Stored as DATE, no time component. */ + @PrimaryColumn({ type: 'date' }) + day!: string; + + /** 32 random bytes. */ + @Column({ type: 'bytea' }) + salt!: Buffer; +}