@drakkar.software/starfish-client 3.0.0-alpha.21 → 3.0.0-alpha.23

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/dist/sync.js CHANGED
@@ -1,7 +1,13 @@
1
- import { deepMerge, stableStringify } from "@drakkar.software/starfish-protocol";
1
+ import { AUTHOR_PUBKEY_FIELD, AUTHOR_SIGNATURE_FIELD, deepMerge, docAuthorCanonicalInput, getBase64, } from "@drakkar.software/starfish-protocol";
2
2
  import { ConflictError } from "./types.js";
3
- import { createEncryptor } from "./crypto.js";
3
+ import { stripPushPrefix } from "./client.js";
4
4
  import { ValidationError } from "./validate.js";
5
+ export class AbortError extends Error {
6
+ constructor() {
7
+ super("SyncManager was aborted");
8
+ this.name = "AbortError";
9
+ }
10
+ }
5
11
  export class SyncManager {
6
12
  client;
7
13
  pullPath;
@@ -9,28 +15,31 @@ export class SyncManager {
9
15
  onConflict;
10
16
  maxRetries;
11
17
  encryptor;
12
- signData;
18
+ signer;
13
19
  logger;
14
20
  loggerName;
15
21
  validate;
16
22
  lastHash = null;
17
23
  lastCheckpoint = 0;
18
24
  localData = {};
25
+ aborted = false;
19
26
  constructor(options) {
20
27
  this.client = options.client;
21
28
  this.pullPath = options.pullPath;
22
29
  this.pushPath = options.pushPath;
23
30
  this.onConflict = options.onConflict ?? deepMerge;
24
31
  this.maxRetries = options.maxRetries ?? 3;
25
- this.signData = options.signData;
32
+ this.signer = options.signer;
26
33
  this.logger = options.logger;
27
34
  this.loggerName = options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
28
35
  this.validate = options.validate;
29
- this.encryptor =
30
- options.encryptor ??
31
- (options.encryptionSecret && options.encryptionSalt
32
- ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)
33
- : null);
36
+ this.encryptor = options.encryptor ?? null;
37
+ }
38
+ abort() {
39
+ this.aborted = true;
40
+ }
41
+ get isAborted() {
42
+ return this.aborted;
34
43
  }
35
44
  getData() {
36
45
  return { ...this.localData };
@@ -38,16 +47,31 @@ export class SyncManager {
38
47
  getHash() {
39
48
  return this.lastHash;
40
49
  }
50
+ /** Set the last-known server hash. Used by persistence layers to restore state across restarts. */
51
+ setHash(hash) {
52
+ this.lastHash = hash;
53
+ }
41
54
  getCheckpoint() {
42
55
  return this.lastCheckpoint;
43
56
  }
44
57
  async pull() {
58
+ if (this.aborted)
59
+ throw new AbortError();
45
60
  this.logger?.pullStart(this.loggerName);
46
61
  const start = performance.now();
47
62
  try {
63
+ // NOTE: `SyncManager.pull` does NOT auto-enable `withKeyring`. Clients
64
+ // that drive the keyring helpers from `recipients.ts` and want to save
65
+ // the cold-start round-trip should call `client.pull(path, {withKeyring: true})`
66
+ // directly. We keep `SyncManager` keyring-agnostic so it stays usable
67
+ // for collections that don't use delegated encryption.
48
68
  const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
69
+ if (this.aborted)
70
+ throw new AbortError();
49
71
  if (this.encryptor) {
50
72
  const decrypted = await this.encryptor.decrypt(result.data);
73
+ if (this.aborted)
74
+ throw new AbortError();
51
75
  this.localData = decrypted;
52
76
  result.data = decrypted;
53
77
  }
@@ -69,6 +93,8 @@ export class SyncManager {
69
93
  }
70
94
  }
71
95
  async push(data) {
96
+ if (this.aborted)
97
+ throw new AbortError();
72
98
  if (this.validate) {
73
99
  const result = this.validate(data);
74
100
  if (result !== true)
@@ -80,13 +106,33 @@ export class SyncManager {
80
106
  let pendingData = data;
81
107
  while (attempt <= this.maxRetries) {
82
108
  try {
83
- const payload = this.encryptor
109
+ const sealed = this.encryptor
84
110
  ? await this.encryptor.encrypt(pendingData)
85
111
  : pendingData;
86
- const sig = this.signData
87
- ? await this.signData(stableStringify(payload))
88
- : undefined;
89
- const result = await this.client.push(this.pushPath, payload, this.lastHash, sig);
112
+ if (this.aborted)
113
+ throw new AbortError();
114
+ // v3.0 signer path: sign the document author proof over the doc-author
115
+ // canonical input (domain-tagged, bound to documentKey) and pass it as
116
+ // top-level body siblings of `data` (NOT inside `data`), where the server
117
+ // verifies it and stores the raw author pubkey.
118
+ let author;
119
+ if (this.signer) {
120
+ const { devEdPubHex, sign } = await this.signer.getSigner();
121
+ if (this.aborted)
122
+ throw new AbortError();
123
+ const documentKey = stripPushPrefix(this.pushPath);
124
+ const canonical = docAuthorCanonicalInput(documentKey, sealed);
125
+ const sigBytes = await sign(new TextEncoder().encode(canonical));
126
+ if (this.aborted)
127
+ throw new AbortError();
128
+ author = {
129
+ [AUTHOR_PUBKEY_FIELD]: devEdPubHex,
130
+ [AUTHOR_SIGNATURE_FIELD]: getBase64().encode(sigBytes),
131
+ };
132
+ }
133
+ const result = await this.client.push(this.pushPath, sealed, this.lastHash, author);
134
+ if (this.aborted)
135
+ throw new AbortError();
90
136
  this.lastHash = result.hash;
91
137
  this.lastCheckpoint = result.timestamp;
92
138
  this.localData = pendingData;
@@ -94,6 +140,8 @@ export class SyncManager {
94
140
  return result;
95
141
  }
96
142
  catch (err) {
143
+ if (err instanceof AbortError)
144
+ throw err;
97
145
  if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {
98
146
  this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err));
99
147
  throw err;
@@ -101,14 +149,20 @@ export class SyncManager {
101
149
  this.logger?.conflict(this.loggerName, attempt + 1);
102
150
  try {
103
151
  const remote = await this.client.pull(this.pullPath);
152
+ if (this.aborted)
153
+ throw new AbortError();
104
154
  const remoteData = this.encryptor
105
155
  ? await this.encryptor.decrypt(remote.data)
106
156
  : remote.data;
157
+ if (this.aborted)
158
+ throw new AbortError();
107
159
  this.lastHash = remote.hash;
108
160
  this.lastCheckpoint = remote.timestamp;
109
161
  pendingData = this.onConflict(pendingData, remoteData);
110
162
  }
111
163
  catch (resolveErr) {
164
+ if (resolveErr instanceof AbortError)
165
+ throw resolveErr;
112
166
  const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr);
113
167
  this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`);
114
168
  throw resolveErr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakkar.software/starfish-client",
3
- "version": "3.0.0-alpha.21",
3
+ "version": "3.0.0-alpha.23",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/Drakkar-Software/starfish.git",
@@ -60,7 +60,7 @@
60
60
  }
61
61
  },
62
62
  "dependencies": {
63
- "@drakkar.software/starfish-protocol": "3.0.0-alpha.21"
63
+ "@drakkar.software/starfish-protocol": "3.0.0-alpha.23"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@legendapp/state": "^2.0.0",
package/dist/append.d.ts DELETED
@@ -1,50 +0,0 @@
1
- import type { StarfishClient } from "./client.js";
2
- import type { PushSuccess } from "@drakkar.software/starfish-protocol";
3
- /**
4
- * Appends `item` to an append-only collection.
5
- *
6
- * Sends `{ data: item, baseHash: null }` — the server ignores `baseHash` for
7
- * append-only collections (conflict detection is disabled or delegated to
8
- * `checkLastItem` on the server side).
9
- *
10
- * ```ts
11
- * await pushAppend(client, "/push/events", { type: "click", ts: Date.now() })
12
- * ```
13
- */
14
- export declare function pushAppend(client: StarfishClient, path: string, item: Record<string, unknown>): Promise<PushSuccess>;
15
- export interface PullAppendListOptions {
16
- /** Array field name. Defaults to `"items"` (server default). */
17
- field?: string;
18
- /**
19
- * Return only items appended after this timestamp (milliseconds since epoch).
20
- * Sent as `?checkpoint=<since>`. Omit for a full pull.
21
- */
22
- since?: number;
23
- /**
24
- * Return only the last K items. Applied after the `since` filter.
25
- * Useful for "latest N entries" queries without a tracked checkpoint.
26
- * Sent as `?last=<K>`.
27
- */
28
- last?: number;
29
- }
30
- /**
31
- * Pulls the stored item array from an append-only collection.
32
- *
33
- * Returns `data[field]` filtered to an array; returns `[]` when the document
34
- * does not exist yet or the field is absent / not an array.
35
- *
36
- * Pass `{ since: ts }` for incremental pulls — only items appended after `ts`
37
- * are returned (requires per-item timestamps on the server, available from 2.0.0).
38
- *
39
- * ```ts
40
- * // Full pull
41
- * const events = await pullAppendList(client, "/pull/events")
42
- *
43
- * // Incremental pull
44
- * const newEvents = await pullAppendList(client, "/pull/events", { since: lastSyncTs })
45
- *
46
- * // Custom field name
47
- * const logs = await pullAppendList(client, "/pull/audit", { field: "logs" })
48
- * ```
49
- */
50
- export declare function pullAppendList<T = unknown>(client: StarfishClient, path: string, options?: PullAppendListOptions): Promise<T[]>;
@@ -1,19 +0,0 @@
1
- import type { StoreApi } from "zustand/vanilla";
2
- import type { StarfishStore } from "./zustand.js";
3
- /**
4
- * Syncs a Zustand Starfish store across browser tabs using BroadcastChannel.
5
- * Returns a cleanup function that closes the channel.
6
- */
7
- export declare function setupBroadcastSync(store: StoreApi<StarfishStore>, name: string): () => void;
8
- /**
9
- * Syncs a Zustand Starfish store across browser tabs using storage events.
10
- * Fallback for environments without BroadcastChannel.
11
- * Returns a cleanup function.
12
- */
13
- export declare function setupStorageFallback(store: StoreApi<StarfishStore>, name: string): () => void;
14
- /**
15
- * Auto-detects the best cross-tab sync mechanism and sets it up.
16
- * Uses BroadcastChannel when available, falls back to storage events.
17
- * Returns a cleanup function.
18
- */
19
- export declare function setupCrossTabSync(store: StoreApi<StarfishStore>, name: string): () => void;
@@ -1,65 +0,0 @@
1
- /**
2
- * Syncs a Zustand Starfish store across browser tabs using BroadcastChannel.
3
- * Returns a cleanup function that closes the channel.
4
- */
5
- export function setupBroadcastSync(store, name) {
6
- const channel = new BroadcastChannel(`starfish-${name}`);
7
- let lastReceivedData = null;
8
- channel.onmessage = (event) => {
9
- lastReceivedData = event.data.data;
10
- store.setState({ data: event.data.data, dirty: event.data.dirty });
11
- };
12
- const unsub = store.subscribe((state, prev) => {
13
- if (state.data === lastReceivedData)
14
- return;
15
- if (state.data !== prev.data || state.dirty !== prev.dirty) {
16
- channel.postMessage({ data: state.data, dirty: state.dirty });
17
- }
18
- });
19
- return () => {
20
- unsub();
21
- channel.close();
22
- };
23
- }
24
- /**
25
- * Syncs a Zustand Starfish store across browser tabs using storage events.
26
- * Fallback for environments without BroadcastChannel.
27
- * Returns a cleanup function.
28
- */
29
- export function setupStorageFallback(store, name) {
30
- const storageKey = `starfish-broadcast-${name}`;
31
- let lastReceivedData = null;
32
- const onStorage = (e) => {
33
- if (e.key !== storageKey || !e.newValue)
34
- return;
35
- const payload = JSON.parse(e.newValue);
36
- lastReceivedData = payload.data;
37
- store.setState({ data: payload.data, dirty: payload.dirty });
38
- };
39
- globalThis.addEventListener("storage", onStorage);
40
- const unsub = store.subscribe((state, prev) => {
41
- if (state.data === lastReceivedData)
42
- return;
43
- if (state.data !== prev.data || state.dirty !== prev.dirty) {
44
- localStorage.setItem(storageKey, JSON.stringify({ data: state.data, dirty: state.dirty }));
45
- }
46
- });
47
- return () => {
48
- unsub();
49
- globalThis.removeEventListener("storage", onStorage);
50
- };
51
- }
52
- /**
53
- * Auto-detects the best cross-tab sync mechanism and sets it up.
54
- * Uses BroadcastChannel when available, falls back to storage events.
55
- * Returns a cleanup function.
56
- */
57
- export function setupCrossTabSync(store, name) {
58
- if (typeof BroadcastChannel !== "undefined") {
59
- return setupBroadcastSync(store, name);
60
- }
61
- if (typeof globalThis.addEventListener === "function" && typeof localStorage !== "undefined") {
62
- return setupStorageFallback(store, name);
63
- }
64
- return () => { };
65
- }
@@ -1,12 +0,0 @@
1
- import type { StoreApi } from "zustand/vanilla";
2
- import type { StarfishStore, StarfishState } from "./zustand.js";
3
- /** Derived sync status for UI display. */
4
- export type SyncStatus = "synced" | "syncing" | "pending" | "error" | "offline";
5
- /** Derive a single sync status from store state. */
6
- export declare function deriveSyncStatus(state: StarfishState): SyncStatus;
7
- /** Use the full Starfish store state and actions. */
8
- export declare function useStarfish(store: StoreApi<StarfishStore>): StarfishStore;
9
- /** Use only the synced data, with an optional selector for fine-grained subscriptions. */
10
- export declare function useStarfishData<T = Record<string, unknown>>(store: StoreApi<StarfishStore>, selector?: (data: Record<string, unknown>) => T): T;
11
- /** Use the derived sync status (synced | syncing | pending | error | offline). */
12
- export declare function useSyncStatus(store: StoreApi<StarfishStore>): SyncStatus;
@@ -1,25 +0,0 @@
1
- import { useStore } from "zustand";
2
- /** Derive a single sync status from store state. */
3
- export function deriveSyncStatus(state) {
4
- if (!state.online)
5
- return "offline";
6
- if (state.error)
7
- return "error";
8
- if (state.syncing)
9
- return "syncing";
10
- if (state.dirty)
11
- return "pending";
12
- return "synced";
13
- }
14
- /** Use the full Starfish store state and actions. */
15
- export function useStarfish(store) {
16
- return useStore(store);
17
- }
18
- /** Use only the synced data, with an optional selector for fine-grained subscriptions. */
19
- export function useStarfishData(store, selector) {
20
- return useStore(store, (state) => selector ? selector(state.data) : state.data);
21
- }
22
- /** Use the derived sync status (synced | syncing | pending | error | offline). */
23
- export function useSyncStatus(store) {
24
- return useStore(store, deriveSyncStatus);
25
- }
package/dist/crypto.js DELETED
@@ -1,49 +0,0 @@
1
- import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from "@drakkar.software/starfish-protocol";
2
- const ALGO = "AES-GCM";
3
- export { ENCRYPTED_KEY };
4
- /**
5
- * Creates an Encryptor that uses AES-256-GCM with HKDF-derived keys.
6
- */
7
- export function createEncryptor(secret, salt, info = "starfish-e2e") {
8
- if (!secret)
9
- throw new Error("encryptionSecret must not be empty");
10
- if (!salt)
11
- throw new Error("encryptionSalt must not be empty");
12
- const keyPromise = deriveKey(secret, salt, info);
13
- return {
14
- async encrypt(data) {
15
- const key = await keyPromise;
16
- const c = getCrypto();
17
- const b64 = getBase64();
18
- const plaintext = new TextEncoder().encode(JSON.stringify(data));
19
- const iv = c.getRandomValues(new Uint8Array(IV_BYTES));
20
- const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext);
21
- const combined = new Uint8Array(iv.length + ciphertext.byteLength);
22
- combined.set(iv);
23
- combined.set(new Uint8Array(ciphertext), iv.length);
24
- return { [ENCRYPTED_KEY]: b64.encode(combined) };
25
- },
26
- async decrypt(wrapper) {
27
- const encoded = wrapper[ENCRYPTED_KEY];
28
- if (typeof encoded !== "string") {
29
- throw new Error("Expected encrypted data but received unencrypted document");
30
- }
31
- const key = await keyPromise;
32
- const c = getCrypto();
33
- const b64 = getBase64();
34
- const combined = b64.decode(encoded);
35
- if (combined.length < IV_BYTES) {
36
- throw new Error("Encrypted data is too short");
37
- }
38
- const iv = combined.slice(0, IV_BYTES);
39
- const ciphertext = combined.slice(IV_BYTES);
40
- try {
41
- const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext);
42
- return JSON.parse(new TextDecoder().decode(plaintext));
43
- }
44
- catch (err) {
45
- throw new Error("Decryption failed: data may be tampered or key is incorrect", { cause: err });
46
- }
47
- },
48
- };
49
- }
@@ -1,41 +0,0 @@
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
- }
@@ -1,111 +0,0 @@
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>;