@aldus-runtime/gate-engine 0.1.0
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/LICENSE +201 -0
- package/NOTICE +21 -0
- package/dist/binding.d.ts +82 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +129 -0
- package/dist/binding.js.map +1 -0
- package/dist/definition.d.ts +187 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +231 -0
- package/dist/definition.js.map +1 -0
- package/dist/engine.d.ts +194 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +361 -0
- package/dist/engine.js.map +1 -0
- package/dist/errors.d.ts +61 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +57 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/money.d.ts +52 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +116 -0
- package/dist/money.js.map +1 -0
- package/dist/ports.d.ts +60 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +51 -0
- package/dist/ports.js.map +1 -0
- package/dist/spend.d.ts +127 -0
- package/dist/spend.d.ts.map +1 -0
- package/dist/spend.js +154 -0
- package/dist/spend.js.map +1 -0
- package/package.json +48 -0
- package/src/binding.ts +179 -0
- package/src/definition.ts +380 -0
- package/src/engine.ts +544 -0
- package/src/errors.ts +66 -0
- package/src/index.ts +102 -0
- package/src/money.ts +146 -0
- package/src/ports.ts +87 -0
- package/src/spend.ts +240 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AA6DjD,2CAA2C;AAC3C,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAIlD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAIvD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAE3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAK3D;AAED,+CAA+C;AAC/C,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAErD;AAED,wCAAwC;AACxC,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAErD;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAGlF;AAED,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAEhD"}
|
package/dist/money.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact decimal arithmetic on {@link Money}.
|
|
3
|
+
*
|
|
4
|
+
* Contract §19.3 requires per-request and per-run limits, actual cost recording, and
|
|
5
|
+
* stop-on-budget behaviour. All three are comparisons and sums over money, and Core deliberately
|
|
6
|
+
* models an amount as a decimal *string* because TTS costs are fractional-cent and IEEE-754
|
|
7
|
+
* accumulation silently corrupts the totals an operator authorises spend against.
|
|
8
|
+
*
|
|
9
|
+
* Honouring that decision means never converting an amount to `number`. Every operation here
|
|
10
|
+
* scales both operands to a common exponent and works in `bigint`, so a sum of ten thousand
|
|
11
|
+
* ten-thousandth-of-a-cent charges is exact rather than approximately exact.
|
|
12
|
+
*/
|
|
13
|
+
import { GateEngineErrorCodes, gateEngineError } from "./errors.js";
|
|
14
|
+
/** Matches Core's `decimalAmount`: an optionally signed integer with an optional fraction. */
|
|
15
|
+
const DECIMAL_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
16
|
+
function parseDecimal(amount, context = {}) {
|
|
17
|
+
if (!DECIMAL_PATTERN.test(amount)) {
|
|
18
|
+
throw gateEngineError(GateEngineErrorCodes.MONEY_MALFORMED, `Monetary amount "${amount}" is not a decimal string. Amounts are exact decimals, never ` +
|
|
19
|
+
"floating-point (contract §19.3).", { category: "validation", details: { amount, ...context } });
|
|
20
|
+
}
|
|
21
|
+
const [whole = "0", fraction = ""] = amount.split(".");
|
|
22
|
+
const negative = whole.startsWith("-");
|
|
23
|
+
const digits = `${whole.replace("-", "")}${fraction}`;
|
|
24
|
+
const units = BigInt(digits) * (negative ? -1n : 1n);
|
|
25
|
+
return { units, scale: fraction.length };
|
|
26
|
+
}
|
|
27
|
+
/** Rescale two decimals to a common exponent so they can be added or compared exactly. */
|
|
28
|
+
function align(a, b) {
|
|
29
|
+
const scale = Math.max(a.scale, b.scale);
|
|
30
|
+
return {
|
|
31
|
+
a: a.units * 10n ** BigInt(scale - a.scale),
|
|
32
|
+
b: b.units * 10n ** BigInt(scale - b.scale),
|
|
33
|
+
scale,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function formatDecimal(units, scale) {
|
|
37
|
+
if (scale === 0)
|
|
38
|
+
return units.toString();
|
|
39
|
+
const negative = units < 0n;
|
|
40
|
+
const digits = (negative ? -units : units).toString().padStart(scale + 1, "0");
|
|
41
|
+
const whole = digits.slice(0, digits.length - scale);
|
|
42
|
+
const fraction = digits.slice(digits.length - scale);
|
|
43
|
+
return `${negative ? "-" : ""}${whole}.${fraction}`;
|
|
44
|
+
}
|
|
45
|
+
function assertSameCurrency(a, b) {
|
|
46
|
+
if (a.currency !== b.currency) {
|
|
47
|
+
throw gateEngineError(GateEngineErrorCodes.CURRENCY_MISMATCH, `Cannot combine ${a.currency} with ${b.currency}. Converting between currencies would ` +
|
|
48
|
+
"require a rate this runtime does not hold, and a guessed rate would misstate a budget.", { category: "validation", details: { left: a.currency, right: b.currency } });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** A zero amount in the given currency. */
|
|
52
|
+
export function zeroMoney(currency) {
|
|
53
|
+
return { amount: "0", currency };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sum two amounts exactly.
|
|
57
|
+
*
|
|
58
|
+
* @throws {AldusError} `ALDUS_CURRENCY_MISMATCH` if the currencies differ.
|
|
59
|
+
*/
|
|
60
|
+
export function addMoney(a, b) {
|
|
61
|
+
assertSameCurrency(a, b);
|
|
62
|
+
const aligned = align(parseDecimal(a.amount), parseDecimal(b.amount));
|
|
63
|
+
return { amount: formatDecimal(aligned.a + aligned.b, aligned.scale), currency: a.currency };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Subtract `b` from `a` exactly. The result may be negative.
|
|
67
|
+
*
|
|
68
|
+
* @throws {AldusError} `ALDUS_CURRENCY_MISMATCH` if the currencies differ.
|
|
69
|
+
*/
|
|
70
|
+
export function subtractMoney(a, b) {
|
|
71
|
+
assertSameCurrency(a, b);
|
|
72
|
+
const aligned = align(parseDecimal(a.amount), parseDecimal(b.amount));
|
|
73
|
+
return { amount: formatDecimal(aligned.a - aligned.b, aligned.scale), currency: a.currency };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Total a list of amounts exactly.
|
|
77
|
+
*
|
|
78
|
+
* @throws {AldusError} `ALDUS_CURRENCY_MISMATCH` if the amounts are not all one currency.
|
|
79
|
+
*/
|
|
80
|
+
export function sumMoney(amounts, currency) {
|
|
81
|
+
return amounts.reduce((total, amount) => addMoney(total, amount), zeroMoney(currency));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compare two amounts: `-1` if `a < b`, `0` if equal, `1` if `a > b`.
|
|
85
|
+
*
|
|
86
|
+
* Numerically equal amounts written differently — `"1.5"` and `"1.50"` — compare equal, because
|
|
87
|
+
* trailing zeros are a presentation choice and treating them as a difference would make a budget
|
|
88
|
+
* check depend on how a provider happened to format its invoice.
|
|
89
|
+
*
|
|
90
|
+
* @throws {AldusError} `ALDUS_CURRENCY_MISMATCH` if the currencies differ.
|
|
91
|
+
*/
|
|
92
|
+
export function compareMoney(a, b) {
|
|
93
|
+
assertSameCurrency(a, b);
|
|
94
|
+
const aligned = align(parseDecimal(a.amount), parseDecimal(b.amount));
|
|
95
|
+
if (aligned.a === aligned.b)
|
|
96
|
+
return 0;
|
|
97
|
+
return aligned.a < aligned.b ? -1 : 1;
|
|
98
|
+
}
|
|
99
|
+
/** True if the amount is greater than zero. */
|
|
100
|
+
export function isPositiveMoney(money) {
|
|
101
|
+
return parseDecimal(money.amount).units > 0n;
|
|
102
|
+
}
|
|
103
|
+
/** True if the amount is below zero. */
|
|
104
|
+
export function isNegativeMoney(money) {
|
|
105
|
+
return parseDecimal(money.amount).units < 0n;
|
|
106
|
+
}
|
|
107
|
+
/** Validate an amount, raising `ALDUS_MONEY_MALFORMED` if it is not an exact decimal. */
|
|
108
|
+
export function assertMoney(money, context) {
|
|
109
|
+
parseDecimal(money.amount, context);
|
|
110
|
+
return money;
|
|
111
|
+
}
|
|
112
|
+
/** Render an amount for an operator-facing message. */
|
|
113
|
+
export function formatMoney(money) {
|
|
114
|
+
return `${money.amount} ${money.currency}`;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.js","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"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"}
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports the gate engine reads and writes through.
|
|
3
|
+
*
|
|
4
|
+
* Contract §7 requires core models to be independent of physical storage. This package therefore
|
|
5
|
+
* depends on no store implementation: an adopter wires these to `@aldus-runtime/file-store`'s `RunStore`
|
|
6
|
+
* and `EventStore`, or to anything else that honours them. The in-memory implementations here are
|
|
7
|
+
* for tests and for a caller evaluating gates without persisting anything.
|
|
8
|
+
*
|
|
9
|
+
* Each interface is kept to the operations this package actually uses. An aspirational method on
|
|
10
|
+
* a port is worse than an absent one, because a second adapter is written against it and only
|
|
11
|
+
* discovers at runtime that nothing honours it.
|
|
12
|
+
*/
|
|
13
|
+
import type { AldusEvent, CostRecord, GateDecision } from "@aldus-runtime/core";
|
|
14
|
+
/**
|
|
15
|
+
* Append-only storage for gate decisions (contract §7 `approvals.json`).
|
|
16
|
+
*
|
|
17
|
+
* There is no update or delete. §13's decisions are an audit record: a rejection that was later
|
|
18
|
+
* approved is two decisions, not one edited decision, and an interface that cannot express
|
|
19
|
+
* mutation is a stronger guarantee than one that merely declines to.
|
|
20
|
+
*/
|
|
21
|
+
export interface GateDecisionStore {
|
|
22
|
+
/** Every decision recorded for a Run, in the order they were appended. */
|
|
23
|
+
list(runId: string): Promise<GateDecision[]>;
|
|
24
|
+
/** Append one decision. */
|
|
25
|
+
append(runId: string, decision: GateDecision): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** Read access to a Run's recorded costs (contract §7 `costs.json`). */
|
|
28
|
+
export interface CostReader {
|
|
29
|
+
/** Every cost recorded for a Run. */
|
|
30
|
+
list(runId: string): Promise<CostRecord[]>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Where lifecycle events go (contract §6.4).
|
|
34
|
+
*
|
|
35
|
+
* §6.4 requires **every** state mutation to emit an immutable event, so recording a decision and
|
|
36
|
+
* emitting its event are one operation from a caller's point of view.
|
|
37
|
+
*/
|
|
38
|
+
export interface GateEventSink {
|
|
39
|
+
/** Emit one event. */
|
|
40
|
+
emit(event: AldusEvent): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** An in-memory {@link GateDecisionStore}, for tests and for evaluation without persistence. */
|
|
43
|
+
export declare class MemoryGateDecisionStore implements GateDecisionStore {
|
|
44
|
+
#private;
|
|
45
|
+
list(runId: string): Promise<GateDecision[]>;
|
|
46
|
+
append(runId: string, decision: GateDecision): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** An in-memory {@link CostReader}, for tests. */
|
|
49
|
+
export declare class MemoryCostReader implements CostReader {
|
|
50
|
+
#private;
|
|
51
|
+
list(runId: string): Promise<CostRecord[]>;
|
|
52
|
+
/** Record a cost, as a stage would after a paid request. */
|
|
53
|
+
add(record: CostRecord): void;
|
|
54
|
+
}
|
|
55
|
+
/** An in-memory {@link GateEventSink} that retains what it was given, for tests. */
|
|
56
|
+
export declare class MemoryGateEventSink implements GateEventSink {
|
|
57
|
+
readonly events: AldusEvent[];
|
|
58
|
+
emit(event: AldusEvent): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEhF;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC7C,2BAA2B;IAC3B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D;AAED,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACzB,qCAAqC;IACrC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;CAC5C;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,gGAAgG;AAChG,qBAAa,uBAAwB,YAAW,iBAAiB;;IAG/D,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAE3C;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAK3D;CACF;AAED,kDAAkD;AAClD,qBAAa,gBAAiB,YAAW,UAAU;;IAGjD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAEzC;IAED,4DAA4D;IAC5D,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAI5B;CACF;AAED,oFAAoF;AACpF,qBAAa,mBAAoB,YAAW,aAAa;IACvD,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,CAAM;IAEnC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrC;CACF"}
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports the gate engine reads and writes through.
|
|
3
|
+
*
|
|
4
|
+
* Contract §7 requires core models to be independent of physical storage. This package therefore
|
|
5
|
+
* depends on no store implementation: an adopter wires these to `@aldus-runtime/file-store`'s `RunStore`
|
|
6
|
+
* and `EventStore`, or to anything else that honours them. The in-memory implementations here are
|
|
7
|
+
* for tests and for a caller evaluating gates without persisting anything.
|
|
8
|
+
*
|
|
9
|
+
* Each interface is kept to the operations this package actually uses. An aspirational method on
|
|
10
|
+
* a port is worse than an absent one, because a second adapter is written against it and only
|
|
11
|
+
* discovers at runtime that nothing honours it.
|
|
12
|
+
*/
|
|
13
|
+
/** An in-memory {@link GateDecisionStore}, for tests and for evaluation without persistence. */
|
|
14
|
+
export class MemoryGateDecisionStore {
|
|
15
|
+
#byRun = new Map();
|
|
16
|
+
list(runId) {
|
|
17
|
+
return Promise.resolve([...(this.#byRun.get(runId) ?? [])]);
|
|
18
|
+
}
|
|
19
|
+
append(runId, decision) {
|
|
20
|
+
const existing = this.#byRun.get(runId);
|
|
21
|
+
if (existing === undefined)
|
|
22
|
+
this.#byRun.set(runId, [decision]);
|
|
23
|
+
else
|
|
24
|
+
existing.push(decision);
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** An in-memory {@link CostReader}, for tests. */
|
|
29
|
+
export class MemoryCostReader {
|
|
30
|
+
#byRun = new Map();
|
|
31
|
+
list(runId) {
|
|
32
|
+
return Promise.resolve([...(this.#byRun.get(runId) ?? [])]);
|
|
33
|
+
}
|
|
34
|
+
/** Record a cost, as a stage would after a paid request. */
|
|
35
|
+
add(record) {
|
|
36
|
+
const existing = this.#byRun.get(record.runId);
|
|
37
|
+
if (existing === undefined)
|
|
38
|
+
this.#byRun.set(record.runId, [record]);
|
|
39
|
+
else
|
|
40
|
+
existing.push(record);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** An in-memory {@link GateEventSink} that retains what it was given, for tests. */
|
|
44
|
+
export class MemoryGateEventSink {
|
|
45
|
+
events = [];
|
|
46
|
+
emit(event) {
|
|
47
|
+
this.events.push(event);
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmCH,gGAAgG;AAChG,MAAM,OAAO,uBAAuB;IACzB,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEpD,IAAI,CAAC,KAAa;QAChB,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,MAAM,CAAC,KAAa,EAAE,QAAsB;QAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,QAAQ,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;;YAC1D,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;CACF;AAED,kDAAkD;AAClD,MAAM,OAAO,gBAAgB;IAClB,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAElD,IAAI,CAAC,KAAa;QAChB,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,4DAA4D;IAC5D,GAAG,CAAC,MAAkB;QACpB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,QAAQ,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;;YAC/D,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;CACF;AAED,oFAAoF;AACpF,MAAM,OAAO,mBAAmB;IACrB,MAAM,GAAiB,EAAE,CAAC;IAEnC,IAAI,CAAC,KAAiB;QACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;CACF"}
|
package/dist/spend.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spend grants and stop-on-budget (architecture contract §19.3, §13.2).
|
|
3
|
+
*
|
|
4
|
+
* §19.3 requires cost-incurring stages to support per-request and per-run limits, explicit spend
|
|
5
|
+
* authorization, stop-on-budget behaviour, and safe handling of unknown provider billing status.
|
|
6
|
+
* §13.2 adds that paid TTS must not run until the operator has approved a **maximum authorized
|
|
7
|
+
* cost**, and that the authorization is void if any bound value changes.
|
|
8
|
+
*
|
|
9
|
+
* Those two requirements pull in different directions, and reconciling them is the whole design
|
|
10
|
+
* problem here. Enforcing a limit needs the limit's *value*; binding it needs its *digest*, and
|
|
11
|
+
* `GateDecision` stores only digests. Carrying the limit in a record beside the decision would
|
|
12
|
+
* let someone raise the ceiling without touching the approval.
|
|
13
|
+
*
|
|
14
|
+
* So a grant does both: it holds the values, and {@link grantLimitsDigest} is included among the
|
|
15
|
+
* gate's bound subjects. Raising a limit changes that digest, which drifts from `subjectHashes`,
|
|
16
|
+
* which voids the authorization exactly as §13.2 requires. The ceiling cannot move without an
|
|
17
|
+
* operator re-approving it.
|
|
18
|
+
*/
|
|
19
|
+
import type { CostRecord, Money } from "@aldus-runtime/core";
|
|
20
|
+
/**
|
|
21
|
+
* The subject key under which a grant's limits are bound.
|
|
22
|
+
*
|
|
23
|
+
* A conventional default. A gate definition may bind the limits under any key it likes — §4.3
|
|
24
|
+
* leaves gate composition to adopters — but a shared default means the common case needs no
|
|
25
|
+
* configuration.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SPEND_LIMIT_SUBJECT_KEY = "spendLimit";
|
|
28
|
+
/** An operator's explicit authorization to spend, bound to a gate decision (§13.2, §19.3). */
|
|
29
|
+
export interface SpendGrant {
|
|
30
|
+
/** Identity of this grant. */
|
|
31
|
+
grantId: string;
|
|
32
|
+
/** Run the grant applies to. */
|
|
33
|
+
runId: string;
|
|
34
|
+
/** Gate whose approval established the grant. */
|
|
35
|
+
gateId: string;
|
|
36
|
+
/**
|
|
37
|
+
* The `GateDecision.decisionId` that authorized this spend.
|
|
38
|
+
*
|
|
39
|
+
* `CostRecord.authorizationId` carries the same value, which is what links an incurred cost
|
|
40
|
+
* back to the approval that permitted it (§19.3 "explicit spend authorization").
|
|
41
|
+
*/
|
|
42
|
+
decisionId: string;
|
|
43
|
+
/** Maximum total spend authorized across the Run (§13.2 "maximum authorized cost"). */
|
|
44
|
+
maxTotal: Money;
|
|
45
|
+
/** Maximum spend authorized for any single request (§19.3 "per-request ... limits"). */
|
|
46
|
+
maxPerRequest?: Money;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The digest a gate must bind for the grant's limits to be tamper-evident.
|
|
50
|
+
*
|
|
51
|
+
* Only the limits are digested, not the grant's identity: re-issuing an identical ceiling under a
|
|
52
|
+
* new `grantId` should not read as the operator having approved something different.
|
|
53
|
+
*/
|
|
54
|
+
export declare function grantLimitsDigest(grant: SpendGrant): string;
|
|
55
|
+
/**
|
|
56
|
+
* Whether a cost record consumes budget.
|
|
57
|
+
*
|
|
58
|
+
* `voided` is excluded because a voided charge did not happen. **Everything else counts,
|
|
59
|
+
* including `unknown`** — §19.3 requires safe handling of an unconfirmed billing status, and the
|
|
60
|
+
* only safe direction is to assume an unconfirmed charge landed. Treating `unknown` as free would
|
|
61
|
+
* let a run whose provider never confirmed quietly spend past its ceiling, which is precisely the
|
|
62
|
+
* failure stop-on-budget exists to prevent.
|
|
63
|
+
*/
|
|
64
|
+
export declare function consumesBudget(record: CostRecord): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* What one cost record draws against a grant.
|
|
67
|
+
*
|
|
68
|
+
* The actual charge when known, the estimate otherwise. An estimate is the best available
|
|
69
|
+
* evidence that money is committed, and ignoring it until confirmation arrives would let a burst
|
|
70
|
+
* of in-flight requests overshoot a ceiling that looked untouched.
|
|
71
|
+
*/
|
|
72
|
+
export declare function costRecordDraw(record: CostRecord, currency: string): Money;
|
|
73
|
+
/** How much of a grant has been drawn, and by what. */
|
|
74
|
+
export interface SpendLedger {
|
|
75
|
+
/** Total drawn against the grant. */
|
|
76
|
+
consumed: Money;
|
|
77
|
+
/** Remaining headroom. Never negative — see {@link SpendLedger.overspent}. */
|
|
78
|
+
remaining: Money;
|
|
79
|
+
/** True if recorded costs already exceed the ceiling. */
|
|
80
|
+
overspent: boolean;
|
|
81
|
+
/** Cost records counted, in the order supplied. */
|
|
82
|
+
counted: CostRecord[];
|
|
83
|
+
/** Cost records excluded because they were voided. */
|
|
84
|
+
excluded: CostRecord[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Total what has been drawn against a grant.
|
|
88
|
+
*
|
|
89
|
+
* Only records naming this grant's `decisionId` in `authorizationId` are counted. A cost with no
|
|
90
|
+
* authorization, or one pointing at a different decision, is not this grant's business — and
|
|
91
|
+
* silently absorbing it would make one gate's ceiling depend on another's spending.
|
|
92
|
+
*/
|
|
93
|
+
export declare function computeLedger(grant: SpendGrant, costs: readonly CostRecord[]): SpendLedger;
|
|
94
|
+
/** A request to spend against a grant. */
|
|
95
|
+
export interface SpendRequest {
|
|
96
|
+
/** The amount about to be committed. */
|
|
97
|
+
amount: Money;
|
|
98
|
+
/** What the spend is for, for the refusal message. An open string (§4.2). */
|
|
99
|
+
operation?: string;
|
|
100
|
+
}
|
|
101
|
+
/** Why a spend was refused. */
|
|
102
|
+
export type SpendRefusalReason = "per-request-limit" | "total-limit" | "already-overspent" | "negative-amount";
|
|
103
|
+
/** The outcome of a stop-on-budget check. */
|
|
104
|
+
export type SpendCheck = {
|
|
105
|
+
allowed: true;
|
|
106
|
+
ledger: SpendLedger;
|
|
107
|
+
remainingAfter: Money;
|
|
108
|
+
} | {
|
|
109
|
+
allowed: false;
|
|
110
|
+
reason: SpendRefusalReason;
|
|
111
|
+
ledger: SpendLedger;
|
|
112
|
+
explanation: string;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Decide whether a spend may proceed (§19.3 stop-on-budget).
|
|
116
|
+
*
|
|
117
|
+
* Returns a refusal rather than throwing. A caller needs to *display* why a run stopped — an
|
|
118
|
+
* operator staring at a halted production wants the ceiling and the shortfall, not a stack trace
|
|
119
|
+
* — and the same reasoning ADR-0006 gives for pack resolution applies here.
|
|
120
|
+
*
|
|
121
|
+
* The check is deliberately conservative at every boundary: an exactly-equal request is allowed,
|
|
122
|
+
* but anything beyond is refused, and a grant already overspent refuses everything including a
|
|
123
|
+
* zero-value request, because the correct response to an overspent budget is a new authorization
|
|
124
|
+
* rather than another draw.
|
|
125
|
+
*/
|
|
126
|
+
export declare function checkSpend(grant: SpendGrant, costs: readonly CostRecord[], request: SpendRequest): SpendCheck;
|
|
127
|
+
//# sourceMappingURL=spend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend.d.ts","sourceRoot":"","sources":["../src/spend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAY7D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAe,CAAC;AAEpD,8FAA8F;AAC9F,MAAM,WAAW,UAAU;IACzB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,QAAQ,EAAE,KAAK,CAAC;IAChB,wFAAwF;IACxF,aAAa,CAAC,EAAE,KAAK,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAK3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAG1E;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,qCAAqC;IACrC,QAAQ,EAAE,KAAK,CAAC;IAChB,8EAA8E;IAC9E,SAAS,EAAE,KAAK,CAAC;IACjB,yDAAyD;IACzD,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sDAAsD;IACtD,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,WAAW,CAyB1F;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,MAAM,EAAE,KAAK,CAAC;IACd,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,+BAA+B;AAC/B,MAAM,MAAM,kBAAkB,GAC5B,mBAAmB,GAAG,aAAa,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAEhF,6CAA6C;AAC7C,MAAM,MAAM,UAAU,GAClB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,cAAc,EAAE,KAAK,CAAA;CAAE,GAC7D;IACE,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,SAAS,UAAU,EAAE,EAC5B,OAAO,EAAE,YAAY,GACpB,UAAU,CAsDZ"}
|
package/dist/spend.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spend grants and stop-on-budget (architecture contract §19.3, §13.2).
|
|
3
|
+
*
|
|
4
|
+
* §19.3 requires cost-incurring stages to support per-request and per-run limits, explicit spend
|
|
5
|
+
* authorization, stop-on-budget behaviour, and safe handling of unknown provider billing status.
|
|
6
|
+
* §13.2 adds that paid TTS must not run until the operator has approved a **maximum authorized
|
|
7
|
+
* cost**, and that the authorization is void if any bound value changes.
|
|
8
|
+
*
|
|
9
|
+
* Those two requirements pull in different directions, and reconciling them is the whole design
|
|
10
|
+
* problem here. Enforcing a limit needs the limit's *value*; binding it needs its *digest*, and
|
|
11
|
+
* `GateDecision` stores only digests. Carrying the limit in a record beside the decision would
|
|
12
|
+
* let someone raise the ceiling without touching the approval.
|
|
13
|
+
*
|
|
14
|
+
* So a grant does both: it holds the values, and {@link grantLimitsDigest} is included among the
|
|
15
|
+
* gate's bound subjects. Raising a limit changes that digest, which drifts from `subjectHashes`,
|
|
16
|
+
* which voids the authorization exactly as §13.2 requires. The ceiling cannot move without an
|
|
17
|
+
* operator re-approving it.
|
|
18
|
+
*/
|
|
19
|
+
import { digestSubjectValue } from "./binding.js";
|
|
20
|
+
import { addMoney, compareMoney, formatMoney, isNegativeMoney, subtractMoney, zeroMoney, } from "./money.js";
|
|
21
|
+
/**
|
|
22
|
+
* The subject key under which a grant's limits are bound.
|
|
23
|
+
*
|
|
24
|
+
* A conventional default. A gate definition may bind the limits under any key it likes — §4.3
|
|
25
|
+
* leaves gate composition to adopters — but a shared default means the common case needs no
|
|
26
|
+
* configuration.
|
|
27
|
+
*/
|
|
28
|
+
export const SPEND_LIMIT_SUBJECT_KEY = "spendLimit";
|
|
29
|
+
/**
|
|
30
|
+
* The digest a gate must bind for the grant's limits to be tamper-evident.
|
|
31
|
+
*
|
|
32
|
+
* Only the limits are digested, not the grant's identity: re-issuing an identical ceiling under a
|
|
33
|
+
* new `grantId` should not read as the operator having approved something different.
|
|
34
|
+
*/
|
|
35
|
+
export function grantLimitsDigest(grant) {
|
|
36
|
+
return digestSubjectValue({
|
|
37
|
+
maxTotal: grant.maxTotal,
|
|
38
|
+
maxPerRequest: grant.maxPerRequest ?? null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether a cost record consumes budget.
|
|
43
|
+
*
|
|
44
|
+
* `voided` is excluded because a voided charge did not happen. **Everything else counts,
|
|
45
|
+
* including `unknown`** — §19.3 requires safe handling of an unconfirmed billing status, and the
|
|
46
|
+
* only safe direction is to assume an unconfirmed charge landed. Treating `unknown` as free would
|
|
47
|
+
* let a run whose provider never confirmed quietly spend past its ceiling, which is precisely the
|
|
48
|
+
* failure stop-on-budget exists to prevent.
|
|
49
|
+
*/
|
|
50
|
+
export function consumesBudget(record) {
|
|
51
|
+
return record.billingStatus !== "voided";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What one cost record draws against a grant.
|
|
55
|
+
*
|
|
56
|
+
* The actual charge when known, the estimate otherwise. An estimate is the best available
|
|
57
|
+
* evidence that money is committed, and ignoring it until confirmation arrives would let a burst
|
|
58
|
+
* of in-flight requests overshoot a ceiling that looked untouched.
|
|
59
|
+
*/
|
|
60
|
+
export function costRecordDraw(record, currency) {
|
|
61
|
+
if (!consumesBudget(record))
|
|
62
|
+
return zeroMoney(currency);
|
|
63
|
+
return record.actual ?? record.estimated ?? zeroMoney(currency);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Total what has been drawn against a grant.
|
|
67
|
+
*
|
|
68
|
+
* Only records naming this grant's `decisionId` in `authorizationId` are counted. A cost with no
|
|
69
|
+
* authorization, or one pointing at a different decision, is not this grant's business — and
|
|
70
|
+
* silently absorbing it would make one gate's ceiling depend on another's spending.
|
|
71
|
+
*/
|
|
72
|
+
export function computeLedger(grant, costs) {
|
|
73
|
+
const currency = grant.maxTotal.currency;
|
|
74
|
+
const mine = costs.filter((record) => record.authorizationId === grant.decisionId);
|
|
75
|
+
const counted = [];
|
|
76
|
+
const excluded = [];
|
|
77
|
+
let consumed = zeroMoney(currency);
|
|
78
|
+
for (const record of mine) {
|
|
79
|
+
if (!consumesBudget(record)) {
|
|
80
|
+
excluded.push(record);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
counted.push(record);
|
|
84
|
+
consumed = addMoney(consumed, costRecordDraw(record, currency));
|
|
85
|
+
}
|
|
86
|
+
const headroom = subtractMoney(grant.maxTotal, consumed);
|
|
87
|
+
const overspent = isNegativeMoney(headroom);
|
|
88
|
+
return {
|
|
89
|
+
consumed,
|
|
90
|
+
remaining: overspent ? zeroMoney(currency) : headroom,
|
|
91
|
+
overspent,
|
|
92
|
+
counted,
|
|
93
|
+
excluded,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Decide whether a spend may proceed (§19.3 stop-on-budget).
|
|
98
|
+
*
|
|
99
|
+
* Returns a refusal rather than throwing. A caller needs to *display* why a run stopped — an
|
|
100
|
+
* operator staring at a halted production wants the ceiling and the shortfall, not a stack trace
|
|
101
|
+
* — and the same reasoning ADR-0006 gives for pack resolution applies here.
|
|
102
|
+
*
|
|
103
|
+
* The check is deliberately conservative at every boundary: an exactly-equal request is allowed,
|
|
104
|
+
* but anything beyond is refused, and a grant already overspent refuses everything including a
|
|
105
|
+
* zero-value request, because the correct response to an overspent budget is a new authorization
|
|
106
|
+
* rather than another draw.
|
|
107
|
+
*/
|
|
108
|
+
export function checkSpend(grant, costs, request) {
|
|
109
|
+
const ledger = computeLedger(grant, costs);
|
|
110
|
+
if (isNegativeMoney(request.amount)) {
|
|
111
|
+
return {
|
|
112
|
+
allowed: false,
|
|
113
|
+
reason: "negative-amount",
|
|
114
|
+
ledger,
|
|
115
|
+
explanation: `A spend of ${formatMoney(request.amount)} is negative. A refund is recorded as a voided ` +
|
|
116
|
+
"cost record, not as a negative draw, so that the audit trail keeps the original charge.",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (ledger.overspent) {
|
|
120
|
+
return {
|
|
121
|
+
allowed: false,
|
|
122
|
+
reason: "already-overspent",
|
|
123
|
+
ledger,
|
|
124
|
+
explanation: `Recorded costs of ${formatMoney(ledger.consumed)} already exceed the authorized maximum ` +
|
|
125
|
+
`of ${formatMoney(grant.maxTotal)}. Further spend needs a new authorization (§13.2).`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (grant.maxPerRequest !== undefined && compareMoney(request.amount, grant.maxPerRequest) > 0) {
|
|
129
|
+
return {
|
|
130
|
+
allowed: false,
|
|
131
|
+
reason: "per-request-limit",
|
|
132
|
+
ledger,
|
|
133
|
+
explanation: `A single request of ${formatMoney(request.amount)} exceeds the per-request limit of ` +
|
|
134
|
+
`${formatMoney(grant.maxPerRequest)} (§19.3).`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const projected = addMoney(ledger.consumed, request.amount);
|
|
138
|
+
if (compareMoney(projected, grant.maxTotal) > 0) {
|
|
139
|
+
return {
|
|
140
|
+
allowed: false,
|
|
141
|
+
reason: "total-limit",
|
|
142
|
+
ledger,
|
|
143
|
+
explanation: `Spending ${formatMoney(request.amount)} would bring the total to ` +
|
|
144
|
+
`${formatMoney(projected)}, past the authorized maximum of ` +
|
|
145
|
+
`${formatMoney(grant.maxTotal)}. ${formatMoney(ledger.remaining)} remains (§19.3).`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
allowed: true,
|
|
150
|
+
ledger,
|
|
151
|
+
remainingAfter: subtractMoney(grant.maxTotal, projected),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=spend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend.js","sourceRoot":"","sources":["../src/spend.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aldus-runtime/gate-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Aldus Gate Engine \u2014 hash-bound human gates, cascading invalidation, and spend authorization.",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/jamchen/aldus",
|
|
10
|
+
"directory": "packages/aldus-gate-engine"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/jamchen/aldus/tree/main/packages/aldus-gate-engine#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/jamchen/aldus/issues"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"NOTICE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -b",
|
|
30
|
+
"typecheck": "tsc -b --pretty",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"typecheck:test": "tsc -p tsconfig.test.json"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@aldus-runtime/core": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@aldus-runtime/testkit": "0.1.0",
|
|
40
|
+
"@types/node": "^26.2.0",
|
|
41
|
+
"typescript": "^7.0.2",
|
|
42
|
+
"vitest": "^4.1.10"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org"
|
|
47
|
+
}
|
|
48
|
+
}
|