@tinycloud/cli 1.4.0 → 1.5.0-beta.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.
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +485 -105
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +4 -4
- package/skills/tc-cli/REFERENCE.md +1 -1
package/dist/legacy-entry.js
CHANGED
|
@@ -9049,7 +9049,8 @@ async function assertPendingRemoteLogoutSession(name, expected) {
|
|
|
9049
9049
|
}
|
|
9050
9050
|
async function clearProfileCredentials(name, syncDirectory = false, legacyCleanup = false) {
|
|
9051
9051
|
const profile = await ProfileManager.getProfile(name);
|
|
9052
|
-
|
|
9052
|
+
const { namedKeys: _namedKeys, ...signedOutProfile } = profile;
|
|
9053
|
+
await ProfileManager.setProfileDurable(name, { ...signedOutProfile, authSignedOut: true, ...legacyCleanup ? { legacyReplicaCleanup: true } : {} });
|
|
9053
9054
|
let failed = false;
|
|
9054
9055
|
let error;
|
|
9055
9056
|
try {
|
|
@@ -9481,7 +9482,8 @@ async function bootstrapDelegatedSession(ctx, delegation) {
|
|
|
9481
9482
|
jwk,
|
|
9482
9483
|
verificationMethod: sessionDid
|
|
9483
9484
|
};
|
|
9484
|
-
const
|
|
9485
|
+
const { namedKeys: _namedKeys, ...previousFields } = previousProfile;
|
|
9486
|
+
const profile = { ...previousFields, sessionDid, spaceId: delegation.spaceId };
|
|
9485
9487
|
try {
|
|
9486
9488
|
await ProfileManager.setSession(ctx.profile, session);
|
|
9487
9489
|
await ProfileManager.setProfile(ctx.profile, profile);
|
|
@@ -30611,7 +30613,7 @@ function createEncryptedProfileHistory(profileName2, sessionSigner) {
|
|
|
30611
30613
|
throw new ShareHistoryReadError();
|
|
30612
30614
|
}
|
|
30613
30615
|
};
|
|
30614
|
-
const
|
|
30616
|
+
const parseEnvelope2 = (text) => {
|
|
30615
30617
|
let envelope;
|
|
30616
30618
|
try {
|
|
30617
30619
|
envelope = JSON.parse(text);
|
|
@@ -30633,7 +30635,7 @@ function createEncryptedProfileHistory(profileName2, sessionSigner) {
|
|
|
30633
30635
|
const preparedSalt = async (profile) => {
|
|
30634
30636
|
const file = await readIfPresent(await path(profile));
|
|
30635
30637
|
if (file === void 0) return crypto.getRandomValues(new Uint8Array(16));
|
|
30636
|
-
const salt = unb64(
|
|
30638
|
+
const salt = unb64(parseEnvelope2(file.toString("utf8")).kdfSalt);
|
|
30637
30639
|
if (salt.length < 16) throw new ShareHistoryUnreadableError();
|
|
30638
30640
|
return salt;
|
|
30639
30641
|
};
|
|
@@ -30644,7 +30646,7 @@ function createEncryptedProfileHistory(profileName2, sessionSigner) {
|
|
|
30644
30646
|
if (legacy === void 0 || legacy.length <= 12) return [];
|
|
30645
30647
|
return decryptRecords(ready.legacyKey, new Uint8Array(legacy.subarray(0, 12)), new Uint8Array(legacy.subarray(12)));
|
|
30646
30648
|
}
|
|
30647
|
-
const envelope =
|
|
30649
|
+
const envelope = parseEnvelope2(file.toString("utf8"));
|
|
30648
30650
|
if (envelope.kdfSalt !== b64(ready.salt)) throw saltChanged;
|
|
30649
30651
|
const iv = unb64(envelope.iv);
|
|
30650
30652
|
const ciphertext = unb64(envelope.ciphertext);
|
|
@@ -31281,9 +31283,16 @@ async function localKeySignIn(options) {
|
|
|
31281
31283
|
privateKey: options.privateKey,
|
|
31282
31284
|
host: options.host,
|
|
31283
31285
|
autoCreateSpace: true,
|
|
31286
|
+
...options.keyAbilities ? { additionalRawAbilities: options.keyAbilities } : {},
|
|
31284
31287
|
...replication ? { replication } : {}
|
|
31285
31288
|
});
|
|
31286
31289
|
await node.signIn();
|
|
31290
|
+
let networkReady;
|
|
31291
|
+
if (options.keyAbilities && !node.freshEncryptionNetworkCreated) {
|
|
31292
|
+
networkReady = await node.ensureEncryptionNetwork().then(() => true, () => false);
|
|
31293
|
+
} else if (options.keyAbilities) {
|
|
31294
|
+
networkReady = true;
|
|
31295
|
+
}
|
|
31287
31296
|
const address = await new PrivateKeySigner(options.privateKey).getAddress();
|
|
31288
31297
|
const session = node.session;
|
|
31289
31298
|
if (!session) {
|
|
@@ -31299,7 +31308,8 @@ async function localKeySignIn(options) {
|
|
|
31299
31308
|
verificationMethod: session.verificationMethod,
|
|
31300
31309
|
siwe: session.siwe,
|
|
31301
31310
|
signature: session.signature,
|
|
31302
|
-
...node.replicaStamp === void 0 ? {} : { replicaStamp: node.replicaStamp }
|
|
31311
|
+
...node.replicaStamp === void 0 ? {} : { replicaStamp: node.replicaStamp },
|
|
31312
|
+
...networkReady === void 0 ? {} : { networkReady }
|
|
31303
31313
|
};
|
|
31304
31314
|
}
|
|
31305
31315
|
|
|
@@ -31638,13 +31648,87 @@ function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
|
|
|
31638
31648
|
return { session, legacyNested };
|
|
31639
31649
|
}
|
|
31640
31650
|
|
|
31651
|
+
// src/auth/login-commit.ts
|
|
31652
|
+
import { lstat as lstat6 } from "fs/promises";
|
|
31653
|
+
|
|
31654
|
+
// src/lib/named-keys.ts
|
|
31655
|
+
init_constants();
|
|
31656
|
+
init_errors();
|
|
31657
|
+
import { extractRecapAttenuations } from "@tinycloud/sdk-core";
|
|
31658
|
+
import {
|
|
31659
|
+
EncryptionResourceError,
|
|
31660
|
+
KEY_CREATE_ABILITY,
|
|
31661
|
+
formatPrefixResource,
|
|
31662
|
+
nodeSupportsNamedKeys
|
|
31663
|
+
} from "@tinycloud/sdk-services/encryption/resource";
|
|
31664
|
+
import { DECRYPT_ACTION } from "@tinycloud/sdk-services";
|
|
31665
|
+
var NAMED_KEYS_NETWORK = "default";
|
|
31666
|
+
function namedKeysNetworkId(ownerDid) {
|
|
31667
|
+
return `urn:tinycloud:encryption:${ownerDid}:${NAMED_KEYS_NETWORK}`;
|
|
31668
|
+
}
|
|
31669
|
+
function parseKeysPrefix(input, ownerDid) {
|
|
31670
|
+
const prefix = input.endsWith("/") ? input.slice(0, -1) : input;
|
|
31671
|
+
const invalid3 = (reason) => new CLIError("INVALID_KEY_PREFIX", `Invalid \`--keys\` folder "${input}": ${reason}`, ExitCode.USAGE_ERROR);
|
|
31672
|
+
if (prefix.length === 0) throw invalid3("the folder can't be empty.");
|
|
31673
|
+
if (prefix === "apps" || prefix.startsWith("apps/")) {
|
|
31674
|
+
throw invalid3("folders under `apps/` belong to apps and can't be used from the CLI.");
|
|
31675
|
+
}
|
|
31676
|
+
try {
|
|
31677
|
+
return { prefix, resource: formatPrefixResource(namedKeysNetworkId(ownerDid), prefix) };
|
|
31678
|
+
} catch (error) {
|
|
31679
|
+
if (error instanceof EncryptionResourceError) throw invalid3(error.message);
|
|
31680
|
+
throw error;
|
|
31681
|
+
}
|
|
31682
|
+
}
|
|
31683
|
+
function keyAbilitiesFor(keys) {
|
|
31684
|
+
return { [keys.resource]: [KEY_CREATE_ABILITY, DECRYPT_ACTION] };
|
|
31685
|
+
}
|
|
31686
|
+
function keyPathInPrefix(prefix, path) {
|
|
31687
|
+
return path.startsWith(`${prefix}/`) && path.length > prefix.length + 1;
|
|
31688
|
+
}
|
|
31689
|
+
async function assertNodeSupportsNamedKeys(host, fetchImpl = fetch) {
|
|
31690
|
+
let info = null;
|
|
31691
|
+
try {
|
|
31692
|
+
const response = await fetchImpl(`${host}/info`);
|
|
31693
|
+
if (response.ok) info = await response.json();
|
|
31694
|
+
} catch {
|
|
31695
|
+
info = null;
|
|
31696
|
+
}
|
|
31697
|
+
if (!nodeSupportsNamedKeys(info)) {
|
|
31698
|
+
throw new CLIError(
|
|
31699
|
+
"NAMED_KEYS_UNAVAILABLE",
|
|
31700
|
+
`${host} doesn't support named keys, so \`--keys\` can't be used there. Nothing was signed.`,
|
|
31701
|
+
ExitCode.NODE_ERROR
|
|
31702
|
+
);
|
|
31703
|
+
}
|
|
31704
|
+
}
|
|
31705
|
+
function namedKeysGrantIn(siwe, keys) {
|
|
31706
|
+
let recap = {};
|
|
31707
|
+
try {
|
|
31708
|
+
recap = siwe === void 0 ? {} : extractRecapAttenuations(siwe);
|
|
31709
|
+
} catch {
|
|
31710
|
+
return "none";
|
|
31711
|
+
}
|
|
31712
|
+
const row = recap[keys.resource] ?? {};
|
|
31713
|
+
const wanted = keyAbilitiesFor(keys)[keys.resource];
|
|
31714
|
+
const granted = wanted.filter((ability) => ability in row);
|
|
31715
|
+
if (granted.length === 0) return "none";
|
|
31716
|
+
return granted.length === wanted.length && Object.keys(row).length === wanted.length ? "both" : "partial";
|
|
31717
|
+
}
|
|
31718
|
+
function withVerifiedNamedKeys(profile, session) {
|
|
31719
|
+
if (profile.namedKeys === void 0) return profile;
|
|
31720
|
+
const siwe = typeof session === "object" && session !== null ? session.siwe : void 0;
|
|
31721
|
+
if (typeof siwe === "string" && namedKeysGrantIn(siwe, profile.namedKeys) === "both") return profile;
|
|
31722
|
+
const { namedKeys: _dropped, ...rest } = profile;
|
|
31723
|
+
return rest;
|
|
31724
|
+
}
|
|
31725
|
+
|
|
31641
31726
|
// src/auth/login-commit.ts
|
|
31642
31727
|
init_constants();
|
|
31643
31728
|
init_profiles();
|
|
31644
31729
|
init_storage();
|
|
31645
31730
|
init_errors();
|
|
31646
31731
|
init_space();
|
|
31647
|
-
import { lstat as lstat6 } from "fs/promises";
|
|
31648
31732
|
import { join as join13 } from "path";
|
|
31649
31733
|
import { REPLICA_LOGOUT_PENDING_FILE as REPLICA_LOGOUT_PENDING_FILE3 } from "@tinycloud/node-sdk";
|
|
31650
31734
|
import { CLI_NAME as CLI_NAME12 } from "@tinycloud/operations/cli-name";
|
|
@@ -31758,13 +31842,14 @@ async function commitLogin(profileName2, snapshot, commit) {
|
|
|
31758
31842
|
assertSessionReplaceable(profileName2, current, commit.approved.ownerDid, commit.approved.scope, newExpiresAt);
|
|
31759
31843
|
}
|
|
31760
31844
|
try {
|
|
31845
|
+
const profile = withVerifiedNamedKeys(commit.profile, commit.session);
|
|
31761
31846
|
await ProfileManager.setKey(profileName2, commit.key);
|
|
31762
31847
|
await ProfileManager.setSession(profileName2, commit.session);
|
|
31763
31848
|
if (current.profile?.authSignedOut === true || commit.durable === true) {
|
|
31764
31849
|
await syncAuthCredentialFiles(profilePath7(profileName2));
|
|
31765
|
-
await ProfileManager.setProfileDurable(profileName2, { ...
|
|
31850
|
+
await ProfileManager.setProfileDurable(profileName2, { ...profile, authSignedOut: false });
|
|
31766
31851
|
} else {
|
|
31767
|
-
await ProfileManager.setProfile(profileName2,
|
|
31852
|
+
await ProfileManager.setProfile(profileName2, profile);
|
|
31768
31853
|
}
|
|
31769
31854
|
} catch (error) {
|
|
31770
31855
|
const failures = await restore(profileName2, current);
|
|
@@ -32209,6 +32294,7 @@ async function loginWithDeviceAuthorization(input) {
|
|
|
32209
32294
|
operatorType: existing?.operatorType ?? "human",
|
|
32210
32295
|
authMethod: "openkey"
|
|
32211
32296
|
}, result.primary);
|
|
32297
|
+
delete profile.namedKeys;
|
|
32212
32298
|
await commitLogin(input.profileName, snapshot, {
|
|
32213
32299
|
key,
|
|
32214
32300
|
session: result.session,
|
|
@@ -33965,7 +34051,7 @@ async function finishProfileLogout(ctx, options = {}) {
|
|
|
33965
34051
|
function registerAuthCommand(program, options) {
|
|
33966
34052
|
const auth = program.command("auth").description("Authentication management");
|
|
33967
34053
|
registerAuthGrantsCommand(auth, { openKeyAcquisition: options?.openKeyAcquisition });
|
|
33968
|
-
auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (Safari may require paste)").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`${CLI_NAME17} share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").option("--force", "Replica-enabled OpenKey login: replace the stored session key even when recorded shares could not be checked or hold unexpired shares that need the current key").option("--replication-prefix <prefix>", "Add replication get+sync authority for a folder prefix ending in /, or root (repeatable)", (prefix, values = []) => [...values, prefix]).option("--replication-allow-secrets", "Allow replication of secret material (network-wide decrypt warning)").action(async (options2, cmd) => {
|
|
34054
|
+
auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (Safari may require paste)").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`${CLI_NAME17} share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").option("--force", "Replica-enabled OpenKey login: replace the stored session key even when recorded shares could not be checked or hold unexpired shares that need the current key").option("--keys <folder>", "Also let this sign-in create and use encryption keys in this folder (for example `qa`). The node must support named keys. With OpenKey, the first time on a profile also needs `--owner`.").option("--replication-prefix <prefix>", "Add replication get+sync authority for a folder prefix ending in /, or root (repeatable)", (prefix, values = []) => [...values, prefix]).option("--replication-allow-secrets", "Allow replication of secret material (network-wide decrypt warning)").action(async (options2, cmd) => {
|
|
33969
34055
|
try {
|
|
33970
34056
|
if (options2.device && options2.paste) {
|
|
33971
34057
|
throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
|
|
@@ -33981,6 +34067,9 @@ function registerAuthCommand(program, options) {
|
|
|
33981
34067
|
ExitCode.USAGE_ERROR
|
|
33982
34068
|
);
|
|
33983
34069
|
}
|
|
34070
|
+
if (options2.keys !== void 0 && (options2.device || options2.manifest)) {
|
|
34071
|
+
throw new CLIError("INVALID_ARGUMENT", "`--keys` can't be used with `--device` or `--manifest`.", ExitCode.USAGE_ERROR);
|
|
34072
|
+
}
|
|
33984
34073
|
const globalOpts = cmd.optsWithGlobals();
|
|
33985
34074
|
const ctx = await ProfileManager.resolveContext(globalOpts, { storedSession: "unused" });
|
|
33986
34075
|
await recoverPendingLogout(ctx.profile);
|
|
@@ -34042,6 +34131,8 @@ function registerAuthCommand(program, options) {
|
|
|
34042
34131
|
);
|
|
34043
34132
|
}
|
|
34044
34133
|
method = options2.method;
|
|
34134
|
+
} else if (options2.keys !== void 0) {
|
|
34135
|
+
method = profileConfig?.authMethod === "openkey" || options2.paste || options2.owner ? "openkey" : "local";
|
|
34045
34136
|
} else {
|
|
34046
34137
|
method = scoped ? "openkey" : await promptAuthMethod();
|
|
34047
34138
|
}
|
|
@@ -34054,7 +34145,7 @@ function registerAuthCommand(program, options) {
|
|
|
34054
34145
|
}
|
|
34055
34146
|
if (method === "local") {
|
|
34056
34147
|
buildReplicationLoginRequest(void 0, replicationOptions);
|
|
34057
|
-
await handleLocalAuth(ctx.profile, ctx.host, { replication: replicationOptions });
|
|
34148
|
+
await handleLocalAuth(ctx.profile, ctx.host, { replication: replicationOptions, keys: options2.keys });
|
|
34058
34149
|
await persistReplicationLogin(ctx.profile, replicationOptions);
|
|
34059
34150
|
if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
|
|
34060
34151
|
} else {
|
|
@@ -34067,7 +34158,8 @@ function registerAuthCommand(program, options) {
|
|
|
34067
34158
|
expectedOwner: owner,
|
|
34068
34159
|
replaceSession: options2.replaceSession === true,
|
|
34069
34160
|
persistHost,
|
|
34070
|
-
force: options2.force === true
|
|
34161
|
+
force: options2.force === true,
|
|
34162
|
+
keys: options2.keys
|
|
34071
34163
|
});
|
|
34072
34164
|
if (!preparedCommitted) await persistReplicationLogin(ctx.profile, replicationOptions);
|
|
34073
34165
|
if (replicationOptions.allowSecrets) writeReplicationSecretsWarning();
|
|
@@ -35333,7 +35425,8 @@ async function rotateAuthKey(profileName2, host, options = {}) {
|
|
|
35333
35425
|
);
|
|
35334
35426
|
}
|
|
35335
35427
|
const result2 = await handleLocalAuth(profileName2, host, {
|
|
35336
|
-
emitOutput: false
|
|
35428
|
+
emitOutput: false,
|
|
35429
|
+
keys: profile.namedKeys?.prefix
|
|
35337
35430
|
});
|
|
35338
35431
|
outputRotationResult(result2.profile, profileName2, oldDid, "local");
|
|
35339
35432
|
return;
|
|
@@ -35358,8 +35451,9 @@ async function rotateAuthKey(profileName2, host, options = {}) {
|
|
|
35358
35451
|
}
|
|
35359
35452
|
await ProfileManager.setKey(profileName2, jwk);
|
|
35360
35453
|
await ProfileManager.clearSession(profileName2);
|
|
35454
|
+
const { namedKeys: _rotated, ...currentFields } = current;
|
|
35361
35455
|
await ProfileManager.setProfile(profileName2, {
|
|
35362
|
-
...
|
|
35456
|
+
...currentFields,
|
|
35363
35457
|
host,
|
|
35364
35458
|
did,
|
|
35365
35459
|
sessionDid: did,
|
|
@@ -35371,7 +35465,8 @@ async function rotateAuthKey(profileName2, host, options = {}) {
|
|
|
35371
35465
|
const result = await refreshOpenKeySession(profileName2, host, {
|
|
35372
35466
|
paste: options.paste,
|
|
35373
35467
|
noPopup: options.noPopup,
|
|
35374
|
-
permissions
|
|
35468
|
+
permissions,
|
|
35469
|
+
...profile.namedKeys === void 0 ? {} : { keys: profile.namedKeys.prefix, expectedOwner: profile.ownerDid }
|
|
35375
35470
|
});
|
|
35376
35471
|
outputRotationResult(result.profile, profileName2, oldDid, "openkey");
|
|
35377
35472
|
}
|
|
@@ -35420,12 +35515,12 @@ async function persistCurrentLocalSession(profileName2, profile, session, replic
|
|
|
35420
35515
|
signature: session.signature,
|
|
35421
35516
|
...replicaStamp === void 0 ? {} : { replicaStamp }
|
|
35422
35517
|
});
|
|
35423
|
-
if (profile.sessionDid !== session.verificationMethod || profile.spaceId !== session.spaceId) {
|
|
35424
|
-
await ProfileManager.updateProfile(profileName2, (current) => ({
|
|
35518
|
+
if (profile.sessionDid !== session.verificationMethod || profile.spaceId !== session.spaceId || profile.namedKeys !== void 0) {
|
|
35519
|
+
await ProfileManager.updateProfile(profileName2, (current) => withVerifiedNamedKeys({
|
|
35425
35520
|
...current,
|
|
35426
35521
|
sessionDid: session.verificationMethod,
|
|
35427
35522
|
spaceId: session.spaceId
|
|
35428
|
-
}));
|
|
35523
|
+
}, session));
|
|
35429
35524
|
}
|
|
35430
35525
|
});
|
|
35431
35526
|
}
|
|
@@ -35457,11 +35552,14 @@ async function handleLocalAuth(profileName2, host, options = {}) {
|
|
|
35457
35552
|
process.stderr.write(formatField("DID", did) + "\n\n");
|
|
35458
35553
|
}
|
|
35459
35554
|
}
|
|
35555
|
+
const namedKeys = options.keys === void 0 ? void 0 : parseKeysPrefix(options.keys, did);
|
|
35556
|
+
if (namedKeys !== void 0) await assertNodeSupportsNamedKeys(host);
|
|
35460
35557
|
const sessionResult = await withSpinner("Signing in...", async () => {
|
|
35461
35558
|
const replication = options.replication ?? profile?.replication;
|
|
35462
35559
|
return localKeySignIn({
|
|
35463
35560
|
privateKey,
|
|
35464
35561
|
host,
|
|
35562
|
+
...namedKeys === void 0 ? {} : { keyAbilities: keyAbilitiesFor(namedKeys) },
|
|
35465
35563
|
...replication?.prefixes.length ? { profile: profileName2, replication } : {}
|
|
35466
35564
|
});
|
|
35467
35565
|
});
|
|
@@ -35496,9 +35594,12 @@ async function handleLocalAuth(profileName2, host, options = {}) {
|
|
|
35496
35594
|
operatorType: profile?.operatorType ?? "human",
|
|
35497
35595
|
authMethod: "local",
|
|
35498
35596
|
privateKey,
|
|
35499
|
-
address
|
|
35597
|
+
address,
|
|
35598
|
+
...namedKeys === void 0 ? {} : { namedKeys }
|
|
35500
35599
|
};
|
|
35600
|
+
if (namedKeys === void 0) delete updatedProfile.namedKeys;
|
|
35501
35601
|
await commitLogin(profileName2, snapshot, { key, session, profile: updatedProfile });
|
|
35602
|
+
if (namedKeys !== void 0 && sessionResult.networkReady === false) process.stderr.write(KEYS_FOLDER_NOT_READY);
|
|
35502
35603
|
if (options.emitOutput ?? true) {
|
|
35503
35604
|
outputJson({
|
|
35504
35605
|
authenticated: true,
|
|
@@ -35660,6 +35761,104 @@ async function refreshPreparedOpenKeySession(params) {
|
|
|
35660
35761
|
throw error;
|
|
35661
35762
|
}
|
|
35662
35763
|
}
|
|
35764
|
+
var KEYS_APPROVAL_NOTE = (folder) => `On the OpenKey page, the row that mentions tinycloud.encryption lets this profile create and use keys in the folder "${folder}/". Keep every row ticked. To sign in without keys, untick that whole row and nothing else; any other change makes OpenKey refuse the sign-in.
|
|
35765
|
+
`;
|
|
35766
|
+
var keysNotGrantedAfterLogin = (profileName2) => `You're signed in, but without named keys: the tinycloud.encryption permission wasn't approved. To add them, sign in again with \`${CLI_NAME17} --profile ${profileName2} auth login --keys <folder>\` and leave the row that mentions tinycloud.encryption ticked.
|
|
35767
|
+
`;
|
|
35768
|
+
var KEYS_FOLDER_NOT_READY = `You're signed in, but your encryption keys aren't set up yet. Try \`${CLI_NAME17} keys ensure <folder>/<name>\` again in a minute.
|
|
35769
|
+
`;
|
|
35770
|
+
async function readyNamedKeysNetwork(node) {
|
|
35771
|
+
try {
|
|
35772
|
+
await node.ensureEncryptionNetwork();
|
|
35773
|
+
} catch {
|
|
35774
|
+
process.stderr.write(KEYS_FOLDER_NOT_READY);
|
|
35775
|
+
}
|
|
35776
|
+
}
|
|
35777
|
+
function isNarrowedRecapMismatch(error) {
|
|
35778
|
+
const text = error instanceof Error ? `${error.code ?? ""} ${error.message}` : String(error);
|
|
35779
|
+
return text.includes("narrowed_recap_mismatch");
|
|
35780
|
+
}
|
|
35781
|
+
async function refreshPreparedKeysSession(params) {
|
|
35782
|
+
const { profileName: profileName2, host, profile, snapshot, options, expectedOwner, expiry } = params;
|
|
35783
|
+
const storedOwner = snapshot.session && snapshot.key && profile.sessionDid ? verifySignedSession(snapshot.session, snapshot.key, profile.sessionDid).ownerDid : void 0;
|
|
35784
|
+
const owner = options.expectedOwner ? canonicalOwnerDid(options.expectedOwner) : storedOwner ?? (profile.ownerDid ? canonicalOwnerDid(profile.ownerDid) : void 0);
|
|
35785
|
+
if (!owner) {
|
|
35786
|
+
throw new CLIError(
|
|
35787
|
+
"OWNER_REQUIRED",
|
|
35788
|
+
"The first OpenKey sign-in with `--keys` needs your account: add `--owner did:pkh:eip155:<chain>:<address>`. Nothing was started.",
|
|
35789
|
+
ExitCode.AUTH_REQUIRED
|
|
35790
|
+
);
|
|
35791
|
+
}
|
|
35792
|
+
if (expectedOwner && normalizePkhIdentifier(owner) !== normalizePkhIdentifier(expectedOwner)) {
|
|
35793
|
+
throw new CLIError("OPENKEY_OWNER_MISMATCH", "The verified owner differs from this profile. No approval was started.", ExitCode.PERMISSION_DENIED);
|
|
35794
|
+
}
|
|
35795
|
+
const namedKeys = parseKeysPrefix(options.keys, owner);
|
|
35796
|
+
await assertNodeSupportsNamedKeys(host);
|
|
35797
|
+
if (options.force !== true) await assertShareHistoryAllowsKeyDrop(profileName2, "login");
|
|
35798
|
+
const node = new TinyCloudNode3({
|
|
35799
|
+
host,
|
|
35800
|
+
signer: ownerIdentitySigner(owner),
|
|
35801
|
+
additionalRawAbilities: keyAbilitiesFor(namedKeys),
|
|
35802
|
+
includeAccountRegistryPermissions: false,
|
|
35803
|
+
autoBootstrapAccount: false,
|
|
35804
|
+
...expiry ? { sessionExpirationMs: expiry.durationMs } : {}
|
|
35805
|
+
});
|
|
35806
|
+
let handoff;
|
|
35807
|
+
let preparedDid;
|
|
35808
|
+
try {
|
|
35809
|
+
process.stderr.write(KEYS_APPROVAL_NOTE(namedKeys.prefix));
|
|
35810
|
+
await node.signInWithOpenKey(async (prepared) => {
|
|
35811
|
+
preparedDid = prepared.verificationMethod;
|
|
35812
|
+
handoff = await startPreparedOpenKeyHandoff({
|
|
35813
|
+
prepared,
|
|
35814
|
+
openKeyHost: resolveOpenKeyHost(profile),
|
|
35815
|
+
noPopup: options.noPopup,
|
|
35816
|
+
paste: options.paste
|
|
35817
|
+
});
|
|
35818
|
+
return handoff.result;
|
|
35819
|
+
});
|
|
35820
|
+
const active = node.restorableSession;
|
|
35821
|
+
if (!active || !preparedDid || node.sessionDid !== preparedDid || active.verificationMethod !== preparedDid) {
|
|
35822
|
+
throw new CLIError("OPENKEY_SESSION_MISMATCH", "OpenKey signed a different SDK session key; no profile or session was saved.", ExitCode.PERMISSION_DENIED);
|
|
35823
|
+
}
|
|
35824
|
+
const key = active.jwk;
|
|
35825
|
+
const signed = verifySignedSession({ ...active }, key, preparedDid, { expectedOwner: owner, expiry });
|
|
35826
|
+
const grant = namedKeysGrantIn(active.siwe, namedKeys);
|
|
35827
|
+
if (grant === "partial") throw partialKeysGrantError();
|
|
35828
|
+
const session = { ...withVerifiedAuthority({ ...active, jwk: key }, signed), authMethod: "openkey" };
|
|
35829
|
+
const updatedProfile = {
|
|
35830
|
+
...profile,
|
|
35831
|
+
host: options.persistHost === true || !profile.host ? host : profile.host,
|
|
35832
|
+
did: preparedDid,
|
|
35833
|
+
sessionDid: preparedDid,
|
|
35834
|
+
posture: profile.posture ?? "owner-openkey",
|
|
35835
|
+
operatorType: profile.operatorType ?? "human",
|
|
35836
|
+
authMethod: "openkey",
|
|
35837
|
+
ownerDid: signed.ownerDid,
|
|
35838
|
+
spaceId: active.spaceId
|
|
35839
|
+
};
|
|
35840
|
+
delete updatedProfile.namedKeys;
|
|
35841
|
+
if (grant === "both") updatedProfile.namedKeys = namedKeys;
|
|
35842
|
+
await commitLogin(profileName2, snapshot, { key, session, profile: updatedProfile });
|
|
35843
|
+
await handoff?.complete(true);
|
|
35844
|
+
if (grant === "both") await readyNamedKeysNetwork(node);
|
|
35845
|
+
if (grant === "none") {
|
|
35846
|
+
process.stderr.write(keysNotGrantedAfterLogin(profileName2));
|
|
35847
|
+
}
|
|
35848
|
+
return { profile: updatedProfile, delegationData: session, declined: [], legacyNested: [] };
|
|
35849
|
+
} catch (error) {
|
|
35850
|
+
await handoff?.complete(false);
|
|
35851
|
+
await node.replication?.close();
|
|
35852
|
+
throw isNarrowedRecapMismatch(error) ? partialKeysGrantError() : error;
|
|
35853
|
+
}
|
|
35854
|
+
}
|
|
35855
|
+
function partialKeysGrantError() {
|
|
35856
|
+
return new CLIError(
|
|
35857
|
+
"KEYS_PARTIAL_GRANT",
|
|
35858
|
+
"Named keys need both permissions (create and use), but only one was approved, so this sign-in wasn't saved. Sign in again and approve both.",
|
|
35859
|
+
ExitCode.PERMISSION_DENIED
|
|
35860
|
+
);
|
|
35861
|
+
}
|
|
35663
35862
|
async function undoPreparedAttempt(profileName2, node, owner, gateWasActive) {
|
|
35664
35863
|
if (!gateWasActive) {
|
|
35665
35864
|
const undone = await ProfileManager.withLock(profileName2, async () => {
|
|
@@ -35698,6 +35897,16 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
|
|
|
35698
35897
|
const estimatedExpiry = expiry === void 0 ? void 0 : new Date(Date.now() + expiry.durationMs).toISOString();
|
|
35699
35898
|
assertSessionReplaceable(profileName2, snapshot, expectedOwner, permissions, estimatedExpiry);
|
|
35700
35899
|
}
|
|
35900
|
+
if (options.keys !== void 0) {
|
|
35901
|
+
if (replication.prefixes.length > 0) {
|
|
35902
|
+
throw new CLIError(
|
|
35903
|
+
"INVALID_ARGUMENT",
|
|
35904
|
+
`With OpenKey sign-in, \`--keys\` can't be used on a profile that has replication prefixes (given with \`--replication-prefix\` or saved from an earlier sign-in). Nothing was started. Use a separate profile for named keys: \`${CLI_NAME17} --profile <name> auth login --owner did:pkh:eip155:<chain>:<address> --keys <folder>\`.`,
|
|
35905
|
+
ExitCode.USAGE_ERROR
|
|
35906
|
+
);
|
|
35907
|
+
}
|
|
35908
|
+
return refreshPreparedKeysSession({ profileName: profileName2, host, profile, snapshot, options, expectedOwner, expiry });
|
|
35909
|
+
}
|
|
35701
35910
|
if (replication.prefixes.length > 0) {
|
|
35702
35911
|
return refreshPreparedOpenKeySession({
|
|
35703
35912
|
profileName: profileName2,
|
|
@@ -35760,6 +35969,7 @@ async function refreshOpenKeySession(profileName2, host, options = {}) {
|
|
|
35760
35969
|
...typeof sanitizedSession.spaceId === "string" ? { spaceId: sanitizedSession.spaceId } : {},
|
|
35761
35970
|
...verifiedOwner === void 0 ? {} : { ownerDid: verifiedOwner }
|
|
35762
35971
|
}, ownerKeyPrimary);
|
|
35972
|
+
delete updatedProfile.namedKeys;
|
|
35763
35973
|
await commitLogin(profileName2, snapshot, {
|
|
35764
35974
|
key,
|
|
35765
35975
|
session: sanitizedSession,
|
|
@@ -37337,6 +37547,175 @@ function registerKvCommand(program) {
|
|
|
37337
37547
|
});
|
|
37338
37548
|
}
|
|
37339
37549
|
|
|
37550
|
+
// src/commands/keys.ts
|
|
37551
|
+
init_profiles();
|
|
37552
|
+
init_constants();
|
|
37553
|
+
init_formatter();
|
|
37554
|
+
init_errors();
|
|
37555
|
+
init_sdk();
|
|
37556
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
37557
|
+
import { NamedKeyError, createNamedKeyClient } from "@tinycloud/node-sdk/encryption-keys";
|
|
37558
|
+
import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
|
|
37559
|
+
async function readStdin3() {
|
|
37560
|
+
const chunks = [];
|
|
37561
|
+
for await (const chunk of process.stdin) {
|
|
37562
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
37563
|
+
}
|
|
37564
|
+
return Buffer.concat(chunks);
|
|
37565
|
+
}
|
|
37566
|
+
async function readInput(file) {
|
|
37567
|
+
return file === void 0 ? readStdin3() : readFile12(file);
|
|
37568
|
+
}
|
|
37569
|
+
function cliErrorFromNamedKey(error, command, path) {
|
|
37570
|
+
if (!(error instanceof NamedKeyError)) return error;
|
|
37571
|
+
switch (error.code) {
|
|
37572
|
+
case "NAMED_KEYS_UNSUPPORTED":
|
|
37573
|
+
return new CLIError("NAMED_KEYS_UNAVAILABLE", "This node doesn't support named keys.", ExitCode.NODE_ERROR);
|
|
37574
|
+
case "INVALID_KEY_TARGET":
|
|
37575
|
+
case "KEY_PATH_INVALID":
|
|
37576
|
+
return new CLIError("INVALID_KEY_PATH", "That key name isn't valid.", ExitCode.USAGE_ERROR);
|
|
37577
|
+
case "INVALID_ENVELOPE":
|
|
37578
|
+
return new CLIError("INVALID_ENVELOPE", "The envelope is damaged or incomplete.", ExitCode.USAGE_ERROR);
|
|
37579
|
+
case "KEY_NOT_FOUND":
|
|
37580
|
+
return new CLIError(
|
|
37581
|
+
"NOT_FOUND",
|
|
37582
|
+
command === "decrypt" ? "The key this envelope needs doesn't exist on this node, so it can't be decrypted here." : `There's no key at ${path} yet. Create it with \`${CLI_NAME26} keys ensure ${path}\`.`,
|
|
37583
|
+
ExitCode.NOT_FOUND
|
|
37584
|
+
);
|
|
37585
|
+
case "KEY_BLOCKED":
|
|
37586
|
+
return new CLIError("KEY_BLOCKED", `Key ${path} is blocked, so it can't be used.`, ExitCode.PERMISSION_DENIED);
|
|
37587
|
+
case "DECRYPT_FAILED":
|
|
37588
|
+
return new CLIError("DECRYPT_FAILED", `Couldn't decrypt this envelope with key ${path}.`, ExitCode.ERROR);
|
|
37589
|
+
}
|
|
37590
|
+
if (error.status === 401 || error.status === 403) {
|
|
37591
|
+
return new CLIError("PERMISSION_DENIED", error.message, ExitCode.PERMISSION_DENIED, {
|
|
37592
|
+
hint: `Sign in with \`${CLI_NAME26} auth login --keys <folder>\` for this key's folder.`
|
|
37593
|
+
});
|
|
37594
|
+
}
|
|
37595
|
+
return new CLIError(error.code, error.message, error.status === void 0 ? ExitCode.ERROR : ExitCode.NODE_ERROR);
|
|
37596
|
+
}
|
|
37597
|
+
async function withNetworkSetup(node, call) {
|
|
37598
|
+
try {
|
|
37599
|
+
return await call();
|
|
37600
|
+
} catch (error) {
|
|
37601
|
+
if (!(error instanceof NamedKeyError) || error.code !== "NETWORK_NOT_FOUND") throw error;
|
|
37602
|
+
await node.ensureEncryptionNetwork();
|
|
37603
|
+
return call();
|
|
37604
|
+
}
|
|
37605
|
+
}
|
|
37606
|
+
async function grantedPrefix(deps, profileName2) {
|
|
37607
|
+
const profile = await deps.getProfile(profileName2);
|
|
37608
|
+
const keys = profile.namedKeys;
|
|
37609
|
+
if (keys === void 0) {
|
|
37610
|
+
throw new CLIError(
|
|
37611
|
+
"KEYS_NOT_GRANTED",
|
|
37612
|
+
`Profile "${profileName2}" can't use named keys yet. Sign in with \`${CLI_NAME26} --profile ${profileName2} auth login --keys <folder>\`.`,
|
|
37613
|
+
ExitCode.PERMISSION_DENIED
|
|
37614
|
+
);
|
|
37615
|
+
}
|
|
37616
|
+
return keys.prefix;
|
|
37617
|
+
}
|
|
37618
|
+
function assertInGrantedPrefix(prefix, path, profileName2) {
|
|
37619
|
+
if (!keyPathInPrefix(prefix, path)) {
|
|
37620
|
+
throw new CLIError(
|
|
37621
|
+
"KEY_OUTSIDE_GRANTED_PREFIX",
|
|
37622
|
+
`Key "${path}" is outside the folder "${prefix}/" this sign-in covers. To use it, sign in with \`${CLI_NAME26} --profile ${profileName2} auth login --method local --keys <folder>\`.`,
|
|
37623
|
+
ExitCode.PERMISSION_DENIED
|
|
37624
|
+
);
|
|
37625
|
+
}
|
|
37626
|
+
}
|
|
37627
|
+
function parseEnvelope(bytes) {
|
|
37628
|
+
let parsed;
|
|
37629
|
+
try {
|
|
37630
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
37631
|
+
} catch {
|
|
37632
|
+
throw new CLIError("INVALID_ENVELOPE", "The input isn't an envelope (expected JSON).", ExitCode.USAGE_ERROR);
|
|
37633
|
+
}
|
|
37634
|
+
const envelope = parsed;
|
|
37635
|
+
if (typeof envelope !== "object" || envelope === null || envelope.v !== 2 || typeof envelope.keyPath !== "string") {
|
|
37636
|
+
throw new CLIError("INVALID_ENVELOPE", 'The input isn\'t a named-key envelope (it needs `"v": 2` and a `keyPath`).', ExitCode.USAGE_ERROR);
|
|
37637
|
+
}
|
|
37638
|
+
return envelope;
|
|
37639
|
+
}
|
|
37640
|
+
var defaultDeps = {
|
|
37641
|
+
resolveContext: (options) => ProfileManager.resolveContext(options),
|
|
37642
|
+
getProfile: (name) => ProfileManager.getProfile(name),
|
|
37643
|
+
authenticate: (ctx) => ensureAuthenticated(ctx),
|
|
37644
|
+
createClient: createNamedKeyClient,
|
|
37645
|
+
outputJson,
|
|
37646
|
+
handleError
|
|
37647
|
+
};
|
|
37648
|
+
function registerKeysCommand(program, deps = defaultDeps) {
|
|
37649
|
+
const { outputJson: outputJson2, handleError: handleError2 } = deps;
|
|
37650
|
+
const keys = program.command("keys").description(`Named encryption keys (sign in with \`${CLI_NAME26} auth login --keys <folder>\` first)`);
|
|
37651
|
+
keys.command("ensure <path>").description("Create the named key if it doesn't exist yet, then show it").action(async (path, _options, cmd) => {
|
|
37652
|
+
try {
|
|
37653
|
+
const ctx = await deps.resolveContext(cmd.optsWithGlobals());
|
|
37654
|
+
assertInGrantedPrefix(await grantedPrefix(deps, ctx.profile), path, ctx.profile);
|
|
37655
|
+
const node = await deps.authenticate(ctx);
|
|
37656
|
+
const client = deps.createClient(node);
|
|
37657
|
+
const descriptor = await withSpinner(`Getting or creating key ${path}\u2026`, () => withNetworkSetup(node, () => client.ensureKey(path))).catch((error) => {
|
|
37658
|
+
throw cliErrorFromNamedKey(error, "ensure", path);
|
|
37659
|
+
});
|
|
37660
|
+
outputJson2({ ...descriptor });
|
|
37661
|
+
} catch (error) {
|
|
37662
|
+
await handleError2(error);
|
|
37663
|
+
}
|
|
37664
|
+
});
|
|
37665
|
+
keys.command("encrypt <path>").description("Encrypt data with a named key and output the encrypted envelope as JSON").option("--in <file>", "Read the data to encrypt from a file (default: standard input)").option("--out <file>", "Write the envelope to a file (default: standard output)").action(async (path, options, cmd) => {
|
|
37666
|
+
try {
|
|
37667
|
+
const ctx = await deps.resolveContext(cmd.optsWithGlobals());
|
|
37668
|
+
assertInGrantedPrefix(await grantedPrefix(deps, ctx.profile), path, ctx.profile);
|
|
37669
|
+
if (options.out) await validatePrivateOutput(options.out, "--out");
|
|
37670
|
+
const plaintext = await readInput(options.in);
|
|
37671
|
+
const node = await deps.authenticate(ctx);
|
|
37672
|
+
const client = deps.createClient(node);
|
|
37673
|
+
const envelope = await withSpinner(`Encrypting with ${path}\u2026`, () => withNetworkSetup(node, () => client.encrypt(path, plaintext))).catch((error) => {
|
|
37674
|
+
throw cliErrorFromNamedKey(error, "encrypt", path);
|
|
37675
|
+
});
|
|
37676
|
+
const json = JSON.stringify(envelope);
|
|
37677
|
+
if (options.out) {
|
|
37678
|
+
await writePrivateOutput(options.out, new TextEncoder().encode(json));
|
|
37679
|
+
outputJson2({ keyPath: envelope.keyPath, networkId: envelope.networkId, written: options.out });
|
|
37680
|
+
return;
|
|
37681
|
+
}
|
|
37682
|
+
process.stdout.write(json + "\n");
|
|
37683
|
+
} catch (error) {
|
|
37684
|
+
await handleError2(error);
|
|
37685
|
+
}
|
|
37686
|
+
});
|
|
37687
|
+
keys.command("decrypt").description("Decrypt an envelope made with a named key and output the original data").option("--in <file>", "Read the envelope from a file (default: standard input)").option("--out <file>", "Write the decrypted data to a file (default: standard output)").action(async (options, cmd) => {
|
|
37688
|
+
try {
|
|
37689
|
+
const ctx = await deps.resolveContext(cmd.optsWithGlobals());
|
|
37690
|
+
const prefix = await grantedPrefix(deps, ctx.profile);
|
|
37691
|
+
if (options.out) await validatePrivateOutput(options.out, "--out");
|
|
37692
|
+
const envelope = parseEnvelope(await readInput(options.in));
|
|
37693
|
+
assertInGrantedPrefix(prefix, envelope.keyPath, ctx.profile);
|
|
37694
|
+
const profile = await deps.getProfile(ctx.profile);
|
|
37695
|
+
if (profile.ownerDid !== void 0 && envelope.networkId !== namedKeysNetworkId(profile.ownerDid)) {
|
|
37696
|
+
throw new CLIError(
|
|
37697
|
+
"KEY_OUTSIDE_GRANTED_PREFIX",
|
|
37698
|
+
"This envelope was made with a different encryption network than this profile's, so this profile can't decrypt it.",
|
|
37699
|
+
ExitCode.PERMISSION_DENIED
|
|
37700
|
+
);
|
|
37701
|
+
}
|
|
37702
|
+
const node = await deps.authenticate(ctx);
|
|
37703
|
+
const client = deps.createClient(node);
|
|
37704
|
+
const plaintext = await withSpinner("Decrypting\u2026", () => client.decrypt(envelope)).catch((error) => {
|
|
37705
|
+
throw cliErrorFromNamedKey(error, "decrypt", envelope.keyPath);
|
|
37706
|
+
});
|
|
37707
|
+
if (options.out) {
|
|
37708
|
+
await writePrivateOutput(options.out, plaintext);
|
|
37709
|
+
outputJson2({ keyPath: envelope.keyPath, written: options.out });
|
|
37710
|
+
return;
|
|
37711
|
+
}
|
|
37712
|
+
process.stdout.write(plaintext);
|
|
37713
|
+
} catch (error) {
|
|
37714
|
+
await handleError2(error);
|
|
37715
|
+
}
|
|
37716
|
+
});
|
|
37717
|
+
}
|
|
37718
|
+
|
|
37340
37719
|
// src/commands/manifest.ts
|
|
37341
37720
|
init_profiles();
|
|
37342
37721
|
init_sdk();
|
|
@@ -37346,16 +37725,16 @@ init_errors();
|
|
|
37346
37725
|
init_constants();
|
|
37347
37726
|
init_theme();
|
|
37348
37727
|
import { resolveManifestKnowledgeRoot as resolveManifestKnowledgeRoot2 } from "@tinycloud/sdk-core";
|
|
37349
|
-
import { CLI_NAME as
|
|
37350
|
-
import { readFile as
|
|
37728
|
+
import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
|
|
37729
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
37351
37730
|
var DEFAULT_APP_SPACE = "applications";
|
|
37352
37731
|
function registerManifestCommand(program) {
|
|
37353
37732
|
const manifest = program.command("manifest").description("Inspect TinyCloud app manifests");
|
|
37354
37733
|
manifest.command("resolve <source>").description("Resolve a manifest file or URL to its effective space, paths, and DB basenames").addHelpText("after", `
|
|
37355
37734
|
|
|
37356
37735
|
Examples:
|
|
37357
|
-
$ ${
|
|
37358
|
-
$ ${
|
|
37736
|
+
$ ${CLI_NAME27} manifest resolve ./manifest.json
|
|
37737
|
+
$ ${CLI_NAME27} manifest resolve https://app.example.com/manifest.json --json
|
|
37359
37738
|
|
|
37360
37739
|
What it shows:
|
|
37361
37740
|
- app_id, name, manifest_version
|
|
@@ -37365,7 +37744,7 @@ What it shows:
|
|
|
37365
37744
|
|
|
37366
37745
|
This command is read-only and does NOT contact the node \u2014 it just resolves
|
|
37367
37746
|
the manifest against the active profile's address/chain so you know which
|
|
37368
|
-
\`--space\` and \`--db\` values to pass to other ${
|
|
37747
|
+
\`--space\` and \`--db\` values to pass to other ${CLI_NAME27} commands.
|
|
37369
37748
|
`).action(async (source, _options, cmd) => {
|
|
37370
37749
|
try {
|
|
37371
37750
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -37434,7 +37813,7 @@ ${theme.heading("SQL databases")}
|
|
|
37434
37813
|
`);
|
|
37435
37814
|
}
|
|
37436
37815
|
process.stdout.write(theme.muted(`
|
|
37437
|
-
Use with: ${
|
|
37816
|
+
Use with: ${CLI_NAME27} sql query --space ${spaceName} --db <db> "..."
|
|
37438
37817
|
`));
|
|
37439
37818
|
}
|
|
37440
37819
|
if (permissions.length > 0) {
|
|
@@ -37461,7 +37840,7 @@ async function loadManifestSource2(source) {
|
|
|
37461
37840
|
}
|
|
37462
37841
|
return response.text();
|
|
37463
37842
|
}
|
|
37464
|
-
return
|
|
37843
|
+
return readFile13(source, "utf8");
|
|
37465
37844
|
}
|
|
37466
37845
|
function prefixWithAppId(path, appId) {
|
|
37467
37846
|
const slash = path.indexOf("/");
|
|
@@ -37554,7 +37933,7 @@ init_profiles();
|
|
|
37554
37933
|
init_formatter();
|
|
37555
37934
|
init_errors();
|
|
37556
37935
|
init_constants();
|
|
37557
|
-
import { CLI_NAME as
|
|
37936
|
+
import { CLI_NAME as CLI_NAME28 } from "@tinycloud/operations/cli-name";
|
|
37558
37937
|
import { createInterface as createInterface5 } from "readline";
|
|
37559
37938
|
init_theme();
|
|
37560
37939
|
init_types();
|
|
@@ -37700,9 +38079,9 @@ profile at command time. Precedence for every kv/sql command:
|
|
|
37700
38079
|
explicit --space flag > profile defaultSpace > primary space.
|
|
37701
38080
|
|
|
37702
38081
|
Examples:
|
|
37703
|
-
$ ${
|
|
37704
|
-
$ ${
|
|
37705
|
-
$ ${
|
|
38082
|
+
$ ${CLI_NAME28} profile set-default-space applications
|
|
38083
|
+
$ ${CLI_NAME28} profile set-default-space applications --profile cli-test
|
|
38084
|
+
$ ${CLI_NAME28} profile set-default-space --unset
|
|
37706
38085
|
`).action(async (name, options, cmd) => {
|
|
37707
38086
|
try {
|
|
37708
38087
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -37714,7 +38093,7 @@ Examples:
|
|
|
37714
38093
|
if (!options.unset && (name === void 0 || name === "")) {
|
|
37715
38094
|
throw new CLIError(
|
|
37716
38095
|
"USAGE_ERROR",
|
|
37717
|
-
`Provide a space name (e.g. \`${
|
|
38096
|
+
`Provide a space name (e.g. \`${CLI_NAME28} profile set-default-space applications\`) or pass --unset.`,
|
|
37718
38097
|
ExitCode.USAGE_ERROR
|
|
37719
38098
|
);
|
|
37720
38099
|
}
|
|
@@ -37773,7 +38152,7 @@ function parseOperatorType(raw) {
|
|
|
37773
38152
|
|
|
37774
38153
|
// src/commands/replica.ts
|
|
37775
38154
|
init_profile_replicas();
|
|
37776
|
-
import { CLI_NAME as
|
|
38155
|
+
import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
|
|
37777
38156
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
37778
38157
|
import { constants as fsConstants5 } from "fs";
|
|
37779
38158
|
import { mkdir as mkdir5, open as open11, readdir as readdir6, rename as rename4, unlink as unlink5, lstat as lstat9 } from "fs/promises";
|
|
@@ -37914,11 +38293,11 @@ var EXIT_BY_CODE = {
|
|
|
37914
38293
|
var READ_FAILURE = {
|
|
37915
38294
|
absent: { code: "KEY_ABSENT", exit: ExitCode.NOT_FOUND, what: "is not in the replica (the replica covers its whole prefix)" },
|
|
37916
38295
|
deleted: { code: "KEY_DELETED", exit: ExitCode.NOT_FOUND, what: "was deleted at the source" },
|
|
37917
|
-
content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${
|
|
38296
|
+
content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME29} replica sync` },
|
|
37918
38297
|
coverage_incomplete: {
|
|
37919
38298
|
code: "COVERAGE_INCOMPLETE",
|
|
37920
38299
|
exit: ExitCode.NOT_FOUND,
|
|
37921
|
-
what: `is not in the replica, which has not finished its first sync; run ${
|
|
38300
|
+
what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME29} replica sync`
|
|
37922
38301
|
},
|
|
37923
38302
|
not_covered: { code: "NOT_COVERED", exit: ExitCode.USAGE_ERROR, what: "is outside the replica's prefix" }
|
|
37924
38303
|
};
|
|
@@ -38036,7 +38415,7 @@ async function existingReplicaName(profile, option) {
|
|
|
38036
38415
|
const names = await replicaNames(profile);
|
|
38037
38416
|
if (names.length === 1) return names[0];
|
|
38038
38417
|
if (names.length === 0) {
|
|
38039
|
-
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${
|
|
38418
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME29} replica sync --prefix <prefix>`);
|
|
38040
38419
|
}
|
|
38041
38420
|
throw new CLIError("USAGE_ERROR", `Profile "${profile}" has several replicas (${names.join(", ")}); pass --replica <name>.`, ExitCode.USAGE_ERROR);
|
|
38042
38421
|
}
|
|
@@ -38144,9 +38523,9 @@ async function storedGrants(profile) {
|
|
|
38144
38523
|
function grantHint(space, prefix) {
|
|
38145
38524
|
return [
|
|
38146
38525
|
"A replica needs a device grant with get and sync on its prefix:",
|
|
38147
|
-
` device: ${
|
|
38148
|
-
` owner: ${
|
|
38149
|
-
` device: ${
|
|
38526
|
+
` device: ${CLI_NAME29} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
|
|
38527
|
+
` owner: ${CLI_NAME29} auth grant req.json > grant.json`,
|
|
38528
|
+
` device: ${CLI_NAME29} auth import grant.json`
|
|
38150
38529
|
].join("\n");
|
|
38151
38530
|
}
|
|
38152
38531
|
function resolveSpace(input, prefix, grants) {
|
|
@@ -38237,7 +38616,7 @@ function describeStatus(status) {
|
|
|
38237
38616
|
formatField("Size", formatBytes(status.bytes)),
|
|
38238
38617
|
formatField("Grant", status.device.delegationCid),
|
|
38239
38618
|
formatField("Last error", status.lastError ? `${status.lastError.code}: ${status.lastError.message}` : null),
|
|
38240
|
-
formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${
|
|
38619
|
+
formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME29} replica command retries` : null)
|
|
38241
38620
|
].join("\n");
|
|
38242
38621
|
}
|
|
38243
38622
|
async function writeFileAtomic(path, bytes) {
|
|
@@ -38401,7 +38780,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
38401
38780
|
(options, cmd) => run(async () => {
|
|
38402
38781
|
const profile = await profileName(cmd);
|
|
38403
38782
|
if (options.purge) {
|
|
38404
|
-
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${
|
|
38783
|
+
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME29} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
|
|
38405
38784
|
}
|
|
38406
38785
|
if (options.clearPending) {
|
|
38407
38786
|
throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
|
|
@@ -38503,7 +38882,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
38503
38882
|
if (differs !== void 0) {
|
|
38504
38883
|
throw new ReplicaError(
|
|
38505
38884
|
ReplicaErrorCode.CONFIG_MISMATCH,
|
|
38506
|
-
`Replica ${name} already follows ${differs}. Use another --replica name, or ${
|
|
38885
|
+
`Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME29} replica reset --replica ${name} --purge first.`
|
|
38507
38886
|
);
|
|
38508
38887
|
}
|
|
38509
38888
|
}
|
|
@@ -38668,8 +39047,8 @@ init_theme();
|
|
|
38668
39047
|
init_errors();
|
|
38669
39048
|
init_warnings();
|
|
38670
39049
|
init_constants();
|
|
38671
|
-
import { CLI_NAME as
|
|
38672
|
-
import { readFile as
|
|
39050
|
+
import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
|
|
39051
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
38673
39052
|
import { join as join17 } from "path";
|
|
38674
39053
|
import { homedir, hostname, userInfo } from "os";
|
|
38675
39054
|
import { appSecretScope } from "@tinycloud/sdk-core";
|
|
@@ -38879,7 +39258,7 @@ function parseStatusEnvelope(line) {
|
|
|
38879
39258
|
// src/lib/secrets-wait.ts
|
|
38880
39259
|
init_constants();
|
|
38881
39260
|
init_errors();
|
|
38882
|
-
import { CLI_NAME as
|
|
39261
|
+
import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
|
|
38883
39262
|
var WAIT_BACKOFF_MS = [3e3, 4500, 6750, 10125];
|
|
38884
39263
|
var WAIT_STEADY_INTERVAL_MS = 15e3;
|
|
38885
39264
|
var DEFAULT_WAIT_TIMEOUT = "5m";
|
|
@@ -38887,7 +39266,7 @@ var MAX_WAIT_TIMEOUT_MS = 9 * 6e4;
|
|
|
38887
39266
|
var MAX_CONSECUTIVE_FAILURES = 5;
|
|
38888
39267
|
var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
|
|
38889
39268
|
var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
|
|
38890
|
-
var EXPLICIT_KEY_HINT = `${
|
|
39269
|
+
var EXPLICIT_KEY_HINT = `${CLI_NAME30} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME30} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
|
|
38891
39270
|
function abortableSleep(ms2, signal) {
|
|
38892
39271
|
return new Promise((resolve9) => {
|
|
38893
39272
|
if (signal.aborted) {
|
|
@@ -38923,7 +39302,7 @@ async function runSecretsWait(request, deps) {
|
|
|
38923
39302
|
let step = 0;
|
|
38924
39303
|
const cancelled = () => new CLIError(
|
|
38925
39304
|
"CANCELLED",
|
|
38926
|
-
`Interrupted: ${
|
|
39305
|
+
`Interrupted: ${CLI_NAME30} secrets wait stopped before the secrets were ready.`,
|
|
38927
39306
|
ExitCode.CANCELLED,
|
|
38928
39307
|
progress === void 0 ? {} : { progress }
|
|
38929
39308
|
);
|
|
@@ -39067,13 +39446,13 @@ function statusError(error, profile, warnings) {
|
|
|
39067
39446
|
if (error.code === "AUTH_REQUIRED" || error.code === "SESSION_NOT_FOUND") {
|
|
39068
39447
|
const recovery = recoveryOf(error, "sign_in");
|
|
39069
39448
|
if (recovery !== void 0) {
|
|
39070
|
-
const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${
|
|
39449
|
+
const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME30} auth login, then retry.`;
|
|
39071
39450
|
return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint, recovery, ...extra });
|
|
39072
39451
|
}
|
|
39073
39452
|
}
|
|
39074
39453
|
if (error.code === "PROFILE_MIGRATION_REQUIRED") {
|
|
39075
39454
|
const recovery = recoveryOf(error, "complete_migration");
|
|
39076
|
-
const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${
|
|
39455
|
+
const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME30} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
|
|
39077
39456
|
return new CLIError("PROFILE_MIGRATION_REQUIRED", error.message, ExitCode.ERROR, {
|
|
39078
39457
|
hint,
|
|
39079
39458
|
...recovery === void 0 ? {} : { recovery },
|
|
@@ -39099,7 +39478,7 @@ var SECRET_KV_ABILITIES = {
|
|
|
39099
39478
|
del: secretCapabilityAction("del"),
|
|
39100
39479
|
list: secretCapabilityAction("list")
|
|
39101
39480
|
};
|
|
39102
|
-
async function
|
|
39481
|
+
async function readStdin4() {
|
|
39103
39482
|
const chunks = [];
|
|
39104
39483
|
for await (const chunk of process.stdin) {
|
|
39105
39484
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -39265,11 +39644,11 @@ async function runSecretOperation(params) {
|
|
|
39265
39644
|
}
|
|
39266
39645
|
function secretPermissionReason(action, name) {
|
|
39267
39646
|
const target = name ? ` secret "${name}"` : " secrets";
|
|
39268
|
-
return `Allow \`${
|
|
39647
|
+
return `Allow \`${CLI_NAME31} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
|
|
39269
39648
|
}
|
|
39270
39649
|
function assertOwnerApprovalPossible(profileName2, profile, action, name, missing = []) {
|
|
39271
39650
|
if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
|
|
39272
|
-
const command = `${
|
|
39651
|
+
const command = `${CLI_NAME31} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
|
|
39273
39652
|
throw new CLIError(
|
|
39274
39653
|
"PERMISSION_DENIED",
|
|
39275
39654
|
action === "get" ? missing.length > 0 && missing.every((permission) => permission.service === "tinycloud.encryption" && Array.isArray(permission.actions) && permission.actions.length > 0 && permission.actions.every((capability) => capability === SECRET_DECRYPT_CAPABILITY)) ? `Profile "${profileName2}" lacks the scoped decrypt authority (${SECRET_DECRYPT_CAPABILITY}) needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" lacks a scoped read or decrypt grant needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" holds no grant for \`${command}\`, and requesting one needs an interactive browser approval.`,
|
|
@@ -39280,7 +39659,7 @@ function assertOwnerApprovalPossible(profileName2, profile, action, name, missin
|
|
|
39280
39659
|
);
|
|
39281
39660
|
}
|
|
39282
39661
|
function scopedSecretLoginHint(profileName2) {
|
|
39283
|
-
return `Have the owner approve a scoped login whose manifest names the secret: ${
|
|
39662
|
+
return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME31} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
|
|
39284
39663
|
}
|
|
39285
39664
|
async function runSecretOperationAttempt(label, operation) {
|
|
39286
39665
|
try {
|
|
@@ -39414,7 +39793,7 @@ ${message}`,
|
|
|
39414
39793
|
"AUTH_REQUIRED",
|
|
39415
39794
|
"Not signed in to TinyCloud.",
|
|
39416
39795
|
ExitCode.AUTH_REQUIRED,
|
|
39417
|
-
{ hint: `Sign in with: ${
|
|
39796
|
+
{ hint: `Sign in with: ${CLI_NAME31} --profile ${result.context.profile} auth login` }
|
|
39418
39797
|
);
|
|
39419
39798
|
}
|
|
39420
39799
|
if (result.error.code === "NODE_UNREACHABLE") {
|
|
@@ -39563,7 +39942,7 @@ function permissionsFromDelegation2(delegation) {
|
|
|
39563
39942
|
}
|
|
39564
39943
|
async function loadDelegationCandidates(source) {
|
|
39565
39944
|
try {
|
|
39566
|
-
const raw = JSON.parse(await
|
|
39945
|
+
const raw = JSON.parse(await readFile14(source, "utf8"));
|
|
39567
39946
|
return normalizeDelegationCandidates(raw, source);
|
|
39568
39947
|
} catch (error) {
|
|
39569
39948
|
if (error?.code !== "ENOENT") {
|
|
@@ -39583,7 +39962,7 @@ async function loadDelegationCandidates(source) {
|
|
|
39583
39962
|
}
|
|
39584
39963
|
try {
|
|
39585
39964
|
const importedPath = join17(resolveProfilesDir(), source, "additional-delegations.json");
|
|
39586
|
-
const raw = JSON.parse(await
|
|
39965
|
+
const raw = JSON.parse(await readFile14(importedPath, "utf8"));
|
|
39587
39966
|
return normalizeDelegationCandidates(raw, source);
|
|
39588
39967
|
} catch (error) {
|
|
39589
39968
|
if (error?.code === "ENOENT") {
|
|
@@ -39891,7 +40270,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
39891
40270
|
name: "Encryption network",
|
|
39892
40271
|
ok: false,
|
|
39893
40272
|
detail: `${networkName} not found`,
|
|
39894
|
-
hint: `${
|
|
40273
|
+
hint: `${CLI_NAME31} secrets network init ${networkName}`
|
|
39895
40274
|
}
|
|
39896
40275
|
];
|
|
39897
40276
|
let secret;
|
|
@@ -39937,7 +40316,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
39937
40316
|
name: "Secret access",
|
|
39938
40317
|
ok: false,
|
|
39939
40318
|
detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
|
|
39940
|
-
hint: notFound ? local ? `${
|
|
40319
|
+
hint: notFound ? local ? `${CLI_NAME31} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME31} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
|
|
39941
40320
|
});
|
|
39942
40321
|
}
|
|
39943
40322
|
} else {
|
|
@@ -40090,9 +40469,9 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
40090
40469
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
40091
40470
|
}
|
|
40092
40471
|
if (options.file) {
|
|
40093
|
-
secretValue = await
|
|
40472
|
+
secretValue = await readFile14(options.file, "utf-8");
|
|
40094
40473
|
} else if (options.stdin) {
|
|
40095
|
-
secretValue = (await
|
|
40474
|
+
secretValue = (await readStdin4()).toString("utf-8");
|
|
40096
40475
|
} else {
|
|
40097
40476
|
secretValue = value;
|
|
40098
40477
|
}
|
|
@@ -40181,7 +40560,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
40181
40560
|
if (effectiveLocal(profile, options)) {
|
|
40182
40561
|
throw new CLIError(
|
|
40183
40562
|
"LOCAL_OWNER_PROFILE",
|
|
40184
|
-
`Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${
|
|
40563
|
+
`Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME31} secrets put NAME --stdin instead of requesting them.`,
|
|
40185
40564
|
ExitCode.USAGE_ERROR
|
|
40186
40565
|
);
|
|
40187
40566
|
}
|
|
@@ -40304,7 +40683,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
40304
40683
|
|
|
40305
40684
|
// src/commands/share.ts
|
|
40306
40685
|
import { Option } from "commander";
|
|
40307
|
-
import { CLI_NAME as
|
|
40686
|
+
import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
|
|
40308
40687
|
import { ProfileLockTimeoutError as ProfileLockTimeoutError3 } from "@tinycloud/operations/state";
|
|
40309
40688
|
init_errors();
|
|
40310
40689
|
|
|
@@ -40399,7 +40778,7 @@ function receiveJson(result, path) {
|
|
|
40399
40778
|
|
|
40400
40779
|
// src/share/io.ts
|
|
40401
40780
|
import { constants } from "fs";
|
|
40402
|
-
import { lstat as lstat10, mkdir as mkdir6, mkdtemp, open as open13, readFile as
|
|
40781
|
+
import { lstat as lstat10, mkdir as mkdir6, mkdtemp, open as open13, readFile as readFile15, realpath as realpath2, stat as stat6, link as link3, rename as rename5, rm as rm6, unlink as unlink6 } from "fs/promises";
|
|
40403
40782
|
import { randomBytes as randomBytes8 } from "crypto";
|
|
40404
40783
|
import { basename as basename4, join as join18, resolve as resolve6, sep as sep3 } from "path";
|
|
40405
40784
|
init_errors();
|
|
@@ -40446,7 +40825,7 @@ async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
|
|
|
40446
40825
|
const path = resolve6(input);
|
|
40447
40826
|
const info = await stat6(path);
|
|
40448
40827
|
if (!info.isFile() || info.size > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
40449
|
-
const bytes = new Uint8Array(await
|
|
40828
|
+
const bytes = new Uint8Array(await readFile15(path));
|
|
40450
40829
|
if (bytes.byteLength > limit) throw new Error("MAX_BYTES_EXCEEDED");
|
|
40451
40830
|
return { bytes, filename };
|
|
40452
40831
|
}
|
|
@@ -41765,7 +42144,7 @@ function shareCliError(error, operation = "publish") {
|
|
|
41765
42144
|
"SHARE_HISTORY_UNREADABLE",
|
|
41766
42145
|
"sender history cannot be read with the current session key",
|
|
41767
42146
|
1,
|
|
41768
|
-
{ hint: `Run \`${
|
|
42147
|
+
{ hint: `Run \`${CLI_NAME32} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
|
|
41769
42148
|
);
|
|
41770
42149
|
}
|
|
41771
42150
|
if (error instanceof SharePublishAuthorityError) {
|
|
@@ -41773,12 +42152,12 @@ function shareCliError(error, operation = "publish") {
|
|
|
41773
42152
|
const profileName2 = "profileName" in failure ? failure.profileName : void 0;
|
|
41774
42153
|
const localKey = "localKey" in failure && failure.localKey === true;
|
|
41775
42154
|
const profileHint = profileName2 === void 0 ? "" : `--profile ${profileName2} `;
|
|
41776
|
-
const loginHint = localKey ? `\`${
|
|
42155
|
+
const loginHint = localKey ? `\`${CLI_NAME32} ${profileHint}auth login --method local\`` : `\`${CLI_NAME32} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME32} ${profileHint}enable share\`)`;
|
|
41777
42156
|
if (failure.kind === "caveated-session") {
|
|
41778
42157
|
const holder = profileName2 === void 0 ? "this session" : `profile ${profileName2}'s session`;
|
|
41779
42158
|
return new CLIError(
|
|
41780
42159
|
"PERMISSION_DENIED",
|
|
41781
|
-
`${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${
|
|
42160
|
+
`${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${CLI_NAME32} init --name publisher --key-only && ${CLI_NAME32} --profile publisher enable share\``,
|
|
41782
42161
|
5
|
|
41783
42162
|
);
|
|
41784
42163
|
}
|
|
@@ -41879,7 +42258,7 @@ function shareCliError(error, operation = "publish") {
|
|
|
41879
42258
|
if (nodeCode === "ENOENT") return new CLIError("INVALID_ARGUMENT", "share input was not found", 2);
|
|
41880
42259
|
if (nodeCode === "EISDIR") return new CLIError("INVALID_ARGUMENT", "share input must be a Markdown file", 2);
|
|
41881
42260
|
const mapped = typeof nodeCode === "string" && known[nodeCode] !== void 0 ? known[nodeCode] : known[message];
|
|
41882
|
-
const classified = mapped === void 0 ? classifyShareFailure(error, `\`${
|
|
42261
|
+
const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME32} auth login\``) : void 0;
|
|
41883
42262
|
if (classified !== void 0) return Object.assign(new CLIError(classified.code, classified.reason, classified.exitCode), { cause: error });
|
|
41884
42263
|
if (error instanceof TypeError) return new CLIError("INVALID_ARGUMENT", "share input is invalid", 2);
|
|
41885
42264
|
return new CLIError(mapped?.code ?? "INVALID_ARGUMENT", mapped ? mapped.code : "share operation failed", mapped?.exit ?? 2);
|
|
@@ -42289,7 +42668,7 @@ init_constants();
|
|
|
42289
42668
|
init_sdk();
|
|
42290
42669
|
init_host();
|
|
42291
42670
|
init_theme();
|
|
42292
|
-
import { CLI_NAME as
|
|
42671
|
+
import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
|
|
42293
42672
|
import { ACCOUNT_REGISTRY_SPACE } from "@tinycloud/node-sdk";
|
|
42294
42673
|
import { randomBytes as randomBytes9 } from "crypto";
|
|
42295
42674
|
import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -42336,7 +42715,7 @@ function registerSpaceCommand(program) {
|
|
|
42336
42715
|
} else {
|
|
42337
42716
|
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
42338
42717
|
const cap = "tinycloud.kv:account:spaces/:get,list";
|
|
42339
|
-
const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${
|
|
42718
|
+
const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME33} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME33} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
|
|
42340
42719
|
throw new CLIError(
|
|
42341
42720
|
"PERMISSION_DENIED",
|
|
42342
42721
|
`Listing spaces needs tinycloud.kv/get and tinycloud.kv/list on the account space's spaces/ registry (${accountSpaceId ?? ACCOUNT_REGISTRY_SPACE}), or tinycloud.space/list. This session holds neither.`,
|
|
@@ -42373,10 +42752,10 @@ function registerSpaceCommand(program) {
|
|
|
42373
42752
|
|
|
42374
42753
|
A delegate cannot host a space \u2014 only its owner (root authority) can. This
|
|
42375
42754
|
emits a tinycloud.host.request artifact naming the space and its owner so you
|
|
42376
|
-
can hand it to the owner; they run \`${
|
|
42755
|
+
can hand it to the owner; they run \`${CLI_NAME33} space host <name>\` and confirm.
|
|
42377
42756
|
|
|
42378
42757
|
If you ARE the owner of the resolved space, this refuses and tells you to host
|
|
42379
|
-
it directly with \`${
|
|
42758
|
+
it directly with \`${CLI_NAME33} space host <name>\` (no request needed).
|
|
42380
42759
|
`).action(async (name, options, cmd) => {
|
|
42381
42760
|
try {
|
|
42382
42761
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -42387,7 +42766,7 @@ it directly with \`${CLI_NAME32} space host <name>\` (no request needed).
|
|
|
42387
42766
|
if (await isRootAuthority(spaceId, ctx.profile)) {
|
|
42388
42767
|
throw new CLIError(
|
|
42389
42768
|
"ALREADY_ROOT_AUTHORITY",
|
|
42390
|
-
`You are the owner of ${spaceId}. Host it directly: ${
|
|
42769
|
+
`You are the owner of ${spaceId}. Host it directly: ${CLI_NAME33} space host ${spaceName}`,
|
|
42391
42770
|
ExitCode.USAGE_ERROR
|
|
42392
42771
|
);
|
|
42393
42772
|
}
|
|
@@ -42473,7 +42852,7 @@ init_sdk();
|
|
|
42473
42852
|
init_space();
|
|
42474
42853
|
init_host();
|
|
42475
42854
|
init_theme();
|
|
42476
|
-
import { CLI_NAME as
|
|
42855
|
+
import { CLI_NAME as CLI_NAME34 } from "@tinycloud/operations/cli-name";
|
|
42477
42856
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
42478
42857
|
import { resolve as resolve8 } from "path";
|
|
42479
42858
|
async function dbHandle(node, dbName, spaceInput, profileName2) {
|
|
@@ -42497,12 +42876,12 @@ database for simple apps, or pass --db to target a named database. Pass
|
|
|
42497
42876
|
--space to target a non-primary space (e.g. the manifest "applications" space).
|
|
42498
42877
|
|
|
42499
42878
|
Common workflows:
|
|
42500
|
-
$ ${
|
|
42501
|
-
$ ${
|
|
42502
|
-
$ ${
|
|
42503
|
-
$ ${
|
|
42504
|
-
$ ${
|
|
42505
|
-
$ ${
|
|
42879
|
+
$ ${CLI_NAME34} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
|
|
42880
|
+
$ ${CLI_NAME34} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
|
|
42881
|
+
$ ${CLI_NAME34} sql query "SELECT id, body FROM notes ORDER BY id"
|
|
42882
|
+
$ ${CLI_NAME34} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
|
|
42883
|
+
$ ${CLI_NAME34} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
|
|
42884
|
+
$ ${CLI_NAME34} sql export --db analytics --output analytics.db
|
|
42506
42885
|
|
|
42507
42886
|
Commands:
|
|
42508
42887
|
query Read rows with SELECT statements
|
|
@@ -42519,10 +42898,10 @@ Tips:
|
|
|
42519
42898
|
sql.command("query <sql>").description("Run a read-only SELECT query").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
|
|
42520
42899
|
|
|
42521
42900
|
Examples:
|
|
42522
|
-
$ ${
|
|
42523
|
-
$ ${
|
|
42524
|
-
$ ${
|
|
42525
|
-
$ ${
|
|
42901
|
+
$ ${CLI_NAME34} sql query "SELECT * FROM notes ORDER BY id"
|
|
42902
|
+
$ ${CLI_NAME34} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
|
|
42903
|
+
$ ${CLI_NAME34} sql query "SELECT count(*) AS total FROM events" --db analytics --json
|
|
42904
|
+
$ ${CLI_NAME34} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
|
|
42526
42905
|
|
|
42527
42906
|
Output:
|
|
42528
42907
|
Human output is formatted as a table. Piped output or --json returns
|
|
@@ -42563,11 +42942,11 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
42563
42942
|
sql.command("execute <sql>").description("Run a write or schema statement").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
|
|
42564
42943
|
|
|
42565
42944
|
Examples:
|
|
42566
|
-
$ ${
|
|
42567
|
-
$ ${
|
|
42568
|
-
$ ${
|
|
42569
|
-
$ ${
|
|
42570
|
-
$ ${
|
|
42945
|
+
$ ${CLI_NAME34} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
|
|
42946
|
+
$ ${CLI_NAME34} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
|
|
42947
|
+
$ ${CLI_NAME34} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
|
|
42948
|
+
$ ${CLI_NAME34} sql execute "DROP TABLE old_notes" --db archive
|
|
42949
|
+
$ ${CLI_NAME34} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
|
|
42571
42950
|
|
|
42572
42951
|
Output:
|
|
42573
42952
|
Returns JSON with the changed row count and last inserted row id when available.
|
|
@@ -42596,9 +42975,9 @@ Output:
|
|
|
42596
42975
|
sql.command("export").description("Export a SQLite database as a binary .db file").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("-o, --output <file>", "Output file path", "export.db").addHelpText("after", `
|
|
42597
42976
|
|
|
42598
42977
|
Examples:
|
|
42599
|
-
$ ${
|
|
42600
|
-
$ ${
|
|
42601
|
-
$ ${
|
|
42978
|
+
$ ${CLI_NAME34} sql export
|
|
42979
|
+
$ ${CLI_NAME34} sql export --db analytics --output analytics.db
|
|
42980
|
+
$ ${CLI_NAME34} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
|
|
42602
42981
|
|
|
42603
42982
|
Output:
|
|
42604
42983
|
Writes the database file locally and returns JSON with the path and size.
|
|
@@ -42631,19 +43010,19 @@ Output:
|
|
|
42631
43010
|
sql.command("copy").description("Copy rows between SQL databases (optionally across spaces)").requiredOption("--from-db <name>", "Source database name").requiredOption("--to-db <name>", "Destination database name").option("--from-space <name|uri>", "Source space (defaults to primary)").option("--to-space <name|uri>", "Destination space (defaults to primary)").option("--table <name...>", "Restrict copy to specific tables (repeat or comma-separated)").option("--dry-run", "Print the plan without writing", false).addHelpText("after", `
|
|
42632
43011
|
|
|
42633
43012
|
Examples:
|
|
42634
|
-
$ ${
|
|
43013
|
+
$ ${CLI_NAME34} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
|
|
42635
43014
|
--to-db xyz.tinycloud.listen/conversations \\
|
|
42636
43015
|
--space applications --dry-run
|
|
42637
|
-
$ ${
|
|
43016
|
+
$ ${CLI_NAME34} sql copy --from-space applications --from-db com.foo/data \\
|
|
42638
43017
|
--to-space applications --to-db com.bar/data \\
|
|
42639
43018
|
--table conversation --table participant
|
|
42640
43019
|
|
|
42641
43020
|
Notes:
|
|
42642
43021
|
- Refuses to run when (resolved space, db) is identical for source and destination.
|
|
42643
|
-
- Does NOT create destination tables. Run the target app once (or use \`${
|
|
43022
|
+
- Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME34} sql execute\`)
|
|
42644
43023
|
to materialize the schema before copying.
|
|
42645
43024
|
- One row at a time; suitable for small/medium datasets. Large copies should
|
|
42646
|
-
use \`${
|
|
43025
|
+
use \`${CLI_NAME34} sql export\` + bulk import.
|
|
42647
43026
|
- Authorization: the active session/delegation must cover sql/read on source
|
|
42648
43027
|
AND sql/write on destination. Otherwise the relevant operation will fail.
|
|
42649
43028
|
`).action(async (options, cmd) => {
|
|
@@ -42743,7 +43122,7 @@ init_errors();
|
|
|
42743
43122
|
init_formatter();
|
|
42744
43123
|
init_theme();
|
|
42745
43124
|
init_permissions();
|
|
42746
|
-
import { CLI_NAME as
|
|
43125
|
+
import { CLI_NAME as CLI_NAME35 } from "@tinycloud/operations/cli-name";
|
|
42747
43126
|
import {
|
|
42748
43127
|
NodeWasmBindings as NodeWasmBindings2
|
|
42749
43128
|
} from "@tinycloud/node-sdk";
|
|
@@ -43017,7 +43396,7 @@ function formatStatus(summary) {
|
|
|
43017
43396
|
lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
|
|
43018
43397
|
lines.push("");
|
|
43019
43398
|
if (summary.profiles.length === 0) {
|
|
43020
|
-
lines.push(theme.muted(`No profiles configured. Run: ${
|
|
43399
|
+
lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME35} init`));
|
|
43021
43400
|
return `${lines.join("\n")}
|
|
43022
43401
|
`;
|
|
43023
43402
|
}
|
|
@@ -43173,14 +43552,14 @@ function registerUpgradeCommand(program) {
|
|
|
43173
43552
|
}
|
|
43174
43553
|
|
|
43175
43554
|
// src/commands/vault.ts
|
|
43176
|
-
import { readFile as
|
|
43555
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
43177
43556
|
init_profiles();
|
|
43178
43557
|
init_formatter();
|
|
43179
43558
|
init_errors();
|
|
43180
43559
|
init_constants();
|
|
43181
43560
|
init_sdk();
|
|
43182
43561
|
import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
|
|
43183
|
-
async function
|
|
43562
|
+
async function readStdin5() {
|
|
43184
43563
|
const chunks = [];
|
|
43185
43564
|
for await (const chunk of process.stdin) {
|
|
43186
43565
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -43235,9 +43614,9 @@ function registerVaultCommand(program) {
|
|
|
43235
43614
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
43236
43615
|
}
|
|
43237
43616
|
if (options.file) {
|
|
43238
|
-
putValue = new Uint8Array(await
|
|
43617
|
+
putValue = new Uint8Array(await readFile16(options.file));
|
|
43239
43618
|
} else if (options.stdin) {
|
|
43240
|
-
putValue = new Uint8Array(await
|
|
43619
|
+
putValue = new Uint8Array(await readStdin5());
|
|
43241
43620
|
} else {
|
|
43242
43621
|
putValue = value;
|
|
43243
43622
|
}
|
|
@@ -43350,14 +43729,14 @@ function registerVaultCommand(program) {
|
|
|
43350
43729
|
}
|
|
43351
43730
|
|
|
43352
43731
|
// src/commands/vars.ts
|
|
43353
|
-
import { readFile as
|
|
43732
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
43354
43733
|
init_profiles();
|
|
43355
43734
|
init_formatter();
|
|
43356
43735
|
init_errors();
|
|
43357
43736
|
init_constants();
|
|
43358
43737
|
init_sdk();
|
|
43359
43738
|
var VARIABLES_PREFIX = "variables/";
|
|
43360
|
-
async function
|
|
43739
|
+
async function readStdin6() {
|
|
43361
43740
|
const chunks = [];
|
|
43362
43741
|
for await (const chunk of process.stdin) {
|
|
43363
43742
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -43455,9 +43834,9 @@ function registerVarsCommand(program) {
|
|
|
43455
43834
|
throw new CLIError("USAGE_ERROR", "Provide only one of: value argument, --file, or --stdin", ExitCode.USAGE_ERROR);
|
|
43456
43835
|
}
|
|
43457
43836
|
if (options.file) {
|
|
43458
|
-
varValue = await
|
|
43837
|
+
varValue = await readFile17(options.file, "utf-8");
|
|
43459
43838
|
} else if (options.stdin) {
|
|
43460
|
-
varValue = (await
|
|
43839
|
+
varValue = (await readStdin6()).toString("utf-8");
|
|
43461
43840
|
} else {
|
|
43462
43841
|
varValue = value;
|
|
43463
43842
|
}
|
|
@@ -43500,6 +43879,7 @@ function registerTinyCloudCommands(program) {
|
|
|
43500
43879
|
registerAuthCommand(program);
|
|
43501
43880
|
registerEnableCommand(program);
|
|
43502
43881
|
registerKvCommand(program);
|
|
43882
|
+
registerKeysCommand(program);
|
|
43503
43883
|
registerReplicaCommand(program);
|
|
43504
43884
|
registerCourierCommand(program);
|
|
43505
43885
|
registerSpaceCommand(program);
|