@drakkar.software/octospaces-sdk 0.1.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.
- package/dist/index.d.ts +972 -0
- package/dist/index.js +1656 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/index.d.ts +9 -0
- package/dist/platform/index.js +111 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/index.native.d.ts +9 -0
- package/dist/platform/index.native.js +106 -0
- package/dist/platform/index.native.js.map +1 -0
- package/package.json +50 -0
- package/src/core/adapters.ts +34 -0
- package/src/core/config.ts +87 -0
- package/src/core/ids.test.ts +45 -0
- package/src/core/ids.ts +29 -0
- package/src/core/space-access-error.ts +13 -0
- package/src/core/storage-types.ts +71 -0
- package/src/core/types.ts +162 -0
- package/src/index.ts +221 -0
- package/src/objects/objects.test.ts +288 -0
- package/src/objects/objects.ts +296 -0
- package/src/platform/index.native.ts +3 -0
- package/src/platform/index.ts +3 -0
- package/src/platform/kv.native.ts +23 -0
- package/src/platform/kv.ts +29 -0
- package/src/platform/platform.native.ts +16 -0
- package/src/platform/platform.ts +10 -0
- package/src/spaces/members.test.ts +87 -0
- package/src/spaces/members.ts +271 -0
- package/src/spaces/object-index.test.ts +105 -0
- package/src/spaces/object-index.ts +160 -0
- package/src/spaces/registry.test.ts +111 -0
- package/src/spaces/registry.ts +466 -0
- package/src/sync/account-seal.test.ts +70 -0
- package/src/sync/account-seal.ts +80 -0
- package/src/sync/base64.ts +89 -0
- package/src/sync/base64url.ts +22 -0
- package/src/sync/client.ts +301 -0
- package/src/sync/fetch-timeout.test.ts +26 -0
- package/src/sync/fetch-timeout.ts +23 -0
- package/src/sync/identity.ts +158 -0
- package/src/sync/pairing.ts +103 -0
- package/src/sync/paths.test.ts +135 -0
- package/src/sync/paths.ts +177 -0
- package/src/sync/profile-cache.ts +34 -0
- package/src/sync/pull-cache.test.ts +55 -0
- package/src/sync/pull-cache.ts +33 -0
- package/src/sync/space-access-store.test.ts +129 -0
- package/src/sync/space-access-store.ts +117 -0
- package/src/sync/space-access.ts +136 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +40 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/platform/platform.native.ts
|
|
2
|
+
import { install } from "react-native-quick-crypto";
|
|
3
|
+
import { configurePlatform } from "@drakkar.software/starfish-protocol";
|
|
4
|
+
|
|
5
|
+
// src/sync/base64.ts
|
|
6
|
+
var CHUNK = 24576;
|
|
7
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
8
|
+
var REVERSE = (() => {
|
|
9
|
+
const table = new Int16Array(128).fill(-1);
|
|
10
|
+
for (let i = 0; i < ALPHABET.length; i++) table[ALPHABET.charCodeAt(i)] = i;
|
|
11
|
+
return table;
|
|
12
|
+
})();
|
|
13
|
+
var nativeCodec = typeof globalThis !== "undefined" && typeof globalThis.btoa === "function" && typeof globalThis.atob === "function";
|
|
14
|
+
function encodeViaBtoa(data) {
|
|
15
|
+
let binary = "";
|
|
16
|
+
for (let i = 0; i < data.length; i += CHUNK) {
|
|
17
|
+
binary += String.fromCharCode.apply(null, data.subarray(i, i + CHUNK));
|
|
18
|
+
}
|
|
19
|
+
return globalThis.btoa(binary);
|
|
20
|
+
}
|
|
21
|
+
function decodeViaAtob(encoded) {
|
|
22
|
+
const binary = globalThis.atob(encoded);
|
|
23
|
+
const out = new Uint8Array(binary.length);
|
|
24
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function encodePure(data) {
|
|
28
|
+
const len = data.length;
|
|
29
|
+
const full = len - len % 3;
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (let start = 0; start < full; start += CHUNK) {
|
|
32
|
+
const stop = Math.min(start + CHUNK, full);
|
|
33
|
+
let s = "";
|
|
34
|
+
for (let i = start; i < stop; i += 3) {
|
|
35
|
+
const n = data[i] << 16 | data[i + 1] << 8 | data[i + 2];
|
|
36
|
+
s += ALPHABET[n >> 18 & 63] + ALPHABET[n >> 12 & 63] + ALPHABET[n >> 6 & 63] + ALPHABET[n & 63];
|
|
37
|
+
}
|
|
38
|
+
parts.push(s);
|
|
39
|
+
}
|
|
40
|
+
if (len - full === 1) {
|
|
41
|
+
const n = data[full] << 16;
|
|
42
|
+
parts.push(ALPHABET[n >> 18 & 63] + ALPHABET[n >> 12 & 63] + "==");
|
|
43
|
+
} else if (len - full === 2) {
|
|
44
|
+
const n = data[full] << 16 | data[full + 1] << 8;
|
|
45
|
+
parts.push(ALPHABET[n >> 18 & 63] + ALPHABET[n >> 12 & 63] + ALPHABET[n >> 6 & 63] + "=");
|
|
46
|
+
}
|
|
47
|
+
return parts.join("");
|
|
48
|
+
}
|
|
49
|
+
function decodePure(encoded) {
|
|
50
|
+
let validLen = encoded.length;
|
|
51
|
+
while (validLen > 0 && encoded.charCodeAt(validLen - 1) === 61) validLen--;
|
|
52
|
+
const out = new Uint8Array(validLen * 3 >> 2);
|
|
53
|
+
let o = 0, buf = 0, bits = 0;
|
|
54
|
+
for (let i = 0; i < validLen; i++) {
|
|
55
|
+
const code = encoded.charCodeAt(i);
|
|
56
|
+
const v = code < 128 ? REVERSE[code] : -1;
|
|
57
|
+
if (v < 0) continue;
|
|
58
|
+
buf = buf << 6 | v;
|
|
59
|
+
bits += 6;
|
|
60
|
+
if (bits >= 8) {
|
|
61
|
+
bits -= 8;
|
|
62
|
+
out[o++] = buf >> bits & 255;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return o === out.length ? out : out.subarray(0, o);
|
|
66
|
+
}
|
|
67
|
+
var starfishBase64 = nativeCodec ? { encode: encodeViaBtoa, decode: decodeViaAtob } : { encode: encodePure, decode: decodePure };
|
|
68
|
+
|
|
69
|
+
// src/platform/platform.native.ts
|
|
70
|
+
install();
|
|
71
|
+
function configureStarfishPlatform() {
|
|
72
|
+
configurePlatform({ base64: starfishBase64 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/platform/kv.native.ts
|
|
76
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
77
|
+
|
|
78
|
+
// src/core/adapters.ts
|
|
79
|
+
var kv = null;
|
|
80
|
+
function configureKv(adapter) {
|
|
81
|
+
kv = adapter;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/platform/kv.native.ts
|
|
85
|
+
function kvGet(key) {
|
|
86
|
+
return AsyncStorage.getItem(key).catch(() => null);
|
|
87
|
+
}
|
|
88
|
+
function kvSet(key, value) {
|
|
89
|
+
return AsyncStorage.setItem(key, value).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function kvRemove(key) {
|
|
93
|
+
return AsyncStorage.removeItem(key).catch(() => {
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function configureNativeKv() {
|
|
97
|
+
configureKv({ get: kvGet, set: kvSet, remove: kvRemove });
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
configureNativeKv,
|
|
101
|
+
configureStarfishPlatform,
|
|
102
|
+
kvGet,
|
|
103
|
+
kvRemove,
|
|
104
|
+
kvSet
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=index.native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/platform/platform.native.ts","../../src/sync/base64.ts","../../src/platform/kv.native.ts","../../src/core/adapters.ts"],"sourcesContent":["/**\n * Native crypto setup. Installs react-native-quick-crypto to patch\n * `globalThis.crypto` and `globalThis.Buffer` before any SDK call.\n * Requires a custom dev build (not Expo Go) + New Architecture.\n */\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore — optional peer dep; only present in native builds\nimport { install } from 'react-native-quick-crypto';\nimport { configurePlatform } from '@drakkar.software/starfish-protocol';\nimport { starfishBase64 } from '../sync/base64.js';\n\ninstall();\n\nexport function configureStarfishPlatform(): void {\n configurePlatform({ base64: starfishBase64 });\n}\n","/**\n * A chunked base64 provider for the Starfish platform.\n *\n * The SDK's default web encoder is `btoa(String.fromCharCode(...data))`, which\n * spreads the entire byte array into one call — a multi-megabyte attachment\n * overflows the argument/stack limit and throws \"Maximum call stack size exceeded\".\n * This provider walks the bytes in fixed windows instead, so it scales to large blobs.\n *\n * Prefers the platform's own `btoa`/`atob` (web) and falls back to a pure\n * implementation where they're absent (Hermes/native).\n */\nimport type { Base64Provider } from '@drakkar.software/starfish-protocol';\n\nconst CHUNK = 0x6000; // 24 576 bytes — multiple of 3, well under V8's apply limit\n\nconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\nconst REVERSE = (() => {\n const table = new Int16Array(128).fill(-1);\n for (let i = 0; i < ALPHABET.length; i++) table[ALPHABET.charCodeAt(i)] = i;\n return table;\n})();\n\nconst nativeCodec =\n typeof globalThis !== 'undefined' &&\n typeof globalThis.btoa === 'function' &&\n typeof globalThis.atob === 'function';\n\nfunction encodeViaBtoa(data: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < data.length; i += CHUNK) {\n binary += String.fromCharCode.apply(null, data.subarray(i, i + CHUNK) as unknown as number[]);\n }\n return globalThis.btoa(binary);\n}\n\nfunction decodeViaAtob(encoded: string): Uint8Array {\n const binary = globalThis.atob(encoded);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n\nfunction encodePure(data: Uint8Array): string {\n const len = data.length;\n const full = len - (len % 3);\n const parts: string[] = [];\n for (let start = 0; start < full; start += CHUNK) {\n const stop = Math.min(start + CHUNK, full);\n let s = '';\n for (let i = start; i < stop; i += 3) {\n const n = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2];\n s += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63] + ALPHABET[n & 63];\n }\n parts.push(s);\n }\n if (len - full === 1) {\n const n = data[full] << 16;\n parts.push(ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + '==');\n } else if (len - full === 2) {\n const n = (data[full] << 16) | (data[full + 1] << 8);\n parts.push(ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63] + '=');\n }\n return parts.join('');\n}\n\nfunction decodePure(encoded: string): Uint8Array {\n let validLen = encoded.length;\n while (validLen > 0 && encoded.charCodeAt(validLen - 1) === 61) validLen--;\n const out = new Uint8Array((validLen * 3) >> 2);\n let o = 0, buf = 0, bits = 0;\n for (let i = 0; i < validLen; i++) {\n const code = encoded.charCodeAt(i);\n const v = code < 128 ? REVERSE[code] : -1;\n if (v < 0) continue;\n buf = (buf << 6) | v;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[o++] = (buf >> bits) & 0xff;\n }\n }\n return o === out.length ? out : out.subarray(0, o);\n}\n\n/** Spread-free, chunked base64 — a drop-in for the SDK's default provider. */\nexport const starfishBase64: Base64Provider = nativeCodec\n ? { encode: encodeViaBtoa, decode: decodeViaAtob }\n : { encode: encodePure, decode: decodePure };\n","/** Native KV adapter — backed by `@react-native-async-storage/async-storage`. */\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore — optional peer dep; the native bundle is only loaded on RN targets.\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n\nimport { configureKv } from '../core/adapters.js';\n\nexport function kvGet(key: string): Promise<string | null> {\n return (AsyncStorage as { getItem(k: string): Promise<string | null> }).getItem(key).catch(() => null);\n}\n\nexport function kvSet(key: string, value: string): Promise<void> {\n return (AsyncStorage as { setItem(k: string, v: string): Promise<void> }).setItem(key, value).catch(() => {});\n}\n\nexport function kvRemove(key: string): Promise<void> {\n return (AsyncStorage as { removeItem(k: string): Promise<void> }).removeItem(key).catch(() => {});\n}\n\n/** Call once at app boot (native). Wires `AsyncStorage` into the SDK. */\nexport function configureNativeKv(): void {\n configureKv({ get: kvGet, set: kvSet, remove: kvRemove });\n}\n","/**\n * Platform adapters the headless SDK needs the host app to provide.\n *\n * The SDK can't do Metro `.native.ts` file-extension resolution and must not bind\n * to localStorage / AsyncStorage / SecureStore directly, so the host injects a\n * key/value store at boot via {@link configureKv}. This holds account-scoped state\n * the SDK persists offline (joined-space member caps, the public-space access map,\n * read marks, mutes, profile/pull caches).\n */\n\n/** Async key/value store — web `localStorage`, native `AsyncStorage`, etc. */\nexport interface KvAdapter {\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<void>;\n remove(key: string): Promise<void>;\n}\n\nlet kv: KvAdapter | null = null;\n\n/** Install the host's key/value store. Call once at app boot. */\nexport function configureKv(adapter: KvAdapter): void {\n kv = adapter;\n}\n\n/** The configured KV store, or throw if the host never called {@link configureKv}. */\nexport function getKv(): KvAdapter {\n if (!kv) throw new Error('octospaces-sdk: configureKv() not called — wire it at app boot.');\n return kv;\n}\n\n// Free-function shims matching the historical `kv` module surface.\nexport const kvGet = (key: string): Promise<string | null> => getKv().get(key);\nexport const kvSet = (key: string, value: string): Promise<void> => getKv().set(key, value);\nexport const kvRemove = (key: string): Promise<void> => getKv().remove(key);\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drakkar.software/octospaces-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared headless spaces core — identity, registry, objects, and sync plumbing.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./platform": {
|
|
15
|
+
"react-native": "./dist/platform/index.native.js",
|
|
16
|
+
"import": "./dist/platform/index.js",
|
|
17
|
+
"types": "./dist/platform/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@noble/curves": "^2.2.0",
|
|
22
|
+
"@noble/hashes": "^2.2.0",
|
|
23
|
+
"@scure/bip39": "^2.0.0",
|
|
24
|
+
"hash-wasm": "^4.12.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@drakkar.software/starfish-client": ">=3.0.0-alpha.27",
|
|
28
|
+
"@drakkar.software/starfish-identities": ">=3.0.0-alpha.27",
|
|
29
|
+
"@drakkar.software/starfish-keyring": ">=3.0.0-alpha.27",
|
|
30
|
+
"@drakkar.software/starfish-protocol": ">=3.0.0-alpha.27",
|
|
31
|
+
"@drakkar.software/starfish-sharing": ">=3.0.0-alpha.27"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@drakkar.software/starfish-client": "3.0.0-alpha.27",
|
|
35
|
+
"@drakkar.software/starfish-identities": "3.0.0-alpha.27",
|
|
36
|
+
"@drakkar.software/starfish-keyring": "3.0.0-alpha.27",
|
|
37
|
+
"@drakkar.software/starfish-protocol": "3.0.0-alpha.27",
|
|
38
|
+
"@drakkar.software/starfish-sharing": "3.0.0-alpha.27",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"tsup": "^8.3.0",
|
|
41
|
+
"typescript": "~6.0.3",
|
|
42
|
+
"vitest": "^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"lint": "tsc --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform adapters the headless SDK needs the host app to provide.
|
|
3
|
+
*
|
|
4
|
+
* The SDK can't do Metro `.native.ts` file-extension resolution and must not bind
|
|
5
|
+
* to localStorage / AsyncStorage / SecureStore directly, so the host injects a
|
|
6
|
+
* key/value store at boot via {@link configureKv}. This holds account-scoped state
|
|
7
|
+
* the SDK persists offline (joined-space member caps, the public-space access map,
|
|
8
|
+
* read marks, mutes, profile/pull caches).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Async key/value store — web `localStorage`, native `AsyncStorage`, etc. */
|
|
12
|
+
export interface KvAdapter {
|
|
13
|
+
get(key: string): Promise<string | null>;
|
|
14
|
+
set(key: string, value: string): Promise<void>;
|
|
15
|
+
remove(key: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let kv: KvAdapter | null = null;
|
|
19
|
+
|
|
20
|
+
/** Install the host's key/value store. Call once at app boot. */
|
|
21
|
+
export function configureKv(adapter: KvAdapter): void {
|
|
22
|
+
kv = adapter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The configured KV store, or throw if the host never called {@link configureKv}. */
|
|
26
|
+
export function getKv(): KvAdapter {
|
|
27
|
+
if (!kv) throw new Error('octospaces-sdk: configureKv() not called — wire it at app boot.');
|
|
28
|
+
return kv;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Free-function shims matching the historical `kv` module surface.
|
|
32
|
+
export const kvGet = (key: string): Promise<string | null> => getKv().get(key);
|
|
33
|
+
export const kvSet = (key: string, value: string): Promise<void> => getKv().set(key, value);
|
|
34
|
+
export const kvRemove = (key: string): Promise<void> => getKv().remove(key);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration for the OctoSpaces SDK — the Starfish sync server URL,
|
|
3
|
+
* optional namespace, events-stream URL, and public web origin.
|
|
4
|
+
*
|
|
5
|
+
* The SDK is headless and platform-agnostic, so it does NOT read environment
|
|
6
|
+
* variables itself. The host app reads its own env (e.g. Expo `EXPO_PUBLIC_*`) and
|
|
7
|
+
* calls {@link configureOctoSpaces} once at boot, before any sync/identity API runs.
|
|
8
|
+
* Getters throw a clear error if called before configuration so a misconfigured
|
|
9
|
+
* host fails fast rather than silently signing the wrong path.
|
|
10
|
+
*/
|
|
11
|
+
export interface OctoSpacesConfig {
|
|
12
|
+
/** Starfish sync server base URL (e.g. `http://localhost:8787`). */
|
|
13
|
+
syncBase: string;
|
|
14
|
+
/** Bare namespace name; the SDK prepends `/v1/<namespace>` to signed paths.
|
|
15
|
+
* Unset for a root-mounted (local dev) server. */
|
|
16
|
+
syncNamespace?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional SEPARATE namespace for cross-app shared-spaces storage. When set,
|
|
19
|
+
* space registry ops use this namespace instead of `syncNamespace`, enabling a
|
|
20
|
+
* single shared space list across multiple app namespaces (e.g. OctoChat and
|
|
21
|
+
* OctoVault sharing spaces at `/v1/shared`). If unset, falls back to the default
|
|
22
|
+
* namespace for all operations (single-app behavior).
|
|
23
|
+
*/
|
|
24
|
+
sharedSpacesNamespace?: string;
|
|
25
|
+
/** Override the live change-event SSE endpoint. Defaults to
|
|
26
|
+
* `${syncBase}${syncPrefix}/events`. */
|
|
27
|
+
eventsUrl?: string;
|
|
28
|
+
/** Public origin of the web app, used to build shareable invite links on
|
|
29
|
+
* platforms without `window.location` (native). Empty by default. */
|
|
30
|
+
webBase?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Called when a background Starfish revalidation succeeds after a 429/5xx
|
|
33
|
+
* cache-fallback (stale-while-revalidate). Use it to signal that the server
|
|
34
|
+
* is reachable again so any stale views re-pull and recover.
|
|
35
|
+
*/
|
|
36
|
+
onServerReachable?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cfg: OctoSpacesConfig | null = null;
|
|
40
|
+
|
|
41
|
+
/** Configure the SDK. Call once at app boot before any sync/identity API. */
|
|
42
|
+
export function configureOctoSpaces(config: OctoSpacesConfig): void {
|
|
43
|
+
// Guard against the common mistake of passing `namespace` (wrong key) instead of
|
|
44
|
+
// `syncNamespace`. TypeScript's excess-property check is bypassed when the config
|
|
45
|
+
// is assembled via a conditional spread, so the wrong key would be silently ignored.
|
|
46
|
+
if ('namespace' in config && !config.syncNamespace) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`octospaces-sdk: configureOctoSpaces received "namespace" — did you mean "syncNamespace"?`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const ns = (config.syncNamespace ?? '').trim();
|
|
52
|
+
if (ns !== '' && !/^[A-Za-z0-9_-]+$/.test(ns)) {
|
|
53
|
+
throw new Error(`octospaces-sdk: syncNamespace must be a bare name ([A-Za-z0-9_-]+), got "${ns}"`);
|
|
54
|
+
}
|
|
55
|
+
const sharedNs = (config.sharedSpacesNamespace ?? '').trim();
|
|
56
|
+
if (sharedNs !== '' && !/^[A-Za-z0-9_-]+$/.test(sharedNs)) {
|
|
57
|
+
throw new Error(`octospaces-sdk: sharedSpacesNamespace must be a bare name ([A-Za-z0-9_-]+), got "${sharedNs}"`);
|
|
58
|
+
}
|
|
59
|
+
cfg = {
|
|
60
|
+
...config,
|
|
61
|
+
syncNamespace: ns || undefined,
|
|
62
|
+
sharedSpacesNamespace: sharedNs || undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function req(): OctoSpacesConfig {
|
|
67
|
+
if (!cfg) throw new Error('octospaces-sdk: configureOctoSpaces() not called — wire it at app boot.');
|
|
68
|
+
return cfg;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Starfish sync server base URL. */
|
|
72
|
+
export const getSyncBase = (): string => req().syncBase;
|
|
73
|
+
/** Bare namespace name (or `undefined` for a root-mounted server). */
|
|
74
|
+
export const getSyncNamespace = (): string | undefined => req().syncNamespace;
|
|
75
|
+
/** Namespaced path prefix (`/v1/<namespace>`, or `''` locally). */
|
|
76
|
+
export const getSyncPrefix = (): string => {
|
|
77
|
+
const ns = req().syncNamespace;
|
|
78
|
+
return ns ? `/v1/${ns}` : '';
|
|
79
|
+
};
|
|
80
|
+
/** Optional separate namespace for shared-spaces storage. `undefined` means use the default namespace. */
|
|
81
|
+
export const getSharedSpacesNamespace = (): string | undefined => cfg?.sharedSpacesNamespace;
|
|
82
|
+
/** Live change-event SSE endpoint. */
|
|
83
|
+
export const getEventsUrl = (): string => req().eventsUrl ?? `${getSyncBase()}${getSyncPrefix()}/events`;
|
|
84
|
+
/** Public web origin (right-trimmed of trailing slashes; `''` by default). */
|
|
85
|
+
export const getWebBase = (): string => (req().webBase ?? '').replace(/\/+$/, '');
|
|
86
|
+
/** Callback to invoke when a background Starfish revalidation succeeds. */
|
|
87
|
+
export const getOnServerReachable = (): (() => void) | undefined => cfg?.onServerReachable;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { randomId, roomSlug } from './ids.js';
|
|
3
|
+
|
|
4
|
+
describe('randomId', () => {
|
|
5
|
+
it('returns a non-empty string', () => {
|
|
6
|
+
expect(typeof randomId()).toBe('string');
|
|
7
|
+
expect(randomId().length).toBeGreaterThan(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns unique values', () => {
|
|
11
|
+
const ids = new Set(Array.from({ length: 100 }, () => randomId()));
|
|
12
|
+
expect(ids.size).toBe(100);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('contains only URL-safe characters', () => {
|
|
16
|
+
for (let i = 0; i < 50; i++) {
|
|
17
|
+
expect(randomId()).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('roomSlug', () => {
|
|
23
|
+
it('lowercases and replaces spaces with hyphens', () => {
|
|
24
|
+
expect(roomSlug('General Channel')).toBe('general-channel');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('strips non-alphanumeric characters', () => {
|
|
28
|
+
expect(roomSlug('Off-Topic!')).toBe('off-topic');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles empty string — returns a non-empty fallback or empty string', () => {
|
|
32
|
+
// roomSlug may return a fallback (e.g. 'room') for empty input.
|
|
33
|
+
const result = roomSlug('');
|
|
34
|
+
expect(typeof result).toBe('string');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns lowercase for single word', () => {
|
|
38
|
+
expect(roomSlug('General')).toBe('general');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('trims leading/trailing hyphens', () => {
|
|
42
|
+
const slug = roomSlug(' hello ');
|
|
43
|
+
expect(slug).not.toMatch(/^-|-$/);
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/core/ids.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier helpers — one source for unguessable ids.
|
|
3
|
+
*
|
|
4
|
+
* `randomId()` is a CSPRNG-backed 128-bit id (16 random bytes, hex). Use it for
|
|
5
|
+
* EVERY storage/space/room/object/blob id. Hex output is path-safe and server-safe.
|
|
6
|
+
*/
|
|
7
|
+
export function randomId(): string {
|
|
8
|
+
const bytes = new Uint8Array(16);
|
|
9
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
10
|
+
let s = '';
|
|
11
|
+
for (const b of bytes) s += b.toString(16).padStart(2, '0');
|
|
12
|
+
return s;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Slug for the human part of an id (e.g. `<spaceId>-<slug>-<ts>`). Restricted to
|
|
17
|
+
* URL-clean `[a-z0-9-]` so the id is safe as both a URL path segment and a
|
|
18
|
+
* server storage-path leaf (the server's FilesystemObjectStore rejects any key
|
|
19
|
+
* outside `[a-zA-Z0-9._:@/-]`). Falls back to `'room'` when a name strips to nothing.
|
|
20
|
+
*/
|
|
21
|
+
export function roomSlug(name: string): string {
|
|
22
|
+
return (
|
|
23
|
+
name
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '')
|
|
27
|
+
.slice(0, 40) || 'room'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link SpaceAccessError} — a GENUINE access denial (not a transient connectivity failure).
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own dependency-free module so both the low-level keyring opener
|
|
5
|
+
* (`client.ts`) and the higher-level space-encryptor cache (`space-encryptor.ts`) can
|
|
6
|
+
* throw it without an import cycle.
|
|
7
|
+
*/
|
|
8
|
+
export class SpaceAccessError extends Error {
|
|
9
|
+
constructor(message: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'SpaceAccessError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the persisted-session storage layer. Both platform variants
|
|
3
|
+
* (`storage.ts` web, `storage.native.ts` native) implement the same contract so
|
|
4
|
+
* the session context stays platform-agnostic.
|
|
5
|
+
*/
|
|
6
|
+
import type { BootstrapOrigin } from '@drakkar.software/starfish-identities';
|
|
7
|
+
import type { CapCert } from '@drakkar.software/starfish-protocol';
|
|
8
|
+
|
|
9
|
+
import type { DeviceKeys } from '../sync/client.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The root identity already derived from the seed (userId + device keys). Caching
|
|
13
|
+
* it lets unlock/cold-start skip the heavy `bootstrapRootIdentity` Argon2id.
|
|
14
|
+
* Equivalent in sensitivity to the seed, so it lives inside the same sealed blob.
|
|
15
|
+
*/
|
|
16
|
+
export interface DerivedIdentity {
|
|
17
|
+
userId: string;
|
|
18
|
+
keys: DeviceKeys;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The recovery seed + display name — the minimum needed to re-derive an identity. */
|
|
22
|
+
export interface PersistedSession {
|
|
23
|
+
/** BIP-39 recovery seed. Absent for non-seed origins. */
|
|
24
|
+
seed?: string[];
|
|
25
|
+
name: string;
|
|
26
|
+
/** Cached root identity so restore skips the bootstrap Argon2id. */
|
|
27
|
+
derived?: DerivedIdentity;
|
|
28
|
+
/** How this identity was bootstrapped. Absent for seed-derived identities. */
|
|
29
|
+
bootstrapOrigin?: BootstrapOrigin;
|
|
30
|
+
/** Root-signed cap-cert for a PAIRED (linked) device. */
|
|
31
|
+
capCert?: CapCert;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every account held on this device plus which one is active. The whole vault is
|
|
36
|
+
* sealed as a unit (web: under one app-lock via a vault master key; native: a
|
|
37
|
+
* single secure-store entry).
|
|
38
|
+
*/
|
|
39
|
+
export interface Vault {
|
|
40
|
+
accounts: PersistedSession[];
|
|
41
|
+
activeId: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Ways the web-persisted seed can be unlocked. */
|
|
45
|
+
export type UnlockMethod = 'pin' | 'passkey';
|
|
46
|
+
|
|
47
|
+
/** A registered passkey + the PRF secret used to seal the seed for it. */
|
|
48
|
+
export interface PasskeyEnrollment {
|
|
49
|
+
credentialId: string;
|
|
50
|
+
salt: string;
|
|
51
|
+
secretHex: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** How to lock the seed when persisting it (web only). */
|
|
55
|
+
export interface SeedLock {
|
|
56
|
+
pin: string;
|
|
57
|
+
passkey?: PasskeyEnrollment;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Result of probing storage at launch:
|
|
62
|
+
* - `none` — nothing stored; start signed-out.
|
|
63
|
+
* - `ready` — vault available immediately (native Keychain path).
|
|
64
|
+
* - `locked` — a sealed vault exists; unlock with one of `methods` (web path).
|
|
65
|
+
* - `error` — storage read failed.
|
|
66
|
+
*/
|
|
67
|
+
export type VaultLoad =
|
|
68
|
+
| { kind: 'none' }
|
|
69
|
+
| { kind: 'ready'; vault: Vault }
|
|
70
|
+
| { kind: 'locked'; methods: UnlockMethod[] }
|
|
71
|
+
| { kind: 'error'; error: unknown };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain model for OctoSpaces — space + object types shared by the SDK and any UI.
|
|
3
|
+
*
|
|
4
|
+
* A space's contents are modelled as a tree of typed {@link ObjectNode}s stored in
|
|
5
|
+
* a union-merged index at `spaces/{spaceId}/objects/_index`. Everything — rooms,
|
|
6
|
+
* categories, docs, projects, tasks — is an `ObjectNode` discriminated by `type`.
|
|
7
|
+
* Apps extend the model by adding their own `ObjectType` strings; the generic
|
|
8
|
+
* primitives here are app-neutral.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Re-export SealedBlob so consumers get it from one place.
|
|
12
|
+
export type { SealedBlob } from '../sync/account-seal.js';
|
|
13
|
+
|
|
14
|
+
export type ID = string;
|
|
15
|
+
|
|
16
|
+
/** A user's presence indicator. The theme maps each to a color (app-side). */
|
|
17
|
+
export type PresenceStatus = 'online' | 'away' | 'dnd' | 'offline';
|
|
18
|
+
|
|
19
|
+
/** A security item's verification state. The theme maps each to a color (app-side). */
|
|
20
|
+
export type VerificationLevel = 'verified' | 'pending' | 'unverified';
|
|
21
|
+
|
|
22
|
+
/** Maps a joined private space's id → its owner-issued member cap-cert (serialized
|
|
23
|
+
* JSON). Persisted both in device-local kv (`member-caps.ts`) and, for durability,
|
|
24
|
+
* in the user's own synced `_spaces` doc so a fresh device re-hydrates it. */
|
|
25
|
+
export type CapMap = Record<string, string>;
|
|
26
|
+
|
|
27
|
+
/** Maps a joined PUBLIC space's id → its invitation credential (the owner-signed cap
|
|
28
|
+
* plus the link's ephemeral private key) SEALED to the account's own key. Unlike a
|
|
29
|
+
* member cap (safe in the clear — see {@link CapMap}), a public-join credential
|
|
30
|
+
* embeds a bearer secret, so it is sealed before riding in the plaintext `_spaces`
|
|
31
|
+
* doc. Recovered on any device with the same seed. See `account-seal.ts` and
|
|
32
|
+
* `space-access-store.ts`. */
|
|
33
|
+
export type PubAccessMap = Record<string, import('../sync/account-seal.js').SealedBlob>;
|
|
34
|
+
|
|
35
|
+
/** Maps a DM peer's userId → the private DM-space id shared with them. */
|
|
36
|
+
export type DmMap = Record<string, string>;
|
|
37
|
+
|
|
38
|
+
/** The set of DM-space ids the user has archived (hidden from the DM list). */
|
|
39
|
+
export type ArchivedDms = Record<string, true>;
|
|
40
|
+
|
|
41
|
+
/** A mute entry. `true` = muted indefinitely; a number = muted UNTIL that epoch-ms instant. */
|
|
42
|
+
export type MuteValue = true | number;
|
|
43
|
+
|
|
44
|
+
/** Per-user mute preferences: which rooms and which whole spaces are silenced. */
|
|
45
|
+
export interface MutePrefs {
|
|
46
|
+
rooms: Record<string, MuteValue>;
|
|
47
|
+
spaces: Record<string, MuteValue>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A per-room read mark: the epoch-ms instant the viewer last read that room. */
|
|
51
|
+
export type ReadValue = number;
|
|
52
|
+
|
|
53
|
+
/** Per-user read marks — the timestamp each room was last read. */
|
|
54
|
+
export interface ReadPrefs {
|
|
55
|
+
rooms: Record<string, ReadValue>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether a space encrypts its content client-side. */
|
|
59
|
+
export type SpaceVisibility = 'private' | 'public';
|
|
60
|
+
|
|
61
|
+
export interface Space {
|
|
62
|
+
id: ID;
|
|
63
|
+
name: string;
|
|
64
|
+
/** 2-letter monogram used in the space rail. */
|
|
65
|
+
short: string;
|
|
66
|
+
/** Uploaded space image as a data URI; absent → render the `short` monogram. */
|
|
67
|
+
image?: string;
|
|
68
|
+
members: number;
|
|
69
|
+
unread?: number;
|
|
70
|
+
/** 'private' (E2EE keyring, the default) or 'public' (plaintext, joined via a
|
|
71
|
+
* space-wide invitation link). Absent ⇒ treat as 'private' (back-compat). */
|
|
72
|
+
visibility?: SpaceVisibility;
|
|
73
|
+
/** Public spaces only: the owner's userId (derived from the cap issuer). */
|
|
74
|
+
ownerId?: string;
|
|
75
|
+
/** Public spaces only (joiner side): whether this identity's invite link grants write. */
|
|
76
|
+
write?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Legacy room kind — used while apps migrate their content onto objects.
|
|
80
|
+
* New code should use {@link RoomSubtype} directly. */
|
|
81
|
+
export type RoomKind = 'channel' | 'dm' | 'automated';
|
|
82
|
+
|
|
83
|
+
/** Scheduled-fetch cadence for automated rooms. */
|
|
84
|
+
export type AutomationSchedule =
|
|
85
|
+
| { kind: 'interval'; everyMin: number }
|
|
86
|
+
| { kind: 'daily'; hour: number; minute: number }
|
|
87
|
+
| { kind: 'weekly'; weekday: number; hour: number; minute: number }
|
|
88
|
+
| { kind: 'cron'; expression: string };
|
|
89
|
+
|
|
90
|
+
/** Stored, synced configuration of an `automated` room. */
|
|
91
|
+
export interface AutomationMeta {
|
|
92
|
+
providerId: string;
|
|
93
|
+
params: Record<string, unknown>;
|
|
94
|
+
intervalMin: number;
|
|
95
|
+
schedule?: AutomationSchedule;
|
|
96
|
+
onOpen?: boolean;
|
|
97
|
+
enabled: boolean;
|
|
98
|
+
credential: import('../sync/account-seal.js').SealedBlob;
|
|
99
|
+
botUserId?: string;
|
|
100
|
+
runOnDeviceId: string | null;
|
|
101
|
+
lastRunAt: number | null;
|
|
102
|
+
lastFetchHash?: string | null;
|
|
103
|
+
lastError: string | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A transitional Room shape (used while apps migrate onto the object model). */
|
|
107
|
+
export interface Room {
|
|
108
|
+
id: ID;
|
|
109
|
+
spaceId: ID;
|
|
110
|
+
category: string;
|
|
111
|
+
name: string;
|
|
112
|
+
kind: RoomKind;
|
|
113
|
+
topic?: string;
|
|
114
|
+
unread?: number;
|
|
115
|
+
mention?: boolean;
|
|
116
|
+
avatar?: string;
|
|
117
|
+
automation?: AutomationMeta;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Object model ─────────────────────────────────────────────────────────────
|
|
121
|
+
// Everything in a space — rooms, categories, docs, projects, etc. — is an
|
|
122
|
+
// ObjectNode with a type. Apps extend ObjectType with their own strings.
|
|
123
|
+
|
|
124
|
+
/** The builtin object types. Custom types ride the same `string` field. */
|
|
125
|
+
export type BuiltinObjectType = 'room' | 'category' | 'automation' | 'doc' | 'project' | 'task';
|
|
126
|
+
export type ObjectType = BuiltinObjectType | (string & {});
|
|
127
|
+
|
|
128
|
+
/** Runtime set of builtin type strings for renderer branching. */
|
|
129
|
+
export const BUILTIN_OBJECT_TYPES: readonly BuiltinObjectType[] = ['room', 'category', 'automation', 'doc', 'project', 'task'];
|
|
130
|
+
|
|
131
|
+
/** How an object's content syncs. Builtins infer this; custom types set it explicitly. */
|
|
132
|
+
export type ObjectContentKind = 'merge' | 'append' | 'none';
|
|
133
|
+
|
|
134
|
+
/** When `type === 'room'`, the room flavour. */
|
|
135
|
+
export type RoomSubtype = 'channel' | 'dm' | 'automation';
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One entry in a space's object index (`spaces/{spaceId}/objects/_index`).
|
|
139
|
+
* Identity + tree position + light metadata ONLY — heavy content (messages, doc
|
|
140
|
+
* blocks, project event log) lives in a per-object content doc keyed by `id`.
|
|
141
|
+
*/
|
|
142
|
+
export interface ObjectNode {
|
|
143
|
+
id: ID;
|
|
144
|
+
type: ObjectType;
|
|
145
|
+
subtype?: RoomSubtype;
|
|
146
|
+
parentId: ID | null;
|
|
147
|
+
order: number;
|
|
148
|
+
title: string;
|
|
149
|
+
emoji?: string;
|
|
150
|
+
updatedAt: number;
|
|
151
|
+
archived?: boolean;
|
|
152
|
+
automation?: AutomationMeta;
|
|
153
|
+
contentKind?: ObjectContentKind;
|
|
154
|
+
meta?: Record<string, unknown>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The object-index doc: the union-merged list of every object in a space. */
|
|
158
|
+
export interface ObjectsIndex {
|
|
159
|
+
v: 1;
|
|
160
|
+
objects: ObjectNode[];
|
|
161
|
+
updatedAt: number;
|
|
162
|
+
}
|