@percher/shared 0.1.8 → 1.0.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 (53) hide show
  1. package/LICENSE +132 -0
  2. package/dist/brand.d.ts +64 -0
  3. package/dist/brand.d.ts.map +1 -0
  4. package/dist/brand.js +67 -0
  5. package/dist/brand.js.map +1 -0
  6. package/dist/client-ip-attestation.d.ts +67 -0
  7. package/dist/client-ip-attestation.d.ts.map +1 -0
  8. package/dist/client-ip-attestation.js +74 -0
  9. package/dist/client-ip-attestation.js.map +1 -0
  10. package/dist/index.d.ts +10 -3
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +10 -3
  13. package/dist/index.js.map +1 -1
  14. package/dist/legal-changes.d.ts +118 -0
  15. package/dist/legal-changes.d.ts.map +1 -0
  16. package/dist/legal-changes.js +135 -0
  17. package/dist/legal-changes.js.map +1 -0
  18. package/dist/legal.d.ts +58 -0
  19. package/dist/legal.d.ts.map +1 -0
  20. package/dist/legal.js +54 -0
  21. package/dist/legal.js.map +1 -0
  22. package/dist/limits.d.ts +8 -0
  23. package/dist/limits.d.ts.map +1 -1
  24. package/dist/limits.js +8 -0
  25. package/dist/limits.js.map +1 -1
  26. package/dist/oauth-client-name.d.ts +22 -0
  27. package/dist/oauth-client-name.d.ts.map +1 -0
  28. package/dist/oauth-client-name.js +37 -0
  29. package/dist/oauth-client-name.js.map +1 -0
  30. package/dist/oauth-scopes.d.ts +19 -0
  31. package/dist/oauth-scopes.d.ts.map +1 -0
  32. package/dist/oauth-scopes.js +31 -0
  33. package/dist/oauth-scopes.js.map +1 -0
  34. package/dist/platform-env.d.ts.map +1 -1
  35. package/dist/platform-env.js +6 -0
  36. package/dist/platform-env.js.map +1 -1
  37. package/dist/platform.d.ts +18 -1
  38. package/dist/platform.d.ts.map +1 -1
  39. package/dist/platform.js +18 -1
  40. package/dist/platform.js.map +1 -1
  41. package/dist/signup-invite-hash.d.ts +8 -0
  42. package/dist/signup-invite-hash.d.ts.map +1 -0
  43. package/dist/signup-invite-hash.js +11 -0
  44. package/dist/signup-invite-hash.js.map +1 -0
  45. package/dist/signup-invite.d.ts +23 -0
  46. package/dist/signup-invite.d.ts.map +1 -0
  47. package/dist/signup-invite.js +29 -0
  48. package/dist/signup-invite.js.map +1 -0
  49. package/dist/timeouts.d.ts +85 -0
  50. package/dist/timeouts.d.ts.map +1 -1
  51. package/dist/timeouts.js +84 -0
  52. package/dist/timeouts.js.map +1 -1
  53. package/package.json +12 -2
