monoidentity 0.11.2 → 0.12.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.
Files changed (39) hide show
  1. package/dist/+client.d.ts +6 -0
  2. package/dist/+client.js +6 -0
  3. package/dist/+common.d.ts +1 -0
  4. package/dist/+common.js +1 -0
  5. package/dist/+server.d.ts +2 -0
  6. package/dist/+server.js +2 -0
  7. package/dist/Monoidentity.svelte +28 -26
  8. package/dist/storage/backupcloud.d.ts +2 -0
  9. package/dist/storage/backupcloud.js +107 -0
  10. package/dist/storage/backuplocally.d.ts +1 -0
  11. package/dist/storage/backuplocally.js +85 -0
  12. package/dist/storage/storageclient.svelte.d.ts +10 -0
  13. package/dist/storage/storageclient.svelte.js +69 -0
  14. package/dist/storage.d.ts +4 -6
  15. package/dist/storage.js +17 -107
  16. package/dist/trackready.js +14 -21
  17. package/dist/verification-server.d.ts +1 -0
  18. package/dist/verification-server.js +6 -0
  19. package/package.json +6 -7
  20. package/dist/index.d.ts +0 -5
  21. package/dist/index.js +0 -5
  22. package/dist/storage/_cloud.d.ts +0 -3
  23. package/dist/storage/_cloud.js +0 -39
  24. package/dist/storage/_md5.d.ts +0 -1
  25. package/dist/storage/_md5.js +0 -52
  26. package/dist/storage/_replay.d.ts +0 -16
  27. package/dist/storage/_replay.js +0 -95
  28. package/dist/storage/_timing.d.ts +0 -1
  29. package/dist/storage/_timing.js +0 -21
  30. package/dist/storage/createlocalstorage.d.ts +0 -1
  31. package/dist/storage/createlocalstorage.js +0 -42
  32. package/dist/storage/createstore.d.ts +0 -11
  33. package/dist/storage/createstore.js +0 -9
  34. package/dist/storage/wrapbackup.d.ts +0 -2
  35. package/dist/storage/wrapbackup.js +0 -88
  36. package/dist/storage/wrapcloud.d.ts +0 -3
  37. package/dist/storage/wrapcloud.js +0 -54
  38. /package/dist/{storage-attest.d.ts → verification-client.d.ts} +0 -0
  39. /package/dist/{storage-attest.js → verification-client.js} +0 -0
@@ -0,0 +1,6 @@
1
+ export * from "./+common.js";
2
+ export { getLoginRecognized, getVerification, getStorage, getScopedFS } from "./storage.js";
3
+ export { retrieveVerification } from "./verification-client.js";
4
+ export { default as rawAttest } from "./verification/attest-remote.js";
5
+ export { trackReady } from "./trackready.js";
6
+ export { default as Monoidentity } from "./Monoidentity.svelte";
@@ -0,0 +1,6 @@
1
+ export * from "./+common.js";
2
+ export { getLoginRecognized, getVerification, getStorage, getScopedFS } from "./storage.js";
3
+ export { retrieveVerification } from "./verification-client.js";
4
+ export { default as rawAttest } from "./verification/attest-remote.js";
5
+ export { trackReady } from "./trackready.js";
6
+ export { default as Monoidentity } from "./Monoidentity.svelte";
@@ -0,0 +1 @@
1
+ export { encode, decode } from "./utils-base36.js";
@@ -0,0 +1 @@
1
+ export { encode, decode } from "./utils-base36.js";
@@ -0,0 +1,2 @@
1
+ export * from "./+common.js";
2
+ export { useVerification } from "./verification-server.js";
@@ -0,0 +1,2 @@
1
+ export * from "./+common.js";
2
+ export { useVerification } from "./verification-server.js";
@@ -19,10 +19,8 @@
19
19
 
