@tinycloud/cli 1.1.0-beta.11 → 1.1.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.
@@ -59,7 +59,9 @@ var init_constants = __esm({
59
59
  NOT_FOUND: 4,
60
60
  PERMISSION_DENIED: 5,
61
61
  NETWORK_ERROR: 6,
62
- NODE_ERROR: 7
62
+ NODE_ERROR: 7,
63
+ /** A write was refused because the owner's TinyCloud storage is full. */
64
+ STORAGE_FULL: 10
63
65
  };
64
66
  }
65
67
  });
@@ -258,6 +260,86 @@ var init_formatter = __esm({
258
260
  }
259
261
  });
260
262
 
263
+ // src/output/storage.ts
264
+ function storageRejection(error) {
265
+ let node = error;
266
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null; depth += 1) {
267
+ const record = node;
268
+ let code3;
269
+ if (record.code === "STORAGE_QUOTA_EXCEEDED" || record.code === "STORAGE_LIMIT_REACHED") {
270
+ code3 = record.code;
271
+ } else if (record.code !== void 0) {
272
+ return void 0;
273
+ } else if (isStorageStatus(record)) {
274
+ code3 = storageCodeFromText(record.message);
275
+ }
276
+ if (code3) {
277
+ const account = accountTotals(record) ?? accountTotals(record.meta) ?? accountTotals(record.metadata);
278
+ return account ? { code: code3, account } : { code: code3 };
279
+ }
280
+ node = record.cause;
281
+ }
282
+ return void 0;
283
+ }
284
+ function describeStorageRejection(rejection, progress) {
285
+ const reason = rejection.code === "STORAGE_LIMIT_REACHED" ? "This write is larger than the TinyCloud storage you have left" : "TinyCloud storage is full";
286
+ const message = progress ? `${progress}${reason}.` : `${reason}; nothing was written.`;
287
+ const { account } = rejection;
288
+ const totals = account ? `${formatStorageBytes(account.usedBytes)} used of ${formatStorageBytes(account.limitBytes)}${account.plan ? ` (${account.plan} plan)` : ""}. ` : "";
289
+ return {
290
+ message,
291
+ hint: `${totals}Reading still works.
292
+ Free up space or upgrade: ${MANAGE_STORAGE_URL}`
293
+ };
294
+ }
295
+ function formatStorageBytes(bytes) {
296
+ const units = ["KiB", "MiB", "GiB", "TiB"];
297
+ if (bytes < 1024) return `${bytes} B`;
298
+ let value = bytes / 1024;
299
+ let unit = 0;
300
+ while (value >= 1024 && unit < units.length - 1) {
301
+ value /= 1024;
302
+ unit += 1;
303
+ }
304
+ return `${value.toFixed(1).replace(/\.0$/, "")} ${units[unit]}`;
305
+ }
306
+ function storageCodeFromText(message) {
307
+ if (typeof message !== "string") return void 0;
308
+ if (message.includes("larger than the TinyCloud storage you have left")) return "STORAGE_LIMIT_REACHED";
309
+ if (message.includes("TinyCloud storage is full") || /storage quota exceeded/i.test(message)) {
310
+ return "STORAGE_QUOTA_EXCEEDED";
311
+ }
312
+ return void 0;
313
+ }
314
+ function isStorageStatus(record) {
315
+ const meta = "meta" in record && typeof record.meta === "object" && record.meta !== null ? record.meta : {};
316
+ const statuses = [
317
+ "status" in record ? record.status : void 0,
318
+ "statusCode" in record ? record.statusCode : void 0,
319
+ "status" in meta ? meta.status : void 0
320
+ ];
321
+ return statuses.some((value) => value === 402 || value === 413);
322
+ }
323
+ function accountTotals(meta) {
324
+ if (typeof meta !== "object" || meta === null) return void 0;
325
+ const account = meta.account;
326
+ if (typeof account !== "object" || account === null) return void 0;
327
+ const { usedBytes, limitBytes, plan } = account;
328
+ if (!isByteCount(usedBytes) || !isByteCount(limitBytes) || limitBytes === 0) return void 0;
329
+ return typeof plan === "string" && /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,31}$/.test(plan) ? { usedBytes, limitBytes, plan } : { usedBytes, limitBytes };
330
+ }
331
+ function isByteCount(value) {
332
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
333
+ }
334
+ var MANAGE_STORAGE_URL, MAX_CAUSE_DEPTH;
335
+ var init_storage2 = __esm({
336
+ "src/output/storage.ts"() {
337
+ "use strict";
338
+ MANAGE_STORAGE_URL = "https://account.tinycloud.xyz/billing";
339
+ MAX_CAUSE_DEPTH = 4;
340
+ }
341
+ });
342
+
261
343
  // src/output/errors.ts