package/LICENSE ADDED
@@ -0,0 +1,132 @@
1
+ # Scope and exceptions
2
+
3
+ This licence applies to Percher-authored source, tests, scripts, configuration
4
+ and generated artefacts in this package, whether published or unpublished.
5
+ Versions below 1.0.0 retain their original MIT grants; version 1.0.0 and later
6
+ use FSL-1.1-MIT. Earlier MIT permissions are not revoked or withdrawn.
7
+
8
+ Scaffold payloads from packages/core/templates and AI instruction payloads
9
+ from packages/core/src/templates/ai-files and docs/CLAUDE.md, including their distributed copies
10
+ and generated output, are licensed under 0BSD, as carried with those payloads.
11
+ This exception does not change the licence of a customer's existing project.
12
+
13
+ Third-party components retain their own licences, including components named
14
+ in THIRD-PARTY-NOTICES.txt and the Apache-2.0 fixture files enumerated in
15
+ packages/core/test/fixtures/supabase-apps/*/SOURCES.md under their own LICENSE.
16
+ FSL does not relicense those components. packages/mark is private and excluded
17
+ from all MIT, 0BSD and FSL grants here.
18
+
19
+ In case of overlap, third-party component licences take precedence, followed
20
+ by packages/mark/LICENSE, the 0BSD payload exception, this package LICENSE,
21
+ and finally the repository root LICENSE.
22
+
23
+ # Functional Source License, Version 1.1, MIT Future License
24
+
25
+ ## Abbreviation
26
+
27
+ FSL-1.1-MIT
28
+
29
+ ## Notice
30
+
31
+ Copyright 2026 Mattias Malmborg
32
+
33
+ ## Terms and Conditions
34
+
35
+ ### Licensor ("We")
36
+
37
+ The party offering the Software under these Terms and Conditions.
38
+
39
+ ### The Software
40
+
41
+ The "Software" is each version of the software that we make available under
42
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
43
+ Conditions with the Software.
44
+
45
+ ### License Grant
46
+
47
+ Subject to your compliance with this License Grant and the Patents,
48
+ Redistribution and Trademark clauses below, we hereby grant you the right to
49
+ use, copy, modify, create derivative works, publicly perform, publicly display
50
+ and redistribute the Software for any Permitted Purpose identified below.
51
+
52
+ ### Permitted Purpose
53
+
54
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
55
+ means making the Software available to others in a commercial product or
56
+ service that:
57
+
58
+ 1. substitutes for the Software;
59
+
60
+ 2. substitutes for any other product or service we offer using the Software
61
+ that exists as of the date we make the Software available; or
62
+
63
+ 3. offers the same or substantially similar functionality as the Software.
64
+
65
+ Permitted Purposes specifically include using the Software:
66
+
67
+ 1. for your internal use and access;
68
+
69
+ 2. for non-commercial education;
70
+
71
+ 3. for non-commercial research; and
72
+
73
+ 4. in connection with professional services that you provide to a licensee
74
+ using the Software in accordance with these Terms and Conditions.
75
+
76
+ ### Patents
77
+
78
+ To the extent your use for a Permitted Purpose would necessarily infringe our
79
+ patents, the license grant above includes a license under our patents. If you
80
+ make a claim against any party that the Software infringes or contributes to
81
+ the infringement of any patent, then your patent license to the Software ends
82
+ immediately.
83
+
84
+ ### Redistribution
85
+
86
+ The Terms and Conditions apply to all copies, modifications and derivatives of
87
+ the Software.
88
+
89
+ If you redistribute any copies, modifications or derivatives of the Software,
90
+ you must include a copy of or a link to these Terms and Conditions and not
91
+ remove any copyright notices provided in or with the Software.
92
+
93
+ ### Disclaimer
94
+
95
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
96
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
97
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
98
+
99
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
100
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
101
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
102
+
103
+ ### Trademarks
104
+
105
+ Except for displaying the License Details and identifying us as the origin of
106
+ the Software, you have no right under these Terms and Conditions to use our
107
+ trademarks, trade names, service marks or product names.
108
+
109
+ ## Grant of Future License
110
+
111
+ We hereby irrevocably grant you an additional license to use the Software under
112
+ the MIT license that is effective on the second anniversary of the date we make
113
+ the Software available. On or after that date, you may use the Software under
114
+ the MIT license, in which case the following will apply:
115
+
116
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
117
+ this software and associated documentation files (the "Software"), to deal in
118
+ the Software without restriction, including without limitation the rights to
119
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
120
+ of the Software, and to permit persons to whom the Software is furnished to do
121
+ so, subject to the following conditions:
122
+
123
+ The above copyright notice and this permission notice shall be included in all
124
+ copies or substantial portions of the Software.
125
+
126
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
127
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
128
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
129
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
130
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
131
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
132
+ SOFTWARE.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Brand colours for the places that CANNOT read a CSS variable.
3
+ *
4
+ * The palette's real home is `apps/dashboard/app/theme.css`, and anything
5
+ * rendering inside `.pc-theme` must keep using `var(--pc-primary)` and friends
6
+ * — those adapt per theme, these cannot. This file exists for the contexts
7
+ * where a custom property never resolves:
8
+ *
9
+ * - HTML email (no CSS variable support in most clients)
10
+ * - satori OG images (renders to a bitmap, no cascade)
11
+ * - downloadable artifacts: PWA manifests, icon files, the Caddy badge
12
+ * - pages served outside the dashboard's React tree (the app password gate)
13
+ * - `var(--token, fallback)` fallbacks, for the moment before the sheet loads
14
+ *
15
+ * Before this existed those contexts each kept their own literal, and they
16
+ * drifted: thirteen email templates, the password gate and the customer-facing
17
+ * manifest generator were all still painting `#2d6a4f` — a green that has never
18
+ * matched `--pc-primary` in any version of the v2 palette.
19
+ *
20
+ * `apps/dashboard/test/lib/brand-tokens.test.ts` parses theme.css and fails if
21
+ * any value here disagrees with it, so the two cannot drift apart again
22
+ * silently. If that test fails, theme.css is right and this file is stale.
23
+ *
24
+ * Colours only. The mark's geometry lives in `@percher/mark`, a private
25
+ * workspace package with its own proprietary LICENSE, because this package is
26
+ * published to npm and the artwork must not be inside anything that carries a
27
+ * copying grant. Hex values are not the thing being protected, and published
28
+ * `@percher/core/appstore-artifacts` needs `iconColourHex` to build the user's
29
+ * own PWA manifest, so the palette stays here and ships.
30
+ */
31
+ export declare const BRAND: {
32
+ /** `--pc-primary`, light theme. The green in every email and manifest. */
33
+ readonly green: "#2e8055";
34
+ /** `--pc-primary`, dark theme. OG cards and the dark PWA icons are dark-only. */
35
+ readonly greenDark: "#4faa7d";
36
+ /** `--pc-accent`, light theme. */
37
+ readonly pink: "#d4004c";
38
+ /** `--pc-accent`, dark theme. */
39
+ readonly pinkDark: "#e26a8d";
40
+ /** `--pc-accent-bg` / `--pc-accent-text`, light — only needed as fallbacks. */
41
+ readonly pinkTint: "#fce8ee";
42
+ /** Ink on a light surface — `--pc-fg`. */
43
+ readonly ink: "#33291d";
44
+ };
45
+ /**
46
+ * The owner-pickable icon colours, keyed by the value stored in the apps
47
+ * table's `icon_colour` column.
48
+ *
49
+ * NOTE the `terracotta` key. The colour it names became pink when the accent
50
+ * moved; the key did not, because it is persisted data and renaming it is a
51
+ * migration, not a rename. Read it as "the secondary accent" — the label shown
52
+ * in the picker is defined separately, and does say Pink.
53
+ */
54
+ export declare const ICON_COLOURS: {
55
+ readonly green: "#2e8055";
56
+ readonly terracotta: "#d4004c";
57
+ readonly blue: "#4a6fa4";
58
+ readonly purple: "#6e4dba";
59
+ readonly olive: "#b8923a";
60
+ };
61
+ export type IconColourKey = keyof typeof ICON_COLOURS;
62
+ /** Resolve a stored `icon_colour` to a hex, falling back to the brand green. */
63
+ export declare function iconColourHex(token: string | null | undefined): string;
64
+ //# sourceMappingURL=brand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../src/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,KAAK;IAChB,0EAA0E;aAC1E,KAAK,EAAE,SAAS;IAChB,iFAAiF;aACjF,SAAS,EAAE,SAAS;IACpB,kCAAkC;aAClC,IAAI,EAAE,SAAS;IACf,iCAAiC;aACjC,QAAQ,EAAE,SAAS;IACnB,+EAA+E;aAC/E,QAAQ,EAAE,SAAS;IACnB,0CAA0C;aAC1C,GAAG,EAAE,SAAS;CACN,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY;aACvB,KAAK;aACL,UAAU;aACV,IAAI,EAAE,SAAS;aACf,MAAM,EAAE,SAAS;aACjB,KAAK,EAAE,SAAS;CACR,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,YAAY,CAAC;AAEtD,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGtE"}
package/dist/brand.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Brand colours for the places that CANNOT read a CSS variable.
3
+ *
4
+ * The palette's real home is `apps/dashboard/app/theme.css`, and anything
5
+ * rendering inside `.pc-theme` must keep using `var(--pc-primary)` and friends
6
+ * — those adapt per theme, these cannot. This file exists for the contexts
7
+ * where a custom property never resolves:
8
+ *
9
+ * - HTML email (no CSS variable support in most clients)
10
+ * - satori OG images (renders to a bitmap, no cascade)
11
+ * - downloadable artifacts: PWA manifests, icon files, the Caddy badge
12
+ * - pages served outside the dashboard's React tree (the app password gate)
13
+ * - `var(--token, fallback)` fallbacks, for the moment before the sheet loads
14
+ *
15
+ * Before this existed those contexts each kept their own literal, and they
16
+ * drifted: thirteen email templates, the password gate and the customer-facing
17
+ * manifest generator were all still painting `#2d6a4f` — a green that has never
18
+ * matched `--pc-primary` in any version of the v2 palette.
19
+ *
20
+ * `apps/dashboard/test/lib/brand-tokens.test.ts` parses theme.css and fails if
21
+ * any value here disagrees with it, so the two cannot drift apart again
22
+ * silently. If that test fails, theme.css is right and this file is stale.
23
+ *
24
+ * Colours only. The mark's geometry lives in `@percher/mark`, a private
25
+ * workspace package with its own proprietary LICENSE, because this package is
26
+ * published to npm and the artwork must not be inside anything that carries a
27
+ * copying grant. Hex values are not the thing being protected, and published
28
+ * `@percher/core/appstore-artifacts` needs `iconColourHex` to build the user's
29
+ * own PWA manifest, so the palette stays here and ships.
30
+ */
31
+ export const BRAND = {
32
+ /** `--pc-primary`, light theme. The green in every email and manifest. */
33
+ green: "#2e8055",
34
+ /** `--pc-primary`, dark theme. OG cards and the dark PWA icons are dark-only. */
35
+ greenDark: "#4faa7d",
36
+ /** `--pc-accent`, light theme. */
37
+ pink: "#d4004c",
38
+ /** `--pc-accent`, dark theme. */
39
+ pinkDark: "#e26a8d",
40
+ /** `--pc-accent-bg` / `--pc-accent-text`, light — only needed as fallbacks. */
41
+ pinkTint: "#fce8ee",
42
+ /** Ink on a light surface — `--pc-fg`. */
43
+ ink: "#33291d",
44
+ };
45
+ /**
46
+ * The owner-pickable icon colours, keyed by the value stored in the apps
47
+ * table's `icon_colour` column.
48
+ *
49
+ * NOTE the `terracotta` key. The colour it names became pink when the accent
50
+ * moved; the key did not, because it is persisted data and renaming it is a
51
+ * migration, not a rename. Read it as "the secondary accent" — the label shown
52
+ * in the picker is defined separately, and does say Pink.
53
+ */
54
+ export const ICON_COLOURS = {
55
+ green: BRAND.green,
56
+ terracotta: BRAND.pink,
57
+ blue: "#4a6fa4",
58
+ purple: "#6e4dba",
59
+ olive: "#b8923a",
60
+ };
61
+ /** Resolve a stored `icon_colour` to a hex, falling back to the brand green. */
62
+ export function iconColourHex(token) {
63
+ if (!token)
64
+ return BRAND.green;
65
+ return ICON_COLOURS[token] ?? BRAND.green;
66
+ }
67
+ //# sourceMappingURL=brand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand.js","sourceRoot":"","sources":["../src/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,0EAA0E;IAC1E,KAAK,EAAE,SAAS;IAChB,iFAAiF;IACjF,SAAS,EAAE,SAAS;IACpB,kCAAkC;IAClC,IAAI,EAAE,SAAS;IACf,iCAAiC;IACjC,QAAQ,EAAE,SAAS;IACnB,+EAA+E;IAC/E,QAAQ,EAAE,SAAS;IACnB,0CAA0C;IAC1C,GAAG,EAAE,SAAS;CACN,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,KAAK,EAAE,KAAK,CAAC,KAAK;IAClB,UAAU,EAAE,KAAK,CAAC,IAAI;IACtB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,KAAK,EAAE,SAAS;CACR,CAAC;AAIX,gFAAgF;AAChF,MAAM,UAAU,aAAa,CAAC,KAAgC;IAC5D,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC;IAC/B,OAAO,YAAY,CAAC,KAAsB,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Wire contract for a signed, single-use client-IP attestation on the
3
+ * dashboard -> API hop.
4
+ *
5
+ * The API decides a request's rate-limit / audit IP from `X-Real-IP`, trusted
6
+ * only behind Caddy's `X-Percher-Edge` marker. That marker is a REUSABLE bearer:
7
+ * whoever holds it can assert any IP, on any request, forever. It is the right
8
+ * shape for Caddy (one hop, config-injected, nothing else to work with), and the
9
+ * wrong shape for a service forwarding an end user's IP — an observer of one
10
+ * cleartext control-network request could lift it and re-assert it with an IP of
11
+ * their choosing, forging attribution and smearing rate limits.
12
+ *
13
+ * So a service that HOLDS the internal secret proves the IP instead of bearing a
14
+ * token for it: an HMAC over (version, ip, timestamp). Three properties follow,
15
+ * and all three are the point:
16
+ *
17
+ * - it is bound to ONE ip — a captured attestation re-asserts that same
18
+ * address and nothing else, so it cannot be aimed at a victim;
19
+ * - it is bound to a timestamp — replay dies with the skew window, rather
20
+ * than lasting until the internal secret is rotated;
21
+ * - it never puts a long-lived credential on the wire at all.
22
+ *
23
+ * A service that holds NO secret (the hosted-MCP transport, deliberately) cannot
24
+ * sign, and keeps forwarding Caddy's marker; that hop is bounded by the
25
+ * per-bearer and per-IP limits instead. This module is the single source of the
26
+ * canonical string so the signer and the verifier cannot drift, and it is
27
+ * dependency-free (no crypto) so it bundles into the dashboard (Node) as well as
28
+ * the API (Bun) — the same shape as ./internal-sig.
29
+ */
30
+ /** The asserted client IP. Meaningless without the proof header below. */
31
+ export declare const CLIENT_IP_HEADER = "X-Percher-Client-IP";
32
+ /** `<unix-ms>.<hex hmac-sha256>` over {@link clientIpAttestationPayload}. */
33
+ export declare const CLIENT_IP_PROOF_HEADER = "X-Percher-Client-IP-Proof";
34
+ /** Canonical-form version, so a verifier can refuse a shape it can't compare. */
35
+ export declare const CLIENT_IP_ATTESTATION_VERSION = "v1";
36
+ /**
37
+ * Replay window. Deliberately much tighter than the 5 minutes `/internal/*`
38
+ * signing allows: the signer and verifier are two containers on one host with
39
+ * no meaningful clock skew, and the only thing a replay buys is re-asserting an
40
+ * IP that genuinely just made a request — so there is no reason to leave the
41
+ * door open for minutes.
42
+ */
43
+ export declare const CLIENT_IP_ATTESTATION_MAX_SKEW_MS = 60000;
44
+ /** Longest asserted IP accepted. An IPv6 address with a zone fits in 45. */
45
+ export declare const CLIENT_IP_MAX_LENGTH = 45;
46
+ export interface ClientIpAttestationParts {
47
+ /** The end user's address, exactly as it will be sent. */
48
+ ip: string;
49
+ /** The value placed in the proof header, stringified verbatim so the
50
+ * verifier signs the exact bytes — no Number round-trip. */
51
+ timestamp: number | string;
52
+ }
53
+ /** The exact string both the signer and the verifier feed to HMAC-SHA256. */
54
+ export declare function clientIpAttestationPayload(parts: ClientIpAttestationParts): string;
55
+ /** Assemble the proof header value. */
56
+ export declare function formatClientIpProof(timestamp: number | string, signature: string): string;
57
+ /**
58
+ * Split a proof header back into its parts, or null when it is not the
59
+ * `<digits>.<hex>` shape. Returning null (rather than throwing) keeps a
60
+ * malformed header indistinguishable from an absent one at the call site: both
61
+ * mean "not attested", which is the safe reading.
62
+ */
63
+ export declare function parseClientIpProof(raw: string | null | undefined): {
64
+ timestamp: string;
65
+ signature: string;
66
+ } | null;
67
+ //# sourceMappingURL=client-ip-attestation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip-attestation.d.ts","sourceRoot":"","sources":["../src/client-ip-attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AACtD,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,8BAA8B,CAAC;AAElE,iFAAiF;AACjF,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,QAAS,CAAC;AAExD,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,MAAM,WAAW,wBAAwB;IACvC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX;iEAC6D;IAC7D,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B;AAED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAElF;AAED,uCAAuC;AACvC,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASjD"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Wire contract for a signed, single-use client-IP attestation on the
3
+ * dashboard -> API hop.
4
+ *
5
+ * The API decides a request's rate-limit / audit IP from `X-Real-IP`, trusted
6
+ * only behind Caddy's `X-Percher-Edge` marker. That marker is a REUSABLE bearer:
7
+ * whoever holds it can assert any IP, on any request, forever. It is the right
8
+ * shape for Caddy (one hop, config-injected, nothing else to work with), and the
9
+ * wrong shape for a service forwarding an end user's IP — an observer of one
10
+ * cleartext control-network request could lift it and re-assert it with an IP of
11
+ * their choosing, forging attribution and smearing rate limits.
12
+ *
13
+ * So a service that HOLDS the internal secret proves the IP instead of bearing a
14
+ * token for it: an HMAC over (version, ip, timestamp). Three properties follow,
15
+ * and all three are the point:
16
+ *
17
+ * - it is bound to ONE ip — a captured attestation re-asserts that same
18
+ * address and nothing else, so it cannot be aimed at a victim;
19
+ * - it is bound to a timestamp — replay dies with the skew window, rather
20
+ * than lasting until the internal secret is rotated;
21
+ * - it never puts a long-lived credential on the wire at all.
22
+ *
23
+ * A service that holds NO secret (the hosted-MCP transport, deliberately) cannot
24
+ * sign, and keeps forwarding Caddy's marker; that hop is bounded by the
25
+ * per-bearer and per-IP limits instead. This module is the single source of the
26
+ * canonical string so the signer and the verifier cannot drift, and it is
27
+ * dependency-free (no crypto) so it bundles into the dashboard (Node) as well as
28
+ * the API (Bun) — the same shape as ./internal-sig.
29
+ */
30
+ /** The asserted client IP. Meaningless without the proof header below. */
31
+ export const CLIENT_IP_HEADER = "X-Percher-Client-IP";
32
+ /** `<unix-ms>.<hex hmac-sha256>` over {@link clientIpAttestationPayload}. */
33
+ export const CLIENT_IP_PROOF_HEADER = "X-Percher-Client-IP-Proof";
34
+ /** Canonical-form version, so a verifier can refuse a shape it can't compare. */
35
+ export const CLIENT_IP_ATTESTATION_VERSION = "v1";
36
+ /**
37
+ * Replay window. Deliberately much tighter than the 5 minutes `/internal/*`
38
+ * signing allows: the signer and verifier are two containers on one host with
39
+ * no meaningful clock skew, and the only thing a replay buys is re-asserting an
40
+ * IP that genuinely just made a request — so there is no reason to leave the
41
+ * door open for minutes.
42
+ */
43
+ export const CLIENT_IP_ATTESTATION_MAX_SKEW_MS = 60_000;
44
+ /** Longest asserted IP accepted. An IPv6 address with a zone fits in 45. */
45
+ export const CLIENT_IP_MAX_LENGTH = 45;
46
+ /** The exact string both the signer and the verifier feed to HMAC-SHA256. */
47
+ export function clientIpAttestationPayload(parts) {
48
+ return [CLIENT_IP_ATTESTATION_VERSION, parts.ip, String(parts.timestamp)].join("\n");
49
+ }
50
+ /** Assemble the proof header value. */
51
+ export function formatClientIpProof(timestamp, signature) {
52
+ return `${timestamp}.${signature}`;
53
+ }
54
+ /**
55
+ * Split a proof header back into its parts, or null when it is not the
56
+ * `<digits>.<hex>` shape. Returning null (rather than throwing) keeps a
57
+ * malformed header indistinguishable from an absent one at the call site: both
58
+ * mean "not attested", which is the safe reading.
59
+ */
60
+ export function parseClientIpProof(raw) {
61
+ if (!raw)
62
+ return null;
63
+ const dot = raw.indexOf(".");
64
+ if (dot <= 0 || dot === raw.length - 1)
65
+ return null;
66
+ const timestamp = raw.slice(0, dot);
67
+ const signature = raw.slice(dot + 1);
68
+ if (!/^\d{1,15}$/.test(timestamp))
69
+ return null;
70
+ if (!/^[0-9a-f]{64}$/i.test(signature))
71
+ return null;
72
+ return { timestamp, signature };
73
+ }
74
+ //# sourceMappingURL=client-ip-attestation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip-attestation.js","sourceRoot":"","sources":["../src/client-ip-attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG,qBAAqB,CAAC;AACtD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,sBAAsB,GAAG,2BAA2B,CAAC;AAElE,iFAAiF;AACjF,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,MAAM,CAAC;AAExD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAUvC,6EAA6E;AAC7E,MAAM,UAAU,0BAA0B,CAAC,KAA+B;IACxE,OAAO,CAAC,6BAA6B,EAAE,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvF,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,mBAAmB,CAAC,SAA0B,EAAE,SAAiB;IAC/E,OAAO,GAAG,SAAS,IAAI,SAAS,EAAE,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAA8B;IAE9B,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACrC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;AAClC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  export { APP_NAME_RE } from "./app-name.js";
2
+ export { BRAND, ICON_COLOURS, type IconColourKey, iconColourHex } from "./brand.js";
3
+ export { CLIENT_IP_ATTESTATION_MAX_SKEW_MS, CLIENT_IP_ATTESTATION_VERSION, CLIENT_IP_HEADER, CLIENT_IP_MAX_LENGTH, CLIENT_IP_PROOF_HEADER, type ClientIpAttestationParts, clientIpAttestationPayload, formatClientIpProof, parseClientIpProof, } from "./client-ip-attestation.js";
2
4
  export { INTERNAL_SIG_HEADER, INTERNAL_SIG_MAX_SKEW_MS, INTERNAL_SIG_VERSION, INTERNAL_TS_HEADER, type InternalSigParts, internalSigPayload, } from "./internal-sig.js";
