@tinycloud/operations 0.4.0-beta.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { I as InvocationTarget, a as OperationResult } from './contract-CjuEjE-M.cjs';
1
+ import { I as InvocationTarget, a as OperationResult } from './contract-BRwWJpPs.cjs';
2
2
 
3
3
  /**
4
4
  * The only published CLI acquisition helper. Its fixed operation identity is
@@ -1,4 +1,4 @@
1
- import { I as InvocationTarget, a as OperationResult } from './contract-CjuEjE-M.js';
1
+ import { I as InvocationTarget, a as OperationResult } from './contract-BRwWJpPs.js';
2
2
 
3
3
  /**
4
4
  * The only published CLI acquisition helper. Its fixed operation identity is
@@ -40,6 +40,17 @@ function operationError(code, message, options = {}) {
40
40
  ...options.details === void 0 ? {} : { details: options.details }
41
41
  };
42
42
  }
43
+ function storageFullOperationError(error) {
44
+ if (typeof error !== "object" || error === null) return void 0;
45
+ const { code, meta } = error;
46
+ if (code !== "STORAGE_QUOTA_EXCEEDED" && code !== "STORAGE_LIMIT_REACHED") return void 0;
47
+ const account = typeof meta === "object" && meta !== null ? meta.account : void 0;
48
+ return operationError(
49
+ "STORAGE_QUOTA_EXCEEDED",
50
+ code === "STORAGE_LIMIT_REACHED" ? `This write is larger than the TinyCloud storage left, so nothing was written. Reading still works. ${MANAGE_STORAGE}` : `TinyCloud storage is full, so nothing was written. Reading still works. ${MANAGE_STORAGE}`,
51
+ { retryable: false, ...account === void 0 ? {} : { details: { account } } }
52
+ );
53
+ }
43
54
  function isOperationErrorCode(value) {
44
55
  return typeof value === "string" && OPERATION_ERROR_CODES.includes(value);
45
56
  }
@@ -65,7 +76,7 @@ function sanitizeOperationError(error) {
65
76
  ...candidate.details === void 0 ? {} : { details: candidate.details }
66
77
  });
67
78
  }
68
- var OPERATION_ERROR_CODES, OperationInvocationError;
79
+ var OPERATION_ERROR_CODES, MANAGE_STORAGE, OperationInvocationError;
69
80
  var init_errors = __esm({
70
81
  "src/errors.ts"() {
71
82
  "use strict";
@@ -88,6 +99,7 @@ var init_errors = __esm({
88
99
  "ENCRYPTION_NETWORK_UNRESOLVED",
89
100
  "NODE_UNREACHABLE",
90
101
  "NODE_ERROR",
102
+ "STORAGE_QUOTA_EXCEEDED",
91
103
  "KV_NOT_FOUND",
92
104
  "KV_PRECONDITION_FAILED",
93
105
  "KV_CONFLICT",
@@ -101,6 +113,7 @@ var init_errors = __esm({
101
113
  "OUTPUT_INVALID",
102
114
  "INTERNAL_ERROR"
103
115
  ];
116
+ MANAGE_STORAGE = "Do not retry: the owner must free up space or upgrade at https://account.tinycloud.xyz/billing.";
104
117
  OperationInvocationError = class extends Error {
105
118
  constructor(operationError2) {
106
119
  super(operationError2.message);
@@ -342,8 +355,13 @@ function canonicalizeRecapCaveats(caveats) {
342
355
  }
343
356
  return `[${branches.join(",")}]`;
344
357
  }
358
+ function isExplicitOnlyAction(action) {
359
+ return Object.prototype.hasOwnProperty.call(EXPLICIT_ONLY_ACTIONS, action);
360
+ }
345
361
  function actionContains(grantedAction, requestedAction) {
346
- if (grantedAction === "*" || grantedAction === requestedAction) return true;
362
+ if (grantedAction === requestedAction) return true;
363
+ if (isExplicitOnlyAction(requestedAction)) return false;
364
+ if (grantedAction === "*") return true;
347
365
  if (!grantedAction.endsWith("/*")) return false;
348
366
  const service = grantedAction.slice(0, -2);
349
367
  return requestedAction === service || requestedAction.startsWith(`${service}/`);
@@ -368,10 +386,15 @@ function cloneEntry(entry) {
368
386
  ...entry.expiry !== void 0 ? { expiry: entry.expiry } : {}
369
387
  };
370
388
  }
389
+ var EXPLICIT_ONLY_ACTIONS;
371
390
  var init_capabilities = __esm({
372
391
  "../sdk-core/src/capabilities.ts"() {
373
392
  "use strict";
374
393
  init_manifest();
394
+ EXPLICIT_ONLY_ACTIONS = {
395
+ "tinycloud.kv/sync": true,
396
+ "tinycloud.kv/retain": true
397
+ };
375
398
  }
376
399
  });
377
400
 
@@ -714,7 +737,8 @@ async function acquireProfileLock(profile, options) {
714
737
  const lockPath = profileLockPath(profile);
715
738
  const turnDirectory = profileTurnLockPath(profile);
716
739
  const directory = profilePath(profile);
717
- await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
740
+ if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
741
+ if (!options.requireProfile) await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
718
742
  await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
719
743
  for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
720
744
  await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
@@ -723,11 +747,15 @@ async function acquireProfileLock(profile, options) {
723
747
  }
724
748
  const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
725
749
  try {
750
+ if (options.requireProfile && !await exists(profileConfigPath(profile))) {
751
+ await rmdir(directory).catch(() => void 0);
752
+ throw new ProfileDeletedError(profile);
753
+ }
726
754
  const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
727
755
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
728
756
  while (true) {
729
757
  const token = randomUUID();
730
- if (await publishProfileLock(profile, lockPath, token)) {
758
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
731
759
  return {
732
760
  deletedWhileWaiting,
733
761
  turnSlot: turn.slot,
@@ -972,12 +1000,13 @@ async function collectTurns(profile, turn) {
972
1000
  }
973
1001
  }
974
1002
  }
975
- async function publishProfileLock(profile, lockPath, token) {
1003
+ async function publishProfileLock(profile, lockPath, token, requireProfile) {
976
1004
  try {
977
1005
  await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
978
1006
  } catch (error) {
979
1007
  if (isErrno(error, "EEXIST")) return false;
980
1008
  if (!isErrno(error, "ENOENT")) throw error;
1009
+ if (requireProfile) throw new ProfileDeletedError(profile);
981
1010
  await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
982
1011
  return false;
983
1012
  }
@@ -2545,6 +2574,8 @@ function kvFailure(error, action) {
2545
2574
  if (error.code === "KV_RESPONSE_TOO_LARGE") {
2546
2575
  return { status: "error", error: operationError("KV_RESPONSE_TOO_LARGE", `The TinyCloud KV value exceeds ${MAX_KV_VALUE_BYTES} bytes.`) };
2547
2576
  }
2577
+ const storageFull = storageFullOperationError(error);
2578
+ if (storageFull) return { status: "error", error: storageFull };
2548
2579
  return nodeFailure(action);
2549
2580
  }
2550
2581
  function nodeFailure(action) {
@@ -3163,7 +3194,7 @@ async function executeSqlDml(context, input) {
3163
3194
  const runtime = runtimeContext2(context);
3164
3195
  const space = resolveSqlSpace(runtime, input.space);
3165
3196
  const result = await runtimeNode2(runtime).sqlForSpace(space).db(input.database).execute(input.sql, input.params.map(decodeSqlInputValue));
3166
- if (!result.ok) return authorizationFailure(result.error, "execute the SQLite statement") ?? sqlMutationFailure();
3197
+ if (!result.ok) return authorizationFailure(result.error, "execute the SQLite statement") ?? sqlMutationFailure(result.error);
3167
3198
  const normalized = normalizeExecuteResult(result.data);
3168
3199
  return {
3169
3200
  status: "ok",
@@ -3177,7 +3208,7 @@ async function executeSqlDml(context, input) {
3177
3208
  };
3178
3209
  } catch (error) {
3179
3210
  if (error instanceof OperationInvocationError) return { status: "error", error: error.operationError };
3180
- return authorizationFailure(error, "execute the SQLite statement") ?? sqlMutationFailure();
3211
+ return authorizationFailure(error, "execute the SQLite statement") ?? sqlMutationFailure(error);
3181
3212
  }
3182
3213
  }
3183
3214
  function isSingleReadQuery(sql) {
@@ -3354,7 +3385,9 @@ function sqlServiceFailure(error, action) {
3354
3385
  }
3355
3386
  return nodeFailure2(action);
3356
3387
  }
3357
- function sqlMutationFailure() {
3388
+ function sqlMutationFailure(error) {
3389
+ const storageFull = storageFullOperationError(error);
3390
+ if (storageFull) return { status: "error", error: storageFull };
3358
3391
  return {
3359
3392
  status: "error",
3360
3393
  error: operationError(
@@ -3609,6 +3642,32 @@ ORDER BY type, name`;
3609
3642
  }
3610
3643
  });
3611
3644
 
3645
+ // src/contract.ts
3646
+ var STORED_GRANT_SKIP_REASONS, DELEGATION_CID_PATTERN;
3647
+ var init_contract = __esm({
3648
+ "src/contract.ts"() {
3649
+ "use strict";
3650
+ STORED_GRANT_SKIP_REASONS = [
3651
+ "malformed",
3652
+ "unbound",
3653
+ "outside_request",
3654
+ "host_mismatch",
3655
+ "expired",
3656
+ "cid_mismatch",
3657
+ "authority_invalid",
3658
+ "proof_missing",
3659
+ "proof_invalid",
3660
+ "owner_mismatch",
3661
+ "audience_mismatch",
3662
+ "resources_mismatch",
3663
+ "unsupported",
3664
+ "activation_rejected",
3665
+ "invalid"
3666
+ ];
3667
+ DELEGATION_CID_PATTERN = "^bafkr4i[a-z2-7]{52}$";
3668
+ }
3669
+ });
3670
+
3612
3671
  // src/delegation-binding.ts
3613
3672
  import { join as join2 } from "path";
3614
3673
  import * as nodeSdk3 from "@tinycloud/node-sdk";
@@ -3634,12 +3693,12 @@ function replayLimit(entry, migrated) {
3634
3693
  if (binding.success) return binding.data.requested;
3635
3694
  return !migrated && kind === "compact" && !("authorityRequest" in entry) ? "signed" : void 0;
3636
3695
  }
3637
- function bindToSignedCapabilities(record, signed, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3696
+ function bindToRequest(record, requested, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3638
3697
  return {
3639
3698
  ...record,
3640
3699
  authorityRequest: DelegationRequestBindingSchema.parse({
3641
3700
  requestId: `${REQUEST_ID_PREFIX[source]}:${record.delegation.cid}`,
3642
- requested: structuredClone(signed)
3701
+ requested: structuredClone(requested)
3643
3702
  }),
3644
3703
  authorityRequestAudit: { source, recordedAt, note: BINDING_NOTES[source] }
3645
3704
  };
@@ -3695,23 +3754,40 @@ async function migratedRecord(node, entry, host, recordedAt) {
3695
3754
  const signed = await signedCapabilities(node, delegation, host);
3696
3755
  if (signed === void 0) return void 0;
3697
3756
  try {
3698
- return bindToSignedCapabilities({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
3757
+ return bindToRequest({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
3699
3758
  } catch {
3700
3759
  return void 0;
3701
3760
  }
3702
3761
  }
3762
+ function skipped(reason) {
3763
+ return { status: "skipped", reason };
3764
+ }
3703
3765
  async function replayStoredDelegation(node, entry, options) {
3766
+ const kind = storedDelegationKind(entry);
3704
3767
  const delegation = normalizeStoredDelegation(entry);
3705
- if (delegation === void 0 || delegation.expiry.getTime() <= Date.now()) return void 0;
3768
+ if (kind === "refused" || delegation === void 0) return skipped("malformed");
3769
+ if (delegation.expiry.getTime() <= Date.now()) return NOT_REPLAYED;
3770
+ if (kind === "other") return skipped("proof_missing");
3706
3771
  const limit = replayLimit(entry, options.migrated);
3707
- if (limit === void 0) return void 0;
3708
- const authorize = limit === "signed" ? () => true : (effective) => delegationWithinRequest(limit, effective, options.resolveSpace);
3772
+ if (limit === void 0) return skipped("unbound");
3773
+ let outsideRequest = false;
3774
+ const authorize = limit === "signed" ? () => true : (effective) => {
3775
+ outsideRequest = !delegationWithinRequest(limit, effective, options.resolveSpace);
3776
+ return !outsideRequest;
3777
+ };
3709
3778
  try {
3710
- return await nodeSdk3.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize });
3711
- } catch {
3712
- return void 0;
3779
+ return {
3780
+ status: "installed",
3781
+ delegation: await nodeSdk3.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize })
3782
+ };
3783
+ } catch (error) {
3784
+ return skipped(outsideRequest ? "outside_request" : rejectionReason(error));
3713
3785
  }
3714
3786
  }
3787
+ function rejectionReason(error) {
3788
+ const reason = isRecord4(error) ? error.reason : void 0;
3789
+ return typeof reason === "string" && STORED_GRANT_SKIP_REASONS.includes(reason) ? reason : "invalid";
3790
+ }
3715
3791
  async function bindingMigrationRecorded(profile) {
3716
3792
  try {
3717
3793
  return await readJson(bindingMigrationPath(profile)) !== null;
@@ -3756,23 +3832,27 @@ function recordKey(entry) {
3756
3832
  function isRecord4(value) {
3757
3833
  return typeof value === "object" && value !== null && !Array.isArray(value);
3758
3834
  }
3759
- var REQUEST_ID_PREFIX, BINDING_NOTES, MIGRATION_LOCK_TIMEOUT_MS;
3835
+ var REQUEST_ID_PREFIX, BINDING_NOTES, MIGRATION_LOCK_TIMEOUT_MS, NOT_REPLAYED;
3760
3836
  var init_delegation_binding = __esm({
3761
3837
  "src/delegation-binding.ts"() {
3762
3838
  "use strict";
3763
3839
  init_artifacts();
3764
3840
  init_authority();
3841
+ init_contract();
3765
3842
  init_state();
3766
3843
  init_secrets();
3767
3844
  REQUEST_ID_PREFIX = {
3768
3845
  migration: "migrated",
3769
- "unbound-import": "unbound-import"
3846
+ "unbound-import": "unbound-import",
3847
+ "cli-grant": "cli-grant"
3770
3848
  };
3771
3849
  BINDING_NOTES = {
3772
3850
  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.",
3773
- "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."
3851
+ "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.",
3852
+ "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."
3774
3853
  };
3775
3854
  MIGRATION_LOCK_TIMEOUT_MS = 250;
3855
+ NOT_REPLAYED = { status: "not-replayed" };
3776
3856
  }
3777
3857
  });
3778
3858
 
@@ -4185,7 +4265,18 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4185
4265
  ));
4186
4266
  }
4187
4267
  } else {
4188
- await node.restoreSession(activeSession);
4268
+ try {
4269
+ await node.restoreSession(activeSession);
4270
+ } catch (error) {
4271
+ if (isRecord7(error) && error.code === "AUTH_EXPIRED") {
4272
+ return failed(summary, operationError(
4273
+ "AUTH_REQUIRED",
4274
+ "The stored session has expired or is no longer valid. Sign in again.",
4275
+ { retryable: false }
4276
+ ));
4277
+ }
4278
+ throw error;
4279
+ }
4189
4280
  }
4190
4281
  const activeSessionDid = normalizeDid(node.sessionDid);
4191
4282
  const livePrincipalDid = normalizeDid(node.did);
@@ -4193,6 +4284,7 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4193
4284
  const livePermissions = [...node.getVerifiedSessionCapabilities()];
4194
4285
  const resolveSpace2 = operationSpaceResolver(node, authenticatedSpace ?? summary.space);
4195
4286
  const seenCids = /* @__PURE__ */ new Set();
