@drakkar.software/starfish-client 1.14.0 → 1.17.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.
@@ -0,0 +1,39 @@
1
+ import type { StarfishClient } from "./client.js";
2
+ export interface PullEntitlementsOptions {
3
+ /**
4
+ * Path template for the entitlement document.
5
+ * `{userId}` is replaced with the `userId` argument.
6
+ * Defaults to `"/pull/users/{userId}/entitlements"`.
7
+ */
8
+ path?: string;
9
+ /**
10
+ * Field name in the document `data` object that holds the feature slug array.
11
+ * Defaults to `"features"`.
12
+ */
13
+ field?: string;
14
+ }
15
+ /**
16
+ * Fetches the list of feature slugs from a user's entitlement document.
17
+ *
18
+ * Returns an empty array if the document does not exist yet or the features
19
+ * field is absent — so callers never need to handle a 404.
20
+ *
21
+ * ```ts
22
+ * import { pullEntitlements } from "@drakkar.software/starfish-client"
23
+ *
24
+ * const features = await pullEntitlements(client, userId)
25
+ * // e.g. ["premium-package-1", "paid-cloud-sync"]
26
+ *
27
+ * if (features.includes("paid-cloud-sync")) {
28
+ * // unlock cloud sync UI
29
+ * }
30
+ * ```
31
+ *
32
+ * The path template must match the server-side collection's `storagePath`.
33
+ * With the recommended default config:
34
+ * ```ts
35
+ * { storagePath: "users/{identity}/entitlements" }
36
+ * // → path: "/pull/users/{userId}/entitlements" (default)
37
+ * ```
38
+ */
39
+ export declare function pullEntitlements(client: StarfishClient, userId: string, opts?: PullEntitlementsOptions): Promise<string[]>;
@@ -0,0 +1,41 @@
1
+ import { StarfishHttpError } from "./types.js";
2
+ /**
3
+ * Fetches the list of feature slugs from a user's entitlement document.
4
+ *
5
+ * Returns an empty array if the document does not exist yet or the features
6
+ * field is absent — so callers never need to handle a 404.
7
+ *
8
+ * ```ts
9
+ * import { pullEntitlements } from "@drakkar.software/starfish-client"
10
+ *
11
+ * const features = await pullEntitlements(client, userId)
12
+ * // e.g. ["premium-package-1", "paid-cloud-sync"]
13
+ *
14
+ * if (features.includes("paid-cloud-sync")) {
15
+ * // unlock cloud sync UI
16
+ * }
17
+ * ```
18
+ *
19
+ * The path template must match the server-side collection's `storagePath`.
20
+ * With the recommended default config:
21
+ * ```ts
22
+ * { storagePath: "users/{identity}/entitlements" }
23
+ * // → path: "/pull/users/{userId}/entitlements" (default)
24
+ * ```
25
+ */
26
+ export async function pullEntitlements(client, userId, opts) {
27
+ const path = (opts?.path ?? "/pull/users/{userId}/entitlements").replace("{userId}", userId);
28
+ const field = opts?.field ?? "features";
29
+ try {
30
+ const result = await client.pull(path);
31
+ const list = result.data?.[field];
32
+ if (!Array.isArray(list))
33
+ return [];
34
+ return list.filter((s) => typeof s === "string");
35
+ }
36
+ catch (err) {
37
+ if (err instanceof StarfishHttpError && err.status === 404)
38
+ return [];
39
+ throw err;
40
+ }
41
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Group encryption utilities for Starfish.
3
+ *
4
+ * Enables multiple users to share a common encrypted collection without sharing
5
+ * a passphrase. Each member holds their own credentials; a Group Encryption Key
6
+ * (GEK) is distributed per-member using X25519 ECDH key agreement.
7
+ *
8
+ * Typical flow:
9
+ * 1. Each user calls `deriveCredentials(passphrase)` — now includes groupPublicKey / groupPrivateKey.
10
+ * 2. Admin calls `createGroupKeyring(...)` to create a keyring document.
11
+ * 3. Members call `createGroupEncryptor(keyringData, myIdentity, myPrivateKey)` to get an Encryptor.
12
+ * 4. The Encryptor is passed to SyncManager via the `encryptor` option.
13
+ */
14
+ import type { Encryptor } from "./crypto.js";
15
+ /** An ECDH key pair used for group encryption. Hex-encoded for easy serialization. */
16
+ export interface GroupKeyPair {
17
+ /** Hex-encoded X25519 private key (32 bytes). Keep secret — never store on server. */
18
+ privateKey: string;
19
+ /** Hex-encoded X25519 public key (32 bytes). Safe to publish. */
20
+ publicKey: string;
21
+ }
22
+ /** One epoch's wrapped keys: each member's GEK encrypted to their public key. */
23
+ export interface EpochKeyring {
24
+ /** The admin's hex-encoded X25519 public key (used for ECDH by members). */
25
+ adminPublicKey: string;
26
+ /** Map from member identity (userId) → base64(IV || AES-GCM(GEK)) */
27
+ wrappedKeys: Record<string, string>;
28
+ }
29
+ /** The full keyring document stored in a Starfish collection. Push this with any SyncManager. */
30
+ export interface GroupKeyring {
31
+ /** The epoch number currently used for new encryptions. */
32
+ currentEpoch: number;
33
+ /** All epochs. Members unwrap the GEK for whichever epoch a document was encrypted with. */
34
+ epochs: Record<string, EpochKeyring>;
35
+ }
36
+ /**
37
+ * Derives a deterministic X25519 key pair from a passphrase + userId.
38
+ *
39
+ * The derivation uses SHA-256 with a fixed domain separator so it is distinct
40
+ * from the auth token and encryption key derivations. Same passphrase + userId
41
+ * always produces the same key pair on any device (stateless).
42
+ */
43
+ export declare function deriveGroupKeyPair(passphrase: string, userId: string): Promise<GroupKeyPair>;
44
+ /** Generates a random 256-bit Group Encryption Key as a hex string. */
45
+ export declare function generateGroupKey(): string;
46
+ /**
47
+ * Wraps a GEK for a specific member using ECDH key agreement.
48
+ *
49
+ * The wrapper (admin) and member each have an X25519 key pair. ECDH between
50
+ * `wrapperPrivateKey` and `memberPublicKey` produces a shared secret, which is
51
+ * used to derive an AES-256-GCM key that encrypts the GEK.
52
+ *
53
+ * @returns base64(IV || AES-GCM-ciphertext)
54
+ */
55
+ export declare function wrapGroupKey(gek: string, memberPublicKey: string, wrapperPrivateKey: string): Promise<string>;
56
+ /**
57
+ * Unwraps a GEK using the member's own private key and the admin's public key.
58
+ *
59
+ * ECDH between `memberPrivateKey` and `adminPublicKey` yields the same shared
60
+ * secret as the wrapping step, so the same AES key is derived and the GEK is
61
+ * recovered.
62
+ *
63
+ * @returns GEK as a hex string
64
+ */
65
+ export declare function unwrapGroupKey(wrapped: string, memberPrivateKey: string, adminPublicKey: string): Promise<string>;
66
+ /**
67
+ * Creates a new group keyring document with epoch 1.
68
+ *
69
+ * @param adminKeyPair The admin's key pair (from `deriveGroupKeyPair` or `deriveCredentials`)
70
+ * @param members Map from member identity (userId) → hex public key
71
+ * @param gek Optional GEK to use; generated randomly if omitted
72
+ * @returns The keyring document and the raw GEK (admin keeps the GEK to add future members)
73
+ */
74
+ export declare function createGroupKeyring(adminKeyPair: GroupKeyPair, members: Record<string, string>, gek?: string): Promise<{
75
+ keyring: GroupKeyring;
76
+ gek: string;
77
+ }>;
78
+ /**
79
+ * Adds a new member to the current epoch of an existing keyring.
80
+ *
81
+ * The admin supplies the current GEK (returned by `createGroupKeyring` or
82
+ * `rotateGroupKey`) and their key pair to wrap it for the new member.
83
+ * This does NOT rotate the GEK — the new member can read all existing
84
+ * documents encrypted with the current epoch key.
85
+ *
86
+ * Only the admin (whose `publicKey` matches `epochKeyring.adminPublicKey`) can
87
+ * add members, because all wrapped entries must use the same ECDH key pair.
88
+ */
89
+ export declare function addGroupMember(keyring: GroupKeyring, adminKeyPair: GroupKeyPair, currentGek: string, newMemberId: string, newMemberPublicKey: string): Promise<GroupKeyring>;
90
+ /**
91
+ * Rotates the group key, creating a new epoch.
92
+ *
93
+ * Used when removing a member. The removed member retains their old epoch key
94
+ * (and can still read old documents), but cannot read new documents.
95
+ *
96
+ * @param remainingMembers Map from identity → hex public key for members who keep access
97
+ */
98
+ export declare function rotateGroupKey(keyring: GroupKeyring, adminKeyPair: GroupKeyPair, remainingMembers: Record<string, string>, newGek?: string): Promise<{
99
+ keyring: GroupKeyring;
100
+ gek: string;
101
+ }>;
102
+ /**
103
+ * Creates an Encryptor that can decrypt any epoch and encrypts with the current epoch.
104
+ *
105
+ * Wire format: `{ _encrypted: "base64(IV || ciphertext)", _epoch: N }`
106
+ *
107
+ * @param keyring The keyring document fetched from Starfish
108
+ * @param myIdentity The caller's userId (to locate their wrapped key in each epoch)
109
+ * @param myPrivateKey The caller's hex-encoded X25519 private key
110
+ */
111
+ export declare function createGroupEncryptor(keyring: GroupKeyring, myIdentity: string, myPrivateKey: string): Promise<Encryptor>;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Group encryption utilities for Starfish.
3
+ *
4
+ * Enables multiple users to share a common encrypted collection without sharing
5
+ * a passphrase. Each member holds their own credentials; a Group Encryption Key
6
+ * (GEK) is distributed per-member using X25519 ECDH key agreement.
7
+ *
8
+ * Typical flow:
9
+ * 1. Each user calls `deriveCredentials(passphrase)` — now includes groupPublicKey / groupPrivateKey.
10
+ * 2. Admin calls `createGroupKeyring(...)` to create a keyring document.
11
+ * 3. Members call `createGroupEncryptor(keyringData, myIdentity, myPrivateKey)` to get an Encryptor.
12
+ * 4. The Encryptor is passed to SyncManager via the `encryptor` option.
13
+ */
14
+ import { x25519 } from "@noble/curves/ed25519.js";
15
+ import { getCrypto, getBase64, IV_BYTES, deriveKey } from "@drakkar.software/starfish-protocol";
16
+ import { createEncryptor } from "./crypto.js";
17
+ // ── Internal helpers ──────────────────────────────────────────────────────────
18
+ function bytesToHex(bytes) {
19
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
20
+ }
21
+ function hexToBytes(hex) {
22
+ const bytes = new Uint8Array(hex.length / 2);
23
+ for (let i = 0; i < bytes.length; i++) {
24
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
25
+ }
26
+ return bytes;
27
+ }
28
+ const ALGO = "AES-GCM";
29
+ const GROUP_WRAP_SALT = "starfish-group-wrap";
30
+ const GROUP_WRAP_INFO = "starfish-group-wrap";
31
+ const GROUP_ECDH_DOMAIN = "starfish-group-ecdh";
32
+ const GROUP_DATA_INFO = "starfish-group";
33
+ const GEK_BYTES = 32;
34
+ // ── Key derivation ────────────────────────────────────────────────────────────
35
+ /**
36
+ * Derives a deterministic X25519 key pair from a passphrase + userId.
37
+ *
38
+ * The derivation uses SHA-256 with a fixed domain separator so it is distinct
39
+ * from the auth token and encryption key derivations. Same passphrase + userId
40
+ * always produces the same key pair on any device (stateless).
41
+ */
42
+ export async function deriveGroupKeyPair(passphrase, userId) {
43
+ const c = getCrypto();
44
+ const enc = new TextEncoder();
45
+ const input = enc.encode(`${passphrase}:${userId}:${GROUP_ECDH_DOMAIN}`);
46
+ const hash = await c.subtle.digest("SHA-256", input);
47
+ const privateKeyBytes = new Uint8Array(hash);
48
+ const publicKeyBytes = x25519.getPublicKey(privateKeyBytes);
49
+ return { privateKey: bytesToHex(privateKeyBytes), publicKey: bytesToHex(publicKeyBytes) };
50
+ }
51
+ // ── GEK generation ────────────────────────────────────────────────────────────
52
+ /** Generates a random 256-bit Group Encryption Key as a hex string. */
53
+ export function generateGroupKey() {
54
+ const c = getCrypto();
55
+ return bytesToHex(c.getRandomValues(new Uint8Array(GEK_BYTES)));
56
+ }
57
+ // ── Key wrapping / unwrapping ─────────────────────────────────────────────────
58
+ /**
59
+ * Wraps a GEK for a specific member using ECDH key agreement.
60
+ *
61
+ * The wrapper (admin) and member each have an X25519 key pair. ECDH between
62
+ * `wrapperPrivateKey` and `memberPublicKey` produces a shared secret, which is
63
+ * used to derive an AES-256-GCM key that encrypts the GEK.
64
+ *
65
+ * @returns base64(IV || AES-GCM-ciphertext)
66
+ */
67
+ export async function wrapGroupKey(gek, memberPublicKey, wrapperPrivateKey) {
68
+ const sharedSecret = x25519.getSharedSecret(hexToBytes(wrapperPrivateKey), hexToBytes(memberPublicKey));
69
+ const wrappingKey = await deriveKey(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO);
70
+ const c = getCrypto();
71
+ const b64 = getBase64();
72
+ const iv = c.getRandomValues(new Uint8Array(IV_BYTES));
73
+ const encrypted = await c.subtle.encrypt({ name: ALGO, iv }, wrappingKey, hexToBytes(gek).buffer);
74
+ const combined = new Uint8Array(IV_BYTES + encrypted.byteLength);
75
+ combined.set(iv);
76
+ combined.set(new Uint8Array(encrypted), IV_BYTES);
77
+ return b64.encode(combined);
78
+ }
79
+ /**
80
+ * Unwraps a GEK using the member's own private key and the admin's public key.
81
+ *
82
+ * ECDH between `memberPrivateKey` and `adminPublicKey` yields the same shared
83
+ * secret as the wrapping step, so the same AES key is derived and the GEK is
84
+ * recovered.
85
+ *
86
+ * @returns GEK as a hex string
87
+ */
88
+ export async function unwrapGroupKey(wrapped, memberPrivateKey, adminPublicKey) {
89
+ const sharedSecret = x25519.getSharedSecret(hexToBytes(memberPrivateKey), hexToBytes(adminPublicKey));
90
+ const wrappingKey = await deriveKey(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO);
91
+ const b64 = getBase64();
92
+ const c = getCrypto();
93
+ const combined = b64.decode(wrapped);
94
+ const iv = combined.slice(0, IV_BYTES);
95
+ const ciphertext = combined.slice(IV_BYTES);
96
+ try {
97
+ const decrypted = await c.subtle.decrypt({ name: ALGO, iv }, wrappingKey, ciphertext);
98
+ return bytesToHex(new Uint8Array(decrypted));
99
+ }
100
+ catch {
101
+ throw new Error("Failed to unwrap group key: decryption failed (wrong keys or corrupted data)");
102
+ }
103
+ }
104
+ // ── Keyring management ────────────────────────────────────────────────────────
105
+ /**
106
+ * Creates a new group keyring document with epoch 1.
107
+ *
108
+ * @param adminKeyPair The admin's key pair (from `deriveGroupKeyPair` or `deriveCredentials`)
109
+ * @param members Map from member identity (userId) → hex public key
110
+ * @param gek Optional GEK to use; generated randomly if omitted
111
+ * @returns The keyring document and the raw GEK (admin keeps the GEK to add future members)
112
+ */
113
+ export async function createGroupKeyring(adminKeyPair, members, gek) {
114
+ const resolvedGek = gek ?? generateGroupKey();
115
+ const wrappedKeys = {};
116
+ for (const [memberId, memberPublicKey] of Object.entries(members)) {
117
+ wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey);
118
+ }
119
+ const keyring = {
120
+ currentEpoch: 1,
121
+ epochs: {
122
+ "1": { adminPublicKey: adminKeyPair.publicKey, wrappedKeys },
123
+ },
124
+ };
125
+ return { keyring, gek: resolvedGek };
126
+ }
127
+ /**
128
+ * Adds a new member to the current epoch of an existing keyring.
129
+ *
130
+ * The admin supplies the current GEK (returned by `createGroupKeyring` or
131
+ * `rotateGroupKey`) and their key pair to wrap it for the new member.
132
+ * This does NOT rotate the GEK — the new member can read all existing
133
+ * documents encrypted with the current epoch key.
134
+ *
135
+ * Only the admin (whose `publicKey` matches `epochKeyring.adminPublicKey`) can
136
+ * add members, because all wrapped entries must use the same ECDH key pair.
137
+ */
138
+ export async function addGroupMember(keyring, adminKeyPair, currentGek, newMemberId, newMemberPublicKey) {
139
+ const epochKey = String(keyring.currentEpoch);
140
+ const epochKeyring = keyring.epochs[epochKey];
141
+ if (!epochKeyring)
142
+ throw new Error(`Epoch ${keyring.currentEpoch} not found in keyring`);
143
+ if (epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {
144
+ throw new Error(`Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`);
145
+ }
146
+ const wrapped = await wrapGroupKey(currentGek, newMemberPublicKey, adminKeyPair.privateKey);
147
+ return {
148
+ ...keyring,
149
+ epochs: {
150
+ ...keyring.epochs,
151
+ [epochKey]: {
152
+ ...epochKeyring,
153
+ wrappedKeys: { ...epochKeyring.wrappedKeys, [newMemberId]: wrapped },
154
+ },
155
+ },
156
+ };
157
+ }
158
+ /**
159
+ * Rotates the group key, creating a new epoch.
160
+ *
161
+ * Used when removing a member. The removed member retains their old epoch key
162
+ * (and can still read old documents), but cannot read new documents.
163
+ *
164
+ * @param remainingMembers Map from identity → hex public key for members who keep access
165
+ */
166
+ export async function rotateGroupKey(keyring, adminKeyPair, remainingMembers, newGek) {
167
+ const epochKey = String(keyring.currentEpoch);
168
+ const epochKeyring = keyring.epochs[epochKey];
169
+ if (epochKeyring && epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {
170
+ throw new Error(`Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`);
171
+ }
172
+ const resolvedGek = newGek ?? generateGroupKey();
173
+ const newEpoch = keyring.currentEpoch + 1;
174
+ const wrappedKeys = {};
175
+ for (const [memberId, memberPublicKey] of Object.entries(remainingMembers)) {
176
+ wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey);
177
+ }
178
+ const newKeyring = {
179
+ currentEpoch: newEpoch,
180
+ epochs: {
181
+ ...keyring.epochs,
182
+ [String(newEpoch)]: { adminPublicKey: adminKeyPair.publicKey, wrappedKeys },
183
+ },
184
+ };
185
+ return { keyring: newKeyring, gek: resolvedGek };
186
+ }
187
+ // ── Encryptor factory ─────────────────────────────────────────────────────────
188
+ /**
189
+ * Creates an Encryptor that can decrypt any epoch and encrypts with the current epoch.
190
+ *
191
+ * Wire format: `{ _encrypted: "base64(IV || ciphertext)", _epoch: N }`
192
+ *
193
+ * @param keyring The keyring document fetched from Starfish
194
+ * @param myIdentity The caller's userId (to locate their wrapped key in each epoch)
195
+ * @param myPrivateKey The caller's hex-encoded X25519 private key
196
+ */
197
+ export async function createGroupEncryptor(keyring, myIdentity, myPrivateKey) {
198
+ // Unwrap GEK for each epoch we have a key for
199
+ const epochEncryptors = new Map();
200
+ for (const [epochStr, epochKeyring] of Object.entries(keyring.epochs)) {
201
+ const epoch = parseInt(epochStr, 10);
202
+ const wrapped = epochKeyring.wrappedKeys[myIdentity];
203
+ if (!wrapped)
204
+ continue;
205
+ const gek = await unwrapGroupKey(wrapped, myPrivateKey, epochKeyring.adminPublicKey);
206
+ epochEncryptors.set(epoch, createEncryptor(gek, `epoch-${epoch}`, GROUP_DATA_INFO));
207
+ }
208
+ const currentEpoch = keyring.currentEpoch;
209
+ const currentEncryptor = epochEncryptors.get(currentEpoch);
210
+ if (!currentEncryptor) {
211
+ throw new Error(`No wrapped key found for identity "${myIdentity}" in epoch ${currentEpoch}. ` +
212
+ `Ensure the admin has added this member to the keyring.`);
213
+ }
214
+ return {
215
+ async encrypt(data) {
216
+ const encrypted = await currentEncryptor.encrypt(data);
217
+ return { ...encrypted, _epoch: currentEpoch };
218
+ },
219
+ async decrypt(wrapper) {
220
+ const epoch = typeof wrapper._epoch === "number" ? wrapper._epoch : currentEpoch;
221
+ const encryptor = epochEncryptors.get(epoch);
222
+ if (!encryptor) {
223
+ throw new Error(`No key available for epoch ${epoch}. ` +
224
+ `This document was encrypted in a different epoch. ` +
225
+ `Ensure your keyring is up to date.`);
226
+ }
227
+ return encryptor.decrypt(wrapper);
228
+ },
229
+ };
230
+ }
@@ -19,6 +19,18 @@ export interface DerivedCredentials {
19
19
  * their encryption keys are different.
20
20
  */
21
21
  encryptionSalt: string;
22
+ /**
23
+ * Hex-encoded X25519 public key for group encryption.
24
+ * Publish this so group admins can wrap the Group Encryption Key (GEK) for you.
25
+ * Safe to store in a public Starfish document.
26
+ */
27
+ groupPublicKey: string;
28
+ /**
29
+ * Hex-encoded X25519 private key for group encryption.
30
+ * Used to unwrap the GEK from a keyring document.
31
+ * Never transmit this — keep it in memory or derive it from the passphrase.
32
+ */
33
+ groupPrivateKey: string;
22
34
  }
23
35
  /**
24
36
  * Generates a cryptographically random passphrase from the built-in 256-word list.
package/dist/identity.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getCrypto, getBase64 } from "@drakkar.software/starfish-protocol";
2
+ import { deriveGroupKeyPair } from "./group-crypto.js";
2
3
  // ── Word list ─────────────────────────────────────────────────────────────────
3
4
  // 256 common English words, one per index (0-255). One byte of entropy per word.
4
5
  // 12 words = 96 bits of entropy (stronger than a random UUID).
@@ -114,11 +115,15 @@ export async function deriveCredentials(passphrase) {
114
115
  // encryptionSecret = SHA-256(passphrase + ":" + userId)
115
116
  // Domain separation from authToken ensures they cannot be recovered from each other.
116
117
  const encryptionSecret = await sha256Hex(`${passphrase}:${userId}`);
118
+ // X25519 key pair for group encryption — deterministically derived from passphrase.
119
+ const { publicKey: groupPublicKey, privateKey: groupPrivateKey } = await deriveGroupKeyPair(passphrase, userId);
117
120
  return {
118
121
  authToken,
119
122
  userId,
120
123
  encryptionSecret,
121
124
  encryptionSalt: userId,
125
+ groupPublicKey,
126
+ groupPrivateKey,
122
127
  };
123
128
  }
124
129
  /**
package/dist/index.d.ts CHANGED
@@ -42,3 +42,7 @@ export { createMobileLifecycle } from "./mobile-lifecycle.js";
42
42
  export type { AppStateModule, NetInfoModule, MobileLifecycleDeps, MobileLifecycleOptions } from "./mobile-lifecycle.js";
43
43
  export { createMultiStoreSync } from "./multi-store.js";
44
44
  export type { StoreSlice, BackupDocument, MultiStoreMigrationFn, MultiStoreSyncOptions, MultiStoreSync, } from "./multi-store.js";
45
+ export { deriveGroupKeyPair, generateGroupKey, wrapGroupKey, unwrapGroupKey, createGroupKeyring, addGroupMember, rotateGroupKey, createGroupEncryptor, } from "./group-crypto.js";
46
+ export type { GroupKeyPair, EpochKeyring, GroupKeyring } from "./group-crypto.js";
47
+ export { pullEntitlements } from "./entitlements.js";
48
+ export type { PullEntitlementsOptions } from "./entitlements.js";
package/dist/index.js CHANGED
@@ -21,3 +21,5 @@ export { createSuspenseResource } from "./bindings/suspense.js";
21
21
  export { createDebouncedSync, createDebouncedPush } from "./debounced-sync.js";
22
22
  export { createMobileLifecycle } from "./mobile-lifecycle.js";
23
23
  export { createMultiStoreSync } from "./multi-store.js";
24
+ export { deriveGroupKeyPair, generateGroupKey, wrapGroupKey, unwrapGroupKey, createGroupKeyring, addGroupMember, rotateGroupKey, createGroupEncryptor, } from "./group-crypto.js";
25
+ export { pullEntitlements } from "./entitlements.js";
package/dist/sync.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { PullResult } from "@drakkar.software/starfish-protocol";
2
2
  import type { ConflictResolver } from "./types.js";
3
3
  import { StarfishClient } from "./client.js";
4
+ import type { Encryptor } from "./crypto.js";
4
5
  import type { SyncLogger } from "./logger.js";
5
6
  import type { Validator } from "./validate.js";
6
7
  export interface SyncManagerOptions {
@@ -14,6 +15,11 @@ export interface SyncManagerOptions {
14
15
  encryptionSecret?: string;
15
16
  encryptionSalt?: string;
16
17
  encryptionInfo?: string;
18
+ /**
19
+ * Pre-created Encryptor. Use this with `createGroupEncryptor` for group encryption.
20
+ * Takes precedence over `encryptionSecret` / `encryptionSalt` if both are provided.
21
+ */
22
+ encryptor?: Encryptor;
17
23
  signData?: (data: string) => Promise<string>;
18
24
  /** Structured logger for sync events. */
19
25
  logger?: SyncLogger;
package/dist/sync.js CHANGED
@@ -27,9 +27,10 @@ export class SyncManager {
27
27
  this.loggerName = options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
28
28
  this.validate = options.validate;
29
29
  this.encryptor =
30
- options.encryptionSecret && options.encryptionSalt
31
- ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)
32
- : null;
30
+ options.encryptor ??
31
+ (options.encryptionSecret && options.encryptionSalt
32
+ ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)
33
+ : null);
33
34
  }
34
35
  getData() {
35
36
  return { ...this.localData };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakkar.software/starfish-client",
3
- "version": "1.14.0",
3
+ "version": "1.17.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/Drakkar-Software/starfish.git",
@@ -41,6 +41,10 @@
41
41
  "./identity": {
42
42
  "types": "./dist/identity.d.ts",
43
43
  "import": "./dist/identity.js"
44
+ },
45
+ "./group": {
46
+ "types": "./dist/group-crypto.d.ts",
47
+ "import": "./dist/group-crypto.js"
44
48
  }
45
49
  },
46
50
  "peerDependencies": {
@@ -64,7 +68,8 @@
64
68
  }
65
69
  },
66
70
  "dependencies": {
67
- "@drakkar.software/starfish-protocol": "1.14.0"
71
+ "@noble/curves": "^2.2.0",
72
+ "@drakkar.software/starfish-protocol": "1.17.0"
68
73
  },
69
74
  "devDependencies": {
70
75
  "@legendapp/state": "^2.0.0",