3
- export { MAX_INLINE_PUBLISH_BYTES, MAX_OPEN_UPLOAD_SESSIONS_PER_USER, MAX_TARBALL_BYTES, MAX_UPLOAD_CHUNK_BYTES, MAX_UPLOAD_SESSION_FILES, MIN_UPLOAD_CHUNK_BYTES, UPLOAD_SESSION_MAX_LIFETIME_MS, UPLOAD_SESSION_TTL_MS, } from "./limits.js";
5
+ export { CURRENT_TERMS_VERSION, LEGAL_DOCUMENT_HASHES, LEGAL_DOCUMENT_LAST_UPDATED, LEGAL_DOCUMENT_VERSIONS, type TermsAcceptanceEvidence, } from "./legal.js";
6
+ export { canonicalLegalChangeJson, compareVersions, declaredVersion, LEGAL_CHANGE_ID_PATTERN, LEGAL_CHANGES, LEGAL_VERSION_PATTERN, type LegalChange, type LegalChangeDocument, type LegalChangeEnforcement, type LegalChangeReason, type LegalDocumentKey, MATERIAL_CHANGE_NOTICE_MS, openMaterialChanges, pendingAcknowledgement, SIGNUP_DISCLOSURE_HEADER, } from "./legal-changes.js";
7
+ export { MAX_ANON_TARBALL_BYTES, MAX_INLINE_PUBLISH_BYTES, MAX_OPEN_UPLOAD_SESSIONS_PER_USER, MAX_TARBALL_BYTES, MAX_UPLOAD_CHUNK_BYTES, MAX_UPLOAD_SESSION_FILES, MIN_UPLOAD_CHUNK_BYTES, UPLOAD_SESSION_MAX_LIFETIME_MS, UPLOAD_SESSION_TTL_MS, } from "./limits.js";
4
8
  export { MAX_LOG_SEARCH_LENGTH } from "./logs.js";
