@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.
@@ -42,6 +42,17 @@ function operationError(code, message, options = {}) {
42
42
  ...options.details === void 0 ? {} : { details: options.details }
43
43
  };
44
44
  }
45
+ function storageFullOperationError(error) {
46
+ if (typeof error !== "object" || error === null) return void 0;
47
+ const { code, meta } = error;
48
+ if (code !== "STORAGE_QUOTA_EXCEEDED" && code !== "STORAGE_LIMIT_REACHED") return void 0;
49
+ const account = typeof meta === "object" && meta !== null ? meta.account : void 0;
50
+ return operationError(
51
+ "STORAGE_QUOTA_EXCEEDED",
52
+ 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}`,
53
+ { retryable: false, ...account === void 0 ? {} : { details: { account } } }
54
+ );
55
+ }
45
56
  function isOperationErrorCode(value) {
46
57
  return typeof value === "string" && OPERATION_ERROR_CODES.includes(value);
47
58
  }
@@ -67,7 +78,7 @@ function sanitizeOperationError(error) {
67
78
  ...candidate.details === void 0 ? {} : { details: candidate.details }
68
79
  });
69
80
  }
70
- var OPERATION_ERROR_CODES, OperationInvocationError;
81
+ var OPERATION_ERROR_CODES, MANAGE_STORAGE, OperationInvocationError;
71
82
  var init_errors = __esm({
72
83
  "src/errors.ts"() {
73
84
  "use strict";
@@ -90,6 +101,7 @@ var init_errors = __esm({
90
101
  "ENCRYPTION_NETWORK_UNRESOLVED",
91
102
  "NODE_UNREACHABLE",
92
103
  "NODE_ERROR",
104
+ "STORAGE_QUOTA_EXCEEDED",
93
105
  "KV_NOT_FOUND",
94
106
  "KV_PRECONDITION_FAILED",
95
107
  "KV_CONFLICT",
@@ -103,6 +115,7 @@ var init_errors = __esm({
103
115
  "OUTPUT_INVALID",
104
116
  "INTERNAL_ERROR"
105
117
  ];
118
+ MANAGE_STORAGE = "Do not retry: the owner must free up space or upgrade at https://account.tinycloud.xyz/billing.";
106
119
  OperationInvocationError = class extends Error {
107
120
  constructor(operationError2) {
108
121
  super(operationError2.message);
@@ -344,8 +357,13 @@ function canonicalizeRecapCaveats(caveats) {
344
357
  }
345
358
  return `[${branches.join(",")}]`;
346
359
  }
360
+ function isExplicitOnlyAction(action) {
361
+ return Object.prototype.hasOwnProperty.call(EXPLICIT_ONLY_ACTIONS, action);
362
+ }
347
363
  function actionContains(grantedAction, requestedAction) {
348
- if (grantedAction === "*" || grantedAction === requestedAction) return true;
364
+ if (grantedAction === requestedAction) return true;
365
+ if (isExplicitOnlyAction(requestedAction)) return false;
366
+ if (grantedAction === "*") return true;
349
367
  if (!grantedAction.endsWith("/*")) return false;
350
368
  const service = grantedAction.slice(0, -2);
351
369
  return requestedAction === service || requestedAction.startsWith(`${service}/`);
@@ -370,10 +388,15 @@ function cloneEntry(entry) {
370
388
  ...entry.expiry !== void 0 ? { expiry: entry.expiry } : {}
371
389
  };
372
390
  }
391
+ var EXPLICIT_ONLY_ACTIONS;
373
392
  var init_capabilities = __esm({
374
393
  "../sdk-core/src/capabilities.ts"() {
375
394
  "use strict";
376
395
  init_manifest();
396
+ EXPLICIT_ONLY_ACTIONS = {
397
+ "tinycloud.kv/sync": true,
398
+ "tinycloud.kv/retain": true
399
+ };
377
400
  }
378
401
  });
379
402
 
@@ -698,7 +721,8 @@ async function acquireProfileLock(profile, options) {
698
721
  const lockPath = profileLockPath(profile);
699
722
  const turnDirectory = profileTurnLockPath(profile);
700
723
  const directory = profilePath(profile);
701
- await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
724
+ if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
725
+ if (!options.requireProfile) await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
702
726
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
703
727
  for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
704
728
  await (0, import_promises.chmod)(path, PRIVATE_DIR_MODE).catch((error) => {
@@ -707,11 +731,15 @@ async function acquireProfileLock(profile, options) {
707
731
  }
708
732
  const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
709
733
  try {
734
+ if (options.requireProfile && !await exists(profileConfigPath(profile))) {
735
+ await (0, import_promises.rmdir)(directory).catch(() => void 0);
736
+ throw new ProfileDeletedError(profile);
737
+ }
710
738
  const deletion = await readJson((0, import_node_path.join)(turnDirectory, "deleted.json")).catch(() => null);
711
739
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
712
740
  while (true) {
713
741
  const token = (0, import_node_crypto.randomUUID)();
714
- if (await publishProfileLock(profile, lockPath, token)) {
742
+ if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
715
743
  return {
716
744
  deletedWhileWaiting,
717
745
  turnSlot: turn.slot,
@@ -956,12 +984,13 @@ async function collectTurns(profile, turn) {
956
984
  }
957
985
  }
958
986
  }
959
- async function publishProfileLock(profile, lockPath, token) {
987
+ async function publishProfileLock(profile, lockPath, token, requireProfile) {
960
988
  try {
961
989
  await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
962
990
  } catch (error) {
963
991
  if (isErrno(error, "EEXIST")) return false;
964
992
  if (!isErrno(error, "ENOENT")) throw error;
993
+ if (requireProfile) throw new ProfileDeletedError(profile);
965
994
  await (0, import_promises.mkdir)(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
966
995
  return false;
967
996
  }
@@ -2532,6 +2561,8 @@ function kvFailure(error, action) {
2532
2561
  if (error.code === "KV_RESPONSE_TOO_LARGE") {
2533
2562
  return { status: "error", error: operationError("KV_RESPONSE_TOO_LARGE", `The TinyCloud KV value exceeds ${MAX_KV_VALUE_BYTES} bytes.`) };
2534
2563
  }
2564
+ const storageFull = storageFullOperationError(error);
2565
+ if (storageFull) return { status: "error", error: storageFull };
2535
2566
  return nodeFailure(action);
2536
2567
  }
2537
2568
  function nodeFailure(action) {
@@ -3150,7 +3181,7 @@ async function executeSqlDml(context, input) {
3150
3181
  const runtime = runtimeContext2(context);
3151
3182
  const space = resolveSqlSpace(runtime, input.space);
3152
3183
  const result = await runtimeNode2(runtime).sqlForSpace(space).db(input.database).execute(input.sql, input.params.map(decodeSqlInputValue));
3153
- if (!result.ok) return authorizationFailure(result.error, "execute the SQLite statement") ?? sqlMutationFailure();
3184
+ if (!result.ok) return authorizationFailure(result.error, "execute the SQLite statement") ?? sqlMutationFailure(result.error);
3154
3185
  const normalized = normalizeExecuteResult(result.data);
3155
3186
  return {
3156
3187
  status: "ok",
@@ -3164,7 +3195,7 @@ async function executeSqlDml(context, input) {
3164
3195
  };
3165
3196
  } catch (error) {
3166
3197
  if (error instanceof OperationInvocationError) return { status: "error", error: error.operationError };
3167
- return authorizationFailure(error, "execute the SQLite statement") ?? sqlMutationFailure();
3198
+ return authorizationFailure(error, "execute the SQLite statement") ?? sqlMutationFailure(error);
3168
3199
  }
3169
3200
  }
3170
3201
  function isSingleReadQuery(sql) {
@@ -3341,7 +3372,9 @@ function sqlServiceFailure(error, action) {
3341
3372
  }
3342
3373
  return nodeFailure2(action);
3343
3374
  }
3344
- function sqlMutationFailure() {
3375
+ function sqlMutationFailure(error) {
3376
+ const storageFull = storageFullOperationError(error);
3377
+ if (storageFull) return { status: "error", error: storageFull };
3345
3378
  return {
3346
3379
  status: "error",
3347
3380
  error: operationError(
@@ -3598,6 +3631,32 @@ ORDER BY type, name`;
3598
3631
  }
