@bounded-sh/core 0.0.35 → 0.0.37

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.mjs CHANGED
@@ -3838,6 +3838,126 @@ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = fals
3838
3838
  await getActiveSessionManager().updateIdTokenAndAccessToken(idToken, accessToken, refreshToken);
3839
3839
  }
3840
3840
 
3841
+ // ---------------------------------------------------------------------------
3842
+ // decline.ts -- the structured "declined write" surface.
3843
+ //
3844
+ // When a data-plane write violates a proven policy rule or invariant, the
3845
+ // realtime backend rejects it: HTTP 403 { verdict:'declined', reason:'rule' }
3846
+ // or 409 { verdict:'declined', reason:'invariant', invariant } on the request
3847
+ // path, or a coded error frame over the room socket. This module turns those
3848
+ // rejections into a first-class, share-worthy decline:
3849
+ //
3850
+ // - `BoundedDeclineError` -- the thrown error. Keeps every legacy field apps
3851
+ // already catch (code/status/statusCode/requestId/...), and adds `.decline`
3852
+ // (the structured object below) plus a stable `.isBoundedDecline` flag.
3853
+ // - `deriveDecline(body, status)` -- normalizes a server rejection body into a
3854
+ // `BoundedDecline`, trusting a backend-sent `decline` object when present
3855
+ // and synthesizing a minimal one from legacy fields otherwise. Returns
3856
+ // `null` when the rejection is not a policy/invariant decline.
3857
+ // - `dispatchDeclineEvent(decline, appId?)` -- in a browser, fires the
3858
+ // `bounded:decline` CustomEvent the hosted widget listens to. Fire-and-
3859
+ // forget: guarded for non-browser/SSR, never throws, never delays the throw.
3860
+ // ---------------------------------------------------------------------------
3861
+ const RULE_DEFAULT_MESSAGE = 'This write is not allowed by policy.';
3862
+ const INVARIANT_DEFAULT_MESSAGE = 'This write would violate a proven invariant.';
3863
+ /** The window event the hosted widget listens to for rendering a decline card. */
3864
+ const DECLINE_EVENT_NAME = 'bounded:decline';
3865
+ /**
3866
+ * Normalize a server rejection body into a structured `BoundedDecline`, or
3867
+ * `null` when the rejection is not a policy/invariant decline.
3868
+ *
3869
+ * Prefers a backend-sent `decline` object verbatim; otherwise synthesizes a
3870
+ * minimal `{ verdict, reason, invariant?, message }` from the legacy envelope
3871
+ * (`verdict` / `reason` / `code` / `invariant`). Only strong signals count —
3872
+ * a bare 403/409 with no policy marker is left alone (returns `null`).
3873
+ */
3874
+ function deriveDecline(body, status) {
3875
+ if (!body || typeof body !== 'object')
3876
+ return null;
3877
+ // Trust a backend-sent structured decline verbatim.
3878
+ const provided = body.decline;
3879
+ if (provided && typeof provided === 'object' && provided.verdict === 'declined') {
3880
+ return provided;
3881
+ }
3882
+ const rawReason = body.reason;
3883
+ const code = body.code;
3884
+ const hasInvariant = body.invariant != null;
3885
+ let reason = rawReason === 'rule' || rawReason === 'invariant' ? rawReason : undefined;
3886
+ if (!reason) {
3887
+ if (code === 'invariant_violation' || hasInvariant)
3888
+ reason = 'invariant';
3889
+ else if (code === 'policy_denied')
3890
+ reason = 'rule';
3891
+ }
3892
+ const isDecline = body.verdict === 'declined' || reason === 'rule' || reason === 'invariant';
3893
+ if (!isDecline)
3894
+ return null;
3895
+ const resolvedReason = reason !== null && reason !== void 0 ? reason : (status === 409 ? 'invariant' : 'rule');
3896
+ const rawMessage = body.message;
3897
+ const message = typeof rawMessage === 'string' && rawMessage.length > 0
3898
+ ? rawMessage
3899
+ : resolvedReason === 'invariant'
3900
+ ? INVARIANT_DEFAULT_MESSAGE
3901
+ : RULE_DEFAULT_MESSAGE;
3902
+ const decline = { verdict: 'declined', reason: resolvedReason, message };
3903
+ if (hasInvariant)
3904
+ decline.invariant = body.invariant;
3905
+ if (typeof body.collection === 'string')
3906
+ decline.collection = body.collection;
3907
+ if (typeof body.op === 'string')
3908
+ decline.op = body.op;
3909
+ if (body.boundary && typeof body.boundary === 'object')
3910
+ decline.boundary = body.boundary;
3911
+ if (typeof body.provenAtDeploy === 'boolean')
3912
+ decline.provenAtDeploy = body.provenAtDeploy;
3913
+ return decline;
3914
+ }
3915
+ /**
3916
+ * In a browser, dispatch a `bounded:decline` CustomEvent for the surfaced
3917
+ * decline. Fire-and-forget: guarded for non-browser/SSR (no `window`/
3918
+ * `CustomEvent`), never throws, and never delays the rejection it accompanies.
3919
+ */
3920
+ function dispatchDeclineEvent(decline, appId) {
3921
+ try {
3922
+ if (typeof window === 'undefined')
3923
+ return;
3924
+ const Ctor = globalThis.CustomEvent;
3925
+ if (typeof Ctor !== 'function')
3926
+ return;
3927
+ const detail = Object.assign({}, decline);
3928
+ if (appId)
3929
+ detail.appId = appId;
3930
+ detail.at = Date.now();
3931
+ window.dispatchEvent(new Ctor(DECLINE_EVENT_NAME, { detail }));
3932
+ }
3933
+ catch (_a) {
3934
+ // Fire-and-forget: a widget/telemetry hiccup must never mask the decline.
3935
+ }
3936
+ }
3937
+ /**
3938
+ * A rejected write that violated a proven policy rule or invariant. Thrown by
3939
+ * the data-plane write paths (set/setMany/update/delete) in place of a bare
3940
+ * Error, and attached (as `.decline`) to live-intent rejections.
3941
+ *
3942
+ * Back-compatible: the full server envelope (code/status/statusCode/requestId/
3943
+ * ...) is copied onto the instance, so existing `catch` handlers keep working;
3944
+ * new code reads `.decline`. Detect it with `err.name === 'BoundedDeclineError'`
3945
+ * or the bundle-copy-safe `err.isBoundedDecline === true`.
3946
+ */
3947
+ class BoundedDeclineError extends Error {
3948
+ constructor(decline, envelope) {
3949
+ super(`Declined: ${decline.message}`);
3950
+ // Copy the legacy envelope FIRST, then re-assert our own fields so a
3951
+ // stray envelope key can never clobber name/message/decline/flag.
3952
+ if (envelope && typeof envelope === 'object')
3953
+ Object.assign(this, envelope);
3954
+ this.name = 'BoundedDeclineError';
3955
+ this.message = `Declined: ${decline.message}`;
3956
+ this.decline = decline;
3957
+ this.isBoundedDecline = true;
3958
+ }
3959
+ }
3960
+
3841
3961
  const apiClient = axios.create();
