@tinycloud/cli 1.1.0-beta.6 → 1.1.0-beta.8
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/dist/index.js +96 -20
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +146 -47
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +3 -3
- package/skills/tc-cli/AUTH.md +14 -1
- package/skills/tc-cli/REFERENCE.md +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinycloud/cli",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
3
|
+
"version": "1.1.0-beta.8",
|
|
4
4
|
"description": "TinyCloud CLI - manage data, delegations, and nodes from the terminal",
|
|
5
5
|
"author": "TinyCloud, Inc.",
|
|
6
6
|
"license": "EGPL",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@tinycloud/share-sdk": "1.1.0-beta.1",
|
|
33
33
|
"@tinycloud/share-envelope": "1.1.0-beta.0",
|
|
34
|
-
"@tinycloud/node-sdk": "3.1.0-beta.
|
|
34
|
+
"@tinycloud/node-sdk": "3.1.0-beta.5",
|
|
35
35
|
"@tinycloud/node-sdk-wasm": "1.7.6",
|
|
36
36
|
"@tinycloud/sdk-core": "3.1.0-beta.4",
|
|
37
|
-
"@tinycloud/operations": "0.
|
|
37
|
+
"@tinycloud/operations": "0.4.0-beta.8",
|
|
38
38
|
"chalk": "^5.3.0",
|
|
39
39
|
"commander": "^14.0.0",
|
|
40
40
|
"open": "^11.0.0",
|
package/skills/tc-cli/AUTH.md
CHANGED
|
@@ -23,7 +23,7 @@ TC="$(npm prefix --global)/bin/tc"
|
|
|
23
23
|
- Scoped and device logins refuse a profile that holds a local owner key (`LOCAL_OWNER_PROFILE`): a local-owner-key posture, `authMethod: "local"`, or a stored private key, whatever the recorded posture. Use a separate profile.
|
|
24
24
|
- A live, unexpired session is replaced only by a scope that keeps everything it holds for the same owner and lasts at least as long: renewing the same manifest, or widening a session the owner narrowed earlier. Anything else, including an approval the owner narrowed, one that signs a caveat (a restriction such as a tenant) onto an action the live session holds unrestricted, or a renewal that would end earlier, refuses with `SESSION_IN_USE` and saves nothing. Use a new profile; pass `--replace-session` only when replacing that session is intended. The check runs on the request before consent and again on the approved scope before saving.
|
|
25
25
|
- Approval can take minutes. Before saving, the CLI re-reads the profile, key and session under the profile lock; if another login, key rotation or logout changed them meanwhile, it refuses with `PROFILE_CHANGED_DURING_LOGIN` and saves nothing. The CLI's profile, key and session writers take the same lock, and a failed commit write restores all three before the lock is released. If that restore fails too, the login fails with `PROFILE_STATE_INCONSISTENT` naming each failure; a crash between writes surfaces the same code on the next scoped login; check `tc --profile PROFILE context`, then use `--replace-session` or a new profile. The commit waits up to 45 s for the lock (a crashed holder's lock is reclaimed after 30 s), else `PROFILE_LOCK_TIMEOUT`; other commands that time out on the lock report the same code. Wait for the other tc or MCP process and retry.
|
|
26
|
-
- The other profile writers follow the same discipline. Local `tc auth rotate` signs in first and replaces the session in that compare-and-commit, so a change meanwhile refuses with `PROFILE_CHANGED_DURING_LOGIN` and the previous session stays. `tc profile delete` waits for the profile lock and removes the profile's files while holding it; a name that is not one path segment fails with `INVALID_PROFILE_NAME`. When `tc auth import` bootstraps a delegate profile's first session, the bootstrap reads and writes under the lock and never replaces a session that appeared after the import checked for one (`PROFILE_CHANGED_DURING_IMPORT`); if the bootstrap itself fails, it is rolled back. If a request-bound import (an artifact with a `requestId`) then fails, its bootstrap is rolled back only if the profile still
|
|
26
|
+
- The other profile writers follow the same discipline. Local `tc auth rotate` signs in first and replaces the session in that compare-and-commit, so a change meanwhile refuses with `PROFILE_CHANGED_DURING_LOGIN` and the previous session stays. `tc profile delete` waits for the profile lock and removes the profile's files while holding it; a key, session, delegation or permission-request write that was waiting for the lock meanwhile then fails with `PROFILE_NOT_FOUND` instead of recreating the profile (see REFERENCE.md, Profile lock). A name that is not one path segment fails with `INVALID_PROFILE_NAME`. When `tc auth import` bootstraps a delegate profile's first session, the bootstrap reads and writes under the lock and never replaces a session that appeared after the import checked for one (`PROFILE_CHANGED_DURING_IMPORT`); if the bootstrap itself fails, it is rolled back. If a request-bound import (an artifact with a `requestId`) then fails, its bootstrap is rolled back only if the session and the profile's session DID and space still hold exactly what the bootstrap wrote (other profile changes made meanwhile, such as a new default space, are kept); if another login, logout or profile update replaced them, that newer state is kept and the error says so. Other import forms keep a successful bootstrap when a later step fails.
|
|
27
27
|
|
|
28
28
|
While waiting, the command writes one approval line to stderr and keeps polling for the whole approval window (about 10 minutes), riding out dropped connections:
|
|
29
29
|
|
|
@@ -100,6 +100,19 @@ tc --profile PROFILE auth request --manifest FILE --grant --device
|
|
|
100
100
|
|
|
101
101
|
`auth request --device` defaults to a 7-day lifetime (`--expiry` to change it, at most `30d`). Without `--device`, `--grant` opens the OpenKey browser flow. Browser grants require a signed proof for the profile's session key and owner. Only the permissions and expiry in that proof can be activated and stored; an older space-nested decrypt proof, missing or foreign-owner proof, broader grant, wrong space, or too-long expiry is refused without storing a delegation.
|
|
102
102
|
|
|
103
|
+
## Imported delegations and their request bindings
|
|
104
|
+
|
|
105
|
+
Delegations stored in a profile's `additional-delegations.json` are installed again on every later command. A compact-UCAN delegation is installed only through validated activation, and only when its record carries an `authorityRequest` binding whose `requested` capabilities contain everything the delegation's signed bytes grant. A delegation broader than its binding is refused before it is activated. The CLI, `tc secrets get`, and MCP tools all apply this rule. A stored record whose `delegationHeader` is anything but a single string `Authorization` installs nothing.
|
|
106
|
+
|
|
107
|
+
- **Request-bound import.** `tc auth import` of an artifact with a `requestId` (from `tc auth grant`, or MCP `tinycloud_auth_import`) binds the delegation to the stored request it was checked against. Importing a stored delegation again this way replaces its record and binding.
|
|
108
|
+
- **Import without a request.** `tc auth import` of a bare portable delegation, a stored record, or an envelope with no `requestId`, addressed to this profile's session key, is validated the same way and bound to exactly its own signed capabilities: request ID `unbound-import:<cid>`, with an `authorityRequestAudit` note. No request limits it: the profile gets the delegation's full signed authority, whatever the artifact's `permissions` say. To limit what a delegation may grant, import it against a request instead (`tc auth request --emit`, then `tc auth grant` by the owner, then `tc auth import` of the result). This route never replaces or weakens a binding already stored for the same CID. A delegation whose `delegationHeader` is anything but a single string `Authorization` is refused with `INVALID_AUTH_IMPORT`.
|
|
109
|
+
- **One-time migration.** Records stored by earlier releases have no binding. The first command that restores the profile's own session binds each one to exactly its own signed capabilities (`migrated:<cid>`, audit note `source: "migration"`) and writes `delegation-binding-migration.json` in the profile directory, listing the CIDs it bound and the ones it left unbound. A migrated record never grants more than it did before. A record that cannot be bound (expired, for another session key, malformed, or signing capabilities no request can hold) stays unbound and installs nothing afterwards; the others are still migrated. Migration is best-effort and restartable: it runs under the profile lock, only while the stored session is still the one the command restored, writes the bound records first and the marker last, and a command that is interrupted or finds the lock busy leaves the rest to the next command. Bindings already written are kept.
|
|
110
|
+
- **After migration**, a compact-UCAN record without a valid binding installs nothing. For example, a delegation an older release imported, or a record pasted into the file. To recover it, run `tc auth import` on the artifact again with this release, or import a request-bound artifact for it.
|
|
111
|
+
- Older releases ignore the new fields. They do not enforce bindings, and an older `tc auth import` of a CID that is already stored replaces its record with an unbound one, which this release then refuses until it is imported again.
|
|
112
|
+
- Signed-login grants stored by `tc auth request --grant` and secret-read approvals are installed by the CLI as before, and a new grant never replaces a stored record for the same CID that carries a binding. A signed-login record that carries an `authorityRequest` but no `siweProof` cannot be held to its binding and installs nothing. The operations runtime behind MCP tools and `tc secrets get` never installed them and still does not. A cross-user delegation (addressed to the profile's identity rather than its session key) is not installed at all; `secrets get --delegation` reads it directly.
|
|
113
|
+
|
|
114
|
+
The binding is local profile data. It stops records written by other paths from granting authority. It does not stop someone who can write the profile directory, who already holds the session key and the signed bytes.
|
|
115
|
+
|
|
103
116
|
## Unscoped login and sign-out
|
|
104
117
|
|
|
105
118
|
`tc init` and `tc auth login` without a manifest request broad default-space consent, including writes, and replace the profile's session as before. Prefer `init --key-only` followed by a scoped login. They still verify OpenKey's signed proof whenever it carries one, the profile records an owner, or `--expiry` is given, and they keep the recorded owner. Non-interactive `auth login` never switches to device mode on its own, and never waits on a browser that cannot open: without `--device --manifest`, `--paste` or `--no-popup` it fails fast with `INTERACTIVE_LOGIN_REQUIRED`.
|
|
@@ -80,6 +80,8 @@ tc delegation revoke <cid>
|
|
|
80
80
|
|
|
81
81
|
Actions are auto-prefixed with `tinycloud.` if not already.
|
|
82
82
|
|
|
83
|
+
`tc auth import` stores delegations in the profile's `additional-delegations.json`; a compact-UCAN delegation addressed to the profile's session key is stored with an `authorityRequest` binding. Later commands install such a delegation only when its signed capabilities fit inside that binding. [AUTH.md](AUTH.md#imported-delegations-and-their-request-bindings) describes how each import route binds a delegation, the one-time migration of records from earlier releases, and how to recover a record that installs nothing.
|
|
84
|
+
|
|
83
85
|
## Spaces
|
|
84
86
|
|
|
85
87
|
```bash
|
|
@@ -225,14 +227,32 @@ KV keys cannot contain spaces or control characters: the SDK sends keys unescape
|
|
|
225
227
|
```
|
|
226
228
|
~/.tinycloud/ # 0700
|
|
227
229
|
├── config.json # Global config (defaultProfile)
|
|
230
|
+
├── profile-locks/
|
|
231
|
+
│ └── {name}/ # Turn lock (see below); kept after `tc profile delete`
|
|
228
232
|
└── profiles/
|
|
229
233
|
└── {name}/ # 0700
|
|
230
234
|
├── profile.json # Host, DID, chainId, ownerDid, spaceId (0600)
|
|
231
235
|
├── key.json # Ed25519 JWK keypair (0600)
|
|
232
236
|
├── session.json # Delegation, spaceId (0600)
|
|
237
|
+
├── additional-delegations.json # Imported and granted delegations, with request bindings (0600)
|
|
238
|
+
├── auth-requests.json # Stored permission requests (0600)
|
|
239
|
+
├── delegation-binding-migration.json # Present once unbound records were migrated (0600)
|
|
240
|
+
├── .lock/ # Lock shared with older releases, while held
|
|
233
241
|
└── cache/
|
|
234
242
|
```
|
|
235
243
|
|
|
244
|
+
### Profile lock
|
|
245
|
+
|
|
246
|
+
Every write to a profile (key, session, settings, delegations, permission requests) runs under the profile's lock, shared by `tc` and the MCP server. Processes of this release first take a turn in `profile-locks/{name}/`, then the `profiles/{name}/.lock` directory every release uses, so they keep excluding CLI 1.0.0-beta.16 and older (which hold `.lock` from its creation) and 1.0.0-beta.17 … 1.0.1-beta.4 (which hold it once `owner.json` is linked in), and those exclude them. Among processes of this release, mutual exclusion holds however long any of them is paused, and needs no hard links (FAT/exFAT and SMB mounts work).
|
|
247
|
+
|
|
248
|
+
That guarantee assumes every process using the profile runs on the same host and in the same PID namespace (a crashed process is recognised by its PID being gone), and a filesystem whose `rename` of a directory fails when the target exists. It does not hold for a `TC_HOME` shared across machines (NFS) or between containers that do not share a PID namespace: there a live process can look crashed, have its turn or staging directory removed, and the lock can be taken twice or stop working. Older releases running at the same time keep their own limits: a 1.0.0-beta.16-or-older process paused for longer than 30 s right after creating `.lock`, or a 1.0.0-beta.17 … 1.0.1-beta.4 process reclaiming `.lock` at the same moment as such a process, can still let two writers in. CLI 1.0.0-beta.16 and older never reclaim an abandoned `.lock` without an owner record; a newer release does, once it is 30 s old.
|
|
249
|
+
|
|
250
|
+
A crashed holder's lock is reclaimed automatically (`.lock` once 30 s old). With hard links, dead-owner recovery keeps a `.recover-*` claim inside `.lock` until it finishes; it never unlinks or moves a newly published live owner, and a failed claim never removes another writer's empty `.lock`. A crashed current-release recoverer may leave only a `.recover-*` claim. Releases through 1.0.1-beta.4 cannot reclaim it even after it ages. A current-release command that acquires the affected profile's lock processes the claim, restoring a live owner's record if it contains one.
|
|
251
|
+
|
|
252
|
+
On either recovery path, an orphaned `.recover-*` claim can hold a replacement owner's live record: the hard-link path may claim it before detecting a mismatch, and the no-hard-links path may move it before putting it back. If the recoverer crashes and that holder releases while its process stays alive, the next acquisition restores the claimed record as `owner.json`. Acquisition can then remain blocked until the PID exits **and** the record is at least 30 s old—for a long-lived MCP server, possibly until it restarts. This costs availability only; it never admits a second holder. To clear the stalled profile manually, remove its `profiles/{name}/.lock` **only when no `tc` or MCP process is running**.
|
|
253
|
+
|
|
254
|
+
`PROFILE_LOCK_TIMEOUT` naming `profile-locks/{name}` means its turn lock is damaged (for example by a disk error): when no `tc` or MCP process is using that profile, remove `~/.tinycloud/profile-locks/{name}`; it is recreated on the next write. A write that waited for the lock while `tc profile delete` removed the profile fails with `PROFILE_NOT_FOUND` instead of recreating it.
|
|
255
|
+
|
|
236
256
|
## DID Formats
|
|
237
257
|
|
|
238
258
|
- **Session key**: `did:key:z6Mk...#z6Mk...` — generated at init
|