@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.
@@ -0,0 +1,73 @@
1
+ export type DeclineReason = 'rule' | 'invariant';
2
+ /**
3
+ * The specific boundary a declined write crossed, when the backend can name it
4
+ * (e.g. a spend cap: `{ type:'rollingSum', field:'amount', cap:1000,
5
+ * windowSeconds:86400, current:950, attempted:200 }`).
6
+ */
7
+ export interface DeclineBoundary {
8
+ type: string;
9
+ field?: string;
10
+ cap?: number;
11
+ windowSeconds?: number;
12
+ current?: number;
13
+ attempted?: number;
14
+ [key: string]: unknown;
15
+ }
16
+ /**
17
+ * A structured, render-ready explanation of a declined write. `provenAtDeploy`
18
+ * marks that the crossed boundary was proven to hold against every input at
19
+ * deploy — the fact the hosted decline card leans on.
20
+ */
21
+ export interface BoundedDecline {
22
+ verdict: 'declined';
23
+ /** 'invariant' (a proven cap/conservation/isolation boundary) or 'rule' (an auth/policy rule). */
24
+ reason: DeclineReason | string;
25
+ /** Invariant name, present when `reason === 'invariant'`. */
26
+ invariant?: string;
27
+ collection?: string;
28
+ op?: string;
29
+ boundary?: DeclineBoundary;
30
+ /** Human-readable, render-ready explanation. */
31
+ message: string;
32
+ /** True when the crossed boundary was proven to hold at deploy. */
33
+ provenAtDeploy?: boolean;
34
+ [key: string]: unknown;
35
+ }
36
+ /** The window event the hosted widget listens to for rendering a decline card. */
37
+ export declare const DECLINE_EVENT_NAME = "bounded:decline";
38
+ /**
39
+ * Normalize a server rejection body into a structured `BoundedDecline`, or
40
+ * `null` when the rejection is not a policy/invariant decline.
41
+ *
42
+ * Prefers a backend-sent `decline` object verbatim; otherwise synthesizes a
43
+ * minimal `{ verdict, reason, invariant?, message }` from the legacy envelope
44
+ * (`verdict` / `reason` / `code` / `invariant`). Only strong signals count —
45
+ * a bare 403/409 with no policy marker is left alone (returns `null`).
46
+ */
47
+ export declare function deriveDecline(body: any, status?: number): BoundedDecline | null;
48
+ /**
49
+ * In a browser, dispatch a `bounded:decline` CustomEvent for the surfaced
50
+ * decline. Fire-and-forget: guarded for non-browser/SSR (no `window`/
51
+ * `CustomEvent`), never throws, and never delays the rejection it accompanies.
52
+ */
53
+ export declare function dispatchDeclineEvent(decline: BoundedDecline, appId?: string): void;
54
+ /**
55
+ * A rejected write that violated a proven policy rule or invariant. Thrown by
56
+ * the data-plane write paths (set/setMany/update/delete) in place of a bare
57
+ * Error, and attached (as `.decline`) to live-intent rejections.
58
+ *
59
+ * Back-compatible: the full server envelope (code/status/statusCode/requestId/
60
+ * ...) is copied onto the instance, so existing `catch` handlers keep working;
61
+ * new code reads `.decline`. Detect it with `err.name === 'BoundedDeclineError'`
62
+ * or the bundle-copy-safe `err.isBoundedDecline === true`.
63
+ */
64
+ export declare class BoundedDeclineError extends Error {
65
+ /** Stable discriminant that survives multiple bundled SDK copies (no `instanceof`). */
66
+ isBoundedDecline: true;
67
+ decline: BoundedDecline;
68
+ code?: string;
69
+ status?: number;
70
+ statusCode?: number;
71
+ requestId?: string;
72
+ constructor(decline: BoundedDecline, envelope?: Record<string, any>);
73
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export { getConfig, ClientConfig } from './client/config';
3
3
  export { getWebhookKeysUrl } from './client/config';
4
4
  export { get, getMany, docId, set, setMany, setFile, getFiles, search, queryAggregate, runQuery, runQueryMany, runExpression, runExpressionMany, signMessage, signTransaction, signAndSubmitTransaction, count, aggregate, RequestOverrides, SetOptions, GetOptions, SearchOptions, RunQueryOptions, CountOptions, AggregateOptions, AggregateOperation, AggregateResult, AggregateSpec, AggregateRow, QueryAggregateOptions, RunExpressionOptions, RunExpressionResult, InsufficientBalanceError, GetManyResult } from './client/operations';
5
5
  export { subscribe, closeAllSubscriptions, clearCache, getCachedData, reconnectWithNewAuth } from './client/subscription';
6
+ export { BoundedDeclineError, deriveDecline, dispatchDeclineEvent, DECLINE_EVENT_NAME } from './client/decline';
7
+ export type { BoundedDecline, DeclineReason, DeclineBoundary } from './client/decline';
6
8
  export { hasActiveConnection } from './client/subscription-v2';
7
9
  export * from './types';
8
10
  export { increment, serverTimestamp } from './client/field-values';
package/dist/index.js CHANGED
@@ -3858,6 +3858,126 @@ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = fals
3858
3858
  await getActiveSessionManager().updateIdTokenAndAccessToken(idToken, accessToken, refreshToken);
3859
3859
  }