5
- export { DEFAULT_DOMAIN_SUFFIX, DEFAULT_MCP_HTTP_PORT, DEFAULT_POCKETBASE_IMAGE, } from "./platform.js";
9
+ export { MAX_OAUTH_CLIENT_NAME_DISPLAY_CODE_POINTS, normaliseOAuthClientName, } from "./oauth-client-name.js";
10
+ export { describeOAuthScope, OAUTH_SCOPE_LABELS, parseOAuthScopes } from "./oauth-scopes.js";
11
+ export { DEFAULT_DOMAIN_SUFFIX, DEFAULT_MCP_HTTP_PORT, DEFAULT_POCKETBASE_IMAGE, MCP_TOOL_COUNT_HOSTED, MCP_TOOL_COUNT_LOCAL, } from "./platform.js";
6
12
  export { canonicalInjectedKey, isPercherInjected, PERCHER_INJECTED_ENV_KEYS, } from "./platform-env.js";
7
13
  export { DEFAULT_NODE_VERSION_LABEL, DEFAULT_NODE_VERSION_MAJOR } from "./runtime.js";
8
- export { TIMEOUTS, type TimeoutKey } from "./timeouts.js";
14
+ export { normalizeSignupInviteCode, SIGNUP_INVITE_CODE_ALPHABET, SIGNUP_INVITE_CODE_LENGTH, SIGNUP_INVITE_CODE_PREFIX, SIGNUP_INVITE_HEADER, } from "./signup-invite.js";
15
+ export { BUN_MAX_IDLE_TIMEOUT_SECONDS, HTTP_IDLE_TIMEOUT_SECONDS, type HttpIdleTimeoutSurface, TIMEOUTS, type TimeoutKey, } from "./timeouts.js";
9
16
  export { WAITLIST_THROTTLE_WINDOW_MS } from "./waitlist.js";
