@vibes.diy/api-svc 14.1.37 → 14.1.38

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 (53) hide show
  1. package/cf-serve.d.ts +2 -0
  2. package/cf-serve.js +9 -2
  3. package/cf-serve.js.map +1 -1
  4. package/create-handler.js +2 -0
  5. package/create-handler.js.map +1 -1
  6. package/evento-handler-manifest.js +2 -0
  7. package/evento-handler-manifest.js.map +1 -1
  8. package/intern/ensure-slug-binding.js +1 -0
  9. package/intern/ensure-slug-binding.js.map +1 -1
  10. package/intern/hubspot-maker-facts.d.ts +5 -0
  11. package/intern/hubspot-maker-facts.js +86 -4
  12. package/intern/hubspot-maker-facts.js.map +1 -1
  13. package/intern/hubspot-signup.d.ts +6 -0
  14. package/intern/hubspot-signup.js +5 -2
  15. package/intern/hubspot-signup.js.map +1 -1
  16. package/intern/maker-identity.d.ts +41 -0
  17. package/intern/maker-identity.js +122 -0
  18. package/intern/maker-identity.js.map +1 -0
  19. package/intern/mint-job-store.js +0 -2
  20. package/intern/mint-job-store.js.map +1 -1
  21. package/intern/signup-attribution-capture.js +15 -3
  22. package/intern/signup-attribution-capture.js.map +1 -1
  23. package/package.json +12 -11
  24. package/public/recapture-screenshot.d.ts +5 -0
  25. package/public/recapture-screenshot.js +83 -0
  26. package/public/recapture-screenshot.js.map +1 -0
  27. package/public/report-activity.js +2 -2
  28. package/public/report-activity.js.map +1 -1
  29. package/public/report-cache.d.ts +5 -2
  30. package/public/report-cache.js +4 -1
  31. package/public/report-cache.js.map +1 -1
  32. package/public/set-mode-fsid.js +13 -2
  33. package/public/set-mode-fsid.js.map +1 -1
  34. package/public/vibe-mail-image.d.ts +10 -0
  35. package/public/vibe-mail-image.js +80 -0
  36. package/public/vibe-mail-image.js.map +1 -0
  37. package/types.d.ts +1 -0
  38. package/types.js.map +1 -1
  39. package/usage-report/backfill-hubspot-contacts.js +83 -3
  40. package/usage-report/backfill-hubspot-contacts.js.map +1 -1
  41. package/usage-report/gold-drip-sweep-util.d.ts +116 -0
  42. package/usage-report/gold-drip-sweep-util.js +197 -0
  43. package/usage-report/gold-drip-sweep-util.js.map +1 -0
  44. package/usage-report/gold-drip-sweep.d.ts +2 -0
  45. package/usage-report/gold-drip-sweep.js +327 -0
  46. package/usage-report/gold-drip-sweep.js.map +1 -0
  47. package/usage-report/new-user-alert-util.d.ts +9 -0
  48. package/usage-report/new-user-alert-util.js +10 -1
  49. package/usage-report/new-user-alert-util.js.map +1 -1
  50. package/usage-report/new-user-alert.js +13 -6
  51. package/usage-report/new-user-alert.js.map +1 -1
  52. package/vibes-req-res-evento.js +2 -1
  53. package/vibes-req-res-evento.js.map +1 -1
