@graffiti-garden/implementation-decentralized 0.1.2 → 0.1.3

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 (35) hide show
  1. package/dist/3-protocol/2-handles.d.ts.map +1 -1
  2. package/dist/3-protocol/4-graffiti.d.ts +1 -2
  3. package/dist/3-protocol/4-graffiti.d.ts.map +1 -1
  4. package/dist/3-protocol/login-dialog.html.d.ts +1 -1
  5. package/dist/3-protocol/login-dialog.html.d.ts.map +1 -1
  6. package/dist/browser/index.js +6 -6
  7. package/dist/browser/index.js.map +3 -3
  8. package/dist/browser/{login-dialog.html-E6VRKMXH.js → login-dialog.html-WIDO3XZF.js} +18 -4
  9. package/dist/browser/{login-dialog.html-E6VRKMXH.js.map → login-dialog.html-WIDO3XZF.js.map} +2 -2
  10. package/dist/cjs/3-protocol/2-handles-tests.js +8 -0
  11. package/dist/cjs/3-protocol/2-handles-tests.js.map +2 -2
  12. package/dist/cjs/3-protocol/2-handles.js +3 -4
  13. package/dist/cjs/3-protocol/2-handles.js.map +2 -2
  14. package/dist/cjs/3-protocol/4-graffiti.js +30 -43
  15. package/dist/cjs/3-protocol/4-graffiti.js.map +2 -2
  16. package/dist/cjs/3-protocol/login-dialog.html.js +17 -3
  17. package/dist/cjs/3-protocol/login-dialog.html.js.map +2 -2
  18. package/dist/cjs/index.spec.js +89 -0
  19. package/dist/cjs/index.spec.js.map +3 -3
  20. package/dist/esm/3-protocol/2-handles-tests.js +8 -0
  21. package/dist/esm/3-protocol/2-handles-tests.js.map +2 -2
  22. package/dist/esm/3-protocol/2-handles.js +3 -4
  23. package/dist/esm/3-protocol/2-handles.js.map +2 -2
  24. package/dist/esm/3-protocol/4-graffiti.js +30 -43
  25. package/dist/esm/3-protocol/4-graffiti.js.map +2 -2
  26. package/dist/esm/3-protocol/login-dialog.html.js +17 -3
  27. package/dist/esm/3-protocol/login-dialog.html.js.map +2 -2
  28. package/dist/esm/index.spec.js +90 -1
  29. package/dist/esm/index.spec.js.map +2 -2
  30. package/package.json +1 -1
  31. package/src/3-protocol/2-handles-tests.ts +12 -0
  32. package/src/3-protocol/2-handles.ts +3 -5
  33. package/src/3-protocol/4-graffiti.ts +43 -73
  34. package/src/3-protocol/login-dialog.html.ts +17 -3
  35. package/src/index.spec.ts +103 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/3-protocol/login-dialog.html.ts"],
