@percher/shared 1.1.0 → 1.3.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.
Files changed (80) hide show
  1. package/dist/data-filter.d.ts +27 -0
  2. package/dist/data-filter.d.ts.map +1 -0
  3. package/dist/data-filter.js +288 -0
  4. package/dist/data-filter.js.map +1 -0
  5. package/dist/data-records.d.ts +359 -0
  6. package/dist/data-records.d.ts.map +1 -0
  7. package/dist/data-records.js +124 -0
  8. package/dist/data-records.js.map +1 -0
  9. package/dist/deployment-security.d.ts +32 -0
  10. package/dist/deployment-security.d.ts.map +1 -0
  11. package/dist/deployment-security.js +146 -0
  12. package/dist/deployment-security.js.map +1 -0
  13. package/dist/disposable-email-domains.d.ts +11 -0
  14. package/dist/disposable-email-domains.d.ts.map +1 -0
  15. package/dist/disposable-email-domains.js +112 -0
  16. package/dist/disposable-email-domains.js.map +1 -0
  17. package/dist/email-budget.d.ts +60 -0
  18. package/dist/email-budget.d.ts.map +1 -0
  19. package/dist/email-budget.js +135 -0
  20. package/dist/email-budget.js.map +1 -0
  21. package/dist/email-change.d.ts +11 -0
  22. package/dist/email-change.d.ts.map +1 -0
  23. package/dist/email-change.js +23 -0
  24. package/dist/email-change.js.map +1 -0
  25. package/dist/email-template.d.ts +19 -0
  26. package/dist/email-template.d.ts.map +1 -0
  27. package/dist/email-template.js +53 -0
  28. package/dist/email-template.js.map +1 -0
  29. package/dist/image-rejection.d.ts +45 -0
  30. package/dist/image-rejection.d.ts.map +1 -0
  31. package/dist/image-rejection.js +27 -0
  32. package/dist/image-rejection.js.map +1 -0
  33. package/dist/index.d.ts +14 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +14 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/internal-sig.d.ts +2 -2
  38. package/dist/internal-sig.js +2 -2
  39. package/dist/ip-prefix.d.ts +43 -0
  40. package/dist/ip-prefix.d.ts.map +1 -0
  41. package/dist/ip-prefix.js +225 -0
  42. package/dist/ip-prefix.js.map +1 -0
  43. package/dist/legal-changes.d.ts +4 -2
  44. package/dist/legal-changes.d.ts.map +1 -1
  45. package/dist/legal-changes.js +124 -0
  46. package/dist/legal-changes.js.map +1 -1
  47. package/dist/legal.d.ts +14 -14
  48. package/dist/legal.d.ts.map +1 -1
  49. package/dist/legal.js +13 -13
  50. package/dist/legal.js.map +1 -1
  51. package/dist/limits.d.ts +11 -0
  52. package/dist/limits.d.ts.map +1 -1
  53. package/dist/limits.js +11 -0
  54. package/dist/limits.js.map +1 -1
  55. package/dist/oauth-scopes.d.ts +8 -0
  56. package/dist/oauth-scopes.d.ts.map +1 -1
  57. package/dist/oauth-scopes.js +11 -13
  58. package/dist/oauth-scopes.js.map +1 -1
  59. package/dist/platform.d.ts +3 -3
  60. package/dist/platform.js +3 -3
  61. package/dist/signup-capacity.d.ts +12 -0
  62. package/dist/signup-capacity.d.ts.map +1 -0
  63. package/dist/signup-capacity.js +29 -0
  64. package/dist/signup-capacity.js.map +1 -0
  65. package/dist/signup-day.d.ts +7 -0
  66. package/dist/signup-day.d.ts.map +1 -0
  67. package/dist/signup-day.js +42 -0
  68. package/dist/signup-day.js.map +1 -0
  69. package/dist/switches.d.ts.map +1 -1
  70. package/dist/switches.js +2 -1
  71. package/dist/switches.js.map +1 -1
  72. package/dist/verified-signup.d.ts +24 -0
  73. package/dist/verified-signup.d.ts.map +1 -0
  74. package/dist/verified-signup.js +58 -0
  75. package/dist/verified-signup.js.map +1 -0
  76. package/dist/waitlist-token.d.ts +49 -0
  77. package/dist/waitlist-token.d.ts.map +1 -0
  78. package/dist/waitlist-token.js +71 -0
  79. package/dist/waitlist-token.js.map +1 -0
  80. package/package.json +6 -1
