@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.
Files changed (87) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +3 -3
  3. package/dist/bin/graphorin.js +51 -13
  4. package/dist/bin/graphorin.js.map +1 -1
  5. package/dist/commands/audit.d.ts.map +1 -1
  6. package/dist/commands/audit.js +2 -1
  7. package/dist/commands/audit.js.map +1 -1
  8. package/dist/commands/consolidator.d.ts +56 -1
  9. package/dist/commands/consolidator.d.ts.map +1 -1
  10. package/dist/commands/consolidator.js +88 -2
  11. package/dist/commands/consolidator.js.map +1 -1
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/index.d.ts +4 -4
  14. package/dist/commands/index.js +4 -4
  15. package/dist/commands/init.d.ts +11 -4
  16. package/dist/commands/init.d.ts.map +1 -1
  17. package/dist/commands/init.js +15 -11
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/memory.d.ts +26 -1
  20. package/dist/commands/memory.d.ts.map +1 -1
  21. package/dist/commands/memory.js +56 -3
  22. package/dist/commands/memory.js.map +1 -1
  23. package/dist/commands/pricing.d.ts +6 -0
  24. package/dist/commands/pricing.d.ts.map +1 -1
  25. package/dist/commands/pricing.js +5 -2
  26. package/dist/commands/pricing.js.map +1 -1
  27. package/dist/commands/secrets.d.ts.map +1 -1
  28. package/dist/commands/secrets.js +2 -2
  29. package/dist/commands/secrets.js.map +1 -1
  30. package/dist/commands/skills.d.ts.map +1 -1
  31. package/dist/commands/skills.js +1 -1
  32. package/dist/commands/skills.js.map +1 -1
  33. package/dist/commands/storage.d.ts +41 -1
  34. package/dist/commands/storage.d.ts.map +1 -1
  35. package/dist/commands/storage.js +75 -1
  36. package/dist/commands/storage.js.map +1 -1
  37. package/dist/commands/token.d.ts.map +1 -1
  38. package/dist/commands/token.js +2 -2
  39. package/dist/commands/token.js.map +1 -1
  40. package/dist/commands/tools-lint.js +1 -1
  41. package/dist/commands/tools-lint.js.map +1 -1
  42. package/dist/commands/traces.d.ts +14 -2
  43. package/dist/commands/traces.d.ts.map +1 -1
  44. package/dist/commands/traces.js +39 -22
  45. package/dist/commands/traces.js.map +1 -1
  46. package/dist/commands/triggers.d.ts.map +1 -1
  47. package/dist/commands/triggers.js +5 -2
  48. package/dist/commands/triggers.js.map +1 -1
  49. package/dist/index.d.ts +4 -4
  50. package/dist/index.js +4 -5
  51. package/dist/index.js.map +1 -1
  52. package/dist/internal/output.js +6 -0
  53. package/dist/internal/output.js.map +1 -1
  54. package/dist/internal/store-context.js +13 -2
  55. package/dist/internal/store-context.js.map +1 -1
  56. package/dist/package.js +1 -1
  57. package/dist/package.js.map +1 -1
  58. package/package.json +18 -14
  59. package/src/bin/graphorin.ts +1387 -0
  60. package/src/commands/audit.ts +256 -0
  61. package/src/commands/auth.ts +238 -0
  62. package/src/commands/consolidator.ts +382 -0
  63. package/src/commands/doctor.ts +253 -0
  64. package/src/commands/guard.ts +144 -0
  65. package/src/commands/index.ts +223 -0
  66. package/src/commands/init.ts +194 -0
  67. package/src/commands/memory.ts +1052 -0
  68. package/src/commands/migrate-config.ts +77 -0
  69. package/src/commands/migrate-export.ts +117 -0
  70. package/src/commands/migrate.ts +83 -0
  71. package/src/commands/pricing.ts +244 -0
  72. package/src/commands/secrets.ts +309 -0
  73. package/src/commands/skills.ts +272 -0
  74. package/src/commands/start.ts +180 -0
  75. package/src/commands/storage.ts +659 -0
  76. package/src/commands/telemetry.ts +91 -0
  77. package/src/commands/token.ts +361 -0
  78. package/src/commands/tools-lint.ts +430 -0
  79. package/src/commands/traces.ts +188 -0
  80. package/src/commands/triggers.ts +237 -0
  81. package/src/index.ts +30 -0
  82. package/src/internal/exit.ts +62 -0
  83. package/src/internal/load-config.ts +107 -0
  84. package/src/internal/offline.ts +81 -0
  85. package/src/internal/output.ts +146 -0
  86. package/src/internal/prompts.ts +58 -0
  87. 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
+ }