3842
3962
  axiosRetry(apiClient, {
3843
3963
  retries: 2,
@@ -3893,7 +4013,7 @@ async function refreshAuthSessionOnce(appId, isServer) {
3893
4013
  return refreshPromise;
3894
4014
  }
3895
4015
  async function makeApiRequest(method, urlPath, data, _overrides) {
3896
- var _a, _b, _c, _d, _e, _f, _g, _h;
4016
+ var _a, _b, _c, _d, _e, _f;
3897
4017
  const config = await getConfig();
3898
4018
  async function executeRequest() {
3899
4019
  var _a;
@@ -3944,7 +4064,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3944
4064
  }
3945
4065
  return await executeRequest();
3946
4066
  }
3947
- catch (_j) {
4067
+ catch (_g) {
3948
4068
  throw error;
3949
4069
  }
3950
4070
  }
@@ -3955,17 +4075,34 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3955
4075
  const responseData = typeof error.response.data === 'object'
3956
4076
  ? error.response.data
3957
4077
  : { message: String(error.response.data) };
3958
- const apiError = new Error(responseData.message || error.message || 'Request failed');
4078
+ const httpStatus = (_d = error.response) === null || _d === void 0 ? void 0 : _d.status;
4079
+ // A policy/invariant denial (403 rule / 409 invariant) is surfaced as
4080
+ // a first-class BoundedDeclineError carrying `.decline`; every other
4081
+ // failure stays a bare enriched Error (unchanged behavior).
4082
+ const decline = deriveDecline(responseData, httpStatus);
4083
+ const apiError = decline
4084
+ ? new BoundedDeclineError(decline)
4085
+ : new Error(responseData.message || error.message || 'Request failed');
3959
4086
  Object.assign(apiError, responseData);
3960
- apiError.statusCode = (_d = error.response) === null || _d === void 0 ? void 0 : _d.status;
4087
+ apiError.statusCode = httpStatus;
3961
4088
  // Surface the structured error envelope { code, status, requestId } so
3962
4089
  // callers can branch on the stable category (e.g.
3963
4090
  // `if (e.code === 'invariant_violation')`) without parsing the message.
3964
4091
  // `code` is a stable category ('policy_denied' | 'invariant_violation');
3965
4092
  // `status` is the HTTP status; `requestId` correlates with the decision log.
3966
4093
  apiError.code = (_e = responseData.code) !== null && _e !== void 0 ? _e : apiError.code;
3967
- apiError.status = (_g = (_f = error.response) === null || _f === void 0 ? void 0 : _f.status) !== null && _g !== void 0 ? _g : responseData.status;
3968
- apiError.requestId = (_h = responseData.requestId) !== null && _h !== void 0 ? _h : apiError.requestId;
4094
+ apiError.status = httpStatus !== null && httpStatus !== void 0 ? httpStatus : responseData.status;
4095
+ apiError.requestId = (_f = responseData.requestId) !== null && _f !== void 0 ? _f : apiError.requestId;
4096
+ if (decline) {
4097
+ // Re-assert decline-owned fields after the envelope copy, then
4098
+ // notify the browser widget. Fire-and-forget: never delays or
4099
+ // masks the throw.
4100
+ apiError.decline = decline;
4101
+ apiError.name = 'BoundedDeclineError';
4102
+ apiError.isBoundedDecline = true;
4103
+ apiError.message = `Declined: ${decline.message}`;
4104
+ dispatchDeclineEvent(decline, config.appId);
4105
+ }
3969
4106
  throw apiError;
3970
4107
  }
3971
4108
  throw error;
@@ -6042,6 +6179,24 @@ function handleServerMessage(connection, message) {
6042
6179
  err.requestId = message.requestId;
6043
6180
  if (message.subscriptionId)
6044
6181
  err.subscriptionId = message.subscriptionId;
6182
+ // A policy/invariant denial over the socket carries the same
6183
+ // structured decline as the HTTP path (live-intent acks, WS
6184
+ // writes). Mirror `.decline` and notify the browser widget.
6185
+ const declineBody = message.decline
6186
+ ? { decline: message.decline }
6187
+ : {
6188
+ code: message.code,
6189
+ message: message.message,
6190
+ reason: message.reason,
6191
+ invariant: message.invariant,
6192
+ verdict: message.verdict,
6193
+ };
6194
+ const decline = deriveDecline(declineBody);
6195
+ if (decline) {
6196
+ err.decline = decline;
6197
+ err.isBoundedDecline = true;
6198
+ dispatchDeclineEvent(decline, connection.appId);
6199
+ }
6045
6200
  return err;
6046
6201
  };
6047
6202
  const wsError = buildWsError();
@@ -7109,7 +7264,17 @@ async function intent(roomPath, intent, opts = {}) {
7109
7264
  }
7110
7265
  if (!res.ok) {
7111
7266
  const message = (_g = (_f = body === null || body === void 0 ? void 0 : body.error) !== null && _f !== void 0 ? _f : body === null || body === void 0 ? void 0 : body.message) !== null && _g !== void 0 ? _g : `Live intent failed with HTTP ${res.status}`;
7112
- throw new LiveIntentError(message, res.status, body);
7267
+ const liveError = new LiveIntentError(message, res.status, body);
7268
+ // A policy/invariant denial of a live intent carries the same structured
7269
+ // decline as the data-write path. Mirror `.decline` onto the LiveIntentError
7270
+ // (its name/contract is unchanged) and notify the browser widget.
7271
+ const decline = deriveDecline(body, res.status);
7272
+ if (decline) {
7273
+ liveError.decline = decline;
7274
+ liveError.isBoundedDecline = true;
7275
+ dispatchDeclineEvent(decline, config.appId);
7276
+ }
7277
+ throw liveError;
7113
7278
  }
7114
7279
  return (body !== null && body !== void 0 ? body : { ok: true });
7115
7280
  }
@@ -7263,5 +7428,5 @@ function defineLiveModule(mod) {
7263
7428
  return mod;
7264
7429
  }
7265
7430
 
7266
- export { EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveUserIdentityFromIdToken, docId, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, now, queryAggregate, reconnectWithNewAuth, refreshSession, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toMillis, toSeconds, withEffects };
7431
+ export { BoundedDeclineError, DECLINE_EVENT_NAME, EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveDecline, deriveUserIdentityFromIdToken, dispatchDeclineEvent, docId, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, now, queryAggregate, reconnectWithNewAuth, refreshSession, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toMillis, toSeconds, withEffects };
7267
7432
  //# sourceMappingURL=index.mjs.map