@percher/shared 1.0.2 → 1.2.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 (85) hide show
  1. package/dist/billing-test-accounts.d.ts +20 -0
  2. package/dist/billing-test-accounts.d.ts.map +1 -0
  3. package/dist/billing-test-accounts.js +32 -0
  4. package/dist/billing-test-accounts.js.map +1 -0
  5. package/dist/crash-prompt.d.ts +8 -0
  6. package/dist/crash-prompt.d.ts.map +1 -0
  7. package/dist/crash-prompt.js +10 -0
  8. package/dist/crash-prompt.js.map +1 -0
  9. package/dist/data-filter.d.ts +27 -0
  10. package/dist/data-filter.d.ts.map +1 -0
  11. package/dist/data-filter.js +288 -0
  12. package/dist/data-filter.js.map +1 -0
  13. package/dist/data-records.d.ts +359 -0
  14. package/dist/data-records.d.ts.map +1 -0
  15. package/dist/data-records.js +124 -0
  16. package/dist/data-records.js.map +1 -0
  17. package/dist/disposable-email-domains.d.ts +11 -0
  18. package/dist/disposable-email-domains.d.ts.map +1 -0
  19. package/dist/disposable-email-domains.js +112 -0
  20. package/dist/disposable-email-domains.js.map +1 -0
  21. package/dist/domain-name.d.ts +14 -0
  22. package/dist/domain-name.d.ts.map +1 -0
  23. package/dist/domain-name.js +14 -0
  24. package/dist/domain-name.js.map +1 -0
  25. package/dist/email-budget.d.ts +60 -0
  26. package/dist/email-budget.d.ts.map +1 -0
  27. package/dist/email-budget.js +135 -0
  28. package/dist/email-budget.js.map +1 -0
  29. package/dist/env-mask.d.ts +17 -0
  30. package/dist/env-mask.d.ts.map +1 -0
  31. package/dist/env-mask.js +23 -0
  32. package/dist/env-mask.js.map +1 -0
  33. package/dist/env-name.d.ts +25 -0
  34. package/dist/env-name.d.ts.map +1 -0
  35. package/dist/env-name.js +25 -0
  36. package/dist/env-name.js.map +1 -0
  37. package/dist/escape-html.d.ts +10 -0
  38. package/dist/escape-html.d.ts.map +1 -0
  39. package/dist/escape-html.js +17 -0
  40. package/dist/escape-html.js.map +1 -0
  41. package/dist/image-rejection.d.ts +45 -0
  42. package/dist/image-rejection.d.ts.map +1 -0
  43. package/dist/image-rejection.js +27 -0
  44. package/dist/image-rejection.js.map +1 -0
  45. package/dist/index.d.ts +15 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +15 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal-sig.d.ts +2 -2
  50. package/dist/internal-sig.js +2 -2
  51. package/dist/legal-changes.d.ts +5 -7
  52. package/dist/legal-changes.d.ts.map +1 -1
  53. package/dist/legal-changes.js +189 -6
  54. package/dist/legal-changes.js.map +1 -1
  55. package/dist/legal.d.ts +16 -16
  56. package/dist/legal.d.ts.map +1 -1
  57. package/dist/legal.js +15 -15
  58. package/dist/legal.js.map +1 -1
  59. package/dist/oauth-scopes.d.ts +8 -0
  60. package/dist/oauth-scopes.d.ts.map +1 -1
  61. package/dist/oauth-scopes.js +11 -13
  62. package/dist/oauth-scopes.js.map +1 -1
  63. package/dist/platform.d.ts +2 -2
  64. package/dist/platform.js +2 -2
  65. package/dist/secure-compare.d.ts +16 -0
  66. package/dist/secure-compare.d.ts.map +1 -0
  67. package/dist/secure-compare.js +27 -0
  68. package/dist/secure-compare.js.map +1 -0
  69. package/dist/signup-capacity.d.ts +12 -0
  70. package/dist/signup-capacity.d.ts.map +1 -0
  71. package/dist/signup-capacity.js +29 -0
  72. package/dist/signup-capacity.js.map +1 -0
  73. package/dist/signup-day.d.ts +7 -0
  74. package/dist/signup-day.d.ts.map +1 -0
  75. package/dist/signup-day.js +42 -0
  76. package/dist/signup-day.js.map +1 -0
  77. package/dist/switches.d.ts +88 -0
  78. package/dist/switches.d.ts.map +1 -0
  79. package/dist/switches.js +232 -0
  80. package/dist/switches.js.map +1 -0
  81. package/dist/waitlist-token.d.ts +49 -0
  82. package/dist/waitlist-token.d.ts.map +1 -0
  83. package/dist/waitlist-token.js +71 -0
  84. package/dist/waitlist-token.js.map +1 -0
  85. package/package.json +11 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Every environment switch the platform reads, declared once.