4
- "sourcesContent": ["export const template = `<template id=\"graffiti-login-welcome\">\n <h1>\n <a target=\"_blank\" href=\"https://graffiti.garden\">Graffiti Log&nbsp;In</a>\n </h1>\n\n <ul>\n <li><a type=\"button\" id=\"graffiti-login-new\">Create&nbsp;new Graffiti&nbsp;identity</a></li>\n <li><button class=\"secondary\" id=\"graffiti-login-existing\">Use&nbsp;existing Graffiti&nbsp;identity</button></li>\n </ul>\n\n <aside>\n This application is built with\n <a target=\"_blank\" href=\"https://graffiti.garden\">Graffiti</a>.\n </aside>\n</template>\n\n<template id=\"graffiti-login-handle\">\n<h1>\n <a target=\"_blank\" href=\"https://graffiti.garden\">Graffiti Log&nbsp;In</a>\n</h1>\n\n <form id=\"graffiti-login-handle-form\">\n <label for=\"username\">Graffiti handle:</label>\n <input\n type=\"text\"\n name=\"username\"\n id=\"username\"\n autocomplete=\"username\"\n autocapitalize=\"none\"\n spellcheck=\"false\"\n inputmode=\"url\"\n placeholder=\"you.graffiti.actor\"\n required\n >\n <button id=\"graffiti-login-handle-submit\" type=\"submit\">\n Log In\n </button>\n </form>\n\n <p>\n Don't&nbsp;have&nbsp;a Graffiti&nbsp;handle? <a id=\"graffiti-login-new\">Create&nbsp;one</a>.\n </p>\n</template>`;\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;",
4
+ "sourcesContent": ["export const template = `<template id=\"graffiti-login-welcome\">\n <h1>\n <a target=\"_blank\" href=\"https://graffiti.garden\">Graffiti Log&nbsp;In</a>\n </h1>\n\n <ul>\n <li><a type=\"button\" id=\"graffiti-login-new\">Create&nbsp;new Graffiti&nbsp;identity</a></li>\n <li><button class=\"secondary\" id=\"graffiti-login-existing\">Use&nbsp;existing Graffiti&nbsp;identity</button></li>\n </ul>\n\n <aside>\n This application is built with\n <a target=\"_blank\" href=\"https://graffiti.garden\">Graffiti</a>.\n </aside>\n</template>\n\n<template id=\"graffiti-login-handle\">\n<h1>\n <a target=\"_blank\" href=\"https://graffiti.garden\">Graffiti Log&nbsp;In</a>\n</h1>\n\n <form id=\"graffiti-login-handle-form\">\n <label for=\"username\">Graffiti handle:</label>\n <input\n type=\"text\"\n name=\"username\"\n id=\"username\"\n autocomplete=\"username\"\n autocapitalize=\"none\"\n spellcheck=\"false\"\n inputmode=\"url\"\n placeholder=\"you.graffiti.actor\"\n required\n >\n <button id=\"graffiti-login-handle-submit\" type=\"submit\">\n Log In\n </button>\n </form>\n\n <div style=\"display: grid; gap: 0.5em\">\n <p><a href=\"#\" id=\"graffiti-login-forgot\">Forgot your handle?</a></p>\n <p>Don't&nbsp;have&nbsp;a Graffiti&nbsp;handle? <a id=\"graffiti-login-new\">Create&nbsp;one</a>.</p>\n </div>\n</template>\n\n<template id=\"graffiti-login-provider\">\n <h1>\n <a target=\"_blank\" href=\"https://graffiti.garden\">Graffiti Log&nbsp;In</a>\n </h1>\n\n <form id=\"graffiti-login-provider-form\">\n <label for=\"graffiti-provider\">Where did you create your Graffiti account?</label>\n <input id=\"graffiti-provider\" type=\"text\" autocapitalize=\"none\" spellcheck=\"false\" required>\n <button type=\"submit\">Continue to provider</button>\n </form>\n <p><a href=\"#\" id=\"graffiti-login-provider-back\">\u2190 Back</a></p>\n</template>`;\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ var import_storage_buckets = require("./1-services/3-storage-buckets");
6
6
  var import_inboxes = require("./1-services/4-inboxes");
7
7
  var import_sessions = require("./3-protocol/1-sessions");
8
8
  var import_vitest = require("vitest");
9
+ var import_dag_cbor = require("@ipld/dag-cbor");
9
10
  var import_handles = require("./3-protocol/2-handles");
10
11
  var import_dids_tests = require("./1-services/2-dids-tests");
11
12
  var import_storage_buckets_tests = require("./1-services/3-storage-buckets-tests");
@@ -80,6 +81,94 @@ var import_environment = require("../test/environment");
80
81
  () => sessions[0],
81
82
  () => sessions[1]
82
83
  );
84
+ (0, import_vitest.test)("OAuth codes require approval from the selected account", async () => {
85
+ const account = import_environment.decentralizedTestUsers[0];
86
+ const redirectUri = "https://client.example/callback";
87
+ const endpoint = "https://localhost:5173/app/oauth/authorize";
88
+ const cookie = `account_${account.accountId}=${account.token}`;
89
+ const headers = {
90
+ Cookie: cookie,
91
+ "X-Graffiti-Account": String(account.accountId),
92
+ "Content-Type": "application/json",
93
+ Origin: "https://localhost:5173"
94
+ };
95
+ const request = (scope, requestHeaders = headers) => fetch(endpoint, {
96
+ method: "POST",
97
+ headers: requestHeaders,
98
+ body: JSON.stringify({ redirect_uri: redirectUri, state: "test-state", scope })
99
+ });
100
+ const navigation = await fetch(
101
+ `${endpoint}?account=${account.accountId}&redirect_uri=${encodeURIComponent(redirectUri)}`,
102
+ { headers: { Cookie: cookie } }
103
+ );
104
+ (0, import_vitest.expect)(navigation.status).toBe(404);
105
+ (0, import_vitest.expect)((await request([account.bucketEndpoint], { Cookie: cookie })).status).toBe(401);
106
+ (0, import_vitest.expect)((await request([account.bucketEndpoint], {
107
+ ...headers,
108
+ Origin: "https://attacker.example"
109
+ })).status).toBe(403);
110
+ (0, import_vitest.expect)((await request([import_environment.decentralizedTestUsers[1].bucketEndpoint])).status).toBe(403);
111
+ const approval = await request([account.bucketEndpoint]);
112
+ (0, import_vitest.expect)(approval.status).toBe(200);
113
+ const { redirectUri: callback } = await approval.json();
114
+ const code = new URL(callback).searchParams.get("code");
115
+ (0, import_vitest.expect)(code).toBeTruthy();
116
+ (0, import_vitest.expect)(new URL(callback).searchParams.get("state")).toBe("test-state");
117
+ const exchange = await fetch("https://localhost:5173/app/oauth/token", {
118
+ method: "POST",
119
+ body: new URLSearchParams({ code, redirect_uri: redirectUri })
120
+ });
121
+ (0, import_vitest.expect)(exchange.status).toBe(200);
122
+ (0, import_vitest.expect)((await exchange.json()).access_token).toBeTypeOf("string");
123
+ });
124
+ (0, import_vitest.test)("deleting a bucket removes its values and rejects requests to its old URL", async () => {
125
+ const account = import_environment.decentralizedTestUsers[0];
126
+ const smallKey = crypto.randomUUID();
127
+ const largeKey = crypto.randomUUID();
128
+ const largeValue = new Uint8Array(32 * 1024 + 1).fill(7);
129
+ const authorization = { Authorization: `Bearer ${account.token}` };
130
+ for (const [key, value] of [
131
+ [smallKey, new Uint8Array([1, 2, 3])],
132
+ [largeKey, largeValue]
133
+ ]) {
134
+ const response = await fetch(`${account.bucketEndpoint}/value/${key}`, {
135
+ method: "PUT",
136
+ headers: authorization,
137
+ body: value
138
+ });
139
+ (0, import_vitest.expect)(response.status).toBe(201);
140
+ }
141
+ const r2Key = `${account.bucketId}/${largeKey}`;
142
+ (0, import_vitest.expect)(await environment.storedR2ObjectExists(r2Key)).toBe(true);
143
+ const deletion = await fetch(
144
+ `https://localhost:5173/app/service-instances/bucket/service/${account.bucketId}`,
145
+ {
146
+ method: "DELETE",
147
+ headers: {
148
+ Cookie: `account_${account.accountId}=${account.token}`,
149
+ "X-Graffiti-Account": String(account.accountId)
150
+ }
151
+ }
152
+ );
153
+ (0, import_vitest.expect)(deletion.status).toBe(200);
154
+ (0, import_vitest.expect)(await environment.storedR2ObjectExists(r2Key)).toBe(false);
155
+ for (const key of [smallKey, largeKey]) {
156
+ const response = await fetch(`${account.bucketEndpoint}/value/${key}`);
157
+ (0, import_vitest.expect)(response.status).toBe(404);
158
+ }
159
+ const batch = await fetch(`${account.bucketEndpoint}/values`, {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/cbor" },
162
+ body: (0, import_dag_cbor.encode)({ keys: [smallKey, largeKey], maxValueBytes: largeValue.length }).slice()
163
+ });
164
+ (0, import_vitest.expect)((0, import_dag_cbor.decode)(await batch.arrayBuffer()).results).toEqual([{ status: 404 }, { status: 404 }]);
165
+ const putAfterDeletion = await fetch(`${account.bucketEndpoint}/value/${smallKey}`, {
166
+ method: "PUT",
167
+ headers: authorization,
168
+ body: new Uint8Array([4])
169
+ });
170
+ (0, import_vitest.expect)(putAfterDeletion.status).toBe(404);
171
+ });
83
172
  async function login(handle) {
84
173
  const actor = await handleMethods.handleToActor(handle);
85
174
  return await new Promise((resolve, reject) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.spec.ts"],
4
- "sourcesContent": ["import type {\n GraffitiLoginEvent,\n GraffitiLogoutEvent,\n GraffitiSession,\n} from \"@graffiti-garden/api\";\nimport {\n graffitiCRUDTests,\n graffitiDiscoverTests,\n graffitiMediaTests,\n} from \"@graffiti-garden/api/tests\";\nimport { DecentralizedIdentifiers } from \"./1-services/2-dids\";\nimport { Authorization } from \"./1-services/1-authorization\";\nimport { StorageBuckets } from \"./1-services/3-storage-buckets\";\nimport { Inboxes } from \"./1-services/4-inboxes\";\nimport { Sessions } from \"./3-protocol/1-sessions\";\nimport { afterAll, beforeAll, describe } from \"vitest\";\nimport { Handles } from \"./3-protocol/2-handles\";\nimport { didTests } from \"./1-services/2-dids-tests\";\nimport { storageBucketTests } from \"./1-services/3-storage-buckets-tests\";\nimport { inboxTests } from \"./1-services/4-inboxes-tests\";\nimport { stringEncodingTests } from \"./2-primitives/1-string-encoding-tests\";\nimport { contentAddressesTests } from \"./2-primitives/2-content-addresses-tests\";\nimport { channelAttestationTests } from \"./2-primitives/3-channel-attestations-tests\";\nimport { allowedAttestationTests } from \"./2-primitives/4-allowed-attestations-tests\";\nimport { handleTests } from \"./3-protocol/2-handles-tests\";\nimport { objectEncodingTests } from \"./3-protocol/3-object-encoding-tests\";\nimport { GraffitiDecentralized } from \"./3-protocol/4-graffiti\";\nimport {\n DecentralizedTestEnvironment,\n decentralizedSharedInbox,\n decentralizedTestUsers,\n} from \"../test/environment\";\n\ndescribe(\"GraffitiDecentralized Tests\", () => {\n const environment = new DecentralizedTestEnvironment();\n const handles = decentralizedTestUsers.map((user) => user.handle);\n let sessions: GraffitiSession[] = [];\n let sessionMethods: Sessions;\n let handleMethods: Handles;\n\n beforeAll(async () => {\n await environment.start();\n\n const dids = new DecentralizedIdentifiers();\n sessionMethods = new Sessions({\n dids,\n authorization: new Authorization(),\n inboxes: new Inboxes(),\n storageBuckets: new StorageBuckets(),\n });\n handleMethods = new Handles({ dids });\n\n for (const handle of handles) {\n sessions.push(await login(handle));\n }\n });\n\n afterAll(async () => {\n try {\n for (const session of sessions) {\n await logout(session.actor);\n }\n } finally {\n await environment.stop();\n }\n });\n\n // Service tests\n didTests();\n storageBucketTests(\n decentralizedTestUsers[0].bucketEndpoint,\n decentralizedTestUsers[0].token,\n );\n inboxTests(\n decentralizedTestUsers[0].inboxEndpoint,\n decentralizedTestUsers[0].token,\n );\n\n // Primitive tests\n stringEncodingTests();\n contentAddressesTests();\n channelAttestationTests();\n allowedAttestationTests();\n\n // Protocol tests\n handleTests(handles[0]);\n objectEncodingTests();\n\n const useGraffiti = () =>\n new GraffitiDecentralized({\n defaultInboxEndpoints: [decentralizedSharedInbox],\n });\n graffitiCRUDTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiMediaTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiDiscoverTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n\n // How to log in/out vvv\n async function login(handle: string) {\n const actor = await handleMethods.handleToActor(handle);\n\n return await new Promise<GraffitiSession>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"login\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLoginEvent[\"detail\"];\n if (detail.session?.actor !== actor) return;\n cleanup();\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve(detail.session);\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"login\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Authorization timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.login(actor);\n });\n }\n\n async function logout(actor: string) {\n return await new Promise<void>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"logout\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLogoutEvent[\"detail\"];\n if (detail.actor !== actor) return;\n cleanup();\n\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve();\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"logout\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Logout timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.logout(actor);\n });\n }\n});\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import type {\n GraffitiLoginEvent,\n GraffitiLogoutEvent,\n GraffitiSession,\n} from \"@graffiti-garden/api\";\nimport {\n graffitiCRUDTests,\n graffitiDiscoverTests,\n graffitiMediaTests,\n} from \"@graffiti-garden/api/tests\";\nimport { DecentralizedIdentifiers } from \"./1-services/2-dids\";\nimport { Authorization } from \"./1-services/1-authorization\";\nimport { StorageBuckets } from \"./1-services/3-storage-buckets\";\nimport { Inboxes } from \"./1-services/4-inboxes\";\nimport { Sessions } from \"./3-protocol/1-sessions\";\nimport { afterAll, beforeAll, describe, expect, test } from \"vitest\";\nimport { decode as dagCborDecode, encode as dagCborEncode } from \"@ipld/dag-cbor\";\nimport { Handles } from \"./3-protocol/2-handles\";\nimport { didTests } from \"./1-services/2-dids-tests\";\nimport { storageBucketTests } from \"./1-services/3-storage-buckets-tests\";\nimport { inboxTests } from \"./1-services/4-inboxes-tests\";\nimport { stringEncodingTests } from \"./2-primitives/1-string-encoding-tests\";\nimport { contentAddressesTests } from \"./2-primitives/2-content-addresses-tests\";\nimport { channelAttestationTests } from \"./2-primitives/3-channel-attestations-tests\";\nimport { allowedAttestationTests } from \"./2-primitives/4-allowed-attestations-tests\";\nimport { handleTests } from \"./3-protocol/2-handles-tests\";\nimport { objectEncodingTests } from \"./3-protocol/3-object-encoding-tests\";\nimport { GraffitiDecentralized } from \"./3-protocol/4-graffiti\";\nimport {\n DecentralizedTestEnvironment,\n decentralizedSharedInbox,\n decentralizedTestUsers,\n} from \"../test/environment\";\n\ndescribe(\"GraffitiDecentralized Tests\", () => {\n const environment = new DecentralizedTestEnvironment();\n const handles = decentralizedTestUsers.map((user) => user.handle);\n let sessions: GraffitiSession[] = [];\n let sessionMethods: Sessions;\n let handleMethods: Handles;\n\n beforeAll(async () => {\n await environment.start();\n\n const dids = new DecentralizedIdentifiers();\n sessionMethods = new Sessions({\n dids,\n authorization: new Authorization(),\n inboxes: new Inboxes(),\n storageBuckets: new StorageBuckets(),\n });\n handleMethods = new Handles({ dids });\n\n for (const handle of handles) {\n sessions.push(await login(handle));\n }\n });\n\n afterAll(async () => {\n try {\n for (const session of sessions) {\n await logout(session.actor);\n }\n } finally {\n await environment.stop();\n }\n });\n\n // Service tests\n didTests();\n storageBucketTests(\n decentralizedTestUsers[0].bucketEndpoint,\n decentralizedTestUsers[0].token,\n );\n inboxTests(\n decentralizedTestUsers[0].inboxEndpoint,\n decentralizedTestUsers[0].token,\n );\n\n // Primitive tests\n stringEncodingTests();\n contentAddressesTests();\n channelAttestationTests();\n allowedAttestationTests();\n\n // Protocol tests\n handleTests(handles[0]);\n objectEncodingTests();\n\n const useGraffiti = () =>\n new GraffitiDecentralized({\n defaultInboxEndpoints: [decentralizedSharedInbox],\n });\n graffitiCRUDTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiMediaTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiDiscoverTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n\n test(\"OAuth codes require approval from the selected account\", async () => {\n const account = decentralizedTestUsers[0];\n const redirectUri = \"https://client.example/callback\";\n const endpoint = \"https://localhost:5173/app/oauth/authorize\";\n const cookie = `account_${account.accountId}=${account.token}`;\n const headers = {\n Cookie: cookie,\n \"X-Graffiti-Account\": String(account.accountId),\n \"Content-Type\": \"application/json\",\n Origin: \"https://localhost:5173\",\n };\n const request = (scope: string[], requestHeaders: HeadersInit = headers) =>\n fetch(endpoint, {\n method: \"POST\",\n headers: requestHeaders,\n body: JSON.stringify({ redirect_uri: redirectUri, state: \"test-state\", scope }),\n });\n\n const navigation = await fetch(\n `${endpoint}?account=${account.accountId}&redirect_uri=${encodeURIComponent(redirectUri)}`,\n { headers: { Cookie: cookie } },\n );\n expect(navigation.status).toBe(404);\n\n expect((await request([account.bucketEndpoint], { Cookie: cookie })).status).toBe(401);\n expect((await request([account.bucketEndpoint], {\n ...headers,\n Origin: \"https://attacker.example\",\n })).status).toBe(403);\n expect((await request([decentralizedTestUsers[1].bucketEndpoint])).status).toBe(403);\n\n const approval = await request([account.bucketEndpoint]);\n expect(approval.status).toBe(200);\n const { redirectUri: callback } = await approval.json() as { redirectUri: string };\n const code = new URL(callback).searchParams.get(\"code\");\n expect(code).toBeTruthy();\n expect(new URL(callback).searchParams.get(\"state\")).toBe(\"test-state\");\n\n const exchange = await fetch(\"https://localhost:5173/app/oauth/token\", {\n method: \"POST\",\n body: new URLSearchParams({ code: code!, redirect_uri: redirectUri }),\n });\n expect(exchange.status).toBe(200);\n expect((await exchange.json()).access_token).toBeTypeOf(\"string\");\n });\n\n test(\"deleting a bucket removes its values and rejects requests to its old URL\", async () => {\n const account = decentralizedTestUsers[0];\n const smallKey = crypto.randomUUID();\n const largeKey = crypto.randomUUID();\n const largeValue = new Uint8Array(32 * 1024 + 1).fill(7);\n const authorization = { Authorization: `Bearer ${account.token}` };\n\n for (const [key, value] of [\n [smallKey, new Uint8Array([1, 2, 3])],\n [largeKey, largeValue],\n ] as const) {\n const response = await fetch(`${account.bucketEndpoint}/value/${key}`, {\n method: \"PUT\",\n headers: authorization,\n body: value,\n });\n expect(response.status).toBe(201);\n }\n\n const r2Key = `${account.bucketId}/${largeKey}`;\n expect(await environment.storedR2ObjectExists(r2Key)).toBe(true);\n\n const deletion = await fetch(\n `https://localhost:5173/app/service-instances/bucket/service/${account.bucketId}`,\n {\n method: \"DELETE\",\n headers: {\n Cookie: `account_${account.accountId}=${account.token}`,\n \"X-Graffiti-Account\": String(account.accountId),\n },\n },\n );\n expect(deletion.status).toBe(200);\n\n expect(await environment.storedR2ObjectExists(r2Key)).toBe(false);\n for (const key of [smallKey, largeKey]) {\n const response = await fetch(`${account.bucketEndpoint}/value/${key}`);\n expect(response.status).toBe(404);\n }\n const batch = await fetch(`${account.bucketEndpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\" },\n body: dagCborEncode({ keys: [smallKey, largeKey], maxValueBytes: largeValue.length }).slice(),\n });\n expect((dagCborDecode(await batch.arrayBuffer()) as { results: { status: number }[] }).results)\n .toEqual([{ status: 404 }, { status: 404 }]);\n\n const putAfterDeletion = await fetch(`${account.bucketEndpoint}/value/${smallKey}`, {\n method: \"PUT\",\n headers: authorization,\n body: new Uint8Array([4]),\n });\n expect(putAfterDeletion.status).toBe(404);\n });\n\n // How to log in/out vvv\n async function login(handle: string) {\n const actor = await handleMethods.handleToActor(handle);\n\n return await new Promise<GraffitiSession>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"login\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLoginEvent[\"detail\"];\n if (detail.session?.actor !== actor) return;\n cleanup();\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve(detail.session);\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"login\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Authorization timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.login(actor);\n });\n }\n\n async function logout(actor: string) {\n return await new Promise<void>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"logout\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLogoutEvent[\"detail\"];\n if (detail.actor !== actor) return;\n cleanup();\n\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve();\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"logout\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Logout timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.logout(actor);\n });\n }\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["dagCborEncode", "dagCborDecode"]
7
7
  }
