@homie-rocks/studio 0.33.1 → 0.34.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.
package/worker/stripe.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * THE STUDIO'S OWN STRIPE, from its Worker (@homie-rocks/studio 0.24.0). No SDK: a few form-encoded calls with the
3
- * studio's restricted key (a Worker secret), and Stripe's documented webhook signature check with WebCrypto.
3
+ * studio's key (a Worker secret), and Stripe's documented webhook signature check with WebCrypto.
4
4
  *
5
- * createCheckoutSession(env, params) POST /v1/checkout/sessions (Stripe's hosted page; the TV never calls it)
5
+ * createCheckoutSession(env, params) POST /v1/checkout/sessions (Stripe's hosted page)
6
6
  * createRefund(env, params) POST /v1/refunds (only ever from the owner's tap, or a confirmed ask)
7
7
  * webhook endpoints list, create and turn off: only `homie-studio shop connect` calls these, on the
8
8
  * owner's computer with the key the owner pasted there (0.24.3), so a new
@@ -18,12 +18,12 @@
18
18
  /** The API version the kit is written against: the first one Managed Payments takes ("2025-03-31.basil or later"). */
19
19
  export const STRIPE_VERSION = '2025-03-31.basil';
20
20
  export const STRIPE_API = 'https://api.stripe.com';
21
- /** The webhook's freshness window, as Stripe's own libraries use: a replayed old event is refused. */
21
+ /** Stripe's replay defense: https://docs.stripe.com/webhooks#preventing-replay-attacks */
22
22
  export const TOLERANCE_S = 300;
23
23
 
24
- /** A restricted key (rk_test_/rk_live_), or a test secret key for local tests. A live full secret key is refused. */
25
- export const KEY_SHAPE = /^(?:rk_(?:test|live)|sk_test)_[A-Za-z0-9]{10,247}$/;
26
- export const WEBHOOK_SECRET_SHAPE = /^whsec_[A-Za-z0-9+/=_-]{16,200}$/;
24
+ /** Stripe key types: https://docs.stripe.com/keys — the studio chooses restricted or secret keys. */
25
+ export const KEY_SHAPE = /^(?:rk|sk)_(?:test|live)_[A-Za-z0-9]+$/;
26
+ export const WEBHOOK_SECRET_SHAPE = /^whsec_[A-Za-z0-9+/=_-]+$/;
27
27
 
28
28
  /** test or live, from the key alone (never by calling Stripe). */
29
29
  export const modeOf = (key) => (/^(?:rk|sk)_live_/.test(String(key ?? '')) ? 'live' : 'test');
@@ -51,14 +51,21 @@ export function formEncode(params, prefix = '', out = new URLSearchParams()) {
51
51
  return out;
52
52
  }
53
53
 
54
+ /** Redact before truncating: provider errors can echo authorization or client secrets. */
55
+ export function redactStripe(value, env = {}) {
56
+ let text = String(value ?? '');
57
+ for (const secret of [env.STRIPE_KEY, env.STRIPE_WEBHOOK_SECRET]) if (secret) text = text.replaceAll(String(secret), '[redacted]');
58
+ return text.replace(/(?:[rs]k_(?:test|live)_[A-Za-z0-9]+|whsec_[A-Za-z0-9+/=_-]+|[A-Za-z0-9_]+_secret_[A-Za-z0-9_-]+)/g, '[redacted]');
59
+ }
60
+
54
61
  export class StripeError extends Error {
55
62
  constructor(status, body) {
56
63
  const e = body?.error ?? {};
57
- super(e.message ? String(e.message).slice(0, 300) : `Stripe answered ${status}`);
64
+ super(e.message ? redactStripe(e.message).slice(0, 300) : `Stripe answered ${status}`);
58
65
  this.status = status;
59
- this.code = e.code ?? e.type ?? 'stripe';
60
- this.type = e.type ?? null;
61
- this.param = typeof e.param === 'string' ? e.param.slice(0, 120) : null;
66
+ this.code = redactStripe(e.code ?? e.type ?? 'stripe');
67
+ this.type = e.type ? redactStripe(e.type) : null;
68
+ this.param = typeof e.param === 'string' ? redactStripe(e.param).slice(0, 120) : null;
62
69
  }
63
70
  }
64
71
 
@@ -91,7 +98,8 @@ export async function stripeCall(env, method, path, params = null, { idempotency
91
98
  if (!KEY_SHAPE.test(key)) throw new StripeError(0, { error: { message: 'no Stripe key on this Worker', code: 'no-key' } });
92
99
  const body = params && method !== 'GET' ? formEncode(params).toString() : null;
93
100
  const query = params && method === 'GET' ? `?${formEncode(params).toString()}` : '';
94
- const res = await fetcher(`${apiBase(env)}${path}${query}`, {
101
+ let res;
102
+ try { res = await fetcher(`${apiBase(env)}${path}${query}`, {
95
103
  method,
96
104
  headers: {
97
105
  authorization: `Bearer ${key}`,
@@ -102,9 +110,10 @@ export async function stripeCall(env, method, path, params = null, { idempotency
102
110
  ...(body !== null ? { body } : {}),
103
111
  signal: AbortSignal.timeout(timeout),
104
112
  });
113
+ } catch (error) { throw new StripeError(502, { error: { message: redactStripe(error?.message ?? 'Stripe did not answer', env), code: 'network' } }); }
105
114
  let json = null;
106
115
  try { json = await res.json(); } catch { json = null; }
107
- if (!res.ok) throw new StripeError(res.status, json);
116
+ if (!res.ok) throw new StripeError(res.status, JSON.parse(redactStripe(JSON.stringify(json), env)));
108
117
  return json;
109
118
  }
110
119
 
@@ -1,2 +1,2 @@
1
1
  /** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
2
- export const STUDIO_VERSION_TAG = '0.33.1';
2
+ export const STUDIO_VERSION_TAG = '0.34.0';