10
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,EACtB,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EACL,iCAAiC,EACjC,6BAA6B,EAC7B,gBAAgB,EAChB,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,2BAA2B,EAC3B,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,iCAAiC,EACjC,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,EACtB,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EACL,yCAAyC,EACzC,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,QAAQ,EACR,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,10 +1,17 @@
1
1
  export { APP_NAME_RE } from "./app-name.js";
2
+ export { BRAND, ICON_COLOURS, iconColourHex } from "./brand.js";
3
+ export { CLIENT_IP_ATTESTATION_MAX_SKEW_MS, CLIENT_IP_ATTESTATION_VERSION, CLIENT_IP_HEADER, CLIENT_IP_MAX_LENGTH, CLIENT_IP_PROOF_HEADER, clientIpAttestationPayload, formatClientIpProof, parseClientIpProof, } from "./client-ip-attestation.js";
2
4
  export { INTERNAL_SIG_HEADER, INTERNAL_SIG_MAX_SKEW_MS, INTERNAL_SIG_VERSION, INTERNAL_TS_HEADER, internalSigPayload, } from "./internal-sig.js";
3
- export { MAX_INLINE_PUBLISH_BYTES, MAX_OPEN_UPLOAD_SESSIONS_PER_USER, MAX_TARBALL_BYTES, MAX_UPLOAD_CHUNK_BYTES, MAX_UPLOAD_SESSION_FILES, MIN_UPLOAD_CHUNK_BYTES, UPLOAD_SESSION_MAX_LIFETIME_MS, UPLOAD_SESSION_TTL_MS, } from "./limits.js";
5
+ export { CURRENT_TERMS_VERSION, LEGAL_DOCUMENT_HASHES, LEGAL_DOCUMENT_LAST_UPDATED, LEGAL_DOCUMENT_VERSIONS, } from "./legal.js";
6
+ export { canonicalLegalChangeJson, compareVersions, declaredVersion, LEGAL_CHANGE_ID_PATTERN, LEGAL_CHANGES, LEGAL_VERSION_PATTERN, MATERIAL_CHANGE_NOTICE_MS, openMaterialChanges, pendingAcknowledgement, SIGNUP_DISCLOSURE_HEADER, } from "./legal-changes.js";
7
+ export { MAX_ANON_TARBALL_BYTES, MAX_INLINE_PUBLISH_BYTES, MAX_OPEN_UPLOAD_SESSIONS_PER_USER, MAX_TARBALL_BYTES, MAX_UPLOAD_CHUNK_BYTES, MAX_UPLOAD_SESSION_FILES, MIN_UPLOAD_CHUNK_BYTES, UPLOAD_SESSION_MAX_LIFETIME_MS, UPLOAD_SESSION_TTL_MS, } from "./limits.js";
4
8
  export { MAX_LOG_SEARCH_LENGTH } from "./logs.js";