@@ -0,0 +1,116 @@
1
+ export { sanitizeForDiscord, appUrl } from "./new-user-alert-util.js";
2
+ export declare const FIRST_TURN_WINDOW_HOURS = 24;
3
+ export declare const QUIET_MINUTES = 15;
4
+ export declare const QUALIFYING_TURN_MIN_TOTAL_TOKENS = 1000;
5
+ export declare const UNKNOWN_USER_ID = "unknown";
6
+ export declare const BAR_REQUIRES_TWO_TURNS = true;
7
+ export declare function meetsBar(qualifyingTurns: number): boolean;
8
+ export declare const SCREENSHOT_TRUSTWORTHY_FROM_ISO = "2026-09-09T18:17:09Z";
9
+ export declare function screenshotIsTrustworthy(releaseCreatedIso: string): boolean;
10
+ export declare const CLAIM_PROPERTY = "gold_drip_claimed_at";
11
+ export declare const CONTACT_PROPERTIES_READ: readonly string[];
12
+ export declare const HUBSPOT_PORTAL_ID = "47913361";
13
+ export declare function hubspotContactUrl(contactId: string): string;
14
+ export declare const GOLD_DRIP_SWEEP_ENV_VARS: readonly string[];
15
+ export declare function sweepIsEnabled(raw: string | undefined): boolean;
16
+ export declare const EXTRA_EXCLUDED_HANDLE_PATTERNS: readonly RegExp[];
17
+ export interface UserSettingsFacts {
18
+ readonly contactEmail?: string;
19
+ readonly geoCountry?: string;
20
+ }
21
+ export declare function readSettingsFacts(settings: unknown): UserSettingsFacts;
22
+ export interface ProductGateInput {
23
+ readonly userId: string;
24
+ readonly handles: readonly string[];
25
+ readonly firstTurnIso: string;
26
+ readonly lastTurnIso: string;
27
+ readonly qualifyingTurns: number;
28
+ readonly settings: UserSettingsFacts;
29
+ readonly windowStartIso: string;
30
+ readonly quietBeforeIso: string;
31
+ readonly staffOrQa: boolean;
32
+ }
33
+ export type ProductSkip = "staff-or-qa" | "first-turn-outside-window" | "still-working" | "bar-not-met" | "country-unknown" | "not-us" | "no-contact-email" | "invalid-contact-email";
34
+ export type ProductVerdict = {
35
+ readonly ok: true;
36
+ readonly email: string;
37
+ } | {
38
+ readonly ok: false;
39
+ readonly reason: ProductSkip;
40
+ };
41
+ export declare function gateProduct(input: ProductGateInput): ProductVerdict;
42
+ export type ContactLookup = {
43
+ readonly state: "found";
44
+ readonly id: string;
45
+ readonly claimedAt?: string;
46
+ readonly isInternal?: boolean;
47
+ readonly supersededBy?: string;
48
+ } | {
49
+ readonly state: "absent";
50
+ } | {
51
+ readonly state: "failed";
52
+ readonly reason: string;
53
+ };
54
+ export type ContactSkip = "no-contact" | "lookup-failed" | "internal" | "superseded" | "already-claimed";
55
+ export type ContactVerdict = {
56
+ readonly ok: true;
57
+ readonly contactId: string;
58
+ } | {
59
+ readonly ok: false;
60
+ readonly reason: ContactSkip;
61
+ };
62
+ export declare function gateContact(lookup: ContactLookup): ContactVerdict;
63
+ export declare function asHubSpotBool(raw: unknown): boolean | undefined;
64
+ export declare function asHubSpotString(raw: unknown): string | undefined;
65
+ export declare function readContactRow(row: {
66
+ readonly id?: unknown;
67
+ readonly properties?: Record<string, unknown>;
68
+ }): ContactLookup;
69
+ export declare const MISSING_CLAIM_PROPERTY_MESSAGE: string;
70
+ export declare const MAX_PROMPT_CHARS = 280;
71
+ export declare function truncatePrompt(text: string, max?: number): string;
72
+ export declare function extractUserPromptFromBlocks(blocks: unknown): string | undefined;
73
+ export declare function mailImageUrl(opts: {
74
+ readonly publicBaseUrl: string;
75
+ readonly appSlug: string;
76
+ readonly fsId: string;
77
+ readonly ownerUserId: string;
78
+ readonly token: string;
79
+ }): string;
80
+ export interface DiscordEmbedBody {
81
+ readonly content?: string;
82
+ readonly allowed_mentions?: {
83
+ readonly parse: readonly string[];
84
+ readonly users?: readonly string[];
85
+ };
86
+ readonly embeds?: readonly {
87
+ readonly title?: string;
88
+ readonly description?: string;
89
+ readonly color?: number;
90
+ readonly image?: {
91
+ readonly url: string;
92
+ };
93
+ }[];
94
+ }
95
+ export declare const DISCORD_DESCRIPTION_LIMIT = 4096;
96
+ export interface ClaimedMaker {
97
+ readonly handle: string;
98
+ readonly appSlug: string;
99
+ readonly ownerHandle: string;
100
+ readonly published: boolean;
101
+ readonly qualifyingTurns: number;
102
+ readonly firstTurnIso: string;
103
+ readonly contactId: string;
104
+ readonly firstPrompt?: string;
105
+ readonly imageUrl?: string;
106
+ readonly imageOmittedBecause?: string;
107
+ }
108
+ export declare const NO_REVIEW_YET_NOTE = "No review and no email exist yet \u2014 nothing has been written, drafted or sent. This post is the queue: it is asking a human to go and look.";
109
+ export declare function mentionString(userId: string): string;
110
+ export declare function buildGoldDripMessage(maker: ClaimedMaker, mentionUserId: string): DiscordEmbedBody;
111
+ export declare function logLine(input: {
112
+ readonly handle: string;
113
+ readonly ownerHandle?: string;
114
+ readonly appSlug?: string;
115
+ readonly verdict: string;
116
+ }): string;
@@ -0,0 +1,197 @@
1
+ import { MAIL_IMAGE_TOKEN_PARAM, MAIL_IMAGE_USER_PARAM, isUsCountry, isUserSettingContactEmail, isUserSettingGeoCountry, } from "@vibes.diy/api-types";
2
+ import { isValidEmailContact } from "../public/social-invite-store.js";
3
+ export { sanitizeForDiscord, appUrl } from "./new-user-alert-util.js";
4
+ import { sanitizeForDiscord, appUrl } from "./new-user-alert-util.js";
5
+ export const FIRST_TURN_WINDOW_HOURS = 24;
6
+ export const QUIET_MINUTES = 15;
7
+ export const QUALIFYING_TURN_MIN_TOTAL_TOKENS = 1000;
8
+ export const UNKNOWN_USER_ID = "unknown";
9
+ export const BAR_REQUIRES_TWO_TURNS = true;
10
+ export function meetsBar(qualifyingTurns) {
11
+ return qualifyingTurns >= (BAR_REQUIRES_TWO_TURNS ? 2 : 1);
12
+ }
13
+ export const SCREENSHOT_TRUSTWORTHY_FROM_ISO = "2026-09-09T18:17:09Z";
14
+ export function screenshotIsTrustworthy(releaseCreatedIso) {
15
+ return releaseCreatedIso >= SCREENSHOT_TRUSTWORTHY_FROM_ISO;
16
+ }
17
+ export const CLAIM_PROPERTY = "gold_drip_claimed_at";
18
+ export const CONTACT_PROPERTIES_READ = [
19
+ CLAIM_PROPERTY,
20
+ "clerk_user_id",
21
+ "is_internal",
22
+ "identity_superseded_by",
23
+ "email",
24
+ ];
25
+ export const HUBSPOT_PORTAL_ID = "47913361";
26
+ export function hubspotContactUrl(contactId) {
27
+ return `https://app.hubspot.com/contacts/${HUBSPOT_PORTAL_ID}/record/0-1/${contactId}`;
28
+ }
29
+ export const GOLD_DRIP_SWEEP_ENV_VARS = [
30
+ "NEON_DATABASE_URL",
31
+ "HUBSPOT_TOKEN",
32
+ "DISCORD_WEBHOOK_URL",
33
+ "GOLD_DRIP_SWEEP_ENABLED",
34
+ "GOLD_DRIP_MENTION_ID",
35
+ "BACKEND_INTERNAL_SECRET",
36
+ "VIBES_DIY_PUBLIC_BASE_URL",
37
+ "ALERT_EXCLUDE_DOMAINS",
38
+ "ALERT_EXCLUDE_USER_IDS",
39
+ ];
40
+ export function sweepIsEnabled(raw) {
41
+ return raw === "true";
42
+ }
43
+ export const EXTRA_EXCLUDED_HANDLE_PATTERNS = [/^eval$/i];
44
+ export function readSettingsFacts(settings) {
45
+ const items = Array.isArray(settings) ? settings : [];
46
+ const contact = items.find(isUserSettingContactEmail);
47
+ const geo = items.find(isUserSettingGeoCountry);
48
+ return { contactEmail: contact?.email, geoCountry: geo?.country };
49
+ }
50
+ export function gateProduct(input) {
51
+ if (input.staffOrQa)
52
+ return { ok: false, reason: "staff-or-qa" };
53
+ if (input.firstTurnIso < input.windowStartIso)
54
+ return { ok: false, reason: "first-turn-outside-window" };
55
+ if (input.lastTurnIso > input.quietBeforeIso)
56
+ return { ok: false, reason: "still-working" };
57
+ if (meetsBar(input.qualifyingTurns) === false)
58
+ return { ok: false, reason: "bar-not-met" };
59
+ const country = input.settings.geoCountry;
60
+ if (country === undefined || country === "")
61
+ return { ok: false, reason: "country-unknown" };
62
+ if (isUsCountry(country) === false)
63
+ return { ok: false, reason: "not-us" };
64
+ const email = input.settings.contactEmail;
65
+ if (email === undefined || email === "")
66
+ return { ok: false, reason: "no-contact-email" };
67
+ if (isValidEmailContact(email) === false)
68
+ return { ok: false, reason: "invalid-contact-email" };
69
+ return { ok: true, email };
70
+ }
71
+ export function gateContact(lookup) {
72
+ switch (lookup.state) {
73
+ case "absent":
74
+ return { ok: false, reason: "no-contact" };
75
+ case "failed":
76
+ return { ok: false, reason: "lookup-failed" };
77
+ case "found":
78
+ if (lookup.isInternal === true)
79
+ return { ok: false, reason: "internal" };
80
+ if (lookup.supersededBy !== undefined && lookup.supersededBy !== "")
81
+ return { ok: false, reason: "superseded" };
82
+ if (lookup.claimedAt !== undefined && lookup.claimedAt !== "")
83
+ return { ok: false, reason: "already-claimed" };
84
+ return { ok: true, contactId: lookup.id };
85
+ }
86
+ }
87
+ export function asHubSpotBool(raw) {
88
+ if (raw === true || raw === "true")
89
+ return true;
90
+ if (raw === false || raw === "false")
91
+ return false;
92
+ return undefined;
93
+ }
94
+ export function asHubSpotString(raw) {
95
+ return typeof raw === "string" && raw !== "" ? raw : undefined;
96
+ }
97
+ export function readContactRow(row) {
98
+ const id = asHubSpotString(row.id);
99
+ if (id === undefined)
100
+ return { state: "failed", reason: "result carried no contact id" };
101
+ const props = row.properties ?? {};
102
+ return {
103
+ state: "found",
104
+ id,
105
+ claimedAt: asHubSpotString(props[CLAIM_PROPERTY]),
106
+ isInternal: asHubSpotBool(props["is_internal"]),
107
+ supersededBy: asHubSpotString(props["identity_superseded_by"]),
108
+ };
109
+ }
110
+ export const MISSING_CLAIM_PROPERTY_MESSAGE = `HubSpot has no contact property "${CLAIM_PROPERTY}" — the sweep cannot tell a claimed maker from an unclaimed one, ` +
111
+ `and guessing would re-post everybody every fifteen minutes. It is defined in traction's hubspot/schema.md and applied ` +
112
+ `to the portal by a human running hubspot/apply-schema.mjs. Nothing was posted and nothing was claimed.`;
113
+ export const MAX_PROMPT_CHARS = 280;
114
+ export function truncatePrompt(text, max = MAX_PROMPT_CHARS) {
115
+ const collapsed = text.replaceAll(/\s+/g, " ").trim();
116
+ return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}…`;
117
+ }
118
+ export function extractUserPromptFromBlocks(blocks) {
119
+ if (!Array.isArray(blocks))
120
+ return undefined;
121
+ for (const raw of blocks) {
122
+ const block = raw;
123
+ if (block.type !== "prompt.req")
124
+ continue;
125
+ const messages = block.request?.messages;
126
+ if (!Array.isArray(messages))
127
+ continue;
128
+ for (const m of messages) {
129
+ const msg = m;
130
+ if (msg.role !== "user")
131
+ continue;
132
+ if (typeof msg.content === "string" && msg.content.trim() !== "")
133
+ return msg.content;
134
+ if (!Array.isArray(msg.content))
135
+ continue;
136
+ const text = msg.content
137
+ .map((p) => p)
138
+ .filter((p) => p.type === "text" && typeof p.text === "string")
139
+ .map((p) => p.text)
140
+ .join("\n")
141
+ .trim();
142
+ if (text !== "")
143
+ return text;
144
+ }
145
+ }
146
+ return undefined;
147
+ }
148
+ export function mailImageUrl(opts) {
149
+ const base = opts.publicBaseUrl.replace(/\/+$/, "");
150
+ const q = new URLSearchParams({ [MAIL_IMAGE_USER_PARAM]: opts.ownerUserId, [MAIL_IMAGE_TOKEN_PARAM]: opts.token });
151
+ return `${base}/assets/vibe-mail-image/${encodeURIComponent(opts.appSlug)}/${encodeURIComponent(opts.fsId)}.png?${q.toString()}`;
152
+ }
153
+ const COLOR_GOLD = 16766720;
154
+ export const DISCORD_DESCRIPTION_LIMIT = 4096;
155
+ export const NO_REVIEW_YET_NOTE = "No review and no email exist yet — nothing has been written, drafted or sent. This post is the queue: it is asking a human to go and look.";
156
+ export function mentionString(userId) {
157
+ return `<@${userId}>`;
158
+ }
159
+ export function buildGoldDripMessage(maker, mentionUserId) {
160
+ const link = maker.published
161
+ ? appUrl(maker.ownerHandle, maker.appSlug)
162
+ : `${sanitizeForDiscord(`${maker.ownerHandle}/${maker.appSlug}`)} — not published`;
163
+ const lines = [
164
+ `**@${sanitizeForDiscord(maker.handle)}** · ${maker.qualifyingTurns} turn${maker.qualifyingTurns === 1 ? "" : "s"} · first turn ${maker.firstTurnIso}`,
165
+ link,
166
+ hubspotContactUrl(maker.contactId),
167
+ ];
168
+ if (maker.firstPrompt !== undefined && maker.firstPrompt !== "") {
169
+ lines.push("", `> ${sanitizeForDiscord(truncatePrompt(maker.firstPrompt))}`);
170
+ }
171
+ if (maker.imageUrl === undefined && maker.imageOmittedBecause !== undefined) {
172
+ lines.push("", `_No screenshot: ${sanitizeForDiscord(maker.imageOmittedBecause)}._`);
173
+ }
174
+ lines.push("", NO_REVIEW_YET_NOTE);
175
+ let description = lines.join("\n");
176
+ if (description.length > DISCORD_DESCRIPTION_LIMIT) {
177
+ const ellipsis = "\n…(truncated)";
178
+ description = `${description.slice(0, DISCORD_DESCRIPTION_LIMIT - ellipsis.length)}${ellipsis}`;
179
+ }
180
+ return {
181
+ content: `✨ New maker to look at — ${mentionString(mentionUserId)}`,
182
+ allowed_mentions: { parse: [], users: [mentionUserId] },
183
+ embeds: [
184
+ {
185
+ title: `Gold drip: ${maker.ownerHandle}/${maker.appSlug}`,
186
+ description,
187
+ color: COLOR_GOLD,
188
+ ...(maker.imageUrl === undefined ? {} : { image: { url: maker.imageUrl } }),
189
+ },
190
+ ],
191
+ };
192
+ }
193
+ export function logLine(input) {
194
+ const app = input.ownerHandle !== undefined && input.appSlug !== undefined ? ` app=${input.ownerHandle}/${input.appSlug}` : "";
195
+ return `[gold-drip-sweep] handle=${input.handle}${app} verdict=${input.verdict}`;
196
+ }
197
+ //# sourceMappingURL=gold-drip-sweep-util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gold-drip-sweep-util.js","sourceRoot":"","sources":["../../jsr/usage-report/gold-drip-sweep-util.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { Result } from "@adviser/cement";
2
+ export declare function run(): Promise<Result<void>>;
@@ -0,0 +1,327 @@
1
+ import { Pool } from "@neondatabase/serverless";
2
+ import { Result, exception2Result } from "@adviser/cement";
3
+ import { flag, formatError, loadDevVars } from "./usage-report-util.js";
4
+ import { isStaffOrQa, resolveExclusions } from "./new-user-alert-util.js";
5
+ import { MAIL_IMAGE_TOKEN_TTL_SECONDS, signMailImageToken } from "@vibes.diy/api-types";
6
+ import { CLAIM_PROPERTY, CONTACT_PROPERTIES_READ, EXTRA_EXCLUDED_HANDLE_PATTERNS, FIRST_TURN_WINDOW_HOURS, MISSING_CLAIM_PROPERTY_MESSAGE, QUALIFYING_TURN_MIN_TOTAL_TOKENS, QUIET_MINUTES, UNKNOWN_USER_ID, buildGoldDripMessage, extractUserPromptFromBlocks, gateContact, gateProduct, logLine, mailImageUrl, readContactRow, readSettingsFacts, screenshotIsTrustworthy, sweepIsEnabled, } from "./gold-drip-sweep-util.js";
7
+ const HUBSPOT_BASE = "https://api.hubapi.com";
8
+ function asString(v) {
9
+ return typeof v === "string" && v !== "" ? v : undefined;
10
+ }
11
+ async function batchReadByProperty(token, idProperty, ids) {
12
+ const rRes = await exception2Result(() => fetch(`${HUBSPOT_BASE}/crm/v3/objects/contacts/batch/read`, {
13
+ method: "POST",
14
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
15
+ body: JSON.stringify({ idProperty, properties: CONTACT_PROPERTIES_READ, inputs: ids.map((id) => ({ id })) }),
16
+ }));
17
+ if (rRes.isErr())
18
+ return Result.Err(rRes);
19
+ const res = rRes.Ok();
20
+ if (!res.ok) {
21
+ return Result.Err(new Error(`hubspot batch/read (${idProperty}) returned ${res.status}`));
22
+ }
23
+ const parsed = await exception2Result(() => res.json());
24
+ if (parsed.isErr())
25
+ return Result.Err(parsed);
26
+ const byInput = new Map();
27
+ for (const row of parsed.Ok().results ?? []) {
28
+ const props = (row["properties"] ?? {});
29
+ const key = asString(props[idProperty]);
30
+ if (key !== undefined)
31
+ byInput.set(key, readContactRow(row));
32
+ }
33
+ return Result.Ok({ byInput });
34
+ }
35
+ async function lookupByEmail(token, email) {
36
+ const r = await batchReadByProperty(token, "email", [email]);
37
+ if (r.isErr())
38
+ return Result.Err(r);
39
+ const rows = [...r.Ok().byInput.values()];
40
+ if (rows.length === 0)
41
+ return Result.Ok({ state: "absent" });
42
+ return Result.Ok(rows[0]);
43
+ }
44
+ async function writeClaim(token, contactId, nowIso) {
45
+ const rRes = await exception2Result(() => fetch(`${HUBSPOT_BASE}/crm/v3/objects/contacts/${encodeURIComponent(contactId)}`, {
46
+ method: "PATCH",
47
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
48
+ body: JSON.stringify({ properties: { [CLAIM_PROPERTY]: nowIso } }),
49
+ }));
50
+ if (rRes.isErr())
51
+ return Result.Err(rRes);
52
+ if (!rRes.Ok().ok)
53
+ return Result.Err(new Error(`hubspot claim write returned ${rRes.Ok().status}`));
54
+ return Result.Ok();
55
+ }
56
+ async function postDiscord(webhookUrl, body) {
57
+ const rRes = await exception2Result(() => fetch(webhookUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }));
58
+ if (rRes.isErr())
59
+ return Result.Err(rRes);
60
+ if (rRes.Ok().status !== 204)
61
+ return Result.Err(new Error(`discord post returned ${rRes.Ok().status}`));
62
+ return Result.Ok();
63
+ }
64
+ async function claimPropertyExists(token) {
65
+ const rRes = await exception2Result(() => fetch(`${HUBSPOT_BASE}/crm/v3/properties/contacts/${CLAIM_PROPERTY}`, {
66
+ headers: { authorization: `Bearer ${token}` },
67
+ }));
68
+ if (rRes.isErr())
69
+ return Result.Err(rRes);
70
+ const status = rRes.Ok().status;
71
+ if (status === 200)
72
+ return Result.Ok(true);
73
+ if (status === 404)
74
+ return Result.Ok(false);
75
+ return Result.Err(new Error(`hubspot properties/contacts/${CLAIM_PROPERTY} returned ${status}`));
76
+ }
77
+ export async function run() {
78
+ loadDevVars();
79
+ const connectionString = process.env["NEON_DATABASE_URL"];
80
+ const hubspotToken = process.env["HUBSPOT_TOKEN"];
81
+ const webhookUrl = process.env["DISCORD_WEBHOOK_URL"];
82
+ const enabledRaw = process.env["GOLD_DRIP_SWEEP_ENABLED"];
83
+ const mentionId = process.env["GOLD_DRIP_MENTION_ID"];
84
+ const signingSecret = process.env["BACKEND_INTERNAL_SECRET"];
85
+ const publicBaseUrl = process.env["VIBES_DIY_PUBLIC_BASE_URL"];
86
+ const exclusions = resolveExclusions({
87
+ ALERT_EXCLUDE_DOMAINS: process.env["ALERT_EXCLUDE_DOMAINS"],
88
+ ALERT_EXCLUDE_USER_IDS: process.env["ALERT_EXCLUDE_USER_IDS"],
89
+ });
90
+ if (connectionString === undefined || connectionString === "") {
91
+ return Result.Err("NEON_DATABASE_URL is required (prod Neon) — nothing was measured");
92
+ }
93
+ const reasons = [];
94
+ if (sweepIsEnabled(enabledRaw) === false)
95
+ reasons.push('GOLD_DRIP_SWEEP_ENABLED is not "true"');
96
+ if (hubspotToken === undefined || hubspotToken === "")
97
+ reasons.push("no HUBSPOT_TOKEN");
98
+ if (webhookUrl === undefined || webhookUrl === "")
99
+ reasons.push("no DISCORD_WEBHOOK_URL");
100
+ if (mentionId === undefined || mentionId === "")
101
+ reasons.push("no GOLD_DRIP_MENTION_ID");
102
+ if (flag("--dry-run"))
103
+ reasons.push("--dry-run");
104
+ const dryRun = reasons.length > 0;
105
+ const nowMs = Date.now();
106
+ const nowIso = new Date(nowMs).toISOString();
107
+ const windowStartIso = new Date(nowMs - FIRST_TURN_WINDOW_HOURS * 3600_000).toISOString();
108
+ const quietBeforeIso = new Date(nowMs - QUIET_MINUTES * 60_000).toISOString();
109
+ const pool = new Pool({ connectionString });
110
+ const result = await exception2Result(async () => {
111
+ const candidateRes = await pool.query(`select "userId", min(created) as first_turn, max(created) as last_turn, count(*) as turns
112
+ from "PromptContexts"
113
+ where "userId" <> $3 and "totalTokens" >= $4
114
+ and "userId" in (
115
+ select "userId" from "PromptContexts"
116
+ where created >= $1 and "userId" <> $3 and "totalTokens" >= $4)
117
+ group by "userId"
118
+ having min(created) >= $1 and max(created) <= $2`, [windowStartIso, quietBeforeIso, UNKNOWN_USER_ID, QUALIFYING_TURN_MIN_TOTAL_TOKENS]);
119
+ const candidates = candidateRes.rows.map((r) => ({
120
+ userId: asString(r["userId"]) ?? "",
121
+ firstTurnIso: asString(r["first_turn"]) ?? "",
122
+ lastTurnIso: asString(r["last_turn"]) ?? "",
123
+ qualifyingTurns: Number(r["turns"] ?? 0),
124
+ }));
125
+ const userIds = candidates.map((c) => c.userId).filter((u) => u !== "");
126
+ const handles = new Map();
127
+ const settings = new Map();
128
+ const apps = new Map();
129
+ const prompts = new Map();
130
+ if (userIds.length > 0) {
131
+ const hRes = await pool.query(`select "userId", "userSlug" as handle from "UserSlugBindings" where "userId" = any($1) order by created`, [userIds]);
132
+ for (const r of hRes.rows) {
133
+ const id = asString(r["userId"]) ?? "";
134
+ handles.set(id, [...(handles.get(id) ?? []), asString(r["handle"]) ?? ""]);
135
+ }
136
+ const sRes = await pool.query(`select "userId", settings from "UserSettings" where "userId" = any($1)`, [userIds]);
137
+ for (const r of sRes.rows)
138
+ settings.set(asString(r["userId"]) ?? "", r["settings"]);
139
+ const aRes = await pool.query(`select distinct on (a."userId")
140
+ a."userId", a."appSlug", a."userSlug" as owner_handle, a."fsId", a.created,
141
+ (a.mode = 'production' and coalesce(b."unpublishedAt", '') = '') as published,
142
+ exists (select 1 from jsonb_array_elements(a.meta) m
143
+ where m->>'type' = 'screen-shot-ref') as has_shot
144
+ from "Apps" a
145
+ left join "AppSlugBindings" b on b."userSlug" = a."userSlug" and b."appSlug" = a."appSlug"
146
+ where a."userId" = any($1) and a.mode = 'production'
147
+ order by a."userId", a.created desc`, [userIds]);
148
+ for (const r of aRes.rows) {
149
+ apps.set(asString(r["userId"]) ?? "", {
150
+ appSlug: asString(r["appSlug"]) ?? "",
151
+ ownerHandle: asString(r["owner_handle"]) ?? "",
152
+ fsId: asString(r["fsId"]) ?? "",
153
+ createdIso: asString(r["created"]) ?? "",
154
+ published: r["published"] === true,
155
+ hasStoredShot: r["has_shot"] === true,
156
+ });
157
+ }
158
+ const appUserIds = [...apps.keys()];
159
+ const appSlugs = appUserIds.map((u) => apps.get(u)?.appSlug ?? "");
160
+ const appHandles = appUserIds.map((u) => apps.get(u)?.ownerHandle ?? "");
161
+ if (appUserIds.length > 0) {
162
+ const mRes = await pool.query(`select distinct on (m."userId") m."userId", m.prompt
163
+ from "MintJobs" m
164
+ join unnest($1::text[], $2::text[], $3::text[]) as t(uid, slug, handle)
165
+ on m."userId" = t.uid and m."appSlug" = t.slug and m."userSlug" = t.handle
166
+ order by m."userId", m.created asc`, [appUserIds, appSlugs, appHandles]);
167
+ for (const r of mRes.rows) {
168
+ const p = asString(r["prompt"]);
169
+ if (p !== undefined)
170
+ prompts.set(asString(r["userId"]) ?? "", p);
171
+ }
172
+ const missingIdx = appUserIds.map((u, i) => (prompts.has(u) ? -1 : i)).filter((i) => i >= 0);
173
+ if (missingIdx.length > 0) {
174
+ const cRes = await pool.query(`select distinct on (c."userId") c."userId", s.blocks
175
+ from "ChatContexts" c
176
+ join "ChatSections" s on s."chatId" = c."chatId"
177
+ join unnest($1::text[], $2::text[], $3::text[]) as t(uid, slug, handle)
178
+ on c."userId" = t.uid and c."appSlug" = t.slug and c."userSlug" = t.handle
179
+ order by c."userId", s.created asc`, [missingIdx.map((i) => appUserIds[i]), missingIdx.map((i) => appSlugs[i]), missingIdx.map((i) => appHandles[i])]);
180
+ for (const r of cRes.rows) {
181
+ const text = extractUserPromptFromBlocks(r["blocks"]);
182
+ if (text !== undefined)
183
+ prompts.set(asString(r["userId"]) ?? "", text);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ const passed = [];
189
+ const verdicts = new Map();
190
+ const count = (v) => {
191
+ verdicts.set(v, (verdicts.get(v) ?? 0) + 1);
192
+ };
193
+ for (const candidate of candidates) {
194
+ const userHandles = handles.get(candidate.userId) ?? [];
195
+ const facts = readSettingsFacts(settings.get(candidate.userId));
196
+ const staffOrQa = isStaffOrQa({ userId: candidate.userId, email: facts.contactEmail, handles: userHandles }, exclusions) ||
197
+ userHandles.some((h) => EXTRA_EXCLUDED_HANDLE_PATTERNS.some((re) => re.test(h.trim())));
198
+ const verdict = gateProduct({
199
+ userId: candidate.userId,
200
+ handles: userHandles,
201
+ firstTurnIso: candidate.firstTurnIso,
202
+ lastTurnIso: candidate.lastTurnIso,
203
+ qualifyingTurns: candidate.qualifyingTurns,
204
+ settings: facts,
205
+ windowStartIso,
206
+ quietBeforeIso,
207
+ staffOrQa,
208
+ });
209
+ const handle = userHandles[0] ?? "(no handle)";
210
+ if (verdict.ok === false) {
211
+ count(verdict.reason);
212
+ console.log(logLine({ handle, verdict: verdict.reason }));
213
+ continue;
214
+ }
215
+ passed.push({ candidate, handle, email: verdict.email });
216
+ }
217
+ const lookups = new Map();
218
+ if (passed.length > 0 && hubspotToken !== undefined && hubspotToken !== "") {
219
+ const rExists = await claimPropertyExists(hubspotToken);
220
+ if (rExists.isErr())
221
+ throw rExists.Err();
222
+ if (rExists.Ok() === false)
223
+ throw new Error(MISSING_CLAIM_PROPERTY_MESSAGE);
224
+ const byClerk = await batchReadByProperty(hubspotToken, "clerk_user_id", passed.map((p) => p.candidate.userId));
225
+ if (byClerk.isErr())
226
+ throw byClerk.Err();
227
+ for (const p of passed) {
228
+ const hit = byClerk.Ok().byInput.get(p.candidate.userId);
229
+ if (hit !== undefined) {
230
+ lookups.set(p.candidate.userId, hit);
231
+ continue;
232
+ }
233
+ const byEmail = await lookupByEmail(hubspotToken, p.email);
234
+ if (byEmail.isErr())
235
+ throw byEmail.Err();
236
+ lookups.set(p.candidate.userId, byEmail.Ok());
237
+ }
238
+ }
239
+ let claimed = 0;
240
+ for (const p of passed) {
241
+ const app = apps.get(p.candidate.userId);
242
+ const lookup = lookups.get(p.candidate.userId) ?? { state: "failed", reason: "no HubSpot token — dry run" };
243
+ const contact = gateContact(lookup);
244
+ if (contact.ok === false) {
245
+ count(contact.reason);
246
+ console.log(logLine({ handle: p.handle, ownerHandle: app?.ownerHandle, appSlug: app?.appSlug, verdict: contact.reason }));
247
+ continue;
248
+ }
249
+ let imageUrl;
250
+ let imageOmittedBecause;
251
+ if (app === undefined)
252
+ imageOmittedBecause = "no production release found";
253
+ else if (app.hasStoredShot === false)
254
+ imageOmittedBecause = "the release carries no stored shot";
255
+ else if (screenshotIsTrustworthy(app.createdIso) === false)
256
+ imageOmittedBecause = "the release predates the #5304 fix, so its stored shot may be an error card";
257
+ else if (signingSecret === undefined || signingSecret === "" || publicBaseUrl === undefined || publicBaseUrl === "")
258
+ imageOmittedBecause = "image signing is not configured in this environment";
259
+ else {
260
+ const token = await signMailImageToken({
261
+ ownerUserId: p.candidate.userId,
262
+ appSlug: app.appSlug,
263
+ fsId: app.fsId,
264
+ expUnix: Math.floor(nowMs / 1000) + MAIL_IMAGE_TOKEN_TTL_SECONDS,
265
+ }, signingSecret);
266
+ imageUrl = mailImageUrl({
267
+ publicBaseUrl,
268
+ appSlug: app.appSlug,
269
+ fsId: app.fsId,
270
+ ownerUserId: p.candidate.userId,
271
+ token,
272
+ });
273
+ }
274
+ const maker = {
275
+ handle: p.handle,
276
+ appSlug: app?.appSlug ?? "(no app)",
277
+ ownerHandle: app?.ownerHandle ?? p.handle,
278
+ published: app?.published === true,
279
+ qualifyingTurns: p.candidate.qualifyingTurns,
280
+ firstTurnIso: p.candidate.firstTurnIso,
281
+ contactId: contact.contactId,
282
+ firstPrompt: prompts.get(p.candidate.userId),
283
+ imageUrl,
284
+ imageOmittedBecause,
285
+ };
286
+ const body = buildGoldDripMessage(maker, mentionId ?? "0");
287
+ if (dryRun) {
288
+ count("would-claim");
289
+ console.log(logLine({ handle: p.handle, ownerHandle: maker.ownerHandle, appSlug: maker.appSlug, verdict: "would-claim" }));
290
+ continue;
291
+ }
292
+ const posted = await postDiscord(webhookUrl, body);
293
+ if (posted.isErr()) {
294
+ count("post-failed");
295
+ console.log(logLine({ handle: p.handle, ownerHandle: maker.ownerHandle, appSlug: maker.appSlug, verdict: "post-failed" }));
296
+ continue;
297
+ }
298
+ const wrote = await writeClaim(hubspotToken, contact.contactId, nowIso);
299
+ if (wrote.isErr()) {
300
+ count("claim-write-failed");
301
+ console.log(logLine({ handle: p.handle, ownerHandle: maker.ownerHandle, appSlug: maker.appSlug, verdict: "claim-write-failed" }));
302
+ continue;
303
+ }
304
+ claimed += 1;
305
+ count("claimed");
306
+ console.log(logLine({ handle: p.handle, ownerHandle: maker.ownerHandle, appSlug: maker.appSlug, verdict: "claimed" }));
307
+ }
308
+ const breakdown = [...verdicts.entries()]
309
+ .sort((a, b) => a[0].localeCompare(b[0]))
310
+ .map(([k, v]) => `${k}=${v}`)
311
+ .join(" ");
312
+ console.log(`[gold-drip-sweep] window=${FIRST_TURN_WINDOW_HOURS}h quiet=${QUIET_MINUTES}m candidates=${candidates.length} passedProductGates=${passed.length} claimed=${claimed} live=${!dryRun} ${breakdown}`);
313
+ if (dryRun)
314
+ console.log(`[gold-drip-sweep] dry run: ${reasons.join("; ")}`);
315
+ });
316
+ await pool.end().catch(() => undefined);
317
+ if (result.isErr())
318
+ return Result.Err(result);
319
+ return Result.Ok();
320
+ }
321
+ run().then((result) => {
322
+ if (result.isErr()) {
323
+ console.error(formatError(result.Err()));
324
+ process.exitCode = 1;
325
+ }
326
+ });
327
+ //# sourceMappingURL=gold-drip-sweep.js.map