@xshieldai/aegis-guard 0.5.0 β 0.6.1
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 +37 -3
- package/package.json +2 -2
- package/src/approval-token.ts +74 -8
- package/src/index.ts +4 -0
- package/src/signing.ts +22 -0
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# @xshieldai/aegis-guard
|
|
2
2
|
|
|
3
|
-
> **π Verification status (2026-10-
|
|
4
|
-
> - **Tests:** β
**
|
|
3
|
+
> **π Verification status (2026-10-11 IST β v0.6.1)**
|
|
4
|
+
> - **Tests:** β
**184/184 passing** in the repository; the npm package ships `src/` only, so run them from a clone (`bun test`): 70 in [tests/aegis-guard.test.ts](https://github.com/rocketlang/aegis/blob/master/packages/aegis-guard/tests/aegis-guard.test.ts) and 114 in [tests/hardening.test.ts](https://github.com/rocketlang/aegis/blob/master/packages/aegis-guard/tests/hardening.test.ts) for key trust, malformed tokens, nonces, the duplicate check, the audit event, the quality bar, the envelope helpers, the stated limits and the action digest. No test contacts a network or touches `~/.aegis`.
|
|
5
5
|
> - **Examples:** β
runnable quickstart, in the repository (not in the npm package): [examples/quickstart.ts](https://github.com/rocketlang/aegis/blob/master/packages/aegis-guard/examples/quickstart.ts) β `bun run examples/quickstart.ts` shows all 5 Locks in action with receipts
|
|
6
6
|
> - **Live demo:** β οΈ planned (Tier 3, see [PROOF-STACK.md](https://github.com/rocketlang/aegis/blob/master/PROOF-STACK.md))
|
|
7
7
|
> - **Limits:** see "What this does not do" below, and the v0.2.0 ACC section
|
|
8
8
|
> - **Upgrading from 0.3.x:** some calls that passed are now refused, and minting no longer makes a key. See "Upgrading from 0.3.x".
|
|
9
|
+
> - **Upgrading from 0.4.x, 0.5.0 or 0.6.0:** minting needs `AEGIS_MINT_AUTHORITY=1`, an approval must name the concrete action, and since 0.6.1 an action must be plain data. See "Upgrading to 0.6.x".
|
|
9
10
|
|
|
10
11
|
AEGIS Guard SDK β reusable approval-token, nonce, idempotency, SENSE, and quality-evidence primitives for AEGIS-governed services.
|
|
11
12
|
|
|
@@ -53,7 +54,12 @@ An approval token is an EdDSA-signed JWT. One box signs; every other box only ve
|
|
|
53
54
|
|
|
54
55
|
`verifyApprovalToken()` checks, in order: the signature; that service, capability and operation match exactly; that `expires_at` is a number and not past; that `issued_at`, when present, is a number, not more than 60 seconds ahead of the clock and not after `expires_at`; that `status`, when present, is exactly `approved`. Anything else throws `IrrNoApprovalError` (`code: 'IRR-NOAPPROVAL'`) β including a token that is not text.
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
Since 0.6.0 two things are refused by default, and the two calls below are therefore not enough on their own:
|
|
58
|
+
|
|
59
|
+
- **Minting is an authority action.** `mintApprovalToken()` and `ensureSigningKeypair()` throw unless the process declares itself the approving authority with `AEGIS_MINT_AUTHORITY=1`. An agent process verifies; it does not mint. On one user account an agent can set that variable itself: the real boundary is a private key under a different account that the agent cannot read.
|
|
60
|
+
- **An approval must name the concrete action.** A token without an `action_digest` is refused by every verify call, because an approval scoped only to service, capability and operation fits every instance of that operation. Use the action-bound pair described in the next section. `AEGIS_ALLOW_LABEL_ONLY_APPROVAL=1` brings back the old label-only behaviour for a caller that knowingly accepts it.
|
|
61
|
+
|
|
62
|
+
Mint with `issued_at` and `expires_at` in milliseconds: `mintActionApprovalToken({ service_id, capability, operation, nonce, issued_at: Date.now(), expires_at: Date.now() + 60_000 }, action)`.
|
|
57
63
|
|
|
58
64
|
```typescript
|
|
59
65
|
import { verifyApprovalToken, verifyScopedApprovalToken } from '@xshieldai/aegis-guard';
|
|
@@ -70,6 +76,25 @@ const payload = verifyScopedApprovalToken(
|
|
|
70
76
|
|
|
71
77
|
Each scope field must be the token's own field and strictly equal (`100` is not `"100"`). A field the caller has no value for (`undefined` or `null`) is refused: it would bind the token to nothing.
|
|
72
78
|
|
|
79
|
+
### One approval, one action
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { mintActionApprovalToken, verifyActionApprovalToken } from '@xshieldai/aegis-guard';
|
|
83
|
+
|
|
84
|
+
// On the authority (AEGIS_MINT_AUTHORITY=1): the approval names what was approved.
|
|
85
|
+
const action = { table: 'audit_log_2023', run_at: '2026-10-11T06:40:00.000Z' };
|
|
86
|
+
const token = mintActionApprovalToken({ service_id, capability, operation, nonce, issued_at, expires_at }, action);
|
|
87
|
+
|
|
88
|
+
// In the service, with the action it is about to perform:
|
|
89
|
+
const payload = verifyActionApprovalToken(token, 'my-service', 'schema', 'drop_table', actionInHand);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The token carries a SHA-256 digest of the action; the service recomputes it from the action in hand and a mismatch is refused. Keys are sorted at every depth, so the order they were written in does not matter.
|
|
93
|
+
|
|
94
|
+
**An action must be plain data: text, finite numbers, booleans, `null`, arrays and plain objects of these.** Anything else is refused with `IrrNoApprovalError`, at mint and at verify, and the message says where in the action it is (`action.pay.to[0].when is a Date, not a plain object`). That covers `Date`, `URL`, `Map`, `Set`, `RegExp`, `Buffer`, any class instance, `NaN`, `Β±Infinity`, `undefined` (also as a field's value, and a hole in an array), `bigint`, functions, symbols, and an action that contains itself. Write such a value as text yourself: a date as its ISO text, a URL as its href, a set as a sorted array, a large integer as digits.
|
|
95
|
+
|
|
96
|
+
Through 0.6.0 the digest gave those values a guessed spelling and the guesses collided: every `Date`, `URL`, `Map`, `Set`, `RegExp` and class instance was written `{}`, and `NaN`, `Infinity` and `undefined` were written `null`. **An approval minted for one such action verified for a different one.** If a service on 0.5.0 or 0.6.0 passes any of those as part of an action, upgrade. For an action that was plain data already, the digest is unchanged, so tokens minted before the upgrade still verify.
|
|
97
|
+
|
|
73
98
|
### LOCK_3 β observability (SENSE)
|
|
74
99
|
|
|
75
100
|
```typescript
|
|
@@ -176,6 +201,13 @@ const redisNonceStore: NonceStore = {
|
|
|
176
201
|
- `quality_drift_score`: bits 12β15, `aegis-quality-16bit-v1`
|
|
177
202
|
- AEG-Q-003 invariant: bits 12β15 must **never** be set in `quality_mask_at_promotion`
|
|
178
203
|
|
|
204
|
+
## Upgrading to 0.6.x
|
|
205
|
+
|
|
206
|
+
- **0.5.0** added `actionDigest()`, `mintActionApprovalToken()` and `verifyActionApprovalToken()`.
|
|
207
|
+
- **0.6.0: set `AEGIS_MINT_AUTHORITY=1` for the one process that mints**, and for no other. Without it minting and key generation throw. `AEGIS_ALLOW_INSECURE_LOCAL_MINT=1` is the stated opt-out for a single-process development box.
|
|
208
|
+
- **0.6.0: mint and verify with the action-bound pair.** A token with no `action_digest` is refused by `verifyApprovalToken()` and `verifyScopedApprovalToken()` too, unless `AEGIS_ALLOW_LABEL_ONLY_APPROVAL=1`.
|
|
209
|
+
- **0.6.1: an action must be plain data.** A `Date`, `URL`, `Map`, `Set`, `RegExp`, `Buffer`, class instance, `NaN`, `Β±Infinity`, `undefined` or `bigint` anywhere in an action is now refused, where 0.6.0 digested it (wrongly, see "One approval, one action"). Convert it to text on both sides, the minting side and the verifying side. A `Buffer` was digested correctly before and is refused now as well: write it as hex or base64 text.
|
|
210
|
+
|
|
179
211
|
## Upgrading from 0.3.x
|
|
180
212
|
|
|
181
213
|
- **Call `ensureSigningKeypair()` once on the authority box before minting.** Minting no longer makes a key. On a verifier box, do not call it at all.
|
|
@@ -191,6 +223,8 @@ const redisNonceStore: NonceStore = {
|
|
|
191
223
|
Each of these is pinned by a test in `tests/hardening.test.ts`, so it cannot change unnoticed β except the two about what is absent (a shared nonce store, a revocation list), which no test can show.
|
|
192
224
|
|
|
193
225
|
- **No upper limit on a token's life.** The signer chooses `expires_at`.
|
|
226
|
+
- **The package does not decide what "the action" is.** The caller builds it. A field the caller leaves out of the action is not bound, and two services that describe the same act differently do not match.
|
|
227
|
+
- **Verifying an action-bound token does not use it up.** The same token verifies again until it expires; a second use is stopped only by `verifyAndConsumeNonce()`.
|
|
194
228
|
- **An empty scope checks nothing more** than `verifyApprovalToken()`. A scope value that is an object never matches.
|
|
195
229
|
- **`verifyAndConsumeNonce()` checks no signature and no status.** It trusts the payload it is given.
|
|
196
230
|
- **A key file swapped under a running process is not noticed until restart.** A change of `AEGIS_APPROVAL_PUBKEY_PEM` is.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xshieldai/aegis-guard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AEGIS Guard SDK
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "AEGIS Guard SDK \u2014 Five Locks primitives (approval-token, nonce, idempotency, SENSE, quality-evidence) + opt-in Agentic Control Center event bus for AEGIS-governed services",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Capt. Anil Sharma <capt.anil.sharma@powerpbox.org>",
|
package/src/approval-token.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// @rule:AEG-HG-2B-008 β every field a decision rests on is checked for its type before it is
|
|
5
5
|
// compared; anything the gate cannot read is a refusal, with the
|
|
6
6
|
// package's own error, never a pass and never a TypeError
|
|
7
|
+
// @rule:AEG-HG-2B-010 β an action is plain data or it is refused; the digest never guesses a spelling
|
|
7
8
|
|
|
8
9
|
import { createHash } from 'crypto';
|
|
9
10
|
import { IrrNoApprovalError } from './errors.js';
|
|
@@ -32,6 +33,17 @@ const isText = (v: unknown): v is string => typeof v === 'string' && v.length >
|
|
|
32
33
|
const say = (v: unknown): string => (typeof v === 'string' ? v : typeof v).slice(0, 60);
|
|
33
34
|
const own = (o: object, k: string): boolean => Object.prototype.hasOwnProperty.call(o, k);
|
|
34
35
|
|
|
36
|
+
// @rule:AEG-E-016 β action-binding is the ENFORCED default (review point 4, 2026-10-07): a token
|
|
37
|
+
// that does not name the concrete act (no action_digest) is refused, so one approval cannot stand
|
|
38
|
+
// in for every instance. A caller that knowingly accepts the old label-only behaviour sets
|
|
39
|
+
// AEGIS_ALLOW_LABEL_ONLY_APPROVAL=1. (The full match β digest equals the act in hand β is still the
|
|
40
|
+
// job of verifyActionApprovalToken; this base check guarantees no loose token is accepted anywhere.)
|
|
41
|
+
export const LABEL_ONLY_ENV = 'AEGIS_ALLOW_LABEL_ONLY_APPROVAL';
|
|
42
|
+
const labelOnlyAllowed = (): boolean => {
|
|
43
|
+
const v = process.env[LABEL_ONLY_ENV];
|
|
44
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
45
|
+
};
|
|
46
|
+
|
|
35
47
|
// @rule:AEG-HG-2B-005 β SENSE stores proof reference, not proof secret.
|
|
36
48
|
// Returns first 24 hex chars of SHA-256 (96 bits) β sufficient for correlation, not reconstruction.
|
|
37
49
|
export function digestApprovalToken(token: string): string {
|
|
@@ -128,6 +140,16 @@ export function verifyApprovalToken(
|
|
|
128
140
|
if (payload.nonce !== undefined && !isText(payload.nonce)) {
|
|
129
141
|
throw new IrrNoApprovalError(cap, 'AEG-E-016: token nonce is not text');
|
|
130
142
|
}
|
|
143
|
+
// Action-binding is the enforced default (point 4): a label-only approval authorises every
|
|
144
|
+
// instance of the operation, so refuse it unless the caller opted into the old loose behaviour.
|
|
145
|
+
if (!isText(payload.action_digest) && !labelOnlyAllowed()) {
|
|
146
|
+
throw new IrrNoApprovalError(
|
|
147
|
+
cap,
|
|
148
|
+
`AEG-E-016: approval is not bound to a concrete action (no action_digest) β a label-only ` +
|
|
149
|
+
`approval authorises every instance. Mint with mintActionApprovalToken()/verify with ` +
|
|
150
|
+
`verifyActionApprovalToken(), or set ${LABEL_ONLY_ENV}=1 to accept label-only approvals.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
131
153
|
|
|
132
154
|
emitAccReceipt({
|
|
133
155
|
receipt_id: `aegis-guard-verify-${digestApprovalToken(token)}`,
|
|
@@ -264,21 +286,65 @@ export function verifyScopedApprovalToken(
|
|
|
264
286
|
// so a token minted without an action_digest cannot pass the action-bound check at all.
|
|
265
287
|
|
|
266
288
|
// Deterministic JSON: keys sorted, so {a,b} and {b,a} digest the same.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
289
|
+
//
|
|
290
|
+
// @rule:AEG-HG-2B-010 β an action is PLAIN DATA or it is refused: text, finite numbers, booleans,
|
|
291
|
+
// null, arrays of these, and plain objects of these. Through 0.6.0 every other value was given a
|
|
292
|
+
// guessed spelling, and the guesses collided: a Date, a URL, a Set, a Map, a RegExp and any class
|
|
293
|
+
// instance were all written "{}", and NaN, Infinity and undefined were all written "null". An
|
|
294
|
+
// approval minted for one such act then verified for a different one (found 11 October 2026 on
|
|
295
|
+
// the published 0.6.0). The digest does not guess any more. A caller writes such a value as text
|
|
296
|
+
// itself (a date as its ISO text, a URL as its href, a set as a sorted array), so that what was
|
|
297
|
+
// approved is what a person could read.
|
|
298
|
+
//
|
|
299
|
+
// For an action that was plain data already, the digest is the same as in 0.5.0 and 0.6.0.
|
|
300
|
+
function stableStringify(v: unknown, cap: string, at: string, seen: Set<object>): string {
|
|
301
|
+
const refuse = (what: string): never => {
|
|
302
|
+
throw new IrrNoApprovalError(
|
|
303
|
+
cap,
|
|
304
|
+
`AEG-E-016: the action cannot be bound β ${at} is ${what}. An action must be plain data ` +
|
|
305
|
+
`(text, finite numbers, booleans, null, arrays and plain objects); write other values as text.`,
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
if (v === null || typeof v === 'string' || typeof v === 'boolean') return JSON.stringify(v);
|
|
309
|
+
if (typeof v === 'number') return Number.isFinite(v) ? JSON.stringify(v) : refuse(`the number ${String(v)}`);
|
|
310
|
+
if (typeof v !== 'object') return refuse(v === undefined ? 'undefined' : `a ${typeof v}`);
|
|
311
|
+
if (seen.has(v)) return refuse('a value that contains itself');
|
|
312
|
+
seen.add(v);
|
|
313
|
+
let out: string;
|
|
314
|
+
if (Array.isArray(v)) {
|
|
315
|
+
const parts: string[] = [];
|
|
316
|
+
// By index, not map(): map() skips a hole, and a hole is a missing value.
|
|
317
|
+
for (let i = 0; i < v.length; i++) parts.push(stableStringify(v[i], cap, `${at}[${i}]`, seen));
|
|
318
|
+
out = '[' + parts.join(',') + ']';
|
|
319
|
+
} else {
|
|
320
|
+
const proto = Object.getPrototypeOf(v);
|
|
321
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
322
|
+
const name = (proto as { constructor?: { name?: unknown } })?.constructor?.name;
|
|
323
|
+
refuse(`a ${typeof name === 'string' && name ? name.slice(0, 40) : 'class instance'}, not a plain object`);
|
|
324
|
+
}
|
|
325
|
+
if (Object.getOwnPropertySymbols(v).length > 0) refuse('an object with a symbol key');
|
|
326
|
+
const o = v as Record<string, unknown>;
|
|
327
|
+
out = '{' + Object.keys(o).sort().map((k) =>
|
|
328
|
+
JSON.stringify(k) + ':' + stableStringify(o[k], cap, `${at}.${k.slice(0, 40)}`, seen)).join(',') + '}';
|
|
329
|
+
}
|
|
330
|
+
seen.delete(v);
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function digestFor(action: unknown, cap: string): string {
|
|
335
|
+
return createHash('sha256').update(stableStringify(action, cap, 'action', new Set())).digest('hex');
|
|
272
336
|
}
|
|
273
337
|
|
|
274
338
|
// SHA-256 of the canonical action. The caller owns what 'the action' is; the SDK never names domain concepts.
|
|
339
|
+
// Throws IrrNoApprovalError for an action that is not plain data (see AEG-HG-2B-010 above).
|
|
275
340
|
export function actionDigest(action: unknown): string {
|
|
276
|
-
return
|
|
341
|
+
return digestFor(action, 'unknown');
|
|
277
342
|
}
|
|
278
343
|
|
|
279
344
|
// Mint an approval already bound to a concrete action (adds action_digest to the signed payload).
|
|
280
345
|
export function mintActionApprovalToken(payload: ApprovalTokenPayload, action: unknown): string {
|
|
281
|
-
|
|
346
|
+
const cap = payload !== null && typeof payload === 'object' && isText(payload.capability) ? payload.capability : 'unknown';
|
|
347
|
+
return mintApprovalToken({ ...payload, action_digest: digestFor(action, cap) });
|
|
282
348
|
}
|
|
283
349
|
|
|
284
350
|
// Verify an approval against the concrete action in hand. The token MUST carry a matching action_digest;
|
|
@@ -292,6 +358,6 @@ export function verifyActionApprovalToken(
|
|
|
292
358
|
): ApprovalTokenPayload {
|
|
293
359
|
return verifyScopedApprovalToken(
|
|
294
360
|
token, expectedServiceId, expectedCapability, expectedOperation,
|
|
295
|
-
{ action_digest:
|
|
361
|
+
{ action_digest: digestFor(action, isText(expectedCapability) ? expectedCapability : 'unknown') },
|
|
296
362
|
);
|
|
297
363
|
}
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export {
|
|
|
49
49
|
actionDigest,
|
|
50
50
|
mintActionApprovalToken,
|
|
51
51
|
verifyActionApprovalToken,
|
|
52
|
+
LABEL_ONLY_ENV,
|
|
52
53
|
} from './approval-token.js';
|
|
53
54
|
|
|
54
55
|
// @rule:KGT-002 β KGT-T1.1 (v0.3.0): approval tokens are AEGIS-signed EdDSA JWTs.
|
|
@@ -62,6 +63,9 @@ export {
|
|
|
62
63
|
getPublicKeyPem,
|
|
63
64
|
signApprovalJwt,
|
|
64
65
|
verifyApprovalJwt,
|
|
66
|
+
mintAllowed,
|
|
67
|
+
MINT_AUTHORITY_ENV,
|
|
68
|
+
INSECURE_LOCAL_MINT_ENV,
|
|
65
69
|
} from './signing.js';
|
|
66
70
|
|
|
67
71
|
export {
|
package/src/signing.ts
CHANGED
|
@@ -27,6 +27,25 @@ export const APPROVAL_JWT_ALG = 'EdDSA';
|
|
|
27
27
|
const KEY_FILE = 'approval-signing.key'; // PKCS8 PEM (private) β mode 600
|
|
28
28
|
const PUB_FILE = 'approval-signing.pub'; // SPKI PEM (public) β mode 644
|
|
29
29
|
|
|
30
|
+
// @rule:KGT-002 β minting is an AUTHORITY action; separation is the default (review point 1,
|
|
31
|
+
// key leg, 2026-10-07). An ordinary agent process VERIFIES; it does not mint, and it does not
|
|
32
|
+
// make a signing key. Only the designated authority (the AEGIS dashboard, ideally under its own
|
|
33
|
+
// uid) mints β it declares itself with AEGIS_MINT_AUTHORITY=1. A single-process dev box that
|
|
34
|
+
// knowingly accepts the risk sets AEGIS_ALLOW_INSECURE_LOCAL_MINT=1 (explicit opt-out).
|
|
35
|
+
//
|
|
36
|
+
// HONEST CEILING: on one uid a determined agent can set the env itself. The real boundary is the
|
|
37
|
+
// private key living under a DIFFERENT uid the agent cannot read; this gate stops the lazy path
|
|
38
|
+
// (an agent that mints just by calling the SDK), and the OS stops the determined one.
|
|
39
|
+
export const MINT_AUTHORITY_ENV = 'AEGIS_MINT_AUTHORITY';
|
|
40
|
+
export const INSECURE_LOCAL_MINT_ENV = 'AEGIS_ALLOW_INSECURE_LOCAL_MINT';
|
|
41
|
+
const envTrue = (v?: string): boolean => v === '1' || v === 'true' || v === 'yes';
|
|
42
|
+
export const mintAllowed = (): boolean =>
|
|
43
|
+
envTrue(process.env[MINT_AUTHORITY_ENV]) || envTrue(process.env[INSECURE_LOCAL_MINT_ENV]);
|
|
44
|
+
const MINT_REFUSAL =
|
|
45
|
+
'KGT-002: minting is an authority action β this process is not the approval authority. Agents verify, ' +
|
|
46
|
+
'they do not mint. Run the authority (the AEGIS dashboard, ideally under its own uid) with ' +
|
|
47
|
+
'AEGIS_MINT_AUTHORITY=1, or set AEGIS_ALLOW_INSECURE_LOCAL_MINT=1 to accept single-process minting.';
|
|
48
|
+
|
|
30
49
|
function aegisDir(): string {
|
|
31
50
|
return process.env.AEGIS_DIR ?? join(process.env.HOME ?? homedir(), '.aegis');
|
|
32
51
|
}
|
|
@@ -102,6 +121,8 @@ function loadKeys(): Keys {
|
|
|
102
121
|
// replace the authority's key with the box's own, and the box would then accept approvals
|
|
103
122
|
// it signed itself.
|
|
104
123
|
export function ensureSigningKeypair(): { publicKeyPem: string } {
|
|
124
|
+
// Making a signing key is an authority action, not something an agent does by calling the SDK.
|
|
125
|
+
if (!mintAllowed()) throw new Error(MINT_REFUSAL);
|
|
105
126
|
const dir = aegisDir();
|
|
106
127
|
const keyPath = join(dir, KEY_FILE);
|
|
107
128
|
const pubPath = join(dir, PUB_FILE);
|
|
@@ -145,6 +166,7 @@ function b64url(buf: Buffer | string): string {
|
|
|
145
166
|
// It never makes a key: before v0.4.0 it did, on any box, at the first mint. The authority
|
|
146
167
|
// calls ensureSigningKeypair() once, at boot.
|
|
147
168
|
export function signApprovalJwt(payload: Record<string, unknown>): string {
|
|
169
|
+
if (!mintAllowed()) throw new Error(MINT_REFUSAL);
|
|
148
170
|
const { priv, conflict } = loadKeys();
|
|
149
171
|
if (!priv) {
|
|
150
172
|
throw new Error(
|