5
- export { DEFAULT_DOMAIN_SUFFIX, DEFAULT_MCP_HTTP_PORT, DEFAULT_POCKETBASE_IMAGE, } from "./platform.js";
9
+ export { MAX_OAUTH_CLIENT_NAME_DISPLAY_CODE_POINTS, normaliseOAuthClientName, } from "./oauth-client-name.js";
10
+ export { describeOAuthScope, OAUTH_SCOPE_LABELS, parseOAuthScopes } from "./oauth-scopes.js";
11
+ export { DEFAULT_DOMAIN_SUFFIX, DEFAULT_MCP_HTTP_PORT, DEFAULT_POCKETBASE_IMAGE, MCP_TOOL_COUNT_HOSTED, MCP_TOOL_COUNT_LOCAL, } from "./platform.js";
6
12
  export { canonicalInjectedKey, isPercherInjected, PERCHER_INJECTED_ENV_KEYS, } from "./platform-env.js";
7
13
  export { DEFAULT_NODE_VERSION_LABEL, DEFAULT_NODE_VERSION_MAJOR } from "./runtime.js";
8
- export { TIMEOUTS } from "./timeouts.js";
14
+ export { normalizeSignupInviteCode, SIGNUP_INVITE_CODE_ALPHABET, SIGNUP_INVITE_CODE_LENGTH, SIGNUP_INVITE_CODE_PREFIX, SIGNUP_INVITE_HEADER, } from "./signup-invite.js";
15
+ export { BUN_MAX_IDLE_TIMEOUT_SECONDS, HTTP_IDLE_TIMEOUT_SECONDS, TIMEOUTS, } from "./timeouts.js";
9
16
  export { WAITLIST_THROTTLE_WINDOW_MS } from "./waitlist.js";
