@drakkar.software/starfish-client 1.3.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.
@@ -0,0 +1,25 @@
1
+ import type { Observable } from "@legendapp/state";
2
+ import type { SyncManager } from "../sync.js";
3
+ export interface StarfishLegendState {
4
+ data: Record<string, unknown>;
5
+ syncing: boolean;
6
+ online: boolean;
7
+ dirty: boolean;
8
+ error: string | null;
9
+ }
10
+ export interface StarfishLegendStore {
11
+ /** The observable state tree — read fields with `.get()` inside `observer` components. */
12
+ state: Observable<StarfishLegendState>;
13
+ pull: () => Promise<void>;
14
+ set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void;
15
+ flush: () => Promise<void>;
16
+ setOnline: (online: boolean) => void;
17
+ }
18
+ export interface CreateStarfishObservableOptions {
19
+ /** Unique name for this collection (used for persistence keys when applicable). */
20
+ name: string;
21
+ syncManager: SyncManager;
22
+ /** Pass `produce` from `immer` to enable draft-based mutations in `set()`. */
23
+ produce?: <T>(base: T, recipe: (draft: T) => T | void) => T;
24
+ }
25
+ export declare function createStarfishObservable(options: CreateStarfishObservableOptions): StarfishLegendStore;
@@ -0,0 +1,63 @@
1
+ import { observable } from "@legendapp/state";
2
+ export function createStarfishObservable(options) {
3
+ const state = observable({
4
+ data: {},
5
+ syncing: false,
6
+ online: true,
7
+ dirty: false,
8
+ error: null,
9
+ });
10
+ const flush = async () => {
11
+ if (state.syncing.get() || !state.dirty.get())
12
+ return;
13
+ state.syncing.set(true);
14
+ state.error.set(null);
15
+ try {
16
+ await options.syncManager.push(state.data.get());
17
+ state.data.set(options.syncManager.getData());
18
+ state.dirty.set(false);
19
+ }
20
+ catch (err) {
21
+ state.error.set(err.message);
22
+ }
23
+ finally {
24
+ state.syncing.set(false);
25
+ }
26
+ };
27
+ const pull = async () => {
28
+ state.syncing.set(true);
29
+ state.error.set(null);
30
+ try {
31
+ await options.syncManager.pull();
32
+ state.data.set(options.syncManager.getData());
33
+ }
34
+ catch (err) {
35
+ state.error.set(err.message);
36
+ }
37
+ finally {
38
+ state.syncing.set(false);
39
+ }
40
+ };
41
+ const set = (modifier) => {
42
+ try {
43
+ const current = state.data.get();
44
+ const next = options.produce
45
+ ? options.produce(current, modifier)
46
+ : modifier(current);
47
+ state.data.set(next);
48
+ state.dirty.set(true);
49
+ state.error.set(null);
50
+ if (state.online.get())
51
+ flush();
52
+ }
53
+ catch (err) {
54
+ state.error.set(err.message);
55
+ }
56
+ };
57
+ const setOnline = (online) => {
58
+ state.online.set(online);
59
+ if (online && state.dirty.get())
60
+ flush();
61
+ };
62
+ return { state, pull, set, flush, setOnline };
63
+ }
@@ -0,0 +1,30 @@
1
+ import { type StoreApi } from "zustand/vanilla";
2
+ import { type StateStorage, type DevtoolsOptions } from "zustand/middleware";
3
+ import type { SyncManager } from "../sync.js";
4
+ export interface StarfishState {
5
+ data: Record<string, unknown>;
6
+ syncing: boolean;
7
+ online: boolean;
8
+ dirty: boolean;
9
+ error: string | null;
10
+ }
11
+ export interface StarfishActions {
12
+ pull: () => Promise<void>;
13
+ set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void;
14
+ flush: () => Promise<void>;
15
+ setOnline: (online: boolean) => void;
16
+ }
17
+ export type StarfishStore = StarfishState & StarfishActions;
18
+ export interface CreateStarfishStoreOptions {
19
+ /** Unique name used as the persistence key (prefixed with `starfish-`) */
20
+ name: string;
21
+ syncManager: SyncManager;
22
+ /** Pass `false` to disable persistence. Defaults to `localStorage` in browsers. */
23
+ storage?: StateStorage | false;
24
+ /** Enable Redux DevTools. Pass `true` or a `DevtoolsOptions` object. */
25
+ devtools?: boolean | DevtoolsOptions;
26
+ /** Pass `produce` from `immer` to enable draft-based mutations in `set()`. */
27
+ produce?: <T>(base: T, recipe: (draft: T) => T | void) => T;
28
+ }
29
+ export type { DevtoolsOptions };
30
+ export declare function createStarfishStore(options: CreateStarfishStoreOptions): StoreApi<StarfishStore>;
@@ -0,0 +1,73 @@
1
+ import { createStore } from "zustand/vanilla";
2
+ import { persist, devtools, subscribeWithSelector, createJSONStorage, } from "zustand/middleware";
3
+ export function createStarfishStore(options) {
4
+ const { name, syncManager, storage } = options;
5
+ const storeCreator = (rawSet, get) => {
6
+ const set = rawSet;
7
+ return {
8
+ data: {},
9
+ syncing: false,
10
+ online: true,
11
+ dirty: false,
12
+ error: null,
13
+ pull: async () => {
14
+ set({ syncing: true, error: null }, false, "pull/start");
15
+ try {
16
+ await syncManager.pull();
17
+ set({ data: syncManager.getData(), syncing: false }, false, "pull/success");
18
+ }
19
+ catch (err) {
20
+ set({ syncing: false, error: err.message }, false, "pull/error");
21
+ }
22
+ },
23
+ set: (modifier) => {
24
+ try {
25
+ const next = options.produce
26
+ ? options.produce(get().data, modifier)
27
+ : modifier(get().data);
28
+ set({ data: next, dirty: true, error: null }, false, "set");
29
+ if (get().online)
30
+ get().flush();
31
+ }
32
+ catch (err) {
33
+ set({ error: err.message }, false, "set/error");
34
+ }
35
+ },
36
+ flush: async () => {
37
+ if (get().syncing || !get().dirty)
38
+ return;
39
+ set({ syncing: true, error: null }, false, "flush/start");
40
+ try {
41
+ await syncManager.push(get().data);
42
+ set({ data: syncManager.getData(), syncing: false, dirty: false }, false, "flush/success");
43
+ }
44
+ catch (err) {
45
+ set({ syncing: false, error: err.message }, false, "flush/error");
46
+ }
47
+ },
48
+ setOnline: (online) => {
49
+ set({ online }, false, "setOnline");
50
+ if (online && get().dirty)
51
+ get().flush();
52
+ },
53
+ };
54
+ };
55
+ const withPersist = storage === false
56
+ ? storeCreator
57
+ : persist(storeCreator, {
58
+ name: `starfish-${name}`,
59
+ storage: storage ? createJSONStorage(() => storage) : undefined,
60
+ partialize: (state) => ({
61
+ data: state.data,
62
+ dirty: state.dirty,
63
+ }),
64
+ });
65
+ const withSelector = subscribeWithSelector(withPersist);
66
+ if (options.devtools) {
67
+ const devtoolsOpts = typeof options.devtools === "object"
68
+ ? options.devtools
69
+ : { name: `starfish-${name}` };
70
+ return createStore()(devtools(withSelector, devtoolsOpts));
71
+ }
72
+ return createStore()(withSelector);
73
+ }
@@ -0,0 +1,27 @@
1
+ import type { PullResult, PushSuccess } from "@drakkar.software/starfish-protocol";
2
+ import type { StarfishClientOptions } from "./types.js";
3
+ /**
4
+ * Low-level HTTP client for the Starfish sync protocol.
5
+ * Handles auth headers and response parsing.
6
+ */
7
+ export declare class StarfishClient {
8
+ private readonly baseUrl;
9
+ private readonly auth?;
10
+ private readonly fetch;
11
+ constructor(options: StarfishClientOptions);
12
+ /**
13
+ * Pull synced data from the server.
14
+ * @param path - The pull endpoint path (e.g. "/pull/users/abc/settings")
15
+ * @param checkpoint - Only return data updated after this timestamp (0 = full pull)
16
+ */
17
+ pull(path: string, checkpoint?: number): Promise<PullResult>;
18
+ /**
19
+ * Push synced data to the server.
20
+ * @param path - The push endpoint path (e.g. "/push/users/abc/settings")
21
+ * @param data - The full document data to push
22
+ * @param baseHash - Hash of the document this push is based on (null for first push)
23
+ * @param authorSignature - Optional author signature for provenance
24
+ * @throws {ConflictError} if the server detects a hash mismatch (409)
25
+ */
26
+ push(path: string, data: Record<string, unknown>, baseHash: string | null, authorSignature?: string): Promise<PushSuccess>;
27
+ }
package/dist/client.js ADDED
@@ -0,0 +1,70 @@
1
+ import { ConflictError, StarfishHttpError } from "./types.js";
2
+ /**
3
+ * Low-level HTTP client for the Starfish sync protocol.
4
+ * Handles auth headers and response parsing.
5
+ */
6
+ export class StarfishClient {
7
+ baseUrl;
8
+ auth;
9
+ fetch;
10
+ constructor(options) {
11
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
12
+ this.auth = options.auth;
13
+ this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
14
+ }
15
+ /**
16
+ * Pull synced data from the server.
17
+ * @param path - The pull endpoint path (e.g. "/pull/users/abc/settings")
18
+ * @param checkpoint - Only return data updated after this timestamp (0 = full pull)
19
+ */
20
+ async pull(path, checkpoint) {
21
+ const url = checkpoint
22
+ ? `${this.baseUrl}${path}?checkpoint=${checkpoint}`
23
+ : `${this.baseUrl}${path}`;
24
+ const authHeaders = this.auth
25
+ ? await this.auth({ method: "GET", path, body: null })
26
+ : {};
27
+ const res = await this.fetch(url, {
28
+ method: "GET",
29
+ headers: { Accept: "application/json", ...authHeaders },
30
+ });
31
+ if (!res.ok) {
32
+ throw new StarfishHttpError(res.status, await res.text());
33
+ }
34
+ return res.json();
35
+ }
36
+ /**
37
+ * Push synced data to the server.
38
+ * @param path - The push endpoint path (e.g. "/push/users/abc/settings")
39
+ * @param data - The full document data to push
40
+ * @param baseHash - Hash of the document this push is based on (null for first push)
41
+ * @param authorSignature - Optional author signature for provenance
42
+ * @throws {ConflictError} if the server detects a hash mismatch (409)
43
+ */
44
+ async push(path, data, baseHash, authorSignature) {
45
+ const body = JSON.stringify({
46
+ data,
47
+ baseHash,
48
+ ...(authorSignature && { authorSignature }),
49
+ });
50
+ const authHeaders = this.auth
51
+ ? await this.auth({ method: "POST", path, body })
52
+ : {};
53
+ const res = await this.fetch(`${this.baseUrl}${path}`, {
54
+ method: "POST",
55
+ headers: {
56
+ "Content-Type": "application/json",
57
+ Accept: "application/json",
58
+ ...authHeaders,
59
+ },
60
+ body,
61
+ });
62
+ if (res.status === 409) {
63
+ throw new ConflictError();
64
+ }
65
+ if (!res.ok) {
66
+ throw new StarfishHttpError(res.status, await res.text());
67
+ }
68
+ return res.json();
69
+ }
70
+ }
@@ -0,0 +1,11 @@
1
+ import { ENCRYPTED_KEY } from "@drakkar.software/starfish-protocol";
2
+ export { ENCRYPTED_KEY };
3
+ /** Encrypt/decrypt interface for client-side E2E encryption. */
4
+ export interface Encryptor {
5
+ encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>>;
6
+ decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>>;
7
+ }
8
+ /**
9
+ * Creates an Encryptor that uses AES-256-GCM with HKDF-derived keys.
10
+ */
11
+ export declare function createEncryptor(secret: string, salt: string, info?: string): Encryptor;
package/dist/crypto.js ADDED
@@ -0,0 +1,49 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ export { configurePlatform } from "@drakkar.software/starfish-protocol";
2
+ export type { CryptoProvider, Base64Provider, PlatformConfig } from "@drakkar.software/starfish-protocol";
3
+ export { stableStringify, computeHash } from "@drakkar.software/starfish-protocol";
4
+ export type { PullResult, PushSuccess } from "@drakkar.software/starfish-protocol";
5
+ export { StarfishClient } from "./client.js";
6
+ export { SyncManager } from "./sync.js";
7
+ export type { SyncManagerOptions } from "./sync.js";
8
+ export { createEncryptor, ENCRYPTED_KEY } from "./crypto.js";
9
+ export type { Encryptor } from "./crypto.js";
10
+ export { ConflictError, StarfishHttpError, } from "./types.js";
11
+ export type { StarfishClientOptions, AuthProvider, ConflictResolver, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { configurePlatform } from "@drakkar.software/starfish-protocol";
2
+ export { stableStringify, computeHash } from "@drakkar.software/starfish-protocol";
3
+ export { StarfishClient } from "./client.js";
4
+ export { SyncManager } from "./sync.js";
5
+ export { createEncryptor, ENCRYPTED_KEY } from "./crypto.js";
6
+ export { ConflictError, StarfishHttpError, } from "./types.js";
package/dist/sync.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { PullResult } from "@drakkar.software/starfish-protocol";
2
+ import type { ConflictResolver } from "./types.js";
3
+ import { StarfishClient } from "./client.js";
4
+ export interface SyncManagerOptions {
5
+ client: StarfishClient;
6
+ pullPath: string;
7
+ pushPath: string;
8
+ /** Custom conflict resolver. Defaults to remote-wins deep merge. Arrays are atomic. */
9
+ onConflict?: ConflictResolver;
10
+ /** Max conflict retry attempts (default: 3). */
11
+ maxRetries?: number;
12
+ encryptionSecret?: string;
13
+ encryptionSalt?: string;
14
+ encryptionInfo?: string;
15
+ signData?: (data: string) => Promise<string>;
16
+ }
17
+ export declare class SyncManager {
18
+ private readonly client;
19
+ private readonly pullPath;
20
+ private readonly pushPath;
21
+ private readonly onConflict;
22
+ private readonly maxRetries;
23
+ private readonly encryptor;
24
+ private readonly signData?;
25
+ private lastHash;
26
+ private lastCheckpoint;
27
+ private localData;
28
+ constructor(options: SyncManagerOptions);
29
+ getData(): Record<string, unknown>;
30
+ getHash(): string | null;
31
+ getCheckpoint(): number;
32
+ pull(): Promise<PullResult>;
33
+ push(data: Record<string, unknown>): Promise<{
34
+ hash: string;
35
+ timestamp: number;
36
+ }>;
37
+ update(modifier: (current: Record<string, unknown>) => Record<string, unknown>): Promise<{
38
+ hash: string;
39
+ timestamp: number;
40
+ }>;
41
+ }
package/dist/sync.js ADDED
@@ -0,0 +1,92 @@
1
+ import { deepMerge, stableStringify } from "@drakkar.software/starfish-protocol";
2
+ import { ConflictError } from "./types.js";
3
+ import { createEncryptor } from "./crypto.js";
4
+ export class SyncManager {
5
+ client;
6
+ pullPath;
7
+ pushPath;
8
+ onConflict;
9
+ maxRetries;
10
+ encryptor;
11
+ signData;
12
+ lastHash = null;
13
+ lastCheckpoint = 0;
14
+ localData = {};
15
+ constructor(options) {
16
+ this.client = options.client;
17
+ this.pullPath = options.pullPath;
18
+ this.pushPath = options.pushPath;
19
+ this.onConflict = options.onConflict ?? deepMerge;
20
+ this.maxRetries = options.maxRetries ?? 3;
21
+ this.signData = options.signData;
22
+ this.encryptor =
23
+ options.encryptionSecret && options.encryptionSalt
24
+ ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)
25
+ : null;
26
+ }
27
+ getData() {
28
+ return { ...this.localData };
29
+ }
30
+ getHash() {
31
+ return this.lastHash;
32
+ }
33
+ getCheckpoint() {
34
+ return this.lastCheckpoint;
35
+ }
36
+ async pull() {
37
+ const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
38
+ if (this.encryptor) {
39
+ const decrypted = await this.encryptor.decrypt(result.data);
40
+ this.localData = decrypted;
41
+ result.data = decrypted;
42
+ }
43
+ else if (this.lastCheckpoint > 0) {
44
+ this.localData = deepMerge(this.localData, result.data);
45
+ }
46
+ else {
47
+ this.localData = result.data;
48
+ }
49
+ this.lastHash = result.hash;
50
+ this.lastCheckpoint = result.timestamp;
51
+ return result;
52
+ }
53
+ async push(data) {
54
+ let attempt = 0;
55
+ let pendingData = data;
56
+ while (attempt <= this.maxRetries) {
57
+ try {
58
+ const payload = this.encryptor
59
+ ? await this.encryptor.encrypt(pendingData)
60
+ : pendingData;
61
+ const sig = this.signData
62
+ ? await this.signData(stableStringify(payload))
63
+ : undefined;
64
+ const result = await this.client.push(this.pushPath, payload, this.lastHash, sig);
65
+ this.lastHash = result.hash;
66
+ this.lastCheckpoint = result.timestamp;
67
+ this.localData = pendingData;
68
+ return result;
69
+ }
70
+ catch (err) {
71
+ if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {
72
+ throw err;
73
+ }
74
+ const remote = await this.client.pull(this.pullPath);
75
+ this.lastHash = remote.hash;
76
+ this.lastCheckpoint = remote.timestamp;
77
+ const remoteData = this.encryptor
78
+ ? await this.encryptor.decrypt(remote.data)
79
+ : remote.data;
80
+ pendingData = this.onConflict(pendingData, remoteData);
81
+ await new Promise(resolve => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2000) + Math.random() * 100));
82
+ attempt++;
83
+ }
84
+ }
85
+ throw new ConflictError();
86
+ }
87
+ async update(modifier) {
88
+ await this.pull();
89
+ const updated = modifier(this.localData);
90
+ return this.push(updated);
91
+ }
92
+ }
@@ -0,0 +1,30 @@
1
+ /** Push conflict error (HTTP 409). */
2
+ export declare class ConflictError extends Error {
3
+ constructor();
4
+ }
5
+ /** HTTP error from the Starfish server. */
6
+ export declare class StarfishHttpError extends Error {
7
+ readonly status: number;
8
+ readonly body: string;
9
+ constructor(status: number, body: string);
10
+ }
11
+ /**
12
+ * Auth provider: returns headers to include in requests.
13
+ * Called for every authenticated request (pull and push).
14
+ */
15
+ export type AuthProvider = (req: {
16
+ method: string;
17
+ path: string;
18
+ body: string | null;
19
+ }) => Record<string, string> | Promise<Record<string, string>>;
20
+ /** Options for creating a StarfishClient. */
21
+ export interface StarfishClientOptions {
22
+ /** Base URL of the Starfish server (e.g. "https://api.example.com/v1"). */
23
+ baseUrl: string;
24
+ /** Auth provider that returns headers for authenticated requests. Optional for public-read collections. */
25
+ auth?: AuthProvider;
26
+ /** Optional fetch implementation (defaults to global fetch). */
27
+ fetch?: typeof fetch;
28
+ }
29
+ /** Conflict resolver: given local and remote data, return merged result. */
30
+ export type ConflictResolver = (local: Record<string, unknown>, remote: Record<string, unknown>) => Record<string, unknown>;
package/dist/types.js ADDED
@@ -0,0 +1,18 @@
1
+ /** Push conflict error (HTTP 409). */
2
+ export class ConflictError extends Error {
3
+ constructor() {
4
+ super("hash_mismatch");
5
+ this.name = "ConflictError";
6
+ }
7
+ }
8
+ /** HTTP error from the Starfish server. */
9
+ export class StarfishHttpError extends Error {
10
+ status;
11
+ body;
12
+ constructor(status, body) {
13
+ super(`HTTP ${status}: ${body}`);
14
+ this.status = status;
15
+ this.body = body;
16
+ this.name = "StarfishHttpError";
17
+ }
18
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@drakkar.software/starfish-client",
3
+ "version": "1.3.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/Drakkar-Software/starfish.git",
7
+ "directory": "packages/ts/client"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "description": "TypeScript client SDK for the Starfish sync protocol",
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./zustand": {
22
+ "types": "./dist/bindings/zustand.d.ts",
23
+ "import": "./dist/bindings/zustand.js"
24
+ },
25
+ "./legend": {
26
+ "types": "./dist/bindings/legend.d.ts",
27
+ "import": "./dist/bindings/legend.js"
28
+ }
29
+ },
30
+ "peerDependencies": {
31
+ "zustand": ">=4.0.0",
32
+ "immer": ">=9.0.0",
33
+ "@legendapp/state": ">=2.0.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "zustand": {
37
+ "optional": true
38
+ },
39
+ "immer": {
40
+ "optional": true
41
+ },
42
+ "@legendapp/state": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "dependencies": {
47
+ "@drakkar.software/starfish-protocol": "workspace:*"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc --build",
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run"
53
+ },
54
+ "devDependencies": {
55
+ "@legendapp/state": "^2.0.0",
56
+ "hono": "^4.12.7",
57
+ "immer": "^11.1.4",
58
+ "typescript": "^5.5.0",
59
+ "vitest": "^3.0.0",
60
+ "zustand": "^5.0.11"
61
+ },
62
+ "files": [
63
+ "dist"
64
+ ]
65
+ }