@elixpo/lixblogs-cli 1.1.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.
package/API.md ADDED
@@ -0,0 +1,86 @@
1
+ # LixBlogs API v1 contract
2
+
3
+ The CLI is a public OAuth client. Accounts issues credentials; LixBlogs is the
4
+ resource server. The CLI must never connect to D1 or carry a client secret.
5
+
6
+ ## Origins and discovery
7
+
8
+ - OAuth issuer: `https://accounts.elixpo.com`
9
+ - Token audience: `blogs.elixpo.com`
10
+ - Resource root: `https://blogs.elixpo.com/api/v1`
11
+ - Contract metadata: `GET /api/v1`
12
+
13
+ Production bearer tokens are EdDSA access tokens issued to an allowlisted CLI
14
+ client. LixBlogs verifies their signature, expiry, audience, client, scopes,
15
+ and local account before querying creator data. Tokens are never persisted or
16
+ logged by the resource API.
17
+
18
+ ## Initial resources
19
+
20
+ | Method | Path | Scope | Behavior |
21
+ | --- | --- | --- | --- |
22
+ | `GET` | `/api/v1` | public | API and compatibility metadata |
23
+ | `GET` | `/api/v1/blogs` | `lixblogs:blog:read` | Accessible blog metadata |
24
+ | `GET` | `/api/v1/blogs/{id}` | `lixblogs:blog:read` | One accessible blog and its content |
25
+ | `POST` | `/api/v1/blogs` | `lixblogs:blog:write` | Create a draft |
26
+ | `PATCH` | `/api/v1/blogs/{id}` | `lixblogs:blog:write` | Edit a draft or post |
27
+ | `POST` | `/api/v1/blogs/{id}/publish` | `lixblogs:blog:publish` | Publish a post |
28
+ | `POST` | `/api/v1/blogs/{id}/unpublish` | `lixblogs:blog:publish` | Return a post to draft |
29
+ | `DELETE` | `/api/v1/blogs/{id}` | `lixblogs:blog:delete` | Move a post to trash |
30
+ | `POST` | `/api/v1/blogs/{id}/restore` | `lixblogs:blog:delete` | Restore a trashed post |
31
+
32
+ `GET /api/v1/blogs` accepts `status=all|draft|published`, `limit=1..100`, and
33
+ an opaque `cursor`. Results include authored blogs, accepted collaborations,
34
+ and blogs belonging to organizations of which the caller is a member. Access
35
+ misses return `404` so resource existence is not disclosed.
36
+
37
+ Permanent deletion uses `DELETE /api/v1/blogs/{id}?permanent=true`. It requires
38
+ the additional `lixblogs:blog:delete:permanent` scope and an
39
+ `X-Confirm-Permanent-Delete` header equal to the blog ID.
40
+
41
+ ## Response shape
42
+
43
+ Successful responses use:
44
+
45
+ ```json
46
+ { "data": {}, "meta": {} }
47
+ ```
48
+
49
+ Errors use a stable machine-readable envelope:
50
+
51
+ ```json
52
+ {
53
+ "error": {
54
+ "code": "invalid_token",
55
+ "message": "The access token is malformed.",
56
+ "requestId": "..."
57
+ }
58
+ }
59
+ ```
60
+
61
+ Every response carries `X-LixBlogs-API-Version` and `X-Request-ID`. Authenticated
62
+ responses also expose bounded per-minute `X-RateLimit-*` values. Clients should
63
+ use error codes rather than matching human-readable messages.
64
+
65
+ ## Concurrency and retries
66
+
67
+ Single-blog reads include a strong `ETag`. Future write endpoints require
68
+ `If-Match` so a stale CLI cannot silently overwrite a newer browser edit.
69
+
70
+ Mutation requests use an `Idempotency-Key` containing 8–128 URL-safe
71
+ characters. Reservations live for 24 hours:
72
+
73
+ - same key and same request: replay the retained response;
74
+ - same key while running: `idempotency_in_progress`;
75
+ - same key with different input: `idempotency_key_reused`.
76
+
77
+ Operational rows are pruned in bounded batches. Audit events record the caller,
78
+ client, action, resource, outcome, and request ID, never bearer material.
79
+
80
+ ## Compatibility policy
81
+
82
+ The `/api/v1` URL and envelope are stable for the v1 lifetime. Fields may be
83
+ added without a major version change; existing fields and error codes are not
84
+ removed or redefined. A breaking change requires `/api/v2`. The metadata
85
+ endpoint advertises `minCliVersion`; clients below it must stop before making
86
+ authenticated requests and present an upgrade instruction.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elixpo (Ayushman Bhattacharya)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @elixpo/lixblogs-cli
2
+
3
+ The official CLI for LixBlogs — publish, manage, and inspect blogs through
4
+ the supported API. Built for creators and agent/automation use.
5
+
6
+ **Status: initial release.** This package implements production device-flow
7
+ authentication and the core blog lifecycle over the stable LixBlogs API v1
8
+ contract. The interactive terminal UI is intentionally a separate follow-up.
9
+
10
+ ## Install (local development)
11
+
12
+ ```bash
13
+ cd packages/lixblogs-cli
14
+ npm install
15
+ ```
16
+
17
+ There's no published npm release yet. Once available, install will be:
18
+ ```bash
19
+ npm install -g @elixpo/lixblogs-cli
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ node bin/lixblogs.mjs --help
26
+ ```
27
+
28
+ ### Authentication
29
+
30
+ ```bash
31
+ # Log in via device authorization
32
+ node bin/lixblogs.mjs auth login
33
+
34
+ # Check login status
35
+ node bin/lixblogs.mjs auth status
36
+
37
+ # List profiles and choose the active one
38
+ node bin/lixblogs.mjs auth profiles
39
+ node bin/lixblogs.mjs auth use work
40
+
41
+ # Log out (clears local credentials only)
42
+ node bin/lixblogs.mjs auth logout
43
+
44
+ # Revoke the token server-side and clear local credentials (destructive)
45
+ node bin/lixblogs.mjs auth revoke --yes
46
+ ```
47
+
48
+ ### Blog lifecycle
49
+
50
+ Request the permissions needed for the operations you intend to use:
51
+
52
+ ```bash
53
+ node bin/lixblogs.mjs auth login \
54
+ --scope openid --scope profile --scope lixblogs:blog:read \
55
+ --scope lixblogs:blog:write --scope lixblogs:blog:publish \
56
+ --scope lixblogs:blog:delete
57
+ ```
58
+
59
+ Then work with Markdown without any database or Cloudflare credentials:
60
+
61
+ ```bash
62
+ lixblogs blog list --status draft
63
+ lixblogs blog create --file post.md --title "A new post" --tag engineering
64
+ lixblogs blog get <id> --json
65
+ lixblogs blog edit <id> --editor
66
+ lixblogs blog publish <id>
67
+ lixblogs blog unpublish <id>
68
+ lixblogs blog delete <id> --yes
69
+ lixblogs blog list --status trashed
70
+ lixblogs blog restore <id>
71
+ ```
72
+
73
+ `create`, `edit`, `publish`, `unpublish`, `delete`, and `restore` accept
74
+ `--dry-run`. Content input is mutually exclusive: `--file`, `--stdin`,
75
+ `--content`, or `--editor`. Permanent deletion additionally requires
76
+ `--permanent --yes` and the `lixblogs:blog:delete:permanent` scope.
77
+
78
+ Edits use the server ETag automatically. If another editor wins the race, the
79
+ command exits with code 3 and retains both versions under
80
+ `.lixblogs-conflicts/`; it never overwrites the newer server revision.
81
+
82
+ Global flags:
83
+ - `--profile <name>` — named profile to use (default: `"default"`)
84
+ - `--env <environment>` — override environment (`development` | `staging` | `production`)
85
+ - `--scope <scope>` — request an additional/alternate OAuth scope; repeatable
86
+ - `--open` — open the verification URL with the device code pre-filled
87
+ - `--accounts-url <url>` — override the Accounts issuer for local/staging tests
88
+ - `--api-url <url>` — override the LixBlogs API origin; production defaults to
89
+ `https://blogs.elixpo.com`
90
+ - `--json` — machine-readable JSON output
91
+ - `--quiet` — suppress non-essential output
92
+ - `--yes`, `-y` — auto-confirm destructive actions (required for `revoke`)
93
+ - `--allow-insecure-fallback` — explicit opt-in: if the OS keychain is
94
+ unavailable, use a non-persistent in-memory store instead of failing
95
+
96
+ ### Service boundary
97
+
98
+ - `https://accounts.elixpo.com` issues, refreshes, and revokes OAuth tokens.
99
+ - `https://blogs.elixpo.com/api/v1` is the only production resource API.
100
+ - The CLI discovers Accounts endpoints before login and rejects incompatible
101
+ contract versions or endpoints on an unexpected origin.
102
+ - The mock provider is available only with an explicit non-production
103
+ environment, for example `--env development --auth-provider mock`.
104
+ - The resource contract, scopes, pagination, errors, and mutation guarantees
105
+ are documented in [API.md](API.md).
106
+
107
+ The production client is public and has no client secret. Never add one to
108
+ CLI configuration, package files, or GitHub secrets.
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ npm test # runs the full CLI test suite
114
+ ```
115
+
116
+ Tests exercise both a mocked auth provider and, where relevant, the real
117
+ OS keychain backend on whatever machine runs them — see
118
+ `THREAT_MODEL.md` and inline comments in `src/config/KeychainCredentialStore.js`
119
+ for known platform-specific behavior (e.g. a documented WSL/keyring-rs quirk).
120
+
121
+ ## Architecture
122
+
123
+ ```
124
+ bin/lixblogs.mjs CLI entry point (Node's native util.parseArgs, no
125
+ third-party parsing dependency)
126
+ src/auth/ Accounts provider, development mock, refresh-safe
127
+ authenticated client, and production safety gate
128
+ src/commands/auth/ Command logic (login, status, logout, revoke) —
129
+ framework-agnostic, testable independently of the CLI shell
130
+ src/commands/blog/ Blog lifecycle commands and Markdown/editor input
131
+ src/api/ Versioned LixBlogs resource client and stable errors
132
+ src/content/ Dependency-free Markdown/block conversion
133
+ src/config/ Credential storage (real keychain + gated fallback),
134
+ profile registry, config resolution, token redaction
135
+ tests/ Full test suite
136
+ THREAT_MODEL.md Security threat model for the auth system
137
+ ```
138
+
139
+ Command logic under `src/commands/` is deliberately decoupled from the CLI
140
+ parsing layer in `bin/`, so the parser (or any other interface built on top
141
+ of these commands later) can change without touching command logic or its
142
+ tests.
143
+
144
+ ## Roadmap
145
+
146
+ See [#135](https://github.com/elixpo/blogs.elixpo/issues/135) for the full
147
+ scope. Rough remaining order:
148
+
149
+ 1. Media, organization, and stats commands
150
+ 2. Packaging and release automation
151
+ 3. Interactive terminal UI and branding in a separate issue
152
+ 4. Agent skill packages and cross-repository E2E coverage
153
+
154
+ ## Contributing
155
+
156
+ This package is part of the [blogs.elixpo](https://github.com/elixpo/blogs.elixpo)
157
+ monorepo. See the root repository's contribution guidelines.
@@ -0,0 +1,91 @@
1
+ # LixBlogs CLI — Auth Threat Model
2
+
3
+ Tracking: elixpo/blogs.elixpo#137
4
+ Status: **Resolved by implementer** — this doc's existence and scope was an
5
+ open question on #137 ("should a threat model be written as part of this
6
+ issue, and what should it contain?"). Decision: yes, scoped to the five
7
+ areas #137 itself named. Flagged for the maintainer to expand or narrow.
8
+
9
+ ## 1. Token storage
10
+
11
+ - Access and refresh tokens are stored in the OS keychain
12
+ (Keychain on macOS, Secret Service/libsecret on Linux, Credential Manager
13
+ on Windows) — never in a plain config file or environment variable by
14
+ default.
15
+ - A fallback (e.g. encrypted file on disk) is only used if the keychain is
16
+ unavailable, and only with explicit user opt-in at that moment — never a
17
+ silent default.
18
+ - Redaction: tokens must never appear in logs, `--json` output, telemetry,
19
+ or crash reports. This is verified by test (see `auth.test.mjs`), not
20
+ just documented — string values matching known token prefixes should be
21
+ masked in any error-serialization path.
22
+ - Risk if this fails: a leaked token grants whatever scopes it holds until
23
+ revoked or expired — this is why short-lived access tokens (with refresh
24
+ rotation) matter more than keychain storage alone.
25
+
26
+ ## 2. Scope boundaries
27
+
28
+ - Scopes are least-privilege by default; publishing and destructive actions
29
+ require scopes distinct from read/draft scopes (per #135).
30
+ - Accounts publishes the registered LixBlogs scope list. Login defaults to
31
+ identity plus profile/blog read scopes; broader scopes must be requested
32
+ explicitly and remain bounded by the public client's registration.
33
+ - Risk: an overly broad default scope grant (e.g. `login` implicitly
34
+ granting `publish`) would mean any compromised session can publish
35
+ without the user having explicitly consented to that. Mitigation: the
36
+ CLI must request only the scopes a given command needs, not a blanket
37
+ "everything" scope at login time.
38
+
39
+ ## 3. Blast radius of a compromised agent/CLI session
40
+
41
+ - Worst case if a token is stolen or an agent is compromised: whatever
42
+ scopes that specific token holds, until it's revoked or naturally
43
+ expires (access tokens are short-lived; refresh tokens are the more
44
+ valuable target).
45
+ - Mitigations already in scope: `lixblogs auth revoke`, tested refresh
46
+ rotation/reuse rejection, and multi-profile isolation (below).
47
+ - Not yet mitigated / open for later work: there's no mechanism yet for a
48
+ user to see "which sessions/devices currently hold a valid token for my
49
+ account" and revoke just one — this would materially reduce blast radius
50
+ and is worth a follow-up issue, not blocking this one.
51
+
52
+ ## 4. Device-flow-specific risks
53
+
54
+ - **Code interception**: the user code is short and meant to be read aloud/
55
+ typed manually; the device code (long, unguessable) is what's actually
56
+ exchanged for a token. If a device code leaks (e.g. via a compromised
57
+ clipboard), same blast radius as a stolen token.
58
+ - **Polling abuse**: a malicious client could poll rapidly to try to beat
59
+ the user to approving/denying. Mitigated by `pollIntervalSeconds` and the
60
+ `slow_down` response (see MockAuthProvider) which forces callers to back
61
+ off — the real provider is expected to enforce this server-side too, not
62
+ just suggest it.
63
+ - **Expired/denied code handling**: both must be treated as fully dead ends
64
+ — no retry-with-same-code path. The mock encodes this by design (an
65
+ expired/denied device code never later returns `approved`).
66
+ - **Reused-code replay**: an already-approved device code should not be
67
+ usable to fetch a second, independent token. Not yet tested in the mock
68
+ — worth adding once the "one device code → one token exchange" contract
69
+ is confirmed for the real provider, since the mock doesn't currently
70
+ invalidate a code after issuing a token for it.
71
+
72
+ ## 5. Multi-profile / account isolation
73
+
74
+ - Each named profile's tokens must be stored under a distinct keychain
75
+ entry — one profile's credential lookup must never return another
76
+ profile's token, even if both are logged in simultaneously.
77
+ - Switching the active profile must not require re-entering credentials for
78
+ profiles already logged in, but must also never leak which other profiles
79
+ exist to a scope that shouldn't know (e.g. `--json` output for one
80
+ profile shouldn't enumerate other profiles' identifiers).
81
+ - The active profile is non-sensitive registry metadata. Each profile's
82
+ credentials remain isolated in its own OS-keychain entry, and concurrent
83
+ refreshes for one profile share a single rotation operation.
84
+
85
+ ## Explicitly out of scope for this document
86
+
87
+ - Threat modeling the API server itself (auth middleware, D1 access
88
+ patterns) — that's elixpo/blogs.elixpo#136's concern.
89
+ - Supply-chain risk on the npm package itself (dependency compromise,
90
+ publish-time integrity) — worth a separate doc once packaging/release
91
+ work (later phase) is underway.