@braintwopoint0/playback-commons 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.ts +34 -0
- package/dist/api/index.js +26 -0
- package/dist/api/index.js.map +1 -0
- package/dist/ui/index.js +13 -9
- package/dist/ui/index.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type CorsOptions = {
|
|
2
|
+
/** Comma-separated method list. Default: 'POST, OPTIONS'. */
|
|
3
|
+
methods?: string;
|
|
4
|
+
/** Comma-separated header allowlist. Default: 'Content-Type'. */
|
|
5
|
+
headers?: string;
|
|
6
|
+
/** Preflight cache duration in seconds. Default: 86400 (24h). */
|
|
7
|
+
maxAge?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Build CORS response headers.
|
|
11
|
+
*
|
|
12
|
+
* Always returns `Vary: Origin` so caches don't serve a CORS-allowed response
|
|
13
|
+
* to a different origin. Only emits `Access-Control-*` headers when the request
|
|
14
|
+
* Origin is in the allowlist — exact-match, no wildcards, so a typo'd subdomain
|
|
15
|
+
* or rogue preview URL can't post into a production endpoint.
|
|
16
|
+
*
|
|
17
|
+
* Same-origin requests have no Origin header on POST in some browsers and never
|
|
18
|
+
* preflight, so an absent or unknown origin returning the empty CORS set is the
|
|
19
|
+
* correct behavior.
|
|
20
|
+
*/
|
|
21
|
+
declare function corsHeaders(origin: string | null, allowed: ReadonlySet<string>, opts?: CorsOptions): Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Build a preflight (OPTIONS) Response. Use directly as the route's OPTIONS export:
|
|
24
|
+
*
|
|
25
|
+
* export const OPTIONS = (req: NextRequest) => corsPreflight(req, ALLOWED_ORIGINS)
|
|
26
|
+
*
|
|
27
|
+
* Returns 204 No Content with the CORS headers built from the request's Origin
|
|
28
|
+
* against the allowlist. If the origin isn't allowed, the response still returns
|
|
29
|
+
* 204 but without the `Access-Control-Allow-*` headers — which is what the browser
|
|
30
|
+
* needs to see in order to block the subsequent real request.
|
|
31
|
+
*/
|
|
32
|
+
declare function corsPreflight(req: Request, allowed: ReadonlySet<string>, opts?: CorsOptions): Response;
|
|
33
|
+
|
|
34
|
+
export { type CorsOptions, corsHeaders, corsPreflight };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/api/index.ts
|
|
2
|
+
var DEFAULT_METHODS = "POST, OPTIONS";
|
|
3
|
+
var DEFAULT_HEADERS = "Content-Type";
|
|
4
|
+
var DEFAULT_MAX_AGE = 86400;
|
|
5
|
+
function corsHeaders(origin, allowed, opts = {}) {
|
|
6
|
+
const headers = { Vary: "Origin" };
|
|
7
|
+
if (origin && allowed.has(origin)) {
|
|
8
|
+
headers["Access-Control-Allow-Origin"] = origin;
|
|
9
|
+
headers["Access-Control-Allow-Methods"] = opts.methods ?? DEFAULT_METHODS;
|
|
10
|
+
headers["Access-Control-Allow-Headers"] = opts.headers ?? DEFAULT_HEADERS;
|
|
11
|
+
headers["Access-Control-Max-Age"] = String(opts.maxAge ?? DEFAULT_MAX_AGE);
|
|
12
|
+
}
|
|
13
|
+
return headers;
|
|
14
|
+
}
|
|
15
|
+
function corsPreflight(req, allowed, opts) {
|
|
16
|
+
const origin = req.headers.get("origin");
|
|
17
|
+
return new Response(null, {
|
|
18
|
+
status: 204,
|
|
19
|
+
headers: corsHeaders(origin, allowed, opts)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
corsHeaders,
|
|
24
|
+
corsPreflight
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/index.ts"],"sourcesContent":["// CORS primitives for cross-origin API routes between PLAYBACK / PLAYHUB / future\n// PLAYBACK-suite apps.\n//\n// The mechanism (header construction, preflight response) lives here. The policy\n// — i.e. which origins are allowed to call a given endpoint — is intentionally\n// per-route, since \"who can hit my newsletter endpoint\" is a different decision\n// from \"who can hit my admin endpoint\".\n\nexport type CorsOptions = {\n /** Comma-separated method list. Default: 'POST, OPTIONS'. */\n methods?: string\n /** Comma-separated header allowlist. Default: 'Content-Type'. */\n headers?: string\n /** Preflight cache duration in seconds. Default: 86400 (24h). */\n maxAge?: number\n}\n\nconst DEFAULT_METHODS = 'POST, OPTIONS'\nconst DEFAULT_HEADERS = 'Content-Type'\nconst DEFAULT_MAX_AGE = 86400\n\n/**\n * Build CORS response headers.\n *\n * Always returns `Vary: Origin` so caches don't serve a CORS-allowed response\n * to a different origin. Only emits `Access-Control-*` headers when the request\n * Origin is in the allowlist — exact-match, no wildcards, so a typo'd subdomain\n * or rogue preview URL can't post into a production endpoint.\n *\n * Same-origin requests have no Origin header on POST in some browsers and never\n * preflight, so an absent or unknown origin returning the empty CORS set is the\n * correct behavior.\n */\nexport function corsHeaders(\n origin: string | null,\n allowed: ReadonlySet<string>,\n opts: CorsOptions = {}\n): Record<string, string> {\n const headers: Record<string, string> = { Vary: 'Origin' }\n if (origin && allowed.has(origin)) {\n headers['Access-Control-Allow-Origin'] = origin\n headers['Access-Control-Allow-Methods'] = opts.methods ?? DEFAULT_METHODS\n headers['Access-Control-Allow-Headers'] = opts.headers ?? DEFAULT_HEADERS\n headers['Access-Control-Max-Age'] = String(opts.maxAge ?? DEFAULT_MAX_AGE)\n }\n return headers\n}\n\n/**\n * Build a preflight (OPTIONS) Response. Use directly as the route's OPTIONS export:\n *\n * export const OPTIONS = (req: NextRequest) => corsPreflight(req, ALLOWED_ORIGINS)\n *\n * Returns 204 No Content with the CORS headers built from the request's Origin\n * against the allowlist. If the origin isn't allowed, the response still returns\n * 204 but without the `Access-Control-Allow-*` headers — which is what the browser\n * needs to see in order to block the subsequent real request.\n */\nexport function corsPreflight(\n req: Request,\n allowed: ReadonlySet<string>,\n opts?: CorsOptions\n): Response {\n const origin = req.headers.get('origin')\n return new Response(null, {\n status: 204,\n headers: corsHeaders(origin, allowed, opts),\n })\n}\n"],"mappings":";AAiBA,IAAM,kBAAkB;AACxB,IAAM,kBAAkB;AACxB,IAAM,kBAAkB;AAcjB,SAAS,YACd,QACA,SACA,OAAoB,CAAC,GACG;AACxB,QAAM,UAAkC,EAAE,MAAM,SAAS;AACzD,MAAI,UAAU,QAAQ,IAAI,MAAM,GAAG;AACjC,YAAQ,6BAA6B,IAAI;AACzC,YAAQ,8BAA8B,IAAI,KAAK,WAAW;AAC1D,YAAQ,8BAA8B,IAAI,KAAK,WAAW;AAC1D,YAAQ,wBAAwB,IAAI,OAAO,KAAK,UAAU,eAAe;AAAA,EAC3E;AACA,SAAO;AACT;AAYO,SAAS,cACd,KACA,SACA,MACU;AACV,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ;AACvC,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ;AAAA,IACR,SAAS,YAAY,QAAQ,SAAS,IAAI;AAAA,EAC5C,CAAC;AACH;","names":[]}
|
package/dist/ui/index.js
CHANGED
|
@@ -2744,7 +2744,7 @@ function NewsletterForm2({
|
|
|
2744
2744
|
const trimmed = email.trim();
|
|
2745
2745
|
const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed);
|
|
2746
2746
|
if (!ok) {
|
|
2747
|
-
setState("
|
|
2747
|
+
setState("invalid_format");
|
|
2748
2748
|
return;
|
|
2749
2749
|
}
|
|
2750
2750
|
inFlight.current = true;
|
|
@@ -2765,11 +2765,13 @@ function NewsletterForm2({
|
|
|
2765
2765
|
setEmail("");
|
|
2766
2766
|
} else if (res.status === 429) {
|
|
2767
2767
|
setState("rate_limited");
|
|
2768
|
+
} else if (res.status === 400) {
|
|
2769
|
+
setState("invalid_format");
|
|
2768
2770
|
} else {
|
|
2769
|
-
setState("
|
|
2771
|
+
setState("server_error");
|
|
2770
2772
|
}
|
|
2771
2773
|
} catch {
|
|
2772
|
-
setState("
|
|
2774
|
+
setState("server_error");
|
|
2773
2775
|
} finally {
|
|
2774
2776
|
inFlight.current = false;
|
|
2775
2777
|
}
|
|
@@ -2820,16 +2822,17 @@ function NewsletterForm2({
|
|
|
2820
2822
|
value: email,
|
|
2821
2823
|
onChange: (e) => {
|
|
2822
2824
|
setEmail(e.target.value);
|
|
2823
|
-
if (state === "
|
|
2825
|
+
if (state === "invalid_format" || state === "server_error" || state === "rate_limited")
|
|
2826
|
+
setState("idle");
|
|
2824
2827
|
},
|
|
2825
2828
|
placeholder,
|
|
2826
|
-
"aria-invalid": state === "
|
|
2829
|
+
"aria-invalid": state === "invalid_format",
|
|
2827
2830
|
"aria-describedby": feedbackId,
|
|
2828
2831
|
className: cn(
|
|
2829
2832
|
"w-full sm:flex-1 h-12 sm:h-11 rounded-full bg-[var(--surface-1,#0f1512)] border px-5 text-[16px] sm:text-[14px] text-[var(--timberwolf)] placeholder:text-[rgba(214,213,201,0.44)]",
|
|
2830
2833
|
"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--timberwolf)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--night)]",
|
|
2831
2834
|
"disabled:opacity-60",
|
|
2832
|
-
state === "
|
|
2835
|
+
state === "invalid_format" || state === "server_error" ? "border-[rgba(237,106,106,0.5)]" : "border-[rgba(214,213,201,0.08)]"
|
|
2833
2836
|
)
|
|
2834
2837
|
}
|
|
2835
2838
|
),
|
|
@@ -2851,13 +2854,14 @@ function NewsletterForm2({
|
|
|
2851
2854
|
className: cn(
|
|
2852
2855
|
"mt-2 -ml-0.5 text-[13px] min-h-[1.25rem]",
|
|
2853
2856
|
state === "sent" && "text-[var(--timberwolf)]",
|
|
2854
|
-
(state === "
|
|
2857
|
+
(state === "invalid_format" || state === "server_error" || state === "rate_limited") && "text-[rgb(237,106,106)]",
|
|
2855
2858
|
(state === "idle" || state === "sending") && "text-[rgba(214,213,201,0.44)]"
|
|
2856
2859
|
),
|
|
2857
|
-
role: state === "
|
|
2860
|
+
role: state === "invalid_format" || state === "server_error" || state === "rate_limited" ? "alert" : void 0,
|
|
2858
2861
|
children: [
|
|
2859
2862
|
state === "sent" && "Thanks \u2014 we\u2019ll be in touch.",
|
|
2860
|
-
state === "
|
|
2863
|
+
state === "invalid_format" && "Please enter a valid email address.",
|
|
2864
|
+
state === "server_error" && "Could not subscribe right now. Please try again in a moment.",
|
|
2861
2865
|
state === "rate_limited" && "Too many attempts. Try again in a minute."
|
|
2862
2866
|
]
|
|
2863
2867
|
}
|