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

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.
@@ -1,5 +1,6 @@
1
- import { RuntimeDelegationActivator, PortableDelegation, PermissionEntry, ValidatedRuntimeDelegation } from '@tinycloud/node-sdk';
1
+ import { ValidatedRuntimeDelegation, RuntimeDelegationActivator, PortableDelegation, PermissionEntry } from '@tinycloud/node-sdk';
2
2
  import { O as OperationSpaceResolver } from './authority-ofwbEqmj.cjs';
3
+ import { S as StoredGrantSkipReason } from './contract-BRwWJpPs.cjs';
3
4
  import '@tinycloud/sdk-core';
4
5
 
5
6
  /** Creates the one owner-aware resolver shared by operation matching paths. */
@@ -19,16 +20,20 @@ declare function operationSpaceResolver(node: unknown, ownerSpace?: string): (sp
19
20
  * own signed capabilities (`unbound-import:<cid>`).
20
21
  * - The one-time migration of records stored before bindings existed: exactly
21
22
  * each record's own signed capabilities (`migrated:<cid>`).
23
+ * - The CLI's own grants (`tc auth request --grant`, permission escalation),
24
+ * once checked against the request they answer: exactly each grant's own
25
+ * signed capabilities (`cli-grant:<cid>`). Their signed-login records carry
26
+ * the `siweProof` replay verifies them from.
22
27
  *
23
- * Records the CLI stores from its own signed-login grants carry no
24
- * `siweProof`, cannot be verified here, and keep their legacy CLI replay.
28
+ * Signed-login records the CLI stored before it kept a `siweProof` cannot be
29
+ * verified here and keep their legacy CLI replay.
25
30
  *
26
31
  * The binding is local profile data. It stops records written by any other
27
32
  * path from granting authority. It does not stop someone who can write the
28
33
  * profile directory, who already holds the session key and the signed bytes.
29
34
  */
30
- type BindingSource = "migration" | "unbound-import";
31
- /** The audit note written beside a binding synthesized from a record's own signed capabilities. */
35
+ type BindingSource = "migration" | "unbound-import" | "cli-grant";
36
+ /** The audit note written beside a binding the record's writer synthesized. */
32
37
  declare const BINDING_NOTES: Record<BindingSource, string>;
33
38
  /** The per-profile marker: present once that profile's unbound records were migrated. */
34
39
  declare function bindingMigrationPath(profile: string): string;
@@ -38,9 +43,9 @@ declare function bindingMigrationPath(profile: string): string;
38
43
  * - `compact` and `signed-login` records go through validated activation and
39
44
  * the binding rule.
40
45
  * - `other` records keep the CLI's legacy replay: an unbound record whose
41
- * bytes are not compact and that has no `siweProof`, which is how the CLI
42
- * stores its own signed-login grants. The operations runtime never installs
43
- * them.
46
+ * bytes are not compact and that has no `siweProof`, which is how earlier
47
+ * CLI releases stored their own signed-login grants. The operations runtime
48
+ * never installs them.
44
49
  * - `refused` records install nothing anywhere.
45
50
  *
46
51
  * A record is `refused` unless its `delegationHeader` has exactly one own
@@ -61,6 +66,16 @@ declare function storedDelegationKind(entry: unknown): "compact" | "signed-login
61
66
  * when the record installs nothing.
62
67
  */
63
68
  declare function replayLimit(entry: unknown, migrated: boolean): readonly PermissionEntry[] | "signed" | undefined;
69
+ /**
70
+ * Binds the record of one of the CLI's own grants, already checked against
71
+ * the request it answers, to exactly the capabilities its signed bytes grant.
72
+ * They are read through the validated activation replay uses (nothing is
73
+ * activated), so the binding is the authority replay will derive. Throws when
74
+ * validated activation cannot read them for `node`'s session.
75
+ */
76
+ declare function bindCliGrant<T extends {
77
+ readonly delegation: PortableDelegation;
78
+ }>(node: RuntimeDelegationActivator, record: T, host: string): Promise<T & Record<"authorityRequest" | "authorityRequestAudit", unknown>>;
64
79
  /**
65
80
  * Validates and activates a compact delegation that `tc auth import` received
66
81
  * without a stored request, through the same validated activation replay uses,
@@ -110,21 +125,36 @@ declare function prepareStoredDelegationReplay(profile: string, node: RuntimeDel
110
125
  readonly host: string;
111
126
  readonly migrate: boolean;
112
127
  }): Promise<boolean>;
128
+ /**
129
+ * What replay did with one stored record. `skipped` records grant nothing and
130
+ * carry a fixed reason code that is safe to publish; `not-replayed` records
131
+ * are routine (expired) and give nothing to act on.
132
+ */
133
+ type StoredDelegationReplay = {
134
+ readonly status: "installed";
135
+ readonly delegation: ValidatedRuntimeDelegation;
136
+ } | {
137
+ readonly status: "skipped";
138
+ readonly reason: StoredGrantSkipReason;
139
+ } | {
140
+ readonly status: "not-replayed";
141
+ };
113
142
  /**
114
143
  * Replays one stored record under the binding rule. A record with a valid
115
144
  * binding installs only if its signed capabilities fit inside it. Before the
116
145
  * profile has migrated, an unbound compact record installs with its signed
117
146
  * authority as it always did; after, it installs nothing. A signed-login
118
- * record installs only with a binding. Returns the installed delegation, or
119
- * `undefined` when the record installs nothing. Never throws: stored data is
120
- * untrusted transport material.
147
+ * record installs only with a binding. A record with neither compact bytes
148
+ * nor a `siweProof` cannot be verified here and installs nothing. Never
149
+ * throws, and never reports exception text: stored data is untrusted
150
+ * transport material, and messages can quote it or a node's response.
121
151
  */
122
152
  declare function replayStoredDelegation(node: RuntimeDelegationActivator, entry: unknown, options: {
123
153
  readonly host: string;
124
154
  readonly migrated: boolean;
125
155
  readonly resolveSpace: OperationSpaceResolver;
126
- }): Promise<ValidatedRuntimeDelegation | undefined>;
156
+ }): Promise<StoredDelegationReplay>;
127
157
  /** An unreadable marker counts as migrated: a damaged marker never reopens migration. */