20
20
  {#snippet backupUI(yes: () => void, no: () => void)}
21
21
  <p>Avoid reconfiguration with a backup folder.</p>
22
- <div class="buttons">
23
- <button class="primary" onclick={yes}>Connect</button>
24
- <button onclick={no}>Skip</button>
25
- </div>
22
+ <button onclick={no}>Skip</button>
23
+ <button class="primary" onclick={yes}>Connect</button>
26
24
  {/snippet}
27
25
 
28
26
  {#await ready}
@@ -40,32 +38,36 @@
40
38
  .backup {
41
39
  display: flex;
42
40
  flex-direction: column;
43
- gap: 0.5rem;
44
-
45
- background-color: light-dark(#000, #fff);
46
- color: light-dark(#fff, #000);
41
+ gap: 0.25rem;
47
42
  line-height: 1;
48
- padding: 0.75rem;
49
- border-radius: 1.5rem;
50
43
 
51
- > .buttons {
44
+ > * {
52
45
  display: flex;
53
- gap: 0.5rem;
54
- > button {
55
- display: flex;
56
- flex: 1;
57
- align-items: center;
58
- justify-content: center;
59
- height: 2rem;
60
- border-radius: 0.75rem;
61
- &.primary {
62
- background-color: light-dark(#fff, #000);
63
- color: light-dark(#000, #fff);
64
- }
65
- border: none;
66
- font: inherit;
67
- cursor: pointer;
46
+ align-items: center;
47
+ justify-content: center;
48
+ height: 3rem;
49
+ margin: 0;
50
+ padding-inline: 0.5rem;
51
+ border-radius: 0.5rem;
52
+ border: none;
53
+ font: inherit;
54
+ &:first-child {
55
+ border-start-start-radius: 1.5rem;
56
+ border-start-end-radius: 1.5rem;
57
+ }
58
+ &:last-child {
59
+ border-end-start-radius: 1.5rem;
60
+ border-end-end-radius: 1.5rem;
68
61
  }
62
+ background-color: light-dark(#fff, #000);
63
+ color: light-dark(#000, #fff);
64
+ }
65
+ > button {
66
+ cursor: pointer;
67
+ }
68
+ > .primary {
69
+ background-color: light-dark(#000, #fff);
70
+ color: light-dark(#fff, #000);
69
71
  }
70
72
  }
71
73
  .toast {
@@ -0,0 +1,2 @@
1
+ import type { Bucket } from "../utils-bucket.js";
2
+ export declare const backupCloud: (bucket: Bucket) => Promise<void>;
@@ -0,0 +1,107 @@
1
+ import { AwsClient } from "aws4fetch";
2
+ import { storageClient, STORAGE_EVENT } from "./storageclient.svelte.js";
3
+ const CLOUD_CACHE_KEY = "monoidentity-x/cloud-cache";
4
+ const isJunk = (key) => key.includes(".obsidian/") || key.includes(".cache/");
5
+ const loadFromCloud = async (base, client) => {
6
+ const listResp = await client.fetch(base);
7
+ if (!listResp.ok)
8
+ throw new Error(`List bucket failed: ${listResp.status}`);
9
+ const listXml = await listResp.text();
10
+ const objects = [...listXml.matchAll(/<Key>(.*?)<\/Key>.*?<ETag>(.*?)<\/ETag>/gs)]
11
+ .map((m) => m.slice(1).map((s) => s.replaceAll("&quot;", `"`).replaceAll("&apos;", `'`)))
12
+ .map(([key, etag]) => ({ key, etag: etag.replaceAll(`"`, "") }))
13
+ .filter(({ key }) => !isJunk(key));
14
+ const prevCache = JSON.parse(localStorage[CLOUD_CACHE_KEY] || "{}");
15
+ const nextCache = {};
16
+ const model = {};
17
+ await Promise.all(objects.map(async ({ key, etag }) => {
18
+ const cached = prevCache[key];
19
+ if (cached?.etag == etag) {
20
+ model[key] = cached.content;
21
+ nextCache[key] = cached;
22
+ return;
23
+ }
24
+ console.debug("[monoidentity cloud] loading", key);
25
+ const r = await client.fetch(`${base}/${key}`);
26
+ if (!r.ok)
27
+ throw new Error(`Fetch ${key} failed: ${r.status}`);
28
+ let content;
29
+ if (key.endsWith(".md") || key.endsWith(".devalue")) {
30
+ content = await r.text();
31
+ }
32
+ else {
33
+ const buf = new Uint8Array(await r.arrayBuffer());
34
+ content = "";
35
+ const chunk = 8192;
36
+ for (let i = 0; i < buf.length; i += chunk) {
37
+ content += String.fromCharCode.apply(null, buf.subarray(i, i + chunk));
38
+ }
39
+ }
40
+ model[key] = content;
41
+ nextCache[key] = { etag, content };
42
+ }));
43
+ localStorage[CLOUD_CACHE_KEY] = JSON.stringify(nextCache);
44
+ return model;
45
+ };
46
+ const syncFromCloud = async (bucket, client) => {
47
+ const remote = await loadFromCloud(bucket.base, client);
48
+ const local = storageClient();
49
+ for (const key of Object.keys(local)) {
50
+ if (!(key in remote)) {
51
+ delete local[key];
52
+ }
53
+ }
54
+ for (const [key, value] of Object.entries(remote)) {
55
+ local[key] = value;
56
+ }
57
+ };
58
+ export const backupCloud = async (bucket) => {
59
+ const client = new AwsClient({
60
+ accessKeyId: bucket.accessKeyId,
61
+ secretAccessKey: bucket.secretAccessKey,
62
+ });
63
+ await syncFromCloud(bucket, client);
64
+ setInterval(() => syncFromCloud(bucket, client), 15 * 60 * 1000);
65
+ // Continuous sync: mirror local changes to cloud
66
+ addEventListener(STORAGE_EVENT, async (event) => {
67
+ let key = event.detail.key;
68
+ if (!key.startsWith("monoidentity/"))
69
+ return;
70
+ key = key.slice("monoidentity/".length);
71
+ if (isJunk(key))
72
+ return;
73
+ console.debug("[monoidentity cloud] saving", key);
74
+ const url = `${bucket.base}/${key}`;
75
+ try {
76
+ if (event.detail.value != undefined) {
77
+ // PUT content (unconditional to start; you can add If-Match/If-None-Match for safety)
78
+ const r = await client.fetch(url, {
79
+ method: "PUT",
80
+ headers: { "content-type": "application/octet-stream" },
81
+ body: event.detail.value,
82
+ });
83
+ if (!r.ok)
84
+ throw new Error(`PUT ${key} failed: ${r.status}`);
85
+ // Update cache
86
+ const etag = r.headers.get("etag")?.replaceAll('"', "");
87
+ if (etag) {
88
+ const cache = JSON.parse(localStorage[CLOUD_CACHE_KEY] || "{}");
89
+ cache[key] = { etag, content: event.detail.value };
90
+ localStorage[CLOUD_CACHE_KEY] = JSON.stringify(cache);
91
+ }
92
+ }
93
+ else {
94
+ // DELETE key
95
+ const r = await client.fetch(url, { method: "DELETE" });
96
+ if (!r.ok && r.status != 404)
97
+ throw new Error(`DELETE ${key} failed: ${r.status}`);
98
+ const cache = JSON.parse(localStorage[CLOUD_CACHE_KEY] || "{}");
99
+ delete cache[key];
100
+ localStorage[CLOUD_CACHE_KEY] = JSON.stringify(cache);
101
+ }
102
+ }
103
+ catch (err) {
104
+ console.warn("[monoidentity cloud] sync failed", key, err);
105
+ }
106
+ });
107
+ };
@@ -0,0 +1 @@
1
+ export declare const backupLocally: (requestBackup: (startBackup: () => void) => void) => Promise<void>;
@@ -0,0 +1,85 @@
1
+ import { createStore, get, set } from "idb-keyval";
2
+ import { STORAGE_EVENT, storageClient } from "./storageclient.svelte.js";
3
+ import { canBackup } from "../utils-transport.js";
4
+ const saveToDir = (dir) => {
5
+ let dirCache = {};
6
+ const getDirCached = async (route) => {
7
+ let key = "";
8
+ let parent = dir;
9
+ for (const path of route) {
10
+ key += "/";
11
+ key += path;
12
+ if (!dirCache[key]) {
13
+ dirCache[key] = await parent.getDirectoryHandle(path, { create: true });
14
+ }
15
+ parent = dirCache[key];
16
+ }
17
+ return parent;
18
+ };
19
+ addEventListener(STORAGE_EVENT, async (event) => {
20
+ let key = event.detail.key;
21
+ if (!key.startsWith("monoidentity/"))
22
+ return;
23
+ key = key.slice("monoidentity/".length);
24
+ const pathParts = key.split("/");
25
+ const name = pathParts.at(-1);
26
+ const value = event.detail.value;
27
+ console.debug("[monoidentity backup] saving", name);
28
+ const parent = await getDirCached(pathParts.slice(0, -1));
29
+ if (value != undefined) {
30
+ const file = await parent.getFileHandle(name, { create: true });
31
+ const writable = await file.createWritable();
32
+ await writable.write(value);
33
+ await writable.close();
34
+ }
35
+ else {
36
+ await parent.removeEntry(name);
37
+ }
38
+ });
39
+ };
40
+ export const backupLocally = async (requestBackup) => {
41
+ if (!canBackup)
42
+ return;
43
+ if (localStorage["monoidentity-x/backup"] == "off")
44
+ return;
45
+ const handles = createStore("monoidentity-x", "handles");
46
+ if (localStorage["monoidentity-x/backup"] == "on") {
47
+ const dir = await get("backup", handles);
48
+ if (!dir)
49
+ throw new Error("No backup handle found");
50
+ saveToDir(dir);
51
+ }
52
+ else {
53
+ localStorage["monoidentity-x/backup"] = "off";
54
+ requestBackup(async () => {
55
+ const dir = await showDirectoryPicker({ mode: "readwrite" });
56
+ await set("backup", dir, handles);
57
+ localStorage["monoidentity-x/backup"] = "on";
58
+ // Restore from backup
59
+ const backup = {};
60
+ const traverse = async (d, path) => {
61
+ for await (const entry of d.values()) {
62
+ if (entry.kind == "file") {
63
+ const file = await entry.getFile();
64
+ const text = await file.text();
65
+ backup[`${path}${entry.name}`] = text;
66
+ }
67
+ else if (entry.kind == "directory") {
68
+ await traverse(entry, `${path}${entry.name}/`);
69
+ }
70
+ }
71
+ };
72
+ await traverse(dir, "");
73
+ if (Object.keys(backup).length) {
74
+ const client = storageClient();
75
+ for (const key in backup) {
76
+ console.debug("[monoidentity backup] loading", key);
77
+ client[key] = backup[key];
78
+ }
79
+ location.reload();
80
+ return;
81
+ }
82
+ saveToDir(dir);
83
+ });
84
+ }
85
+ };
@@ -0,0 +1,10 @@
1
+ declare global {
2
+ interface WindowEventMap {
3
+ "monoidentity-storage": CustomEvent<{
4
+ key: string;
5
+ value: string | undefined;
6
+ }>;
7
+ }
8
+ }
9
+ export declare const STORAGE_EVENT = "monoidentity-storage";
10
+ export declare const storageClient: (prefix?: (key: string) => string, unprefix?: (key: string) => string | undefined, serialize?: (data: any) => string, deserialize?: (data: string) => any) => Record<string, any>;
@@ -0,0 +1,69 @@
1
+ export const STORAGE_EVENT = "monoidentity-storage";
2
+ const announce = (key, value) => {
3
+ // Announce to all, even third parties
4
+ window.dispatchEvent(new CustomEvent(STORAGE_EVENT, { detail: { key, value } }));
5
+ };
6
+ const storageCounters = $state({});
7
+ let allCounter = $state(0);
8
+ const increment = (key) => {
9
+ storageCounters[key] = (storageCounters[key] || 0) + 1;
10
+ allCounter++;
11
+ };
12
+ addEventListener(STORAGE_EVENT, (event) => increment(event.detail.key));
13
+ addEventListener("storage", (event) => {
14
+ if (event.storageArea != localStorage)
15
+ return;
16
+ if (!event.key)
17
+ return;
18
+ increment(event.key);
19
+ });
20
+ export const storageClient = (prefix, unprefix, serialize, deserialize) => {
21
+ if (prefix) {
22
+ const oldPrefix = prefix;
23
+ prefix = (key) => `monoidentity/${oldPrefix(key)}`;
24
+ }
25
+ else {
26
+ prefix = (key) => `monoidentity/${key}`;
27
+ }
28
+ return new Proxy({}, {
29
+ get(_, key) {
30
+ key = prefix(key);
31
+ storageCounters[key];
32
+ const raw = localStorage[key];
33
+ return raw && deserialize ? deserialize(raw) : raw;
34
+ },
35
+ set(_, key, value) {
36
+ key = prefix(key);
37
+ localStorage[key] = serialize ? serialize(value) : value;
38
+ announce(key, value);
39
+ return true;
40
+ },
41
+ deleteProperty(_, key) {
42
+ key = prefix(key);
43
+ delete localStorage[key];
44
+ announce(key);
45
+ return true;
46
+ },
47
+ ownKeys() {
48
+ allCounter;
49
+ const keys = [];
50
+ for (let key in localStorage) {
51
+ if (!key.startsWith("monoidentity/"))
52
+ continue;
53
+ key = key.slice("monoidentity/".length);
54
+ if (unprefix) {
55
+ const unprefixed = unprefix(key);
56
+ if (!unprefixed)
57
+ continue;
58
+ key = unprefixed;
59
+ }
60
+ keys.push(key);
61
+ }
62
+ return keys;
63
+ },
64
+ getOwnPropertyDescriptor(_, key) {
65
+ key = prefix(key);
66
+ return Reflect.getOwnPropertyDescriptor(localStorage, key);
67
+ },
68
+ });
69
+ };
package/dist/storage.d.ts CHANGED
@@ -1,12 +1,10 @@
1
- export declare const conf: (i: Record<string, string>, a: string) => void;
2
- export declare const LOGIN_RECOGNIZED_PATH = ".core/login.encjson";
1
+ export declare const conf: (a: string) => void;
3
2
  export declare const getLoginRecognized: () => {
4
3
  email: string;
5
4
  password: string;
6
5
  };
7
- export declare const VERIFICATION_PATH = ".core/verification.jwt";
8
- export declare const getVerification: () => Promise<string>;
6
+ export declare const setLoginRecognized: (login: string) => void;
7
+ export declare const getVerification: () => Promise<any>;
9
8
  export declare const setVerification: (jwt: string) => void;
10
- export declare const useVerification: (jwt: string) => Promise<import("@tsndr/cloudflare-worker-jwt").JwtData<{}, {}>>;
11
9
  export declare const getStorage: (realm: "config" | "cache") => Record<string, any>;
12
- export declare const getScopedFS: (dir: string) => Record<string, string>;
10
+ export declare const getScopedFS: (dir: string) => Record<string, any>;
package/dist/storage.js CHANGED
@@ -1,128 +1,38 @@
1
1
  import { stringify, parse } from "devalue";
2
2
  import { parse as useSchema } from "valibot";
3
3
  import { decode } from "./utils-base36.js";
4
- import { createStore } from "./storage/createstore.js";
5
4
  import { login as loginSchema } from "./utils-transport.js";
6
5
  import { verify } from "@tsndr/cloudflare-worker-jwt";
7
6
  import publicKey from "./verification/public-key.js";
8
- let implementation;
9
- let app = "";
10
- export const conf = (i, a) => {
11
- implementation = i;
7
+ import { storageClient } from "./storage/storageclient.svelte.js";
8
+ let app = "unknown";
9
+ export const conf = (a) => {
12
10
  app = a;
13
11
  };
14
- export const LOGIN_RECOGNIZED_PATH = ".core/login.encjson";
12
+ const LOGIN_RECOGNIZED_PATH = ".core/login.encjson";
15
13
  export const getLoginRecognized = () => {
16
- if (!implementation)
17
- throw new Error("No implementation set");
18
- const login = implementation[LOGIN_RECOGNIZED_PATH];
14
+ const client = storageClient();
15
+ const login = client[LOGIN_RECOGNIZED_PATH];
19
16
  if (!login)
20
17
  throw new Error("No login found");
21
18
  return useSchema(loginSchema, JSON.parse(decode(login)));
22
19
  };
23
- export const VERIFICATION_PATH = ".core/verification.jwt";
20
+ export const setLoginRecognized = (login) => {
21
+ const client = storageClient();
22
+ client[LOGIN_RECOGNIZED_PATH] = login;
23
+ };
24
+ const VERIFICATION_PATH = ".core/verification.jwt";
24
25
  export const getVerification = async () => {
25
- if (!implementation)
26
- throw new Error("No implementation set");
27
- const jwt = implementation[VERIFICATION_PATH];
26
+ const client = storageClient();
27
+ const jwt = client[VERIFICATION_PATH];
28
28
  if (!jwt)
29
29
  throw new Error("No verification found");
30
30
  await verify(jwt, publicKey, { algorithm: "ES256", throwError: true });
31
31
  return jwt;
32
32
  };
33
33
  export const setVerification = (jwt) => {
34
- if (!implementation)
35
- throw new Error("No implementation set");
36
- implementation[VERIFICATION_PATH] = jwt;
37
- };
38
- export const useVerification = async (jwt) => {
39
- const result = await verify(jwt, publicKey, { algorithm: "ES256", throwError: true });
40
- return result;
34
+ const client = storageClient();
35
+ client[VERIFICATION_PATH] = jwt;
41
36
  };
42
- const withPrefix = (obj, prefix, unprefix) => new Proxy(obj, {
43
- get(target, prop) {
44
- return target[prefix(prop)];
45
- },
46
- set(target, prop, value) {
47
- target[prefix(prop)] = value;
48
- return true;
49
- },
50
- has(target, prop) {
51
- return prefix(prop) in target;
52
- },
53
- deleteProperty(target, prop) {
54
- return delete target[prefix(prop)];
55
- },
56
- ownKeys(target) {
57
- if (typeof unprefix != "function") {
58
- throw new Error("unprefix must be a function");
59
- }
60
- return Object.keys(target)
61
- .map((key) => unprefix(key))
62
- .filter((key) => typeof key == "string");
63
- },
64
- getOwnPropertyDescriptor(target, prop) {
65
- return Reflect.getOwnPropertyDescriptor(target, prefix(prop));
66
- },
67
- });
68
- export const getStorage = (realm) => withPrefix(createStore({
69
- get(key) {
70
- if (!implementation)
71
- throw new Error("No implementation set");
72
- const item = implementation[key];
73
- return item ? parse(item) : undefined;
74
- },
75
- set(key, value) {
76
- if (!implementation)
77
- throw new Error("No implementation set");
78
- implementation[key] = stringify(value);
79
- return true;
80
- },
81
- has(key) {
82
- if (!implementation)
83
- throw new Error("No implementation set");
84
- return key in implementation;
85
- },
86
- deleteProperty(key) {
87
- if (!implementation)
88
- throw new Error("No implementation set");
89
- return delete implementation[key];
90
- },
91
- }), (text) => {
92
- if (!app)
93
- throw new Error("No app set");
94
- return `.${realm}/${app}/${text}.devalue`;
95
- });
96
- export const getScopedFS = (dir) => withPrefix(createStore({
97
- get(key) {
98
- if (!implementation)
99
- throw new Error("No implementation set");
100
- return implementation[key];
101
- },
102
- set(key, value) {
103
- if (!implementation)
104
- throw new Error("No implementation set");
105
- implementation[key] = value;
106
- return true;
107
- },
108
- has(key) {
109
- if (!implementation)
110
- throw new Error("No implementation set");
111
- return key in implementation;
112
- },
113
- deleteProperty(key) {
114
- if (!implementation)
115
- throw new Error("No implementation set");
116
- return delete implementation[key];
117
- },
118
- ownKeys() {
119
- if (!implementation)
120
- throw new Error("No implementation set");
121
- return Object.keys(implementation);
122
- },
123
- getOwnPropertyDescriptor(key) {
124
- if (!implementation)
125
- throw new Error("No implementation set");
126
- return Reflect.getOwnPropertyDescriptor(implementation, key);
127
- },
128
- }), (text) => `${dir}/${text}`, (text) => (text.startsWith(`${dir}/`) ? text.slice(dir.length + 1) : undefined));
37
+ export const getStorage = (realm) => storageClient((key) => `.${realm}/${app}/${key}.devalue`, undefined, stringify, parse);
38
+ export const getScopedFS = (dir) => storageClient((key) => `${dir}/${key}`, (key) => (key.startsWith(dir + "/") ? key.slice(dir.length + 1) : undefined));
@@ -1,14 +1,17 @@
1
1
  import {} from "./utils-transport.js";
2
- import { createLocalStorage } from "./storage/createlocalstorage.js";
3
- import { wrapBackup } from "./storage/wrapbackup.js";
4
- import { wrapCloud } from "./storage/wrapcloud.js";
5
- import { LOGIN_RECOGNIZED_PATH, conf } from "./storage.js";
2
+ // import { createLocalStorage } from "./storage/createlocalstorage.js";
3
+ // import { wrapBackup } from "./storage/wrapbackup.js";
4
+ // import { wrapCloud } from "./storage/wrapcloud.js";
5
+ import { conf, setLoginRecognized } from "./storage.js";
6
+ import { backupLocally } from "./storage/backuplocally.js";
7
+ import { backupCloud } from "./storage/backupcloud.js";
6
8
  export const trackReady = async (app, intents, requestBackup) => {
7
- const params = new URLSearchParams(location.hash.slice(1));
9
+ conf(app);
8
10
  let setup = localStorage["monoidentity-x/setup"]
9
11
  ? JSON.parse(localStorage["monoidentity-x/setup"])
10
12
  : undefined;
11
13
  let provisions = [];
14
+ const params = new URLSearchParams(location.hash.slice(1));
12
15
  const cb = params.get("monoidentitycallback");
13
16
  if (cb) {
14
17
  history.replaceState(null, "", location.pathname);
@@ -27,27 +30,17 @@ export const trackReady = async (app, intents, requestBackup) => {
27
30
  redirectURI: location.origin,
28
31
  });
29
32
  location.href = target.toString();
30
- await new Promise(() => {
31
- /* never resolves */
32
- });
33
- throw new Error("unreachable");
33
+ throw new Error("halt: redirecting");
34
34
  }
35
- let storage;
36
- if (setup.method == "cloud") {
37
- storage = createLocalStorage();
38
- storage = await wrapCloud(storage, setup);
39
- }
40
- else if (setup.method == "localStorage") {
41
- storage = createLocalStorage();
42
- storage = wrapBackup(storage, requestBackup);
35
+ if (setup.method == "localStorage") {
36
+ await backupLocally(requestBackup);
43
37
  }
44
- else {
45
- throw new Error("unreachable");
38
+ if (setup.method == "cloud") {
39
+ await backupCloud(setup);
46
40
  }
47
- conf(storage, app);
48
41
  for (const provision of provisions) {
49
42
  if ("createLoginRecognized" in provision) {
50
- storage[LOGIN_RECOGNIZED_PATH] = provision.createLoginRecognized;
43
+ setLoginRecognized(provision.createLoginRecognized);
51
44
  }
52
45
  }
53
46
  };
@@ -0,0 +1 @@
1
+ export declare const useVerification: (jwt: string) => Promise<import("@tsndr/cloudflare-worker-jwt").JwtData<{}, {}>>;
@@ -0,0 +1,6 @@
1
+ import { verify } from "@tsndr/cloudflare-worker-jwt";
2
+ import publicKey from "./verification/public-key.js";
3
+ export const useVerification = async (jwt) => {
4
+ const result = await verify(jwt, publicKey, { algorithm: "ES256", throwError: true });
5
+ return result;
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monoidentity",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "repository": "KTibow/monoidentity",
5
5
  "author": {
6
6
  "name": "KTibow"
@@ -15,16 +15,15 @@
15
15
  "sideEffects": [
16
16
  "**/*.css"
17
17
  ],
18
- "types": "./dist/index.d.ts",
19
18
  "type": "module",
20
19
  "exports": {
21
20
  ".": {
22
- "types": "./dist/index.d.ts",
23
- "import": "./dist/index.js"
21
+ "types": "./dist/+client.d.ts",
22
+ "svelte": "./dist/+client.js"
24
23
  },
25
- "./Monoidentity.svelte": {
26
- "types": "./dist/Monoidentity.svelte.d.ts",
27
- "svelte": "./dist/Monoidentity.svelte"
24
+ "./server": {
25
+ "types": "./dist/+server.d.ts",
26
+ "import": "./dist/+server.js"
28
27
  }
29
28
  },
30
29
  "peerDependencies": {
package/dist/index.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export { getLoginRecognized, getVerification, useVerification, getStorage, getScopedFS, } from "./storage.js";
2
- export { retrieveVerification } from "./storage-attest.js";
3
- export { default as rawAttest } from "./verification/attest-remote.js";
4
- export { trackReady } from "./trackready.js";
5
- export { encode, decode } from "./utils-base36.js";
package/dist/index.js DELETED
@@ -1,5 +0,0 @@
1
- export { getLoginRecognized, getVerification, useVerification, getStorage, getScopedFS, } from "./storage.js";
2
- export { retrieveVerification } from "./storage-attest.js";
3
- export { default as rawAttest } from "./verification/attest-remote.js";
4
- export { trackReady } from "./trackready.js";
5
- export { encode, decode } from "./utils-base36.js";
@@ -1,3 +0,0 @@
1
- import type { AwsClient } from "aws4fetch";
2
- export declare const CLOUD_CACHE_KEY = "monoidentity-x/cloud-cache";
3
- export declare const loadCloud: (url: string, client: AwsClient) => Promise<Record<string, string>>;
@@ -1,39 +0,0 @@
1
- const bytesToBinaryString = (bytes) => {
2
- const chunkSize = 8192;
3
- let result = "";
4
- for (let i = 0; i < bytes.length; i += chunkSize) {
5
- const chunk = bytes.subarray(i, i + chunkSize);
6
- result += String.fromCharCode.apply(null, chunk);
7
- }
8
- return result;
9
- };
10
- export const CLOUD_CACHE_KEY = "monoidentity-x/cloud-cache";
11
- export const loadCloud = async (url, client) => {
12
- // List bucket (source of truth)
13
- const listResponse = await client.fetch(url);
14
- const listXml = await listResponse.text();
15
- const objects = [...listXml.matchAll(/<Key>(.*?)<\/Key>.*?<ETag>(.*?)<\/ETag>/gs)]
16
- .map((m) => m.slice(1).map((s) => s.replaceAll("&quot;", `"`).replaceAll("&apos;", `'`)))
17
- .map(([key, etag]) => ({ key, etag: etag.replaceAll(`"`, "") }))
18
- .filter(({ key }) => !key.includes(".obsidian"));
19
- // Build model by enriching list with content
20
- const model = {};
21
- const cache = JSON.parse(localStorage[CLOUD_CACHE_KEY] || "{}");
22
- const newCache = {};
23
- await Promise.all(objects.map(async ({ key, etag }) => {
24
- // Cache hit: reuse existing content
25
- if (cache[key]?.etag == etag) {
26
- model[key] = cache[key].content;
27
- newCache[key] = cache[key];
28
- return;
29
- }
30
- // Cache miss: fetch from S3
31
- const response = await client.fetch(`${url}/${key}`);
32
- const buffer = await response.arrayBuffer();
33
- const content = bytesToBinaryString(new Uint8Array(buffer));
34
- model[key] = content;
35
- newCache[key] = { etag, content };
36
- }));
37
- localStorage[CLOUD_CACHE_KEY] = JSON.stringify(newCache);
38
- return model;
39
- };
@@ -1 +0,0 @@
1
- export declare const md5: (s: string) => string;
@@ -1,52 +0,0 @@
1
- // Based on https://github.com/jbt/tiny-hashes/blob/master/md5/md5.js
2
- // @ts-nocheck
3
- var k = [], i = 0;
4
- for (; i < 64;) {
5
- k[i] = 0 | (Math.sin(++i % Math.PI) * 4294967296);
6
- }
7
- export const md5 = (s) => {
8
- for (let i = 0; i < s.length; i++) {
9
- if (s.charCodeAt(i) > 255) {
10
- // We weren't passed a binary string
11
- console.log("?!");
12
- s = new TextDecoder().decode(new TextEncoder().encode(s));
13
- break;
14
- }
15
- }
16
- var b, c, d, h = [(b = 0x67452301), (c = 0xefcdab89), ~b, ~c], words = [], j = s + "\x80", a = j.length;
17
- s = (--a / 4 + 2) | 15;
18
- // See "Length bits" in notes
19
- words[--s] = a * 8;
20
- for (; ~a;) {
21
- // a !== -1
22
- words[a >> 2] |= j.charCodeAt(a) << (8 * a--);
23
- }
24
- for (i = j = 0; i < s; i += 16) {
25
- a = h;
26
- for (; j < 64; a = [
27
- (d = a[3]),
28
- b +
29
- (((d =
30
- a[0] +
31
- [(b & c) | (~b & d), (d & b) | (~d & c), b ^ c ^ d, c ^ (b | ~d)][(a = j >> 4)] +
32
- k[j] +
33
- ~~words[i | ([j, 5 * j + 1, 3 * j + 5, 7 * j][a] & 15)]) <<
34
- (a = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21][4 * a + (j++ % 4)])) |
35
- (d >>> -a)),
36
- b,
37
- c,
38
- ]) {
39
- b = a[1] | 0;
40
- c = a[2];
41
- }
42
- // See "Integer safety" in notes
43
- for (j = 4; j;)
44
- h[--j] += a[j];
45
- // j === 0
46
- }
47
- for (s = ""; j < 32;) {
48
- s += ((h[j >> 3] >> ((1 ^ j++) * 4)) & 15).toString(16);
49
- // s += ((h[j >> 3] >> (4 ^ 4 * j++)) & 15).toString(16);
50
- }
51
- return s;
52
- };
@@ -1,16 +0,0 @@
1
- import type { Dict } from "./createstore.js";
2
- type Modification = {
3
- type: "set";
4
- old?: string;
5
- new: string;
6
- } | {
7
- type: "delete";
8
- };
9
- type Transmit = (path: string, mod: Modification) => Promise<void>;
10
- export declare const wrapWithReplay: (storage: Dict) => {
11
- proxy: Dict;
12
- flush: () => Promise<void>;
13
- setTransmit(tx: Transmit): void;
14
- load(external: Dict): void;
15
- };
16
- export {};
@@ -1,95 +0,0 @@
1
- import { throttle } from "./_timing.js";
2
- export const wrapWithReplay = (storage) => {
3
- let modifications = {};
4
- if (localStorage["monoidentity-x/backup/modifications"]) {
5
- modifications = JSON.parse(localStorage["monoidentity-x/backup/modifications"]);
6
- }
7
- const save = () => {
8
- if (Object.keys(modifications).length) {
9
- localStorage["monoidentity-x/backup/modifications"] = JSON.stringify(modifications);
10
- }
11
- else {
12
- delete localStorage["monoidentity-x/backup/modifications"];
13
- }
14
- };
15
- window.addEventListener("beforeunload", save);
16
- window.addEventListener("pagehide", save);
17
- let transmit;
18
- const flush = throttle(async () => {
19
- try {
20
- const tx = transmit;
21
- if (!tx)
22
- return;
23
- const paths = Object.keys(modifications);
24
- const tasks = paths.map((path) => (async () => {
25
- const mod = modifications[path];
26
- delete modifications[path];
27
- if (mod.type == "set" && mod.old == mod.new) {
28
- return;
29
- }
30
- await tx(path, mod);
31
- })()
32
- .catch((err) => {
33
- console.warn(`[monoidentity] transmitting "${path}" failed`, err);
34
- }));
35
- await Promise.all(tasks);
36
- }
37
- finally {
38
- save();
39
- }
40
- }, 1000);
41
- const proxy = new Proxy(storage, {
42
- set(target, prop, value) {
43
- let old = target[prop];
44
- target[prop] = value;
45
- const oldMod = modifications[prop];
46
- if (oldMod?.type == "set")
47
- old = oldMod.old;
48
- modifications[prop] = { type: "set", old, new: value };
49
- flush();
50
- return true;
51
- },
52
- deleteProperty(target, prop) {
53
- const success = delete target[prop];
54
- if (success) {
55
- modifications[prop] = { type: "delete" };
56
- flush();
57
- }
58
- return success;
59
- },
60
- });
61
- return {
62
- proxy,
63
- flush,
64
- setTransmit(tx) {
65
- transmit = tx;
66
- },
67
- // load() just resets to the external version
68
- // if you have modifications, flush() them first
69
- load(external) {
70
- modifications = {};
71
- save();
72
- for (const [key, value] of Object.entries(external)) {
73
- storage[key] = value;
74
- }
75
- for (const key of Object.keys(storage)) {
76
- if (!(key in external)) {
77
- delete storage[key];
78
- }
79
- }
80
- // // If for *some* reason, there are pending modifications, rebase and resend them
81
- // for (const [key, mod] of Object.entries(oldModifications)) {
82
- // if (mod.type == "set") {
83
- // if (proxy[key] != mod.old)
84
- // console.warn(
85
- // `[monoidentity] modification to "${key}" will be force applied over external change`,
86
- // );
87
- // proxy[key] = mod.new;
88
- // } else if (mod.type == "delete") {
89
- // delete proxy[key];
90
- // }
91
- // }
92
- // flush();
93
- },
94
- };
95
- };
@@ -1 +0,0 @@
1
- export declare function throttle(fn: () => Promise<unknown>, delay: number): () => Promise<void>;
@@ -1,21 +0,0 @@
1
- export function throttle(fn, delay) {
2
- let isRunning = false;
3
- let hasPendingCall = false;
4
- return async () => {
5
- hasPendingCall = true;
6
- if (isRunning) {
7
- return;
8
- }
9
- try {
10
- isRunning = true;
11
- while (hasPendingCall) {
12
- hasPendingCall = false;
13
- await new Promise((resolve) => setTimeout(resolve, delay));
14
- await fn();
15
- }
16
- }
17
- finally {
18
- isRunning = false;
19
- }
20
- };
21
- }
@@ -1 +0,0 @@
1
- export declare const createLocalStorage: () => Record<string, string>;
@@ -1,42 +0,0 @@
1
- import { createStore } from "./createstore.js";
2
- const prefix = "monoidentity/";
3
- const prefixed = (key) => `${prefix}${key}`;
4
- const unprefixed = (key) => {
5
- if (!key.startsWith(prefix))
6
- throw new Error("Key is not prefixed");
7
- return key.slice(prefix.length);
8
- };
9
- export const createLocalStorage = () => createStore({
10
- has(key) {
11
- return typeof key == "string" && localStorage[prefixed(key)] !== undefined;
12
- },
13
- get(key) {
14
- if (typeof key != "string")
15
- return undefined;
16
- return localStorage[prefixed(key)];
17
- },
18
- set(key, value) {
19
- if (typeof key != "string")
20
- return false;
21
- localStorage[prefixed(key)] = value;
22
- return true;
23
- },
24
- deleteProperty(key) {
25
- if (typeof key != "string")
26
- return false;
27
- return delete localStorage[prefixed(key)];
28
- },
29
- ownKeys() {
30
- const keys = [];
31
- for (let i = 0; i < localStorage.length; i++) {
32
- const key = localStorage.key(i);
33
- if (key && key.startsWith(prefix)) {
34
- keys.push(unprefixed(key));
35
- }
36
- }
37
- return keys;
38
- },
39
- getOwnPropertyDescriptor(key) {
40
- return Reflect.getOwnPropertyDescriptor(localStorage, prefixed(key));
41
- },
42
- });
@@ -1,11 +0,0 @@
1
- export type Dict = Record<string, string>;
2
- type ProxyHandlerWithoutTarget = {
3
- has?(p: string | symbol): boolean;
4
- get?(p: string | symbol, receiver: any): any;
5
- set?(p: string | symbol, newValue: any, receiver: any): boolean;
6
- deleteProperty?(p: string | symbol): boolean;
7
- ownKeys?(): ArrayLike<string | symbol>;
8
- getOwnPropertyDescriptor?(p: string | symbol): PropertyDescriptor | undefined;
9
- };
10
- export declare const createStore: <T>(implementation: ProxyHandlerWithoutTarget) => Record<string, T>;
11
- export {};
@@ -1,9 +0,0 @@
1
- export const createStore = (implementation) => {
2
- const target = {};
3
- const handler = {};
4
- for (const key of Object.keys(implementation)) {
5
- const trap = implementation[key];
6
- handler[key] = (_, ...args) => trap(...args);
7
- }
8
- return new Proxy(target, handler);
9
- };
@@ -1,2 +0,0 @@
1
- import { type Dict } from "./createstore.js";
2
- export declare const wrapBackup: (storage: Dict, requestBackup: (startBackup: () => void) => void) => Dict;
@@ -1,88 +0,0 @@
1
- import {} from "./createstore.js";
2
- import { wrapWithReplay } from "./_replay.js";
3
- import { canBackup } from "../utils-transport.js";
4
- import { get, set, createStore } from "idb-keyval";
5
- export const wrapBackup = (storage, requestBackup) => {
6
- if (!canBackup)
7
- return storage;
8
- if (localStorage["monoidentity-x/backup"] == "off")
9
- return storage;
10
- const { proxy, flush, setTransmit, load } = wrapWithReplay(storage);
11
- const store = createStore("monoidentity-x", "handles");
12
- const getDir = async () => {
13
- const handle = await get("backup", store);
14
- if (!handle)
15
- throw new Error("No backup handle found");
16
- return handle;
17
- };
18
- const setDir = async (dir) => {
19
- await set("backup", dir, store);
20
- };
21
- const init = async (dir) => {
22
- let dirCache = {};
23
- const getDirCached = async (route) => {
24
- let key = "";
25
- let parent = dir;
26
- for (const path of route) {
27
- key += "/";
28
- key += path;
29
- if (!dirCache[key]) {
30
- dirCache[key] = await parent.getDirectoryHandle(path, { create: true });
31
- }
32
- parent = dirCache[key];
33
- }
34
- return parent;
35
- };
36
- setTransmit(async (path, mod) => {
37
- const pathParts = path.split("/");
38
- const parent = await getDirCached(pathParts.slice(0, -1));
39
- if (mod.type == "set") {
40
- const file = await parent.getFileHandle(pathParts.at(-1), { create: true });
41
- const writable = await file.createWritable();
42
- await writable.write(mod.new);
43
- await writable.close();
44
- }
45
- else if (mod.type == "delete") {
46
- await parent.removeEntry(pathParts.at(-1));
47
- }
48
- });
49
- await flush();
50
- };
51
- if (localStorage["monoidentity-x/backup"] == "on") {
52
- getDir()
53
- .then((dir) => init(dir))
54
- .catch(() => {
55
- delete localStorage["monoidentity-x/backup"];
56
- });
57
- }
58
- else {
59
- localStorage["monoidentity-x/backup"] = "off";
60
- requestBackup(async () => {
61
- const dir = await showDirectoryPicker({ mode: "readwrite" });
62
- await setDir(dir);
63
- await init(dir);
64
- // Restore from backup
65
- const backup = {};
66
- const traverse = async (d, path) => {
67
- for await (const entry of d.values()) {
68
- if (entry.kind == "file") {
69
- const file = await entry.getFile();
70
- const text = await file.text();
71
- backup[`${path}${entry.name}`] = text;
72
- }
73
- else if (entry.kind == "directory") {
74
- await traverse(entry, `${path}${entry.name}/`);
75
- }
76
- }
77
- };
78
- await traverse(dir, "");
79
- const hasBackup = Boolean(Object.keys(backup).length);
80
- if (hasBackup)
81
- load(backup);
82
- localStorage["monoidentity-x/backup"] = "on";
83
- if (hasBackup)
84
- location.reload();
85
- });
86
- }
87
- return proxy;
88
- };
@@ -1,3 +0,0 @@
1
- import type { Dict } from "./createstore.js";
2
- import type { Bucket } from "../utils-bucket.js";
3
- export declare const wrapCloud: (storage: Dict, bucket: Bucket) => Promise<Dict>;
@@ -1,54 +0,0 @@
1
- import { wrapWithReplay } from "./_replay.js";
2
- import { AwsClient } from "aws4fetch";
3
- import { CLOUD_CACHE_KEY, loadCloud } from "./_cloud.js";
4
- import { md5 } from "./_md5.js";
5
- export const wrapCloud = async (storage, bucket) => {
6
- const client = new AwsClient({
7
- accessKeyId: bucket.accessKeyId,
8
- secretAccessKey: bucket.secretAccessKey,
9
- });
10
- const { proxy, setTransmit, flush, load } = wrapWithReplay(storage);
11
- const isFirstLoad = !localStorage[CLOUD_CACHE_KEY];
12
- if (isFirstLoad) {
13
- const data = await loadCloud(bucket.base, client);
14
- load(data);
15
- }
16
- setTransmit(async (path, mod) => {
17
- if (mod.type == "set") {
18
- const url = `${bucket.base}/${path}`;
19
- const headers = new Headers();
20
- if (mod.old) {
21
- // Update only if current matches our known prior content
22
- const etagFromOld = md5(mod.old);
23
- headers.set("If-Match", `"${etagFromOld}"`);
24
- }
25
- else {
26
- // Create only if it does not already exist
27
- headers.set("If-None-Match", "*");
28
- }
29
- const r = await client.fetch(url, {
30
- method: "PUT",
31
- headers,
32
- body: mod.new,
33
- });
34
- if (!r.ok) {
35
- throw new Error(`Cloud is ${r.status}ing`);
36
- }
37
- }
38
- else if (mod.type == "delete") {
39
- const url = `${bucket.base}/${path}`;
40
- const r = await client.fetch(url, {
41
- method: "DELETE",
42
- });
43
- if (!r.ok)
44
- throw new Error(`Cloud is ${r.status}ing`);
45
- }
46
- });
47
- if (!isFirstLoad) {
48
- flush().then(async () => {
49
- const data = await loadCloud(bucket.base, client);
50
- load(data);
51
- });
52
- }
53
- return proxy;
54
- };