10
17
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAElB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,EACtB,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,EAAE,QAAQ,EAAmB,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,YAAY,EAAsB,aAAa,EAAE,MAAM,SAAS,CAAC;AACjF,OAAO,EACL,iCAAiC,EACjC,6BAA6B,EAC7B,gBAAgB,EAChB,oBAAoB,EACpB,sBAAsB,EAEtB,0BAA0B,EAC1B,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAElB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,2BAA2B,EAC3B,uBAAuB,GAExB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EAMrB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,iCAAiC,EACjC,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,EACtB,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EACL,yCAAyC,EACzC,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,EAEzB,QAAQ,GAET,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,118 @@
1
+ import { LEGAL_DOCUMENT_VERSIONS } from "./legal.js";
2
+ /**
3
+ * The declared history of changes to the published legal documents.
4
+ *
5
+ * The Terms and the AUP promise every account holder an email at least 30 days
6
+ * before a material change takes effect, and a free way out during that window.
7
+ * This array is the machinery behind that promise: the notifier job, the public
8
+ * updates list and the review-time guard all read it, so a delivered email, a
9
+ * published page and the document's own `**Version:**` header cannot disagree.
10
+ *
11
+ * Materiality is a person's decision recorded in a reviewed commit. Nothing here
12
+ * is derived from a diff, and there is deliberately no heuristic: the only way
13
+ * that judgement can fail is by quietly downgrading a change to someone's rights
14
+ * into a typo fix, and a classifier would fail in exactly that direction.
15
+ *
16
+ * An entry is frozen once its notice has been sent. Editing it would make a
17
+ * delivered email untrue, so the job stores a hash on first send and refuses to
18
+ * send more notices for that change if the hash stops matching.
19
+ */
20
+ /** Why we are permitted to make the change. Annex 1(j) of 93/13 wants this stated. */
21
+ export type LegalChangeReason = "legal_compliance" | "security" | "abuse_prevention" | "service_change" | "provider_change" | "clarification";
22
+ /**
23
+ * How the change binds.
24
+ *
25
+ * `notice_only` is the default and the only tier the platform implements:
26
+ * notice, a free exit, and continued use on or after the effective date is
27
+ * acceptance of the amended terms. The other two exist so that a change needing
28
+ * affirmative agreement has somewhere to live rather than being smuggled through
29
+ * the silence path. `consent_required` in particular covers anything resting on
30
+ * a GDPR consent basis, where silence is categorically invalid, and anything
31
+ * touching price or the subject matter of the contract.
32
+ */
33
+ export type LegalChangeEnforcement = "notice_only" | "acknowledge_required" | "consent_required";
34
+ export type LegalDocumentKey = keyof typeof LEGAL_DOCUMENT_VERSIONS;
35
+ export interface LegalChangeDocument {
36
+ key: LegalDocumentKey;
37
+ /** The version this document carried before the change. */
38
+ from: string;
39
+ /** The version it carries after. Must equal LEGAL_DOCUMENT_VERSIONS[key] for the latest entry. */
40
+ to: string;
41
+ }
42
+ export interface LegalChange {
43
+ /** `YYYY-MM-DD-slug`. The date half is the effective date, so an id reads as a promise. */
44
+ id: string;
45
+ /** The human decision. An editorial change never sends an email. */
46
+ materiality: "material" | "editorial";
47
+ reason: LegalChangeReason;
48
+ enforcement: LegalChangeEnforcement;
49
+ /** ISO instant, UTC. When the notice may start going out. */
50
+ announceAt: string;
51
+ /** ISO instant, UTC. When the new documents govern. */
52
+ effectiveAt: string;
53
+ /** Every document whose version header moves in this change. */
54
+ documents: readonly LegalChangeDocument[];
55
+ /**
56
+ * Authored plain text, both languages, rendered escaped. A Swedish operator
57
+ * publishing Swedish documents may not send an English-only notice about them.
58
+ */
59
+ summary: {
60
+ en: string;
61
+ sv: string;
62
+ };
63
+ /** Set when the same change also modifies the Service itself (2019/770 Art. 19). */
64
+ modifiesService?: boolean;
65
+ }
66
+ /**
67
+ * Newest last. Empty because version 1.0 is the first publication under
68
+ * Kaizensu AB: there is no earlier version to have changed from, so the chain
69
+ * starts here rather than being backfilled with a notice nobody was owed.
70
+ */
71
+ export declare const LEGAL_CHANGES: readonly LegalChange[];
72
+ /**
73
+ * Header carrying the change ids the signup form actually rendered.
74
+ *
75
+ * Same channel as the terms-acceptance header, for the same reason: the signup
76
+ * flow returns no session when email verification is required, so a follow-up
77
+ * authenticated POST would be refused and the evidence lost. Comma-separated,
78
+ * and never trusted on its own — the server intersects it with what it can
79
+ * independently see was disclosable.
80
+ */
81
+ export declare const SIGNUP_DISCLOSURE_HEADER = "x-percher-disclosed-changes";
82
+ /** The notice period the documents promise, in milliseconds. */
83
+ export declare const MATERIAL_CHANGE_NOTICE_MS: number;
84
+ /** `id` shape: the date half is parsed, so this is a gate and not decoration. */
85
+ export declare const LEGAL_CHANGE_ID_PATTERN: RegExp;
86
+ /** Versions are `1.0` or `1.0.1`; an editorial change may only move the last component. */
87
+ export declare const LEGAL_VERSION_PATTERN: RegExp;
88
+ /**
89
+ * The canonical JSON of an entry, for the hash that freezes it after the first
90
+ * send. Keys are emitted in a fixed order rather than insertion order so that
91
+ * reformatting the source cannot invalidate a change already announced.
92
+ */
93
+ export declare function canonicalLegalChangeJson(change: LegalChange): string;
94
+ /** Entries whose notice may go out now: material, announced, newest last. */
95
+ export declare function openMaterialChanges(now: number, changes?: readonly LegalChange[]): LegalChange[];
96
+ /**
97
+ * The latest declared version of a document, or its current published version
98
+ * when no change has ever named it.
99
+ */
100
+ export declare function declaredVersion(key: LegalDocumentKey, changes?: readonly LegalChange[]): string;
101
+ /**
102
+ * Order two `1.0` / `1.0.1` strings. Unparseable input sorts as older, so a
103
+ * version we cannot read never suppresses a notice.
104
+ */
105
+ export declare function compareVersions(left: string, right: string): number;
106
+ /**
107
+ * The change a user has not yet acknowledged, if any.
108
+ *
109
+ * Read by the dashboard banner and by the operator dry-run. Deliberately NOT
110
+ * read by the API's auth middleware: a stale accepted version is never an
111
+ * authorization failure. Extending the 403 to a version comparison would break
112
+ * every customer's deploy pipeline and every agent integration on a chosen
113
+ * date, with the remedy being a browser click a CI runner cannot perform — and
114
+ * it would be stricter than the Terms promise, which make continued use the
115
+ * acceptance. A test in the API asserts that absence.
116
+ */
117
+ export declare function pendingAcknowledgement(acceptedTermsVersion: string | null | undefined, now: number, changes?: readonly LegalChange[]): LegalChange | null;
118
+ //# sourceMappingURL=legal-changes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legal-changes.d.ts","sourceRoot":"","sources":["../src/legal-changes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;;;;;;;;;;;;GAiBG;AAEH,sFAAsF;AACtF,MAAM,MAAM,iBAAiB,GACzB,kBAAkB,GAClB,UAAU,GACV,kBAAkB,GAClB,gBAAgB,GAChB,iBAAiB,GACjB,eAAe,CAAC;AAEpB;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,GAAG,aAAa,GAAG,sBAAsB,GAAG,kBAAkB,CAAC;AAEjG,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,uBAAuB,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,gBAAgB,CAAC;IACtB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,kGAAkG;IAClG,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAW;IAC1B,2FAA2F;IAC3F,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,WAAW,EAAE,UAAU,GAAG,WAAW,CAAC;IACtC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,WAAW,EAAE,sBAAsB,CAAC;IACpC,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,SAAS,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC1C;;;OAGG;IACH,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,oFAAoF;IACpF,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAAO,CAAC;AAExD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AAEtE,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,QAA2B,CAAC;AAElE,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,QAAiD,CAAC;AAEtF,2FAA2F;AAC3F,eAAO,MAAM,qBAAqB,QAAyB,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAgBpE;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,SAAS,WAAW,EAAkB,GAC9C,WAAW,EAAE,CAIf;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,gBAAgB,EACrB,OAAO,GAAE,SAAS,WAAW,EAAkB,GAC9C,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA6BnE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,SAAS,WAAW,EAAkB,GAC9C,WAAW,GAAG,IAAI,CAiBpB"}