4287
+ const warnings = [];
4196
4288
  if (!explicitPrivateKeyOverride) {
4197
4289
  const activator = node;
4198
4290
  const migrated = await prepareStoredDelegationReplay(profileName, activator, {
@@ -4202,20 +4294,25 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4202
4294
  migrate: activeSession !== void 0
4203
4295
  });
4204
4296
  for (const entry of await readAdditionalDelegations(profileName)) {
4205
- const activated = await replayStoredDelegation(activator, entry, {
4297
+ const replay = await replayStoredDelegation(activator, entry, {
4206
4298
  host: summary.host,
4207
4299
  migrated,
4208
4300
  resolveSpace: resolveSpace2
4209
4301
  });
4210
- if (activated !== void 0 && !seenCids.has(activated.cid)) {
4211
- seenCids.add(activated.cid);
4212
- livePermissions.push(...activated.effectivePermissions);
4302
+ if (replay.status === "installed") {
4303
+ if (!seenCids.has(replay.delegation.cid)) {
4304
+ seenCids.add(replay.delegation.cid);
4305
+ livePermissions.push(...replay.delegation.effectivePermissions);
4306
+ }
4307
+ } else if (replay.status === "skipped") {
4308
+ warnings.push(skippedGrantWarning(node, entry, replay.reason));
4213
4309
  }
4214
4310
  }
4215
4311
  }
4216
4312
  const runtime = {
4217
4313
  node,
4218
- granted: canonicalizeCapabilities(livePermissions)
4314
+ granted: canonicalizeCapabilities(livePermissions),
4315
+ ...warnings.length === 0 ? {} : { warnings }
4219
4316
  };
4220
4317
  return {
4221
4318
  ok: true,
@@ -4239,6 +4336,24 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4239
4336
  ));
4240
4337
  }
4241
4338
  }
