@anuma/sdk 7.1.0 → 7.1.1

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.
package/README.md CHANGED
@@ -74,6 +74,16 @@ The SDK provides entry points for different platforms:
74
74
  - `@anuma/sdk/expo` — React Native / Expo
75
75
  - `@anuma/sdk/client` — Generated API client and types
76
76
 
77
+ ### iCloud backups
78
+
79
+ Add a `backupRevision` field with type `String` to the `ConversationBackup`
80
+ record type in the development schema.
81
+ [Deploy the schema to production](https://developer.apple.com/documentation/cloudkit/deploying-an-icloud-container-s-schema)
82
+ before you release a client that uses iCloud backup.
83
+
84
+ The SDK compares this saved revision with the local conversation and message
85
+ data. The next backup replaces older files that have no saved revision.
86
+
77
87
  ## Features
78
88
 
79
89
  The SDK gives you access to a unified API across multiple LLM providers through
@@ -30968,7 +30968,7 @@ async function requestICloudSignIn() {
30968
30968
  }
30969
30969
  return container.whenUserSignsIn();
30970
30970
  }
30971
- async function uploadFileToICloud(filename, content) {
30971
+ async function uploadFileToICloud(filename, content, backupRevision) {
30972
30972
  const container = await getContainer();
30973
30973
  const database = container.privateCloudDatabase;
30974
30974
  const recordName = `backup_${filename.replace(/[^a-zA-Z0-9]/g, "_")}`;
@@ -30981,7 +30981,8 @@ async function uploadFileToICloud(filename, content) {
30981
30981
  filename: { value: filename },
30982
30982
  data: { value: base64Data },
30983
30983
  size: { value: content.size },
30984
- contentType: { value: content.type || "application/json" }
30984
+ contentType: { value: content.type || "application/json" },
30985
+ ...backupRevision ? { backupRevision: { value: backupRevision } } : {}
30985
30986
  }
30986
30987
  };
30987
30988
  const response = await database.saveRecords(record2);
@@ -30993,7 +30994,8 @@ async function uploadFileToICloud(filename, content) {
30993
30994
  recordName: savedRecord.recordName,
30994
30995
  filename,
30995
30996
  modifiedAt: new Date(savedRecord.modified?.timestamp ?? Date.now()),
30996
- size: content.size
30997
+ size: content.size,
30998
+ backupRevision: savedRecord.fields.backupRevision?.value
30997
30999
  };
30998
31000
  }
30999
31001
  async function listICloudFiles() {
@@ -31014,6 +31016,7 @@ async function listICloudFiles() {
31014
31016
  recordName: record2.recordName,
31015
31017
  filename: record2.fields.filename?.value ?? "",
31016
31018
  modifiedAt: new Date(record2.modified?.timestamp ?? Date.now()),
31019
+ backupRevision: record2.fields.backupRevision?.value,
31017
31020
  size: typeof record2.fields.data?.value === "object" && record2.fields.data?.value !== null ? record2.fields.data.value.size : 0
31018
31021
  }));
31019
31022
  return files.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
@@ -31067,35 +31070,53 @@ async function findICloudFile(filename) {
31067
31070
  recordName: record2.recordName,
31068
31071
  filename: record2.fields.filename?.value ?? "",
31069
31072
  modifiedAt: new Date(record2.modified?.timestamp ?? Date.now()),
31073
+ backupRevision: record2.fields.backupRevision?.value,
31070
31074
  size: typeof record2.fields.data?.value === "object" && record2.fields.data?.value !== null ? record2.fields.data.value.size : 0
31071
31075
  };
31072
31076
  }
31073
31077
 
31074
31078
  // src/lib/backup/icloud/backup.ts
31075
31079
  var isAuthError = (err) => err instanceof Error && (err.message.includes("AUTHENTICATION") || err.message.includes("NOT_AUTHENTICATED") || err.message.includes("sign in"));
