@shiprev/server-sdk-node 0.1.0-beta.1 → 0.1.0-beta.2
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/README.md +3 -3
- package/dist/client/shiprev-client.d.ts +4 -2
- package/dist/client/shiprev-client.js +10 -3
- package/dist/ruling/adjudicator.js +2 -1
- package/dist/transport/http.transport.js +22 -1
- package/dist/types.d.ts +44 -1
- package/dist/types.js +36 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,9 +181,9 @@ A `Ruling` is `{ allowed, source, reason?, remaining?, limit?, adjudicated_at }`
|
|
|
181
181
|
| `degraded` | Offline, ruled from the last snapshot |
|
|
182
182
|
| `client_default` | No basis for a ruling: invalid request, cold start offline, or online with nothing in the mirror |
|
|
183
183
|
|
|
184
|
-
Online rulings from the mirror compare `quantity` exactly against the remaining allowance, which the snapshot carries as fixed-point decimal strings. Beyond the remaining allowance nothing is allowed locally, whatever the overage policy: whether credits
|
|
184
|
+
Online rulings from the mirror compare `quantity` exactly against the remaining allowance, which the snapshot carries as fixed-point decimal strings. Beyond the remaining allowance nothing is allowed locally, whatever the overage policy: whether credits cover the write is state of the instance, so the local answer is a refusal with `reason: 'quota_exhausted'` (`'plan_upgrade_required'` when the capability's overage policy is `PROMPT_UPGRADE`; the instance's ruling uses the same two reasons), which `adjudicateAsync()` turns into a question to the instance and returns its ruling. When no ruling can be had the write stays refused. A capability the mirror does not hold is refused with `reason: 'capability_not_granted'` (which `adjudicateAsync()` turns into a question to the instance).
|
|
185
185
|
|
|
186
|
-
`guardWrite(options, action)` runs `action` only when a `write` ruling allows it
|
|
186
|
+
`guardWrite(options, action)` runs `action` only when a `write` ruling allows it. A refusal with `reason: 'quota_exhausted'` throws `QuotaExhaustedError`, one with `reason: 'plan_upgrade_required'` throws `PlanUpgradeRequiredError`, and any other refusal (disconnected, a capability the mirror does not hold) throws `DisconnectedWriteError`. Both funds errors carry `code` (`'QUOTA_EXHAUSTED'`, `'PLAN_UPGRADE_REQUIRED'`).
|
|
187
187
|
|
|
188
188
|
## Usage reporting
|
|
189
189
|
|
|
@@ -203,7 +203,7 @@ Before sending the buffer, each flush first drains what is waiting in the backlo
|
|
|
203
203
|
|
|
204
204
|
`await flush()` (and `await stop()`) resolves only when every event reported before the call has been accepted, refused (`on_rejected`), or moved to the backlog because it could not be delivered (offline, no service token, or a failed request; `on_flush_failure` is called for a failed buffer batch). If another flush is in flight, for example the one that follows a reconnection, the call waits for it and then makes its own pass. A flush started by `flush()` sends the whole backlog, batch by batch, stopping at the first failed batch; a timer tick skips while a flush is in flight and sends at most one backlog batch.
|
|
205
205
|
|
|
206
|
-
For work whose cost is only known afterwards, reserve first and settle later: `reserve({ organization_id, capability_key, amount, ttl_seconds? })`, then `confirm(reservationId, actualAmount?)` (without an amount, the reserved amount is confirmed) or `release(reservationId)`. Reservations need a service token, which `start()` obtains; without one they throw.
|
|
206
|
+
For work whose cost is only known afterwards, reserve first and settle later: `reserve({ organization_id, capability_key, amount, ttl_seconds? })`, then `confirm(reservationId, actualAmount?)` (without an amount, the reserved amount is confirmed) or `release(reservationId)`. Reservations need a service token, which `start()` obtains; without one they throw. A reservation refused for funds is HTTP `400` with `error.code` `QUOTA_EXHAUSTED` (credits short: offer a top-up) or `PLAN_UPGRADE_REQUIRED` (`PROMPT_UPGRADE` policy: offer a larger plan); `reserve()` throws `QuotaExhaustedError` or `PlanUpgradeRequiredError` accordingly, chosen by the code and never by the message, with `code`, `status` and `request_id`. It is not retried. Any other refusal throws as before.
|
|
207
207
|
|
|
208
208
|
## Connection
|
|
209
209
|
|
|
@@ -83,8 +83,10 @@ export declare class ShipRevClient {
|
|
|
83
83
|
*/
|
|
84
84
|
sendUsage(events: UsageSendEvent[]): Promise<UsageSendResult>;
|
|
85
85
|
/**
|
|
86
|
-
* Guard for write operations (fail-closed).
|
|
87
|
-
*
|
|
86
|
+
* Guard for write operations (fail-closed): runs `action` only when the local ruling allows the write.
|
|
87
|
+
* A refusal for funds throws `QuotaExhaustedError` (`quota_exhausted`) or `PlanUpgradeRequiredError`
|
|
88
|
+
* (`plan_upgrade_required`, PROMPT_UPGRADE); any other refusal, a disconnected write included, throws
|
|
89
|
+
* `DisconnectedWriteError` as before.
|
|
88
90
|
*/
|
|
89
91
|
guardWrite<T>(options: Omit<AdjudicateOptions, 'operation'>, action: () => Promise<T> | T): Promise<T>;
|
|
90
92
|
/**
|
|
@@ -127,7 +127,8 @@ class ShipRevClient {
|
|
|
127
127
|
// Two cases are not decided by the mirror, and the service is asked for an authoritative ruling:
|
|
128
128
|
// the mirror misses the organization or capability, or the write goes beyond the allowance the mirror
|
|
129
129
|
// counts (credits and overdraft are the service's state). The mirror's refusal stands when no ruling comes.
|
|
130
|
-
const beyondAllowance = ruling.reason === 'quota_exhausted'
|
|
130
|
+
const beyondAllowance = (ruling.reason === 'quota_exhausted' || ruling.reason === 'plan_upgrade_required') &&
|
|
131
|
+
ruling.source === 'realtime';
|
|
131
132
|
if (this.supervisor.currentToken() && (ruling.reason === 'capability_not_granted' || beyondAllowance)) {
|
|
132
133
|
try {
|
|
133
134
|
const remote = await this.supervisor.authorized((token) => this.transport.lookupAdjudication({
|
|
@@ -215,12 +216,18 @@ class ShipRevClient {
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
/**
|
|
218
|
-
* Guard for write operations (fail-closed).
|
|
219
|
-
*
|
|
219
|
+
* Guard for write operations (fail-closed): runs `action` only when the local ruling allows the write.
|
|
220
|
+
* A refusal for funds throws `QuotaExhaustedError` (`quota_exhausted`) or `PlanUpgradeRequiredError`
|
|
221
|
+
* (`plan_upgrade_required`, PROMPT_UPGRADE); any other refusal, a disconnected write included, throws
|
|
222
|
+
* `DisconnectedWriteError` as before.
|
|
220
223
|
*/
|
|
221
224
|
async guardWrite(options, action) {
|
|
222
225
|
const ruling = this.adjudicate({ ...options, operation: 'write' });
|
|
223
226
|
if (!ruling.allowed) {
|
|
227
|
+
if (ruling.reason === 'quota_exhausted')
|
|
228
|
+
throw new types_1.QuotaExhaustedError();
|
|
229
|
+
if (ruling.reason === 'plan_upgrade_required')
|
|
230
|
+
throw new types_1.PlanUpgradeRequiredError();
|
|
224
231
|
throw new types_1.DisconnectedWriteError(ruling.reason === 'disconnected_write'
|
|
225
232
|
? 'Write operation denied while client is disconnected'
|
|
226
233
|
: `Write operation denied: ${ruling.reason ?? 'not allowed'}`);
|
|
@@ -94,7 +94,8 @@ class Adjudicator {
|
|
|
94
94
|
remaining: (0, decimal_1.decimalToNumber)(item.remaining),
|
|
95
95
|
limit,
|
|
96
96
|
source: 'realtime',
|
|
97
|
-
|
|
97
|
+
// Under PROMPT_UPGRADE the way on is a larger plan, not more credits; the service rules the same way.
|
|
98
|
+
reason: item.overage_policy === 'PROMPT_UPGRADE' ? 'plan_upgrade_required' : 'quota_exhausted',
|
|
98
99
|
adjudicated_at: now,
|
|
99
100
|
};
|
|
100
101
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HttpTransport = exports.HttpError = void 0;
|
|
4
4
|
exports.isHttpStatus = isHttpStatus;
|
|
5
|
+
const types_1 = require("../types");
|
|
5
6
|
const decimal_1 = require("../util/decimal");
|
|
6
7
|
/** A non-success HTTP answer from the instance; `status` lets callers tell a refused token from an outage. */
|
|
7
8
|
class HttpError extends Error {
|
|
@@ -17,6 +18,25 @@ exports.HttpError = HttpError;
|
|
|
17
18
|
function isHttpStatus(error, ...statuses) {
|
|
18
19
|
return error instanceof HttpError && statuses.includes(error.status);
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The typed error of a refusal for funds, read from `error.code` of the instance's error envelope; undefined for any
|
|
23
|
+
* other answer. Only the code decides: the message text is never parsed for meaning.
|
|
24
|
+
*/
|
|
25
|
+
function fundsRefusal(errorText, status, message) {
|
|
26
|
+
let error;
|
|
27
|
+
try {
|
|
28
|
+
error = JSON.parse(errorText)?.error;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const details = { status, request_id: typeof error?.request_id === 'string' ? error.request_id : undefined };
|
|
34
|
+
if (error?.code === 'QUOTA_EXHAUSTED')
|
|
35
|
+
return new types_1.QuotaExhaustedError(message, details);
|
|
36
|
+
if (error?.code === 'PLAN_UPGRADE_REQUIRED')
|
|
37
|
+
return new types_1.PlanUpgradeRequiredError(message, details);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
20
40
|
class HttpTransport {
|
|
21
41
|
baseUrl;
|
|
22
42
|
timeoutMs;
|
|
@@ -205,7 +225,8 @@ class HttpTransport {
|
|
|
205
225
|
});
|
|
206
226
|
if (!response.ok) {
|
|
207
227
|
const errorText = await response.text();
|
|
208
|
-
|
|
228
|
+
const message = `${failure}: HTTP ${response.status} - ${errorText}`;
|
|
229
|
+
throw fundsRefusal(errorText, response.status, message) ?? new HttpError(message, response.status);
|
|
209
230
|
}
|
|
210
231
|
return (await response.json());
|
|
211
232
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,7 +4,13 @@ export interface Ruling {
|
|
|
4
4
|
remaining?: number;
|
|
5
5
|
limit?: number;
|
|
6
6
|
source: RulingSource;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Why the call was refused. `quota_exhausted`: the allowance is used up and the credits do not cover the overage
|
|
9
|
+
* (BLOCK, ALLOW_AND_BILL). `plan_upgrade_required`: the same under the PROMPT_UPGRADE policy, where the way on
|
|
10
|
+
* is a larger plan. Other values: `capability_not_found`, `capability_not_granted`, `disconnected_write`,
|
|
11
|
+
* `invalid_request`.
|
|
12
|
+
*/
|
|
13
|
+
reason?: 'quota_exhausted' | 'plan_upgrade_required' | 'disconnected_write' | 'invalid_request' | string;
|
|
8
14
|
adjudicated_at: number;
|
|
9
15
|
}
|
|
10
16
|
export interface AdjudicateOptions {
|
|
@@ -55,6 +61,11 @@ export interface OrganizationSnapshot {
|
|
|
55
61
|
* a local ruling with it; beyond the allowance the service decides. Absent on instances that predate it.
|
|
56
62
|
*/
|
|
57
63
|
overdraft_limit?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Credits that could still be spent when the entry was read: `wallet_balance` plus the overdraft limit, minus
|
|
66
|
+
* the credits held by open reservations. Informational, like `overdraft_limit`. Absent on instances that predate it.
|
|
67
|
+
*/
|
|
68
|
+
available_credits?: string;
|
|
58
69
|
items: Record<string, SnapshotItem>;
|
|
59
70
|
/**
|
|
60
71
|
* Degradation level the instance computed for the organization. Instances that predate it leave it out; a
|
|
@@ -208,6 +219,38 @@ export declare class DisconnectedWriteError extends Error {
|
|
|
208
219
|
readonly reason = "disconnected_write";
|
|
209
220
|
constructor(message?: string);
|
|
210
221
|
}
|
|
222
|
+
/** The `error.code` values of a refusal for funds, as the instance sends them and the two error types carry them. */
|
|
223
|
+
export type FundsRefusalCode = 'QUOTA_EXHAUSTED' | 'PLAN_UPGRADE_REQUIRED';
|
|
224
|
+
/** Where a funds refusal came from: the instance's HTTP answer (`status`, `request_id`) or a local ruling. */
|
|
225
|
+
export interface FundsRefusalDetails {
|
|
226
|
+
/** HTTP status of the instance's answer (400); absent when the refusal is a local ruling. */
|
|
227
|
+
status?: number;
|
|
228
|
+
/** The instance's request id, for support; absent when the refusal is a local ruling. */
|
|
229
|
+
request_id?: string;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The allowance is used up and the credits do not cover the overage: the buyer can top up (BLOCK, ALLOW_AND_BILL
|
|
233
|
+
* past the overdraft limit, credits held by other reservations, or an overage that is not for sale). Thrown by
|
|
234
|
+
* `reserve()` when the instance answers `code: 'QUOTA_EXHAUSTED'`, and by `guardWrite()` when the local ruling is
|
|
235
|
+
* `quota_exhausted`. The type follows the code, never the message.
|
|
236
|
+
*/
|
|
237
|
+
export declare class QuotaExhaustedError extends Error {
|
|
238
|
+
readonly code: "QUOTA_EXHAUSTED";
|
|
239
|
+
readonly status?: number;
|
|
240
|
+
readonly request_id?: string;
|
|
241
|
+
constructor(message?: string, details?: FundsRefusalDetails);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The capability's policy is PROMPT_UPGRADE and the allowance is used up: the buyer has to move to a larger plan.
|
|
245
|
+
* Thrown by `reserve()` when the instance answers `code: 'PLAN_UPGRADE_REQUIRED'`, and by `guardWrite()` when the
|
|
246
|
+
* local ruling is `plan_upgrade_required`. The type follows the code, never the message.
|
|
247
|
+
*/
|
|
248
|
+
export declare class PlanUpgradeRequiredError extends Error {
|
|
249
|
+
readonly code: "PLAN_UPGRADE_REQUIRED";
|
|
250
|
+
readonly status?: number;
|
|
251
|
+
readonly request_id?: string;
|
|
252
|
+
constructor(message?: string, details?: FundsRefusalDetails);
|
|
253
|
+
}
|
|
211
254
|
/**
|
|
212
255
|
* Thrown by `sendUsage()` when the batch did not reach the instance or the instance failed to process it (a
|
|
213
256
|
* network error, a timeout or a 5xx answer). Nothing in the batch can be assumed delivered; sending it again is
|
package/dist/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TokenRejectedError = exports.UsageDeliveryError = exports.DisconnectedWriteError = void 0;
|
|
3
|
+
exports.TokenRejectedError = exports.UsageDeliveryError = exports.PlanUpgradeRequiredError = exports.QuotaExhaustedError = exports.DisconnectedWriteError = void 0;
|
|
4
4
|
class DisconnectedWriteError extends Error {
|
|
5
5
|
reason = 'disconnected_write';
|
|
6
6
|
constructor(message = 'Write operation denied while client is disconnected') {
|
|
@@ -9,6 +9,41 @@ class DisconnectedWriteError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
exports.DisconnectedWriteError = DisconnectedWriteError;
|
|
12
|
+
/**
|
|
13
|
+
* The allowance is used up and the credits do not cover the overage: the buyer can top up (BLOCK, ALLOW_AND_BILL
|
|
14
|
+
* past the overdraft limit, credits held by other reservations, or an overage that is not for sale). Thrown by
|
|
15
|
+
* `reserve()` when the instance answers `code: 'QUOTA_EXHAUSTED'`, and by `guardWrite()` when the local ruling is
|
|
16
|
+
* `quota_exhausted`. The type follows the code, never the message.
|
|
17
|
+
*/
|
|
18
|
+
class QuotaExhaustedError extends Error {
|
|
19
|
+
code = 'QUOTA_EXHAUSTED';
|
|
20
|
+
status;
|
|
21
|
+
request_id;
|
|
22
|
+
constructor(message = 'The allowance is used up and the credits do not cover the overage', details = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'QuotaExhaustedError';
|
|
25
|
+
this.status = details.status;
|
|
26
|
+
this.request_id = details.request_id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.QuotaExhaustedError = QuotaExhaustedError;
|
|
30
|
+
/**
|
|
31
|
+
* The capability's policy is PROMPT_UPGRADE and the allowance is used up: the buyer has to move to a larger plan.
|
|
32
|
+
* Thrown by `reserve()` when the instance answers `code: 'PLAN_UPGRADE_REQUIRED'`, and by `guardWrite()` when the
|
|
33
|
+
* local ruling is `plan_upgrade_required`. The type follows the code, never the message.
|
|
34
|
+
*/
|
|
35
|
+
class PlanUpgradeRequiredError extends Error {
|
|
36
|
+
code = 'PLAN_UPGRADE_REQUIRED';
|
|
37
|
+
status;
|
|
38
|
+
request_id;
|
|
39
|
+
constructor(message = 'The allowance is used up; the plan has to be upgraded', details = {}) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'PlanUpgradeRequiredError';
|
|
42
|
+
this.status = details.status;
|
|
43
|
+
this.request_id = details.request_id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.PlanUpgradeRequiredError = PlanUpgradeRequiredError;
|
|
12
47
|
/**
|
|
13
48
|
* Thrown by `sendUsage()` when the batch did not reach the instance or the instance failed to process it (a
|
|
14
49
|
* network error, a timeout or a 5xx answer). Nothing in the batch can be assumed delivered; sending it again is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shiprev/server-sdk-node",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Server SDK for Node.js and NestJS services: token verification, local entitlement rulings, usage reporting, and two-phase reservations against a ShipRev instance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shiprev",
|