@onesub/server 0.26.0 → 0.27.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/dist/index.js CHANGED
@@ -167,6 +167,8 @@ var APPLE_ROOT_CA_PEMS = [APPLE_ROOT_CA_G3_PEM];
167
167
  var LOG_CONTINUATION = "\n | ";
168
168
  var BARE_VALUE = /^[\w.:/@+-]+$/;
169
169
  var LINE_TERMINATORS = /\r\n|\r|\n|\u2028|\u2029/;
170
+ var MAX_VALUE_CHARS = 512;
171
+ var PLAY_TOKEN_IN_URL = /\/tokens\/[^/:"'\s\\]+/g;
170
172
  var MAX_CAUSE_DEPTH = 2;
171
173
  var MAX_AGGREGATE_ERRORS = 3;
172
174
  function esc(value) {
@@ -175,6 +177,13 @@ function esc(value) {
175
177
  function escQuoted(value) {
176
178
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
177
179
  }
180
+ function clamp(value) {
181
+ if (value.length <= MAX_VALUE_CHARS) return value;
182
+ return `${value.slice(0, MAX_VALUE_CHARS)}\u2026+${value.length - MAX_VALUE_CHARS} more`;
183
+ }
184
+ function sanitize(value) {
185
+ return clamp(value.replace(PLAY_TOKEN_IN_URL, "/tokens/[redacted]"));
186
+ }
178
187
  function renderValue(value) {
179
188
  if (value === void 0) return void 0;
180
189
  if (value === null) return "null";
@@ -184,17 +193,19 @@ function renderValue(value) {
184
193
  return String(value);
185
194
  case "bigint":
186
195
  return `${value}n`;
187
- case "string":
188
- return BARE_VALUE.test(value) ? value : `"${escQuoted(value)}"`;
196
+ case "string": {
197
+ const safe = sanitize(value);
198
+ return BARE_VALUE.test(safe) ? safe : `"${escQuoted(safe)}"`;
199
+ }
189
200
  case "object":
190
201
  try {
191
- return `"${escQuoted(JSON.stringify(value) ?? "null")}"`;
202
+ return `"${escQuoted(sanitize(JSON.stringify(value) ?? "null"))}"`;
192
203
  } catch {
193
204
  return '"[unserialisable]"';
194
205
  }
195
206
  default:
196
207
  try {
197
- return `"${escQuoted(String(value))}"`;
208
+ return `"${escQuoted(sanitize(String(value)))}"`;
198
209
  } catch {
199
210
  return '"[unrenderable]"';
200
211
  }
@@ -1951,15 +1962,26 @@ var GOOGLE_FAILURE_MESSAGES = {
1951
1962
  function servesGoogle(config) {
1952
1963
  return getAppRegistry(config).apps.some((app) => !!app.google);
1953
1964
  }
1965
+ function googleWebhookIsAuthenticated(config) {
1966
+ return getAppRegistry(config).apps.some((a) => a.google?.pushAudience);
1967
+ }
1968
+ function googleWebhookAllowsUnauthenticated(config) {
1969
+ return getAppRegistry(config).apps.some((a) => a.google?.allowUnauthenticatedWebhook);
1970
+ }
1954
1971
  function warnIfGoogleWebhookOpen(config) {
1955
1972
  if (process.env["NODE_ENV"] !== "production") return;
1956
1973
  if (!servesGoogle(config)) return;
1957
1974
  const apps = getAppRegistry(config).apps;
1958
- const googleApps = apps.map((a) => a.google).filter((g) => !!g);
1959
- if (!googleApps.some((g) => g.pushAudience)) {
1960
- log.warn(
1961
- "[onesub] SECURITY: POST /onesub/webhook/google accepts unauthenticated requests \u2014 no configured app sets google.pushAudience, so the Pub/Sub OIDC token is never verified. A caller who knows a purchaseToken or orderId can cancel a subscription or delete a one-time purchase. Set google.pushAudience (and google.pushServiceAccountEmail)."
1962
- );
1975
+ if (!googleWebhookIsAuthenticated(config)) {
1976
+ if (googleWebhookAllowsUnauthenticated(config)) {
1977
+ log.warn(
1978
+ "[onesub] SECURITY: POST /onesub/webhook/google runs unauthenticated by explicit opt-in \u2014 google.allowUnauthenticatedWebhook is set and no app sets google.pushAudience. Anything that can reach this endpoint can cancel a subscription or delete a one-time purchase, so the request must already be authenticated in front of this server."
1979
+ );
1980
+ } else {
1981
+ log.warn(
1982
+ "[onesub] POST /onesub/webhook/google will reject every request with 401 \u2014 no configured app sets google.pushAudience, so the Pub/Sub OIDC token cannot be verified. Set google.pushAudience (and google.pushServiceAccountEmail), or set google.allowUnauthenticatedWebhook when something in front of this server already authenticates the request."
1983
+ );
1984
+ }
1963
1985
  }
1964
1986
  if (!apps.some((a) => a.google?.packageName)) {
1965
1987
  log.warn(
@@ -2088,7 +2110,12 @@ async function processGoogleNotification(work, config, store, purchaseStore) {
2088
2110
  }
2089
2111
  async function handleGoogleWebhook(req, res, config, store, purchaseStore, webhookEventStore, webhookQueue) {
2090
2112
  const pushIdentities = getAppRegistry(config).apps.map((app) => app.google).filter((g) => !!g?.pushAudience);
2091
- if (pushIdentities.length > 0) {
2113
+ if (pushIdentities.length === 0) {
2114
+ if (process.env["NODE_ENV"] === "production" && !googleWebhookAllowsUnauthenticated(config)) {
2115
+ sendError(res, 401, ONESUB_ERROR_CODE.UNAUTHORIZED, "Unauthorized");
2116
+ return;
2117
+ }
2118
+ } else {
2092
2119
  let authenticated = false;
2093
2120
  for (const google of pushIdentities) {
2094
2121
  if (await verifyGooglePushToken(req, google.pushAudience, google.pushServiceAccountEmail)) {