@@ -11,6 +11,14 @@ function handleTests(handle) {
11
11
  const resolvedHandle = await handles.actorToHandle(actor);
12
12
  expect(resolvedHandle).toBe(handle);
13
13
  });
14
+ test("actorToHandle uses the first did:web alias", async () => {
15
+ const actor = "did:plc:example";
16
+ const handleDid = "did:web:alice.example";
17
+ const dids = {
18
+ resolve: async (did) => did === actor ? { alsoKnownAs: ["at://alice.example", handleDid, "did:web:other.example"] } : { alsoKnownAs: [actor] }
19
+ };
20
+ expect(await new Handles({ dids }).actorToHandle(actor)).toBe("alice.example");
21
+ });
14
22
  });
15
23
  }
16
24
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/3-protocol/2-handles-tests.ts"],
4
- "sourcesContent": ["import { describe, expect, test } from \"vitest\";\nimport { Handles } from \"./2-handles\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\nexport function handleTests(handle: string) {\n describe(\"Handles\", async () => {\n const handles = new Handles({\n dids: new DecentralizedIdentifiers(),\n });\n\n test(\"handleToActor and actorToHandle\", async () => {\n const actor = await handles.handleToActor(handle);\n const resolvedHandle = await handles.actorToHandle(actor);\n expect(resolvedHandle).toBe(handle);\n });\n });\n}\n"],
5
- "mappings": "AAAA,SAAS,UAAU,QAAQ,YAAY;AACvC,SAAS,eAAe;AACxB,SAAS,gCAAgC;AAElC,SAAS,YAAY,QAAgB;AAC1C,WAAS,WAAW,YAAY;AAC9B,UAAM,UAAU,IAAI,QAAQ;AAAA,MAC1B,MAAM,IAAI,yBAAyB;AAAA,IACrC,CAAC;AAED,SAAK,mCAAmC,YAAY;AAClD,YAAM,QAAQ,MAAM,QAAQ,cAAc,MAAM;AAChD,YAAM,iBAAiB,MAAM,QAAQ,cAAc,KAAK;AACxD,aAAO,cAAc,EAAE,KAAK,MAAM;AAAA,IACpC,CAAC;AAAA,EACH,CAAC;AACH;",
4
+ "sourcesContent": ["import { describe, expect, test } from \"vitest\";\nimport { Handles } from \"./2-handles\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\nexport function handleTests(handle: string) {\n describe(\"Handles\", async () => {\n const handles = new Handles({\n dids: new DecentralizedIdentifiers(),\n });\n\n test(\"handleToActor and actorToHandle\", async () => {\n const actor = await handles.handleToActor(handle);\n const resolvedHandle = await handles.actorToHandle(actor);\n expect(resolvedHandle).toBe(handle);\n });\n\n test(\"actorToHandle uses the first did:web alias\", async () => {\n const actor = \"did:plc:example\";\n const handleDid = \"did:web:alice.example\";\n const dids = {\n resolve: async (did: string) => did === actor\n ? { alsoKnownAs: [\"at://alice.example\", handleDid, \"did:web:other.example\"] }\n : { alsoKnownAs: [actor] },\n } as DecentralizedIdentifiers;\n expect(await new Handles({ dids }).actorToHandle(actor)).toBe(\"alice.example\");\n });\n\n });\n}\n"],
5
+ "mappings": "AAAA,SAAS,UAAU,QAAQ,YAAY;AACvC,SAAS,eAAe;AACxB,SAAS,gCAAgC;AAElC,SAAS,YAAY,QAAgB;AAC1C,WAAS,WAAW,YAAY;AAC9B,UAAM,UAAU,IAAI,QAAQ;AAAA,MAC1B,MAAM,IAAI,yBAAyB;AAAA,IACrC,CAAC;AAED,SAAK,mCAAmC,YAAY;AAClD,YAAM,QAAQ,MAAM,QAAQ,cAAc,MAAM;AAChD,YAAM,iBAAiB,MAAM,QAAQ,cAAc,KAAK;AACxD,aAAO,cAAc,EAAE,KAAK,MAAM;AAAA,IACpC,CAAC;AAED,SAAK,8CAA8C,YAAY;AAC7D,YAAM,QAAQ;AACd,YAAM,YAAY;AAClB,YAAM,OAAO;AAAA,QACX,SAAS,OAAO,QAAgB,QAAQ,QACpC,EAAE,aAAa,CAAC,sBAAsB,WAAW,uBAAuB,EAAE,IAC1E,EAAE,aAAa,CAAC,KAAK,EAAE;AAAA,MAC7B;AACA,aAAO,MAAM,IAAI,QAAQ,EAAE,KAAK,CAAC,EAAE,cAAc,KAAK,CAAC,EAAE,KAAK,eAAe;AAAA,IAC/E,CAAC;AAAA,EAEH,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -8,15 +8,14 @@ class Handles {
8
8
  services;