262
344
  import { readFileSync, readdirSync } from "fs";
263
345
  import { join as join2 } from "path";
@@ -268,6 +350,8 @@ function setActiveProfileName(name) {
268
350
  }
269
351
  function cliErrorFromService(error, message = error.message) {
270
352
  if (error instanceof CLIError) return error;
353
+ const storageFull = storageFullError(error);
354
+ if (storageFull) return storageFull;
271
355
  const meta = { ...error.meta };
272
356
  delete meta.hint;
273
357
  const status = error.status ?? error.statusCode;
@@ -286,6 +370,8 @@ function cliErrorFromService(error, message = error.message) {
286
370
  );
287
371
  }
288
372
  function wrapError(error) {
373
+ const storageFull = storageFullError(error);
374
+ if (storageFull) return storageFull;
289
375
  if (error instanceof CLIError) return error;
290
376
  const message = error instanceof Error ? error.message : String(error);
291
377
  const verdict = authorizationVerdictOf(error);
@@ -332,6 +418,13 @@ function missingPrivateKeyError() {
332
418
  { hint: `Sign in again with: tc --profile ${profileName} auth login --method openkey` }
333
419
  );
334
420
  }
421
+ function storageFullError(error, progress) {
422
+ if (error instanceof CLIError && error.exitCode === ExitCode.STORAGE_FULL) return error;
423
+ const rejection = storageRejection(error);
424
+ if (!rejection) return void 0;
425
+ const { message, hint } = describeStorageRejection(rejection, progress);
426
+ return new CLIError(rejection.code, message, ExitCode.STORAGE_FULL, { hint });
427
+ }
335
428
  function handleError(error) {
336
429
  const cliError = wrapError(error);
337
430
  const prebuilt = typeof cliError.metadata?.hint === "string" ? cliError.metadata.hint : void 0;
@@ -414,6 +507,7 @@ var init_errors = __esm({
414
507
  "use strict";
415
508
  init_constants();
416
509
  init_formatter();
510
+ init_storage2();
417
511
  CLIError = class extends Error {
418
512
  constructor(code3, message, exitCode = ExitCode.ERROR, metadata) {
419
513
  super(message);
@@ -7055,7 +7149,7 @@ function validatedCapabilityOf2(error) {
7055
7149
  if (verdict !== "unauthenticated" && verdict !== "forbidden") return void 0;
7056
7150
  const seen = /* @__PURE__ */ new Set();
7057
7151
  let node = error;
7058
- for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
7152
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH2 && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
7059
7153
  seen.add(node);
7060
7154
  const record = node;
7061
7155
  const status = errorStatusOf(record);
@@ -7075,7 +7169,7 @@ function validatedCapabilityOf2(error) {
7075
7169
  }
7076
7170
  return void 0;
7077
7171
  }
7078
- var MAX_CAUSE_DEPTH = 8;
7172
+ var MAX_CAUSE_DEPTH2 = 8;
7079
7173
  function errorStatusOf(node) {
7080
7174
  const meta = node.meta;
7081
7175
  const candidates = [
@@ -7094,7 +7188,7 @@ function authorizationVerdictOf2(error) {
7094
7188
  const seen = /* @__PURE__ */ new Set();
7095
7189
  let sawUnauthorizedCode = false;
7096
7190
  let node = error;
7097
- for (let depth = 0; depth < MAX_CAUSE_DEPTH && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
7191
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH2 && typeof node === "object" && node !== null && !seen.has(node); depth += 1) {
7098
7192
  seen.add(node);
7099
7193
  const record = node;
7100
7194
  const status = errorStatusOf(record);
@@ -15117,6 +15211,34 @@ async function commitLogin(profileName, snapshot, commit) {
15117
15211
  });
15118
15212
  }
15119
15213
 
15214
+ // src/auth/owner-key.ts
15215
+ init_theme();
15216
+ function openKeyPrimaryFlag(delegation) {
15217
+ return typeof delegation.primary === "boolean" ? delegation.primary : void 0;
15218
+ }
15219
+ function withOwnerKeyPrimary(profile, primary) {
15220
+ const { ownerKeyPrimary: _previous, ...rest } = profile;
15221
+ return primary === void 0 ? rest : { ...rest, ownerKeyPrimary: primary };
15222
+ }
15223
+ function nonPrimaryKeyWarning(ownerDid) {
15224
+ const address = ownerDid.split(":")[4] ?? ownerDid;
15225
+ return `${theme.warn("Warning:")} you signed in with OpenKey key ${address} (${ownerDid}), which is not your account's primary OpenKey key. This key is a separate owner with its own spaces and data, so this profile does not see the data stored under your primary key.
15226
+ To use your primary key, log in again and choose the primary key in OpenKey, for example into a new profile: tc init --name <profile>
15227
+ `;
15228
+ }
15229
+ function warnIfNotPrimaryKey(ownerDid, primary) {
15230
+ if (primary !== false || ownerDid === void 0) return;
15231
+ process.stderr.write(nonPrimaryKeyWarning(ownerDid));
15232
+ }
15233
+ function ownerLoginPermissions(owner) {
15234
+ const space = ownerSpaceId("default", owner);
15235
+ return [
15236
+ { service: "tinycloud.kv", space, path: "", actions: ["tinycloud.kv/put", "tinycloud.kv/get", "tinycloud.kv/del", "tinycloud.kv/list", "tinycloud.kv/metadata"] },
15237
+ { service: "tinycloud.sql", space, path: "", actions: ["tinycloud.sql/read", "tinycloud.sql/write", "tinycloud.sql/admin"] },
15238
+ { service: "tinycloud.capabilities", space, path: "", actions: ["tinycloud.capabilities/read"] }
15239
+ ];
15240
+ }
15241
+
15120
15242
  // src/auth/device-auth.ts
15121
15243
  var DEVICE_DELEGATION_MAX_SECONDS = 30 * 24 * 60 * 60;
15122
15244
  var DEVICE_APPROVAL_WINDOW_MAX_SECONDS = 60 * 60;
@@ -15321,7 +15443,8 @@ async function verifyApproval(input) {
15321
15443
  spaceId: delegation.spaceId,
15322
15444
  expiresAt: session.expiresAt,
15323
15445
  approved: permissionsFromTuples(signed),
15324
- declined: declinedPermissions(input.requested, session.permissions, ownerDid)
15446
+ declined: declinedPermissions(input.requested, session.permissions, ownerDid),
15447
+ ...openKeyPrimaryFlag(delegation) === void 0 ? {} : { primary: openKeyPrimaryFlag(delegation) }
15325
15448
  };
15326
15449
  }
15327
15450
  function writeApprovalPrompt(prompt) {
@@ -15498,7 +15621,7 @@ async function loginWithDeviceAuthorization(input) {
15498
15621
  openkeyHost: input.openkeyHost ?? resolveDeviceApiHost(existing),
15499
15622
  expectedOwner
15500
15623
  });
15501
- const profile = {
15624
+ const profile = withOwnerKeyPrimary({
15502
15625
  ...existing,
15503
15626
  name: input.profileName,
15504
15627
  host: input.persistHost === true || !existing?.host ? input.nodeOrigin : existing.host,
@@ -15512,7 +15635,7 @@ async function loginWithDeviceAuthorization(input) {
15512
15635
  posture: "owner-openkey",
15513
15636
  operatorType: existing?.operatorType ?? "human",
15514
15637
  authMethod: "openkey"
15515
- };
15638
+ }, result.primary);
15516
15639
  await commitLogin(input.profileName, snapshot, {
15517
15640
  key,
15518
15641
  session: result.session,
@@ -15520,6 +15643,7 @@ async function loginWithDeviceAuthorization(input) {
15520
15643
  // The signed permissions, with their caveats; `approved` is the action summary.
15521
15644
  approved: { scope: result.session.permissions, ownerDid: result.ownerDid, replaceSession: input.replaceSession === true }
15522
15645
  });
15646
+ warnIfNotPrimaryKey(result.ownerDid, result.primary);
15523
15647
  return { profile, result };
15524
15648
  }
15525
15649
 
@@ -15557,7 +15681,7 @@ async function promptAuthMethod() {
15557
15681
  }
15558
15682
  function registerAuthCommand(program) {
15559
15683
  const auth = program.command("auth").description("Authentication management");
15560
- auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Require this existing primary DID (did:pkh:eip155:CHAIN:ADDRESS) for scoped login; names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").action(async (options, cmd) => {
15684
+ auth.command("login").description("Authenticate with TinyCloud").option("--device", "Approve on another device (e.g. a phone) through OpenKey device authorization; requires a KV-scoped --manifest (SQL and secret decrypt need browser or --paste login)").option("--paste", "Use manual paste mode instead of browser callback").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--method <method>", "Authentication method: local or openkey").option("--manifest <fileOrBase64>", `Request only this manifest's permissions (one space, plus raw encryption network entries such as a secrets decrypt grant); ${SHARE_PUBLISHING_MANIFEST_REF} covers \`tc share publish\``).option("--expiry <duration>", "OpenKey session lifetime, e.g. 1h or 7d (device login: at most 30d, default 30d)").option("--owner <did>", "Sign in as this owner (did:pkh:eip155:CHAIN:ADDRESS): OpenKey preselects that key, e.g. one that is not the account's primary key, and an approval by any other identity is refused. With --manifest, also names the secrets owner for a manifest's `secrets` on a profile with no recorded owner").option("--replace-session", "Scoped or device login: replace this profile's live session even though the new scope would narrow, change or shorten it (prefer a new profile)").action(async (options, cmd) => {
15561
15685
  try {
15562
15686
  if (options.device && options.paste) {
15563
15687
  throw new CLIError("INVALID_ARGUMENT", "--device and --paste are mutually exclusive.", ExitCode.USAGE_ERROR);
@@ -15566,9 +15690,6 @@ function registerAuthCommand(program) {
15566
15690
  if (scoped && options.method === "local") {
15567
15691
  throw new CLIError("INVALID_ARGUMENT", "--device, --manifest, --expiry and --owner require OpenKey login.", ExitCode.USAGE_ERROR);
15568
15692
  }
15569
- if (options.owner && !options.manifest) {
15570
- throw new CLIError("INVALID_ARGUMENT", "--owner requires --manifest so the signed identity is verified.", ExitCode.USAGE_ERROR);
15571
- }
15572
15693
  if (options.device && !options.manifest) {
15573
15694
  throw new CLIError(
15574
15695
  "MANIFEST_REQUIRED",
@@ -15691,6 +15812,7 @@ function registerAuthCommand(program) {
15691
15812
  did: profile?.did ?? null,
15692
15813
  sessionDid: profile?.sessionDid ?? null,
15693
15814
  ownerDid: profile?.ownerDid ?? null,
15815
+ ownerKeyPrimary: profile?.ownerKeyPrimary ?? null,
15694
15816
  spaceId: profile?.spaceId ?? null,
15695
15817
  host: ctx.host,
15696
15818
  profile: ctx.profile,
@@ -15711,6 +15833,7 @@ function registerAuthCommand(program) {
15711
15833
  process.stdout.write(formatField("DID", profile?.did ?? null) + "\n");
15712
15834
  process.stdout.write(formatField("Session DID", profile?.sessionDid ?? null) + "\n");
15713
15835
  process.stdout.write(formatField("Owner DID", profile?.ownerDid ?? null) + "\n");
15836
+ process.stdout.write(formatField("Primary Key", profile?.ownerKeyPrimary ?? "unknown") + "\n");
15714
15837
  process.stdout.write(formatField("Address", profile?.address ?? null) + "\n");
15715
15838
  process.stdout.write(formatField("Space ID", profile?.spaceId ?? null) + "\n");
15716
15839
  process.stdout.write(formatField("Has Key", hasKey !== null) + "\n");
@@ -16783,7 +16906,9 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
16783
16906
  jwk: key,
16784
16907
  host,
16785
16908
  openkeyHost: resolveOpenKeyHost(profile),
16786
- permissions,
16909
+ // A plain login with --owner names that owner's space, so OpenKey
16910
+ // preselects its key; the signed owner is verified below.
16911
+ permissions: permissions ?? (options.expectedOwner === void 0 ? void 0 : ownerLoginPermissions(expectedOwner)),
16787
16912
  expiry: openKeyExpiry,
16788
16913
  ...permissions ? { reason: "Allow this TinyCloud CLI profile to use the permissions in the requested manifest." } : {}
16789
16914
  });
@@ -16809,7 +16934,8 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
16809
16934
  sanitizedSession = Object.fromEntries(Object.entries(withoutTrustFields(merged)).filter(([name]) => name !== "siwe" && name !== "signature"));
16810
16935
  }
16811
16936
  }
16812
- const updatedProfile = {
16937
+ const ownerKeyPrimary = verifiedOwner === void 0 ? void 0 : openKeyPrimaryFlag(delegationData);
16938
+ const updatedProfile = withOwnerKeyPrimary({
16813
16939
  ...profile,
16814
16940
  host: options.persistHost === true || !profile.host ? host : profile.host,
16815
16941
  sessionDid: profile.sessionDid ?? profile.did,
@@ -16818,13 +16944,14 @@ async function refreshOpenKeySession(profileName, host, options = {}) {
16818
16944
  authMethod: "openkey",
16819
16945
  ...typeof sanitizedSession.spaceId === "string" ? { spaceId: sanitizedSession.spaceId } : {},
16820
16946
  ...verifiedOwner === void 0 ? {} : { ownerDid: verifiedOwner }
16821
- };
16947
+ }, ownerKeyPrimary);
16822
16948
  await commitLogin(profileName, snapshot, {
16823
16949
  key,
16824
16950
  session: sanitizedSession,
16825
16951
  profile: updatedProfile,
16826
16952
  ...permissions && verifiedOwner ? { approved: { scope: sanitizedSession.permissions, ownerDid: verifiedOwner, replaceSession: options.replaceSession === true } } : {}
16827
16953
  });
16954
+ warnIfNotPrimaryKey(verifiedOwner, ownerKeyPrimary);
16828
16955
  return { profile: updatedProfile, delegationData: sanitizedSession, declined, legacyNested };
16829
16956
  }
16830
16957
 
@@ -17498,15 +17625,6 @@ async function throwKvError(error, spaceUri, profileName) {
17498
17625
  if (hosted) throw hosted;
17499
17626
  throw cliErrorFromService(error);
17500
17627
  }
17501
- function isByteCount(value) {
17502
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
17503
- }
17504
- function storageQuotaError(error) {
17505
- if (error.code !== "STORAGE_QUOTA_EXCEEDED") return void 0;
17506
- const { usedBytes, limitBytes } = typeof error.meta === "object" && error.meta !== null ? error.meta : {};
17507
- const sizes = isByteCount(usedBytes) && isByteCount(limitBytes) ? ` (${formatBytes(usedBytes)} used of ${formatBytes(limitBytes)} limit)` : "";
17508
- return new CLIError("STORAGE_QUOTA_EXCEEDED", `storage quota exceeded${sizes}; nothing was written`, ExitCode.ERROR);
17509
- }
17510
17628
  function assertAddressableKey(key) {
17511
17629
  for (let i = 0; i < key.length; i++) {
17512
17630
  const code3 = key.charCodeAt(i);
@@ -17602,8 +17720,6 @@ function registerKvCommand(program) {
17602
17720
  const { kv: kv2, spaceUri } = await kvHandle(node, options.space, ctx.profile);
17603
17721
  const result = await withSpinner(`Writing ${key}...`, () => kv2.put(key, putValue));
17604
17722
  if (!result.ok) {
17605
- const quota = storageQuotaError(result.error);
17606
- if (quota) throw quota;
17607
17723
  await throwKvError(result.error, spaceUri, ctx.profile);
17608
17724
  }
17609
17725
  outputJson({ key, written: true });
@@ -29279,7 +29395,6 @@ var MAX_INLINE_BYTES2 = 256 * 1024;
29279
29395
 
29280
29396
  // src/commands/share.ts
29281
29397
  import { ProfileLockTimeoutError as ProfileLockTimeoutError3 } from "@tinycloud/operations/state";
29282
- init_formatter();
29283
29398
  init_errors();
29284
29399
 
29285
29400
  // src/share/output.ts
@@ -29542,9 +29657,8 @@ function shareCliError(error, operation = "publish") {
29542
29657
  if (failure.kind === "registry-rejected") {
29543
29658
  return new CLIError("REGISTRY_REJECTED", "the TinyCloud location registry rejected this session's location record, so nothing was shared; retrying will not help. Log in again, and report the problem if it persists", 6);
29544
29659
  }
29545
- if (failure.kind === "storage-quota-exceeded") {
29546
- const sizes = failure.usedBytes === void 0 || failure.limitBytes === void 0 ? "" : ` (${formatBytes(failure.usedBytes)} used of ${formatBytes(failure.limitBytes)} limit)`;
29547
- return new CLIError("STORAGE_QUOTA_EXCEEDED", `storage quota exceeded${sizes}; nothing was shared`, 4);
29660
+ if (failure.kind === "storage-full") {
29661
+ return storageFullError(failure);
29548
29662
  }
29549
29663
  if (failure.kind === "upload-failed") {
29550
29664
  return new CLIError("UPLOAD_FAILED", "share source upload failed; nothing was shared", 4);
@@ -29564,6 +29678,8 @@ function shareCliError(error, operation = "publish") {
29564
29678
  const code3 = error.code === "fetch-failed" ? "NOT_FOUND" : error.code.replaceAll("-", "_").toUpperCase();
29565
29679
  return new CLIError(code3, error.message, exit);
29566
29680
  }
29681
+ const storageFull = storageFullError(error);
29682
+ if (storageFull) return storageFull;
29567
29683
  const message = error instanceof Error ? error.message : String(error);
29568
29684
  const nodeCode = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
29569
29685
  const known = {
@@ -30021,6 +30137,8 @@ async function dbHandle(node, dbName, spaceInput, profileName) {
30021
30137
  return { handle: sql.db(dbName), spaceUri };
30022
30138
  }
30023
30139
  async function throwSqlError(error, spaceUri, profileName, prefix) {
30140
+ const storageFull = storageFullError(error, prefix);
30141
+ if (storageFull) throw storageFull;
30024
30142
  const hosted = await unhostedSpaceError(error, spaceUri, profileName);
30025
30143
  if (hosted) throw hosted;
30026
30144
  const message = prefix ? `${prefix}${error.message}` : error.message;