3599
3632
  });
3600
3633
 
3634
+ // src/contract.ts
3635
+ var STORED_GRANT_SKIP_REASONS, DELEGATION_CID_PATTERN;
3636
+ var init_contract = __esm({
3637
+ "src/contract.ts"() {
3638
+ "use strict";
3639
+ STORED_GRANT_SKIP_REASONS = [
3640
+ "malformed",
3641
+ "unbound",
3642
+ "outside_request",
3643
+ "host_mismatch",
3644
+ "expired",
3645
+ "cid_mismatch",
3646
+ "authority_invalid",
3647
+ "proof_missing",
3648
+ "proof_invalid",
3649
+ "owner_mismatch",
3650
+ "audience_mismatch",
3651
+ "resources_mismatch",
3652
+ "unsupported",
3653
+ "activation_rejected",
3654
+ "invalid"
3655
+ ];
3656
+ DELEGATION_CID_PATTERN = "^bafkr4i[a-z2-7]{52}$";
3657
+ }
3658
+ });
3659
+
3601
3660
  // src/delegation-binding.ts
3602
3661
  function bindingMigrationPath(profile) {
3603
3662
  return (0, import_node_path2.join)(profilePath(profile), "delegation-binding-migration.json");
@@ -3621,12 +3680,12 @@ function replayLimit(entry, migrated) {
3621
3680
  if (binding.success) return binding.data.requested;
3622
3681
  return !migrated && kind === "compact" && !("authorityRequest" in entry) ? "signed" : void 0;
3623
3682
  }
3624
- function bindToSignedCapabilities(record, signed, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3683
+ function bindToRequest(record, requested, source, recordedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3625
3684
  return {
3626
3685
  ...record,
3627
3686
  authorityRequest: DelegationRequestBindingSchema.parse({
3628
3687
  requestId: `${REQUEST_ID_PREFIX[source]}:${record.delegation.cid}`,
3629
- requested: structuredClone(signed)
3688
+ requested: structuredClone(requested)
3630
3689
  }),
3631
3690
  authorityRequestAudit: { source, recordedAt, note: BINDING_NOTES[source] }
3632
3691
  };
@@ -3682,23 +3741,40 @@ async function migratedRecord(node, entry, host, recordedAt) {
3682
3741
  const signed = await signedCapabilities(node, delegation, host);
3683
3742
  if (signed === void 0) return void 0;
3684
3743
  try {
3685
- return bindToSignedCapabilities({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
3744
+ return bindToRequest({ ...entry, delegation: { ...entry.delegation, cid: delegation.cid } }, signed, "migration", recordedAt);
3686
3745
  } catch {
3687
3746
  return void 0;
3688
3747
  }
3689
3748
  }
3749
+ function skipped(reason) {
3750
+ return { status: "skipped", reason };
3751
+ }
3690
3752
  async function replayStoredDelegation(node, entry, options) {
3753
+ const kind = storedDelegationKind(entry);
3691
3754
  const delegation = normalizeStoredDelegation(entry);
3692
- if (delegation === void 0 || delegation.expiry.getTime() <= Date.now()) return void 0;
3755
+ if (kind === "refused" || delegation === void 0) return skipped("malformed");
3756
+ if (delegation.expiry.getTime() <= Date.now()) return NOT_REPLAYED;
3757
+ if (kind === "other") return skipped("proof_missing");
3693
3758
  const limit = replayLimit(entry, options.migrated);
3694
- if (limit === void 0) return void 0;
3695
- const authorize = limit === "signed" ? () => true : (effective) => delegationWithinRequest(limit, effective, options.resolveSpace);
3759
+ if (limit === void 0) return skipped("unbound");
3760
+ let outsideRequest = false;
3761
+ const authorize = limit === "signed" ? () => true : (effective) => {
3762
+ outsideRequest = !delegationWithinRequest(limit, effective, options.resolveSpace);
3763
+ return !outsideRequest;
3764
+ };
3696
3765
  try {
3697
- return await nodeSdk3.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize });
3698
- } catch {
3699
- return void 0;
3766
+ return {
3767
+ status: "installed",
3768
+ delegation: await nodeSdk3.activateValidatedRuntimeDelegation(node, delegation, { host: options.host, authorize })
3769
+ };
3770
+ } catch (error) {
3771
+ return skipped(outsideRequest ? "outside_request" : rejectionReason(error));
3700
3772
  }
3701
3773
  }
3774
+ function rejectionReason(error) {
3775
+ const reason = isRecord4(error) ? error.reason : void 0;
3776
+ return typeof reason === "string" && STORED_GRANT_SKIP_REASONS.includes(reason) ? reason : "invalid";
3777
+ }
3702
3778
  async function bindingMigrationRecorded(profile) {
3703
3779
  try {
3704
3780
  return await readJson(bindingMigrationPath(profile)) !== null;
@@ -3743,7 +3819,7 @@ function recordKey(entry) {
3743
3819
  function isRecord4(value) {
3744
3820
  return typeof value === "object" && value !== null && !Array.isArray(value);
3745
3821
  }
3746
- var import_node_path2, nodeSdk3, REQUEST_ID_PREFIX, BINDING_NOTES, MIGRATION_LOCK_TIMEOUT_MS;
3822
+ var import_node_path2, nodeSdk3, REQUEST_ID_PREFIX, BINDING_NOTES, MIGRATION_LOCK_TIMEOUT_MS, NOT_REPLAYED;
3747
3823
  var init_delegation_binding = __esm({
3748
3824
  "src/delegation-binding.ts"() {
3749
3825
  "use strict";
@@ -3751,17 +3827,21 @@ var init_delegation_binding = __esm({
3751
3827
  nodeSdk3 = __toESM(require("@tinycloud/node-sdk"), 1);
3752
3828
  init_artifacts();
3753
3829
  init_authority();
3830
+ init_contract();
3754
3831
  init_state();
3755
3832
  init_secrets();
3756
3833
  REQUEST_ID_PREFIX = {
3757
3834
  migration: "migrated",
3758
- "unbound-import": "unbound-import"
3835
+ "unbound-import": "unbound-import",
3836
+ "cli-grant": "cli-grant"
3759
3837
  };
3760
3838
  BINDING_NOTES = {
3761
3839
  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.",
3762
- "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."
3840
+ "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.",
3841
+ "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."
3763
3842
  };
3764
3843
  MIGRATION_LOCK_TIMEOUT_MS = 250;
3844
+ NOT_REPLAYED = { status: "not-replayed" };
3765
3845
  }
3766
3846
  });
3767
3847
 
@@ -4173,7 +4253,18 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4173
4253
  ));
4174
4254
  }
4175
4255
  } else {
4176
- await node.restoreSession(activeSession);
4256
+ try {
4257
+ await node.restoreSession(activeSession);
4258
+ } catch (error) {
4259
+ if (isRecord7(error) && error.code === "AUTH_EXPIRED") {
4260
+ return failed(summary, operationError(
4261
+ "AUTH_REQUIRED",
4262
+ "The stored session has expired or is no longer valid. Sign in again.",
4263
+ { retryable: false }
4264
+ ));
4265
+ }
4266
+ throw error;
4267
+ }
4177
4268
  }
4178
4269
  const activeSessionDid = normalizeDid(node.sessionDid);
4179
4270
  const livePrincipalDid = normalizeDid(node.did);
@@ -4181,6 +4272,7 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4181
4272
  const livePermissions = [...node.getVerifiedSessionCapabilities()];
4182
4273
  const resolveSpace2 = operationSpaceResolver(node, authenticatedSpace ?? summary.space);
4183
4274
  const seenCids = /* @__PURE__ */ new Set();
4275
+ const warnings = [];
4184
4276
  if (!explicitPrivateKeyOverride) {
4185
4277
  const activator = node;
4186
4278
  const migrated = await prepareStoredDelegationReplay(profileName, activator, {
@@ -4190,20 +4282,25 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4190
4282
  migrate: activeSession !== void 0
4191
4283
  });
4192
4284
  for (const entry of await readAdditionalDelegations(profileName)) {
4193
- const activated = await replayStoredDelegation(activator, entry, {
4285
+ const replay = await replayStoredDelegation(activator, entry, {
4194
4286
  host: summary.host,
4195
4287
  migrated,
4196
4288
  resolveSpace: resolveSpace2
4197
4289
  });
4198
- if (activated !== void 0 && !seenCids.has(activated.cid)) {
4199
- seenCids.add(activated.cid);
4200
- livePermissions.push(...activated.effectivePermissions);
4290
+ if (replay.status === "installed") {
4291
+ if (!seenCids.has(replay.delegation.cid)) {
4292
+ seenCids.add(replay.delegation.cid);
4293
+ livePermissions.push(...replay.delegation.effectivePermissions);
4294
+ }
4295
+ } else if (replay.status === "skipped") {
4296
+ warnings.push(skippedGrantWarning(node, entry, replay.reason));
4201
4297
  }
4202
4298
  }
4203
4299
  }
4204
4300
  const runtime = {
4205
4301
  node,
4206
- granted: canonicalizeCapabilities(livePermissions)
4302
+ granted: canonicalizeCapabilities(livePermissions),
4303
+ ...warnings.length === 0 ? {} : { warnings }
4207
4304
  };
4208
4305
  return {
4209
4306
  ok: true,
@@ -4227,6 +4324,24 @@ async function createInvocationRuntime(target, requirement = "authenticated") {
4227
4324
  ));
4228
4325
  }
4229
4326
  }
4327
+ function skippedGrantWarning(node, entry, reason) {
4328
+ const grantCid = verifiedStoredCid(node, entry);
4329
+ return { code: "STORED_GRANT_SKIPPED", reason, ...grantCid === void 0 ? {} : { grantCid } };
4330
+ }
4331
+ function verifiedStoredCid(node, entry) {
4332
+ const delegation = isRecord7(entry) && isRecord7(entry.delegation) ? entry.delegation : void 0;
4333
+ const header = isRecord7(delegation?.delegationHeader) ? delegation.delegationHeader : void 0;
4334
+ const cid = delegation?.cid;
4335
+ const computeDelegationCid = isRecord7(node) ? node.computeDelegationCid : void 0;
4336
+ if (typeof cid !== "string" || typeof header?.Authorization !== "string" || typeof computeDelegationCid !== "function") {
4337
+ return void 0;
4338
+ }
4339
+ try {
4340
+ return computeDelegationCid.call(node, header.Authorization) === cid && DELEGATION_CID.test(cid) ? cid : void 0;
4341
+ } catch {
4342
+ return void 0;
4343
+ }
4344
+ }
4230
4345
  function spaceForAuthenticatedPrincipal(principal2) {
4231
4346
  if (principal2 === void 0) return void 0;
4232
4347
  try {
@@ -4275,17 +4390,19 @@ function unresolvedSummary(target) {
4275
4390
  posture: "unauthenticated"
4276
4391
  };
4277
4392
  }
4278
- var nodeSdk4;
4393
+ var nodeSdk4, DELEGATION_CID;
4279
4394
  var init_runtime = __esm({
4280
4395
  "src/runtime.ts"() {
4281
4396
  "use strict";
4282
4397
  nodeSdk4 = __toESM(require("@tinycloud/node-sdk"), 1);
4398
+ init_contract();
4283
4399
  init_authority();
4284
4400
  init_errors();
4285
4401
  init_profile();
4286
4402
  init_secrets();
4287
4403
  init_state();
4288
4404
  init_delegation_binding();
4405
+ DELEGATION_CID = new RegExp(DELEGATION_CID_PATTERN);
4289
4406
  }
4290
4407
  });
4291
4408
 
@@ -4590,8 +4707,19 @@ var SAFE_ERROR_DETAIL_POLICIES = {
4590
4707
  DELEGATION_HOST_MISMATCH: {
4591
4708
  expectedHost: safeOriginHost,
4592
4709
  artifactHost: safeOriginHost
4710
+ },
4711
+ STORAGE_QUOTA_EXCEEDED: {
4712
+ account: safeStorageAccount
4593
4713
  }
4594
4714
  };
4715
+ function safeStorageAccount(value) {
4716
+ if (!isRecord(value)) return void 0;
4717
+ const { usedBytes, limitBytes, plan } = value;
4718
+ if (!Number.isSafeInteger(usedBytes) || usedBytes < 0 || !Number.isSafeInteger(limitBytes) || limitBytes <= 0) {
4719
+ return void 0;
4720
+ }
4721
+ return typeof plan === "string" && /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,31}$/.test(plan) ? { usedBytes, limitBytes, plan } : { usedBytes, limitBytes };
4722
+ }
4595
4723
  function redactSafeErrorDetails(code, details) {
4596
4724
  const policy = SAFE_ERROR_DETAIL_POLICIES[code];
4597
4725
  if (policy === void 0) return void 0;
@@ -4677,6 +4805,9 @@ async function invokeLocalAuthorityRetry(operationId, operationVersion, target,
4677
4805
  unknownInput
4678
4806
  );
4679
4807
  if (isPreparedError(prepared)) return prepared.result;
4808
+ return withRuntimeWarnings(await retryWithLocalAuthority(prepared), prepared);
4809
+ }
4810
+ async function retryWithLocalAuthority(prepared) {
4680
4811
  if (prepared.operation.operationId !== "tinycloud.secrets.get" || prepared.operation.operationVersion !== 1) {
4681
4812
  return errorResult(
4682
4813
  prepared.operation,
@@ -4775,6 +4906,10 @@ async function invokeLocalAuthorityRetry(operationId, operationVersion, target,
4775
4906
  return localAuthorityFailure(prepared);
4776
4907
  }
4777
4908
  }
4909
+ function withRuntimeWarnings(result, prepared) {
4910
+ const warnings = prepared.runtimeResolution.context.runtime?.warnings;
4911
+ return warnings === void 0 || warnings.length === 0 ? result : { ...result, warnings: [...warnings] };
4912
+ }
4778
4913
  function isPreparedError(prepared) {
4779
4914
  return "status" in prepared;
4780
4915
  }