@tinycloud/cli 1.1.0-beta.13 → 1.1.0-beta.15
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 +90 -11
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +435 -96
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +5 -5
- package/skills/tc-cli/AUTH.md +6 -4
- package/skills/tc-cli/REFERENCE.md +1 -1
package/dist/legacy-entry.js
CHANGED
|
@@ -165,8 +165,14 @@ import ora from "ora";
|
|
|
165
165
|
function outputJson(data) {
|
|
166
166
|
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
167
167
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
168
|
+
function warningLine(warnings) {
|
|
169
|
+
const which = warnings.map((warning) => `${warning.grantCid ?? "unidentified"}: ${warning.reason}`).join(", ");
|
|
170
|
+
const subject = warnings.length === 1 ? "A stored grant was" : `${warnings.length} stored grants were`;
|
|
171
|
+
return `${subject} not used (${which}). Approve access again when a command needs it; the old grant expires on its own.`;
|
|
172
|
+
}
|
|
173
|
+
function outputError(code3, message, hint, details = {}) {
|
|
174
|
+
const { meta, warnings = [] } = details;
|
|
175
|
+
if (!shouldOutputJson()) {
|
|
170
176
|
process.stderr.write(
|
|
171
177
|
`${theme.error("\u2717")} ${theme.label(code3)}: ${message}
|
|
172
178
|
`
|
|
@@ -177,20 +183,40 @@ function outputError(code3, message, hint, meta) {
|
|
|
177
183
|
`);
|
|
178
184
|
}
|
|
179
185
|
}
|
|
186
|
+
if (warnings.length > 0) process.stderr.write(` ${theme.warn(warningLine(warnings))}
|
|
187
|
+
`);
|
|
180
188
|
} else {
|
|
181
189
|
const payload = {
|
|
182
190
|
error: { code: code3, message }
|
|
183
191
|
};
|
|
184
192
|
if (hint) payload.error.hint = hint;
|
|
185
193
|
if (meta) payload.error.meta = meta;
|
|
194
|
+
if (warnings.length > 0) payload.error.warnings = warnings;
|
|
186
195
|
process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
|
|
187
196
|
}
|
|
188
197
|
}
|
|
198
|
+
function outputWarnings(warnings) {
|
|
199
|
+
if (warnings.length === 0) return;
|
|
200
|
+
process.stderr.write(shouldOutputJson() ? `${JSON.stringify({ warnings })}
|
|
201
|
+
` : `${theme.warn("!")} ${warningLine(warnings)}
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
function operationWarnings(value) {
|
|
205
|
+
if (!Array.isArray(value)) return [];
|
|
206
|
+
return value.flatMap((warning) => {
|
|
207
|
+
if (warning === null || typeof warning !== "object") return [];
|
|
208
|
+
const { code: code3, reason, grantCid } = warning;
|
|
209
|
+
if (typeof code3 !== "string" || !WARNING_CODE.test(code3) || typeof reason !== "string" || !WARNING_REASON.test(reason)) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
return [{ code: code3, reason, ...typeof grantCid === "string" && DELEGATION_CID.test(grantCid) ? { grantCid } : {} }];
|
|
213
|
+
});
|
|
214
|
+
}
|
|
189
215
|
function isInteractive() {
|
|
190
216
|
return Boolean(process.stdout.isTTY);
|
|
191
217
|
}
|
|
192
218
|
async function withSpinner(label, fn) {
|
|
193
|
-
if (
|
|
219
|
+
if (shouldOutputJson()) {
|
|
194
220
|
return fn();
|
|
195
221
|
}
|
|
196
222
|
const spinner = ora(label).start();
|
|
@@ -204,7 +230,7 @@ async function withSpinner(label, fn) {
|
|
|
204
230
|
}
|
|
205
231
|
}
|
|
206
232
|
function shouldOutputJson() {
|
|
207
|
-
return !isInteractive() || process.argv.includes("--json");
|
|
233
|
+
return !isInteractive() || (globalThis[JSON_REQUESTED] ?? process.argv.includes("--json"));
|
|
208
234
|
}
|
|
209
235
|
function formatField(label, value) {
|
|
210
236
|
if (value === null || value === void 0) return ` ${theme.label(label + ":")} ${theme.muted("\u2014")}`;
|
|
@@ -253,10 +279,15 @@ function formatTimeAgo(date) {
|
|
|
253
279
|
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
254
280
|
return `${Math.floor(seconds / 86400)}d ago`;
|
|
255
281
|
}
|
|
282
|
+
var WARNING_CODE, WARNING_REASON, DELEGATION_CID, JSON_REQUESTED;
|
|
256
283
|
var init_formatter = __esm({
|
|
257
284
|
"src/output/formatter.ts"() {
|
|
258
285
|
"use strict";
|
|
259
286
|
init_theme();
|
|
287
|
+
WARNING_CODE = /^[A-Z][A-Z_]{0,63}$/;
|
|
288
|
+
WARNING_REASON = /^[a-z][a-z_]{0,63}$/;
|
|
289
|
+
DELEGATION_CID = /^bafkr4i[a-z2-7]{52}$/;
|
|
290
|
+
JSON_REQUESTED = /* @__PURE__ */ Symbol.for("tinycloud.cli.jsonOutputRequested");
|
|
260
291
|
}
|
|
261
292
|
});
|
|
262
293
|
|
|
@@ -361,11 +392,12 @@ function cliErrorFromService(error, message = error.message) {
|
|
|
361
392
|
return missingPrivateKeyError();
|
|
362
393
|
}
|
|
363
394
|
const missingCapability = validatedCapabilityOf({ ...error, meta }) !== void 0;
|
|
364
|
-
const denied = verdict === "forbidden" || verdict === "unauthenticated" && missingCapability;
|
|
395
|
+
const denied = verdict === "forbidden" || verdict === "unauthenticated" && missingCapability || verdict === void 0 && error.code === "PERMISSION_DENIED";
|
|
396
|
+
const unauthenticated = !denied && (verdict === "unauthenticated" || verdict === void 0 && error.code === "AUTH_REQUIRED");
|
|
365
397
|
return new CLIError(
|
|
366
|
-
denied ? "PERMISSION_DENIED" :
|
|
398
|
+
denied ? "PERMISSION_DENIED" : unauthenticated ? "AUTH_REQUIRED" : error.code,
|
|
367
399
|
message,
|
|
368
|
-
denied ? ExitCode.PERMISSION_DENIED :
|
|
400
|
+
denied ? ExitCode.PERMISSION_DENIED : unauthenticated ? ExitCode.AUTH_REQUIRED : ExitCode.ERROR,
|
|
369
401
|
meta
|
|
370
402
|
);
|
|
371
403
|
}
|
|
@@ -437,7 +469,10 @@ function handleError(error) {
|
|
|
437
469
|
meta.resource = capability.resource;
|
|
438
470
|
meta.requiredAction = capability.requiredAction;
|
|
439
471
|
}
|
|
440
|
-
outputError(cliError.code, cliError.message, hint,
|
|
472
|
+
outputError(cliError.code, cliError.message, hint, {
|
|
473
|
+
...Object.keys(meta).length ? { meta } : {},
|
|
474
|
+
warnings: operationWarnings(cliError.metadata?.warnings)
|
|
475
|
+
});
|
|
441
476
|
process.exit(cliError.exitCode);
|
|
442
477
|
}
|
|
443
478
|
function buildAuthHint(error) {
|
|
@@ -1942,6 +1977,8 @@ var CAPABILITIES = [
|
|
|
1942
1977
|
{ urn: "tinycloud.kv/put", service: "tinycloud.kv", status: "active" },
|
|
1943
1978
|
{ urn: "tinycloud.kv/del", service: "tinycloud.kv", status: "active" },
|
|
1944
1979
|
{ urn: "tinycloud.kv/delete", service: "tinycloud.kv", status: "deprecated-alias", aliasOf: "tinycloud.kv/del" },
|
|
1980
|
+
{ urn: "tinycloud.kv/sync", service: "tinycloud.kv", status: "active" },
|
|
1981
|
+
{ urn: "tinycloud.kv/retain", service: "tinycloud.kv", status: "active" },
|
|
1945
1982
|
{ urn: "tinycloud.sql/read", service: "tinycloud.sql", status: "active" },
|
|
1946
1983
|
{ urn: "tinycloud.sql/select", service: "tinycloud.sql", status: "deprecated-alias", aliasOf: "tinycloud.sql/read" },
|
|
1947
1984
|
{ urn: "tinycloud.sql/write", service: "tinycloud.sql", status: "active" },
|
|
@@ -6521,6 +6558,10 @@ var ErrorCodes = {
|
|
|
6521
6558
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
6522
6559
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
6523
6560
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
6561
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
6562
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
6563
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
6564
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
6524
6565
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
6525
6566
|
TIMEOUT: "TIMEOUT",
|
|
6526
6567
|
ABORTED: "ABORTED",
|
|
@@ -6532,6 +6573,10 @@ var ErrorCodes = {
|
|
|
6532
6573
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
6533
6574
|
KV_CONFLICT: "KV_CONFLICT",
|
|
6534
6575
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
6576
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
6577
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
6578
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
6579
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
6535
6580
|
// SQL-specific errors
|
|
6536
6581
|
SQL_ERROR: "SQL_ERROR",
|
|
6537
6582
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -7592,6 +7637,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
7592
7637
|
removePrefix
|
|
7593
7638
|
});
|
|
7594
7639
|
}
|
|
7640
|
+
/**
|
|
7641
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
7642
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
7643
|
+
* whole-space feed.
|
|
7644
|
+
*/
|
|
7645
|
+
async changes(options) {
|
|
7646
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
7647
|
+
return err(serviceError(
|
|
7648
|
+
ErrorCodes.INVALID_INPUT,
|
|
7649
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
7650
|
+
"kv"
|
|
7651
|
+
));
|
|
7652
|
+
}
|
|
7653
|
+
const syncPrefix = `${this._prefix}/`;
|
|
7654
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
7655
|
+
if (!response.ok) return response;
|
|
7656
|
+
return ok({
|
|
7657
|
+
...response.data,
|
|
7658
|
+
changes: response.data.changes.map((change) => ({
|
|
7659
|
+
...change,
|
|
7660
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
7661
|
+
}))
|
|
7662
|
+
});
|
|
7663
|
+
}
|
|
7595
7664
|
/**
|
|
7596
7665
|
* Delete a key.
|
|
7597
7666
|
*/
|
|
@@ -7642,9 +7711,16 @@ var KVAction = {
|
|
|
7642
7711
|
PUT: "tinycloud.kv/put",
|
|
7643
7712
|
LIST: "tinycloud.kv/list",
|
|
7644
7713
|
DELETE: "tinycloud.kv/del",
|
|
7645
|
-
HEAD: "tinycloud.kv/metadata"
|
|
7714
|
+
HEAD: "tinycloud.kv/metadata",
|
|
7715
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
7716
|
+
SYNC: "tinycloud.kv/sync",
|
|
7717
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
7718
|
+
RETAIN: "tinycloud.kv/retain"
|
|
7646
7719
|
};
|
|
7647
7720
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
7721
|
+
function isJsonObject(value) {
|
|
7722
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7723
|
+
}
|
|
7648
7724
|
function encodeKvBatchPartName(path) {
|
|
7649
7725
|
return encodeURIComponent(path).replace(
|
|
7650
7726
|
/[!'()*]/g,
|
|
@@ -8563,6 +8639,169 @@ var KVService = class extends BaseService {
|
|
|
8563
8639
|
}
|
|
8564
8640
|
});
|
|
8565
8641
|
}
|
|
8642
|
+
/**
|
|
8643
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
8644
|
+
*/
|
|
8645
|
+
async changes(options) {
|
|
8646
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
8647
|
+
if (!this.requireAuth()) {
|
|
8648
|
+
return err(authRequiredError("kv"));
|
|
8649
|
+
}
|
|
8650
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
8651
|
+
return err(serviceError(
|
|
8652
|
+
ErrorCodes.INVALID_INPUT,
|
|
8653
|
+
"KV changes prefix must be a non-empty string",
|
|
8654
|
+
"kv"
|
|
8655
|
+
));
|
|
8656
|
+
}
|
|
8657
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
8658
|
+
return err(serviceError(
|
|
8659
|
+
ErrorCodes.INVALID_INPUT,
|
|
8660
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
8661
|
+
"kv"
|
|
8662
|
+
));
|
|
8663
|
+
}
|
|
8664
|
+
const headers = {};
|
|
8665
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
8666
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
8667
|
+
if (options.retentionGrant !== void 0) {
|
|
8668
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
8669
|
+
}
|
|
8670
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
8671
|
+
try {
|
|
8672
|
+
const response = await this.invokeOperation(
|
|
8673
|
+
options.prefix,
|
|
8674
|
+
KVAction.SYNC,
|
|
8675
|
+
void 0,
|
|
8676
|
+
request.signal,
|
|
8677
|
+
headers
|
|
8678
|
+
);
|
|
8679
|
+
if (!response.ok) {
|
|
8680
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
8681
|
+
}
|
|
8682
|
+
const text = await response.text();
|
|
8683
|
+
let body;
|
|
8684
|
+
try {
|
|
8685
|
+
body = JSON.parse(text);
|
|
8686
|
+
} catch {
|
|
8687
|
+
body = void 0;
|
|
8688
|
+
}
|
|
8689
|
+
const page = this.normalizeChangesResponse(body);
|
|
8690
|
+
if (!page) {
|
|
8691
|
+
return err(serviceError(
|
|
8692
|
+
ErrorCodes.NETWORK_ERROR,
|
|
8693
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
8694
|
+
"kv",
|
|
8695
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
8696
|
+
));
|
|
8697
|
+
}
|
|
8698
|
+
return ok(page);
|
|
8699
|
+
} catch (error) {
|
|
8700
|
+
return err(wrapError2("kv", error));
|
|
8701
|
+
} finally {
|
|
8702
|
+
request.dispose();
|
|
8703
|
+
}
|
|
8704
|
+
});
|
|
8705
|
+
}
|
|
8706
|
+
/**
|
|
8707
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
8708
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose text names
|
|
8709
|
+
* `delegation-revoked` or `delegation-ancestor-revoked`. A body read that
|
|
8710
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
8711
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
8712
|
+
*/
|
|
8713
|
+
async changesFailure(response, prefix, signal) {
|
|
8714
|
+
if (response.status === 404) {
|
|
8715
|
+
return this.classifyNotFound(response, prefix);
|
|
8716
|
+
}
|
|
8717
|
+
let errorText;
|
|
8718
|
+
try {
|
|
8719
|
+
errorText = await response.text();
|
|
8720
|
+
} catch (error) {
|
|
8721
|
+
if (signal.aborted) throw error;
|
|
8722
|
+
errorText = "";
|
|
8723
|
+
}
|
|
8724
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
8725
|
+
let reason;
|
|
8726
|
+
let bodyCode;
|
|
8727
|
+
try {
|
|
8728
|
+
const body = JSON.parse(errorText);
|
|
8729
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
8730
|
+
if (isJsonObject(error)) {
|
|
8731
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
8732
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
8733
|
+
}
|
|
8734
|
+
} catch {
|
|
8735
|
+
}
|
|
8736
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
8737
|
+
if (response.status === 410) {
|
|
8738
|
+
return err(serviceError(
|
|
8739
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
8740
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
8741
|
+
"kv",
|
|
8742
|
+
{ meta: { ...meta, reason } }
|
|
8743
|
+
));
|
|
8744
|
+
}
|
|
8745
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
8746
|
+
return err(serviceError(
|
|
8747
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
8748
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
8749
|
+
"kv",
|
|
8750
|
+
{ meta: { ...meta, reason } }
|
|
8751
|
+
));
|
|
8752
|
+
}
|
|
8753
|
+
if (response.status === 401) {
|
|
8754
|
+
const revoked = /\bdelegation-ancestor-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED : /\bdelegation-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_REVOKED : void 0;
|
|
8755
|
+
if (revoked !== void 0) {
|
|
8756
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8759
|
+
if (response.status === 401 || response.status === 403) {
|
|
8760
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
8761
|
+
}
|
|
8762
|
+
return err(serviceError(
|
|
8763
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
8764
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
8765
|
+
"kv",
|
|
8766
|
+
{ meta }
|
|
8767
|
+
));
|
|
8768
|
+
}
|
|
8769
|
+
normalizeChangesResponse(data) {
|
|
8770
|
+
if (!isJsonObject(data)) return void 0;
|
|
8771
|
+
const { changes, more, cursor, source, authority } = data;
|
|
8772
|
+
if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
|
|
8773
|
+
return void 0;
|
|
8774
|
+
}
|
|
8775
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
8776
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
8777
|
+
return void 0;
|
|
8778
|
+
}
|
|
8779
|
+
const parsed = [];
|
|
8780
|
+
for (const change of changes) {
|
|
8781
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
8782
|
+
if (change.deleted === true) {
|
|
8783
|
+
parsed.push({ key: change.key, deleted: true });
|
|
8784
|
+
continue;
|
|
8785
|
+
}
|
|
8786
|
+
const { metadata } = change;
|
|
8787
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
8788
|
+
return void 0;
|
|
8789
|
+
}
|
|
8790
|
+
parsed.push({
|
|
8791
|
+
key: change.key,
|
|
8792
|
+
deleted: false,
|
|
8793
|
+
etag: change.etag,
|
|
8794
|
+
metadata
|
|
8795
|
+
});
|
|
8796
|
+
}
|
|
8797
|
+
return {
|
|
8798
|
+
changes: parsed,
|
|
8799
|
+
more,
|
|
8800
|
+
cursor,
|
|
8801
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
8802
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
8803
|
+
};
|
|
8804
|
+
}
|
|
8566
8805
|
/**
|
|
8567
8806
|
* Delete a key.
|
|
8568
8807
|
*/
|
|
@@ -13471,6 +13710,10 @@ function canonicalOwnerDid(did, label = "--owner") {
|
|
|
13471
13710
|
}
|
|
13472
13711
|
return `did:pkh:eip155:${match[1]}:${ensureEip55(match[2])}`;
|
|
13473
13712
|
}
|
|
13713
|
+
function canonicalNetworkUrn(path) {
|
|
13714
|
+
const match = /^urn:tinycloud:encryption:(did:pkh:eip155:[1-9]\d*:0x[0-9a-fA-F]{40}):([^:]*)$/.exec(path);
|
|
13715
|
+
return match ? `urn:tinycloud:encryption:${canonicalOwnerDid(match[1])}:${match[2]}` : path;
|
|
13716
|
+
}
|
|
13474
13717
|
function rawEncryptionOwnerMatches(path, ownerDid) {
|
|
13475
13718
|
const match = /^urn:tinycloud:encryption:(did:pkh:eip155:.+):([a-z0-9][a-z0-9-]*)$/.exec(path);
|
|
13476
13719
|
if (!match) return false;
|
|
@@ -13613,13 +13856,14 @@ async function replayAdditionalDelegations(node, profile, options) {
|
|
|
13613
13856
|
for (const stored of entries) {
|
|
13614
13857
|
const kind = storedDelegationKind(stored);
|
|
13615
13858
|
if (kind === "compact" || kind === "signed-login") {
|
|
13616
|
-
const
|
|
13859
|
+
const replay = await replayStoredDelegation(activator, stored, {
|
|
13617
13860
|
host: options.host,
|
|
13618
13861
|
migrated,
|
|
13619
13862
|
resolveSpace
|
|
13620
13863
|
});
|
|
13621
|
-
if (
|
|
13622
|
-
process.stderr.write(
|
|
13864
|
+
if (replay.status === "skipped" && process.env.TC_DEBUG_REPLAY === "1") {
|
|
13865
|
+
process.stderr.write(`[replay] skipping a stored delegation: ${replay.reason}
|
|
13866
|
+
`);
|
|
13623
13867
|
}
|
|
13624
13868
|
continue;
|
|
13625
13869
|
}
|
|
@@ -13649,6 +13893,14 @@ async function replayAdditionalDelegations(node, profile, options) {
|
|
|
13649
13893
|
function storedAdditionalDelegation(delegation, permissions) {
|
|
13650
13894
|
return { delegation, permissions };
|
|
13651
13895
|
}
|
|
13896
|
+
async function cliGrantRecord(node, delegation, permissions, host) {
|
|
13897
|
+
const { bindCliGrant } = await import("@tinycloud/operations/delegation-binding");
|
|
13898
|
+
return bindCliGrant(
|
|
13899
|
+
node,
|
|
13900
|
+
storedAdditionalDelegation(delegation, permissions),
|
|
13901
|
+
host
|
|
13902
|
+
);
|
|
13903
|
+
}
|
|
13652
13904
|
async function appendGrantHistory(profile, entry) {
|
|
13653
13905
|
await ProfileManager.ensureProfileDir(profile);
|
|
13654
13906
|
const line = JSON.stringify({
|
|
@@ -13803,7 +14055,8 @@ function permissionsFromDelegation(delegation) {
|
|
|
13803
14055
|
}
|
|
13804
14056
|
function compactPermission(permission) {
|
|
13805
14057
|
const service = permission.service;
|
|
13806
|
-
const
|
|
14058
|
+
const fullSpace = permission.space ?? (isRawEncryptionPermission(permission) ? ENCRYPTION_MANIFEST_SPACE2 : "");
|
|
14059
|
+
const space = fullSpace.startsWith("tinycloud:") ? fullSpace.slice(fullSpace.lastIndexOf(":") + 1) : fullSpace;
|
|
13807
14060
|
const actions = permission.actions.map((action) => action.startsWith(`${service}/`) ? action.slice(service.length + 1) : action).join(",");
|
|
13808
14061
|
return `${service}:${space}:${permission.path}:${actions}`;
|
|
13809
14062
|
}
|
|
@@ -13899,6 +14152,35 @@ function serviceFromActions(actions) {
|
|
|
13899
14152
|
return first.includes("/") ? first.slice(0, first.indexOf("/")) : "tinycloud.unknown";
|
|
13900
14153
|
}
|
|
13901
14154
|
|
|
14155
|
+
// src/auth/session-expired.ts
|
|
14156
|
+
init_constants();
|
|
14157
|
+
init_errors();
|
|
14158
|
+
var SESSION_EXPIRED_REASON = "session_expired";
|
|
14159
|
+
function signInAgainHint(profileName, posture) {
|
|
14160
|
+
if (posture === "local-owner-key") {
|
|
14161
|
+
return `Sign in again with: tc --profile ${profileName} auth login --method local`;
|
|
14162
|
+
}
|
|
14163
|
+
if (posture === "delegate-session") {
|
|
14164
|
+
return `Have the owner approve a new scoped login: tc --profile ${profileName} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
|
|
14165
|
+
}
|
|
14166
|
+
return `Sign in again with: tc --profile ${profileName} auth login --method openkey`;
|
|
14167
|
+
}
|
|
14168
|
+
function sessionExpiredError(profileName, posture) {
|
|
14169
|
+
return new CLIError(
|
|
14170
|
+
"AUTH_REQUIRED",
|
|
14171
|
+
`The session for profile "${profileName}" has expired or is no longer valid.`,
|
|
14172
|
+
ExitCode.AUTH_REQUIRED,
|
|
14173
|
+
{ hint: signInAgainHint(profileName, posture), reason: SESSION_EXPIRED_REASON }
|
|
14174
|
+
);
|
|
14175
|
+
}
|
|
14176
|
+
function withSignInHint(error, profileName, posture) {
|
|
14177
|
+
if (error.code !== "AUTH_REQUIRED") return error;
|
|
14178
|
+
return new CLIError(error.code, error.message, error.exitCode, {
|
|
14179
|
+
...error.metadata,
|
|
14180
|
+
hint: signInAgainHint(profileName, posture)
|
|
14181
|
+
});
|
|
14182
|
+
}
|
|
14183
|
+
|
|
13902
14184
|
// src/lib/sdk.ts
|
|
13903
14185
|
function jwkHasPrivateParameter(jwk) {
|
|
13904
14186
|
if (!jwk || typeof jwk !== "object") return false;
|
|
@@ -13925,6 +14207,16 @@ function signerJwkForProfile(profileName, sessionJwk, key) {
|
|
|
13925
14207
|
}
|
|
13926
14208
|
);
|
|
13927
14209
|
}
|
|
14210
|
+
async function restoreProfileSession(node, profileName, profile, sessionData) {
|
|
14211
|
+
try {
|
|
14212
|
+
await node.restoreSession(sessionData);
|
|
14213
|
+
} catch (error) {
|
|
14214
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "AUTH_EXPIRED") {
|
|
14215
|
+
throw sessionExpiredError(profileName, profile === null ? void 0 : resolveProfilePosture(profile));
|
|
14216
|
+
}
|
|
14217
|
+
throw error;
|
|
14218
|
+
}
|
|
14219
|
+
}
|
|
13928
14220
|
async function createSDKInstance(ctx, options) {
|
|
13929
14221
|
const profile = options?.privateKey ? await ProfileManager.getProfile(ctx.profile).catch(() => null) : await ProfileManager.getProfile(ctx.profile);
|
|
13930
14222
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
@@ -13944,7 +14236,7 @@ async function createSDKInstance(ctx, options) {
|
|
|
13944
14236
|
});
|
|
13945
14237
|
let restoredOwnSession2 = false;
|
|
13946
14238
|
if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
|
|
13947
|
-
await node2.
|
|
14239
|
+
await restoreProfileSession(node2, ctx.profile, profile, {
|
|
13948
14240
|
delegationHeader: session.delegationHeader,
|
|
13949
14241
|
delegationCid: session.delegationCid,
|
|
13950
14242
|
spaceId: session.spaceId,
|
|
@@ -13974,7 +14266,7 @@ async function createSDKInstance(ctx, options) {
|
|
|
13974
14266
|
if (options?.privateKey) {
|
|
13975
14267
|
await node.signIn();
|
|
13976
14268
|
} else if (session && session.delegationHeader && session.delegationCid && session.spaceId) {
|
|
13977
|
-
await node.
|
|
14269
|
+
await restoreProfileSession(node, ctx.profile, profile, {
|
|
13978
14270
|
delegationHeader: session.delegationHeader,
|
|
13979
14271
|
delegationCid: session.delegationCid,
|
|
13980
14272
|
spaceId: session.spaceId,
|
|
@@ -14556,6 +14848,41 @@ function validateDelegationCallbackPayload(value) {
|
|
|
14556
14848
|
}
|
|
14557
14849
|
return null;
|
|
14558
14850
|
}
|
|
14851
|
+
async function delegationFromInput(input, did, options) {
|
|
14852
|
+
const trimmed = input.trim();
|
|
14853
|
+
let parsed;
|
|
14854
|
+
if (/^[a-z2-7]{4}-[a-z2-7]{4}$/i.test(trimmed)) {
|
|
14855
|
+
if (!options.jwk) throw new Error("A CLI session key is required to retrieve a delegation.");
|
|
14856
|
+
const apiHost = process.env.TC_OPENKEY_API_HOST ?? options.openkeyApiHost ?? (options.openkeyHost && options.openkeyHost !== DEFAULT_OPENKEY_HOST ? options.openkeyHost : DEFAULT_OPENKEY_DEVICE_API_HOST);
|
|
14857
|
+
const response = await fetch(`${apiHost.replace(/\/$/, "")}/api/delegation-codes/${trimmed.toLowerCase()}`, {
|
|
14858
|
+
redirect: "error"
|
|
14859
|
+
});
|
|
14860
|
+
if (!response.ok) {
|
|
14861
|
+
throw new Error(response.status === 404 ? "Delegation code not found or expired. Use the full code shown in OpenKey instead." : `Could not retrieve delegation code (HTTP ${response.status}).`);
|
|
14862
|
+
}
|
|
14863
|
+
const result = await response.json();
|
|
14864
|
+
parsed = result.delegation;
|
|
14865
|
+
const delegation = parsed;
|
|
14866
|
+
const jwk = delegation?.jwk;
|
|
14867
|
+
const localJwk = publicJwkForDelegation(options.jwk);
|
|
14868
|
+
if (typeof delegation?.verificationMethod !== "string" || delegation.verificationMethod.split("#")[0] !== did.split("#")[0] || !jwk || typeof jwk !== "object" || Array.isArray(jwk) || jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string" || jwk.x !== localJwk.x || [...PRIVATE_JWK_FIELDS].some((field) => field in jwk)) {
|
|
14869
|
+
throw new Error("Delegation is bound to a different CLI key.");
|
|
14870
|
+
}
|
|
14871
|
+
} else {
|
|
14872
|
+
try {
|
|
14873
|
+
parsed = JSON.parse(trimmed);
|
|
14874
|
+
} catch {
|
|
14875
|
+
try {
|
|
14876
|
+
parsed = JSON.parse(Buffer.from(trimmed, "base64").toString("utf-8"));
|
|
14877
|
+
} catch {
|
|
14878
|
+
throw new Error("Invalid delegation code. Expected JSON, base64-encoded JSON, or XXXX-XXXX.");
|
|
14879
|
+
}
|
|
14880
|
+
}
|
|
14881
|
+
}
|
|
14882
|
+
const invalid = validateDelegationCallbackPayload(parsed);
|
|
14883
|
+
if (invalid) throw new Error(`Invalid delegation code: ${invalid}`);
|
|
14884
|
+
return parsed;
|
|
14885
|
+
}
|
|
14559
14886
|
function shouldOpenBrowser(options) {
|
|
14560
14887
|
if (options.noPopup) return false;
|
|
14561
14888
|
const env = process.env.TC_AUTH_NO_POPUP ?? process.env.TC_NO_POPUP;
|
|
@@ -14580,21 +14907,6 @@ async function callbackFlow(did, options = {}) {
|
|
|
14580
14907
|
reject(result.error);
|
|
14581
14908
|
}
|
|
14582
14909
|
}
|
|
14583
|
-
function parsePasteInput(input) {
|
|
14584
|
-
const trimmed = input.trim();
|
|
14585
|
-
let parsed;
|
|
14586
|
-
try {
|
|
14587
|
-
parsed = JSON.parse(trimmed);
|
|
14588
|
-
} catch {
|
|
14589
|
-
const decoded = Buffer.from(trimmed, "base64").toString("utf-8");
|
|
14590
|
-
parsed = JSON.parse(decoded);
|
|
14591
|
-
}
|
|
14592
|
-
const invalid = validateDelegationCallbackPayload(parsed);
|
|
14593
|
-
if (invalid) {
|
|
14594
|
-
throw new Error(`Invalid delegation code: ${invalid}`);
|
|
14595
|
-
}
|
|
14596
|
-
return parsed;
|
|
14597
|
-
}
|
|
14598
14910
|
const server = createServer((req, res) => {
|
|
14599
14911
|
if (req.method === "POST" && req.url === "/callback") {
|
|
14600
14912
|
let body = "";
|
|
@@ -14663,18 +14975,21 @@ async function callbackFlow(did, options = {}) {
|
|
|
14663
14975
|
}
|
|
14664
14976
|
if (process.stdin.isTTY) {
|
|
14665
14977
|
console.error(`
|
|
14666
|
-
If the browser can't connect back, paste the delegation code here:`);
|
|
14978
|
+
If the browser can't connect back, enter the short code or paste the full delegation code here:`);
|
|
14667
14979
|
rl = createInterface({
|
|
14668
14980
|
input: process.stdin,
|
|
14669
14981
|
output: process.stderr
|
|
14670
14982
|
});
|
|
14671
|
-
|
|
14672
|
-
|
|
14983
|
+
let resolving = false;
|
|
14984
|
+
rl.on("line", async (input) => {
|
|
14985
|
+
if (settled || resolving) return;
|
|
14986
|
+
resolving = true;
|
|
14673
14987
|
try {
|
|
14674
|
-
|
|
14675
|
-
|
|
14676
|
-
|
|
14677
|
-
|
|
14988
|
+
settle({ data: await delegationFromInput(input, did, options) });
|
|
14989
|
+
} catch (error) {
|
|
14990
|
+
console.error(error instanceof Error ? error.message : "Invalid delegation code. Try again:");
|
|
14991
|
+
} finally {
|
|
14992
|
+
resolving = false;
|
|
14678
14993
|
}
|
|
14679
14994
|
});
|
|
14680
14995
|
}
|
|
@@ -14701,24 +15016,7 @@ Open this URL in a browser to authenticate:
|
|
|
14701
15016
|
if (answered || input.trim() === "") return;
|
|
14702
15017
|
answered = true;
|
|
14703
15018
|
rl.close();
|
|
14704
|
-
|
|
14705
|
-
try {
|
|
14706
|
-
parsed = JSON.parse(input.trim());
|
|
14707
|
-
} catch {
|
|
14708
|
-
try {
|
|
14709
|
-
const decoded = Buffer.from(input.trim(), "base64").toString("utf-8");
|
|
14710
|
-
parsed = JSON.parse(decoded);
|
|
14711
|
-
} catch {
|
|
14712
|
-
reject(new Error("Invalid delegation code. Expected JSON or base64-encoded JSON."));
|
|
14713
|
-
return;
|
|
14714
|
-
}
|
|
14715
|
-
}
|
|
14716
|
-
const invalid = validateDelegationCallbackPayload(parsed);
|
|
14717
|
-
if (invalid) {
|
|
14718
|
-
reject(new Error(`Invalid delegation code: ${invalid}`));
|
|
14719
|
-
return;
|
|
14720
|
-
}
|
|
14721
|
-
resolve4(parsed);
|
|
15019
|
+
void delegationFromInput(input, did, options).then(resolve4, reject);
|
|
14722
15020
|
});
|
|
14723
15021
|
rl.on("close", () => {
|
|
14724
15022
|
if (answered) return;
|
|
@@ -14733,7 +15031,7 @@ Open this URL in a browser to authenticate:
|
|
|
14733
15031
|
}
|
|
14734
15032
|
));
|
|
14735
15033
|
});
|
|
14736
|
-
rl.setPrompt("
|
|
15034
|
+
rl.setPrompt("Enter short code or paste full delegation code: ");
|
|
14737
15035
|
rl.prompt();
|
|
14738
15036
|
});
|
|
14739
15037
|
}
|
|
@@ -15017,7 +15315,13 @@ function validateLoginPermissions(permissions) {
|
|
|
15017
15315
|
}
|
|
15018
15316
|
}
|
|
15019
15317
|
function scopedLoginPermissions(permissions) {
|
|
15020
|
-
|
|
15318
|
+
return withCapabilitiesRead(permissions, permissions.find((p) => !isRawEncryptionPermission(p)).space ?? "");
|
|
15319
|
+
}
|
|
15320
|
+
function grantRequestPermissions(group, anchorSpace) {
|
|
15321
|
+
const request = group.map((p) => isRawEncryptionPermission(p) ? { ...p, space: ENCRYPTION_MANIFEST_SPACE2, path: canonicalNetworkUrn(p.path) } : p);
|
|
15322
|
+
return withCapabilitiesRead(request, request.find((p) => !isRawEncryptionPermission(p))?.space ?? anchorSpace);
|
|
15323
|
+
}
|
|
15324
|
+
function withCapabilitiesRead(permissions, space) {
|
|
15021
15325
|
const hasRead = permissions.some((p) => p.service === "tinycloud.capabilities" && p.path === "" && p.actions.includes(CAPABILITIES_READ) && normalizePkhIdentifier(p.space ?? "") === normalizePkhIdentifier(space));
|
|
15022
15326
|
return hasRead ? permissions : [{ service: "tinycloud.capabilities", space, path: "", actions: [CAPABILITIES_READ] }, ...permissions];
|
|
15023
15327
|
}
|
|
@@ -15904,6 +16208,7 @@ function registerAuthCommand(program) {
|
|
|
15904
16208
|
"Grant requested TinyCloud permissions from `tc auth request --grant`.",
|
|
15905
16209
|
group
|
|
15906
16210
|
);
|
|
16211
|
+
const request = grantRequestPermissions(group, profile.spaceId ?? profile.spaceName);
|
|
15907
16212
|
let delegationData;
|
|
15908
16213
|
if (options.device) {
|
|
15909
16214
|
const approval = await acquireDeviceDelegation({
|
|
@@ -15911,7 +16216,7 @@ function registerAuthCommand(program) {
|
|
|
15911
16216
|
jwk: key,
|
|
15912
16217
|
nodeOrigin: ctx.host,
|
|
15913
16218
|
shareOrigin: DEFAULT_SHARE_ORIGIN,
|
|
15914
|
-
permissions:
|
|
16219
|
+
permissions: request,
|
|
15915
16220
|
expiry: parseRequestedExpiry(expiryOption ?? "7d"),
|
|
15916
16221
|
reason,
|
|
15917
16222
|
expectedOwner: pinnedOwner(profile),
|
|
@@ -15923,17 +16228,17 @@ function registerAuthCommand(program) {
|
|
|
15923
16228
|
delegationData = await startAuthFlow(profile.did, {
|
|
15924
16229
|
jwk: key,
|
|
15925
16230
|
host: ctx.host,
|
|
15926
|
-
permissions:
|
|
16231
|
+
permissions: request,
|
|
15927
16232
|
reason,
|
|
15928
16233
|
openkeyHost,
|
|
15929
16234
|
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
|
|
15930
16235
|
noPopup: options.popup === false
|
|
15931
16236
|
});
|
|
15932
16237
|
}
|
|
15933
|
-
const delegation = portableFromOpenKeyDelegation(delegationData,
|
|
16238
|
+
const delegation = portableFromOpenKeyDelegation(delegationData, request, ctx.host, proof);
|
|
15934
16239
|
grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
|
|
15935
16240
|
}
|
|
15936
|
-
await activateAndStoreOpenKeyGrants(ctx.profile, node, grants, options.manifest ? "manifest" : "cli");
|
|
16241
|
+
await activateAndStoreOpenKeyGrants(ctx.profile, ctx.host, node, grants, options.manifest ? "manifest" : "cli");
|
|
15937
16242
|
const delegationCids2 = grants.map(({ delegation }) => delegation.cid);
|
|
15938
16243
|
const expiry2 = grants.at(-1)?.delegation.expiry.toISOString();
|
|
15939
16244
|
reportDeclined(declined);
|
|
@@ -15969,8 +16274,7 @@ function registerAuthCommand(program) {
|
|
|
15969
16274
|
for (const delegation of delegations) {
|
|
15970
16275
|
const covering = permissionsFromDelegation(delegation);
|
|
15971
16276
|
localEffective.push(...covering);
|
|
15972
|
-
|
|
15973
|
-
await appendAdditionalDelegation(ctx.profile, stored);
|
|
16277
|
+
await appendAdditionalDelegation(ctx.profile, await cliGrantRecord(node, delegation, covering, ctx.host));
|
|
15974
16278
|
delegationCids.push(delegation.cid);
|
|
15975
16279
|
expiry = delegation.expiry.toISOString();
|
|
15976
16280
|
await appendGrantHistory(ctx.profile, {
|
|
@@ -16392,6 +16696,8 @@ async function importRequestBoundDelegation(ctx, artifact) {
|
|
|
16392
16696
|
{ profile: ctx.profile, host: ctx.host, allowOwnerProfile: true },
|
|
16393
16697
|
artifact
|
|
16394
16698
|
);
|
|
16699
|
+
const warnings = operationWarnings(result.warnings);
|
|
16700
|
+
const metadata = warnings.length === 0 ? void 0 : { warnings };
|
|
16395
16701
|
switch (result.status) {
|
|
16396
16702
|
case "ok": {
|
|
16397
16703
|
const output2 = result.output;
|
|
@@ -16404,22 +16710,25 @@ async function importRequestBoundDelegation(ctx, artifact) {
|
|
|
16404
16710
|
permissions: output2.effectivePermissions,
|
|
16405
16711
|
expiry: output2.expiry
|
|
16406
16712
|
});
|
|
16713
|
+
outputWarnings(warnings);
|
|
16407
16714
|
return;
|
|
16408
16715
|
}
|
|
16409
16716
|
case "authority_required":
|
|
16410
16717
|
throw new CLIError(
|
|
16411
16718
|
"AUTHORITY_REQUIRED",
|
|
16412
16719
|
"The active session requires additional authority before importing this delegation.",
|
|
16413
|
-
ExitCode.PERMISSION_DENIED
|
|
16720
|
+
ExitCode.PERMISSION_DENIED,
|
|
16721
|
+
metadata
|
|
16414
16722
|
);
|
|
16415
16723
|
case "setup_required":
|
|
16416
16724
|
throw new CLIError(
|
|
16417
16725
|
"SETUP_REQUIRED",
|
|
16418
16726
|
"The active profile requires setup before importing this delegation.",
|
|
16419
|
-
ExitCode.ERROR
|
|
16727
|
+
ExitCode.ERROR,
|
|
16728
|
+
metadata
|
|
16420
16729
|
);
|
|
16421
16730
|
case "error":
|
|
16422
|
-
throw cliErrorFromService(result.error);
|
|
16731
|
+
throw withSignInHint(cliErrorFromService({ ...result.error, meta: metadata }), ctx.profile, result.context.posture);
|
|
16423
16732
|
}
|
|
16424
16733
|
}
|
|
16425
16734
|
function isStoredDelegationLike(value) {
|
|
@@ -16444,9 +16753,10 @@ function normalizePortableDelegation(delegation) {
|
|
|
16444
16753
|
}
|
|
16445
16754
|
return { ...delegation, expiry };
|
|
16446
16755
|
}
|
|
16447
|
-
async function activateAndStoreOpenKeyGrants(profileName, node, grants, source) {
|
|
16448
|
-
for (const { delegation } of grants) await node.useRuntimeDelegation(delegation);
|
|
16756
|
+
async function activateAndStoreOpenKeyGrants(profileName, host, node, grants, source) {
|
|
16449
16757
|
if (grants.length === 0) return;
|
|
16758
|
+
const records = await Promise.all(grants.map(({ delegation, effective }) => cliGrantRecord(node, delegation, effective, host)));
|
|
16759
|
+
for (const { delegation } of grants) await node.useRuntimeDelegation(delegation);
|
|
16450
16760
|
await ProfileManager.withLock(profileName, async () => {
|
|
16451
16761
|
for (const { delegation, effective } of grants) {
|
|
16452
16762
|
await appendGrantHistory(profileName, {
|
|
@@ -16456,10 +16766,7 @@ async function activateAndStoreOpenKeyGrants(profileName, node, grants, source)
|
|
|
16456
16766
|
expiry: delegation.expiry.toISOString()
|
|
16457
16767
|
});
|
|
16458
16768
|
}
|
|
16459
|
-
await appendAdditionalDelegations(
|
|
16460
|
-
profileName,
|
|
16461
|
-
grants.map(({ delegation, effective }) => storedAdditionalDelegation(delegation, effective))
|
|
16462
|
-
);
|
|
16769
|
+
await appendAdditionalDelegations(profileName, records);
|
|
16463
16770
|
});
|
|
16464
16771
|
}
|
|
16465
16772
|
async function ensureDelegationAuthority(params) {
|
|
@@ -16482,20 +16789,22 @@ async function ensureDelegationAuthority(params) {
|
|
|
16482
16789
|
expectedOwner: pinnedOwner(params.profile),
|
|
16483
16790
|
expiry: expiryCap
|
|
16484
16791
|
};
|
|
16792
|
+
const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
|
|
16485
16793
|
const grants = [];
|
|
16486
16794
|
for (const group of groupPermissionsBySpace(params.requested)) {
|
|
16795
|
+
const request = grantRequestPermissions(group, anchorSpace);
|
|
16487
16796
|
const delegationData = await acquireOpenKey(params.profile.did, {
|
|
16488
16797
|
jwk: key,
|
|
16489
16798
|
host: params.ctx.host,
|
|
16490
|
-
permissions:
|
|
16799
|
+
permissions: request,
|
|
16491
16800
|
reason: permissionGrantReason(params.reason, group),
|
|
16492
16801
|
openkeyHost,
|
|
16493
16802
|
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
|
|
16494
16803
|
});
|
|
16495
|
-
const delegation = portableFromOpenKeyDelegation(delegationData,
|
|
16804
|
+
const delegation = portableFromOpenKeyDelegation(delegationData, request, params.ctx.host, proof);
|
|
16496
16805
|
grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
|
|
16497
16806
|
}
|
|
16498
|
-
await activateAndStoreOpenKeyGrants(params.ctx.profile, params.node, grants, "cli");
|
|
16807
|
+
await activateAndStoreOpenKeyGrants(params.ctx.profile, params.ctx.host, params.node, grants, "cli");
|
|
16499
16808
|
return;
|
|
16500
16809
|
}
|
|
16501
16810
|
if (isInteractive()) {
|
|
@@ -16517,7 +16826,7 @@ async function ensureDelegationAuthority(params) {
|
|
|
16517
16826
|
const covering = permissionsFromDelegation(delegation);
|
|
16518
16827
|
await appendAdditionalDelegation(
|
|
16519
16828
|
params.ctx.profile,
|
|
16520
|
-
|
|
16829
|
+
await cliGrantRecord(params.node, delegation, covering, params.ctx.host)
|
|
16521
16830
|
);
|
|
16522
16831
|
await appendGrantHistory(params.ctx.profile, {
|
|
16523
16832
|
addedCaps: covering,
|
|
@@ -16649,7 +16958,8 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
|
|
|
16649
16958
|
}
|
|
16650
16959
|
const returnedSpace = data.spaceId;
|
|
16651
16960
|
const effective = session.permissions;
|
|
16652
|
-
const
|
|
16961
|
+
const spaced = effective.filter((permission) => !isRawEncryptionPermission(permission));
|
|
16962
|
+
const primary = spaced.find((permission) => permission.service !== "tinycloud.capabilities") ?? spaced[0] ?? effective[0];
|
|
16653
16963
|
const resources = effective.map((permission) => ({
|
|
16654
16964
|
service: permission.service.slice("tinycloud.".length),
|
|
16655
16965
|
space: isVerifiedRawEncryptionPermission(permission) ? "encryption" : returnedSpace,
|
|
@@ -16669,7 +16979,9 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
|
|
|
16669
16979
|
delegateDID: data.verificationMethod,
|
|
16670
16980
|
ownerAddress: ownerParts[4],
|
|
16671
16981
|
chainId: Number(ownerParts[3]),
|
|
16672
|
-
host
|
|
16982
|
+
host,
|
|
16983
|
+
// Verified above; replay rebuilds the CACAO from it before trusting the grant.
|
|
16984
|
+
siweProof: { siwe: data.siwe, signature: data.signature }
|
|
16673
16985
|
};
|
|
16674
16986
|
}
|
|
16675
16987
|
async function rotateAuthKey(profileName, host, options = {}) {
|
|
@@ -18394,6 +18706,7 @@ async function runSecretOperation(params) {
|
|
|
18394
18706
|
reason: secretPermissionReason(params.action, params.name),
|
|
18395
18707
|
yes: true,
|
|
18396
18708
|
force: true,
|
|
18709
|
+
anchorSpace: params.space ?? SECRETS_SPACE3,
|
|
18397
18710
|
openKeyAcquisition: params.openKeyAcquisition
|
|
18398
18711
|
})
|
|
18399
18712
|
);
|
|
@@ -18478,7 +18791,26 @@ async function runSecretOperationAttempt(label, operation) {
|
|
|
18478
18791
|
throw error;
|
|
18479
18792
|
}
|
|
18480
18793
|
}
|
|
18794
|
+
function withOperationWarnings(error, warnings) {
|
|
18795
|
+
if (warnings === void 0 || warnings.length === 0) return error;
|
|
18796
|
+
const cliError = wrapError(error);
|
|
18797
|
+
cliError.metadata = { ...cliError.metadata, warnings };
|
|
18798
|
+
return cliError;
|
|
18799
|
+
}
|
|
18481
18800
|
async function invokeCanonicalSecretGet(params) {
|
|
18801
|
+
const warnings = /* @__PURE__ */ new Map();
|
|
18802
|
+
const collect = (result) => {
|
|
18803
|
+
for (const warning of result.warnings ?? []) warnings.set(JSON.stringify(warning), warning);
|
|
18804
|
+
return result;
|
|
18805
|
+
};
|
|
18806
|
+
try {
|
|
18807
|
+
const result = await canonicalSecretGet(params, collect);
|
|
18808
|
+
return warnings.size === 0 ? result : { ...result, warnings: [...warnings.values()] };
|
|
18809
|
+
} catch (error) {
|
|
18810
|
+
throw withOperationWarnings(error, [...warnings.values()]);
|
|
18811
|
+
}
|
|
18812
|
+
}
|
|
18813
|
+
async function canonicalSecretGet(params, collect) {
|
|
18482
18814
|
const auth = authOptions(params.options);
|
|
18483
18815
|
let ownerNode;
|
|
18484
18816
|
if (!auth?.privateKey) {
|
|
@@ -18501,10 +18833,10 @@ async function invokeCanonicalSecretGet(params) {
|
|
|
18501
18833
|
...params.scope === void 0 ? {} : { scope: params.scope },
|
|
18502
18834
|
...params.space === void 0 ? {} : { space: params.space }
|
|
18503
18835
|
};
|
|
18504
|
-
const invoke = () => withSpinner(
|
|
18836
|
+
const invoke = async () => collect(await withSpinner(
|
|
18505
18837
|
params.label,
|
|
18506
18838
|
() => auth?.privateKey ? invokeSecretsGetWithLocalAuthorityRetry(target, input) : invokeOperation2("tinycloud.secrets.get", 1, target, input)
|
|
18507
|
-
);
|
|
18839
|
+
));
|
|
18508
18840
|
let first = await invoke();
|
|
18509
18841
|
if (first.status === "error" && first.error.code === "SESSION_NOT_FOUND" && auth?.privateKey === void 0) {
|
|
18510
18842
|
const profile2 = await ProfileManager.getProfile(params.ctx.profile);
|
|
@@ -18542,6 +18874,7 @@ async function invokeCanonicalSecretGet(params) {
|
|
|
18542
18874
|
reason: secretPermissionReason("get", params.name),
|
|
18543
18875
|
yes: true,
|
|
18544
18876
|
force: true,
|
|
18877
|
+
anchorSpace: params.space ?? SECRETS_SPACE3,
|
|
18545
18878
|
openKeyAcquisition: params.openKeyAcquisition
|
|
18546
18879
|
})
|
|
18547
18880
|
);
|
|
@@ -18553,7 +18886,7 @@ function throwCanonicalSecretGetError(result, name) {
|
|
|
18553
18886
|
throw new CLIError(
|
|
18554
18887
|
"PERMISSION_DENIED",
|
|
18555
18888
|
"Permission denied while reading secret",
|
|
18556
|
-
ExitCode.
|
|
18889
|
+
ExitCode.PERMISSION_DENIED
|
|
18557
18890
|
);
|
|
18558
18891
|
case "setup_required":
|
|
18559
18892
|
throw new CLIError(
|
|
@@ -18575,11 +18908,10 @@ ${result.setup.message}`, setup: result.setup }
|
|
|
18575
18908
|
if (result.error.code === "NODE_UNREACHABLE") {
|
|
18576
18909
|
throw new CLIError("NETWORK_ERROR", result.error.message, ExitCode.NETWORK_ERROR);
|
|
18577
18910
|
}
|
|
18578
|
-
|
|
18579
|
-
result.error.code,
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
);
|
|
18911
|
+
if (result.error.code === "PERMISSION_HINT_INVALID") {
|
|
18912
|
+
throw new CLIError(result.error.code, result.error.message, ExitCode.PERMISSION_DENIED);
|
|
18913
|
+
}
|
|
18914
|
+
throw withSignInHint(cliErrorFromService(result.error), result.context.profile, result.context.posture);
|
|
18583
18915
|
case "ok":
|
|
18584
18916
|
throw new Error("Expected a failed canonical secret result.");
|
|
18585
18917
|
}
|
|
@@ -19195,19 +19527,26 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
19195
19527
|
openKeyAcquisition
|
|
19196
19528
|
});
|
|
19197
19529
|
if (result.status !== "ok") {
|
|
19198
|
-
|
|
19530
|
+
try {
|
|
19531
|
+
throwCanonicalSecretGetError(result, name);
|
|
19532
|
+
} catch (error) {
|
|
19533
|
+
throw withOperationWarnings(error, result.warnings);
|
|
19534
|
+
}
|
|
19199
19535
|
}
|
|
19200
19536
|
const value = result.output.value;
|
|
19201
|
-
|
|
19202
|
-
|
|
19203
|
-
|
|
19204
|
-
|
|
19205
|
-
|
|
19206
|
-
|
|
19207
|
-
|
|
19208
|
-
|
|
19537
|
+
try {
|
|
19538
|
+
if (options.output) {
|
|
19539
|
+
await writeSecretFile(options.output, value);
|
|
19540
|
+
outputJson({ name, written: options.output });
|
|
19541
|
+
} else if (options.raw || options.valueOnly) {
|
|
19542
|
+
process.stdout.write(value);
|
|
19543
|
+
} else {
|
|
19544
|
+
outputJson({ name, value });
|
|
19545
|
+
}
|
|
19546
|
+
} catch (error) {
|
|
19547
|
+
throw withOperationWarnings(error, result.warnings);
|
|
19209
19548
|
}
|
|
19210
|
-
|
|
19549
|
+
outputWarnings(operationWarnings(result.warnings));
|
|
19211
19550
|
} catch (error) {
|
|
19212
19551
|
handleError(error);
|
|
19213
19552
|
}
|