@tinycloud/cli 1.0.0-beta.22 → 1.0.0-beta.23
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 +19 -0
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +392 -232
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +3 -3
- package/skills/tc-cli/AUTH.md +29 -3
- package/skills/tc-cli/REFERENCE.md +22 -4
package/dist/legacy-entry.js
CHANGED
|
@@ -12993,6 +12993,35 @@ init_errors();
|
|
|
12993
12993
|
init_constants();
|
|
12994
12994
|
init_space();
|
|
12995
12995
|
|
|
12996
|
+
// src/lib/raw-encryption.ts
|
|
12997
|
+
function isRawEncryptionPermission(permission) {
|
|
12998
|
+
return (permission.service === ENCRYPTION_PERMISSION_SERVICE2 || permission.service === "encryption") && typeof permission.path === "string" && permission.path.startsWith("urn:tinycloud:encryption:");
|
|
12999
|
+
}
|
|
13000
|
+
function isVerifiedRawEncryptionPermission(permission) {
|
|
13001
|
+
return isRawEncryptionPermission(permission) && (permission.space === void 0 || permission.space === "encryption");
|
|
13002
|
+
}
|
|
13003
|
+
|
|
13004
|
+
// src/lib/owner-did.ts
|
|
13005
|
+
init_constants();
|
|
13006
|
+
init_errors();
|
|
13007
|
+
import { ensureEip55 } from "@tinycloud/node-sdk-wasm";
|
|
13008
|
+
function canonicalOwnerDid(did, label = "--owner") {
|
|
13009
|
+
const match = /^did:pkh:eip155:([1-9]\d*):(0x[0-9a-fA-F]{40})$/.exec(did);
|
|
13010
|
+
if (!match) {
|
|
13011
|
+
throw new CLIError("INVALID_ARGUMENT", `${label} "${did}" is not a did:pkh:eip155:CHAIN:ADDRESS identity.`, ExitCode.USAGE_ERROR);
|
|
13012
|
+
}
|
|
13013
|
+
return `did:pkh:eip155:${match[1]}:${ensureEip55(match[2])}`;
|
|
13014
|
+
}
|
|
13015
|
+
function rawEncryptionOwnerMatches(path, ownerDid) {
|
|
13016
|
+
const match = /^urn:tinycloud:encryption:(did:pkh:eip155:.+):([a-z0-9][a-z0-9-]*)$/.exec(path);
|
|
13017
|
+
if (!match) return false;
|
|
13018
|
+
try {
|
|
13019
|
+
return canonicalOwnerDid(match[1]) === canonicalOwnerDid(ownerDid);
|
|
13020
|
+
} catch {
|
|
13021
|
+
return false;
|
|
13022
|
+
}
|
|
13023
|
+
}
|
|
13024
|
+
|
|
12996
13025
|
// src/share/publishing-manifest.ts
|
|
12997
13026
|
var SHARE_PUBLISHING_MANIFEST_REF = "builtin:share-publishing";
|
|
12998
13027
|
var SHARE_PUBLISHING_MANIFEST = {
|
|
@@ -13072,6 +13101,9 @@ function didWithoutFragment(did) {
|
|
|
13072
13101
|
async function loadAdditionalDelegations(profile) {
|
|
13073
13102
|
return readAdditionalDelegations(profile);
|
|
13074
13103
|
}
|
|
13104
|
+
async function saveAdditionalDelegations(profile, entries) {
|
|
13105
|
+
await replaceSharedRecords(profile, "additional-delegations", entries);
|
|
13106
|
+
}
|
|
13075
13107
|
async function appendAdditionalDelegation(profile, entry) {
|
|
13076
13108
|
await upsertProfileRecord(
|
|
13077
13109
|
profile,
|
|
@@ -13093,6 +13125,9 @@ async function appendPermissionRequestArtifact(profile, artifact) {
|
|
|
13093
13125
|
await writeSharedRecords(profile, "auth-requests", next);
|
|
13094
13126
|
});
|
|
13095
13127
|
}
|
|
13128
|
+
async function replaceSharedRecords(profile, store, entries) {
|
|
13129
|
+
await withProfileLock2(profile, () => writeSharedRecords(profile, store, entries));
|
|
13130
|
+
}
|
|
13096
13131
|
async function writeSharedRecords(profile, store, entries) {
|
|
13097
13132
|
const path = store === "additional-delegations" ? additionalDelegationsPath(profile) : permissionRequestsPath(profile);
|
|
13098
13133
|
await writeJsonAtomic(path, entries);
|
|
@@ -13180,27 +13215,36 @@ async function loadPermissionRequest(source, profile) {
|
|
|
13180
13215
|
async function loadManifestPermissions(source, profile, options = {}) {
|
|
13181
13216
|
const raw = await loadManifestText(source);
|
|
13182
13217
|
const manifest = JSON.parse(raw);
|
|
13218
|
+
if (options.device && typeof manifest.app_id === "string" && (manifest.secrets !== void 0 || manifest.space === "secrets" || typeof manifest.space === "string" && manifest.space.endsWith(":secrets") || Array.isArray(manifest.permissions) && manifest.permissions.some((entry) => entry !== null && typeof entry === "object" && normalizeService(String(entry.service ?? "")) === "tinycloud.encryption"))) {
|
|
13219
|
+
throw new CLIError("DEVICE_AUTH_UNSUPPORTED_SCOPE", "--device cannot authorize tinycloud.encryption or the secrets space. Use browser or --paste login.", ExitCode.USAGE_ERROR);
|
|
13220
|
+
}
|
|
13183
13221
|
if (typeof manifest.id === "string") {
|
|
13184
13222
|
const resolved = resolveManifest(manifest);
|
|
13223
|
+
if (options.device && resolved.resources.some((entry) => entry.service === "tinycloud.encryption" || entry.space === "secrets" || entry.space?.endsWith(":secrets"))) {
|
|
13224
|
+
throw new CLIError("DEVICE_AUTH_UNSUPPORTED_SCOPE", "--device cannot authorize tinycloud.encryption or the secrets space. Use browser or --paste login.", ExitCode.USAGE_ERROR);
|
|
13225
|
+
}
|
|
13185
13226
|
return resolvePermissionSpaces(resolved.resources, profile, options);
|
|
13186
13227
|
}
|
|
13187
13228
|
if (typeof manifest.app_id === "string") {
|
|
13188
13229
|
const permissions = (manifest.permissions ?? []).filter((entry) => entry !== null && typeof entry === "object").map((entry) => {
|
|
13189
13230
|
const service = normalizeService(String(entry.service ?? ""));
|
|
13190
13231
|
const path = String(entry.path ?? "");
|
|
13191
|
-
const
|
|
13192
|
-
|
|
13232
|
+
const actions = expandActionShortNames(
|
|
13233
|
+
service,
|
|
13234
|
+
Array.isArray(entry.actions) ? entry.actions.map(String) : []
|
|
13235
|
+
);
|
|
13236
|
+
if (isRawEncryptionPermission({ service, path })) {
|
|
13237
|
+
return { service, space: ENCRYPTION_MANIFEST_SPACE2, path, actions };
|
|
13238
|
+
}
|
|
13239
|
+
const resolvedPath = entry.skipPrefix === true ? path : prefixAppManifestPath(path, manifest.app_id);
|
|
13193
13240
|
return {
|
|
13194
13241
|
service,
|
|
13195
13242
|
space: String(manifest.space ?? "applications"),
|
|
13196
13243
|
path: resolvedPath,
|
|
13197
|
-
actions
|
|
13198
|
-
service,
|
|
13199
|
-
Array.isArray(entry.actions) ? entry.actions.map(String) : []
|
|
13200
|
-
)
|
|
13244
|
+
actions
|
|
13201
13245
|
};
|
|
13202
13246
|
});
|
|
13203
|
-
permissions.push(...await secretPermissionsFromAppManifest(manifest, profile));
|
|
13247
|
+
permissions.push(...await secretPermissionsFromAppManifest(manifest, profile, options.ownerDid));
|
|
13204
13248
|
return resolvePermissionSpaces(permissions, profile, options);
|
|
13205
13249
|
}
|
|
13206
13250
|
throw new CLIError(
|
|
@@ -13209,7 +13253,7 @@ async function loadManifestPermissions(source, profile, options = {}) {
|
|
|
13209
13253
|
ExitCode.USAGE_ERROR
|
|
13210
13254
|
);
|
|
13211
13255
|
}
|
|
13212
|
-
async function secretPermissionsFromAppManifest(manifest, profile) {
|
|
13256
|
+
async function secretPermissionsFromAppManifest(manifest, profile, requestedOwner) {
|
|
13213
13257
|
if (manifest.secrets === void 0) {
|
|
13214
13258
|
return [];
|
|
13215
13259
|
}
|
|
@@ -13230,24 +13274,25 @@ async function secretPermissionsFromAppManifest(manifest, profile) {
|
|
|
13230
13274
|
permissions.push({
|
|
13231
13275
|
service: ENCRYPTION_PERMISSION_SERVICE2,
|
|
13232
13276
|
space: ENCRYPTION_MANIFEST_SPACE2,
|
|
13233
|
-
path: await defaultSecretsNetworkId(profile),
|
|
13234
|
-
actions: ["tinycloud.encryption/decrypt"]
|
|
13235
|
-
skipPrefix: true
|
|
13277
|
+
path: await defaultSecretsNetworkId(profile, requestedOwner),
|
|
13278
|
+
actions: ["tinycloud.encryption/decrypt"]
|
|
13236
13279
|
});
|
|
13237
13280
|
}
|
|
13238
13281
|
return permissions;
|
|
13239
13282
|
}
|
|
13240
|
-
async function defaultSecretsNetworkId(profileName) {
|
|
13283
|
+
async function defaultSecretsNetworkId(profileName, requestedOwner) {
|
|
13241
13284
|
const profile = await ProfileManager.getProfile(profileName);
|
|
13242
|
-
const
|
|
13243
|
-
|
|
13285
|
+
const ownDid = profile.did?.split("#")[0];
|
|
13286
|
+
const recorded = profile.ownerDid ?? (ownDid?.startsWith("did:pkh:") ? ownDid : void 0);
|
|
13287
|
+
const owner = (recorded ?? requestedOwner)?.split("#")[0];
|
|
13288
|
+
if (!owner) {
|
|
13244
13289
|
throw new CLIError(
|
|
13245
13290
|
"OWNER_DID_UNKNOWN",
|
|
13246
|
-
`Cannot determine owner
|
|
13291
|
+
`Cannot determine the secrets owner for profile "${profileName}". Pass --owner did:pkh:eip155:CHAIN:ADDRESS with --manifest.`,
|
|
13247
13292
|
ExitCode.AUTH_REQUIRED
|
|
13248
13293
|
);
|
|
13249
13294
|
}
|
|
13250
|
-
return `urn:tinycloud:encryption:${
|
|
13295
|
+
return `urn:tinycloud:encryption:${canonicalOwnerDid(owner, "Secrets owner")}:default`;
|
|
13251
13296
|
}
|
|
13252
13297
|
function permissionsFromDelegation(delegation) {
|
|
13253
13298
|
if (delegation.resources?.length) {
|
|
@@ -13278,6 +13323,12 @@ async function resolvePermissionSpaces(entries, profile, options = {}) {
|
|
|
13278
13323
|
const resolved = [];
|
|
13279
13324
|
for (const entry of entries) {
|
|
13280
13325
|
const service = normalizeService(entry.service);
|
|
13326
|
+
const actions = expandActionShortNames(service, entry.actions);
|
|
13327
|
+
if (isRawEncryptionPermission({ service, path: entry.path })) {
|
|
13328
|
+
const { skipPrefix: _skipPrefix, ...raw } = entry;
|
|
13329
|
+
resolved.push({ ...raw, service, space: ENCRYPTION_MANIFEST_SPACE2, actions });
|
|
13330
|
+
continue;
|
|
13331
|
+
}
|
|
13281
13332
|
let space;
|
|
13282
13333
|
try {
|
|
13283
13334
|
space = await resolveSpaceUri(entry.space, profile) ?? entry.space;
|
|
@@ -13291,7 +13342,7 @@ async function resolvePermissionSpaces(entries, profile, options = {}) {
|
|
|
13291
13342
|
...entry,
|
|
13292
13343
|
service,
|
|
13293
13344
|
space,
|
|
13294
|
-
actions
|
|
13345
|
+
actions
|
|
13295
13346
|
});
|
|
13296
13347
|
}
|
|
13297
13348
|
return resolved;
|
|
@@ -13826,8 +13877,8 @@ import { grantAuthRequest, principalDidEquals } from "@tinycloud/node-sdk";
|
|
|
13826
13877
|
import { invokeOperation } from "@tinycloud/operations";
|
|
13827
13878
|
|
|
13828
13879
|
// src/auth/browser-auth.ts
|
|
13829
|
-
init_formatter();
|
|
13830
13880
|
init_constants();
|
|
13881
|
+
init_errors();
|
|
13831
13882
|
import { createServer } from "http";
|
|
13832
13883
|
import { createInterface } from "readline";
|
|
13833
13884
|
var PRIVATE_JWK_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -13856,7 +13907,7 @@ async function startAuthFlow(did, options = {}) {
|
|
|
13856
13907
|
try {
|
|
13857
13908
|
return await callbackFlow(did, options);
|
|
13858
13909
|
} catch {
|
|
13859
|
-
if (
|
|
13910
|
+
if (process.stdin.isTTY) {
|
|
13860
13911
|
console.error("Could not open browser. Falling back to manual paste mode.");
|
|
13861
13912
|
return pasteFlow(did, options);
|
|
13862
13913
|
}
|
|
@@ -14029,22 +14080,23 @@ async function callbackFlow(did, options = {}) {
|
|
|
14029
14080
|
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
14030
14081
|
const authUrl = buildAuthUrl(did, { ...options, callback: callbackUrl });
|
|
14031
14082
|
const openBrowser = shouldOpenBrowser(options);
|
|
14032
|
-
|
|
14083
|
+
const hasTerminal = Boolean(process.stdin.isTTY || process.stderr.isTTY);
|
|
14084
|
+
if (openBrowser && hasTerminal) {
|
|
14033
14085
|
console.error(`Opening browser for authentication...`);
|
|
14034
14086
|
console.error(`If the browser doesn't open, visit: ${authUrl}`);
|
|
14035
|
-
} else if (!openBrowser ||
|
|
14087
|
+
} else if (!openBrowser || hasTerminal) {
|
|
14036
14088
|
console.error(`Open this URL in a browser to authenticate: ${authUrl}`);
|
|
14037
14089
|
}
|
|
14038
14090
|
if (openBrowser) {
|
|
14039
14091
|
try {
|
|
14040
|
-
const
|
|
14041
|
-
await
|
|
14092
|
+
const open5 = (await import("open")).default;
|
|
14093
|
+
await open5(authUrl);
|
|
14042
14094
|
} catch {
|
|
14043
|
-
|
|
14044
|
-
|
|
14095
|
+
settle({ error: new Error("Failed to open browser") });
|
|
14096
|
+
return;
|
|
14045
14097
|
}
|
|
14046
14098
|
}
|
|
14047
|
-
if (
|
|
14099
|
+
if (process.stdin.isTTY) {
|
|
14048
14100
|
console.error(`
|
|
14049
14101
|
If the browser can't connect back, paste the delegation code here:`);
|
|
14050
14102
|
rl = createInterface({
|
|
@@ -14079,7 +14131,10 @@ Open this URL in a browser to authenticate:
|
|
|
14079
14131
|
output: process.stderr
|
|
14080
14132
|
});
|
|
14081
14133
|
return new Promise((resolve4, reject) => {
|
|
14082
|
-
|
|
14134
|
+
let answered = false;
|
|
14135
|
+
rl.on("line", (input) => {
|
|
14136
|
+
if (answered || input.trim() === "") return;
|
|
14137
|
+
answered = true;
|
|
14083
14138
|
rl.close();
|
|
14084
14139
|
let parsed;
|
|
14085
14140
|
try {
|
|
@@ -14100,6 +14155,21 @@ Open this URL in a browser to authenticate:
|
|
|
14100
14155
|
}
|
|
14101
14156
|
resolve4(parsed);
|
|
14102
14157
|
});
|
|
14158
|
+
rl.on("close", () => {
|
|
14159
|
+
if (answered) return;
|
|
14160
|
+
answered = true;
|
|
14161
|
+
reject(new CLIError(
|
|
14162
|
+
"PASTE_CODE_MISSING",
|
|
14163
|
+
`Stdin ended before a delegation code was pasted; no session was saved. Approve at ${authUrl} and pass the returned code on stdin.`,
|
|
14164
|
+
ExitCode.AUTH_REQUIRED,
|
|
14165
|
+
{
|
|
14166
|
+
approvalUrl: authUrl,
|
|
14167
|
+
hint: `Open ${authUrl}, approve, then pass the code on stdin followed by a newline.`
|
|
14168
|
+
}
|
|
14169
|
+
));
|
|
14170
|
+
});
|
|
14171
|
+
rl.setPrompt("Paste delegation code: ");
|
|
14172
|
+
rl.prompt();
|
|
14103
14173
|
});
|
|
14104
14174
|
}
|
|
14105
14175
|
|
|
@@ -14220,6 +14290,7 @@ function parseExpiry2(input) {
|
|
|
14220
14290
|
|
|
14221
14291
|
// src/auth/scoped-login.ts
|
|
14222
14292
|
init_space();
|
|
14293
|
+
var CAPABILITIES_READ = "tinycloud.capabilities/read";
|
|
14223
14294
|
var CLOCK_SKEW_MS = 3e4;
|
|
14224
14295
|
var SIGNED_RECAP = "signed-recap";
|
|
14225
14296
|
function parseRequestedExpiry(value) {
|
|
@@ -14289,8 +14360,13 @@ function permissionTuples(permissions, ownerDid) {
|
|
|
14289
14360
|
}
|
|
14290
14361
|
function actionTuples(permission, ownerDid) {
|
|
14291
14362
|
const service = permission.service.startsWith("tinycloud.") ? permission.service : `tinycloud.${permission.service}`;
|
|
14292
|
-
const
|
|
14293
|
-
|
|
14363
|
+
const raw = isVerifiedRawEncryptionPermission(permission);
|
|
14364
|
+
const space = raw ? ENCRYPTION_MANIFEST_SPACE2 : normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid));
|
|
14365
|
+
const path = raw ? normalizePkhIdentifier(permission.path) : permission.path;
|
|
14366
|
+
return permission.actions.map((action) => JSON.stringify([service, space, path, action.includes("/") ? action : `${service}/${action}`]));
|
|
14367
|
+
}
|
|
14368
|
+
function isLegacyNestedDecrypt(permission, requested, ownerDid, spaceId) {
|
|
14369
|
+
return isRawEncryptionPermission(permission) && !isVerifiedRawEncryptionPermission(permission) && normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid)) === normalizePkhIdentifier(spaceId) && rawEncryptionOwnerMatches(permission.path, ownerDid) && permission.actions.every((action) => action === "tinycloud.encryption/decrypt") && requested.some((entry) => isRawEncryptionPermission(entry) && normalizePkhIdentifier(entry.path) === normalizePkhIdentifier(permission.path) && permission.actions.every((action) => entry.actions.includes(action)));
|
|
14294
14370
|
}
|
|
14295
14371
|
function canonicalJson(value) {
|
|
14296
14372
|
const canonical = (entry) => Array.isArray(entry) ? entry.map(canonical) : entry && typeof entry === "object" ? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b)).map(([name, inner]) => [name, canonical(inner)])) : entry;
|
|
@@ -14352,14 +14428,36 @@ function permissionsFromTuples(tuples) {
|
|
|
14352
14428
|
}
|
|
14353
14429
|
return [...grouped.values()];
|
|
14354
14430
|
}
|
|
14431
|
+
function declinedPermissions(requested, signed, ownerDid) {
|
|
14432
|
+
const granted = permissionTuples(signed, ownerDid);
|
|
14433
|
+
const missing = [...permissionTuples(requested, ownerDid)].filter((tuple) => !granted.has(tuple));
|
|
14434
|
+
const requestedScopes = /* @__PURE__ */ new Map();
|
|
14435
|
+
for (const entry of requested) {
|
|
14436
|
+
const space = isVerifiedRawEncryptionPermission(entry) ? entry.space ?? ENCRYPTION_MANIFEST_SPACE2 : ownerSpaceId(entry.space ?? "", ownerDid);
|
|
14437
|
+
for (const tuple of actionTuples(entry, ownerDid)) {
|
|
14438
|
+
requestedScopes.set(tuple, { space, path: entry.path });
|
|
14439
|
+
}
|
|
14440
|
+
}
|
|
14441
|
+
return permissionsFromTuples(missing).map((entry) => {
|
|
14442
|
+
const original = requestedScopes.get(actionTuples(entry, ownerDid)[0]);
|
|
14443
|
+
return original === void 0 ? entry : { ...entry, ...original };
|
|
14444
|
+
});
|
|
14445
|
+
}
|
|
14355
14446
|
function validateLoginPermissions(permissions) {
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14447
|
+
const spaced = permissions.filter((p) => !isRawEncryptionPermission(p));
|
|
14448
|
+
if (!spaced.length || permissions.some(
|
|
14449
|
+
(p) => !p.service?.startsWith("tinycloud.") || !p.space || (isRawEncryptionPermission(p) ? p.space !== ENCRYPTION_MANIFEST_SPACE2 : !p.space.startsWith("tinycloud:") && !/^[A-Za-z0-9_-]+$/.test(p.space)) || typeof p.path !== "string" || !p.actions?.length || p.actions.some((action) => !action.startsWith(`${p.service}/`))
|
|
14450
|
+
) || new Set(spaced.map((p) => normalizePkhIdentifier(p.space ?? ""))).size !== 1) {
|
|
14451
|
+
throw new CLIError("INVALID_LOGIN_SCOPE", "First login requires non-empty permissions in one TinyCloud space (raw tinycloud.encryption network entries may accompany them). Request additional spaces after login.", ExitCode.USAGE_ERROR);
|
|
14360
14452
|
}
|
|
14361
14453
|
}
|
|
14454
|
+
function scopedLoginPermissions(permissions) {
|
|
14455
|
+
const space = permissions.find((p) => !isRawEncryptionPermission(p)).space ?? "";
|
|
14456
|
+
const hasRead = permissions.some((p) => p.service === "tinycloud.capabilities" && p.path === "" && p.actions.includes(CAPABILITIES_READ) && normalizePkhIdentifier(p.space ?? "") === normalizePkhIdentifier(space));
|
|
14457
|
+
return hasRead ? permissions : [{ service: "tinycloud.capabilities", space, path: "", actions: [CAPABILITIES_READ] }, ...permissions];
|
|
14458
|
+
}
|
|
14362
14459
|
function verifySignedSession(data, key, sessionDid, expected = {}) {
|
|
14460
|
+
const notStored = expected.purpose === "grant" ? "No grant was stored." : "No session was saved.";
|
|
14363
14461
|
let permissions;
|
|
14364
14462
|
let expiresAt;
|
|
14365
14463
|
try {
|
|
@@ -14384,35 +14482,48 @@ function verifySignedSession(data, key, sessionDid, expected = {}) {
|
|
|
14384
14482
|
expiresAt = proof.expiresAt;
|
|
14385
14483
|
} catch (error) {
|
|
14386
14484
|
if (/expir/i.test(error instanceof Error ? error.message : String(error))) {
|
|
14387
|
-
throw new CLIError("AUTH_EXPIRED",
|
|
14485
|
+
throw new CLIError("AUTH_EXPIRED", `The approved session has expired. ${notStored}`, ExitCode.AUTH_REQUIRED);
|
|
14388
14486
|
}
|
|
14389
|
-
throw new CLIError("OPENKEY_PROOF_INVALID",
|
|
14487
|
+
throw new CLIError("OPENKEY_PROOF_INVALID", `OpenKey did not return a complete, verifiable session proof. ${notStored}`, ExitCode.AUTH_REQUIRED);
|
|
14390
14488
|
}
|
|
14391
14489
|
const signedExpiry = Date.parse(expiresAt);
|
|
14392
14490
|
if (signedExpiry <= Date.now()) {
|
|
14393
|
-
throw new CLIError("AUTH_EXPIRED",
|
|
14491
|
+
throw new CLIError("AUTH_EXPIRED", `The approved session has expired. ${notStored}`, ExitCode.AUTH_REQUIRED);
|
|
14394
14492
|
}
|
|
14395
14493
|
if (expected.expiry !== void 0 && signedExpiry > expiryLimit(expected.expiry)) {
|
|
14396
|
-
throw new CLIError("OPENKEY_EXPIRY_EXCEEDED",
|
|
14494
|
+
throw new CLIError("OPENKEY_EXPIRY_EXCEEDED", `The signed session outlives the requested --expiry. ${notStored}`, ExitCode.PERMISSION_DENIED);
|
|
14397
14495
|
}
|
|
14398
14496
|
const ownerDid = `did:pkh:eip155:${data.chainId}:${data.address}`;
|
|
14399
14497
|
if (expected.expectedOwner && normalizePkhIdentifier(expected.expectedOwner) !== normalizePkhIdentifier(ownerDid)) {
|
|
14400
|
-
throw new CLIError("OPENKEY_OWNER_MISMATCH",
|
|
14498
|
+
throw new CLIError("OPENKEY_OWNER_MISMATCH", `The approved signing identity differs from this profile's owner. ${notStored} Use a new profile for another account.`, ExitCode.PERMISSION_DENIED);
|
|
14401
14499
|
}
|
|
14402
14500
|
return { ownerDid, permissions, expiresAt };
|
|
14403
14501
|
}
|
|
14404
14502
|
function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
|
|
14503
|
+
const notStored = expected.purpose === "grant" ? "No grant was stored." : "No scoped session was saved.";
|
|
14405
14504
|
const signed = verifySignedSession(data, key, sessionDid, expected);
|
|
14406
14505
|
const spaceId = data.spaceId;
|
|
14407
14506
|
for (const permission of requested) {
|
|
14507
|
+
if (isRawEncryptionPermission(permission)) continue;
|
|
14408
14508
|
if (normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", signed.ownerDid)) !== normalizePkhIdentifier(spaceId)) {
|
|
14409
|
-
throw new CLIError("OPENKEY_SCOPE_MISMATCH",
|
|
14509
|
+
throw new CLIError("OPENKEY_SCOPE_MISMATCH", `The approved space differs from the requested space. ${notStored}`, ExitCode.PERMISSION_DENIED);
|
|
14410
14510
|
}
|
|
14411
14511
|
}
|
|
14412
|
-
|
|
14413
|
-
|
|
14512
|
+
const legacyNested = [];
|
|
14513
|
+
const approved = [];
|
|
14514
|
+
for (const permission of signed.permissions) {
|
|
14515
|
+
(isLegacyNestedDecrypt(permission, requested, signed.ownerDid, spaceId) ? legacyNested : approved).push(permission);
|
|
14516
|
+
}
|
|
14517
|
+
for (const permission of signed.permissions) {
|
|
14518
|
+
if (isVerifiedRawEncryptionPermission(permission) && !rawEncryptionOwnerMatches(permission.path, signed.ownerDid)) {
|
|
14519
|
+
throw new CLIError("OPENKEY_SCOPE_MISMATCH", `OpenKey signed decrypt for a network not owned by the approving identity. ${notStored}`, ExitCode.PERMISSION_DENIED);
|
|
14520
|
+
}
|
|
14414
14521
|
}
|
|
14415
|
-
|
|
14522
|
+
if (!scopeCovers(requested, approved, signed.ownerDid)) {
|
|
14523
|
+
throw new CLIError("OPENKEY_GRANT_BROADENED", `The signed grant contains authority beyond the requested ${expected.purpose === "grant" ? "grant" : "manifest"}. ${notStored}`, ExitCode.PERMISSION_DENIED);
|
|
14524
|
+
}
|
|
14525
|
+
const session = withVerifiedAuthority({ ...data, jwk: key }, { ...signed, permissions: approved });
|
|
14526
|
+
return { session, legacyNested };
|
|
14416
14527
|
}
|
|
14417
14528
|
|
|
14418
14529
|
// src/auth/login-commit.ts
|
|
@@ -14468,7 +14579,8 @@ function assertSessionReplaceable(profileName, snapshot, ownerDid, scope, newExp
|
|
|
14468
14579
|
if (session === null) return;
|
|
14469
14580
|
const expiresAt = sessionExpiresAt(session);
|
|
14470
14581
|
if (expiresAt !== null && Date.parse(expiresAt) <= Date.now()) return;
|
|
14471
|
-
const
|
|
14582
|
+
const held = Array.isArray(session.permissions) ? session.permissions.filter((permission) => ownerDid === void 0 || typeof session.spaceId !== "string" || !isLegacyNestedDecrypt(permission, scope, ownerDid, session.spaceId)) : void 0;
|
|
14583
|
+
const keepsScope = ownerDid !== void 0 && session.permissionsSource === SIGNED_RECAP && held !== void 0 && scopeCovers(scope, held, ownerDid);
|
|
14472
14584
|
const shortens = newExpiresAt !== void 0 && expiresAt !== null && Date.parse(newExpiresAt) < Date.parse(expiresAt) - CLOCK_SKEW_MS;
|
|
14473
14585
|
if (keepsScope && !shortens) return;
|
|
14474
14586
|
const space = typeof session.spaceId === "string" ? session.spaceId : "an unknown space";
|
|
@@ -14721,7 +14833,7 @@ async function verifyApproval(input) {
|
|
|
14721
14833
|
}
|
|
14722
14834
|
const invalid = validateDelegationCallbackPayload(delegation);
|
|
14723
14835
|
if (invalid) throw invalidResponse(`OpenKey returned an invalid delegation: ${invalid}`);
|
|
14724
|
-
const session = verifyScopedLogin(delegation, input.key, input.sessionDid, input.requested, {
|
|
14836
|
+
const { session } = verifyScopedLogin(delegation, input.key, input.sessionDid, input.requested, {
|
|
14725
14837
|
expectedOwner: input.expectedOwner,
|
|
14726
14838
|
expiry: input.expiry
|
|
14727
14839
|
});
|
|
@@ -14733,14 +14845,13 @@ async function verifyApproval(input) {
|
|
|
14733
14845
|
if (!sameTuples(signed, permissionTuples(permissionList(binding.permissions, "approved"), ownerDid)) || !sameTuples(signed, permissionTuples(permissionList(delegation.permissions, "delegated"), ownerDid))) {
|
|
14734
14846
|
throw new CLIError("DEVICE_AUTH_BINDING_MISMATCH", "OpenKey's approved permissions differ from the signed grant. No session was saved.", ExitCode.PERMISSION_DENIED);
|
|
14735
14847
|
}
|
|
14736
|
-
const requested = permissionTuples(input.requested, ownerDid);
|
|
14737
14848
|
return {
|
|
14738
14849
|
session,
|
|
14739
14850
|
ownerDid,
|
|
14740
14851
|
spaceId: delegation.spaceId,
|
|
14741
14852
|
expiresAt: session.expiresAt,
|
|
14742
14853
|
approved: permissionsFromTuples(signed),
|
|
14743
|
-
declined:
|
|
14854
|
+
declined: declinedPermissions(input.requested, session.permissions, ownerDid)
|
|
14744
14855
|
};
|
|
14745
14856
|
}
|
|
14746
14857
|
function writeApprovalPrompt(prompt) {
|
|
@@ -14754,6 +14865,13 @@ function writeApprovalPrompt(prompt) {
|
|
|
14754
14865
|
}
|
|
14755
14866
|
async function acquireDeviceDelegation(input) {
|
|
14756
14867
|
validateLoginPermissions(input.permissions);
|
|
14868
|
+
if (input.permissions.some((permission) => permission.service === "tinycloud.encryption" || permission.space === "secrets" || permission.space?.startsWith("tinycloud:") && permission.space.endsWith(":secrets"))) {
|
|
14869
|
+
throw new CLIError(
|
|
14870
|
+
"DEVICE_AUTH_UNSUPPORTED_SCOPE",
|
|
14871
|
+
"--device cannot authorize tinycloud.encryption or the secrets space. Use another OpenKey approval method for this manifest.",
|
|
14872
|
+
ExitCode.USAGE_ERROR
|
|
14873
|
+
);
|
|
14874
|
+
}
|
|
14757
14875
|
const expiry = input.expiry ?? { durationMs: DEVICE_DELEGATION_MAX_SECONDS * 1e3 };
|
|
14758
14876
|
const ttlSeconds = Math.floor(expiry.durationMs / 1e3);
|
|
14759
14877
|
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 60 || ttlSeconds > DEVICE_DELEGATION_MAX_SECONDS) {
|
|
@@ -14894,14 +15012,17 @@ async function loginWithDeviceAuthorization(input) {
|
|
|
14894
15012
|
const existing = snapshot.profile;
|
|
14895
15013
|
assertNotLocalOwner(input.profileName, existing, "Device login");
|
|
14896
15014
|
const expectedOwner = expectedOwnerFor(input.profileName, existing, input.expectedOwner);
|
|
15015
|
+
validateLoginPermissions(input.permissions);
|
|
15016
|
+
const permissions = scopedLoginPermissions(input.permissions);
|
|
14897
15017
|
if (input.replaceSession !== true) {
|
|
14898
15018
|
const estimatedExpiry = new Date(Date.now() + (input.expiry?.durationMs ?? DEVICE_DELEGATION_MAX_SECONDS * 1e3)).toISOString();
|
|
14899
|
-
assertSessionReplaceable(input.profileName, snapshot, expectedOwner,
|
|
15019
|
+
assertSessionReplaceable(input.profileName, snapshot, expectedOwner, permissions, estimatedExpiry);
|
|
14900
15020
|
}
|
|
14901
15021
|
const key = snapshot.key ?? generateKey().jwk;
|
|
14902
15022
|
const sessionDid = keyToDID(key);
|
|
14903
15023
|
const result = await acquireDeviceDelegation({
|
|
14904
15024
|
...input,
|
|
15025
|
+
permissions,
|
|
14905
15026
|
sessionDid,
|
|
14906
15027
|
jwk: key,
|
|
14907
15028
|
openkeyHost: input.openkeyHost ?? resolveDeviceApiHost(existing),
|
|
@@ -14966,7 +15087,7 @@ async function promptAuthMethod() {
|
|
|
14966
15087
|
}
|
|
14967
15088
|
function registerAuthCommand(program) {
|
|
14968
15089
|
const auth = program.command("auth").description("Authentication management");
|
|
14969
|
-
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
|
|
15090
|
+
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", "Use manual paste mode instead of browser callback").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 \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Require this existing primary DID (did:pkh:eip155:CHAIN:ADDRESS) for scoped login; 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)").action(async (options, cmd) => {
|
|
14970
15091
|
try {
|
|
14971
15092
|
if (options.device && options.paste) {
|
|
14972
15093
|
throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
|
|
@@ -14987,7 +15108,8 @@ function registerAuthCommand(program) {
|
|
|
14987
15108
|
}
|
|
14988
15109
|
const globalOpts = cmd.optsWithGlobals();
|
|
14989
15110
|
const ctx = await ProfileManager.resolveContext(globalOpts);
|
|
14990
|
-
const
|
|
15111
|
+
const owner = options.owner === void 0 ? void 0 : canonicalOwnerDid(options.owner);
|
|
15112
|
+
const permissions = options.manifest ? await loadManifestPermissions(options.manifest, ctx.profile, { allowLogicalSpaces: true, ownerDid: owner, device: options.device === true }) : void 0;
|
|
14991
15113
|
const persistHost = globalOpts.host !== void 0;
|
|
14992
15114
|
if (options.device) {
|
|
14993
15115
|
const { profile, result } = await loginWithDeviceAuthorization({
|
|
@@ -14997,7 +15119,7 @@ function registerAuthCommand(program) {
|
|
|
14997
15119
|
permissions,
|
|
14998
15120
|
...options.expiry === void 0 ? {} : { expiry: parseRequestedExpiry(parseExpiryOption(options.expiry)) },
|
|
14999
15121
|
reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest.",
|
|
15000
|
-
expectedOwner:
|
|
15122
|
+
expectedOwner: owner,
|
|
15001
15123
|
replaceSession: options.replaceSession === true,
|
|
15002
15124
|
persistHost
|
|
15003
15125
|
});
|
|
@@ -15031,10 +15153,10 @@ function registerAuthCommand(program) {
|
|
|
15031
15153
|
} else {
|
|
15032
15154
|
method = scoped ? "openkey" : await promptAuthMethod();
|
|
15033
15155
|
}
|
|
15034
|
-
if (method === "openkey" && !options.paste && options.popup !== false && !
|
|
15156
|
+
if (method === "openkey" && !options.paste && options.popup !== false && !process.stdin.isTTY && !process.stderr.isTTY) {
|
|
15035
15157
|
throw new CLIError(
|
|
15036
15158
|
"INTERACTIVE_LOGIN_REQUIRED",
|
|
15037
|
-
|
|
15159
|
+
"Browser login needs a visible approval URL and would wait silently here. Use `--paste` to print the URL and provide the owner's code on stdin.",
|
|
15038
15160
|
ExitCode.USAGE_ERROR
|
|
15039
15161
|
);
|
|
15040
15162
|
}
|
|
@@ -15046,7 +15168,7 @@ function registerAuthCommand(program) {
|
|
|
15046
15168
|
noPopup: options.popup === false,
|
|
15047
15169
|
permissions,
|
|
15048
15170
|
expiry: parseExpiryOption(options.expiry),
|
|
15049
|
-
expectedOwner:
|
|
15171
|
+
expectedOwner: owner,
|
|
15050
15172
|
replaceSession: options.replaceSession === true,
|
|
15051
15173
|
persistHost
|
|
15052
15174
|
});
|
|
@@ -15173,10 +15295,15 @@ function registerAuthCommand(program) {
|
|
|
15173
15295
|
if (!key) {
|
|
15174
15296
|
throw new CLIError("NO_KEY", `No key found for profile "${ctx.profile}". Run \`tc init\` first.`, ExitCode.AUTH_REQUIRED);
|
|
15175
15297
|
}
|
|
15176
|
-
const delegationCids2 = [];
|
|
15177
|
-
let expiry2;
|
|
15178
15298
|
const openkeyHost = resolveOpenKeyHost(profile);
|
|
15179
|
-
const
|
|
15299
|
+
const grants = [];
|
|
15300
|
+
const expiryCap = expiryOption === void 0 ? void 0 : parseRequestedExpiry(expiryOption);
|
|
15301
|
+
const proof = {
|
|
15302
|
+
key,
|
|
15303
|
+
sessionDid: profile.sessionDid ?? profile.did,
|
|
15304
|
+
expectedOwner: pinnedOwner(profile),
|
|
15305
|
+
expiry: expiryCap
|
|
15306
|
+
};
|
|
15180
15307
|
const declined = [];
|
|
15181
15308
|
for (const group of groupPermissionsBySpace(requested)) {
|
|
15182
15309
|
const reason = permissionGrantReason(
|
|
@@ -15205,29 +15332,20 @@ function registerAuthCommand(program) {
|
|
|
15205
15332
|
permissions: group,
|
|
15206
15333
|
reason,
|
|
15207
15334
|
openkeyHost,
|
|
15208
|
-
expiry:
|
|
15335
|
+
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
|
|
15209
15336
|
noPopup: options.popup === false
|
|
15210
15337
|
});
|
|
15211
15338
|
}
|
|
15212
|
-
const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host);
|
|
15213
|
-
|
|
15214
|
-
openkeyEffective.push(...effective);
|
|
15215
|
-
const stored = storedAdditionalDelegation(delegation, effective);
|
|
15216
|
-
await appendAdditionalDelegation(ctx.profile, stored);
|
|
15217
|
-
await node.useRuntimeDelegation(delegation);
|
|
15218
|
-
delegationCids2.push(delegation.cid);
|
|
15219
|
-
expiry2 = delegation.expiry.toISOString();
|
|
15220
|
-
await appendGrantHistory(ctx.profile, {
|
|
15221
|
-
addedCaps: effective,
|
|
15222
|
-
source: options.manifest ? "manifest" : "cli",
|
|
15223
|
-
delegationCid: delegation.cid,
|
|
15224
|
-
expiry: expiry2
|
|
15225
|
-
});
|
|
15339
|
+
const delegation = portableFromOpenKeyDelegation(delegationData, group, ctx.host, proof);
|
|
15340
|
+
grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
|
|
15226
15341
|
}
|
|
15342
|
+
await activateAndStoreOpenKeyGrants(ctx.profile, node, grants, options.manifest ? "manifest" : "cli");
|
|
15343
|
+
const delegationCids2 = grants.map(({ delegation }) => delegation.cid);
|
|
15344
|
+
const expiry2 = grants.at(-1)?.delegation.expiry.toISOString();
|
|
15227
15345
|
reportDeclined(declined);
|
|
15228
15346
|
outputJson({
|
|
15229
15347
|
changed: delegationCids2.length > 0,
|
|
15230
|
-
added:
|
|
15348
|
+
added: grants.flatMap(({ effective }) => effective),
|
|
15231
15349
|
delegationCid: delegationCids2[0],
|
|
15232
15350
|
delegationCids: delegationCids2,
|
|
15233
15351
|
expiry: expiry2,
|
|
@@ -15712,6 +15830,26 @@ function normalizePortableDelegation(delegation) {
|
|
|
15712
15830
|
}
|
|
15713
15831
|
return { ...delegation, expiry };
|
|
15714
15832
|
}
|
|
15833
|
+
async function activateAndStoreOpenKeyGrants(profileName, node, grants, source) {
|
|
15834
|
+
for (const { delegation } of grants) await node.useRuntimeDelegation(delegation);
|
|
15835
|
+
if (grants.length === 0) return;
|
|
15836
|
+
await ProfileManager.withLock(profileName, async () => {
|
|
15837
|
+
const existing = await loadAdditionalDelegations(profileName);
|
|
15838
|
+
const replacing = new Set(grants.map(({ delegation }) => delegation.cid));
|
|
15839
|
+
for (const { delegation, effective } of grants) {
|
|
15840
|
+
await appendGrantHistory(profileName, {
|
|
15841
|
+
addedCaps: effective,
|
|
15842
|
+
source,
|
|
15843
|
+
delegationCid: delegation.cid,
|
|
15844
|
+
expiry: delegation.expiry.toISOString()
|
|
15845
|
+
});
|
|
15846
|
+
}
|
|
15847
|
+
await saveAdditionalDelegations(profileName, [
|
|
15848
|
+
...existing.filter(({ delegation }) => !replacing.has(delegation.cid)),
|
|
15849
|
+
...grants.map(({ delegation, effective }) => storedAdditionalDelegation(delegation, effective))
|
|
15850
|
+
]);
|
|
15851
|
+
});
|
|
15852
|
+
}
|
|
15715
15853
|
async function ensureDelegationAuthority(params) {
|
|
15716
15854
|
if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
|
|
15717
15855
|
if (params.profile.authMethod === "openkey") {
|
|
@@ -15725,6 +15863,14 @@ async function ensureDelegationAuthority(params) {
|
|
|
15725
15863
|
}
|
|
15726
15864
|
const openkeyHost = resolveOpenKeyHost(params.profile);
|
|
15727
15865
|
const acquireOpenKey = params.openKeyAcquisition ?? startAuthFlow;
|
|
15866
|
+
const expiryCap = params.expiryOption === void 0 ? void 0 : parseRequestedExpiry(params.expiryOption);
|
|
15867
|
+
const proof = {
|
|
15868
|
+
key,
|
|
15869
|
+
sessionDid: params.profile.sessionDid ?? params.profile.did,
|
|
15870
|
+
expectedOwner: pinnedOwner(params.profile),
|
|
15871
|
+
expiry: expiryCap
|
|
15872
|
+
};
|
|
15873
|
+
const grants = [];
|
|
15728
15874
|
for (const group of groupPermissionsBySpace(params.requested)) {
|
|
15729
15875
|
const delegationData = await acquireOpenKey(params.profile.did, {
|
|
15730
15876
|
jwk: key,
|
|
@@ -15732,22 +15878,12 @@ async function ensureDelegationAuthority(params) {
|
|
|
15732
15878
|
permissions: group,
|
|
15733
15879
|
reason: permissionGrantReason(params.reason, group),
|
|
15734
15880
|
openkeyHost,
|
|
15735
|
-
expiry:
|
|
15736
|
-
});
|
|
15737
|
-
const delegation = portableFromOpenKeyDelegation(delegationData, group, params.ctx.host);
|
|
15738
|
-
const effective = permissionsFromDelegation(delegation);
|
|
15739
|
-
await appendAdditionalDelegation(
|
|
15740
|
-
params.ctx.profile,
|
|
15741
|
-
storedAdditionalDelegation(delegation, effective)
|
|
15742
|
-
);
|
|
15743
|
-
await params.node.useRuntimeDelegation(delegation);
|
|
15744
|
-
await appendGrantHistory(params.ctx.profile, {
|
|
15745
|
-
addedCaps: effective,
|
|
15746
|
-
source: "cli",
|
|
15747
|
-
delegationCid: delegation.cid,
|
|
15748
|
-
expiry: delegation.expiry.toISOString()
|
|
15881
|
+
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
|
|
15749
15882
|
});
|
|
15883
|
+
const delegation = portableFromOpenKeyDelegation(delegationData, group, params.ctx.host, proof);
|
|
15884
|
+
grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
|
|
15750
15885
|
}
|
|
15886
|
+
await activateAndStoreOpenKeyGrants(params.ctx.profile, params.node, grants, "cli");
|
|
15751
15887
|
return;
|
|
15752
15888
|
}
|
|
15753
15889
|
if (isInteractive()) {
|
|
@@ -15818,7 +15954,7 @@ async function collectRequestedPermissions(options, profile) {
|
|
|
15818
15954
|
permissions.push(...await loadPermissionRequest(options.permission, profile));
|
|
15819
15955
|
}
|
|
15820
15956
|
if (options.manifest) {
|
|
15821
|
-
permissions.push(...await loadManifestPermissions(options.manifest, profile));
|
|
15957
|
+
permissions.push(...await loadManifestPermissions(options.manifest, profile, { device: options.device === true }));
|
|
15822
15958
|
}
|
|
15823
15959
|
return permissions;
|
|
15824
15960
|
}
|
|
@@ -15870,7 +16006,7 @@ function groupPermissionsBySpace(permissions) {
|
|
|
15870
16006
|
const groups = /* @__PURE__ */ new Map();
|
|
15871
16007
|
const rawEntries = [];
|
|
15872
16008
|
for (const permission of permissions) {
|
|
15873
|
-
if (
|
|
16009
|
+
if (isRawEncryptionPermission(permission)) {
|
|
15874
16010
|
rawEntries.push(permission);
|
|
15875
16011
|
continue;
|
|
15876
16012
|
}
|
|
@@ -15886,119 +16022,44 @@ function groupPermissionsBySpace(permissions) {
|
|
|
15886
16022
|
grouped[0].push(...rawEntries);
|
|
15887
16023
|
return grouped;
|
|
15888
16024
|
}
|
|
15889
|
-
function
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
15895
|
-
|
|
15896
|
-
if (!returnedSpace.startsWith("tinycloud:")) return false;
|
|
15897
|
-
const returnedName = returnedSpace.slice(returnedSpace.lastIndexOf(":") + 1);
|
|
15898
|
-
return returnedName === expectedSpace;
|
|
15899
|
-
}
|
|
15900
|
-
function portableFromOpenKeyDelegation(data, permissions, host) {
|
|
15901
|
-
const primary = permissions.find((permission) => !isRawPermission(permission)) ?? permissions[0];
|
|
15902
|
-
const returnedSpace = String(data.spaceId ?? primary.space ?? "encryption");
|
|
15903
|
-
const expectedSpaces = new Set(
|
|
15904
|
-
permissions.filter((permission) => !isRawPermission(permission)).map((permission) => normalizePkhIdentifier(permission.space ?? ""))
|
|
15905
|
-
);
|
|
15906
|
-
const matchesExpectedSpace = expectedSpaces.size === 1 && returnedSpaceMatchesExpected(returnedSpace, Array.from(expectedSpaces)[0]);
|
|
15907
|
-
if (expectedSpaces.size > 0 && !matchesExpectedSpace) {
|
|
16025
|
+
function portableFromOpenKeyDelegation(data, requested, host, proof) {
|
|
16026
|
+
const { session, legacyNested } = verifyScopedLogin(data, proof.key, proof.sessionDid, requested, {
|
|
16027
|
+
expectedOwner: proof.expectedOwner,
|
|
16028
|
+
expiry: proof.expiry,
|
|
16029
|
+
purpose: "grant"
|
|
16030
|
+
});
|
|
16031
|
+
if (legacyNested.length > 0) {
|
|
15908
16032
|
throw new CLIError(
|
|
15909
|
-
"
|
|
15910
|
-
|
|
16033
|
+
"OPENKEY_GRANT_BROADENED",
|
|
16034
|
+
"OpenKey signed decrypt inside the space instead of on the raw network. The OpenKey deployment is too old for agent secret reads.",
|
|
15911
16035
|
ExitCode.PERMISSION_DENIED
|
|
15912
16036
|
);
|
|
15913
16037
|
}
|
|
15914
|
-
const
|
|
15915
|
-
const
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
)
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
15925
|
-
if (returnedPermissions) {
|
|
15926
|
-
const rawSpace = isRawPermission({
|
|
15927
|
-
service: rawService,
|
|
15928
|
-
space: permSpace,
|
|
15929
|
-
path: permission.path,
|
|
15930
|
-
actions: []
|
|
15931
|
-
}) ? permSpace : normalizePkhIdentifier(permSpace);
|
|
15932
|
-
for (const action of permission.actions) {
|
|
15933
|
-
const key = `${rawService}|${rawSpace}|${permission.path}|${action}`;
|
|
15934
|
-
if (!requestedPairs.has(key)) {
|
|
15935
|
-
throw new CLIError(
|
|
15936
|
-
"OPENKEY_GRANT_BROADENED",
|
|
15937
|
-
`OpenKey returned grant ${rawService}/${action} on ${permSpace}/${permission.path} that was not requested.`,
|
|
15938
|
-
ExitCode.PERMISSION_DENIED
|
|
15939
|
-
);
|
|
15940
|
-
}
|
|
15941
|
-
}
|
|
15942
|
-
}
|
|
15943
|
-
const resolvedSpace = isRawPermission({
|
|
15944
|
-
service: rawService,
|
|
15945
|
-
space: permSpace,
|
|
15946
|
-
path: permission.path,
|
|
15947
|
-
actions: []
|
|
15948
|
-
}) ? permSpace : returnedSpace;
|
|
15949
|
-
const caveats = Array.isArray(permission.caveats) && permission.caveats.length > 0 ? permission.caveats.map((caveat) => structuredClone(caveat)) : void 0;
|
|
15950
|
-
return {
|
|
15951
|
-
service,
|
|
15952
|
-
space: resolvedSpace,
|
|
15953
|
-
path: permission.path,
|
|
15954
|
-
actions: [...permission.actions],
|
|
15955
|
-
...caveats === void 0 ? {} : { caveats }
|
|
15956
|
-
};
|
|
15957
|
-
});
|
|
16038
|
+
const returnedSpace = data.spaceId;
|
|
16039
|
+
const effective = session.permissions;
|
|
16040
|
+
const primary = effective.find((permission) => !isRawEncryptionPermission(permission)) ?? effective[0];
|
|
16041
|
+
const resources = effective.map((permission) => ({
|
|
16042
|
+
service: permission.service.slice("tinycloud.".length),
|
|
16043
|
+
space: isVerifiedRawEncryptionPermission(permission) ? "encryption" : returnedSpace,
|
|
16044
|
+
path: permission.path,
|
|
16045
|
+
actions: [...permission.actions],
|
|
16046
|
+
...permission.caveats === void 0 ? {} : { caveats: structuredClone(permission.caveats) }
|
|
16047
|
+
}));
|
|
16048
|
+
const ownerParts = session.ownerDid.split(":");
|
|
15958
16049
|
return {
|
|
15959
|
-
cid:
|
|
16050
|
+
cid: data.delegationCid,
|
|
15960
16051
|
delegationHeader: data.delegationHeader,
|
|
15961
16052
|
spaceId: returnedSpace,
|
|
15962
16053
|
path: primary.path,
|
|
15963
|
-
actions: primary.actions,
|
|
16054
|
+
actions: [...primary.actions],
|
|
15964
16055
|
resources,
|
|
15965
|
-
expiry,
|
|
15966
|
-
delegateDID:
|
|
15967
|
-
ownerAddress:
|
|
15968
|
-
chainId:
|
|
16056
|
+
expiry: new Date(session.expiresAt),
|
|
16057
|
+
delegateDID: data.verificationMethod,
|
|
16058
|
+
ownerAddress: ownerParts[4],
|
|
16059
|
+
chainId: Number(ownerParts[3]),
|
|
15969
16060
|
host
|
|
15970
16061
|
};
|
|
15971
16062
|
}
|
|
15972
|
-
function inferDelegationExpiry(data) {
|
|
15973
|
-
for (const key of ["expiry", "expiresAt", "expirationTime"]) {
|
|
15974
|
-
const parsed = parseDelegationExpiryField(data[key]);
|
|
15975
|
-
if (parsed) return parsed;
|
|
15976
|
-
}
|
|
15977
|
-
if (typeof data.siwe === "string") {
|
|
15978
|
-
const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
|
|
15979
|
-
const parsed = match ? parseDelegationExpiryField(match[1]?.trim()) : null;
|
|
15980
|
-
if (parsed) return parsed;
|
|
15981
|
-
}
|
|
15982
|
-
throw new CLIError(
|
|
15983
|
-
"OPENKEY_EXPIRY_MISSING",
|
|
15984
|
-
"OpenKey delegation response did not include expiry, expiresAt, expirationTime, or a SIWE Expiration Time.",
|
|
15985
|
-
ExitCode.ERROR
|
|
15986
|
-
);
|
|
15987
|
-
}
|
|
15988
|
-
function parseDelegationExpiryField(value) {
|
|
15989
|
-
if (value instanceof Date) {
|
|
15990
|
-
return Number.isNaN(value.getTime()) ? null : value;
|
|
15991
|
-
}
|
|
15992
|
-
if (typeof value === "number") {
|
|
15993
|
-
const parsed = new Date(value < 1e10 ? value * 1e3 : value);
|
|
15994
|
-
if (!Number.isNaN(parsed.getTime())) return parsed;
|
|
15995
|
-
}
|
|
15996
|
-
if (typeof value === "string") {
|
|
15997
|
-
const parsed = new Date(value);
|
|
15998
|
-
if (!Number.isNaN(parsed.getTime())) return parsed;
|
|
15999
|
-
}
|
|
16000
|
-
return null;
|
|
16001
|
-
}
|
|
16002
16063
|
async function rotateAuthKey(profileName, host, options = {}) {
|
|
16003
16064
|
const profile = await ProfileManager.getProfile(profileName);
|
|
16004
16065
|
const posture = resolveProfilePosture(profile);
|
|
@@ -16173,7 +16234,8 @@ async function handleLocalAuth(profileName, host, options = {}) {
|
|
|
16173
16234
|
return { profile: updatedProfile, sessionResult };
|
|
16174
16235
|
}
|
|
16175
16236
|
async function handleOpenKeyAuth(profileName, host, options = {}) {
|
|
16176
|
-
const { profile, delegationData } = await refreshOpenKeySession(profileName, host, options);
|
|
16237
|
+
const { profile, delegationData, declined, legacyNested } = await refreshOpenKeySession(profileName, host, options);
|
|
16238
|
+
reportDeclined(declined, legacyNested);
|
|
16177
16239
|
outputJson({
|
|
16178
16240
|
authenticated: true,
|
|
16179
16241
|
profile: profileName,
|
|
@@ -16185,18 +16247,23 @@ async function handleOpenKeyAuth(profileName, host, options = {}) {
|
|
|
16185
16247
|
ownerDid: profile.ownerDid,
|
|
16186
16248
|
host,
|
|
16187
16249
|
permissions: delegationData.permissions,
|
|
16250
|
+
declined,
|
|
16188
16251
|
expiresAt: delegationData.expiresAt,
|
|
16189
16252
|
activation: delegationData.hostActivated === true ? "confirmed-by-openkey" : "unverified"
|
|
16190
16253
|
} : {}
|
|
16191
16254
|
});
|
|
16192
16255
|
}
|
|
16193
|
-
function reportDeclined(declined) {
|
|
16256
|
+
function reportDeclined(declined, signed) {
|
|
16194
16257
|
if (declined.length === 0) return;
|
|
16195
16258
|
process.stderr.write(
|
|
16196
|
-
`${theme.warn("
|
|
16259
|
+
`${theme.warn("Not granted in this session:")}
|
|
16197
16260
|
${declined.map((permission) => ` ${compactPermission(permission)}`).join("\n")}
|
|
16198
16261
|
`
|
|
16199
16262
|
);
|
|
16263
|
+
if (declined.some((permission) => isRawEncryptionPermission(permission) && signed?.some((entry) => isRawEncryptionPermission(entry) && !isVerifiedRawEncryptionPermission(entry) && normalizePkhIdentifier(entry.path) === normalizePkhIdentifier(permission.path)))) {
|
|
16264
|
+
process.stderr.write(`${theme.warn("OpenKey signed decrypt inside the space; the TinyCloud node refuses that. The OpenKey deployment is too old for agent secret reads.")}
|
|
16265
|
+
`);
|
|
16266
|
+
}
|
|
16200
16267
|
}
|
|
16201
16268
|
async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
16202
16269
|
const snapshot = await readProfileSnapshot(profileName);
|
|
@@ -16209,16 +16276,17 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
|
16209
16276
|
);
|
|
16210
16277
|
}
|
|
16211
16278
|
const profile = snapshot.profile ?? await ProfileManager.getProfile(profileName);
|
|
16212
|
-
if (options.permissions !== void 0)
|
|
16213
|
-
|
|
16279
|
+
if (options.permissions !== void 0) validateLoginPermissions(options.permissions);
|
|
16280
|
+
const permissions = options.permissions === void 0 ? void 0 : scopedLoginPermissions(options.permissions);
|
|
16281
|
+
if (permissions !== void 0) {
|
|
16214
16282
|
assertNotLocalOwner(profileName, profile, "Scoped browser login");
|
|
16215
16283
|
}
|
|
16216
16284
|
const expiry = options.expiry === void 0 ? void 0 : parseRequestedExpiry(options.expiry);
|
|
16217
16285
|
const openKeyExpiry = expiry === void 0 ? void 0 : openKeyExpiryParam(expiry);
|
|
16218
16286
|
const expectedOwner = expectedOwnerFor(profileName, profile, options.expectedOwner);
|
|
16219
|
-
if (
|
|
16287
|
+
if (permissions !== void 0 && options.replaceSession !== true) {
|
|
16220
16288
|
const estimatedExpiry = expiry === void 0 ? void 0 : new Date(Date.now() + expiry.durationMs).toISOString();
|
|
16221
|
-
assertSessionReplaceable(profileName, snapshot, expectedOwner,
|
|
16289
|
+
assertSessionReplaceable(profileName, snapshot, expectedOwner, permissions, estimatedExpiry);
|
|
16222
16290
|
}
|
|
16223
16291
|
const acquireOpenKey = options.openKeyAcquisition ?? startAuthFlow;
|
|
16224
16292
|
const delegationData = await acquireOpenKey(profile.did, {
|
|
@@ -16227,16 +16295,21 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
|
16227
16295
|
jwk: key,
|
|
16228
16296
|
host,
|
|
16229
16297
|
openkeyHost: resolveOpenKeyHost(profile),
|
|
16230
|
-
permissions
|
|
16298
|
+
permissions,
|
|
16231
16299
|
expiry: openKeyExpiry,
|
|
16232
|
-
...
|
|
16300
|
+
...permissions ? { reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest." } : {}
|
|
16233
16301
|
});
|
|
16234
16302
|
const sessionDid = profile.sessionDid ?? profile.did;
|
|
16235
16303
|
let sanitizedSession;
|
|
16236
16304
|
let verifiedOwner;
|
|
16237
|
-
|
|
16238
|
-
|
|
16305
|
+
let declined = [];
|
|
16306
|
+
let legacyNested = [];
|
|
16307
|
+
if (permissions) {
|
|
16308
|
+
const verified = verifyScopedLogin(delegationData, key, sessionDid, permissions, { expectedOwner, expiry });
|
|
16309
|
+
sanitizedSession = verified.session;
|
|
16310
|
+
legacyNested = verified.legacyNested;
|
|
16239
16311
|
verifiedOwner = sanitizedSession.ownerDid;
|
|
16312
|
+
declined = declinedPermissions(permissions, sanitizedSession.permissions, verifiedOwner);
|
|
16240
16313
|
} else {
|
|
16241
16314
|
const carriesProof = typeof delegationData.siwe === "string" && typeof delegationData.signature === "string";
|
|
16242
16315
|
const merged = mergePrivateJwkIntoSession(delegationData, key);
|
|
@@ -16262,9 +16335,9 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
|
|
|
16262
16335
|
key,
|
|
16263
16336
|
session: sanitizedSession,
|
|
16264
16337
|
profile: updatedProfile,
|
|
16265
|
-
...
|
|
16338
|
+
...permissions && verifiedOwner ? { approved: { scope: sanitizedSession.permissions, ownerDid: verifiedOwner, replaceSession: options.replaceSession === true } } : {}
|
|
16266
16339
|
});
|
|
16267
|
-
return { profile: updatedProfile, delegationData: sanitizedSession };
|
|
16340
|
+
return { profile: updatedProfile, delegationData: sanitizedSession, declined, legacyNested };
|
|
16268
16341
|
}
|
|
16269
16342
|
|
|
16270
16343
|
// src/commands/completion.ts
|
|
@@ -17568,9 +17641,10 @@ init_formatter();
|
|
|
17568
17641
|
init_theme();
|
|
17569
17642
|
init_errors();
|
|
17570
17643
|
init_constants();
|
|
17571
|
-
|
|
17572
|
-
import {
|
|
17573
|
-
import {
|
|
17644
|
+
init_storage();
|
|
17645
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
17646
|
+
import { lstat, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
|
|
17647
|
+
import { basename as basename2, dirname as dirname3, join as join5 } from "path";
|
|
17574
17648
|
import { homedir } from "os";
|
|
17575
17649
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
17576
17650
|
import {
|
|
@@ -17673,6 +17747,14 @@ async function ensureSecretsNode(ctx, options, openKeyAcquisition, selectedProfi
|
|
|
17673
17747
|
if (profile?.authMethod === "openkey" && canRequestOwnerPermissions(profile)) {
|
|
17674
17748
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
17675
17749
|
if (!session || isStoredSessionExpired(session)) {
|
|
17750
|
+
if (!process.stdin.isTTY && !process.stderr.isTTY) {
|
|
17751
|
+
throw new CLIError(
|
|
17752
|
+
"AUTH_REQUIRED",
|
|
17753
|
+
`Profile "${ctx.profile}" has ${session ? "an expired" : "no"} OpenKey session; headless secret access cannot open a browser login.`,
|
|
17754
|
+
ExitCode.AUTH_REQUIRED,
|
|
17755
|
+
{ hint: scopedSecretLoginHint(ctx.profile) }
|
|
17756
|
+
);
|
|
17757
|
+
}
|
|
17676
17758
|
await withSpinner(
|
|
17677
17759
|
session ? "Refreshing TinyCloud session..." : "Creating TinyCloud session...",
|
|
17678
17760
|
() => refreshOpenKeySession(ctx.profile, ctx.host, { openKeyAcquisition })
|
|
@@ -17690,6 +17772,7 @@ async function runSecretOperation(params) {
|
|
|
17690
17772
|
if (!canRequestOwnerPermissions(profile)) {
|
|
17691
17773
|
return first;
|
|
17692
17774
|
}
|
|
17775
|
+
assertOwnerApprovalPossible(params.ctx.profile, profile, params.action, params.name);
|
|
17693
17776
|
const requested = secretPermissionEntries({
|
|
17694
17777
|
action: params.action,
|
|
17695
17778
|
name: params.name,
|
|
@@ -17717,6 +17800,72 @@ function secretPermissionReason(action, name) {
|
|
|
17717
17800
|
const target = name ? ` secret "${name}"` : " secrets";
|
|
17718
17801
|
return `Allow \`tc secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
|
|
17719
17802
|
}
|
|
17803
|
+
function assertOwnerApprovalPossible(profileName, profile, action, name, missing = []) {
|
|
17804
|
+
if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
|
|
17805
|
+
const command = `tc secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
|
|
17806
|
+
throw new CLIError(
|
|
17807
|
+
"PERMISSION_DENIED",
|
|
17808
|
+
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 "${profileName}" lacks the scoped decrypt authority (${SECRET_DECRYPT_CAPABILITY}) needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName}" lacks a scoped read or decrypt grant needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName}" holds no grant for \`${command}\`, and requesting one needs an interactive browser approval.`,
|
|
17809
|
+
ExitCode.PERMISSION_DENIED,
|
|
17810
|
+
{
|
|
17811
|
+
hint: scopedSecretLoginHint(profileName)
|
|
17812
|
+
}
|
|
17813
|
+
);
|
|
17814
|
+
}
|
|
17815
|
+
function scopedSecretLoginHint(profileName) {
|
|
17816
|
+
return `Have the owner approve a scoped login whose manifest names the secret: tc --profile ${profileName} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
|
|
17817
|
+
}
|
|
17818
|
+
async function validateSecretOutput(path) {
|
|
17819
|
+
try {
|
|
17820
|
+
const destination = await lstat(path);
|
|
17821
|
+
if (!destination.isFile()) {
|
|
17822
|
+
throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
|
|
17823
|
+
}
|
|
17824
|
+
} catch (error) {
|
|
17825
|
+
if (!isMissingFileError(error)) throw error;
|
|
17826
|
+
}
|
|
17827
|
+
try {
|
|
17828
|
+
const parent = await lstat(dirname3(path));
|
|
17829
|
+
if (!parent.isDirectory()) {
|
|
17830
|
+
throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
17831
|
+
}
|
|
17832
|
+
} catch (error) {
|
|
17833
|
+
if (!isMissingFileError(error)) throw error;
|
|
17834
|
+
throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
17835
|
+
}
|
|
17836
|
+
}
|
|
17837
|
+
async function writeSecretFile(path, value) {
|
|
17838
|
+
await validateSecretOutput(path);
|
|
17839
|
+
const parentPath = dirname3(path);
|
|
17840
|
+
const temp = join5(parentPath, `.${basename2(path)}.${randomUUID2()}.tmp`);
|
|
17841
|
+
let created = false;
|
|
17842
|
+
try {
|
|
17843
|
+
const handle = await open2(temp, "wx", PRIVATE_FILE_MODE);
|
|
17844
|
+
created = true;
|
|
17845
|
+
try {
|
|
17846
|
+
await handle.writeFile(value);
|
|
17847
|
+
await handle.sync();
|
|
17848
|
+
} finally {
|
|
17849
|
+
await handle.close();
|
|
17850
|
+
}
|
|
17851
|
+
await rename2(temp, path);
|
|
17852
|
+
} catch (error) {
|
|
17853
|
+
if (created) {
|
|
17854
|
+
await rm3(temp, { force: true }).catch(() => void 0);
|
|
17855
|
+
}
|
|
17856
|
+
const code2 = error instanceof Error && "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
|
|
17857
|
+
throw new CLIError("ERROR", `Could not write secret output "${path}"${code2}.`, ExitCode.ERROR);
|
|
17858
|
+
}
|
|
17859
|
+
try {
|
|
17860
|
+
const parent = await open2(parentPath, "r");
|
|
17861
|
+
try {
|
|
17862
|
+
await parent.sync();
|
|
17863
|
+
} finally {
|
|
17864
|
+
await parent.close();
|
|
17865
|
+
}
|
|
17866
|
+
} catch {
|
|
17867
|
+
}
|
|
17868
|
+
}
|
|
17720
17869
|
async function runSecretOperationAttempt(label, operation) {
|
|
17721
17870
|
try {
|
|
17722
17871
|
return await withSpinner(label, operation);
|
|
@@ -17729,6 +17878,15 @@ async function runSecretOperationAttempt(label, operation) {
|
|
|
17729
17878
|
async function invokeCanonicalSecretGet(params) {
|
|
17730
17879
|
const auth = authOptions(params.options);
|
|
17731
17880
|
let ownerNode;
|
|
17881
|
+
if (!auth?.privateKey) {
|
|
17882
|
+
const profile2 = await ProfileManager.getProfile(params.ctx.profile).catch(() => null);
|
|
17883
|
+
if (profile2?.authMethod === "openkey" && canRequestOwnerPermissions(profile2)) {
|
|
17884
|
+
const session = await ProfileManager.getSession(params.ctx.profile);
|
|
17885
|
+
if (!session || isStoredSessionExpired(session)) {
|
|
17886
|
+
ownerNode = await ensureSecretsNode(params.ctx, params.options, params.openKeyAcquisition, profile2);
|
|
17887
|
+
}
|
|
17888
|
+
}
|
|
17889
|
+
}
|
|
17732
17890
|
const target = {
|
|
17733
17891
|
profile: params.ctx.profile,
|
|
17734
17892
|
host: params.ctx.host,
|
|
@@ -17764,6 +17922,7 @@ async function invokeCanonicalSecretGet(params) {
|
|
|
17764
17922
|
}
|
|
17765
17923
|
const profile = await ProfileManager.getProfile(params.ctx.profile);
|
|
17766
17924
|
if (!canRequestOwnerPermissions(profile)) return first;
|
|
17925
|
+
assertOwnerApprovalPossible(params.ctx.profile, profile, "get", params.name, first.missing);
|
|
17767
17926
|
const node = params.node ?? ownerNode ?? await ensureSecretsNode(
|
|
17768
17927
|
params.ctx,
|
|
17769
17928
|
params.options,
|
|
@@ -18387,6 +18546,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18387
18546
|
const scopeOptions = resolveSecretScope(options);
|
|
18388
18547
|
const legacySpaceUri = await resolveSecretSpace(options.space, ctx.profile);
|
|
18389
18548
|
const secretPath = resolveSecretPath2(name, scopeOptions).permissionPaths.vault;
|
|
18549
|
+
if (options.output) await validateSecretOutput(options.output);
|
|
18390
18550
|
if (options.delegation) {
|
|
18391
18551
|
const delegated = await resolveDelegatedSecretSource(
|
|
18392
18552
|
options.delegation,
|
|
@@ -18409,7 +18569,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18409
18569
|
})
|
|
18410
18570
|
);
|
|
18411
18571
|
if (options.output) {
|
|
18412
|
-
await
|
|
18572
|
+
await writeSecretFile(options.output, value2);
|
|
18413
18573
|
outputJson({ name, written: options.output });
|
|
18414
18574
|
return;
|
|
18415
18575
|
}
|
|
@@ -18436,7 +18596,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18436
18596
|
}
|
|
18437
18597
|
const value = result.output.value;
|
|
18438
18598
|
if (options.output) {
|
|
18439
|
-
await
|
|
18599
|
+
await writeSecretFile(options.output, value);
|
|
18440
18600
|
outputJson({ name, written: options.output });
|
|
18441
18601
|
return;
|
|
18442
18602
|
}
|
|
@@ -18549,8 +18709,8 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
18549
18709
|
});
|
|
18550
18710
|
secrets.command("manage").description("Open the TinyCloud Secrets Manager in your browser").action(async () => {
|
|
18551
18711
|
try {
|
|
18552
|
-
const
|
|
18553
|
-
await
|
|
18712
|
+
const open5 = (await import("open")).default;
|
|
18713
|
+
await open5("https://secrets.tinycloud.xyz");
|
|
18554
18714
|
outputJson({ opened: "https://secrets.tinycloud.xyz" });
|
|
18555
18715
|
} catch (error) {
|
|
18556
18716
|
handleError(error);
|
|
@@ -25744,7 +25904,7 @@ async function importAesKey(key32, usage) {
|
|
|
25744
25904
|
[usage]
|
|
25745
25905
|
);
|
|
25746
25906
|
}
|
|
25747
|
-
async function
|
|
25907
|
+
async function open3(blob, key32) {
|
|
25748
25908
|
if (blob.length < HEADER_LENGTH + NONCE_LENGTH + TAG_LENGTH) {
|
|
25749
25909
|
throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
|
|
25750
25910
|
}
|
|
@@ -26082,7 +26242,7 @@ async function resolvePolicyShare(link2, options) {
|
|
|
26082
26242
|
options.signal?.throwIfAborted();
|
|
26083
26243
|
let envelope;
|
|
26084
26244
|
try {
|
|
26085
|
-
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await
|
|
26245
|
+
const encoded = new TextDecoder("utf-8", { fatal: true }).decode(await open3(parsed.ciphertext, parsed.key32));
|
|
26086
26246
|
const value = JSON.parse(encoded);
|
|
26087
26247
|
if (canonicalize2(value) !== encoded) throw new Error("non-canonical envelope");
|
|
26088
26248
|
envelope = shareEnvelopeV3Schema.parse(value);
|
|
@@ -27316,9 +27476,9 @@ function receiveJson(result, path) {
|
|
|
27316
27476
|
|
|
27317
27477
|
// src/share/io.ts
|
|
27318
27478
|
import { constants } from "fs";
|
|
27319
|
-
import { lstat, mkdir as mkdir3, mkdtemp, open as
|
|
27479
|
+
import { lstat as lstat2, mkdir as mkdir3, mkdtemp, open as open4, readFile as readFile9, realpath, stat as stat2, link, rename as rename3, rm as rm4, unlink } from "fs/promises";
|
|
27320
27480
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
27321
|
-
import { basename as
|
|
27481
|
+
import { basename as basename3, join as join6, resolve as resolve2, sep } from "path";
|
|
27322
27482
|
init_errors();
|
|
27323
27483
|
var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
|
|
27324
27484
|
var MAX_SHARE_URL_BYTES = 64 * 1024;
|
|
@@ -27355,7 +27515,7 @@ function shareFilename(value) {
|
|
|
27355
27515
|
}
|
|
27356
27516
|
}
|
|
27357
27517
|
function shareInputFilename(input, name) {
|
|
27358
|
-
return shareFilename(name ?? (input === "-" ? "stdin.md" :
|
|
27518
|
+
return shareFilename(name ?? (input === "-" ? "stdin.md" : basename3(resolve2(input))));
|
|
27359
27519
|
}
|
|
27360
27520
|
async function readShareInput(input, name, limit = MAX_SHARE_STDIN_BYTES) {
|
|
27361
27521
|
const filename = shareInputFilename(input, name);
|
|
@@ -27374,7 +27534,7 @@ async function assertDirectory(path) {
|
|
|
27374
27534
|
for (const segment of segments) {
|
|
27375
27535
|
current = current === sep ? join6(current, segment) : join6(current, segment);
|
|
27376
27536
|
try {
|
|
27377
|
-
const info = await
|
|
27537
|
+
const info = await lstat2(current);
|
|
27378
27538
|
if (info.isSymbolicLink()) {
|
|
27379
27539
|
const canonical = await realpath(current);
|
|
27380
27540
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -27383,7 +27543,7 @@ async function assertDirectory(path) {
|
|
|
27383
27543
|
} catch (error) {
|
|
27384
27544
|
if (error.code !== "ENOENT") throw error;
|
|
27385
27545
|
await mkdir3(current, { mode: 448 });
|
|
27386
|
-
const created = await
|
|
27546
|
+
const created = await lstat2(current);
|
|
27387
27547
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
27388
27548
|
}
|
|
27389
27549
|
}
|
|
@@ -27392,7 +27552,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27392
27552
|
const outputDirectory = resolve2(directory);
|
|
27393
27553
|
await assertDirectory(outputDirectory);
|
|
27394
27554
|
const safeName = safeFilename(filename);
|
|
27395
|
-
const directoryHandle = await
|
|
27555
|
+
const directoryHandle = await open4(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
|
|
27396
27556
|
const stableDirectory = await realpath(outputDirectory);
|
|
27397
27557
|
const outputPath = join6(stableDirectory, safeName);
|
|
27398
27558
|
const directoryIdentity = await directoryHandle.stat();
|
|
@@ -27402,7 +27562,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27402
27562
|
};
|
|
27403
27563
|
await assertStableDirectory();
|
|
27404
27564
|
const stagingDirectory = await mkdtemp(join6(stableDirectory, ".tinycloud-share-stage-"));
|
|
27405
|
-
const stagingInfo = await
|
|
27565
|
+
const stagingInfo = await lstat2(stagingDirectory);
|
|
27406
27566
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
27407
27567
|
const stagingPath = join6(stagingDirectory, `.tinycloud-share-${randomBytes4(16).toString("hex")}.tmp`);
|
|
27408
27568
|
let temporaryPath;
|
|
@@ -27410,19 +27570,19 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27410
27570
|
try {
|
|
27411
27571
|
await assertStableDirectory();
|
|
27412
27572
|
try {
|
|
27413
|
-
const existing = await
|
|
27573
|
+
const existing = await lstat2(outputPath);
|
|
27414
27574
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
27415
27575
|
} catch (error) {
|
|
27416
27576
|
if (error.code !== "ENOENT") throw error;
|
|
27417
27577
|
}
|
|
27418
27578
|
temporaryPath = stagingPath;
|
|
27419
|
-
handle = await
|
|
27579
|
+
handle = await open4(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
|
|
27420
27580
|
await handle.writeFile(bytes);
|
|
27421
27581
|
await handle.close();
|
|
27422
27582
|
handle = void 0;
|
|
27423
27583
|
await assertStableDirectory();
|
|
27424
27584
|
if (force) {
|
|
27425
|
-
await
|
|
27585
|
+
await rename3(temporaryPath, outputPath);
|
|
27426
27586
|
} else {
|
|
27427
27587
|
await link(temporaryPath, outputPath);
|
|
27428
27588
|
await unlink(temporaryPath);
|
|
@@ -27438,7 +27598,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27438
27598
|
if (temporaryPath !== void 0) await unlink(temporaryPath);
|
|
27439
27599
|
} catch {
|
|
27440
27600
|
}
|
|
27441
|
-
await
|
|
27601
|
+
await rm4(stagingDirectory, { recursive: true, force: true });
|
|
27442
27602
|
await directoryHandle.close();
|
|
27443
27603
|
}
|
|
27444
27604
|
return join6(outputDirectory, safeName);
|
|
@@ -27786,8 +27946,8 @@ init_formatter();
|
|
|
27786
27946
|
init_errors();
|
|
27787
27947
|
init_constants();
|
|
27788
27948
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
27789
|
-
import { mkdir as mkdir4, writeFile as
|
|
27790
|
-
import { dirname as
|
|
27949
|
+
import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
|
|
27950
|
+
import { dirname as dirname4 } from "path";
|
|
27791
27951
|
init_host();
|
|
27792
27952
|
init_theme();
|
|
27793
27953
|
function didWithoutFragment2(did) {
|
|
@@ -27914,8 +28074,8 @@ it directly with \`tc space host <name>\` (no request needed).
|
|
|
27914
28074
|
}
|
|
27915
28075
|
async function emitHostRequestArtifact(artifact, emitOption) {
|
|
27916
28076
|
if (typeof emitOption === "string" && emitOption.length > 0) {
|
|
27917
|
-
await mkdir4(
|
|
27918
|
-
await
|
|
28077
|
+
await mkdir4(dirname4(emitOption), { recursive: true });
|
|
28078
|
+
await writeFile5(emitOption, JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
27919
28079
|
outputJson({
|
|
27920
28080
|
emitted: true,
|
|
27921
28081
|
path: emitOption,
|
|
@@ -27934,7 +28094,7 @@ init_profiles();
|
|
|
27934
28094
|
init_formatter();
|
|
27935
28095
|
init_errors();
|
|
27936
28096
|
init_constants();
|
|
27937
|
-
import { writeFile as
|
|
28097
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
27938
28098
|
import { resolve as resolve3 } from "path";
|
|
27939
28099
|
init_space();
|
|
27940
28100
|
init_host();
|
|
@@ -28079,7 +28239,7 @@ Output:
|
|
|
28079
28239
|
const blob = result.data;
|
|
28080
28240
|
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
28081
28241
|
const outputPath = resolve3(options.output);
|
|
28082
|
-
await
|
|
28242
|
+
await writeFile6(outputPath, buffer);
|
|
28083
28243
|
outputJson({
|
|
28084
28244
|
file: outputPath,
|
|
28085
28245
|
size: blob.size,
|
|
@@ -28630,7 +28790,7 @@ init_formatter();
|
|
|
28630
28790
|
init_errors();
|
|
28631
28791
|
init_constants();
|
|
28632
28792
|
import { readFile as readFile10 } from "fs/promises";
|
|
28633
|
-
import { writeFile as
|
|
28793
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
28634
28794
|
import { PrivateKeySigner as PrivateKeySigner2 } from "@tinycloud/node-sdk";
|
|
28635
28795
|
async function readStdin4() {
|
|
28636
28796
|
const chunks = [];
|
|
@@ -28719,7 +28879,7 @@ function registerVaultCommand(program) {
|
|
|
28719
28879
|
const data = result.data.data ?? result.data;
|
|
28720
28880
|
if (options.output) {
|
|
28721
28881
|
const content = data instanceof Uint8Array ? Buffer.from(data) : typeof data === "string" ? data : JSON.stringify(data);
|
|
28722
|
-
await
|
|
28882
|
+
await writeFile7(options.output, content);
|
|
28723
28883
|
outputJson({ key, written: options.output });
|
|
28724
28884
|
return;
|
|
28725
28885
|
}
|
|
@@ -28807,7 +28967,7 @@ init_formatter();
|
|
|
28807
28967
|
init_errors();
|
|
28808
28968
|
init_constants();
|
|
28809
28969
|
import { readFile as readFile11 } from "fs/promises";
|
|
28810
|
-
import { writeFile as
|
|
28970
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
28811
28971
|
var VARIABLES_PREFIX = "variables/";
|
|
28812
28972
|
async function readStdin5() {
|
|
28813
28973
|
const chunks = [];
|
|
@@ -28879,7 +29039,7 @@ function registerVarsCommand(program) {
|
|
|
28879
29039
|
value = typeof data === "string" ? data : JSON.stringify(data);
|
|
28880
29040
|
}
|
|
28881
29041
|
if (options.output) {
|
|
28882
|
-
await
|
|
29042
|
+
await writeFile8(options.output, value);
|
|
28883
29043
|
outputJson({ name, written: options.output });
|
|
28884
29044
|
return;
|
|
28885
29045
|
}
|