128
158
  declare function bindingMigrationRecorded(profile: string): Promise<boolean>;
129
159
 
130
- export { BINDING_NOTES, type BindingSource, activateUnboundCompactImport, bindingMigrationPath, bindingMigrationRecorded, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayStoredDelegation, storedDelegationKind };
160
+ export { BINDING_NOTES, type BindingSource, type StoredDelegationReplay, activateUnboundCompactImport, bindCliGrant, bindingMigrationPath, bindingMigrationRecorded, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayStoredDelegation, storedDelegationKind };
@@ -1,5 +1,6 @@
1
- import { RuntimeDelegationActivator, PortableDelegation, PermissionEntry, ValidatedRuntimeDelegation } from '@tinycloud/node-sdk';
1
+ import { ValidatedRuntimeDelegation, RuntimeDelegationActivator, PortableDelegation, PermissionEntry } from '@tinycloud/node-sdk';
2
2
  import { O as OperationSpaceResolver } from './authority-ofwbEqmj.js';
3
+ import { S as StoredGrantSkipReason } from './contract-BRwWJpPs.js';
3
4
  import '@tinycloud/sdk-core';
4
5
 
5
6
  /** Creates the one owner-aware resolver shared by operation matching paths. */
@@ -19,16 +20,20 @@ declare function operationSpaceResolver(node: unknown, ownerSpace?: string): (sp
19
20
  * own signed capabilities (`unbound-import:<cid>`).
20
21
  * - The one-time migration of records stored before bindings existed: exactly
21
22
  * each record's own signed capabilities (`migrated:<cid>`).
23
+ * - The CLI's own grants (`tc auth request --grant`, permission escalation),
24
+ * once checked against the request they answer: exactly each grant's own
25
+ * signed capabilities (`cli-grant:<cid>`). Their signed-login records carry
26
+ * the `siweProof` replay verifies them from.
22
27
  *
23
- * Records the CLI stores from its own signed-login grants carry no
24
- * `siweProof`, cannot be verified here, and keep their legacy CLI replay.
28
+ * Signed-login records the CLI stored before it kept a `siweProof` cannot be
29
+ * verified here and keep their legacy CLI replay.
25
30
  *
26
31
  * The binding is local profile data. It stops records written by any other
27
32
  * path from granting authority. It does not stop someone who can write the
28
33
  * profile directory, who already holds the session key and the signed bytes.
29
34
  */
30
- type BindingSource = "migration" | "unbound-import";
31
- /** The audit note written beside a binding synthesized from a record's own signed capabilities. */
35
+ type BindingSource = "migration" | "unbound-import" | "cli-grant";
36
+ /** The audit note written beside a binding the record's writer synthesized. */
32
37
  declare const BINDING_NOTES: Record<BindingSource, string>;
33
38
  /** The per-profile marker: present once that profile's unbound records were migrated. */
34
39
  declare function bindingMigrationPath(profile: string): string;
@@ -38,9 +43,9 @@ declare function bindingMigrationPath(profile: string): string;
38
43
  * - `compact` and `signed-login` records go through validated activation and
39
44
  * the binding rule.
40
45
  * - `other` records keep the CLI's legacy replay: an unbound record whose
41
- * bytes are not compact and that has no `siweProof`, which is how the CLI
42
- * stores its own signed-login grants. The operations runtime never installs
43
- * them.
46
+ * bytes are not compact and that has no `siweProof`, which is how earlier
47
+ * CLI releases stored their own signed-login grants. The operations runtime
48
+ * never installs them.
44
49
  * - `refused` records install nothing anywhere.
45
50
  *
46
51
  * A record is `refused` unless its `delegationHeader` has exactly one own
@@ -61,6 +66,16 @@ declare function storedDelegationKind(entry: unknown): "compact" | "signed-login
61
66
  * when the record installs nothing.
62
67
  */
63
68
  declare function replayLimit(entry: unknown, migrated: boolean): readonly PermissionEntry[] | "signed" | undefined;
69
+ /**
70
+ * Binds the record of one of the CLI's own grants, already checked against
71
+ * the request it answers, to exactly the capabilities its signed bytes grant.
72
+ * They are read through the validated activation replay uses (nothing is
73
+ * activated), so the binding is the authority replay will derive. Throws when
74
+ * validated activation cannot read them for `node`'s session.
75
+ */
76
+ declare function bindCliGrant<T extends {
77
+ readonly delegation: PortableDelegation;
78
+ }>(node: RuntimeDelegationActivator, record: T, host: string): Promise<T & Record<"authorityRequest" | "authorityRequestAudit", unknown>>;
64
79
  /**
65
80
  * Validates and activates a compact delegation that `tc auth import` received
66
81
  * without a stored request, through the same validated activation replay uses,
@@ -110,21 +125,36 @@ declare function prepareStoredDelegationReplay(profile: string, node: RuntimeDel
110
125
  readonly host: string;
111
126
  readonly migrate: boolean;
112
127
  }): Promise<boolean>;
128
+ /**
129
+ * What replay did with one stored record. `skipped` records grant nothing and
130
+ * carry a fixed reason code that is safe to publish; `not-replayed` records
131
+ * are routine (expired) and give nothing to act on.
132
+ */
133
+ type StoredDelegationReplay = {
134
+ readonly status: "installed";
135
+ readonly delegation: ValidatedRuntimeDelegation;
136
+ } | {
137
+ readonly status: "skipped";
138
+ readonly reason: StoredGrantSkipReason;
139
+ } | {
140
+ readonly status: "not-replayed";
141
+ };
113
142
  /**
114
143
  * Replays one stored record under the binding rule. A record with a valid
115
144
  * binding installs only if its signed capabilities fit inside it. Before the
116
145
  * profile has migrated, an unbound compact record installs with its signed
117
146
  * authority as it always did; after, it installs nothing. A signed-login
118
- * record installs only with a binding. Returns the installed delegation, or
119
- * `undefined` when the record installs nothing. Never throws: stored data is
120
- * untrusted transport material.
147
+ * record installs only with a binding. A record with neither compact bytes
148
+ * nor a `siweProof` cannot be verified here and installs nothing. Never
149
+ * throws, and never reports exception text: stored data is untrusted
150
+ * transport material, and messages can quote it or a node's response.
121
151
  */
122
152
  declare function replayStoredDelegation(node: RuntimeDelegationActivator, entry: unknown, options: {
123
153
  readonly host: string;
124
154
  readonly migrated: boolean;
125
155
  readonly resolveSpace: OperationSpaceResolver;
126
- }): Promise<ValidatedRuntimeDelegation | undefined>;
156
+ }): Promise<StoredDelegationReplay>;
127
157
  /** An unreadable marker counts as migrated: a damaged marker never reopens migration. */
