@elixpo/lixblogs-cli 1.3.3 → 1.4.2

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 (40) hide show
  1. package/README.md +9 -7
  2. package/dist/lixblogs.mjs +102 -0
  3. package/package.json +9 -10
  4. package/API.md +0 -104
  5. package/CHANGELOG.md +0 -10
  6. package/RELEASE.md +0 -30
  7. package/THREAT_MODEL.md +0 -91
  8. package/bin/lixblogs.mjs +0 -802
  9. package/src/api/AnalyticsClient.js +0 -40
  10. package/src/api/BlogClient.js +0 -140
  11. package/src/api/CollaborationClient.js +0 -73
  12. package/src/api/OrgClient.js +0 -158
  13. package/src/auth/AuthProvider.js +0 -90
  14. package/src/auth/AuthenticatedClient.js +0 -131
  15. package/src/auth/ElixpoAuthProvider.js +0 -281
  16. package/src/auth/MockAuthProvider.js +0 -170
  17. package/src/auth/productionGate.js +0 -44
  18. package/src/cli/contract.js +0 -46
  19. package/src/cli/ui.js +0 -54
  20. package/src/commands/analytics/index.js +0 -57
  21. package/src/commands/auth/login.js +0 -117
  22. package/src/commands/auth/logout.js +0 -21
  23. package/src/commands/auth/profileAlias.js +0 -29
  24. package/src/commands/auth/profiles.js +0 -27
  25. package/src/commands/auth/revoke.js +0 -45
  26. package/src/commands/auth/status.js +0 -33
  27. package/src/commands/blog/index.js +0 -85
  28. package/src/commands/blog/input.js +0 -59
  29. package/src/commands/collab/index.js +0 -53
  30. package/src/commands/org/index.js +0 -22
  31. package/src/commands/skill/index.js +0 -83
  32. package/src/config/CredentialStore.js +0 -142
  33. package/src/config/KeychainCredentialStore.js +0 -180
  34. package/src/config/ProfileRegistry.js +0 -105
  35. package/src/config/config.js +0 -60
  36. package/src/config/credentialStoreFactory.js +0 -63
  37. package/src/config/providerFactory.js +0 -42
  38. package/src/config/redact.js +0 -74
  39. package/src/content/markdown.js +0 -68
  40. package/src/content/validate.js +0 -45