3860
3860
 
3861
+ // ---------------------------------------------------------------------------
3862
+ // decline.ts -- the structured "declined write" surface.
3863
+ //
3864
+ // When a data-plane write violates a proven policy rule or invariant, the
3865
+ // realtime backend rejects it: HTTP 403 { verdict:'declined', reason:'rule' }
3866
+ // or 409 { verdict:'declined', reason:'invariant', invariant } on the request
3867
+ // path, or a coded error frame over the room socket. This module turns those
3868
+ // rejections into a first-class, share-worthy decline:
3869
+ //
3870
+ // - `BoundedDeclineError` -- the thrown error. Keeps every legacy field apps
3871
+ // already catch (code/status/statusCode/requestId/...), and adds `.decline`
3872
+ // (the structured object below) plus a stable `.isBoundedDecline` flag.
3873
+ // - `deriveDecline(body, status)` -- normalizes a server rejection body into a
3874
+ // `BoundedDecline`, trusting a backend-sent `decline` object when present
3875
+ // and synthesizing a minimal one from legacy fields otherwise. Returns
3876
+ // `null` when the rejection is not a policy/invariant decline.
3877
+ // - `dispatchDeclineEvent(decline, appId?)` -- in a browser, fires the
3878
+ // `bounded:decline` CustomEvent the hosted widget listens to. Fire-and-
3879
+ // forget: guarded for non-browser/SSR, never throws, never delays the throw.
3880
+ // ---------------------------------------------------------------------------
3881
+ const RULE_DEFAULT_MESSAGE = 'This write is not allowed by policy.';
3882
+ const INVARIANT_DEFAULT_MESSAGE = 'This write would violate a proven invariant.';
3883
+ /** The window event the hosted widget listens to for rendering a decline card. */
3884
+ const DECLINE_EVENT_NAME = 'bounded:decline';
3885
+ /**
3886
+ * Normalize a server rejection body into a structured `BoundedDecline`, or
3887
+ * `null` when the rejection is not a policy/invariant decline.
3888
+ *
3889
+ * Prefers a backend-sent `decline` object verbatim; otherwise synthesizes a
3890
+ * minimal `{ verdict, reason, invariant?, message }` from the legacy envelope
3891
+ * (`verdict` / `reason` / `code` / `invariant`). Only strong signals count —
3892
+ * a bare 403/409 with no policy marker is left alone (returns `null`).
3893
+ */
3894
+ function deriveDecline(body, status) {
3895
+ if (!body || typeof body !== 'object')
3896
+ return null;
3897
+ // Trust a backend-sent structured decline verbatim.
3898
+ const provided = body.decline;
3899
+ if (provided && typeof provided === 'object' && provided.verdict === 'declined') {
3900
+ return provided;
3901
+ }
3902
+ const rawReason = body.reason;
3903
+ const code = body.code;
3904
+ const hasInvariant = body.invariant != null;
3905
+ let reason = rawReason === 'rule' || rawReason === 'invariant' ? rawReason : undefined;
3906
+ if (!reason) {
3907
+ if (code === 'invariant_violation' || hasInvariant)
3908
+ reason = 'invariant';
3909
+ else if (code === 'policy_denied')
3910
+ reason = 'rule';
3911
+ }
3912
+ const isDecline = body.verdict === 'declined' || reason === 'rule' || reason === 'invariant';
3913
+ if (!isDecline)
3914
+ return null;
3915
+ const resolvedReason = reason !== null && reason !== void 0 ? reason : (status === 409 ? 'invariant' : 'rule');
3916
+ const rawMessage = body.message;
3917
+ const message = typeof rawMessage === 'string' && rawMessage.length > 0
3918
+ ? rawMessage
3919
+ : resolvedReason === 'invariant'
3920
+ ? INVARIANT_DEFAULT_MESSAGE
3921
+ : RULE_DEFAULT_MESSAGE;
3922
+ const decline = { verdict: 'declined', reason: resolvedReason, message };
3923
+ if (hasInvariant)
3924
+ decline.invariant = body.invariant;
3925
+ if (typeof body.collection === 'string')
3926
+ decline.collection = body.collection;
3927
+ if (typeof body.op === 'string')
3928
+ decline.op = body.op;
3929
+ if (body.boundary && typeof body.boundary === 'object')
3930
+ decline.boundary = body.boundary;
3931
+ if (typeof body.provenAtDeploy === 'boolean')
3932
+ decline.provenAtDeploy = body.provenAtDeploy;
3933
+ return decline;
3934
+ }
3935
+ /**
3936
+ * In a browser, dispatch a `bounded:decline` CustomEvent for the surfaced
3937
+ * decline. Fire-and-forget: guarded for non-browser/SSR (no `window`/
3938
+ * `CustomEvent`), never throws, and never delays the rejection it accompanies.
3939
+ */
3940
+ function dispatchDeclineEvent(decline, appId) {
3941
+ try {
3942
+ if (typeof window === 'undefined')
3943
+ return;
3944
+ const Ctor = globalThis.CustomEvent;
3945
+ if (typeof Ctor !== 'function')
3946
+ return;
3947
+ const detail = Object.assign({}, decline);
3948
+ if (appId)
3949
+ detail.appId = appId;
3950
+ detail.at = Date.now();
3951
+ window.dispatchEvent(new Ctor(DECLINE_EVENT_NAME, { detail }));
3952
+ }
3953
+ catch (_a) {
3954
+ // Fire-and-forget: a widget/telemetry hiccup must never mask the decline.
3955
+ }
3956
+ }
3957
+ /**
3958
+ * A rejected write that violated a proven policy rule or invariant. Thrown by
3959
+ * the data-plane write paths (set/setMany/update/delete) in place of a bare
3960
+ * Error, and attached (as `.decline`) to live-intent rejections.
3961
+ *
3962
+ * Back-compatible: the full server envelope (code/status/statusCode/requestId/
3963
+ * ...) is copied onto the instance, so existing `catch` handlers keep working;
3964
+ * new code reads `.decline`. Detect it with `err.name === 'BoundedDeclineError'`
3965
+ * or the bundle-copy-safe `err.isBoundedDecline === true`.
3966
+ */
3967
+ class BoundedDeclineError extends Error {
3968
+ constructor(decline, envelope) {
3969
+ super(`Declined: ${decline.message}`);
3970
+ // Copy the legacy envelope FIRST, then re-assert our own fields so a
3971
+ // stray envelope key can never clobber name/message/decline/flag.
3972
+ if (envelope && typeof envelope === 'object')
3973
+ Object.assign(this, envelope);
3974
+ this.name = 'BoundedDeclineError';
3975
+ this.message = `Declined: ${decline.message}`;
3976
+ this.decline = decline;
3977
+ this.isBoundedDecline = true;
3978
+ }
3979
+ }
3980
+
3861
3981
  const apiClient = axios.create();