128
158
  declare function bindingMigrationRecorded(profile: string): Promise<boolean>;
129
159
 
130
- export { BINDING_NOTES, type BindingSource, activateUnboundCompactImport, bindingMigrationPath, bindingMigrationRecorded, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayStoredDelegation, storedDelegationKind };
160
+ export { BINDING_NOTES, type BindingSource, type StoredDelegationReplay, activateUnboundCompactImport, bindCliGrant, bindingMigrationPath, bindingMigrationRecorded, mergeDelegationsWithoutRequest, operationSpaceResolver, prepareStoredDelegationReplay, replayLimit, replayStoredDelegation, storedDelegationKind };
@@ -1137,6 +1137,25 @@ var DelegationImportArtifactSchema = z.object({
1137
1137
  }).strict();
1138
1138
  var DelegationRequestBindingSchema = PermissionRequestArtifactSchema.pick({ requestId: true, requested: true }).strict();
1139
1139
 
1140
+ // src/contract.ts
1141
+ var STORED_GRANT_SKIP_REASONS = [
1142
+ "malformed",
1143
+ "unbound",
1144
+ "outside_request",
1145
+ "host_mismatch",
1146
+ "expired",
1147
+ "cid_mismatch",
1148
+ "authority_invalid",
1149
+ "proof_missing",
1150
+ "proof_invalid",
1151
+ "owner_mismatch",
1152
+ "audience_mismatch",
1153
+ "resources_mismatch",
1154
+ "unsupported",
1155
+ "activation_rejected",
1156
+ "invalid"
1157
+ ];
1158
+
1140
1159
  // src/secrets.ts
