@graffiti-garden/implementation-decentralized 0.0.7 → 0.0.9

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.
@@ -37,6 +37,7 @@ import {
37
37
  type LabeledMessage,
38
38
  type MessageStream,
39
39
  } from "../1-services/4-inboxes";
40
+ import { GraffitiErrorUnauthorized } from "../1-services/utilities";
40
41
 
41
42
  import {
42
43
  StringEncoder,
@@ -133,6 +134,7 @@ export interface GraffitiDecentralizedOptions {
133
134
  }
134
135
 
135
136
  const CONCURRENCY = 16;
137
+ const LOCAL_STORAGE_REMEMBERED_HANDLE_KEY = "graffiti-login-remembered-handle";
136
138
 
137
139
  export class GraffitiDecentralized implements Graffiti {
138
140
  protected readonly dids = new DecentralizedIdentifiers();
@@ -188,10 +190,11 @@ export class GraffitiDecentralized implements Graffiti {
188
190
  this.sessionEvents.addEventListener("login", async (event) => {
189
191
  if (!(event instanceof CustomEvent)) return;
190
192
  const detail = event.detail as GraffitiLoginEvent["detail"];
191
- if (
192
- detail.error !== undefined &&
193
- !("manual" in detail && detail.manual)
194
- ) {
193
+
194
+ if (detail.error !== undefined) {
195
+ // If "manual", the window was closed by the user, nothing to do
196
+ if ("manual" in detail && detail.manual) return;
197
+
195
198
  alert("Login failed: " + detail.error.message);
196
199
  const actor = detail.session?.actor;
197
200
  let handle: string | undefined;
@@ -202,9 +205,46 @@ export class GraffitiDecentralized implements Graffiti {
202
205
  console.error("Failed to handle actor:", error);
203
206
  }
204
207
  }
208
+ // Open the login window again with the failed actor
205
209
  this.login_(handle);
210
+ return;
211
+ }
212
+
213
+ const actor = detail.session.actor;
214
+
215
+ let handle: string | undefined;
216
+ try {
217
+ handle = await this.actorToHandle(actor);
218
+ } catch (error) {
219
+ console.error("Failed to load handle from actor:", error);
220
+ }
221
+
222
+ if (handle) {
223
+ await this.rememberHandleInPasswordManager(handle, actor);
206
224
  }
207
225
  });
226
+
227
+ // If any methods throw an unauthorized error,
228
+ // log out the used session
229
+ for (const method of [
230
+ "post",
231
+ "get",
232
+ "delete",
233
+ "postMedia",
234
+ "getMedia",
235
+ "deleteMedia",
236
+ ]) {
237
+ const fn = (this as any)[method];
238
+ (this as any)[method] = async (...args: unknown[]) => {
239
+ try {
240
+ return await fn(...args);
241
+ } catch (e) {
242
+ const session = args[args.length - 1] as any;
243
+ await this.tryLogoutIfUnauthorized(e, session);
244
+ throw e;
245
+ }
246
+ };
247
+ }
208
248
  }
209
249
 
210
250
  readonly actorToHandle: Graffiti["actorToHandle"] =
@@ -241,7 +281,10 @@ export class GraffitiDecentralized implements Graffiti {
241
281
  const input = template?.querySelector(
242
282
  "#username",
243
283
  ) as HTMLInputElement | null;
244
- input?.setAttribute("value", proposedHandle);
284
+ const rememberedHandle =
285
+ proposedHandle ||
286
+ (await this.getRememberedHandleFromPasswordManager());
287
+ if (input) input.value = rememberedHandle ?? "";
245
288
  input?.addEventListener("focus", () => input?.select());
246
289
  setTimeout(() => input?.focus(), 0);
247
290
 
@@ -353,6 +396,105 @@ export class GraffitiDecentralized implements Graffiti {
353
396
  }
354
397
  }
355
398
 
399
+ protected async rememberHandleInPasswordManager(
400
+ handle: string,
401
+ actor: string,
402
+ ) {
403
+ if (typeof window === "undefined") return;
404
+
405
+ const credentials = window.navigator.credentials;
406
+
407
+ const PasswordCredentialConstructor = (
408
+ window as unknown as {
409
+ PasswordCredential?: new (data: {
410
+ id: string;
411
+ password: string;
412
+ name?: string;
413
+ }) => Credential;
414
+ }
415
+ ).PasswordCredential;
416
+
417
+ if (!credentials?.store || !PasswordCredentialConstructor) {
418
+ this.rememberHandleInBrowserStorage(handle);
419
+ return;
420
+ }
421
+
422
+ try {
423
+ const credential = new PasswordCredentialConstructor({
424
+ id: handle,
425
+ // Graffiti logins use external authorization; this stable placeholder
426
+ // allows browsers/password managers to persist the handle.
427
+ password: "no-password",
428
+ name: handle,
429
+ });
430
+ await credentials.store(credential);
431
+ } catch (error) {
432
+ console.error("Failed to remember handle in password manager:", error);
433
+ this.rememberHandleInBrowserStorage(handle);
434
+ }
435
+ }
436
+
437
+ protected async getRememberedHandleFromPasswordManager() {
438
+ if (typeof window === "undefined") return undefined;
439
+
440
+ const credentials = window.navigator.credentials;
441
+ const PasswordCredentialConstructor = (
442
+ window as unknown as {
443
+ PasswordCredential?: new (data: {
444
+ id: string;
445
+ password: string;
446
+ name?: string;
447
+ }) => Credential;
448
+ }
449
+ ).PasswordCredential;
450
+ if (!credentials?.get || !PasswordCredentialConstructor) {
451
+ return this.getRememberedHandleFromBrowserStorage();
452
+ }
453
+
454
+ try {
455
+ const credential = await credentials.get({
456
+ password: true,
457
+ mediation: "optional",
458
+ } as any);
459
+ if (
460
+ credential &&
461
+ "id" in credential &&
462
+ typeof credential.id === "string"
463
+ ) {
464
+ return credential.id;
465
+ }
466
+ } catch (error) {
467
+ console.error("Failed to load handle from password manager:", error);
468
+ return this.getRememberedHandleFromBrowserStorage();
469
+ }
470
+
471
+ return undefined;
472
+ }
473
+
474
+ protected rememberHandleInBrowserStorage(handle: string) {
475
+ if (typeof window === "undefined") return;
476
+
477
+ try {
478
+ window.localStorage.setItem(LOCAL_STORAGE_REMEMBERED_HANDLE_KEY, handle);
479
+ } catch (error) {
480
+ console.error("Failed to remember handle in browser storage:", error);
481
+ }
482
+ }
483
+
484
+ protected getRememberedHandleFromBrowserStorage() {
485
+ if (typeof window === "undefined") return undefined;
486
+
487
+ try {
488
+ return (
489
+ window.localStorage.getItem(LOCAL_STORAGE_REMEMBERED_HANDLE_KEY) ??
490
+ undefined
491
+ );
492
+ } catch (error) {
493
+ console.error("Failed to load handle from browser storage:", error);
494
+ return undefined;
495
+ }
496
+ }
497
+
356
498
  logout: Graffiti["logout"] = async (session) => {
357
499
  await this.sessions.logout(session.actor);
358
500
  };
@@ -720,6 +862,7 @@ export class GraffitiDecentralized implements Graffiti {
720
862
  error: next.error,
721
863
  origin: allInboxes[next.index].serviceEndpoint,
722
864
  };
865
+ this.tryLogoutIfUnauthorized(next.error, session);
723
866
  } else if (next.result.done) {
724
867
  // Store the cursor for future use
725
868
  const inbox = allInboxes[next.index];
@@ -783,6 +926,9 @@ export class GraffitiDecentralized implements Graffiti {
783
926
  }
784
927
  }
785
928
  }
929
+ } catch (e) {
930
+ this.tryLogoutIfUnauthorized(e, session);
931
+ throw e;
786
932
  } finally {
787
933
  await Promise.all(
788
934
  iterators.map<Promise<void>>(async (it) => {
@@ -823,6 +969,15 @@ export class GraffitiDecentralized implements Graffiti {
823
969
  return this.discoverMeta<{}>(channels, {}, cursors, session);
824
970
  };
825
971
 
972
+ protected async tryLogoutIfUnauthorized(
973
+ e: unknown,
974
+ session?: GraffitiSession | null,
975
+ ) {
976
+ if (e instanceof GraffitiErrorUnauthorized && session) {
977
+ await this.sessions.logout(session.actor);
978
+ }
979
+ }
980
+
826
981
  async announceObject(
827
982
  object: GraffitiObjectBase,
828
983
  tags: Uint8Array[],