flipstream 0.4.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 (102) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +659 -0
  3. package/bin/dev.js +9 -0
  4. package/bin/run.js +7 -0
  5. package/dist/commands/auth/clear-all.d.ts +10 -0
  6. package/dist/commands/auth/clear-all.js +50 -0
  7. package/dist/commands/auth/login.d.ts +11 -0
  8. package/dist/commands/auth/login.js +49 -0
  9. package/dist/commands/auth/logout.d.ts +10 -0
  10. package/dist/commands/auth/logout.js +51 -0
  11. package/dist/commands/auth/status.d.ts +8 -0
  12. package/dist/commands/auth/status.js +47 -0
  13. package/dist/commands/connections/list.d.ts +19 -0
  14. package/dist/commands/connections/list.js +43 -0
  15. package/dist/commands/query.d.ts +13 -0
  16. package/dist/commands/query.js +76 -0
  17. package/dist/commands/version.d.ts +15 -0
  18. package/dist/commands/version.js +17 -0
  19. package/dist/commands/workspaces/connections.d.ts +21 -0
  20. package/dist/commands/workspaces/connections.js +41 -0
  21. package/dist/commands/workspaces/get.d.ts +15 -0
  22. package/dist/commands/workspaces/get.js +65 -0
  23. package/dist/commands/workspaces/list.d.ts +20 -0
  24. package/dist/commands/workspaces/list.js +87 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/lib/api/admin-client.d.ts +19 -0
  28. package/dist/lib/api/admin-client.js +44 -0
  29. package/dist/lib/api/client.d.ts +13 -0
  30. package/dist/lib/api/client.js +21 -0
  31. package/dist/lib/api/connections.d.ts +14 -0
  32. package/dist/lib/api/connections.js +44 -0
  33. package/dist/lib/api/errors.d.ts +7 -0
  34. package/dist/lib/api/errors.js +68 -0
  35. package/dist/lib/api/http.d.ts +9 -0
  36. package/dist/lib/api/http.js +55 -0
  37. package/dist/lib/api/ids.d.ts +1 -0
  38. package/dist/lib/api/ids.js +8 -0
  39. package/dist/lib/api/list.d.ts +12 -0
  40. package/dist/lib/api/list.js +31 -0
  41. package/dist/lib/api/projections.d.ts +2 -0
  42. package/dist/lib/api/projections.js +34 -0
  43. package/dist/lib/api/retry.d.ts +13 -0
  44. package/dist/lib/api/retry.js +37 -0
  45. package/dist/lib/auth/claims.d.ts +3 -0
  46. package/dist/lib/auth/claims.js +20 -0
  47. package/dist/lib/auth/discovery.d.ts +13 -0
  48. package/dist/lib/auth/discovery.js +62 -0
  49. package/dist/lib/auth/flow.d.ts +22 -0
  50. package/dist/lib/auth/flow.js +135 -0
  51. package/dist/lib/auth/headless.d.ts +12 -0
  52. package/dist/lib/auth/headless.js +61 -0
  53. package/dist/lib/auth/loopback.d.ts +15 -0
  54. package/dist/lib/auth/loopback.js +198 -0
  55. package/dist/lib/auth/pkce.d.ts +6 -0
  56. package/dist/lib/auth/pkce.js +17 -0
  57. package/dist/lib/auth/provider.d.ts +15 -0
  58. package/dist/lib/auth/provider.js +23 -0
  59. package/dist/lib/auth/refresh.d.ts +6 -0
  60. package/dist/lib/auth/refresh.js +69 -0
  61. package/dist/lib/auth/register.d.ts +4 -0
  62. package/dist/lib/auth/register.js +43 -0
  63. package/dist/lib/auth/revoke.d.ts +8 -0
  64. package/dist/lib/auth/revoke.js +28 -0
  65. package/dist/lib/auth/session.d.ts +6 -0
  66. package/dist/lib/auth/session.js +13 -0
  67. package/dist/lib/command/admin.d.ts +4 -0
  68. package/dist/lib/command/admin.js +30 -0
  69. package/dist/lib/command/base.d.ts +28 -0
  70. package/dist/lib/command/base.js +75 -0
  71. package/dist/lib/command/crash.d.ts +2 -0
  72. package/dist/lib/command/crash.js +37 -0
  73. package/dist/lib/command/flags.d.ts +7 -0
  74. package/dist/lib/command/flags.js +10 -0
  75. package/dist/lib/config/constants.d.ts +5 -0
  76. package/dist/lib/config/constants.js +20 -0
  77. package/dist/lib/config/xdg.d.ts +21 -0
  78. package/dist/lib/config/xdg.js +97 -0
  79. package/dist/lib/errors.d.ts +38 -0
  80. package/dist/lib/errors.js +122 -0
  81. package/dist/lib/exit-codes.d.ts +10 -0
  82. package/dist/lib/exit-codes.js +13 -0
  83. package/dist/lib/help.d.ts +4 -0
  84. package/dist/lib/help.js +14 -0
  85. package/dist/lib/output/ndjson.d.ts +1 -0
  86. package/dist/lib/output/ndjson.js +16 -0
  87. package/dist/lib/output/redact.d.ts +1 -0
  88. package/dist/lib/output/redact.js +47 -0
  89. package/dist/lib/output/render.d.ts +2 -0
  90. package/dist/lib/output/render.js +8 -0
  91. package/dist/lib/output/table.d.ts +3 -0
  92. package/dist/lib/output/table.js +19 -0
  93. package/dist/lib/store/credentials.d.ts +12 -0
  94. package/dist/lib/store/credentials.js +40 -0
  95. package/dist/lib/store/index.d.ts +5 -0
  96. package/dist/lib/store/index.js +21 -0
  97. package/dist/lib/store/keyring.d.ts +16 -0
  98. package/dist/lib/store/keyring.js +45 -0
  99. package/dist/lib/store/memory-store.d.ts +10 -0
  100. package/dist/lib/store/memory-store.js +21 -0
  101. package/oclif.manifest.json +851 -0
  102. package/package.json +79 -0