4339
+ function skippedGrantWarning(node, entry, reason) {
4340
+ const grantCid = verifiedStoredCid(node, entry);
4341
+ return { code: "STORED_GRANT_SKIPPED", reason, ...grantCid === void 0 ? {} : { grantCid } };
4342
+ }
4343
+ function verifiedStoredCid(node, entry) {
4344
+ const delegation = isRecord7(entry) && isRecord7(entry.delegation) ? entry.delegation : void 0;
4345
+ const header = isRecord7(delegation?.delegationHeader) ? delegation.delegationHeader : void 0;
4346
+ const cid = delegation?.cid;
4347
+ const computeDelegationCid = isRecord7(node) ? node.computeDelegationCid : void 0;
4348
+ if (typeof cid !== "string" || typeof header?.Authorization !== "string" || typeof computeDelegationCid !== "function") {
4349
+ return void 0;
4350
+ }
4351
+ try {
4352
+ return computeDelegationCid.call(node, header.Authorization) === cid && DELEGATION_CID.test(cid) ? cid : void 0;
4353
+ } catch {
4354
+ return void 0;
4355
+ }
4356
+ }
4242
4357
  function spaceForAuthenticatedPrincipal(principal2) {
4243
4358
  if (principal2 === void 0) return void 0;
4244
4359
  try {
@@ -4287,15 +4402,18 @@ function unresolvedSummary(target) {
4287
4402
  posture: "unauthenticated"
4288
4403
  };
4289
4404
  }
4405
+ var DELEGATION_CID;
4290
4406
  var init_runtime = __esm({
4291
4407
  "src/runtime.ts"() {
4292
4408
  "use strict";
4409
+ init_contract();
4293
4410
  init_authority();
4294
4411
  init_errors();
4295
4412
  init_profile();
4296
4413
  init_secrets();
4297
4414
  init_state();
4298
4415
  init_delegation_binding();
4416
+ DELEGATION_CID = new RegExp(DELEGATION_CID_PATTERN);
4299
4417
  }
4300
4418
  });
4301
4419
 
@@ -4597,8 +4715,19 @@ var SAFE_ERROR_DETAIL_POLICIES = {
4597
4715
  DELEGATION_HOST_MISMATCH: {
4598
4716
  expectedHost: safeOriginHost,
4599
4717
  artifactHost: safeOriginHost
4718
+ },
4719
+ STORAGE_QUOTA_EXCEEDED: {
4720
+ account: safeStorageAccount
4600
4721
  }
4601
4722
  };
4723
+ function safeStorageAccount(value) {
4724
+ if (!isRecord(value)) return void 0;
4725
+ const { usedBytes, limitBytes, plan } = value;
4726
+ if (!Number.isSafeInteger(usedBytes) || usedBytes < 0 || !Number.isSafeInteger(limitBytes) || limitBytes <= 0) {
4727
+ return void 0;
4728
+ }
4729
+ return typeof plan === "string" && /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,31}$/.test(plan) ? { usedBytes, limitBytes, plan } : { usedBytes, limitBytes };
4730
+ }
4602
4731
  function redactSafeErrorDetails(code, details) {
4603
4732
  const policy = SAFE_ERROR_DETAIL_POLICIES[code];
4604
4733
  if (policy === void 0) return void 0;
@@ -4684,6 +4813,9 @@ async function invokeLocalAuthorityRetry(operationId, operationVersion, target,
4684
4813
  unknownInput
4685
4814
  );
4686
4815
  if (isPreparedError(prepared)) return prepared.result;
4816
+ return withRuntimeWarnings(await retryWithLocalAuthority(prepared), prepared);
4817
+ }
4818
+ async function retryWithLocalAuthority(prepared) {
4687
4819
  if (prepared.operation.operationId !== "tinycloud.secrets.get" || prepared.operation.operationVersion !== 1) {
4688
4820
  return errorResult(
4689
4821
  prepared.operation,
@@ -4782,6 +4914,10 @@ async function invokeLocalAuthorityRetry(operationId, operationVersion, target,
4782
4914
  return localAuthorityFailure(prepared);
4783
4915
  }
4784
4916
  }
4917
+ function withRuntimeWarnings(result, prepared) {
4918
+ const warnings = prepared.runtimeResolution.context.runtime?.warnings;
4919
+ return warnings === void 0 || warnings.length === 0 ? result : { ...result, warnings: [...warnings] };
4920
+ }
4785
4921
  function isPreparedError(prepared) {
4786
4922
  return "status" in prepared;
4787
4923
  }