9
9
  actorToHandle = async (actor) => {
10
10
  const actorDocument = await this.services.dids.resolve(actor);
11
- const handleDid = actorDocument.alsoKnownAs?.at(0);
11
+ const handleDid = actorDocument.alsoKnownAs?.find(
12
+ (alias) => alias.startsWith(HANDLE_DID_PREFIX)
13
+ );
12
14
  if (!handleDid) {
13
15
  throw new GraffitiErrorNotFound(
14
16
  `Handle for actor DID ${actor} not found`
15
17
  );
16
18
  }
17
- if (!handleDid.startsWith(HANDLE_DID_PREFIX)) {
18
- throw new Error(`Handle DID ${handleDid} is not a valid handle`);
19
- }
20
19
  const handle = handleDid.slice(HANDLE_DID_PREFIX.length);
21
20
  const handleDocument = await this.services.dids.resolve(handleDid);
22
21
  if (!handleDocument.alsoKnownAs || !handleDocument.alsoKnownAs.includes(actor)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/3-protocol/2-handles.ts"],
4
- "sourcesContent": ["import type { Graffiti } from \"@graffiti-garden/api\";\nimport { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\n// Handles used a fixed method\nconst HANDLE_DID_PREFIX = \"did:web:\";\n\nexport class Handles {\n constructor(\n protected readonly services: { dids: DecentralizedIdentifiers },\n ) {}\n\n actorToHandle: Graffiti[\"actorToHandle\"] = async (actor) => {\n const actorDocument = await this.services.dids.resolve(actor);\n\n const handleDid = actorDocument.alsoKnownAs?.at(0);\n if (!handleDid) {\n throw new GraffitiErrorNotFound(\n `Handle for actor DID ${actor} not found`,\n );\n }\n if (!handleDid.startsWith(HANDLE_DID_PREFIX)) {\n throw new Error(`Handle DID ${handleDid} is not a valid handle`);\n }\n\n const handle = handleDid.slice(HANDLE_DID_PREFIX.length);\n\n const handleDocument = await this.services.dids.resolve(handleDid);\n if (\n !handleDocument.alsoKnownAs ||\n !handleDocument.alsoKnownAs.includes(actor)\n ) {\n throw new Error(`Handle ${handle} does not match actor ${actor}`);\n }\n\n return handle;\n };\n\n handleToActor: Graffiti[\"handleToActor\"] = async (handle) => {\n const handleDid = `${HANDLE_DID_PREFIX}${handle}`;\n const handleDocument = await this.services.dids.resolve(handleDid);\n\n const actor = handleDocument.alsoKnownAs?.at(0);\n if (!actor) {\n throw new GraffitiErrorNotFound(\n `Actor for handle DID ${handleDid} not found`,\n );\n }\n\n const actorDocument = await this.services.dids.resolve(actor);\n if (\n !actorDocument.alsoKnownAs ||\n !actorDocument.alsoKnownAs.includes(handleDid)\n ) {\n throw new Error(`Actor ${actor} does not match handle ${handle}`);\n }\n\n return actor;\n };\n}\n"],
5
- "mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,gCAAgC;AAGzC,MAAM,oBAAoB;AAEnB,MAAM,QAAQ;AAAA,EACnB,YACqB,UACnB;AADmB;AAAA,EAClB;AAAA,EADkB;AAAA,EAGrB,gBAA2C,OAAO,UAAU;AAC1D,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAE5D,UAAM,YAAY,cAAc,aAAa,GAAG,CAAC;AACjD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,wBAAwB,KAAK;AAAA,MAC/B;AAAA,IACF;AACA,QAAI,CAAC,UAAU,WAAW,iBAAiB,GAAG;AAC5C,YAAM,IAAI,MAAM,cAAc,SAAS,wBAAwB;AAAA,IACjE;AAEA,UAAM,SAAS,UAAU,MAAM,kBAAkB,MAAM;AAEvD,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AACjE,QACE,CAAC,eAAe,eAChB,CAAC,eAAe,YAAY,SAAS,KAAK,GAC1C;AACA,YAAM,IAAI,MAAM,UAAU,MAAM,yBAAyB,KAAK,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,gBAA2C,OAAO,WAAW;AAC3D,UAAM,YAAY,GAAG,iBAAiB,GAAG,MAAM;AAC/C,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AAEjE,UAAM,QAAQ,eAAe,aAAa,GAAG,CAAC;AAC9C,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR,wBAAwB,SAAS;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAC5D,QACE,CAAC,cAAc,eACf,CAAC,cAAc,YAAY,SAAS,SAAS,GAC7C;AACA,YAAM,IAAI,MAAM,SAAS,KAAK,0BAA0B,MAAM,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import type { Graffiti } from \"@graffiti-garden/api\";\nimport { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\n// Handles used a fixed method\nconst HANDLE_DID_PREFIX = \"did:web:\";\n\nexport class Handles {\n constructor(\n protected readonly services: { dids: DecentralizedIdentifiers },\n ) {}\n\n actorToHandle: Graffiti[\"actorToHandle\"] = async (actor) => {\n const actorDocument = await this.services.dids.resolve(actor);\n\n const handleDid = actorDocument.alsoKnownAs?.find((alias) =>\n alias.startsWith(HANDLE_DID_PREFIX),\n );\n if (!handleDid) {\n throw new GraffitiErrorNotFound(\n `Handle for actor DID ${actor} not found`,\n );\n }\n const handle = handleDid.slice(HANDLE_DID_PREFIX.length);\n\n const handleDocument = await this.services.dids.resolve(handleDid);\n if (\n !handleDocument.alsoKnownAs ||\n !handleDocument.alsoKnownAs.includes(actor)\n ) {\n throw new Error(`Handle ${handle} does not match actor ${actor}`);\n }\n\n return handle;\n };\n\n handleToActor: Graffiti[\"handleToActor\"] = async (handle) => {\n const handleDid = `${HANDLE_DID_PREFIX}${handle}`;\n const handleDocument = await this.services.dids.resolve(handleDid);\n\n const actor = handleDocument.alsoKnownAs?.at(0);\n if (!actor) {\n throw new GraffitiErrorNotFound(\n `Actor for handle DID ${handleDid} not found`,\n );\n }\n\n const actorDocument = await this.services.dids.resolve(actor);\n if (\n !actorDocument.alsoKnownAs ||\n !actorDocument.alsoKnownAs.includes(handleDid)\n ) {\n throw new Error(`Actor ${actor} does not match handle ${handle}`);\n }\n\n return actor;\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,gCAAgC;AAGzC,MAAM,oBAAoB;AAEnB,MAAM,QAAQ;AAAA,EACnB,YACqB,UACnB;AADmB;AAAA,EAClB;AAAA,EADkB;AAAA,EAGrB,gBAA2C,OAAO,UAAU;AAC1D,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAE5D,UAAM,YAAY,cAAc,aAAa;AAAA,MAAK,CAAC,UACjD,MAAM,WAAW,iBAAiB;AAAA,IACpC;AACA,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,wBAAwB,KAAK;AAAA,MAC/B;AAAA,IACF;AACA,UAAM,SAAS,UAAU,MAAM,kBAAkB,MAAM;AAEvD,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AACjE,QACE,CAAC,eAAe,eAChB,CAAC,eAAe,YAAY,SAAS,KAAK,GAC1C;AACA,YAAM,IAAI,MAAM,UAAU,MAAM,yBAAyB,KAAK,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,gBAA2C,OAAO,WAAW;AAC3D,UAAM,YAAY,GAAG,iBAAiB,GAAG,MAAM;AAC/C,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AAEjE,UAAM,QAAQ,eAAe,aAAa,GAAG,CAAC;AAC9C,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR,wBAAwB,SAAS;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAC5D,QACE,CAAC,cAAc,eACf,CAAC,cAAc,YAAY,SAAS,SAAS,GAC7C;AACA,YAAM,IAAI,MAAM,SAAS,KAAK,0BAA0B,MAAM,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -175,7 +175,7 @@ class GraffitiDecentralized {
175
175
  console.error("Failed to load handle from actor:", error);
176
176
  }
177
177
  if (handle) {
178
- await this.rememberHandleInPasswordManager(handle, actor);
178
+ this.rememberHandleInBrowserStorage(handle);
179
179
  }
180
180
  });
