UNPKG

9.73 kBJavaScriptView Raw
1import localforage from 'localforage';
2import * as common from "./common/index.js";
3import * as identifiers from "./common/identifiers.js";
4import * as ipfs from "./ipfs/index.js";
5import * as pathing from "./path.js";
6import * as crypto from "./crypto/index.js";
7import * as storage from "./storage/index.js";
8import * as ucan from "./ucan/internal.js";
9import * as ucanPermissions from "./ucan/permissions.js";
10import { setup } from "./setup/internal.js";
11import * as did from "./did/index.js";
12import { USERNAME_STORAGE_KEY } from "./common/index.js";
13import { loadFileSystem } from "./filesystem.js";
14import FileSystem from "./fs/index.js";
15// SCENARIO
16export var Scenario;
17(function (Scenario) {
18 Scenario["NotAuthorised"] = "NOT_AUTHORISED";
19 Scenario["AuthSucceeded"] = "AUTH_SUCCEEDED";
20 Scenario["AuthCancelled"] = "AUTH_CANCELLED";
21 Scenario["Continuation"] = "CONTINUATION";
22})(Scenario || (Scenario = {}));
23// ERRORS
24/**
25 * Initialisation error
26 */
27export var InitialisationError;
28(function (InitialisationError) {
29 InitialisationError["InsecureContext"] = "INSECURE_CONTEXT";
30 InitialisationError["UnsupportedBrowser"] = "UNSUPPORTED_BROWSER";
31})(InitialisationError || (InitialisationError = {}));
32// INTIALISE
33/**
34 * Check if we're authenticated, process any lobby query-parameters present in the URL,
35 * and initiate the user's file system if authenticated (can be disabled).
36 *
37 * See `loadFileSystem` if you want to load the user's file system yourself.
38 * NOTE: Only works on the main/ui thread, as it uses `window.location`.
39 */
40export async function initialise(options) {
41 options = options || {};
42 const permissions = options.permissions || null;
43 const { autoRemoveUrlParams = true, rootKey } = options;
44 const maybeLoadFs = async (username) => {
45 return options.loadFileSystem === false
46 ? undefined
47 : await loadFileSystem(permissions, username, rootKey);
48 };
49 // Check if browser is supported
50 if (globalThis.isSecureContext === false)
51 throw InitialisationError.InsecureContext;
52 if (await isSupported() === false)
53 throw InitialisationError.UnsupportedBrowser;
54 // URL things
55 const url = new URL(window.location.href);
56 const authorised = url.searchParams.get("authorised");
57 const cancellation = url.searchParams.get("cancelled");
58 // Determine scenario
59 if (authorised) {
60 const newUser = url.searchParams.get("newUser") === "t";
61 const username = url.searchParams.get("username") || "";
62 await importClassifiedInfo(authorised === "via-postmessage"
63 ? await getClassifiedViaPostMessage()
64 : await ipfs.cat(authorised) // in any other case we expect it to be a CID
65 );
66 await storage.setItem(USERNAME_STORAGE_KEY, username);
67 if (autoRemoveUrlParams) {
68 url.searchParams.delete("authorised");
69 url.searchParams.delete("newUser");
70 url.searchParams.delete("username");
71 history.replaceState(null, document.title, url.toString());
72 }
73 if (permissions && await validateSecrets(permissions) === false) {
74 console.warn("Unable to validate filesystem secrets");
75 return scenarioNotAuthorised(permissions);
76 }
77 if (permissions && ucan.validatePermissions(permissions, username) === false) {
78 console.warn("Unable to validate UCAN permissions");
79 return scenarioNotAuthorised(permissions);
80 }
81 return scenarioAuthSucceeded(permissions, newUser, username, await maybeLoadFs(username));
82 }
83 else if (cancellation) {
84 const c = (() => {
85 switch (cancellation) {
86 case "DENIED": return "User denied authorisation";
87 default: return "Unknown reason";
88 }
89 })();
90 return scenarioAuthCancelled(permissions, c);
91 }
92 else {
93 // trigger build for internal ucan dictionary
94 await ucan.store([]);
95 }
96 const authedUsername = await common.authenticatedUsername();
97 if (authedUsername && permissions) {
98 const validSecrets = await validateSecrets(permissions);
99 const validUcans = ucan.validatePermissions(permissions, authedUsername);
100 if (validSecrets && validUcans) {
101 return scenarioContinuation(permissions, authedUsername, await maybeLoadFs(authedUsername));
102 }
103 else {
104 return scenarioNotAuthorised(permissions);
105 }
106 }
107 else if (authedUsername) {
108 return scenarioContinuation(permissions, authedUsername, await maybeLoadFs(authedUsername));
109 }
110 else {
111 return scenarioNotAuthorised(permissions);
112 }
113}
114/**
115 * Alias for `initialise`.
116 */
117export { initialise as initialize };
118// SUPPORTED
119export async function isSupported() {
120 return localforage.supports(localforage.INDEXEDDB)
121 // Firefox in private mode can't use indexedDB properly,
122 // so we test if we can actually make a database.
123 && await (() => new Promise(resolve => {
124 const db = indexedDB.open("testDatabase");
125 db.onsuccess = () => resolve(true);
126 db.onerror = () => resolve(false);
127 }))();
128}
129// EXPORT
130export * from "./auth.js";
131export * from "./filesystem.js";
132export * from "./common/version.js";
133export const fs = FileSystem;
134export * as apps from "./apps/index.js";
135export * as dataRoot from "./data-root.js";
136export * as did from "./did/index.js";
137export * as errors from "./errors.js";
138export * as lobby from "./lobby/index.js";
139export * as path from "./path.js";
140export * as setup from "./setup.js";
141export * as ucan from "./ucan/index.js";
142export * as dns from "./dns/index.js";
143export * as ipfs from "./ipfs/index.js";
144export * as keystore from "./keystore.js";
145export * as machinery from "./common/index.js";
146export * as crypto from "./crypto/index.js";
147export * as cbor from 'cborg';
148// ㊙️ ⚛ SCENARIOS
149function scenarioAuthSucceeded(permissions, newUser, username, fs) {
150 return {
151 scenario: Scenario.AuthSucceeded,
152 permissions,
153 authenticated: true,
154 throughLobby: true,
155 fs,
156 newUser,
157 username
158 };
159}
160function scenarioAuthCancelled(permissions, cancellationReason) {
161 return {
162 scenario: Scenario.AuthCancelled,
163 permissions,
164 authenticated: false,
165 throughLobby: true,
166 cancellationReason
167 };
168}
169function scenarioContinuation(permissions, username, fs) {
170 return {
171 scenario: Scenario.Continuation,
172 permissions,
173 authenticated: true,
174 newUser: false,
175 throughLobby: false,
176 fs,
177 username
178 };
179}
180function scenarioNotAuthorised(permissions) {
181 return {
182 scenario: Scenario.NotAuthorised,
183 permissions,
184 authenticated: false
185 };
186}
187async function importClassifiedInfo(classified) {
188 const info = JSON.parse(classified);
189 // Extract session key and its iv
190 const rawSessionKey = await crypto.keystore.decrypt(info.sessionKey);
191 // Decrypt secrets
192 const secretsStr = await crypto.aes.decryptGCM(info.secrets, rawSessionKey, info.iv);
193 const secrets = JSON.parse(secretsStr);
194 const fsSecrets = secrets.fs;
195 const ucans = secrets.ucans;
196 // Import read keys and bare name filters
197 await Promise.all(Object.entries(fsSecrets).map(async ([posixPath, { bareNameFilter, key }]) => {
198 const path = pathing.fromPosix(posixPath);
199 const readKeyId = await identifiers.readKey({ path });
200 const bareNameFilterId = await identifiers.bareNameFilter({ path });
201 await crypto.keystore.importSymmKey(key, readKeyId);
202 await storage.setItem(bareNameFilterId, bareNameFilter);
203 }));
204 // Add UCANs to the storage
205 await ucan.store(ucans);
206}
207async function getClassifiedViaPostMessage() {
208 const iframe = await new Promise(resolve => {
209 const iframe = document.createElement("iframe");
210 iframe.id = "webnative-secret-exchange";
211 iframe.style.width = "0";
212 iframe.style.height = "0";
213 iframe.style.border = "none";
214 iframe.style.display = "none";
215 document.body.appendChild(iframe);
216 iframe.onload = () => {
217 resolve(iframe);
218 };
219 iframe.src = `${setup.endpoints.lobby}/exchange.html`;
220 });
221 try {
222 const answer = new Promise((resolve, reject) => {
223 window.addEventListener("message", listen);
224 function listen(event) {
225 window.removeEventListener("message", listen);
226 if (event.data) {
227 resolve(event.data);
228 }
229 else {
230 reject(new Error("Can't import UCANs & readKey(s): Missing data"));
231 }
232 }
233 });
234 if (iframe.contentWindow == null)
235 throw new Error("Can't import UCANs & readKey(s): No access to its contentWindow");
236 const message = {
237 webnative: "exchange-secrets",
238 didExchange: await did.exchange()
239 };
240 iframe.contentWindow.postMessage(message, iframe.src);
241 return await answer;
242 }
243 finally {
244 document.body.removeChild(iframe);
245 }
246}
247async function validateSecrets(permissions) {
248 return ucanPermissions.paths(permissions).reduce((acc, path) => acc.then(async (bool) => {
249 if (bool === false)
250 return bool;
251 if (pathing.isBranch(pathing.Branch.Public, path))
252 return bool;
253 const keyName = await identifiers.readKey({ path });
254 return await crypto.keystore.keyExists(keyName);
255 }), Promise.resolve(true));
256}
257//# sourceMappingURL=index.js.map
\No newline at end of file