31080
+ var UNREVISIONED_COLUMNS = /* @__PURE__ */ new Set([
31081
+ "updated_at",
31082
+ "vector",
31083
+ "embedding_model",
31084
+ "chunks",
31085
+ "origin"
31086
+ ]);
31087
+ async function getBackupRevision(database, conversationId) {
31088
+ const serialized = await database.read(async () => {
31089
+ const { Q: Q20 } = await import("@nozbe/watermelondb");
31090
+ const tables = await Promise.all(
31091
+ ["conversations", "history"].map(
31092
+ (table) => database.get(table).query(Q20.where("conversation_id", conversationId), Q20.sortBy("id", Q20.asc)).unsafeFetchRaw()
31093
+ )
31094
+ );
31095
+ return JSON.stringify(
31096
+ tables.map(
31097
+ (rows) => rows.map(
31098
+ (row) => Object.entries(row).filter(([key]) => !key.startsWith("_") && !UNREVISIONED_COLUMNS.has(key)).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
31099
+ )
31100
+ )
31101
+ );
31102
+ });
31103
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialized));
31104
+ return "v1:" + Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
31105
+ }
31076
31106
  async function pushConversationToICloud(database, conversationId, userAddress, deps, _retried = false) {
31077
31107
  try {
31078
31108
  await deps.requestEncryptionKey(userAddress);
31079
31109
  const filename = `${conversationId}.json`;
31080
31110
  const existingFile = await findICloudFile(filename);
31081
- if (existingFile) {
31082
- const { Q: Q20 } = await import("@nozbe/watermelondb");
31083
- const conversationsCollection = database.get("conversations");
31084
- const records = await conversationsCollection.query(Q20.where("conversation_id", conversationId)).fetch();
31085
- if (records.length > 0) {
31086
- const conversation = conversationToStoredRaw(records[0]);
31087
- const localUpdated = conversation.updatedAt.getTime();
31088
- const remoteModified = existingFile.modifiedAt.getTime();
31089
- if (localUpdated <= remoteModified) {
31090
- return "skipped";
31091
- }
31092
- }
31111
+ const backupRevision = await getBackupRevision(database, conversationId);
31112
+ if (existingFile?.backupRevision === backupRevision) {
31113
+ return "skipped";
31093
31114
  }
31094
31115
  const exportResult = await deps.exportConversation(conversationId, userAddress);
31095
31116
  if (!exportResult.success || !exportResult.blob) {
31096
31117
  return "failed";
31097
31118
  }
31098
- await uploadFileToICloud(filename, exportResult.blob);
31119
+ await uploadFileToICloud(filename, exportResult.blob, backupRevision);
31099
31120
  return "uploaded";
31100
31121
  } catch (err) {
31101
31122
  if (isAuthError(err) && !_retried) {
@@ -109,6 +109,9 @@ interface CloudKitRecord {
109
109
  size: number;
110
110
  };
111
111
  };
112
+ backupRevision?: {
113
+ value: string;
114
+ };
112
115
  };
113
116
  modified?: {
114
117
  timestamp: number;
@@ -109,6 +109,9 @@ interface CloudKitRecord {
109
109
  size: number;
110
110
  };
111
111
  };
112
+ backupRevision?: {
113
+ value: string;
114
+ };
112
115
  };
113
116
  modified?: {
114
117
  timestamp: number;
@@ -29112,7 +29112,7 @@ async function requestICloudSignIn() {
29112
29112
  }
29113
29113
  return container.whenUserSignsIn();
29114
29114
  }
29115
- async function uploadFileToICloud(filename, content) {
29115
+ async function uploadFileToICloud(filename, content, backupRevision) {
29116
29116
  const container = await getContainer();
29117
29117
  const database = container.privateCloudDatabase;
29118
29118
  const recordName = `backup_${filename.replace(/[^a-zA-Z0-9]/g, "_")}`;
@@ -29125,7 +29125,8 @@ async function uploadFileToICloud(filename, content) {
29125
29125
  filename: { value: filename },
29126
29126
  data: { value: base64Data },
29127
29127
  size: { value: content.size },
29128
- contentType: { value: content.type || "application/json" }
29128
+ contentType: { value: content.type || "application/json" },
29129
+ ...backupRevision ? { backupRevision: { value: backupRevision } } : {}
29129
29130
  }
29130
29131
  };
29131
29132
  const response = await database.saveRecords(record2);
@@ -29137,7 +29138,8 @@ async function uploadFileToICloud(filename, content) {
29137
29138
  recordName: savedRecord.recordName,
29138
29139
  filename,
29139
29140
  modifiedAt: new Date(savedRecord.modified?.timestamp ?? Date.now()),
29140
- size: content.size
29141
+ size: content.size,
29142
+ backupRevision: savedRecord.fields.backupRevision?.value
29141
29143
  };
29142
29144
  }
29143
29145
  async function listICloudFiles() {
@@ -29158,6 +29160,7 @@ async function listICloudFiles() {
29158
29160
  recordName: record2.recordName,
29159
29161
  filename: record2.fields.filename?.value ?? "",
29160
29162
  modifiedAt: new Date(record2.modified?.timestamp ?? Date.now()),
29163
+ backupRevision: record2.fields.backupRevision?.value,
29161
29164
  size: typeof record2.fields.data?.value === "object" && record2.fields.data?.value !== null ? record2.fields.data.value.size : 0
29162
29165
  }));