181
181
  for (const method of [
@@ -227,7 +227,7 @@ class GraffitiDecentralized {
227
227
  const input = template?.querySelector(
228
228
  "#username"
229
229
  );
230
- const rememberedHandle = proposedHandle || await this.getRememberedHandleFromPasswordManager();
230
+ const rememberedHandle = proposedHandle || this.getRememberedHandleFromBrowserStorage();
231
231
  if (input) input.value = rememberedHandle ?? "";
232
232
  input?.addEventListener("focus", () => input?.select());
233
233
  setTimeout(() => input?.focus(), 0);
@@ -265,6 +265,10 @@ class GraffitiDecentralized {
265
265
  this.login_(handle);
266
266
  }
267
267
  });
268
+ template?.querySelector("#graffiti-login-forgot")?.addEventListener("click", (e) => {
269
+ e.preventDefault();
270
+ void this.loginWithoutHandle();
271
+ });
268
272
  } else {
269
273
  template = await this.modal?.displayTemplate("graffiti-login-welcome");
270
274
  template?.querySelector("#graffiti-login-existing")?.addEventListener("click", (e) => {
@@ -315,48 +319,31 @@ class GraffitiDecentralized {
315
319
  await this.sessions.login(actor);
316
320
  }
317
321
  }
318
- async rememberHandleInPasswordManager(handle, actor) {
319
- if (typeof window === "undefined") return;
320
- const credentials = window.navigator.credentials;
321
- const PasswordCredentialConstructor = window.PasswordCredential;
322
- if (!credentials?.store || !PasswordCredentialConstructor) {
323
- this.rememberHandleInBrowserStorage(handle);
324
- return;
325
- }
326
- try {
327
- const credential = new PasswordCredentialConstructor({
328
- id: handle,
329
- // Graffiti logins use external authorization; this stable placeholder
330
- // allows browsers/password managers to persist the handle.
331
- password: "no-password",
332
- name: handle
333
- });
334
- await credentials.store(credential);
335
- } catch (error) {
336
- console.error("Failed to remember handle in password manager:", error);
337
- this.rememberHandleInBrowserStorage(handle);
338
- }
339
- }
340
- async getRememberedHandleFromPasswordManager() {
341
- if (typeof window === "undefined") return void 0;
342
- const credentials = window.navigator.credentials;
343
- const PasswordCredentialConstructor = window.PasswordCredential;
344
- if (!credentials?.get || !PasswordCredentialConstructor) {
345
- return this.getRememberedHandleFromBrowserStorage();
346
- }
347
- try {
348
- const credential = await credentials.get({
349
- password: true,
350
- mediation: "optional"
351
- });
352
- if (credential && "id" in credential && typeof credential.id === "string") {
353
- return credential.id;
322
+ async loginWithoutHandle() {
323
+ const template = await this.modal?.displayTemplate("graffiti-login-provider");
324
+ const defaultProvider = new URL("/", this.identityCreatorEndpoint);
325
+ const provider = template?.querySelector("#graffiti-provider");
326
+ if (provider) provider.value = defaultProvider.host;
327
+ provider?.addEventListener("focus", () => provider.select());
328
+ setTimeout(() => provider?.focus(), 0);
329
+ template?.querySelector("#graffiti-login-provider-form")?.addEventListener("submit", (event) => {
330
+ event.preventDefault();
331
+ try {
332
+ const domain = provider?.value.trim() ?? "";
333
+ const origin = domain === defaultProvider.host ? defaultProvider : new URL(`https://${domain}`);
334
+ if (!origin.hostname || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) throw new Error("Invalid provider domain");
335
+ const url2 = new URL("/forgot-handle", origin);
336
+ url2.searchParams.set("redirect_uri", window.location.toString());
337
+ window.location.assign(url2.toString());
338
+ } catch {
339
+ alert("Enter a valid provider domain.");
354
340
  }
355
- } catch (error) {
356
- console.error("Failed to load handle from password manager:", error);
357
- return this.getRememberedHandleFromBrowserStorage();
358
- }
359
- return void 0;
341
+ });
342
+ template?.querySelector("#graffiti-login-provider-back")?.addEventListener("click", (event) => {
343
+ event.preventDefault();
344
+ void this.login_("");
345
+ });
346
+ await this.modal?.open();
360
347
  }
361
348
  rememberHandleInBrowserStorage(handle) {
362
349
  if (typeof window === "undefined") return;