3
+ *
4
+ * A switch is a variable that selects a code path (two paths, an enum of
5
+ * modes, or "armed when present"), as opposed to configuration such as a URL
6
+ * or a size. The readers spell their comparisons differently (`=== "1"` in
7
+ * most places, `=== "true"` in five, `!== "0"` for a default-on switch), and
8
+ * an operator who set `=1` on a `"true"` switch got the OFF path with no
9
+ * warning. This registry records each switch's EXISTING accepted spellings.
10
+ * `switchState` names the four states a raw value can be in, and
11
+ * `assertValidSwitches` refuses a boot on an invalid one. Readers resolve a
12
+ * switch through `switchEnabled` / `switchValue`, which return exactly what
13
+ * the literal comparisons they replaced returned;
14
+ * `test/switch-readers-parity.test.ts` holds those comparisons and the
15
+ * switches still read the old way.
16
+ *
17
+ * Widening a reader to accept both spellings is deliberately not done here:
18
+ * a value that is off today because of its spelling would turn on silently
19
+ * at the next deploy. Fix the value, deliberately, one switch at a time.
20
+ */
21
+ export type SwitchReader = "api" | "worker" | "dashboard" | "mcp" | "cli";
22
+ export type SwitchState = "enabled" | "disabled" | "unset" | "invalid";
23
+ interface SwitchBase {
24
+ name: string;
25
+ readers: readonly SwitchReader[];
26
+ /** One line: what the ON path does. */
27
+ purpose: string;
28
+ }
29
+ export interface BooleanSwitch extends SwitchBase {
30
+ kind: "boolean";
31
+ /** Raw spellings the readers treat as ON today. */
32
+ enabled: readonly string[];
33
+ /** Raw spellings that mean an explicit OFF (never reported as invalid). */
34
+ disabled: readonly string[];
35
+ /** What an unset variable resolves to. */
36
+ default: "enabled" | "disabled";
37
+ }
38
+ export interface EnumSwitch extends SwitchBase {
39
+ kind: "enum";
40
+ values: readonly string[];
41
+ /** The value an unset variable resolves to. */
42
+ default: string;
43
+ /** The reader trims and lowercases before comparing, so `" ENFORCE "` works. */
44
+ normalize?: true;
45
+ }
46
+ export interface PresenceSwitch extends SwitchBase {
47
+ kind: "presence";
48
+ }
49
+ export type SwitchSpec = BooleanSwitch | EnumSwitch | PresenceSwitch;
50
+ /** The registry. Order: builders, isolation, admission, product, ops, levers. */
51
+ export declare const SWITCHES: readonly SwitchSpec[];
52
+ export declare const SWITCH_BY_NAME: ReadonlyMap<string, SwitchSpec>;
53
+ /** The state a raw value puts a switch in, by that switch's own spellings. */
54
+ export declare function switchState(spec: SwitchSpec, raw: string | undefined): SwitchState;
55
+ type Env = Record<string, string | undefined>;
56
+ /**
57
+ * The path a boolean switch selects: ON for an enabled spelling, OFF for a
58
+ * disabled one, and the switch's default when unset or invalid. That is what
59
+ * every `=== "1"` / `!== "0"` reader returned, so moving a reader here changes
60
+ * no verdict.
61
+ */
62
+ export declare function switchEnabled(name: string, env: Env): boolean;
63
+ /** The mode an enum switch selects: its (normalized) value, or the default when unset or invalid. */
64
+ export declare function switchValue(name: string, env: Env): string;
65
+ export interface SwitchReport {
66
+ name: string;
67
+ raw: string;
68
+ state: SwitchState;
69
+ /** What the value should have been spelled as. */
70
+ accepted: readonly string[];
71
+ /** What an unset variable resolves to. */
72
+ fallback: string;
73
+ }
74
+ /**
75
+ * The switches whose raw value is set but not a spelling any reader accepts.
76
+ * The input to the boot refusal; it never throws.
77
+ */
78
+ export declare function reportSwitches(env: Record<string, string | undefined>, reader?: SwitchReader): SwitchReport[];
79
+ /**
80
+ * Refuse to boot on a set-but-invalid switch: the reader would silently take
81
+ * its default path, which is how a `"true"` switch set to `1` stayed off.
82
+ * A disabled spelling and an unset variable never refuse. The error names
83
+ * every invalid switch with its raw value and accepted spellings. Call it
84
+ * before any other boot guard and before any listener starts.
85
+ */
86
+ export declare function assertValidSwitches(env: Record<string, string | undefined>, reader: SwitchReader): void;
87
+ export {};
88
+ //# sourceMappingURL=switches.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switches.d.ts","sourceRoot":"","sources":["../src/switches.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,WAAW,GAAG,KAAK,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvE,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,mDAAmD;IACnD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,0CAA0C;IAC1C,OAAO,EAAE,SAAS,GAAG,UAAU,CAAC;CACjC;AAED,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,UAAU,GAAG,cAAc,CAAC;AA2BrE,iFAAiF;AACjF,eAAO,MAAM,QAAQ,EAAE,SAAS,UAAU,EAwPzC,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAE1D,CAAC;AAEF,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAclF;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAW9C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAM7D;AAED,qGAAqG;AACrG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,CAK1D;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,kDAAkD;IAClD,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EACvC,MAAM,CAAC,EAAE,YAAY,GACpB,YAAY,EAAE,CAiBhB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EACvC,MAAM,EAAE,YAAY,GACnB,IAAI,CAaN"}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Every environment switch the platform reads, declared once.
3
+ *
4
+ * A switch is a variable that selects a code path (two paths, an enum of
5
+ * modes, or "armed when present"), as opposed to configuration such as a URL
6
+ * or a size. The readers spell their comparisons differently (`=== "1"` in
7
+ * most places, `=== "true"` in five, `!== "0"` for a default-on switch), and
8
+ * an operator who set `=1` on a `"true"` switch got the OFF path with no
9
+ * warning. This registry records each switch's EXISTING accepted spellings.
10
+ * `switchState` names the four states a raw value can be in, and
11
+ * `assertValidSwitches` refuses a boot on an invalid one. Readers resolve a
12
+ * switch through `switchEnabled` / `switchValue`, which return exactly what
13
+ * the literal comparisons they replaced returned;
14
+ * `test/switch-readers-parity.test.ts` holds those comparisons and the
15
+ * switches still read the old way.
16
+ *
17
+ * Widening a reader to accept both spellings is deliberately not done here:
18
+ * a value that is off today because of its spelling would turn on silently
19
+ * at the next deploy. Fix the value, deliberately, one switch at a time.
20
+ */
21
+ const ONE = ["1"];
22
+ const TRUE = ["true"];
23
+ const OFF_SPELLINGS = ["0", "false"];
24
+ // A default-on reader is written `!== "0"` (or `!== "false"`), which puts
25
+ // EVERY other spelling on the ON side; its disabled list is exactly that one
26
+ // literal, never both conventional spellings, or the registry would report as
27
+ // OFF a value the reader turns ON.
28
+ const bool = (name, readers, purpose, enabled, def = "disabled", disabled = OFF_SPELLINGS) => ({
29
+ kind: "boolean",
30
+ name,
31
+ readers,
32
+ purpose,
33
+ enabled,
34
+ disabled,
35
+ default: def,
36
+ });
37
+ /** The registry. Order: builders, isolation, admission, product, ops, levers. */
38
+ export const SWITCHES = [
39
+ // Build dispatch
40
+ bool("PERCHER_NODE_USE_DOCKERFILE", ["api"], 'Node/Bun apps build through the Percher Dockerfile instead of Nixpacks (default; "0" is the emergency lever)', ONE, "enabled", ["0"]),
41
+ bool("PERCHER_PYTHON_USE_DOCKERFILE", ["api"], 'Python apps build through the Percher Dockerfile instead of Nixpacks (default; "0" is the emergency lever)', ONE, "enabled", ["0"]),
42
+ bool("PERCHER_OPS_BUILD_TRIAGE", ["api"], "append the internal cold-build triage block to build logs", ONE),
43
+ bool("PERCHER_CAPPED_BUILDER_ACTIVE", ["worker"], "builds run on the capped buildx builder; set by the worker itself once that builder is ready, not by the operator", ONE),
44
+ // Tenant isolation and networks
45
+ bool("PERCHER_PER_APP_NETWORKS", ["api", "worker"], "per-app internal networks, pbproxy and per-app egress (production refuses to boot without it)", TRUE),
46
+ bool("PERCHER_CADDY_RATELIMIT_ENABLED", ["api"], "emit Caddy rate_limit handlers for apps that declare [web].rate_limit", ONE),
47
+ bool("PERCHER_UNKNOWN_HOST_404", ["api"], "answer app-suffix hosts that no route serves with a 404 instead of Caddy's empty 200", ONE),
48
+ bool("PERCHER_PB_AUTH_RATELIMIT_ENABLED", ["api"], "limit login, OTP and account-mail requests per client IP on public PocketBase hosts", ONE),
49
+ bool("PERCHER_EGRESS_PROBE", ["api"], "run the in-container egress probe in diagnostics", ONE),
50
+ bool("PERCHER_WILDCARD_TLS", ["api"], "one wildcard DNS-01 certificate instead of per-host certificates", ONE),
51
+ // Admission and disk
52
+ bool("PERCHER_MEASURED_ADMISSION", ["api"], "measured admission, overcommit and box eviction instead of reservation accounting", ONE),
53
+ {
54
+ kind: "enum",
55
+ name: "PERCHER_DISK_GATE",
56
+ readers: ["api"],
57
+ purpose: "refuse uploads when disk state is unknown (required) or allow them (off)",
58
+ values: ["required", "off"],
59
+ default: "required",
60
+ },
61
+ bool("DISK_PRESSURE_AUTO_SUSPEND", ["api"], "auto-suspend PocketBase apps after the disk-pressure grace timeline", TRUE),
62
+ bool("PERCHER_APPDATA_HARD_CAP_ENFORCE", ["api"], "suspend an app immediately when its appdata exceeds the hard cap", TRUE),
63
+ // Accounts and product surface
64
+ bool("PERCHER_REQUIRE_EMAIL_VERIFICATION", ["api"], "block API use until the account's email is verified", ONE),
65
+ bool("PERCHER_ANON_DEPLOYS", ["api"], "open the anonymous deploy lane", ONE),
66
+ bool("PERCHER_SUPPORT_ENABLED", ["api"], "mount the support routes and jobs", ONE),
67
+ bool("PERCHER_SUPPORT_DIGEST_SEND_EMPTY", ["api"], "send the daily support digest even when it is empty", ONE),
68
+ bool("PERCHER_HOSTED_MCP_ENABLED", ["api"], "mount the OAuth authorization server and register mcp.percher.app", ONE),
69
+ bool("PERCHER_LEGAL_NOTICES_ENABLED", ["api"], "send material legal-change notices instead of reporting only", ONE),
70
+ bool("PERCHER_DEPLOY_SCREENSHOTS", ["api", "worker"], "capture a post-deploy screenshot with headless Chromium", ONE),
71
+ bool("PERCHER_RUNTIME_ERROR_CAPTURE", ["api"], "capture runtime errors over CDP instead of the log-trick screenshot", ONE),
72
+ {
73
+ kind: "enum",
74
+ name: "PERCHER_MANAGED_INFERENCE_MODE",
75
+ readers: ["api"],
76
+ purpose: "managed inference enabled (subject to credentials and approval) or disabled",
77
+ values: ["disabled", "enabled"],
78
+ default: "disabled",
79
+ },
80
+ // Deploy pipeline
81
+ bool("PERCHER_CANARY_AUTO_ROLLBACK", ["api"], "roll back on a canary failure instead of logging", TRUE, "enabled", ["false"]),
82
+ bool("PERCHER_CANARY_EDGE_KEEPS_LIVE", ["api"], "keep a deploy live when the canary failure was Percher's own edge", ONE, "enabled", ["0"]),
83
+ bool("PERCHER_DRAIN_AT_SETTLEMENT", ["api"], "drain the predecessor at canary settlement instead of on a timer", ONE, "enabled", ["0"]),
84
+ // Security scanning
85
+ bool("SECURITY_BLOCK_DEPLOY", ["api"], "block the swap on red scan findings instead of advising", TRUE),
86
+ bool("SECURITY_AUTO_HOLD", ["api"], "runtime re-scan places red apps on security hold automatically", TRUE),
87
+ {
88
+ kind: "enum",
89
+ name: "PERCHER_INTERNAL_SIG_MODE",
90
+ readers: ["api"],
91
+ purpose: "HMAC signing of /internal/* calls: off, log or enforce",
92
+ values: ["off", "log", "enforce"],
93
+ default: "off",
94
+ },
95
+ bool("METRICS_PUBLIC", ["api"], "serve /metrics without the internal secret", TRUE),
96
+ // Dashboard
97
+ {
98
+ kind: "enum",
99
+ name: "PERCHER_CONTROL_CSP",
100
+ readers: ["dashboard"],
101
+ purpose: "content-security policy off, report-only or enforced",
102
+ values: ["off", "report", "enforce"],
103
+ default: "report",
104
+ normalize: true,
105
+ },
106
+ bool("NEXT_PUBLIC_PERCHER_E2E", ["dashboard"], "compile the e2e login-as route into the bundle (build time)", ONE),
107
+ {
108
+ kind: "presence",
109
+ name: "PERCHER_E2E_SECRET",
110
+ readers: ["dashboard"],
111
+ purpose: "the e2e login-as route works when set",
112
+ },
113
+ bool("PERCHER_ALLOW_NO_CAPTCHA", ["dashboard"], "allow production without Turnstile", ONE),
114
+ bool("PERCHER_ALLOW_UNVERIFIED_SIGNUP", ["dashboard"], "allow production without Resend", ONE),
115
+ bool("PERCHER_DASHBOARD_SECRET_BUILD", ["dashboard"], "allow an ephemeral BetterAuth key during `next build` only, never at runtime", ONE),
116
+ // Boot-guard opt-outs and topology
117
+ bool("PERCHER_ALLOW_NO_ALERTING", ["api"], "boot without an alert channel", ONE),
118
+ {
119
+ kind: "presence",
120
+ name: "PERCHER_WORKER_URL",
121
+ readers: ["api"],
122
+ purpose: "proxy container operations to the worker daemon (production topology) when set",
123
+ },
124
+ {
125
+ kind: "presence",
126
+ name: "PLATFORM_SUNSET_DATE",
127
+ readers: ["api"],
128
+ purpose: "sunset mode: banners, emails and the deletion schedule, when set",
129
+ },
130
+ // Client telemetry opt-outs
131
+ bool("PERCHER_NO_TELEMETRY", ["cli", "mcp"], "client telemetry off", ["1", "true"]),
132
+ bool("DO_NOT_TRACK", ["cli", "mcp"], "client telemetry off (the cross-tool convention)", ONE),
133
+ // Withdrawn provider seam
134
+ {
135
+ kind: "enum",
136
+ name: "CRASH_ANALYZER_PROVIDER",
137
+ readers: ["api"],
138
+ purpose: "the fake crash analyzer for tests, or none",
139
+ values: ["fake", "disabled"],
140
+ default: "disabled",
141
+ },
142
+ ];
143
+ export const SWITCH_BY_NAME = new Map(SWITCHES.map((s) => [s.name, s]));
144
+ /** The state a raw value puts a switch in, by that switch's own spellings. */
145
+ export function switchState(spec, raw) {
146
+ if (raw === undefined || raw === "")
147
+ return "unset";
148
+ switch (spec.kind) {
149
+ case "boolean":
150
+ if (spec.enabled.includes(raw))
151
+ return "enabled";
152
+ if (spec.disabled.includes(raw))
153
+ return "disabled";
154
+ return "invalid";
155
+ case "enum": {
156
+ const value = spec.normalize ? raw.trim().toLowerCase() : raw;
157
+ return spec.values.includes(value) ? "enabled" : "invalid";
158
+ }
159
+ case "presence":
160
+ return "enabled";
161
+ }
162
+ }
163
+ function registered(name, kind) {
164
+ const spec = SWITCH_BY_NAME.get(name);
165
+ if (spec?.kind !== kind)
166
+ throw new Error(`${name} is not a registered ${kind} switch`);
167
+ return spec;
168
+ }
169
+ /**
170
+ * The path a boolean switch selects: ON for an enabled spelling, OFF for a
171
+ * disabled one, and the switch's default when unset or invalid. That is what
172
+ * every `=== "1"` / `!== "0"` reader returned, so moving a reader here changes
173
+ * no verdict.
174
+ */
175
+ export function switchEnabled(name, env) {
176
+ const spec = registered(name, "boolean");
177
+ const state = switchState(spec, env[name]);
178
+ if (state === "enabled")
179
+ return true;
180
+ if (state === "disabled")
181
+ return false;
182
+ return spec.default === "enabled";
183
+ }
184
+ /** The mode an enum switch selects: its (normalized) value, or the default when unset or invalid. */
185
+ export function switchValue(name, env) {
186
+ const spec = registered(name, "enum");
187
+ const raw = env[name];
188
+ if (raw === undefined || switchState(spec, raw) !== "enabled")
189
+ return spec.default;
190
+ return spec.normalize ? raw.trim().toLowerCase() : raw;
191
+ }
192
+ /**
193
+ * The switches whose raw value is set but not a spelling any reader accepts.
194
+ * The input to the boot refusal; it never throws.
195
+ */
196
+ export function reportSwitches(env, reader) {
197
+ const out = [];
198
+ for (const spec of SWITCHES) {
199
+ if (reader && !spec.readers.includes(reader))
200
+ continue;
201
+ const raw = env[spec.name];
202
+ const state = switchState(spec, raw);
203
+ if (state !== "invalid" || raw === undefined)
204
+ continue;
205
+ const accepted = spec.kind === "boolean"
206
+ ? [...spec.enabled, ...spec.disabled]
207
+ : spec.kind === "enum"
208
+ ? spec.values
209
+ : [];
210
+ const fallback = spec.kind === "presence" ? "unset" : spec.default;
211
+ out.push({ name: spec.name, raw, state, accepted, fallback });
212
+ }
213
+ return out;
214
+ }
215
+ /**
216
+ * Refuse to boot on a set-but-invalid switch: the reader would silently take
217
+ * its default path, which is how a `"true"` switch set to `1` stayed off.
218
+ * A disabled spelling and an unset variable never refuse. The error names
219
+ * every invalid switch with its raw value and accepted spellings. Call it
220
+ * before any other boot guard and before any listener starts.
221
+ */
222
+ export function assertValidSwitches(env, reader) {
223
+ const invalid = reportSwitches(env, reader);
224
+ if (invalid.length === 0)
225
+ return;
226
+ throw new Error(invalid
227
+ .map((r) => `[percher-${reader}] refusing to start: ${r.name}=${JSON.stringify(r.raw)} is not a spelling this switch accepts. ` +
228
+ `Accepted: ${r.accepted.map((a) => JSON.stringify(a)).join(", ")}. ` +
229
+ `Unset it for the default (${r.fallback}).`)
230
+ .join("\n"));
231
+ }
232
+ //# sourceMappingURL=switches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switches.js","sourceRoot":"","sources":["../src/switches.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.0.2",
3
+ "version": "1.2.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",
@@ -36,6 +36,16 @@
36
36
  "import": "./dist/pow.js",
37
37
  "default": "./dist/pow.js"
38
38
  },
39
+ "./secure-compare": {
40
+ "types": "./dist/secure-compare.d.ts",
41
+ "import": "./dist/secure-compare.js",
42
+ "default": "./dist/secure-compare.js"
43
+ },
44
+ "./waitlist-token": {
45
+ "types": "./dist/waitlist-token.d.ts",
46
+ "import": "./dist/waitlist-token.js",
47
+ "default": "./dist/waitlist-token.js"
48
+ },
39
49
  "./signup-invite-hash": {
40
50
  "types": "./dist/signup-invite-hash.d.ts",
41
51
  "import": "./dist/signup-invite-hash.js",