3862
3982
  axiosRetry(apiClient, {
3863
3983
  retries: 2,
@@ -3913,7 +4033,7 @@ async function refreshAuthSessionOnce(appId, isServer) {
3913
4033
  return refreshPromise;
3914
4034
  }
3915
4035
  async function makeApiRequest(method, urlPath, data, _overrides) {
3916
- var _a, _b, _c, _d, _e, _f, _g, _h;
4036
+ var _a, _b, _c, _d, _e, _f;
3917
4037
  const config = await getConfig();
3918
4038
  async function executeRequest() {
3919
4039
  var _a;
@@ -3964,7 +4084,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3964
4084
  }
3965
4085
  return await executeRequest();
3966
4086
  }
3967
- catch (_j) {
4087
+ catch (_g) {
3968
4088
  throw error;
3969
4089
  }
3970
4090
  }
@@ -3975,17 +4095,34 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3975
4095
  const responseData = typeof error.response.data === 'object'
3976
4096
  ? error.response.data
3977
4097
  : { message: String(error.response.data) };
3978
- const apiError = new Error(responseData.message || error.message || 'Request failed');
4098
+ const httpStatus = (_d = error.response) === null || _d === void 0 ? void 0 : _d.status;
4099
+ // A policy/invariant denial (403 rule / 409 invariant) is surfaced as
4100
+ // a first-class BoundedDeclineError carrying `.decline`; every other
4101
+ // failure stays a bare enriched Error (unchanged behavior).
4102
+ const decline = deriveDecline(responseData, httpStatus);
4103
+ const apiError = decline
4104
+ ? new BoundedDeclineError(decline)
4105
+ : new Error(responseData.message || error.message || 'Request failed');
3979
4106
  Object.assign(apiError, responseData);
3980
- apiError.statusCode = (_d = error.response) === null || _d === void 0 ? void 0 : _d.status;
4107
+ apiError.statusCode = httpStatus;
3981
4108
  // Surface the structured error envelope { code, status, requestId } so
3982
4109
  // callers can branch on the stable category (e.g.
3983
4110
  // `if (e.code === 'invariant_violation')`) without parsing the message.
3984
4111
  // `code` is a stable category ('policy_denied' | 'invariant_violation');
3985
4112
  // `status` is the HTTP status; `requestId` correlates with the decision log.
3986
4113
  apiError.code = (_e = responseData.code) !== null && _e !== void 0 ? _e : apiError.code;
3987
- apiError.status = (_g = (_f = error.response) === null || _f === void 0 ? void 0 : _f.status) !== null && _g !== void 0 ? _g : responseData.status;
3988
- apiError.requestId = (_h = responseData.requestId) !== null && _h !== void 0 ? _h : apiError.requestId;
4114
+ apiError.status = httpStatus !== null && httpStatus !== void 0 ? httpStatus : responseData.status;
4115
+ apiError.requestId = (_f = responseData.requestId) !== null && _f !== void 0 ? _f : apiError.requestId;
4116
+ if (decline) {
4117
+ // Re-assert decline-owned fields after the envelope copy, then
4118
+ // notify the browser widget. Fire-and-forget: never delays or
4119
+ // masks the throw.
4120
+ apiError.decline = decline;
4121
+ apiError.name = 'BoundedDeclineError';
4122
+ apiError.isBoundedDecline = true;
4123
+ apiError.message = `Declined: ${decline.message}`;
4124
+ dispatchDeclineEvent(decline, config.appId);
4125
+ }
3989
4126
  throw apiError;
3990
4127
  }
3991
4128
  throw error;
@@ -6062,6 +6199,24 @@ function handleServerMessage(connection, message) {
6062
6199
  err.requestId = message.requestId;
6063
6200
  if (message.subscriptionId)
6064
6201
  err.subscriptionId = message.subscriptionId;
6202
+ // A policy/invariant denial over the socket carries the same
6203
+ // structured decline as the HTTP path (live-intent acks, WS
6204
+ // writes). Mirror `.decline` and notify the browser widget.
6205
+ const declineBody = message.decline
6206
+ ? { decline: message.decline }
6207
+ : {
6208
+ code: message.code,
6209
+ message: message.message,
6210
+ reason: message.reason,
6211
+ invariant: message.invariant,
6212
+ verdict: message.verdict,
6213
+ };
6214
+ const decline = deriveDecline(declineBody);
6215
+ if (decline) {
6216
+ err.decline = decline;
6217
+ err.isBoundedDecline = true;
6218
+ dispatchDeclineEvent(decline, connection.appId);
6219
+ }
6065
6220
  return err;
6066
6221
  };
6067
6222
  const wsError = buildWsError();
@@ -7129,7 +7284,17 @@ async function intent(roomPath, intent, opts = {}) {
7129
7284
  }
7130
7285
  if (!res.ok) {
7131
7286
  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}`;
7132
- throw new LiveIntentError(message, res.status, body);
7287
+ const liveError = new LiveIntentError(message, res.status, body);
7288
+ // A policy/invariant denial of a live intent carries the same structured
7289
+ // decline as the data-write path. Mirror `.decline` onto the LiveIntentError
7290
+ // (its name/contract is unchanged) and notify the browser widget.
7291
+ const decline = deriveDecline(body, res.status);
7292
+ if (decline) {
7293
+ liveError.decline = decline;
7294
+ liveError.isBoundedDecline = true;
7295
+ dispatchDeclineEvent(decline, config.appId);
7296
+ }
7297
+ throw liveError;
7133
7298
  }
7134
7299
  return (body !== null && body !== void 0 ? body : { ok: true });
7135
7300
  }
@@ -7283,6 +7448,8 @@ function defineLiveModule(mod) {
7283
7448
  return mod;
7284
7449
  }
7285
7450
 
7451
+ exports.BoundedDeclineError = BoundedDeclineError;
7452
+ exports.DECLINE_EVENT_NAME = DECLINE_EVENT_NAME;
7286
7453
  exports.EFFECT_INTENT_ADDRESS = EFFECT_INTENT_ADDRESS;
7287
7454
  exports.FunctionInvokeError = FunctionInvokeError;
7288
7455
  exports.InsufficientBalanceError = InsufficientBalanceError;
@@ -7298,7 +7465,9 @@ exports.convertRemainingAccounts = convertRemainingAccounts;
7298
7465
  exports.count = count;
7299
7466
  exports.createSessionWithSignature = createSessionWithSignature;
7300
7467
  exports.defineLiveModule = defineLiveModule;
7468
+ exports.deriveDecline = deriveDecline;
7301
7469
  exports.deriveUserIdentityFromIdToken = deriveUserIdentityFromIdToken;
7470
+ exports.dispatchDeclineEvent = dispatchDeclineEvent;
7302
7471
  exports.docId = docId;
7303
7472
  exports.functions = functions;
7304
7473
  exports.genAuthNonce = genAuthNonce;