@bounded-sh/server 0.0.56 → 0.0.58

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
@@ -810,17 +810,23 @@ class EvmKeypairProvider {
810
810
  }
811
811
  }
812
812
 
813
- var __rest = (undefined && undefined.__rest) || function (s, e) {
814
- var t = {};
815
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
816
- t[p] = s[p];
817
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
818
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
819
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
820
- t[p[i]] = s[p[i]];
821
- }
822
- return t;
823
- };
813
+ // #023: strip caller-supplied auth headers so an operation runs as the isolated wallet, not
814
+ // as whatever headers were forwarded in. HTTP header names are case-insensitive on the wire,
815
+ // so a fixed `Authorization`/`authorization` destructure let a mixed-case `AUTHORIZATION`
816
+ // survive and ride along. Drop every header whose lowercased name is authorization (plus
817
+ // proxy-authorization / cookie, which would likewise leak a caller identity into the session).
818
+ const STRIP_HEADER_NAMES = new Set(["authorization", "proxy-authorization", "cookie"]);
819
+ function stripCallerAuthHeaders(headers) {
820
+ if (!headers)
821
+ return undefined;
822
+ const rest = {};
823
+ for (const [name, value] of Object.entries(headers)) {
824
+ if (!STRIP_HEADER_NAMES.has(name.toLowerCase()))
825
+ rest[name] = value;
826
+ }
827
+ return Object.keys(rest).length > 0 ? rest : undefined;
828
+ }
829
+
824
830
  /* ------------------------------------------------------------------ */
825
831
  /* Keypair parsing */
826
832
  /* ------------------------------------------------------------------ */
@@ -868,7 +874,11 @@ class WalletClient {
868
874
  this.evmTransactionPolicy = evmTransactionPolicy;
869
875
  this.live = {
870
876
  intent: (roomPath, intent, opts = {}) => core.live.intent(roomPath, intent, this.mergeLiveIntentOptions(opts)),
871
- status: (roomPath, opts = {}) => core.live.status(roomPath, Object.assign(Object.assign({}, opts), { headers: this.sanitizeHeaders(opts.headers) })),
877
+ // Route status through the SAME auth merge intent uses so a server-keypair
878
+ // agent's session token rides the request. Without it the agent sends the
879
+ // public app id only and the server returns the slim anonymous shape rather
880
+ // than the full diagnostics the authenticated agent is entitled to (#102).
881
+ status: (roomPath, opts = {}) => core.live.status(roomPath, this.mergeLiveStatusOptions(opts)),
872
882
  // Agent-as-player view stream: subscribe to THIS wallet's per-player view
873
883
  // under the wallet's own session (per-subscription overrides — no global
874
884
  // auth needed). The view id defaults to this wallet's @user.id (== its
@@ -908,12 +918,9 @@ class WalletClient {
908
918
  this.sessionManager.clearSession();
909
919
  }, _walletAddress: this.address }, (this.evmTransactionPolicy ? { _evmTransactionPolicy: this.evmTransactionPolicy } : {}));
910
920
  }
911
- /** Strip Authorization from caller headers to prevent overriding wallet token */
921
+ /** #023: strip caller Authorization (any casing) so operations run as the isolated wallet. */
912
922
  sanitizeHeaders(headers) {
913
- if (!headers)
914
- return undefined;
915
- const _a = headers, { Authorization, authorization } = _a, rest = __rest(_a, ["Authorization", "authorization"]);
916
- return Object.keys(rest).length > 0 ? rest : undefined;
923
+ return stripCallerAuthHeaders(headers);
917
924
  }
918
925
  mergeSetOverrides(opts) {
919
926
  var _a;
@@ -930,6 +937,12 @@ class WalletClient {
930
937
  const merged = Object.assign(Object.assign({}, opts), { headers: this.sanitizeHeaders(opts === null || opts === void 0 ? void 0 : opts.headers), _overrides: Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts._overrides), { headers: this.sanitizeHeaders((_a = opts === null || opts === void 0 ? void 0 : opts._overrides) === null || _a === void 0 ? void 0 : _a.headers), _getAuthHeaders: ovr._getAuthHeaders, _clearAuth: ovr._clearAuth, _walletAddress: ovr._walletAddress }) });
931
938
  return merged;
932
939
  }
940
+ mergeLiveStatusOptions(opts) {
941
+ var _a;
942
+ const ovr = this.buildOverrides();
943
+ const merged = Object.assign(Object.assign({}, opts), { headers: this.sanitizeHeaders(opts === null || opts === void 0 ? void 0 : opts.headers), _overrides: Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts._overrides), { headers: this.sanitizeHeaders((_a = opts === null || opts === void 0 ? void 0 : opts._overrides) === null || _a === void 0 ? void 0 : _a.headers), _getAuthHeaders: ovr._getAuthHeaders, _clearAuth: ovr._clearAuth, _walletAddress: ovr._walletAddress }) });
944
+ return merged;
945
+ }
933
946
  /* ---- Data operations ---- */
934
947
  async set(path, document, options) {
935
948
  return core.set(path, document, this.mergeSetOverrides(options));
@@ -1344,8 +1357,29 @@ async function verifyWebhook(rawBody, headers, opts = {}) {
1344
1357
  if (Math.abs(nowSeconds - payload.timestamp) > maxSkew) {
1345
1358
  throw new WebhookVerificationError(`Webhook timestamp outside allowed skew of ${maxSkew}s (delta ${Math.abs(nowSeconds - payload.timestamp)}s).`);
1346
1359
  }
1347
- if (opts.expectedAppId !== undefined && payload.appId !== opts.expectedAppId) {
1348
- throw new WebhookVerificationError(`Webhook appId "${payload.appId}" does not match expected appId "${opts.expectedAppId}".`);
1360
+ // #072: bind the delivery to an app by DEFAULT. The shared platform signing key means a
1361
+ // valid signature proves "from Bounded", not "from your app"; payload.appId is the ONLY
1362
+ // per-app separation. Previously the bind ran only when expectedAppId was passed, so the
1363
+ // secure check was opt-in and a malicious app owner could point their app's webhook at a
1364
+ // victim receiver. Now: an explicit expectedAppId is authoritative; otherwise bind to the
1365
+ // init()-time app id; a multi-app receiver must set allowAnyAppId to accept any app; and
1366
+ // when no app id is resolvable we FAIL CLOSED rather than silently trust any app.
1367
+ if (opts.expectedAppId !== undefined) {
1368
+ if (payload.appId !== opts.expectedAppId) {
1369
+ throw new WebhookVerificationError(`Webhook appId "${payload.appId}" does not match expected appId "${opts.expectedAppId}".`);
1370
+ }
1371
+ }
1372
+ else if (!opts.allowAnyAppId) {
1373
+ const configuredAppId = core.getConfiguredAppId();
1374
+ if (!configuredAppId) {
1375
+ throw new WebhookVerificationError("verifyWebhook cannot determine which app this webhook is for: no expectedAppId was " +
1376
+ "passed and the SDK has no init()-time app id. Pass expectedAppId, or set " +
1377
+ "allowAnyAppId: true to intentionally accept webhooks from any app.");
1378
+ }
1379
+ if (payload.appId !== configuredAppId) {
1380
+ throw new WebhookVerificationError(`Webhook appId "${payload.appId}" does not match the configured app id "${configuredAppId}". ` +
1381
+ "Pass allowAnyAppId: true to accept webhooks from any app.");
1382
+ }
1349
1383
  }
1350
1384
  // Replay cache: reject a previously-seen signed delivery within the skew
1351
1385
  // window. The worker's payload.id is the document id/path, so using it alone