29163
29166
  return files.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
@@ -29211,35 +29214,53 @@ async function findICloudFile(filename) {
29211
29214
  recordName: record2.recordName,
29212
29215
  filename: record2.fields.filename?.value ?? "",
29213
29216
  modifiedAt: new Date(record2.modified?.timestamp ?? Date.now()),
29217
+ backupRevision: record2.fields.backupRevision?.value,
29214
29218
  size: typeof record2.fields.data?.value === "object" && record2.fields.data?.value !== null ? record2.fields.data.value.size : 0
29215
29219
  };
29216
29220
  }
29217
29221
 
29218
29222
  // src/lib/backup/icloud/backup.ts
29219
29223
  var isAuthError = (err) => err instanceof Error && (err.message.includes("AUTHENTICATION") || err.message.includes("NOT_AUTHENTICATED") || err.message.includes("sign in"));
29224
+ var UNREVISIONED_COLUMNS = /* @__PURE__ */ new Set([
29225
+ "updated_at",
29226
+ "vector",
29227
+ "embedding_model",
29228
+ "chunks",
29229
+ "origin"
29230
+ ]);
29231
+ async function getBackupRevision(database, conversationId) {
29232
+ const serialized = await database.read(async () => {
29233
+ const { Q: Q20 } = await import("@nozbe/watermelondb");
29234
+ const tables = await Promise.all(
29235
+ ["conversations", "history"].map(
29236
+ (table) => database.get(table).query(Q20.where("conversation_id", conversationId), Q20.sortBy("id", Q20.asc)).unsafeFetchRaw()
29237
+ )
29238
+ );
29239
+ return JSON.stringify(
29240
+ tables.map(
29241
+ (rows) => rows.map(
29242
+ (row) => Object.entries(row).filter(([key]) => !key.startsWith("_") && !UNREVISIONED_COLUMNS.has(key)).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
29243
+ )
29244
+ )
29245
+ );
29246
+ });
29247
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialized));
29248
+ return "v1:" + Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
29249
+ }
29220
29250
  async function pushConversationToICloud(database, conversationId, userAddress, deps, _retried = false) {
29221
29251
  try {
29222
29252
  await deps.requestEncryptionKey(userAddress);
29223
29253
  const filename = `${conversationId}.json`;
29224
29254
  const existingFile = await findICloudFile(filename);
29225
- if (existingFile) {
29226
- const { Q: Q20 } = await import("@nozbe/watermelondb");
29227
- const conversationsCollection = database.get("conversations");
29228
- const records = await conversationsCollection.query(Q20.where("conversation_id", conversationId)).fetch();
29229
- if (records.length > 0) {
29230
- const conversation = conversationToStoredRaw(records[0]);
29231
- const localUpdated = conversation.updatedAt.getTime();
29232
- const remoteModified = existingFile.modifiedAt.getTime();
29233
- if (localUpdated <= remoteModified) {
29234
- return "skipped";
29235
- }
29236
- }
29255
+ const backupRevision = await getBackupRevision(database, conversationId);
29256
+ if (existingFile?.backupRevision === backupRevision) {
29257
+ return "skipped";
29237
29258
  }
29238
29259
  const exportResult = await deps.exportConversation(conversationId, userAddress);
29239
29260
  if (!exportResult.success || !exportResult.blob) {
29240
29261
  return "failed";
29241
29262
  }
29242
- await uploadFileToICloud(filename, exportResult.blob);
29263
+ await uploadFileToICloud(filename, exportResult.blob, backupRevision);
29243
29264
  return "uploaded";
29244
29265
  } catch (err) {
29245
29266
  if (isAuthError(err) && !_retried) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",