@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.
@@ -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("error");
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("error");
2771
+ setState("server_error");
2770
2772
  }
2771
2773
  } catch {
2772
- setState("error");
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 === "error" || state === "rate_limited") setState("idle");
2825
+ if (state === "invalid_format" || state === "server_error" || state === "rate_limited")
2826
+ setState("idle");
2824
2827
  },
2825
2828
  placeholder,
2826
- "aria-invalid": state === "error",
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 === "error" ? "border-[rgba(237,106,106,0.5)]" : "border-[rgba(214,213,201,0.08)]"
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 === "error" || state === "rate_limited") && "text-[rgb(237,106,106)]",
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 === "error" || state === "rate_limited" ? "alert" : void 0,
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 === "error" && "Please enter a valid email address.",
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
  }