@tinycloud/operations 0.4.0-beta.11 → 0.4.0-beta.13

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.
@@ -25,6 +25,24 @@ interface OperationContextSummary {
25
25
  readonly ownerDid?: string;
26
26
  readonly space?: string;
27
27
  }
28
+ /**
29
+ * Why a stored grant was not used. A fixed vocabulary: diagnostics never
30
+ * carry exception text, node responses, URLs or other stored transport data.
31
+ * `unbound`: the record has no valid request binding where one is required;
32
+ * `outside_request`: its signed authority exceeds its binding.
33
+ */
34
+ declare const STORED_GRANT_SKIP_REASONS: readonly ["malformed", "unbound", "outside_request", "host_mismatch", "expired", "cid_mismatch", "authority_invalid", "proof_missing", "proof_invalid", "owner_mismatch", "audience_mismatch", "resources_mismatch", "unsupported", "activation_rejected", "invalid"];
35
+ type StoredGrantSkipReason = typeof STORED_GRANT_SKIP_REASONS[number];
36
+ /**
37
+ * A non-fatal diagnostic attached to a result. `STORED_GRANT_SKIPPED`: a
38
+ * stored grant failed validation and gave no authority; `grantCid` is present
39
+ * only when the stored CID is the CID of the stored authorization bytes.
40
+ */
41
+ interface OperationWarning {
42
+ readonly code: "STORED_GRANT_SKIPPED";
43
+ readonly reason: StoredGrantSkipReason;
44
+ readonly grantCid?: string;
45
+ }
28
46
  /**
29
47
  * Capability details are finalized by the authority/artifacts increment. The
30
48
  * kernel intentionally preserves them as canonical operation-owned data.
@@ -58,6 +76,7 @@ interface OperationOkResult<O> {
58
76
  readonly operation: OperationRef;
59
77
  readonly context: OperationContextSummary;
60
78
  readonly output: O;
79
+ readonly warnings?: readonly OperationWarning[];
61
80
  }
62
81
  interface OperationAuthorityRequiredResult {
63
82
  readonly status: "authority_required";
@@ -67,6 +86,7 @@ interface OperationAuthorityRequiredResult {
67
86
  readonly request: PermissionRequestArtifact;
68
87
  readonly approval: ApprovalAction;
69
88
  readonly retry: RetryDescriptor;
89
+ readonly warnings?: readonly OperationWarning[];
70
90
  }
71
91
  interface OperationSetupRequiredResult {
72
92
  readonly status: "setup_required";
@@ -74,12 +94,14 @@ interface OperationSetupRequiredResult {
74
94
  readonly context: OperationContextSummary;
75
95
  readonly setup: SetupAction;
76
96
  readonly retry: RetryDescriptor;
97
+ readonly warnings?: readonly OperationWarning[];
77
98
  }
78
99
  interface OperationErrorResult {
79
100
  readonly status: "error";
80
101
  readonly operation: OperationRef;
81
102
  readonly context: OperationContextSummary;
82
103
  readonly error: OperationError;
104
+ readonly warnings?: readonly OperationWarning[];
83
105
  }
84
106
  type OperationResult<O> = OperationOkResult<O> | OperationAuthorityRequiredResult | OperationSetupRequiredResult | OperationErrorResult;
85
107
  /**
@@ -100,4 +122,4 @@ interface InvocationTarget {
100
122
  readonly privateKey?: string;
101
123
  }
102
124
 
103
- export type { InvocationTarget as I, OperationOperatorType as O, TinyCloudPosture as T, OperationResult as a, OperationContextSummary as b };
125
+ export type { InvocationTarget as I, OperationOperatorType as O, StoredGrantSkipReason as S, TinyCloudPosture as T, OperationResult as a, OperationContextSummary as b };
@@ -25,6 +25,24 @@ interface OperationContextSummary {
25
25
  readonly ownerDid?: string;
26
26
  readonly space?: string;
27
27
  }
28
+ /**
29
+ * Why a stored grant was not used. A fixed vocabulary: diagnostics never
30
+ * carry exception text, node responses, URLs or other stored transport data.
31
+ * `unbound`: the record has no valid request binding where one is required;
32
+ * `outside_request`: its signed authority exceeds its binding.
33
+ */
34
+ declare const STORED_GRANT_SKIP_REASONS: readonly ["malformed", "unbound", "outside_request", "host_mismatch", "expired", "cid_mismatch", "authority_invalid", "proof_missing", "proof_invalid", "owner_mismatch", "audience_mismatch", "resources_mismatch", "unsupported", "activation_rejected", "invalid"];
35
+ type StoredGrantSkipReason = typeof STORED_GRANT_SKIP_REASONS[number];
36
+ /**
37
+ * A non-fatal diagnostic attached to a result. `STORED_GRANT_SKIPPED`: a
38
+ * stored grant failed validation and gave no authority; `grantCid` is present
39
+ * only when the stored CID is the CID of the stored authorization bytes.
40
+ */
41
+ interface OperationWarning {
42
+ readonly code: "STORED_GRANT_SKIPPED";
43
+ readonly reason: StoredGrantSkipReason;
44
+ readonly grantCid?: string;
45
+ }
28
46
  /**
29
47
  * Capability details are finalized by the authority/artifacts increment. The
30
48
  * kernel intentionally preserves them as canonical operation-owned data.
@@ -58,6 +76,7 @@ interface OperationOkResult<O> {
58
76
  readonly operation: OperationRef;
59
77
  readonly context: OperationContextSummary;
60
78
  readonly output: O;
79
+ readonly warnings?: readonly OperationWarning[];
61
80
  }
62
81
  interface OperationAuthorityRequiredResult {
63
82
  readonly status: "authority_required";
@@ -67,6 +86,7 @@ interface OperationAuthorityRequiredResult {
67
86
  readonly request: PermissionRequestArtifact;
68
87
  readonly approval: ApprovalAction;
69
88
  readonly retry: RetryDescriptor;
89
+ readonly warnings?: readonly OperationWarning[];
70
90
  }
71
91
  interface OperationSetupRequiredResult {
72
92
  readonly status: "setup_required";
@@ -74,12 +94,14 @@ interface OperationSetupRequiredResult {
74
94
  readonly context: OperationContextSummary;
75
95
  readonly setup: SetupAction;
76
96
  readonly retry: RetryDescriptor;
97
+ readonly warnings?: readonly OperationWarning[];
77
98
  }
78
99
  interface OperationErrorResult {
79
100
  readonly status: "error";
80
101
  readonly operation: OperationRef;
81
102
  readonly context: OperationContextSummary;
82
103
  readonly error: OperationError;
104
+ readonly warnings?: readonly OperationWarning[];
83
105
  }
84
106
  type OperationResult<O> = OperationOkResult<O> | OperationAuthorityRequiredResult | OperationSetupRequiredResult | OperationErrorResult;
85
107
  /**
@@ -100,4 +122,4 @@ interface InvocationTarget {
100
122
  readonly privateKey?: string;
101
123
  }
102
124
 
103
- export type { InvocationTarget as I, OperationOperatorType as O, TinyCloudPosture as T, OperationResult as a, OperationContextSummary as b };
125
+ export type { InvocationTarget as I, OperationOperatorType as O, StoredGrantSkipReason as S, TinyCloudPosture as T, OperationResult as a, OperationContextSummary as b };
@@ -152,6 +152,7 @@ var delegation_binding_exports = {};
152
152
  __export(delegation_binding_exports, {
153
153
  BINDING_NOTES: () => BINDING_NOTES,
154
154
  activateUnboundCompactImport: () => activateUnboundCompactImport,
155
+ bindCliGrant: () => bindCliGrant,
155
156
  bindingMigrationPath: () => bindingMigrationPath,
156
157
  bindingMigrationRecorded: () => bindingMigrationRecorded,
157
158
  mergeDelegationsWithoutRequest: () => mergeDelegationsWithoutRequest,
@@ -277,8 +278,17 @@ function canonicalizeRecapCaveats(caveats) {
277
278
  }
278
279
  return `[${branches.join(",")}]`;
279
280
  }
281
+ var EXPLICIT_ONLY_ACTIONS = {
282
+ "tinycloud.kv/sync": true,
283
+ "tinycloud.kv/retain": true
284
+ };
285
+ function isExplicitOnlyAction(action) {
286
+ return Object.prototype.hasOwnProperty.call(EXPLICIT_ONLY_ACTIONS, action);
287
+ }
280
288
  function actionContains(grantedAction, requestedAction) {
281
- if (grantedAction === "*" || grantedAction === requestedAction) return true;
289
+ if (grantedAction === requestedAction) return true;
290
+ if (isExplicitOnlyAction(requestedAction)) return false;
291
+ if (grantedAction === "*") return true;
282
292
  if (!grantedAction.endsWith("/*")) return false;
283
293
  const service = grantedAction.slice(0, -2);
284
294
  return requestedAction === service || requestedAction.startsWith(`${service}/`);
@@ -1144,6 +1154,25 @@ var DelegationImportArtifactSchema = import_zod.z.object({
1144
1154
  }).strict();
1145
1155
  var DelegationRequestBindingSchema = PermissionRequestArtifactSchema.pick({ requestId: true, requested: true }).strict();
1146
1156
 
1157
+ // src/contract.ts
1158
+ var STORED_GRANT_SKIP_REASONS = [
1159
+ "malformed",
1160
+ "unbound",
1161
+ "outside_request",
1162
+ "host_mismatch",
1163
+ "expired",
1164
+ "cid_mismatch",
1165
+ "authority_invalid",
1166
+ "proof_missing",
1167
+ "proof_invalid",
1168
+ "owner_mismatch",
1169
+ "audience_mismatch",
1170
+ "resources_mismatch",
1171
+ "unsupported",
1172
+ "activation_rejected",
1173
+ "invalid"
1174
+ ];
1175
+
1147
1176
  // src/secrets.ts
1148
1177
  var nodeSdk = __toESM(require("@tinycloud/node-sdk"), 1);
1149
1178
 
@@ -1224,11 +1253,13 @@ function invalidSecretInput() {
1224
1253
  // src/delegation-binding.ts
1225
1254
  var REQUEST_ID_PREFIX = {
1226
1255
  migration: "migrated",
1227
- "unbound-import": "unbound-import"
1256
+ "unbound-import": "unbound-import",
1257
+ "cli-grant": "cli-grant"
1228
1258
  };
1229
1259
  var BINDING_NOTES = {
1230
1260
  migration: "Stored without a request binding before bindings were required. Bound at migration to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them.",
1231
- "unbound-import": "Imported by `tc auth import` without a stored request. Bound to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them."
1261
+ "unbound-import": "Imported by `tc auth import` without a stored request. Bound to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them.",
1262
+ "cli-grant": "Granted to this CLI by `tc auth request --grant` or a permission escalation, and checked against the request it answered. Bound to exactly its own signed capabilities, so replay refuses any delegation in this record that exceeds them."
1232
1263
  };
1233
1264
  var MIGRATION_LOCK_TIMEOUT_MS = 250;
1234
1265
  function bindingMigrationPath(profile) {
@@ -1253,19 +1284,26 @@ function replayLimit(entry, migrated) {
1253
1284
  if (binding.success) return binding.data.requested;
1254
1285
  return !migrated && kind === "compact" && !("authorityRequest" in entry) ? "signed" : void 0;
1255
1286
  }
1256
- function bindToSignedCapabilities(record, signed, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1287
+ function bindToRequest(record, requested, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1257
1288
  return {
1258
1289
  ...record,
1259
1290
  authorityRequest: DelegationRequestBindingSchema.parse({
1260
1291
  requestId: `${REQUEST_ID_PREFIX[source]}:${record.delegation.cid}`,
1261
- requested: structuredClone(signed)
1292
+ requested: structuredClone(requested)
1262
1293
  }),
1263
1294
  authorityRequestAudit: { source, recordedAt, note: BINDING_NOTES[source] }
1264
1295
  };
1265
1296
  }
1297
+ async function bindCliGrant(node, record, host) {
1298
+ const signed = await signedCapabilities(node, record.delegation, host);
1299
+ if (signed === void 0) {
1300
+ throw new Error("The grant's signed authority could not be validated for this session.");
1301
+ }
1302
+ return bindToRequest(record, signed, "cli-grant");
1303
+ }
1266
1304
  async function activateUnboundCompactImport(node, delegation, host) {
1267
1305
  const activated = await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host });
1268
- return bindToSignedCapabilities(
1306
+ return bindToRequest(
1269
1307
  // The record keeps the delegation as imported; replay validates it again.
1270
1308
  { delegation, permissions: [...activated.effectivePermissions] },
1271
1309
  activated.effectivePermissions,
@@ -1334,23 +1372,41 @@ async function migratedRecord(node, entry, host, recordedAt) {
1334
1372
  const signed = await signedCapabilities(node, delegation, host);
1335
1373
  if (signed === void 0) return void 0;
1336
1374
  try {
1337
- return bindToSignedCapabilities({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
1375
+ return bindToRequest({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
1338
1376
  } catch {
1339
1377
  return void 0;
1340
1378
  }
1341
1379
  }
1380
+ var NOT_REPLAYED = { status: "not-replayed" };
1381
+ function skipped(reason) {
1382
+ return { status: "skipped", reason };
1383
+ }
1342
1384
  async function replayStoredDelegation(node, entry, options) {
1385
+ const kind = storedDelegationKind(entry);
1343
1386
  const delegation = normalizeStoredDelegation(entry);
1344
- if (delegation === void 0 || delegation.expiry.getTime() <= Date.now()) return void 0;
1387
+ if (kind === "refused" || delegation === void 0) return skipped("malformed");
1388
+ if (delegation.expiry.getTime() <= Date.now()) return NOT_REPLAYED;
1389
+ if (kind === "other") return skipped("proof_missing");
1345
1390
  const limit = replayLimit(entry, options.migrated);
1346
- if (limit === void 0) return void 0;
1347
- const authorize = limit === "signed" ? () => true : (effective) => delegationWithinRequest(limit, effective, options.resolveSpace);
1391
+ if (limit === void 0) return skipped("unbound");
1392
+ let outsideRequest = false;
1393
+ const authorize = limit === "signed" ? () => true : (effective) => {
1394
+ outsideRequest = !delegationWithinRequest(limit, effective, options.resolveSpace);
1395
+ return !outsideRequest;
1396
+ };
1348
1397
  try {
1349
- return await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize });
1350
- } catch {
1351
- return void 0;
1398
+ return {
1399
+ status: "installed",
1400
+ delegation: await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize })
1401
+ };
1402
+ } catch (error) {
1403
+ return skipped(outsideRequest ? "outside_request" : rejectionReason(error));
1352
1404
  }
1353
1405
  }
1406
+ function rejectionReason(error) {
1407
+ const reason = isRecord(error) ? error.reason : void 0;
1408
+ return typeof reason === "string" && STORED_GRANT_SKIP_REASONS.includes(reason) ? reason : "invalid";
1409
+ }
1354
1410
  async function bindingMigrationRecorded(profile) {
1355
1411
  try {
1356
1412
  return await readJson(bindingMigrationPath(profile)) !== null;
@@ -1399,6 +1455,7 @@ function isRecord(value) {
1399
1455
  0 && (module.exports = {
1400
1456
  BINDING_NOTES,
1401
1457
  activateUnboundCompactImport,
1458
+ bindCliGrant,
1402
1459
  bindingMigrationPath,
1403
1460
  bindingMigrationRecorded,
1404
1461
  mergeDelegationsWithoutRequest,