@@ -1,142 +0,0 @@
1
- /**
2
- * CredentialStore — abstracts OS keychain access for tokens.
3
- *
4
- * Per #135: "Store credentials in the OS keychain; require an explicit
5
- * opt-in fallback when unavailable." This means:
6
- * - Default backend must be the real OS keychain (macOS Keychain,
7
- * Windows Credential Manager, Linux Secret Service/libsecret)
8
- * - Falling back to anything else (e.g. an encrypted file) requires the
9
- * user to explicitly opt in at the moment it's needed — never silently
10
- *
11
- * This file defines the interface + a real backend wrapper. The actual
12
- * OS-level library (e.g. a keytar-alternative) is expected to be wired in
13
- * here; this scaffold ships with the interface, an in-memory store (tests
14
- * only — never for real use), and the opt-in-gated fallback path so the
15
- * commands built on top of this don't need to change once the real
16
- * keychain library is chosen and added as a dependency.
17
- */
18
-
19
- export class CredentialStoreUnavailableError extends Error {
20
- constructor(message) {
21
- super(message);
22
- this.name = "CredentialStoreUnavailableError";
23
- }
24
- }
25
-
26
- export class CredentialStore {
27
- /**
28
- * @param {string} _profileId
29
- * @returns {Promise<{ accessToken: string, refreshToken: string, expiresAt: number, scopes: string[] } | null>}
30
- */
31
- async get(_profileId) {
32
- throw new Error("CredentialStore.get must be implemented by subclass");
33
- }
34
-
35
- /**
36
- * @param {string} _profileId
37
- * @param {{ accessToken: string, refreshToken: string, expiresAt: number, scopes: string[] }} _credentials
38
- * @returns {Promise<void>}
39
- */
40
- async set(_profileId, _credentials) {
41
- throw new Error("CredentialStore.set must be implemented by subclass");
42
- }
43
-
44
- /**
45
- * @param {string} _profileId
46
- * @returns {Promise<void>} Must not throw if nothing was stored.
47
- */
48
- async delete(_profileId) {
49
- throw new Error("CredentialStore.delete must be implemented by subclass");
50
- }
51
-
52
- /**
53
- * @returns {Promise<string[]>} All profile IDs currently holding credentials.
54
- */
55
- async listProfiles() {
56
- throw new Error("CredentialStore.listProfiles must be implemented by subclass");
57
- }
58
- }
59
-
60
- /**
61
- * In-memory store — for tests only. Never use this for real credential
62
- * storage; it exists purely so command logic can be tested without an OS
63
- * keychain in CI.
64
- */
65
- export class InMemoryCredentialStore extends CredentialStore {
66
- constructor() {
67
- super();
68
- /** @type {Map<string, object>} */
69
- this._store = new Map();
70
- }
71
-
72
- async get(profileId) {
73
- return this._store.get(profileId) ?? null;
74
- }
75
-
76
- async set(profileId, credentials) {
77
- this._store.set(profileId, credentials);
78
- }
79
-
80
- async delete(profileId) {
81
- this._store.delete(profileId);
82
- }
83
-
84
- async listProfiles() {
85
- return [...this._store.keys()];
86
- }
87
- }
88
-
89
- /**
90
- * Wraps a real OS-keychain-backed store, enforcing the "explicit opt-in
91
- * fallback" rule: if the underlying keychain library reports unavailable
92
- * (e.g. no Secret Service running on a headless Linux box), this does NOT
93
- * silently fall back — it throws CredentialStoreUnavailableError, and the
94
- * calling command is responsible for prompting the user for explicit
95
- * opt-in before constructing a fallback store instance.
96
- *
97
- * @param {CredentialStore} realStore - the actual OS-keychain-backed store
98
- */
99
- export class GatedCredentialStore extends CredentialStore {
100
- constructor(realStore) {
101
- super();
102
- this._realStore = realStore;
103
- }
104
-
105
- async get(profileId) {
106
- try {
107
- return await this._realStore.get(profileId);
108
- } catch (err) {
109
- throw new CredentialStoreUnavailableError(
110
- `OS keychain is unavailable: ${err.message}. Re-run with an explicit ` +
111
- `fallback flag if you want to opt in to a less secure storage method.`
112
- );
113
- }
114
- }
115
-
116
- async set(profileId, credentials) {
117
- try {
118
- await this._realStore.set(profileId, credentials);
119
- } catch (err) {
120
- throw new CredentialStoreUnavailableError(
121
- `OS keychain is unavailable: ${err.message}. Re-run with an explicit ` +
122
- `fallback flag if you want to opt in to a less secure storage method.`
123
- );
124
- }
125
- }
126
-
127
- async delete(profileId) {
128
- try {
129
- await this._realStore.delete(profileId);
130
- } catch (err) {
131
- throw new CredentialStoreUnavailableError(`OS keychain is unavailable: ${err.message}`);
132
- }
133
- }
134
-
135
- async listProfiles() {
136
- try {
137
- return await this._realStore.listProfiles();
138
- } catch (err) {
139
- throw new CredentialStoreUnavailableError(`OS keychain is unavailable: ${err.message}`);
140
- }
141
- }
142
- }
@@ -1,180 +0,0 @@
1
- /**
2
- * KeychainCredentialStore — real OS-keychain-backed CredentialStore.
3
- *
4
- * Uses @napi-rs/keyring: macOS Keychain, Windows Credential Manager, Linux
5
- * Secret Service/libsecret. Chosen over `keytar` because keytar is
6
- * archived/deprecated (last release Feb 2022, no longer maintained);
7
- * @napi-rs/keyring is the actively maintained equivalent with a similar API.
8
- *
9
- * Each profile's credentials are stored as a single JSON-serialized secret
10
- * under a per-profile keychain entry — one entry per profile, not one
11
- * entry per token field, so multi-profile isolation (see THREAT_MODEL.md
12
- * §5) maps directly onto separate keychain entries rather than a shared
13
- * blob multiple profiles could collide in.
14
- *
15
- * This class throws on any underlying failure — it does NOT catch and
16
- * silently return null/succeed. Per #135's "explicit opt-in fallback"
17
- * requirement, silent degradation here would be exactly the wrong
18
- * behavior; the caller (via GatedCredentialStore, see CredentialStore.js)
19
- * is responsible for catching this and prompting for explicit opt-in.
20
- */
21
-
22
- import { Entry } from "@napi-rs/keyring";
23
- import { CredentialStore } from "./CredentialStore.js";
24
-
25
- const SERVICE_NAME = "lixblogs-cli";
26
- const PROBE_PROFILE_ID = "__lixblogs_availability_probe__";
27
-
28
- function entryFor(profileId) {
29
- return new Entry(SERVICE_NAME, profileId);
30
- }
31
-
32
- /**
33
- * Checks whether the OS keychain backend is actually reachable.
34
- *
35
- * IMPORTANT, discovered empirically while building this (not assumed):
36
- * on at least one backend (headless Linux, no Secret Service running),
37
- * Entry.getPassword() on a missing entry returns null instead of
38
- * throwing — even when the backend is completely unreachable. That means
39
- * get() alone cannot distinguish "nothing stored yet" from "keychain is
40
- * broken." Entry.setPassword() DOES throw reliably when the backend is
41
- * unreachable in that same environment, so this probe uses a harmless
42
- * write+delete round-trip to get a trustworthy signal there, rather than
43
- * inferring availability from get().
44
- *
45
- * KNOWN LIMITATION, also discovered empirically (not theoretical): on at
46
- * least one WSL setup, a probe call that fails (AccessDenied) can be
47
- * followed by an unrelated Entry's setPassword() succeeding moments
48
- * later, even though the probe itself correctly reported unavailability.
49
- * This matches a documented upstream keyring-rs issue where WSL with
50
- * systemd enabled has only a Secret Service *session* collection and no
51
- * *default* collection — the first call against the (missing) default
52
- * collection fails, but the backend appears to recover or fall back on
53
- * a subsequent call. Practical implication: this probe is a best-effort
54
- * signal, not a guarantee — a single probe result should be trusted for
55
- * the immediate decision (fail vs. proceed) but must not be assumed to
56
- * hold true for every subsequent call in the same process on every
57
- * platform. If this proves unreliable enough in practice, a more robust
58
- * approach (e.g. re-probing before every real operation, or shipping a
59
- * documented "known platforms" support matrix) should be a follow-up.
60
- *
61
- * @returns {Promise<{ available: boolean, error?: string }>}
62
- */
63
- export async function probeKeychainAvailability() {
64
- const entry = entryFor(PROBE_PROFILE_ID);
65
- try {
66
- entry.setPassword("probe");
67
- entry.deletePassword();
68
- return { available: true };
69
- } catch (err) {
70
- // The underlying native error can include a multi-line Rust stack
71
- // trace in its message — take only the first line for anything shown
72
- // to a user; the full message is still available via err if needed
73
- // for debugging (e.g. --verbose diagnostics, a later issue).
74
- const firstLine = String(err.message ?? err).split("\n")[0].trim();
75
- return { available: false, error: firstLine };
76
- }
77
- }
78
-
79
- export class KeychainCredentialStore extends CredentialStore {
80
- async get(profileId) {
81
- const entry = entryFor(profileId);
82
- let raw;
83
- try {
84
- raw = entry.getPassword();
85
- } catch (err) {
86
- // @napi-rs/keyring throws on some platforms/backends when no entry
87
- // exists yet — that's a legitimate "not logged in" case, not a
88
- // keychain-unavailable failure. Distinguish by message rather than
89
- // swallowing all errors, so real unavailability still propagates.
90
- if (isNotFoundError(err)) {
91
- return null;
92
- }
93
- throw err;
94
- }
95
-
96
- // On some platforms/backends, a missing entry returns null/undefined
97
- // rather than throwing (observed behavior, not assumed) — treat that
98
- // the same as "not logged in," not as a parse error.
99
- if (raw === null || raw === undefined) {
100
- return null;
101
- }
102
-
103
- return JSON.parse(raw);
104
- }
105
-
106
- async set(profileId, credentials) {
107
- const entry = entryFor(profileId);
108
- entry.setPassword(JSON.stringify(credentials));
109
- }
110
-
111
- async delete(profileId) {
112
- const entry = entryFor(profileId);
113
- try {
114
- entry.deletePassword();
115
- } catch (err) {
116
- if (isNotFoundError(err)) {
117
- return; // deleting something that isn't there is not an error
118
- }
119
- throw err;
120
- }
121
- }
122
-
123
- async listProfiles() {
124
- // @napi-rs/keyring has no "list all entries for a service" API — OS
125
- // keychains generally don't expose enumeration without extra
126
- // permissions. Tracking known profile IDs is therefore NOT this
127
- // class's job; see ProfileRegistry (profileRegistry.js) for how the
128
- // CLI tracks "which profiles have I ever logged into" separately from
129
- // the keychain itself.
130
- throw new Error(
131
- "KeychainCredentialStore.listProfiles is not supported directly — " +
132
- "use ProfileRegistry to track known profile IDs, then look up each " +
133
- "one via get()."
134
- );
135
- }
136
- }
137
-
138
- function isNotFoundError(err) {
139
- const message = String(err?.message ?? "");
140
- return /no such|not found|nosuchkeyring|nosuchitem/i.test(message);
141
- }
142
-
143
- /**
144
- * RegistryBackedKeychainCredentialStore — combines KeychainCredentialStore
145
- * (actual secret storage) with ProfileRegistry (non-sensitive list of known
146
- * profile IDs) to satisfy the full CredentialStore interface, including
147
- * listProfiles(), which the raw keychain store alone cannot support.
148
- *
149
- * This is the class CLI wiring should actually construct for real use —
150
- * KeychainCredentialStore and ProfileRegistry are exported separately
151
- * mainly so each can be tested/reasoned about independently.
152
- */
153
- export class RegistryBackedKeychainCredentialStore extends CredentialStore {
154
- /**
155
- * @param {import("./ProfileRegistry.js").ProfileRegistry} profileRegistry
156
- */
157
- constructor(profileRegistry) {
158
- super();
159
- this._keychain = new KeychainCredentialStore();
160
- this._registry = profileRegistry;
161
- }
162
-
163
- async get(profileId) {
164
- return this._keychain.get(profileId);
165
- }
166
-
167
- async set(profileId, credentials) {
168
- await this._keychain.set(profileId, credentials);
169
- await this._registry.add(profileId); // registry write after secret write succeeds
170
- }
171
-
172
- async delete(profileId) {
173
- await this._keychain.delete(profileId);
174
- await this._registry.remove(profileId);
175
- }
176
-
177
- async listProfiles() {
178
- return this._registry.list();
179
- }
180
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * ProfileRegistry — tracks which profile IDs the user has ever logged into,
3
- * so the CLI can support `lixblogs auth status` (no profile given, meaning
4
- * "show me all of them") and profile-switching commands.
5
- *
6
- * This deliberately stores NOTHING sensitive — just profile names/IDs, not
7
- * tokens. It's fine for this to live in a plain config file on disk (not
8
- * the keychain), since profile *names* aren't a secret; only the
9
- * credentials tied to them are, and those live in CredentialStore.
10
- *
11
- * Rationale for why this exists at all: OS keychains generally don't
12
- * support "list all entries for this service" without extra permissions
13
- * (see KeychainCredentialStore.listProfiles, which explicitly throws and
14
- * points here instead). This registry is what makes listing/switching
15
- * profiles possible without asking the OS keychain to do something most
16
- * platforms don't reliably support.
17
- */
18
-
19
- import { promises as fs } from "node:fs";
20
- import path from "node:path";
21
- import os from "node:os";
22
-
23
- function defaultRegistryPath() {
24
- return path.join(os.homedir(), ".config", "lixblogs", "profiles.json");
25
- }
26
-
27
- export class ProfileRegistry {
28
- /** @param {string} [registryPath] */
29
- constructor(registryPath = defaultRegistryPath()) {
30
- this._path = registryPath;
31
- }
32
-
33
- /** @returns {Promise<string[]>} */
34
- async list() {
35
- return (await this._read()).profiles;
36
- }
37
-
38
- async getActive() {
39
- const data = await this._read();
40
- return data.activeProfile && data.profiles.includes(data.activeProfile)
41
- ? data.activeProfile
42
- : data.profiles[0] || null;
43
- }
44
-
45
- async setActive(profileId) {
46
- validateProfileId(profileId);
47
- const data = await this._read();
48
- if (!data.profiles.includes(profileId)) {
49
- throw new Error(`Profile "${profileId}" does not exist. Log in with it first.`);
50
- }
51
- await this._write(data.profiles, profileId);
52
- }
53
-
54
- async _read() {
55
- try {
56
- const raw = await fs.readFile(this._path, "utf8");
57
- const data = JSON.parse(raw);
58
- const profiles = Array.isArray(data.profiles)
59
- ? data.profiles.filter((profile) => typeof profile === "string")
60
- : [];
61
- return {
62
- profiles,
63
- activeProfile: typeof data.activeProfile === "string" ? data.activeProfile : null,
64
- };
65
- } catch (err) {
66
- if (err.code === "ENOENT") return { profiles: [], activeProfile: null };
67
- throw err;
68
- }
69
- }
70
-
71
- /** @param {string} profileId */
72
- async add(profileId) {
73
- validateProfileId(profileId);
74
- const data = await this._read();
75
- const profiles = new Set(data.profiles);
76
- profiles.add(profileId);
77
- await this._write([...profiles], data.activeProfile || profileId);
78
- }
79
-
80
- /** @param {string} profileId */
81
- async remove(profileId) {
82
- const data = await this._read();
83
- const profiles = data.profiles.filter((id) => id !== profileId);
84
- const activeProfile = data.activeProfile === profileId ? profiles[0] || null : data.activeProfile;
85
- await this._write(profiles, activeProfile);
86
- }
87
-
88
- async _write(profiles, activeProfile = null) {
89
- await fs.mkdir(path.dirname(this._path), { recursive: true });
90
- const temporaryPath = `${this._path}.${process.pid}.tmp`;
91
- await fs.writeFile(
92
- temporaryPath,
93
- JSON.stringify({ activeProfile, profiles }, null, 2),
94
- { encoding: "utf8", mode: 0o600 },
95
- );
96
- await fs.rename(temporaryPath, this._path);
97
- }
98
- }
99
-
100
- export function validateProfileId(profileId) {
101
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(profileId || "")) {
102
- throw new Error("Profile names must be 1-64 characters using letters, numbers, dot, dash, or underscore.");
103
- }
104
- return profileId;
105
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * config.js — resolves runtime config for the CLI.
3
- *
4
- * Per #135: "Config precedence: flags → environment → named profile →
5
- * defaults." This module implements that precedence for the small set of
6
- * values the auth commands need right now (environment, active profile,
7
- * API base URL placeholder for later). It's deliberately minimal — full
8
- * config-file/profile-file handling belongs to a later CLI-shell issue,
9
- * not this one.
10
- */
11
-
12
- const DEFAULTS = {
13
- environment: "production",
14
- profile: "default",
15
- accountsBaseUrl: "https://accounts.elixpo.com",
16
- apiBaseUrl: "https://blogs.elixpo.com",
17
- };
18
-
19
- const ENVIRONMENT_CLIENTS = {
20
- development: { clientId: "lixblogs-cli-dev", audience: "localhost" },
21
- staging: { clientId: "lixblogs-cli-staging", audience: "staging.blogs.elixpo.com" },
22
- production: { clientId: "lixblogs-cli-prod", audience: "blogs.elixpo.com" },
23
- test: { clientId: "lixblogs-cli-dev", audience: "localhost" },
24
- };
25
-
26
- /**
27
- * @param {Object} params
28
- * @param {Object} [params.flags] - parsed CLI flags, e.g. { profile, env }
29
- * @param {NodeJS.ProcessEnv} [params.env] - defaults to process.env
30
- * @returns {{ environment: string, profile: string, configAllowsProduction: boolean }}
31
- */
32
- export function resolveConfig({ flags = {}, env = process.env } = {}) {
33
- const environment =
34
- flags.env ?? env.LIXBLOGS_ENV ?? DEFAULTS.environment;
35
-
36
- const profile =
37
- flags.profile ?? env.LIXBLOGS_PROFILE ?? DEFAULTS.profile;
38
-
39
- const environmentClient = ENVIRONMENT_CLIENTS[environment] || ENVIRONMENT_CLIENTS.production;
40
- const authProvider =
41
- flags.authProvider ??
42
- env.LIXBLOGS_AUTH_PROVIDER ??
43
- (environment === "production" ? "elixpo" : "mock");
44
- const accountsBaseUrl =
45
- flags.accountsUrl ?? env.LIXBLOGS_ACCOUNTS_URL ?? DEFAULTS.accountsBaseUrl;
46
- const apiBaseUrl = flags.apiUrl ?? env.LIXBLOGS_API_URL ?? DEFAULTS.apiBaseUrl;
47
- const clientId = flags.clientId ?? env.LIXBLOGS_CLIENT_ID ?? environmentClient.clientId;
48
- const audience = flags.audience ?? env.LIXBLOGS_AUDIENCE ?? environmentClient.audience;
49
-
50
- return {
51
- environment,
52
- profile,
53
- profileExplicit: flags.profile !== undefined || env.LIXBLOGS_PROFILE !== undefined,
54
- authProvider,
55
- accountsBaseUrl,
56
- apiBaseUrl,
57
- clientId,
58
- audience,
59
- };
60
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * credentialStoreFactory.js
3
- *
4
- * Real OS-keychain storage is now wired in (KeychainCredentialStore, via
5
- * @napi-rs/keyring — see that file for why it was chosen over keytar).
6
- *
7
- * Per #135: "require an explicit opt-in fallback when unavailable." This
8
- * means: if the keychain genuinely can't be used (e.g. no Secret Service
9
- * running, headless environment), this factory does NOT silently fall
10
- * back to something less secure. It throws CredentialStoreUnavailableError
11
- * with a clear message; the CLI entry point is responsible for surfacing
12
- * that to the user and requiring an explicit --allow-insecure-fallback
13
- * flag (or equivalent) before constructing InMemoryCredentialStore for
14
- * real use. There is currently no non-memory fallback implementation
15
- * (e.g. encrypted file) — InMemoryCredentialStore does not persist between
16
- * runs, so using it as a "fallback" is only acceptable for local dev/testing,
17
- * not as a real opt-in fallback for end users. Building a real persistent
18
- * fallback (e.g. an encrypted-file-backed store) is out of scope here and
19
- * should be a follow-up if keychain unavailability turns out to be common
20
- * in practice.
21
- */
22
-
23
- import {
24
- GatedCredentialStore,
25
- InMemoryCredentialStore,
26
- CredentialStoreUnavailableError,
27
- } from "./CredentialStore.js";
28
- import {
29
- RegistryBackedKeychainCredentialStore,
30
- probeKeychainAvailability,
31
- } from "./KeychainCredentialStore.js";
32
- import { ProfileRegistry } from "./ProfileRegistry.js";
33
-
34
- /**
35
- * @param {{ allowInsecureFallback?: boolean }} [options]
36
- * @returns {Promise<import("./CredentialStore.js").CredentialStore>}
37
- */
38
- export async function createCredentialStore({ allowInsecureFallback = false, profileRegistry } = {}) {
39
- // Proactively probe availability with a real write+delete round-trip
40
- // rather than relying on get() to surface failures — see
41
- // probeKeychainAvailability's doc comment for why get() alone is not
42
- // trustworthy for this on every backend.
43
- const probe = await probeKeychainAvailability();
44
-
45
- if (!probe.available) {
46
- if (!allowInsecureFallback) {
47
- throw new CredentialStoreUnavailableError(
48
- `OS keychain is unavailable: ${probe.error}. Re-run with an explicit ` +
49
- `fallback flag if you want to opt in to a less secure storage method.`
50
- );
51
- }
52
- process.stderr.write(
53
- `warning: OS keychain unavailable (${probe.error}); using in-memory ` +
54
- `fallback because --allow-insecure-fallback was passed. Credentials ` +
55
- `will NOT persist between CLI runs.\n`
56
- );
57
- return new InMemoryCredentialStore();
58
- }
59
-
60
- const registry = profileRegistry || new ProfileRegistry();
61
- const realStore = new RegistryBackedKeychainCredentialStore(registry);
62
- return new GatedCredentialStore(realStore);
63
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * providerFactory.js — the single place an AuthProvider gets constructed.
3
- *
4
- * Every call site that needs an AuthProvider goes through here, not through
5
- * `new MockAuthProvider()` directly, so the production gate (see
6
- * ../auth/productionGate.js) is impossible to bypass by accident. This is
7
- * intentionally a single narrow chokepoint.
8
- */
9
-
10
- import { MockAuthProvider } from "../auth/MockAuthProvider.js";
11
- import { ElixpoAuthProvider } from "../auth/ElixpoAuthProvider.js";
12
- import { assertProviderAllowed } from "../auth/productionGate.js";
13
-
14
- /**
15
- * @param {{ environment: string, authProvider: string, accountsBaseUrl?: string, clientId?: string, audience?: string }} config
16
- * @returns {import("../auth/AuthProvider.js").AuthProvider}
17
- */
18
- export function createAuthProvider(config) {
19
- if (config.authProvider === "mock" && config.environment === "production") {
20
- throw new Error("The mock auth provider cannot run in production.");
21
- }
22
- if (config.authProvider !== "mock" && config.authProvider !== "elixpo") {
23
- throw new Error(`Unknown auth provider "${config.authProvider}".`);
24
- }
25
-
26
- const provider = config.authProvider === "mock"
27
- ? new MockAuthProvider()
28
- : new ElixpoAuthProvider({
29
- accountsBaseUrl: config.accountsBaseUrl,
30
- clientId: config.clientId,
31
- audience: config.audience,
32
- cliVersion: config.cliVersion || "1.2.0",
33
- fetchImpl: config.fetchImpl,
34
- });
35
-
36
- assertProviderAllowed({
37
- providerId: provider.providerId,
38
- environment: config.environment,
39
- });
40
-
41
- return provider;
42
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * redact.js — token redaction, used everywhere output could leak a
3
- * credential: console output, --json output, thrown errors, crash reports.
4
- *
5
- * Per #135: "Never print tokens in logs, JSON output, telemetry, or crash
6
- * reports." This is a hard requirement, tested in redact.test.mjs — not
7
- * just documented.
8
- */
9
-
10
- const REDACTED = "[REDACTED]";
11
-
12
- /**
13
- * Recursively walks an object/array and replaces any value under a key
14
- * matching a known sensitive-field name with REDACTED. Also redacts string
15
- * values that look like tokens even under an unrecognized key, as a
16
- * defense-in-depth measure (e.g. someone renames a field and forgets to
17
- * update this list).
18
- */
19
- const SENSITIVE_KEY_PATTERN = /token|refresh|secret|password|authorization/i;
20
-
21
- // Matches our mock token shapes (mock-access-*, mock-refresh-*) as well as
22
- // generic bearer-token-like strings, so redaction isn't solely dependent on
23
- // key names.
24
- const TOKEN_LIKE_VALUE_PATTERN = /^(mock-(access|refresh)-|Bearer\s+)\S+/i;
25
-
26
- export function redactValue(value) {
27
- if (typeof value === "string" && TOKEN_LIKE_VALUE_PATTERN.test(value)) {
28
- return REDACTED;
29
- }
30
- return value;
31
- }
32
-
33
- export function redactObject(input) {
34
- if (Array.isArray(input)) {
35
- return input.map((item) => redactObject(item));
36
- }
37
- if (input && typeof input === "object") {
38
- const out = {};
39
- for (const [key, value] of Object.entries(input)) {
40
- if (SENSITIVE_KEY_PATTERN.test(key)) {
41
- out[key] = REDACTED;
42
- } else if (typeof value === "object" && value !== null) {
43
- out[key] = redactObject(value);
44
- } else {
45
- out[key] = redactValue(value);
46
- }
47
- }
48
- return out;
49
- }
50
- return redactValue(input);
51
- }
52
-
53
- /**
54
- * Wraps JSON.stringify to redact sensitive fields before serialization.
55
- * Use this for --json output and any log line, never JSON.stringify directly
56
- * on data that might contain credentials.
57
- */
58
- export function safeJsonStringify(input, space) {
59
- return JSON.stringify(redactObject(input), null, space);
60
- }
61
-
62
- /**
63
- * Wraps an error for safe display/logging — strips any token-like content
64
- * from the error message itself, not just structured fields, since error
65
- * messages are free-form strings that could accidentally interpolate a
66
- * token (e.g. "failed to refresh mock-refresh-abc123").
67
- */
68
- export function redactErrorMessage(message) {
69
- if (typeof message !== "string") return message;
70
- return message.replace(
71
- /(mock-(access|refresh)-\S+|Bearer\s+\S+)/gi,
72
- REDACTED
73
- );
74
- }