@graphorin/cli 0.6.1 → 0.7.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/CHANGELOG.md +63 -0
- package/README.md +3 -3
- package/dist/bin/graphorin.js +51 -13
- package/dist/bin/graphorin.js.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +2 -1
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/consolidator.d.ts +56 -1
- package/dist/commands/consolidator.d.ts.map +1 -1
- package/dist/commands/consolidator.js +88 -2
- package/dist/commands/consolidator.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/index.d.ts +4 -4
- package/dist/commands/index.js +4 -4
- package/dist/commands/init.d.ts +11 -4
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +15 -11
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/memory.d.ts +26 -1
- package/dist/commands/memory.d.ts.map +1 -1
- package/dist/commands/memory.js +56 -3
- package/dist/commands/memory.js.map +1 -1
- package/dist/commands/pricing.d.ts +6 -0
- package/dist/commands/pricing.d.ts.map +1 -1
- package/dist/commands/pricing.js +5 -2
- package/dist/commands/pricing.js.map +1 -1
- package/dist/commands/secrets.d.ts.map +1 -1
- package/dist/commands/secrets.js +2 -2
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/storage.d.ts +41 -1
- package/dist/commands/storage.d.ts.map +1 -1
- package/dist/commands/storage.js +75 -1
- package/dist/commands/storage.js.map +1 -1
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +2 -2
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/tools-lint.js +1 -1
- package/dist/commands/tools-lint.js.map +1 -1
- package/dist/commands/traces.d.ts +14 -2
- package/dist/commands/traces.d.ts.map +1 -1
- package/dist/commands/traces.js +39 -22
- package/dist/commands/traces.js.map +1 -1
- package/dist/commands/triggers.d.ts.map +1 -1
- package/dist/commands/triggers.js +5 -2
- package/dist/commands/triggers.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/output.js +6 -0
- package/dist/internal/output.js.map +1 -1
- package/dist/internal/store-context.js +13 -2
- package/dist/internal/store-context.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +18 -14
- package/src/bin/graphorin.ts +1387 -0
- package/src/commands/audit.ts +256 -0
- package/src/commands/auth.ts +238 -0
- package/src/commands/consolidator.ts +382 -0
- package/src/commands/doctor.ts +253 -0
- package/src/commands/guard.ts +144 -0
- package/src/commands/index.ts +223 -0
- package/src/commands/init.ts +194 -0
- package/src/commands/memory.ts +1052 -0
- package/src/commands/migrate-config.ts +77 -0
- package/src/commands/migrate-export.ts +117 -0
- package/src/commands/migrate.ts +83 -0
- package/src/commands/pricing.ts +244 -0
- package/src/commands/secrets.ts +309 -0
- package/src/commands/skills.ts +272 -0
- package/src/commands/start.ts +180 -0
- package/src/commands/storage.ts +659 -0
- package/src/commands/telemetry.ts +91 -0
- package/src/commands/token.ts +361 -0
- package/src/commands/tools-lint.ts +430 -0
- package/src/commands/traces.ts +188 -0
- package/src/commands/triggers.ts +237 -0
- package/src/index.ts +30 -0
- package/src/internal/exit.ts +62 -0
- package/src/internal/load-config.ts +107 -0
- package/src/internal/offline.ts +81 -0
- package/src/internal/output.ts +146 -0
- package/src/internal/prompts.ts +58 -0
- package/src/internal/store-context.ts +165 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graphorin telemetry` - informational stubs.
|
|
3
|
+
*
|
|
4
|
+
* The framework promises **zero default telemetry** (DEC-154 /
|
|
5
|
+
* ADR-041). The Phase 15 surface ships the four subcommands as
|
|
6
|
+
* stubs that explain the policy and point operators at the
|
|
7
|
+
* documentation. Phase v0.2+ will extend the surface with an opt-in
|
|
8
|
+
* collector; today the stubs ensure the surface name-space is
|
|
9
|
+
* reserved + the operator gets a clear answer.
|
|
10
|
+
*
|
|
11
|
+
* Surface (per Phase 15 § Telemetry):
|
|
12
|
+
*
|
|
13
|
+
* - `graphorin telemetry status` - always reports `disabled`.
|
|
14
|
+
* - `graphorin telemetry enable` - refuses with a documentation
|
|
15
|
+
* pointer.
|
|
16
|
+
* - `graphorin telemetry disable` - no-op success.
|
|
17
|
+
* - `graphorin telemetry inspect` - informational summary of the
|
|
18
|
+
* promise (no phone home, no version pings, no crash reports).
|
|
19
|
+
*
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import process from 'node:process';
|
|
24
|
+
|
|
25
|
+
import { EXIT_CODES } from '../internal/exit.js';
|
|
26
|
+
import {
|
|
27
|
+
brand,
|
|
28
|
+
type CommonOutputOptions,
|
|
29
|
+
defaultPrintSink,
|
|
30
|
+
emitReport,
|
|
31
|
+
statusMarker,
|
|
32
|
+
} from '../internal/output.js';
|
|
33
|
+
|
|
34
|
+
/** @stable */
|
|
35
|
+
export interface TelemetryStatusResult {
|
|
36
|
+
readonly enabled: false;
|
|
37
|
+
readonly policy: 'zero-default';
|
|
38
|
+
readonly reference: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PRIVACY_REF = 'SECURITY.md § Privacy & telemetry (DEC-154 / ADR-041)';
|
|
42
|
+
|
|
43
|
+
const ZERO_DEFAULT_RESULT: TelemetryStatusResult = Object.freeze({
|
|
44
|
+
enabled: false,
|
|
45
|
+
policy: 'zero-default',
|
|
46
|
+
reference: PRIVACY_REF,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** @stable */
|
|
50
|
+
export function runTelemetryStatus(options: CommonOutputOptions = {}): TelemetryStatusResult {
|
|
51
|
+
emitReport(options, ZERO_DEFAULT_RESULT, () => {
|
|
52
|
+
const print = options.print ?? defaultPrintSink;
|
|
53
|
+
print(brand(`${statusMarker('info')} telemetry: disabled (policy: zero-default)`));
|
|
54
|
+
print(brand(`reference: ${PRIVACY_REF}`));
|
|
55
|
+
});
|
|
56
|
+
return ZERO_DEFAULT_RESULT;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @stable */
|
|
60
|
+
export function runTelemetryEnable(options: CommonOutputOptions = {}): TelemetryStatusResult {
|
|
61
|
+
emitReport(options, ZERO_DEFAULT_RESULT, () => {
|
|
62
|
+
const print = options.print ?? defaultPrintSink;
|
|
63
|
+
print(brand(`telemetry enable refused - the framework promises zero phone-home (DEC-154).`));
|
|
64
|
+
print(brand(`an opt-in collector is on the v0.2+ roadmap; see ${PRIVACY_REF}.`));
|
|
65
|
+
});
|
|
66
|
+
process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
67
|
+
return ZERO_DEFAULT_RESULT;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @stable */
|
|
71
|
+
export function runTelemetryDisable(options: CommonOutputOptions = {}): TelemetryStatusResult {
|
|
72
|
+
emitReport(options, ZERO_DEFAULT_RESULT, () => {
|
|
73
|
+
const print = options.print ?? defaultPrintSink;
|
|
74
|
+
print(brand(`telemetry is already disabled (no-op; zero-default policy).`));
|
|
75
|
+
});
|
|
76
|
+
return ZERO_DEFAULT_RESULT;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @stable */
|
|
80
|
+
export function runTelemetryInspect(options: CommonOutputOptions = {}): TelemetryStatusResult {
|
|
81
|
+
emitReport(options, ZERO_DEFAULT_RESULT, () => {
|
|
82
|
+
const print = options.print ?? defaultPrintSink;
|
|
83
|
+
print(brand('graphorin telemetry inspect:'));
|
|
84
|
+
print(` ${statusMarker('ok')} no phone home`);
|
|
85
|
+
print(` ${statusMarker('ok')} no version pings`);
|
|
86
|
+
print(` ${statusMarker('ok')} no crash reports`);
|
|
87
|
+
print(` ${statusMarker('ok')} no auto-updates`);
|
|
88
|
+
print(brand(`reference: ${PRIVACY_REF}`));
|
|
89
|
+
});
|
|
90
|
+
return ZERO_DEFAULT_RESULT;
|
|
91
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graphorin token` - manage server auth tokens.
|
|
3
|
+
*
|
|
4
|
+
* The subcommand thin-wraps the library functions in
|
|
5
|
+
* `@graphorin/security/auth` (`createToken`, `listTokens`,
|
|
6
|
+
* `revokeToken`, `rotateToken`, `rekeyTokens`, `verifyOffline`). Per
|
|
7
|
+
* Phase 15 § Tokens the surface ships:
|
|
8
|
+
*
|
|
9
|
+
* - `graphorin token create --label <name> --scopes <list> [--expires-in <duration>]`
|
|
10
|
+
* - `graphorin token list`
|
|
11
|
+
* - `graphorin token revoke <id>`
|
|
12
|
+
* - `graphorin token rotate <id>`
|
|
13
|
+
* - `graphorin token rekey`
|
|
14
|
+
* - `graphorin token verify <token>`
|
|
15
|
+
*
|
|
16
|
+
* The raw token bytes are shown to the operator at most once - at the
|
|
17
|
+
* call site of `create` / `rotate` / `rekey`. They are wrapped in a
|
|
18
|
+
* {@link SecretValue} on the way back to keep accidental logging out
|
|
19
|
+
* of the codepath; the CLI prints them via `value.use((s) => ...)` so
|
|
20
|
+
* the bytes never live as a plain string variable longer than needed.
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
createToken,
|
|
27
|
+
listTokens,
|
|
28
|
+
rekeyTokens,
|
|
29
|
+
revokeToken,
|
|
30
|
+
rotateToken,
|
|
31
|
+
type TokenMetadata,
|
|
32
|
+
verifyOffline,
|
|
33
|
+
} from '@graphorin/security';
|
|
34
|
+
|
|
35
|
+
import { EXIT_CODES } from '../internal/exit.js';
|
|
36
|
+
import {
|
|
37
|
+
brand,
|
|
38
|
+
type CommonOutputOptions,
|
|
39
|
+
defaultPrintSink,
|
|
40
|
+
emitReport,
|
|
41
|
+
statusMarker,
|
|
42
|
+
} from '../internal/output.js';
|
|
43
|
+
import { openStoreContext } from '../internal/store-context.js';
|
|
44
|
+
|
|
45
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
46
|
+
|
|
47
|
+
/** @stable */
|
|
48
|
+
export interface TokenCommonOptions extends CommonOutputOptions {
|
|
49
|
+
readonly config?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @stable */
|
|
53
|
+
export interface TokenCreateOptions extends TokenCommonOptions {
|
|
54
|
+
readonly label?: string;
|
|
55
|
+
/** Comma-separated scope list. */
|
|
56
|
+
readonly scopes: ReadonlyArray<string>;
|
|
57
|
+
/** Duration string: `30d`, `12h`, `90m`, `45s`. */
|
|
58
|
+
readonly expiresIn?: string;
|
|
59
|
+
readonly env?: 'live' | 'test';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @stable */
|
|
63
|
+
export interface TokenCreateResult {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly label?: string;
|
|
66
|
+
readonly scopes: ReadonlyArray<string>;
|
|
67
|
+
readonly raw: string;
|
|
68
|
+
readonly expiresAt?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a token. Returns the raw value once + the persisted record.
|
|
73
|
+
*
|
|
74
|
+
* @stable
|
|
75
|
+
*/
|
|
76
|
+
export async function runTokenCreate(options: TokenCreateOptions): Promise<TokenCreateResult> {
|
|
77
|
+
const ctx = await openStoreContext({
|
|
78
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
79
|
+
requirePepper: true,
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
if (ctx.pepper === undefined) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'[graphorin/cli] internal: pepper resolution should have populated ctx.pepper',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const env = options.env ?? 'live';
|
|
88
|
+
const expiresInMs =
|
|
89
|
+
options.expiresIn !== undefined ? parseDuration(options.expiresIn) : undefined;
|
|
90
|
+
const created = await createToken({
|
|
91
|
+
tokenStore: ctx.store.authTokens,
|
|
92
|
+
pepper: ctx.pepper,
|
|
93
|
+
env,
|
|
94
|
+
scopes: options.scopes,
|
|
95
|
+
...(options.label !== undefined ? { label: options.label } : {}),
|
|
96
|
+
...(expiresInMs !== undefined ? { expiresInMs } : {}),
|
|
97
|
+
});
|
|
98
|
+
const raw = await created.raw.use((s) => String(s));
|
|
99
|
+
const out: TokenCreateResult = Object.freeze({
|
|
100
|
+
id: created.record.id,
|
|
101
|
+
...(created.record.label !== undefined ? { label: created.record.label } : {}),
|
|
102
|
+
scopes: Object.freeze([...created.record.scopes]),
|
|
103
|
+
raw,
|
|
104
|
+
...(created.record.expiresAt !== undefined ? { expiresAt: created.record.expiresAt } : {}),
|
|
105
|
+
});
|
|
106
|
+
emitReport(options, out, () => {
|
|
107
|
+
const print = options.print ?? defaultPrintSink;
|
|
108
|
+
print(brand(`token created (id=${out.id}, scopes=${out.scopes.join(',')})`));
|
|
109
|
+
print(brand(`raw token (shown ONCE):`));
|
|
110
|
+
print(` ${out.raw}`);
|
|
111
|
+
if (out.expiresAt !== undefined) {
|
|
112
|
+
print(brand(`expires at: ${out.expiresAt}`));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return out;
|
|
116
|
+
} finally {
|
|
117
|
+
await ctx.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @stable */
|
|
122
|
+
export interface TokenListOptions extends TokenCommonOptions {
|
|
123
|
+
readonly includeRevoked?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* List token metadata.
|
|
128
|
+
*
|
|
129
|
+
* @stable
|
|
130
|
+
*/
|
|
131
|
+
export async function runTokenList(
|
|
132
|
+
options: TokenListOptions = {},
|
|
133
|
+
): Promise<ReadonlyArray<TokenMetadata>> {
|
|
134
|
+
const ctx = await openStoreContext({
|
|
135
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
const list = await listTokens(ctx.store.authTokens, {
|
|
139
|
+
includeRevoked: options.includeRevoked === true,
|
|
140
|
+
});
|
|
141
|
+
emitReport(options, list, () => {
|
|
142
|
+
const print = options.print ?? defaultPrintSink;
|
|
143
|
+
if (list.length === 0) {
|
|
144
|
+
print(brand('no tokens registered.'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
print(brand(`${list.length} token(s):`));
|
|
148
|
+
for (const t of list) {
|
|
149
|
+
const status = t.revokedAt !== undefined ? statusMarker('warn') : statusMarker('ok');
|
|
150
|
+
print(` ${status} ${t.id} (label=${t.label ?? '-'}, scopes=${t.scopes.join(',')})`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return list;
|
|
154
|
+
} finally {
|
|
155
|
+
await ctx.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @stable */
|
|
160
|
+
export interface TokenRevokeOptions extends TokenCommonOptions {
|
|
161
|
+
readonly id: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Revoke a single token.
|
|
166
|
+
*
|
|
167
|
+
* @stable
|
|
168
|
+
*/
|
|
169
|
+
export async function runTokenRevoke(
|
|
170
|
+
options: TokenRevokeOptions,
|
|
171
|
+
): Promise<TokenMetadata | undefined> {
|
|
172
|
+
const ctx = await openStoreContext({
|
|
173
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
174
|
+
});
|
|
175
|
+
try {
|
|
176
|
+
const result = await revokeToken(ctx.store.authTokens, options.id);
|
|
177
|
+
emitReport(options, result ?? null, () => {
|
|
178
|
+
const print = options.print ?? defaultPrintSink;
|
|
179
|
+
if (result === undefined) {
|
|
180
|
+
print(brand(`token '${options.id}' not found.`));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
print(brand(`token '${result.id}' revoked at ${result.revokedAt ?? '<now>'}`));
|
|
184
|
+
});
|
|
185
|
+
// W-002: exit code independent of --json (see runAuditVerify).
|
|
186
|
+
if (result === undefined) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
187
|
+
return result;
|
|
188
|
+
} finally {
|
|
189
|
+
await ctx.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @stable */
|
|
194
|
+
export interface TokenRotateOptions extends TokenCommonOptions {
|
|
195
|
+
readonly id: string;
|
|
196
|
+
readonly env?: 'live' | 'test';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Revoke an existing token and immediately mint a fresh one with the
|
|
201
|
+
* same scopes. Returns the new raw token bytes once.
|
|
202
|
+
*
|
|
203
|
+
* @stable
|
|
204
|
+
*/
|
|
205
|
+
export async function runTokenRotate(options: TokenRotateOptions): Promise<TokenCreateResult> {
|
|
206
|
+
const ctx = await openStoreContext({
|
|
207
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
208
|
+
requirePepper: true,
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
if (ctx.pepper === undefined) {
|
|
212
|
+
throw new Error('[graphorin/cli] internal: pepper missing.');
|
|
213
|
+
}
|
|
214
|
+
const env = options.env ?? 'live';
|
|
215
|
+
const result = await rotateToken({
|
|
216
|
+
tokenStore: ctx.store.authTokens,
|
|
217
|
+
pepper: ctx.pepper,
|
|
218
|
+
id: options.id,
|
|
219
|
+
env,
|
|
220
|
+
});
|
|
221
|
+
const raw = await result.next.raw.use((s) => String(s));
|
|
222
|
+
const out: TokenCreateResult = Object.freeze({
|
|
223
|
+
id: result.next.record.id,
|
|
224
|
+
...(result.next.record.label !== undefined ? { label: result.next.record.label } : {}),
|
|
225
|
+
scopes: Object.freeze([...result.next.record.scopes]),
|
|
226
|
+
raw,
|
|
227
|
+
...(result.next.record.expiresAt !== undefined
|
|
228
|
+
? { expiresAt: result.next.record.expiresAt }
|
|
229
|
+
: {}),
|
|
230
|
+
});
|
|
231
|
+
emitReport(options, out, () => {
|
|
232
|
+
const print = options.print ?? defaultPrintSink;
|
|
233
|
+
print(brand(`token '${result.old.id}' revoked + replaced by '${out.id}'`));
|
|
234
|
+
print(brand('raw token (shown ONCE):'));
|
|
235
|
+
print(` ${raw}`);
|
|
236
|
+
});
|
|
237
|
+
return out;
|
|
238
|
+
} finally {
|
|
239
|
+
await ctx.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @stable */
|
|
244
|
+
export interface TokenRekeyOptions extends TokenCommonOptions {
|
|
245
|
+
readonly env?: 'live' | 'test';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Re-issue every active token. Used after a known compromise.
|
|
250
|
+
*
|
|
251
|
+
* @stable
|
|
252
|
+
*/
|
|
253
|
+
export async function runTokenRekey(
|
|
254
|
+
options: TokenRekeyOptions = {},
|
|
255
|
+
): Promise<
|
|
256
|
+
ReadonlyArray<{ readonly oldId: string; readonly newId: string; readonly raw: string }>
|
|
257
|
+
> {
|
|
258
|
+
const ctx = await openStoreContext({
|
|
259
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
260
|
+
requirePepper: true,
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
if (ctx.pepper === undefined) {
|
|
264
|
+
throw new Error('[graphorin/cli] internal: pepper missing.');
|
|
265
|
+
}
|
|
266
|
+
const result = await rekeyTokens({
|
|
267
|
+
tokenStore: ctx.store.authTokens,
|
|
268
|
+
pepper: ctx.pepper,
|
|
269
|
+
env: options.env ?? 'live',
|
|
270
|
+
});
|
|
271
|
+
const out: Array<{ readonly oldId: string; readonly newId: string; readonly raw: string }> = [];
|
|
272
|
+
for (const [oldId, created] of result) {
|
|
273
|
+
const raw = await created.raw.use((s) => String(s));
|
|
274
|
+
out.push(Object.freeze({ oldId, newId: created.record.id, raw }));
|
|
275
|
+
}
|
|
276
|
+
const frozen = Object.freeze(out);
|
|
277
|
+
emitReport(options, frozen, () => {
|
|
278
|
+
const print = options.print ?? defaultPrintSink;
|
|
279
|
+
print(brand(`rekeyed ${frozen.length} token(s):`));
|
|
280
|
+
for (const row of frozen) {
|
|
281
|
+
print(` - old=${row.oldId} new=${row.newId}`);
|
|
282
|
+
print(` raw: ${row.raw}`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return frozen;
|
|
286
|
+
} finally {
|
|
287
|
+
await ctx.close();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** @stable */
|
|
292
|
+
export interface TokenVerifyOptions extends CommonOutputOptions {
|
|
293
|
+
readonly token: string;
|
|
294
|
+
/** Optional override of the expected token prefix. */
|
|
295
|
+
readonly prefix?: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @stable */
|
|
299
|
+
export interface TokenVerifyResult {
|
|
300
|
+
readonly ok: boolean;
|
|
301
|
+
readonly reason?: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Offline checksum verification. Confirms the structural shape, the
|
|
306
|
+
* environment marker, and the CRC checksum but does NOT consult the
|
|
307
|
+
* token store - it only proves the token was minted by a Graphorin
|
|
308
|
+
* helper.
|
|
309
|
+
*
|
|
310
|
+
* @stable
|
|
311
|
+
*/
|
|
312
|
+
export function runTokenVerify(options: TokenVerifyOptions): TokenVerifyResult {
|
|
313
|
+
const result = verifyOffline(options.token, {
|
|
314
|
+
...(options.prefix !== undefined ? { acceptPrefix: options.prefix } : {}),
|
|
315
|
+
});
|
|
316
|
+
const out: TokenVerifyResult = Object.freeze({
|
|
317
|
+
ok: result.ok,
|
|
318
|
+
...(result.ok ? {} : { reason: result.reason }),
|
|
319
|
+
});
|
|
320
|
+
emitReport(options, out, () => {
|
|
321
|
+
const print = options.print ?? defaultPrintSink;
|
|
322
|
+
if (out.ok) {
|
|
323
|
+
print(brand(`token format ${statusMarker('ok')} (offline checksum verified)`));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
print(brand(`token format ${statusMarker('fail')} (${out.reason ?? 'unknown reason'})`));
|
|
327
|
+
});
|
|
328
|
+
// W-002: exit code independent of --json (see runAuditVerify).
|
|
329
|
+
if (!out.ok) process.exitCode = EXIT_CODES.RECOVERABLE_FAILURE;
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Tiny duration parser. Accepts `Ns`, `Nm`, `Nh`, `Nd`. Returns
|
|
335
|
+
* milliseconds. Throws on invalid input - surfaced as a fail-fast at
|
|
336
|
+
* the CLI boundary.
|
|
337
|
+
*
|
|
338
|
+
* @internal
|
|
339
|
+
*/
|
|
340
|
+
export function parseDuration(input: string): number {
|
|
341
|
+
const match = /^(\d+)\s*([smhd])$/i.exec(input.trim());
|
|
342
|
+
if (!match) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`[graphorin/cli] invalid --expires-in '${input}'. Use the form '<number><s|m|h|d>' (e.g. 30d).`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const value = Number.parseInt(match[1] as string, 10);
|
|
348
|
+
const unit = (match[2] as string).toLowerCase();
|
|
349
|
+
switch (unit) {
|
|
350
|
+
case 's':
|
|
351
|
+
return value * 1000;
|
|
352
|
+
case 'm':
|
|
353
|
+
return value * 60_000;
|
|
354
|
+
case 'h':
|
|
355
|
+
return value * 60 * 60_000;
|
|
356
|
+
case 'd':
|
|
357
|
+
return value * ONE_DAY_MS;
|
|
358
|
+
default:
|
|
359
|
+
throw new Error(`[graphorin/cli] invalid --expires-in unit '${unit}'.`);
|
|
360
|
+
}
|
|
361
|
+
}
|