@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/client/decline.d.ts +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +176 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +173 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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 (
|
|
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
|
|
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 =
|
|
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 =
|
|
3988
|
-
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
|
-
|
|
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;
|