@@ -0,0 +1,24 @@
1
+ export interface SignupQuotaConnection {
2
+ run(query: string, params?: (string | number | null)[]): void;
3
+ get<T>(query: string, params?: (string | number | null)[]): T | undefined;
4
+ }
5
+ export declare class VerifiedSignupLimitError extends Error {
6
+ readonly code = "DAILY_CAP";
7
+ constructor();
8
+ }
9
+ /** Distinct namespace: historical pending-account totals remain unchanged. */
10
+ export declare function verifiedSignupQuota(db: SignupQuotaConnection, now?: Date): {
11
+ day: string;
12
+ count: number;
13
+ limit: number | null;
14
+ remaining: number | null;
15
+ resetAt: number;
16
+ };
17
+ /** Caller holds an immediate transaction covering this charge and verification. */
18
+ export declare function chargeVerifiedSignup(db: SignupQuotaConnection, now?: Date): void;
19
+ /** One-time upgrade repair. Caller holds the same writer transaction as admission.
20
+ * Snapshot pre-existing identity-bound proof before any new verification write.
21
+ * Absent dashboard schema is not a completed cutover. Invalid schema must fail.
22
+ */
23
+ export declare function initializeVerifiedSignupCutover(db: SignupQuotaConnection, now: Date): void;
24
+ //# sourceMappingURL=verified-signup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verified-signup.d.ts","sourceRoot":"","sources":["../src/verified-signup.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;IAC9D,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;CAC3E;AACD,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,eAAe;IAC5B,cAIC;CACF;AACD,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,qBAAqB,EAAE,GAAG,OAAa;;;;;;EAyB9E;AACD,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,qBAAqB,EAAE,GAAG,OAAa,QAY/E;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,EAAE,EAAE,qBAAqB,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAwB1F"}
@@ -0,0 +1,58 @@
1
+ import { signupDayWindow } from "./signup-day.js";
2
+ export class VerifiedSignupLimitError extends Error {
3
+ code = "DAILY_CAP";
4
+ constructor() {
5
+ super("Today's registration quota is full. Try this link again tomorrow; request a new verification email if it has expired.");
6
+ }
7
+ }
8
+ /** Distinct namespace: historical pending-account totals remain unchanged. */
9
+ export function verifiedSignupQuota(db, now = new Date()) {
10
+ const { day, startMs, endMs } = signupDayWindow(now);
11
+ const key = `verified:${day}`;
12
+ const stored = db.get("SELECT count FROM signup_daily_registrations WHERE day = ?", [key]);
13
+ const count = stored?.count ??
14
+ db.get("SELECT COUNT(*) AS count FROM users WHERE email_verified_at >= ? AND email_verified_at < ?", [Math.floor(startMs / 1000), Math.floor(endMs / 1000)])?.count ??
15
+ 0;
16
+ const raw = db.get("SELECT value FROM platform_flags WHERE key = 'signup_daily_cap'")?.value;
17
+ const limit = raw && /^\d{1,9}$/.test(raw) && Number(raw) > 0 ? Number(raw) : null;
18
+ return {
19
+ day,
20
+ count,
21
+ limit,
22
+ remaining: limit === null ? null : Math.max(0, limit - count),
23
+ resetAt: endMs,
24
+ };
25
+ }
26
+ /** Caller holds an immediate transaction covering this charge and verification. */
27
+ export function chargeVerifiedSignup(db, now = new Date()) {
28
+ const quota = verifiedSignupQuota(db, now);
29
+ const key = `verified:${quota.day}`;
30
+ db.run("INSERT INTO signup_daily_registrations(day,count) VALUES(?,?) ON CONFLICT(day) DO NOTHING", [key, quota.count]);
31
+ const charged = db.get("UPDATE signup_daily_registrations SET count=count+1 WHERE day=? AND (? IS NULL OR count < ?) RETURNING day", [key, quota.limit, quota.limit]);
32
+ if (!charged)
33
+ throw new VerifiedSignupLimitError();
34
+ }
35
+ /** One-time upgrade repair. Caller holds the same writer transaction as admission.
36
+ * Snapshot pre-existing identity-bound proof before any new verification write.
37
+ * Absent dashboard schema is not a completed cutover. Invalid schema must fail.
38
+ */
39
+ export function initializeVerifiedSignupCutover(db, now) {
40
+ const marker = "verified_signup_cutover_v1";
41
+ if (db.get("SELECT value FROM platform_flags WHERE key=?", [marker]))
42
+ return;
43
+ if (!db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='auth_user'"))
44
+ return;
45
+ db.get("SELECT id,email,percher_user_id,email_verified FROM auth_user LIMIT 0");
46
+ const quota = verifiedSignupQuota(db, now);
47
+ const key = `verified:${quota.day}`;
48
+ db.run("INSERT INTO signup_daily_registrations(day,count) VALUES(?,?) ON CONFLICT(day) DO NOTHING", [key, quota.count]);
49
+ const predicate = "email_verified_at IS NULL AND status!='deleting' AND EXISTS (SELECT 1 FROM auth_user a WHERE a.percher_user_id=users.id AND lower(a.email)=lower(users.email) AND a.email_verified=1)";
50
+ const repaired = db.get(`SELECT COUNT(*) AS count FROM users WHERE ${predicate}`)?.count ?? 0;
51
+ db.run(`UPDATE users SET email_verified_at=?,status=CASE WHEN status='pending' AND accepted_terms_at IS NOT NULL THEN 'active' ELSE status END WHERE ${predicate}`, [Math.floor(now.getTime() / 1000)]);
52
+ db.run("INSERT INTO platform_flags(key,value,updated_at) VALUES(?,?,?)", [
53
+ marker,
54
+ JSON.stringify({ version: 1, at: now.getTime(), repaired }),
55
+ Math.floor(now.getTime() / 1000),
56
+ ]);
57
+ }
58
+ //# sourceMappingURL=verified-signup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verified-signup.js","sourceRoot":"","sources":["../src/verified-signup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The personal "remove me" links for the two lists that hold an address
3
+ * until someone asks us to delete it: the pre-launch waitlist
4
+ * (`waitlist_signups`) and a closed tier's "tell me when it opens" list
5
+ * (`plan_interests`). A token names one row and proves the link came from
6
+ * Percher: `<rowId>.<mac>` for the waitlist, `plan.<rowId>.<mac>` for a plan
7
+ * interest. The mac is an HMAC of the row id under a key derived from the
8
+ * platform's internal secret with a fixed label per list, so the key is used
9
+ * for nothing else, no new secret has to be provisioned, and a token for one
10
+ * list never verifies for the other. The API builds the links (the waitlist
11
+ * export, the plan-interest emails) and the dashboard verifies them, so they
12
+ * live here once.
13
+ *
14
+ * A subpath export, never the barrel: `node:crypto` must not reach the
15
+ * dashboard's client bundle (see secure-compare.ts).
16
+ */
17
+ declare const PURPOSES: {
18
+ readonly signup: {
19
+ readonly label: "percher-waitlist-removal-v1";
20
+ readonly prefix: "";
21
+ };
22
+ readonly plan_interest: {
23
+ readonly label: "percher-plan-interest-removal-v1";
24
+ readonly prefix: "plan.";
25
+ };
26
+ };
27
+ export type RemovalPurpose = keyof typeof PURPOSES;
28
+ export declare function waitlistRemovalToken(secret: string, rowId: string): string;
29
+ export declare function planInterestRemovalToken(secret: string, rowId: string): string;
30
+ /**
31
+ * Which list and row a genuine token names, otherwise null. The prefix picks
32
+ * the label, and the label is part of the key, so moving a token to the other
33
+ * list by adding or dropping the prefix fails the mac.
34
+ */
35
+ export declare function verifyRemovalToken(secret: string, token: string | null | undefined): {
36
+ purpose: RemovalPurpose;
37
+ rowId: string;
38
+ } | null;
39
+ /** The waitlist row id when the token is a genuine waitlist link, otherwise null. */
40
+ export declare function verifyWaitlistRemovalToken(secret: string, token: string | null | undefined): string | null;
41
+ /**
42
+ * The public page a token opens; `origin` is the dashboard origin. Both lists
43
+ * use the same page. The query name is `token` because Caddy's access-log
44
+ * filter blanks that name in the URI and the Referer (infra/Caddyfile.prod);
45
+ * a new name would be logged.
46
+ */
47
+ export declare function waitlistRemovalUrl(origin: string, token: string): string;
48
+ export {};
49
+ //# sourceMappingURL=waitlist-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waitlist-token.d.ts","sourceRoot":"","sources":["../src/waitlist-token.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AACH,QAAA,MAAM,QAAQ;aACZ,MAAM;iBAAI,KAAK,EAAE,6BAA6B;iBAAE,MAAM,EAAE,EAAE;;aAC1D,aAAa;iBAAI,KAAK,EAAE,kCAAkC;iBAAE,MAAM,EAAE,OAAO;;CACnE,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,QAAQ,CAAC;AAcnD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B;IAAE,OAAO,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUnD;AAED,qFAAqF;AACrF,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAGf;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAExE"}
@@ -0,0 +1,71 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { secretEquals } from "./secure-compare.js";
3
+ /**
4
+ * The personal "remove me" links for the two lists that hold an address
5
+ * until someone asks us to delete it: the pre-launch waitlist
6
+ * (`waitlist_signups`) and a closed tier's "tell me when it opens" list
7
+ * (`plan_interests`). A token names one row and proves the link came from
8
+ * Percher: `<rowId>.<mac>` for the waitlist, `plan.<rowId>.<mac>` for a plan
9
+ * interest. The mac is an HMAC of the row id under a key derived from the
10
+ * platform's internal secret with a fixed label per list, so the key is used
11
+ * for nothing else, no new secret has to be provisioned, and a token for one
12
+ * list never verifies for the other. The API builds the links (the waitlist
13
+ * export, the plan-interest emails) and the dashboard verifies them, so they
14
+ * live here once.
15
+ *
16
+ * A subpath export, never the barrel: `node:crypto` must not reach the
17
+ * dashboard's client bundle (see secure-compare.ts).
18
+ */
19
+ const PURPOSES = {
20
+ signup: { label: "percher-waitlist-removal-v1", prefix: "" },
21
+ plan_interest: { label: "percher-plan-interest-removal-v1", prefix: "plan." },
22
+ };
23
+ const MAC_CHARS = 32;
24
+ function mac(secret, purpose, rowId) {
25
+ const key = createHmac("sha256", secret).update(PURPOSES[purpose].label).digest();
26
+ return createHmac("sha256", key).update(rowId).digest("base64url").slice(0, MAC_CHARS);
27
+ }
28
+ function removalToken(secret, purpose, rowId) {
29
+ if (!secret)
30
+ throw new Error("A removal token needs the internal secret");
31
+ return `${PURPOSES[purpose].prefix}${rowId}.${mac(secret, purpose, rowId)}`;
32
+ }
33
+ export function waitlistRemovalToken(secret, rowId) {
34
+ return removalToken(secret, "signup", rowId);
35
+ }
36
+ export function planInterestRemovalToken(secret, rowId) {
37
+ return removalToken(secret, "plan_interest", rowId);
38
+ }
39
+ /**
40
+ * Which list and row a genuine token names, otherwise null. The prefix picks
41
+ * the label, and the label is part of the key, so moving a token to the other
42
+ * list by adding or dropping the prefix fails the mac.
43
+ */
44
+ export function verifyRemovalToken(secret, token) {
45
+ if (!secret || !token || token.length > 200)
46
+ return null;
47
+ const purpose = token.startsWith(PURPOSES.plan_interest.prefix)
48
+ ? "plan_interest"
49
+ : "signup";
50
+ const body = token.slice(PURPOSES[purpose].prefix.length);
51
+ const dot = body.lastIndexOf(".");
52
+ if (dot <= 0)
53
+ return null;
54
+ const rowId = body.slice(0, dot);
55
+ return secretEquals(body.slice(dot + 1), mac(secret, purpose, rowId)) ? { purpose, rowId } : null;
56
+ }
57
+ /** The waitlist row id when the token is a genuine waitlist link, otherwise null. */
58
+ export function verifyWaitlistRemovalToken(secret, token) {
59
+ const verified = verifyRemovalToken(secret, token);
60
+ return verified?.purpose === "signup" ? verified.rowId : null;
61
+ }
62
+ /**
63
+ * The public page a token opens; `origin` is the dashboard origin. Both lists
64
+ * use the same page. The query name is `token` because Caddy's access-log
65
+ * filter blanks that name in the URI and the Referer (infra/Caddyfile.prod);
66
+ * a new name would be logged.
67
+ */
68
+ export function waitlistRemovalUrl(origin, token) {
69
+ return `${origin.replace(/\/+$/, "")}/waitlist/leave?token=${encodeURIComponent(token)}`;
70
+ }
71
+ //# sourceMappingURL=waitlist-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waitlist-token.js","sourceRoot":"","sources":["../src/waitlist-token.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percher/shared",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Shared constants and contracts (timeouts, etc.) used across the Percher CLI, API, MCP, and worker. Internal — values change in lockstep across packages.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,6 +41,11 @@
41
41
  "import": "./dist/secure-compare.js",
42
42
  "default": "./dist/secure-compare.js"
43
43
  },
44
+ "./waitlist-token": {
45
+ "types": "./dist/waitlist-token.d.ts",
46
+ "import": "./dist/waitlist-token.js",
47
+ "default": "./dist/waitlist-token.js"
48
+ },
44
49
  "./signup-invite-hash": {
45
50
  "types": "./dist/signup-invite-hash.d.ts",
46
51
  "import": "./dist/signup-invite-hash.js",