@@ -0,0 +1,47 @@
1
+ const MASK = '***';
2
+ // Object keys whose values are secrets, compared in a normalized form so
3
+ // snake_case / camelCase / kebab-case and any casing all match.
4
+ const SECRET_KEYS = new Set([
5
+ 'accesstoken',
6
+ 'authorization',
7
+ 'clientsecret',
8
+ 'code',
9
+ 'codeverifier',
10
+ 'idtoken',
11
+ // Admin/connection material (E7-0b): a connection's secret reference and its
12
+ // provider config blob — masked whole (their presence, not contents, is all an
13
+ // agent needs). secret_id/secretId/secret-id all normalize to 'secretid'.
14
+ 'parameters',
15
+ 'refreshtoken',
16
+ 'secret',
17
+ 'secretid',
18
+ ]);
19
+ function normalizeKey(key) {
20
+ return key.toLowerCase().replaceAll(/[_-]/g, '');
21
+ }
22
+ // Secret query/form params (URLs and x-www-form-urlencoded bodies), preserving
23
+ // structure: only the value is masked, other params (e.g. state) stay visible.
24
+ // `code_verifier` precedes `code` so the longer name wins.
25
+ const SECRET_PARAMS = /(^|[?&])(access_token|refresh_token|id_token|code_verifier|code|authorization|client_secret)=[^&\s]+/gi;
26
+ // JWT-shaped tokens (three base64url segments) anywhere in free text, even with
27
+ // no preceding key — masks tokens that leak into stack traces / messages.
28
+ const JWT_SHAPE = /eyJ[\w-]*\.[\w-]+\.[\w-]+/g;
29
+ function redactString(value) {
30
+ return value.replaceAll(SECRET_PARAMS, `$1$2=${MASK}`).replaceAll(JWT_SHAPE, MASK);
31
+ }
32
+ // Return a redacted deep copy of `value` (never mutates the input). Applied to
33
+ // every log/debug/error path so tokens never reach stdout/stderr/logs.
34
+ export function redact(value) {
35
+ if (typeof value === 'string')
36
+ return redactString(value);
37
+ if (Array.isArray(value))
38
+ return value.map((item) => redact(item));
39
+ if (value && typeof value === 'object') {
40
+ const out = {};
41
+ for (const [key, val] of Object.entries(value)) {
42
+ out[key] = SECRET_KEYS.has(normalizeKey(key)) ? MASK : redact(val);
43
+ }
44
+ return out;
45
+ }
46
+ return value;
47
+ }
@@ -0,0 +1,2 @@
1
+ export { renderNdjson } from './ndjson.js';
2
+ export declare function renderJson(value: unknown): void;
@@ -0,0 +1,8 @@
1
+ export { renderNdjson } from './ndjson.js';
2
+ // Write a single JSON document to stdout (pretty-printed, trailing newline).
3
+ // stdout = data only; diagnostics/prompts go to stderr. NOTE: under oclif's
4
+ // --json (enableJsonFlag) the run() return value is auto-serialized — use this
5
+ // only when NOT relying on that path. BaseCommand.respond() handles the mode gate.
6
+ export function renderJson(value) {
7
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
8
+ }
@@ -0,0 +1,3 @@
1
+ import { type TableOptions } from '@oclif/table';
2
+ export type TableColumns<T extends Record<string, unknown>> = TableOptions<T>['columns'];
3
+ export declare function renderTable<T extends Record<string, unknown>>(data: T[], columns: TableColumns<T>, emptyMessage?: string): void;
@@ -0,0 +1,19 @@
1
+ import { makeTable } from '@oclif/table';
2
+ // Render a table to STDOUT — HUMAN output ONLY. Callers invoke this from a
3
+ // respond/respondList human formatter, which never runs under --json/--ndjson, so
4
+ // the agent contract (machine output is pure JSON/NDJSON) is preserved. Honors
5
+ // NO_COLOR (strips ANSI) and the terminal width (truncates overflowing cells).
6
+ export function renderTable(data, columns, emptyMessage = '(none)') {
7
+ if (data.length === 0) {
8
+ process.stdout.write(`${emptyMessage}\n`);
9
+ return;
10
+ }
11
+ const table = makeTable({
12
+ borderStyle: 'vertical-with-outline',
13
+ columns,
14
+ data,
15
+ noStyle: Boolean(process.env.NO_COLOR),
16
+ overflow: 'truncate-end',
17
+ });
18
+ process.stdout.write(`${table}\n`);
19
+ }
@@ -0,0 +1,12 @@
1
+ export declare const SKEW_SECONDS = 60;
2
+ export interface Credentials {
3
+ accessToken: string;
4
+ clientId: string;
5
+ expiresAt: number;
6
+ redirectUri: string;
7
+ refreshToken: string;
8
+ scopes: string[];
9
+ }
10
+ export declare function keyOf(host: string): string;
11
+ export declare function freshAccessToken(creds: Credentials | null): null | string;
12
+ export declare function parseCredentials(secret: string): Credentials | null;
@@ -0,0 +1,40 @@
1
+ // Treat a token as stale this many seconds BEFORE its real expiry, to avoid
2
+ // using a token that expires mid-request.
3
+ export const SKEW_SECONDS = 60;
4
+ // Per-host keychain account key (trailing slashes stripped so h and h/ collide).
5
+ export function keyOf(host) {
6
+ return host.replace(/\/+$/, '');
7
+ }
8
+ function nowSeconds() {
9
+ return Math.floor(Date.now() / 1000);
10
+ }
11
+ // The access token if present and not within SKEW_SECONDS of expiry, else null.
12
+ export function freshAccessToken(creds) {
13
+ if (creds && creds.accessToken && creds.expiresAt - SKEW_SECONDS > nowSeconds()) {
14
+ return creds.accessToken;
15
+ }
16
+ return null;
17
+ }
18
+ // Parse a stored secret into Credentials; null on non-JSON or missing required fields.
19
+ export function parseCredentials(secret) {
20
+ try {
21
+ const obj = JSON.parse(secret);
22
+ if (typeof obj.accessToken !== 'string' ||
23
+ typeof obj.refreshToken !== 'string' ||
24
+ typeof obj.expiresAt !== 'number' ||
25
+ typeof obj.clientId !== 'string') {
26
+ return null;
27
+ }
28
+ return {
29
+ accessToken: obj.accessToken,
30
+ clientId: obj.clientId,
31
+ expiresAt: obj.expiresAt,
32
+ redirectUri: typeof obj.redirectUri === 'string' ? obj.redirectUri : '',
33
+ refreshToken: obj.refreshToken,
34
+ scopes: Array.isArray(obj.scopes) ? obj.scopes : [],
35
+ };
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
@@ -0,0 +1,5 @@
1
+ import { type TokenStore } from './keyring.js';
2
+ export * from './credentials.js';
3
+ export { KeyringStore, type TokenStore } from './keyring.js';
4
+ export { MemoryStore } from './memory-store.js';
5
+ export declare function createStore(): TokenStore;
@@ -0,0 +1,21 @@
1
+ import { KeyringStore } from './keyring.js';
2
+ import { MemoryStore } from './memory-store.js';
3
+ export * from './credentials.js';
4
+ export { KeyringStore } from './keyring.js';
5
+ export { MemoryStore } from './memory-store.js';
6
+ // Keyed on a registered Symbol so the singleton is shared even when this module
7
+ // is loaded as two instances in one process — which happens on Windows ESM (the
8
+ // same file resolved via different path spellings) and would otherwise split the
9
+ // in-memory store between a test and the in-process command it drives.
10
+ const MEMORY_STORE = Symbol.for('io.flipstream.cli.memory-store');
11
+ // Choose the token store. FLIPSTREAM_NO_KEYRING forces an in-memory store (no
12
+ // keychain) — useful for headless/CI and tests; the process-global singleton
13
+ // keeps state across calls (and across module instances) within a process.
14
+ export function createStore() {
15
+ if (process.env.FLIPSTREAM_NO_KEYRING) {
16
+ const globals = globalThis;
17
+ globals[MEMORY_STORE] ??= new MemoryStore();
18
+ return globals[MEMORY_STORE];
19
+ }
20
+ return new KeyringStore();
21
+ }
@@ -0,0 +1,16 @@
1
+ import { type Credentials } from './credentials.js';
2
+ export declare const KEYRING_SERVICE = "io.flipstream.cli";
3
+ export interface TokenStore {
4
+ accessTokenIfFresh(host: string): null | string;
5
+ available(): boolean;
6
+ clear(host: string): void;
7
+ load(host: string): Credentials | null;
8
+ save(host: string, creds: Credentials): void;
9
+ }
10
+ export declare class KeyringStore implements TokenStore {
11
+ accessTokenIfFresh(host: string): null | string;
12
+ available(): boolean;
13
+ clear(host: string): void;
14
+ load(host: string): Credentials | null;
15
+ save(host: string, creds: Credentials): void;
16
+ }
@@ -0,0 +1,45 @@
1
+ import { Entry } from '@napi-rs/keyring';
2
+ import { freshAccessToken, keyOf, parseCredentials } from './credentials.js';
3
+ // FROZEN. Tokens are stored in the OS keychain under this service id. Renaming
4
+ // it strands users' tokens (silent logout + orphaned secrets), so any change
5
+ // requires a one-time keyring-only migration (read old id -> write new id ->
6
+ // delete old) — never a plaintext intermediary.
7
+ export const KEYRING_SERVICE = 'io.flipstream.cli';
8
+ // TokenStore backed by the OS keychain. The whole Credentials JSON is the single
9
+ // secret per host (keyed by host).
10
+ export class KeyringStore {
11
+ accessTokenIfFresh(host) {
12
+ return freshAccessToken(this.load(host));
13
+ }
14
+ available() {
15
+ try {
16
+ new Entry(KEYRING_SERVICE, '__probe__').getPassword();
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ // "not found" means the keychain works but is empty (available); any other
21
+ // failure (e.g. no Secret Service on headless Linux) means it is unavailable.
22
+ return /not found|no (matching )?(entry|password|item)|could not be found/i.test(error.message);
23
+ }
24
+ }
25
+ clear(host) {
26
+ try {
27
+ new Entry(KEYRING_SERVICE, keyOf(host)).deletePassword();
28
+ }
29
+ catch {
30
+ // Nothing stored for this host.
31
+ }
32
+ }
33
+ load(host) {
34
+ try {
35
+ const secret = new Entry(KEYRING_SERVICE, keyOf(host)).getPassword();
36
+ return secret ? parseCredentials(secret) : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ save(host, creds) {
43
+ new Entry(KEYRING_SERVICE, keyOf(host)).setPassword(JSON.stringify(creds));
44
+ }
45
+ }
@@ -0,0 +1,10 @@
1
+ import type { TokenStore } from './keyring.js';
2
+ import { type Credentials } from './credentials.js';
3
+ export declare class MemoryStore implements TokenStore {
4
+ private readonly entries;
5
+ accessTokenIfFresh(host: string): null | string;
6
+ available(): boolean;
7
+ clear(host: string): void;
8
+ load(host: string): Credentials | null;
9
+ save(host: string, creds: Credentials): void;
10
+ }
@@ -0,0 +1,21 @@
1
+ import { freshAccessToken, keyOf } from './credentials.js';
2
+ // In-memory TokenStore for tests and headless/CI (no real keychain available).
3
+ // Interchangeable with KeyringStore via the TokenStore interface.
4
+ export class MemoryStore {
5
+ entries = new Map();
6
+ accessTokenIfFresh(host) {
7
+ return freshAccessToken(this.load(host));
8
+ }
9
+ available() {
10
+ return true;
11
+ }
12
+ clear(host) {
13
+ this.entries.delete(keyOf(host));
14
+ }
15
+ load(host) {
16
+ return this.entries.get(keyOf(host)) ?? null;
17
+ }
18
+ save(host, creds) {
19
+ this.entries.set(keyOf(host), creds);
20
+ }
21
+ }