@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 +86 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/THREAT_MODEL.md +91 -0
- package/bin/lixblogs.mjs +490 -0
- package/package.json +71 -0
- package/src/api/BlogClient.js +135 -0
- package/src/auth/AuthProvider.js +90 -0
- package/src/auth/AuthenticatedClient.js +116 -0
- package/src/auth/ElixpoAuthProvider.js +281 -0
- package/src/auth/MockAuthProvider.js +170 -0
- package/src/auth/productionGate.js +44 -0
- package/src/commands/auth/login.js +103 -0
- package/src/commands/auth/logout.js +21 -0
- package/src/commands/auth/profiles.js +27 -0
- package/src/commands/auth/revoke.js +45 -0
- package/src/commands/auth/status.js +33 -0
- package/src/commands/blog/index.js +81 -0
- package/src/commands/blog/input.js +59 -0
- package/src/config/CredentialStore.js +142 -0
- package/src/config/KeychainCredentialStore.js +180 -0
- package/src/config/ProfileRegistry.js +105 -0
- package/src/config/config.js +60 -0
- package/src/config/credentialStoreFactory.js +63 -0
- package/src/config/providerFactory.js +42 -0
- package/src/config/redact.js +74 -0
- package/src/content/markdown.js +68 -0
- package/src/content/validate.js +45 -0
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.
|
package/THREAT_MODEL.md
ADDED
|
@@ -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.
|