@hogsend/db 0.21.0 → 0.22.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/drizzle/0025_contact_discord_id.sql +2 -0
- package/drizzle/0026_warm_freak.sql +15 -0
- package/drizzle/meta/0025_snapshot.json +3767 -0
- package/drizzle/meta/0026_snapshot.json +3907 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/schema/connector-link-codes.ts +68 -0
- package/src/schema/contact-aliases.ts +1 -1
- package/src/schema/contacts.ts +13 -0
- package/src/schema/index.ts +1 -0
|
@@ -176,6 +176,20 @@
|
|
|
176
176
|
"when": 1781282015159,
|
|
177
177
|
"tag": "0024_provider_credentials",
|
|
178
178
|
"breakpoints": true
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"idx": 25,
|
|
182
|
+
"version": "7",
|
|
183
|
+
"when": 1781371526854,
|
|
184
|
+
"tag": "0025_contact_discord_id",
|
|
185
|
+
"breakpoints": true
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"idx": 26,
|
|
189
|
+
"version": "7",
|
|
190
|
+
"when": 1781382218591,
|
|
191
|
+
"tag": "0026_warm_freak",
|
|
192
|
+
"breakpoints": true
|
|
179
193
|
}
|
|
180
194
|
]
|
|
181
195
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
index,
|
|
3
|
+
pgTable,
|
|
4
|
+
text,
|
|
5
|
+
timestamp,
|
|
6
|
+
uniqueIndex,
|
|
7
|
+
uuid,
|
|
8
|
+
} from "drizzle-orm/pg-core";
|
|
9
|
+
import { timestamps } from "./_shared.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Single-use verification codes for the native in-connector identify loop
|
|
13
|
+
* (Discord `/link <email>` → emailed code → `/verify <code>`). A member runs
|
|
14
|
+
* `/link`, we mint a code, email it via Hogsend, and store ONLY a sha256 hash
|
|
15
|
+
* of the code here (never the plaintext — the code lives only in the member's
|
|
16
|
+
* inbox). `/verify` re-hashes the typed code, looks the row up by hash, and
|
|
17
|
+
* redeems it: single-use (`used_at IS NULL` guard), TTL'd (`expires_at`), and
|
|
18
|
+
* BOUND to the invoking platform user (`platform_user_id`, constant-time
|
|
19
|
+
* compared) so a code is only valid for the same account that requested it.
|
|
20
|
+
*
|
|
21
|
+
* The `connector_id` + `target_email` columns also back the anti-email-bomb
|
|
22
|
+
* throttle: the create path counts recent rows per invoking user AND per target
|
|
23
|
+
* email within a rolling window and refuses to mint (and send) once either cap
|
|
24
|
+
* is hit — counting on MINT (rows are never deleted on redeem/expiry, only
|
|
25
|
+
* marked used / left to age out) is what makes it an email-bomb control.
|
|
26
|
+
*/
|
|
27
|
+
export const connectorLinkCodes = pgTable(
|
|
28
|
+
"connector_link_codes",
|
|
29
|
+
{
|
|
30
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
31
|
+
// The connector this code belongs to (e.g. "discord"). Scopes throttle
|
|
32
|
+
// counts + lets one engine serve multiple connectors' link loops.
|
|
33
|
+
connectorId: text("connector_id").notNull(),
|
|
34
|
+
// sha256(code) as lowercase hex — the lookup key. The plaintext code is
|
|
35
|
+
// NEVER stored; it exists only in the emailed message.
|
|
36
|
+
codeHash: text("code_hash").notNull(),
|
|
37
|
+
// The invoking platform user the code is BOUND to (Discord snowflake). A
|
|
38
|
+
// redeem must present the SAME platform user id or it is rejected.
|
|
39
|
+
platformUserId: text("platform_user_id").notNull(),
|
|
40
|
+
// The authoritative email the code was issued for (the resolution key the
|
|
41
|
+
// redeem attaches to the platform identity). Stored normalized
|
|
42
|
+
// (trim + toLowerCase) by the caller.
|
|
43
|
+
targetEmail: text("target_email").notNull(),
|
|
44
|
+
// Absolute expiry. A redeem after this instant is rejected as expired.
|
|
45
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
46
|
+
// Single-use marker: NULL until redeemed, set to the redemption instant by
|
|
47
|
+
// the atomic `UPDATE ... WHERE used_at IS NULL`. A second redeem misses.
|
|
48
|
+
usedAt: timestamp("used_at", { withTimezone: true }),
|
|
49
|
+
...timestamps,
|
|
50
|
+
},
|
|
51
|
+
(table) => [
|
|
52
|
+
// Redeem looks the row up by hash. Unique: a hash collision (or a duplicate
|
|
53
|
+
// mint of the identical code) must never produce two redeemable rows.
|
|
54
|
+
uniqueIndex("connector_link_codes_code_hash_idx").on(table.codeHash),
|
|
55
|
+
// Serves the per-invoking-user throttle COUNT (connector + user + recency).
|
|
56
|
+
index("connector_link_codes_throttle_user_idx").on(
|
|
57
|
+
table.connectorId,
|
|
58
|
+
table.platformUserId,
|
|
59
|
+
table.createdAt,
|
|
60
|
+
),
|
|
61
|
+
// Serves the per-target-email throttle COUNT (connector + email + recency).
|
|
62
|
+
index("connector_link_codes_throttle_email_idx").on(
|
|
63
|
+
table.connectorId,
|
|
64
|
+
table.targetEmail,
|
|
65
|
+
table.createdAt,
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
);
|
|
@@ -18,7 +18,7 @@ export const contactAliases = pgTable(
|
|
|
18
18
|
contactId: uuid("contact_id")
|
|
19
19
|
.notNull()
|
|
20
20
|
.references(() => contacts.id, { onDelete: "cascade" }),
|
|
21
|
-
// 'email' | 'external' | 'anonymous'
|
|
21
|
+
// 'email' | 'external' | 'anonymous' | 'discord'
|
|
22
22
|
aliasKind: text("alias_kind").notNull(),
|
|
23
23
|
// The stale key value (the loser's old external_id / normalized email /
|
|
24
24
|
// anonymous_id).
|
package/src/schema/contacts.ts
CHANGED
|
@@ -31,6 +31,16 @@ export const contacts = pgTable(
|
|
|
31
31
|
* index scoped to live, non-deleted rows.
|
|
32
32
|
*/
|
|
33
33
|
anonymousId: text("anonymous_id"),
|
|
34
|
+
/**
|
|
35
|
+
* Nullable Discord user id (snowflake) attached to an email-keyed contact
|
|
36
|
+
* when a member completes the per-member OAuth link. Like external_id it is
|
|
37
|
+
* a RESOLVABLE identity key (a fourth `Kind`), NOT a property — but it is
|
|
38
|
+
* NEVER the canonical text key (`external_id ?? anonymous_id ?? id`), so it
|
|
39
|
+
* does not participate in the history re-point. Uniqueness is the
|
|
40
|
+
* partial-unique live-row index below, identical to
|
|
41
|
+
* contacts_external_id_unique_idx.
|
|
42
|
+
*/
|
|
43
|
+
discordId: text("discord_id"),
|
|
34
44
|
/**
|
|
35
45
|
* Opportunistic IANA-timezone cache (e.g. "America/New_York"). Populated
|
|
36
46
|
* best-effort when a tz is resolved from PostHog person props. PostHog and
|
|
@@ -69,5 +79,8 @@ export const contacts = pgTable(
|
|
|
69
79
|
uniqueIndex("contacts_anonymous_id_unique_idx")
|
|
70
80
|
.on(table.anonymousId)
|
|
71
81
|
.where(sql`anonymous_id IS NOT NULL AND deleted_at IS NULL`),
|
|
82
|
+
uniqueIndex("contacts_discord_id_unique_idx")
|
|
83
|
+
.on(table.discordId)
|
|
84
|
+
.where(sql`discord_id IS NOT NULL AND deleted_at IS NULL`),
|
|
72
85
|
],
|
|
73
86
|
);
|
package/src/schema/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./auth.js";
|
|
|
6
6
|
export * from "./bucket-configs.js";
|
|
7
7
|
export * from "./bucket-memberships.js";
|
|
8
8
|
export * from "./campaigns.js";
|
|
9
|
+
export * from "./connector-link-codes.js";
|
|
9
10
|
export * from "./contact-aliases.js";
|
|
10
11
|
export * from "./contacts.js";
|
|
11
12
|
export * from "./dead-letter-queue.js";
|