1141
1160
  import * as nodeSdk from "@tinycloud/node-sdk";
1142
1161
 
@@ -1217,11 +1236,13 @@ function invalidSecretInput() {
1217
1236
  // src/delegation-binding.ts
1218
1237
  var REQUEST_ID_PREFIX = {
1219
1238
  migration: "migrated",
1220
- "unbound-import": "unbound-import"
1239
+ "unbound-import": "unbound-import",
1240
+ "cli-grant": "cli-grant"
1221
1241
  };
1222
1242
  var BINDING_NOTES = {
1223
1243
  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.",
1224
- "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."
1244
+ "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.",
1245
+ "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."
1225
1246
  };
1226
1247
  var MIGRATION_LOCK_TIMEOUT_MS = 250;
1227
1248
  function bindingMigrationPath(profile) {
@@ -1246,19 +1267,26 @@ function replayLimit(entry, migrated) {
1246
1267
  if (binding.success) return binding.data.requested;
1247
1268
  return !migrated && kind === "compact" && !("authorityRequest" in entry) ? "signed" : void 0;
1248
1269
  }
1249
- function bindToSignedCapabilities(record, signed, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1270
+ function bindToRequest(record, requested, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
1250
1271
  return {
1251
1272
  ...record,
1252
1273
  authorityRequest: DelegationRequestBindingSchema.parse({
1253
1274
  requestId: `${REQUEST_ID_PREFIX[source]}:${record.delegation.cid}`,
1254
- requested: structuredClone(signed)
1275
+ requested: structuredClone(requested)
1255
1276
  }),
1256
1277
  authorityRequestAudit: { source, recordedAt, note: BINDING_NOTES[source] }
1257
1278
  };
1258
1279
  }
1280
+ async function bindCliGrant(node, record, host) {
1281
+ const signed = await signedCapabilities(node, record.delegation, host);
1282
+ if (signed === void 0) {
1283
+ throw new Error("The grant's signed authority could not be validated for this session.");
1284
+ }
1285
+ return bindToRequest(record, signed, "cli-grant");
1286
+ }
1259
1287
  async function activateUnboundCompactImport(node, delegation, host) {
1260
1288
  const activated = await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host });
1261
- return bindToSignedCapabilities(
1289
+ return bindToRequest(
1262
1290
  // The record keeps the delegation as imported; replay validates it again.
1263
1291
  { delegation, permissions: [...activated.effectivePermissions] },
1264
1292
  activated.effectivePermissions,
@@ -1327,23 +1355,41 @@ async function migratedRecord(node, entry, host, recordedAt) {
1327
1355
  const signed = await signedCapabilities(node, delegation, host);
1328
1356
  if (signed === void 0) return void 0;
1329
1357
  try {
1330
- return bindToSignedCapabilities({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
1358
+ return bindToRequest({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
1331
1359
  } catch {
1332
1360
  return void 0;
1333
1361
  }
1334
1362
  }
1363
+ var NOT_REPLAYED = { status: "not-replayed" };
1364
+ function skipped(reason) {
1365
+ return { status: "skipped", reason };
1366
+ }
1335
1367
  async function replayStoredDelegation(node, entry, options) {
1368
+ const kind = storedDelegationKind(entry);
1336
1369
  const delegation = normalizeStoredDelegation(entry);
1337
- if (delegation === void 0 || delegation.expiry.getTime() <= Date.now()) return void 0;
1370
+ if (kind === "refused" || delegation === void 0) return skipped("malformed");
1371
+ if (delegation.expiry.getTime() <= Date.now()) return NOT_REPLAYED;
1372
+ if (kind === "other") return skipped("proof_missing");
1338
1373
  const limit = replayLimit(entry, options.migrated);
1339
- if (limit === void 0) return void 0;
1340
- const authorize = limit === "signed" ? () => true : (effective) => delegationWithinRequest(limit, effective, options.resolveSpace);
1374
+ if (limit === void 0) return skipped("unbound");
1375
+ let outsideRequest = false;
1376
+ const authorize = limit === "signed" ? () => true : (effective) => {
1377
+ outsideRequest = !delegationWithinRequest(limit, effective, options.resolveSpace);
1378
+ return !outsideRequest;
1379
+ };
1341
1380
  try {
1342
- return await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize });
1343
- } catch {
1344
- return void 0;
1381
+ return {
1382
+ status: "installed",
1383
+ delegation: await nodeSdk2.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize })
1384
+ };
1385
+ } catch (error) {
1386
+ return skipped(outsideRequest ? "outside_request" : rejectionReason(error));
1345
1387
  }
1346
1388
  }
1389
+ function rejectionReason(error) {
1390
+ const reason = isRecord(error) ? error.reason : void 0;
1391
+ return typeof reason === "string" && STORED_GRANT_SKIP_REASONS.includes(reason) ? reason : "invalid";
1392
+ }
1347
1393
  async function bindingMigrationRecorded(profile) {
1348
1394
  try {
1349
1395
  return await readJson(bindingMigrationPath(profile)) !== null;
@@ -1391,6 +1437,7 @@ function isRecord(value) {
1391
1437
  export {
1392
1438
  BINDING_NOTES,
1393
1439
  activateUnboundCompactImport,
1440
+ bindCliGrant,
1394
1441
  bindingMigrationPath,
1395
1442
  bindingMigrationRecorded,
1396
1443
  mergeDelegationsWithoutRequest,