@graffiti-garden/implementation-decentralized 0.0.1

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 (193) hide show
  1. package/LICENSE +674 -0
  2. package/dist/1-services/1-authorization.d.ts +37 -0
  3. package/dist/1-services/1-authorization.d.ts.map +1 -0
  4. package/dist/1-services/2-dids-tests.d.ts +2 -0
  5. package/dist/1-services/2-dids-tests.d.ts.map +1 -0
  6. package/dist/1-services/2-dids.d.ts +9 -0
  7. package/dist/1-services/2-dids.d.ts.map +1 -0
  8. package/dist/1-services/3-storage-buckets-tests.d.ts +2 -0
  9. package/dist/1-services/3-storage-buckets-tests.d.ts.map +1 -0
  10. package/dist/1-services/3-storage-buckets.d.ts +11 -0
  11. package/dist/1-services/3-storage-buckets.d.ts.map +1 -0
  12. package/dist/1-services/4-inboxes-tests.d.ts +2 -0
  13. package/dist/1-services/4-inboxes-tests.d.ts.map +1 -0
  14. package/dist/1-services/4-inboxes.d.ts +87 -0
  15. package/dist/1-services/4-inboxes.d.ts.map +1 -0
  16. package/dist/1-services/utilities.d.ts +7 -0
  17. package/dist/1-services/utilities.d.ts.map +1 -0
  18. package/dist/2-primitives/1-string-encoding-tests.d.ts +2 -0
  19. package/dist/2-primitives/1-string-encoding-tests.d.ts.map +1 -0
  20. package/dist/2-primitives/1-string-encoding.d.ts +6 -0
  21. package/dist/2-primitives/1-string-encoding.d.ts.map +1 -0
  22. package/dist/2-primitives/2-content-addresses-tests.d.ts +2 -0
  23. package/dist/2-primitives/2-content-addresses-tests.d.ts.map +1 -0
  24. package/dist/2-primitives/2-content-addresses.d.ts +8 -0
  25. package/dist/2-primitives/2-content-addresses.d.ts.map +1 -0
  26. package/dist/2-primitives/3-channel-attestations-tests.d.ts +2 -0
  27. package/dist/2-primitives/3-channel-attestations-tests.d.ts.map +1 -0
  28. package/dist/2-primitives/3-channel-attestations.d.ts +13 -0
  29. package/dist/2-primitives/3-channel-attestations.d.ts.map +1 -0
  30. package/dist/2-primitives/4-allowed-attestations-tests.d.ts +2 -0
  31. package/dist/2-primitives/4-allowed-attestations-tests.d.ts.map +1 -0
  32. package/dist/2-primitives/4-allowed-attestations.d.ts +9 -0
  33. package/dist/2-primitives/4-allowed-attestations.d.ts.map +1 -0
  34. package/dist/3-protocol/1-sessions.d.ts +81 -0
  35. package/dist/3-protocol/1-sessions.d.ts.map +1 -0
  36. package/dist/3-protocol/2-handles-tests.d.ts +2 -0
  37. package/dist/3-protocol/2-handles-tests.d.ts.map +1 -0
  38. package/dist/3-protocol/2-handles.d.ts +13 -0
  39. package/dist/3-protocol/2-handles.d.ts.map +1 -0
  40. package/dist/3-protocol/3-object-encoding-tests.d.ts +2 -0
  41. package/dist/3-protocol/3-object-encoding-tests.d.ts.map +1 -0
  42. package/dist/3-protocol/3-object-encoding.d.ts +43 -0
  43. package/dist/3-protocol/3-object-encoding.d.ts.map +1 -0
  44. package/dist/3-protocol/4-graffiti.d.ts +79 -0
  45. package/dist/3-protocol/4-graffiti.d.ts.map +1 -0
  46. package/dist/3-protocol/login-dialog.html.d.ts +2 -0
  47. package/dist/3-protocol/login-dialog.html.d.ts.map +1 -0
  48. package/dist/browser/ajv-QBSREQSI.js +9 -0
  49. package/dist/browser/ajv-QBSREQSI.js.map +7 -0
  50. package/dist/browser/build-BXWPS7VK.js +2 -0
  51. package/dist/browser/build-BXWPS7VK.js.map +7 -0
  52. package/dist/browser/chunk-RFBBAUMM.js +2 -0
  53. package/dist/browser/chunk-RFBBAUMM.js.map +7 -0
  54. package/dist/browser/graffiti-KV3G3O72-URO7SJIJ.js +2 -0
  55. package/dist/browser/graffiti-KV3G3O72-URO7SJIJ.js.map +7 -0
  56. package/dist/browser/index.js +16 -0
  57. package/dist/browser/index.js.map +7 -0
  58. package/dist/browser/login-dialog.html-XUWYDNNI.js +44 -0
  59. package/dist/browser/login-dialog.html-XUWYDNNI.js.map +7 -0
  60. package/dist/browser/rock-salt-LI7DAH66-KPFEBIBO.js +2 -0
  61. package/dist/browser/rock-salt-LI7DAH66-KPFEBIBO.js.map +7 -0
  62. package/dist/browser/style-YUTCEBZV-RWYJV575.js +287 -0
  63. package/dist/browser/style-YUTCEBZV-RWYJV575.js.map +7 -0
  64. package/dist/cjs/1-services/1-authorization.js +317 -0
  65. package/dist/cjs/1-services/1-authorization.js.map +7 -0
  66. package/dist/cjs/1-services/2-dids-tests.js +44 -0
  67. package/dist/cjs/1-services/2-dids-tests.js.map +7 -0
  68. package/dist/cjs/1-services/2-dids.js +47 -0
  69. package/dist/cjs/1-services/2-dids.js.map +7 -0
  70. package/dist/cjs/1-services/3-storage-buckets-tests.js +123 -0
  71. package/dist/cjs/1-services/3-storage-buckets-tests.js.map +7 -0
  72. package/dist/cjs/1-services/3-storage-buckets.js +148 -0
  73. package/dist/cjs/1-services/3-storage-buckets.js.map +7 -0
  74. package/dist/cjs/1-services/4-inboxes-tests.js +145 -0
  75. package/dist/cjs/1-services/4-inboxes-tests.js.map +7 -0
  76. package/dist/cjs/1-services/4-inboxes.js +539 -0
  77. package/dist/cjs/1-services/4-inboxes.js.map +7 -0
  78. package/dist/cjs/1-services/utilities.js +75 -0
  79. package/dist/cjs/1-services/utilities.js.map +7 -0
  80. package/dist/cjs/2-primitives/1-string-encoding-tests.js +50 -0
  81. package/dist/cjs/2-primitives/1-string-encoding-tests.js.map +7 -0
  82. package/dist/cjs/2-primitives/1-string-encoding.js +46 -0
  83. package/dist/cjs/2-primitives/1-string-encoding.js.map +7 -0
  84. package/dist/cjs/2-primitives/2-content-addresses-tests.js +62 -0
  85. package/dist/cjs/2-primitives/2-content-addresses-tests.js.map +7 -0
  86. package/dist/cjs/2-primitives/2-content-addresses.js +53 -0
  87. package/dist/cjs/2-primitives/2-content-addresses.js.map +7 -0
  88. package/dist/cjs/2-primitives/3-channel-attestations-tests.js +130 -0
  89. package/dist/cjs/2-primitives/3-channel-attestations-tests.js.map +7 -0
  90. package/dist/cjs/2-primitives/3-channel-attestations.js +84 -0
  91. package/dist/cjs/2-primitives/3-channel-attestations.js.map +7 -0
  92. package/dist/cjs/2-primitives/4-allowed-attestations-tests.js +96 -0
  93. package/dist/cjs/2-primitives/4-allowed-attestations-tests.js.map +7 -0
  94. package/dist/cjs/2-primitives/4-allowed-attestations.js +68 -0
  95. package/dist/cjs/2-primitives/4-allowed-attestations.js.map +7 -0
  96. package/dist/cjs/3-protocol/1-sessions.js +473 -0
  97. package/dist/cjs/3-protocol/1-sessions.js.map +7 -0
  98. package/dist/cjs/3-protocol/2-handles-tests.js +39 -0
  99. package/dist/cjs/3-protocol/2-handles-tests.js.map +7 -0
  100. package/dist/cjs/3-protocol/2-handles.js +65 -0
  101. package/dist/cjs/3-protocol/2-handles.js.map +7 -0
  102. package/dist/cjs/3-protocol/3-object-encoding-tests.js +253 -0
  103. package/dist/cjs/3-protocol/3-object-encoding-tests.js.map +7 -0
  104. package/dist/cjs/3-protocol/3-object-encoding.js +287 -0
  105. package/dist/cjs/3-protocol/3-object-encoding.js.map +7 -0
  106. package/dist/cjs/3-protocol/4-graffiti.js +937 -0
  107. package/dist/cjs/3-protocol/4-graffiti.js.map +7 -0
  108. package/dist/cjs/3-protocol/login-dialog.html.js +67 -0
  109. package/dist/cjs/3-protocol/login-dialog.html.js.map +7 -0
  110. package/dist/cjs/index.js +32 -0
  111. package/dist/cjs/index.js.map +7 -0
  112. package/dist/cjs/index.spec.js +130 -0
  113. package/dist/cjs/index.spec.js.map +7 -0
  114. package/dist/esm/1-services/1-authorization.js +304 -0
  115. package/dist/esm/1-services/1-authorization.js.map +7 -0
  116. package/dist/esm/1-services/2-dids-tests.js +24 -0
  117. package/dist/esm/1-services/2-dids-tests.js.map +7 -0
  118. package/dist/esm/1-services/2-dids.js +27 -0
  119. package/dist/esm/1-services/2-dids.js.map +7 -0
  120. package/dist/esm/1-services/3-storage-buckets-tests.js +103 -0
  121. package/dist/esm/1-services/3-storage-buckets-tests.js.map +7 -0
  122. package/dist/esm/1-services/3-storage-buckets.js +132 -0
  123. package/dist/esm/1-services/3-storage-buckets.js.map +7 -0
  124. package/dist/esm/1-services/4-inboxes-tests.js +125 -0
  125. package/dist/esm/1-services/4-inboxes-tests.js.map +7 -0
  126. package/dist/esm/1-services/4-inboxes.js +533 -0
  127. package/dist/esm/1-services/4-inboxes.js.map +7 -0
  128. package/dist/esm/1-services/utilities.js +60 -0
  129. package/dist/esm/1-services/utilities.js.map +7 -0
  130. package/dist/esm/2-primitives/1-string-encoding-tests.js +33 -0
  131. package/dist/esm/2-primitives/1-string-encoding-tests.js.map +7 -0
  132. package/dist/esm/2-primitives/1-string-encoding.js +26 -0
  133. package/dist/esm/2-primitives/1-string-encoding.js.map +7 -0
  134. package/dist/esm/2-primitives/2-content-addresses-tests.js +45 -0
  135. package/dist/esm/2-primitives/2-content-addresses-tests.js.map +7 -0
  136. package/dist/esm/2-primitives/2-content-addresses.js +33 -0
  137. package/dist/esm/2-primitives/2-content-addresses.js.map +7 -0
  138. package/dist/esm/2-primitives/3-channel-attestations-tests.js +116 -0
  139. package/dist/esm/2-primitives/3-channel-attestations-tests.js.map +7 -0
  140. package/dist/esm/2-primitives/3-channel-attestations.js +69 -0
  141. package/dist/esm/2-primitives/3-channel-attestations.js.map +7 -0
  142. package/dist/esm/2-primitives/4-allowed-attestations-tests.js +82 -0
  143. package/dist/esm/2-primitives/4-allowed-attestations-tests.js.map +7 -0
  144. package/dist/esm/2-primitives/4-allowed-attestations.js +51 -0
  145. package/dist/esm/2-primitives/4-allowed-attestations.js.map +7 -0
  146. package/dist/esm/3-protocol/1-sessions.js +465 -0
  147. package/dist/esm/3-protocol/1-sessions.js.map +7 -0
  148. package/dist/esm/3-protocol/2-handles-tests.js +19 -0
  149. package/dist/esm/3-protocol/2-handles-tests.js.map +7 -0
  150. package/dist/esm/3-protocol/2-handles.js +45 -0
  151. package/dist/esm/3-protocol/2-handles.js.map +7 -0
  152. package/dist/esm/3-protocol/3-object-encoding-tests.js +248 -0
  153. package/dist/esm/3-protocol/3-object-encoding-tests.js.map +7 -0
  154. package/dist/esm/3-protocol/3-object-encoding.js +280 -0
  155. package/dist/esm/3-protocol/3-object-encoding.js.map +7 -0
  156. package/dist/esm/3-protocol/4-graffiti.js +957 -0
  157. package/dist/esm/3-protocol/4-graffiti.js.map +7 -0
  158. package/dist/esm/3-protocol/login-dialog.html.js +47 -0
  159. package/dist/esm/3-protocol/login-dialog.html.js.map +7 -0
  160. package/dist/esm/index.js +14 -0
  161. package/dist/esm/index.js.map +7 -0
  162. package/dist/esm/index.spec.js +133 -0
  163. package/dist/esm/index.spec.js.map +7 -0
  164. package/dist/index.d.ts +10 -0
  165. package/dist/index.d.ts.map +1 -0
  166. package/dist/index.spec.d.ts +2 -0
  167. package/dist/index.spec.d.ts.map +1 -0
  168. package/package.json +62 -0
  169. package/src/1-services/1-authorization.ts +399 -0
  170. package/src/1-services/2-dids-tests.ts +24 -0
  171. package/src/1-services/2-dids.ts +30 -0
  172. package/src/1-services/3-storage-buckets-tests.ts +121 -0
  173. package/src/1-services/3-storage-buckets.ts +183 -0
  174. package/src/1-services/4-inboxes-tests.ts +154 -0
  175. package/src/1-services/4-inboxes.ts +722 -0
  176. package/src/1-services/utilities.ts +65 -0
  177. package/src/2-primitives/1-string-encoding-tests.ts +33 -0
  178. package/src/2-primitives/1-string-encoding.ts +33 -0
  179. package/src/2-primitives/2-content-addresses-tests.ts +46 -0
  180. package/src/2-primitives/2-content-addresses.ts +42 -0
  181. package/src/2-primitives/3-channel-attestations-tests.ts +125 -0
  182. package/src/2-primitives/3-channel-attestations.ts +95 -0
  183. package/src/2-primitives/4-allowed-attestations-tests.ts +86 -0
  184. package/src/2-primitives/4-allowed-attestations.ts +69 -0
  185. package/src/3-protocol/1-sessions.ts +601 -0
  186. package/src/3-protocol/2-handles-tests.ts +17 -0
  187. package/src/3-protocol/2-handles.ts +60 -0
  188. package/src/3-protocol/3-object-encoding-tests.ts +269 -0
  189. package/src/3-protocol/3-object-encoding.ts +396 -0
  190. package/src/3-protocol/4-graffiti.ts +1265 -0
  191. package/src/3-protocol/login-dialog.html.ts +43 -0
  192. package/src/index.spec.ts +158 -0
  193. package/src/index.ts +16 -0
@@ -0,0 +1,601 @@
1
+ import type {
2
+ Graffiti,
3
+ GraffitiLoginEvent,
4
+ GraffitiLogoutEvent,
5
+ GraffitiSession,
6
+ GraffitiSessionInitializedEvent,
7
+ } from "@graffiti-garden/api";
8
+ import { DecentralizedIdentifiers } from "../1-services/2-dids";
9
+ import {
10
+ InitializedEventDetailSchema,
11
+ LoginEventDetailSchema,
12
+ LogoutEventDetailSchema,
13
+ type Authorization,
14
+ } from "../1-services/1-authorization";
15
+ import { StorageBuckets } from "../1-services/3-storage-buckets";
16
+ import type { Inboxes } from "../1-services/4-inboxes";
17
+ import type { Service } from "did-resolver";
18
+ import {
19
+ type infer as infer_,
20
+ extend,
21
+ array,
22
+ string,
23
+ object,
24
+ url,
25
+ tuple,
26
+ enum as enum_,
27
+ } from "zod/mini";
28
+
29
+ export const DID_SERVICE_TYPE_GRAFFITI_INBOX = "GraffitiInbox";
30
+ export const DID_SERVICE_TYPE_GRAFFITI_STORAGE_BUCKET = "GraffitiStorageBucket";
31
+ export const DID_SERVICE_ID_GRAFFITI_PERSONAL_INBOX = "#graffitiPersonalInbox";
32
+ export const DID_SERVICE_ID_GRAFFITI_STORAGE_BUCKET = "#graffitiStorageBucket";
33
+ export const DID_SERVICE_ID_GRAFFITI_SHARED_INBOX_PREFIX =
34
+ "#graffitiSharedInbox_";
35
+
36
+ export class Sessions {
37
+ sessionEvents: Graffiti["sessionEvents"] = new EventTarget();
38
+
39
+ constructor(
40
+ protected readonly services: {
41
+ readonly dids: DecentralizedIdentifiers;
42
+ readonly authorization: Authorization;
43
+ readonly storageBuckets: StorageBuckets;
44
+ readonly inboxes: Inboxes;
45
+ },
46
+ ) {
47
+ const initializedPromise = new Promise<void>((resolve) => {
48
+ this.services.authorization.eventTarget.addEventListener(
49
+ "initialized",
50
+ (e) => {
51
+ if (!(e instanceof CustomEvent)) return;
52
+ const parsed = InitializedEventDetailSchema.safeParse(e.detail);
53
+ if (!parsed.success) return;
54
+ const error = parsed.data?.error;
55
+ if (error) console.log(error);
56
+ resolve();
57
+ },
58
+ );
59
+ });
60
+ this.services.authorization.eventTarget.addEventListener(
61
+ "login",
62
+ this.onLogin.bind(this),
63
+ );
64
+ this.services.authorization.eventTarget.addEventListener(
65
+ "logout",
66
+ this.onLogout.bind(this),
67
+ );
68
+
69
+ // Handle account registration redirect immediately,
70
+ // to prevent SPA routers from hijacking the URL too soon
71
+ let loginPromise: Promise<void> | undefined;
72
+ if (typeof window !== "undefined") {
73
+ const actorEncoded = new URLSearchParams(window.location.search).get(
74
+ "actor",
75
+ );
76
+ if (actorEncoded) {
77
+ try {
78
+ // Get the actor
79
+ const actor = decodeURIComponent(actorEncoded);
80
+ // Strip it from the URL
81
+ const url = new URL(window.location.toString());
82
+ url.searchParams.delete("actor");
83
+ window.history.replaceState({}, "", url.toString());
84
+ // Complete the login
85
+ loginPromise = this.login(actor);
86
+ } catch (error) {
87
+ console.error("Error decoding actor:", error);
88
+ }
89
+ }
90
+ }
91
+
92
+ (async () => {
93
+ // Allow listeners to be added before dispatching events
94
+ await new Promise((resolve) => setTimeout(resolve, 0));
95
+
96
+ // Wait for login to complete, if there
97
+ await loginPromise;
98
+
99
+ for (const session of this.loggedInSessions) {
100
+ const loginEvent: GraffitiLoginEvent = new CustomEvent("login", {
101
+ detail: { session: { actor: session.actor } },
102
+ });
103
+ this.sessionEvents.dispatchEvent(loginEvent);
104
+ }
105
+
106
+ await initializedPromise;
107
+
108
+ // Send own initialized event
109
+ const initializedEvent: GraffitiSessionInitializedEvent = new CustomEvent(
110
+ "initialized",
111
+ );
112
+ this.sessionEvents.dispatchEvent(initializedEvent);
113
+ })();
114
+ }
115
+
116
+ protected inProgressLogin: infer_<typeof InProgressSchema> | undefined =
117
+ undefined;
118
+ protected inProgressLogout: infer_<typeof InProgressSchema> | undefined =
119
+ undefined;
120
+
121
+ async login(actor: string) {
122
+ try {
123
+ await this.login_(actor);
124
+ } catch (e) {
125
+ const loginEvent: GraffitiLoginEvent = new CustomEvent("login", {
126
+ detail: {
127
+ error: e instanceof Error ? e : new Error(String(e)),
128
+ session: { actor },
129
+ },
130
+ });
131
+ this.sessionEvents.dispatchEvent(loginEvent);
132
+ }
133
+ }
134
+ protected async login_(actor: string) {
135
+ // First look to see if we're already logged in
136
+ const existingSession = this.loggedInSessions.find(
137
+ (session) => session.actor === actor,
138
+ );
139
+ if (existingSession) {
140
+ this.sessionEvents.dispatchEvent(
141
+ new CustomEvent("login", { detail: { session: { actor } } }),
142
+ );
143
+ return;
144
+ }
145
+
146
+ const actorDocument = await this.services.dids.resolve(actor);
147
+
148
+ const services = actorDocument.service;
149
+ if (!services) {
150
+ throw new Error(`No services found in actor document for ${actor}`);
151
+ }
152
+
153
+ const storageBucketService = services.find(
154
+ (service) =>
155
+ service.id === DID_SERVICE_ID_GRAFFITI_STORAGE_BUCKET &&
156
+ service.type === DID_SERVICE_TYPE_GRAFFITI_STORAGE_BUCKET,
157
+ );
158
+ const personalInboxService = services.find(
159
+ (service) =>
160
+ service.id === DID_SERVICE_ID_GRAFFITI_PERSONAL_INBOX &&
161
+ service.type === DID_SERVICE_TYPE_GRAFFITI_INBOX,
162
+ );
163
+ const sharedInboxServices = services.filter(
164
+ (service) =>
165
+ service.id.match(
166
+ new RegExp(`^${DID_SERVICE_ID_GRAFFITI_SHARED_INBOX_PREFIX}\\d+$`),
167
+ ) && service.type === DID_SERVICE_TYPE_GRAFFITI_INBOX,
168
+ );
169
+
170
+ if (
171
+ !personalInboxService ||
172
+ !storageBucketService ||
173
+ sharedInboxServices.length === 0
174
+ ) {
175
+ throw new Error(
176
+ `Required services not found in actor document for ${actor}`,
177
+ );
178
+ }
179
+
180
+ // Massage the services into a list of endpoints with types
181
+ const storageBucketEndpoint: string =
182
+ serviceToEndpoint(storageBucketService);
183
+ const personalInboxEndpoint: string =
184
+ serviceToEndpoint(personalInboxService);
185
+ const sharedInboxEndpoints: string[] =
186
+ sharedInboxServices.map(serviceToEndpoint);
187
+ const servicesWithTypes = [
188
+ { endpoint: storageBucketEndpoint, type: "bucket" } as const,
189
+ { endpoint: personalInboxEndpoint, type: "personal-inbox" } as const,
190
+ ...sharedInboxEndpoints.map(
191
+ (endpoint) =>
192
+ ({
193
+ endpoint,
194
+ type: "shared-inbox",
195
+ }) as const,
196
+ ),
197
+ ];
198
+
199
+ // Fetch the authorization endpoints for each service
200
+ const servicesWithAuthorizationEndpoints = await Promise.all(
201
+ servicesWithTypes.map(async ({ endpoint, type }) => {
202
+ const authorizationEndpoint = await (type === "bucket"
203
+ ? this.services.storageBuckets.getAuthorizationEndpoint(endpoint)
204
+ : this.services.inboxes.getAuthorizationEndpoint(endpoint));
205
+ return { endpoint, authorizationEndpoint, type };
206
+ }),
207
+ );
208
+
209
+ // Group the endpoints according to their authorization endpoints
210
+ const servicesByAuthorizationMap: Map<
211
+ string,
212
+ {
213
+ endpoint: string;
214
+ type: "bucket" | "personal-inbox" | "shared-inbox";
215
+ }[]
216
+ > = new Map();
217
+ servicesWithAuthorizationEndpoints.forEach(
218
+ ({ authorizationEndpoint, endpoint, type }) => {
219
+ if (!servicesByAuthorizationMap.has(authorizationEndpoint)) {
220
+ servicesByAuthorizationMap.set(authorizationEndpoint, []);
221
+ }
222
+ servicesByAuthorizationMap
223
+ .get(authorizationEndpoint)!
224
+ .push({ endpoint, type });
225
+ },
226
+ );
227
+ const servicesByAuthorization = [...servicesByAuthorizationMap.entries()];
228
+
229
+ const session: GraffitiSession = { actor };
230
+
231
+ const inProgressLogin: infer_<typeof InProgressSchema> = {
232
+ ...session,
233
+ tokens: [],
234
+ servicesByAuthorization,
235
+ };
236
+
237
+ if (typeof window !== "undefined") {
238
+ // Store the in-progress session in localStorage
239
+ window.localStorage.setItem(
240
+ LOCAL_STORAGE_IN_PROGRESS_LOGIN_KEY,
241
+ JSON.stringify(inProgressLogin),
242
+ );
243
+ } else {
244
+ this.inProgressLogin = inProgressLogin;
245
+ }
246
+
247
+ // Start the login process with the first endpoint
248
+ const [firstAuthorizationEndpoint, firstServices] =
249
+ servicesByAuthorization[0];
250
+ await this.services.authorization.login(
251
+ firstAuthorizationEndpoint,
252
+ actor,
253
+ firstServices.map((s) => s.endpoint),
254
+ );
255
+ }
256
+
257
+ protected async onLogin(event: unknown) {
258
+ if (!(event instanceof CustomEvent)) return;
259
+ const parsed = LoginEventDetailSchema.safeParse(event.detail);
260
+ if (!parsed.success) return;
261
+
262
+ const actor = parsed.data.loginId;
263
+
264
+ try {
265
+ await this.onLogin_(parsed.data);
266
+ } catch (e) {
267
+ const LoginEvent: GraffitiLoginEvent = new CustomEvent("login", {
268
+ detail: {
269
+ error: e instanceof Error ? e : new Error(String(e)),
270
+ session: { actor },
271
+ },
272
+ });
273
+ this.sessionEvents.dispatchEvent(LoginEvent);
274
+ }
275
+ }
276
+ protected async onLogin_(loginDetail: infer_<typeof LoginEventDetailSchema>) {
277
+ if (loginDetail.error) throw loginDetail.error;
278
+
279
+ const token = loginDetail.token;
280
+ const actor = loginDetail.loginId;
281
+
282
+ // Lookup the in-progress session
283
+ let inProgressLogin: infer_<typeof InProgressSchema>;
284
+ if (typeof window !== "undefined") {
285
+ const inProgressLoginString = window.localStorage.getItem(
286
+ LOCAL_STORAGE_IN_PROGRESS_LOGIN_KEY,
287
+ );
288
+ if (!inProgressLoginString) {
289
+ throw new Error("No in-progress login found");
290
+ }
291
+
292
+ const json = JSON.parse(inProgressLoginString);
293
+ inProgressLogin = InProgressSchema.parse(json);
294
+ } else {
295
+ if (!this.inProgressLogin) {
296
+ throw new Error("No in-progress login found");
297
+ }
298
+ inProgressLogin = this.inProgressLogin;
299
+ }
300
+
301
+ if (inProgressLogin.actor !== actor) {
302
+ throw new Error("Actor mismatch in login response - concurrent logins?");
303
+ }
304
+
305
+ inProgressLogin.tokens.push(token);
306
+
307
+ if (
308
+ inProgressLogin.tokens.length ===
309
+ inProgressLogin.servicesByAuthorization.length
310
+ ) {
311
+ // Login complete!
312
+ if (typeof window === "undefined") {
313
+ this.inProgressLogin = undefined;
314
+ } else {
315
+ window.localStorage.removeItem(LOCAL_STORAGE_IN_PROGRESS_LOGIN_KEY);
316
+ }
317
+
318
+ // Build the completed session
319
+ const services = inProgressLogin.servicesByAuthorization.flatMap(
320
+ ([authorizationEndpoint, services], index) =>
321
+ services.map((service) => ({
322
+ token: inProgressLogin.tokens[index],
323
+ serviceEndpoint: service.endpoint,
324
+ authorizationEndpoint,
325
+ type: service.type,
326
+ })),
327
+ );
328
+
329
+ const session: StoredSession = {
330
+ ...inProgressLogin,
331
+ storageBucket: services.find((s) => s.type === "bucket")!,
332
+ personalInbox: services.find((s) => s.type === "personal-inbox")!,
333
+ sharedInboxes: services.filter((s) => s.type === "shared-inbox")!,
334
+ };
335
+
336
+ // Store the completed session
337
+ const sessions = this.loggedInSessions;
338
+ sessions.push(session);
339
+ this.loggedInSessions = sessions;
340
+
341
+ // Return the completed session
342
+ const loginEvent: GraffitiLoginEvent = new CustomEvent("login", {
343
+ detail: { session: { actor } },
344
+ });
345
+ this.sessionEvents.dispatchEvent(loginEvent);
346
+ } else {
347
+ // Store the in progress and continue
348
+ if (typeof window !== "undefined") {
349
+ window.localStorage.setItem(
350
+ LOCAL_STORAGE_IN_PROGRESS_LOGIN_KEY,
351
+ JSON.stringify(inProgressLogin),
352
+ );
353
+ } else {
354
+ this.inProgressLogin = inProgressLogin;
355
+ }
356
+
357
+ // Continue to the next authorization endpoint
358
+ const [authorizationEndpoint, services] =
359
+ inProgressLogin.servicesByAuthorization[inProgressLogin.tokens.length];
360
+ await this.services.authorization.login(
361
+ authorizationEndpoint,
362
+ actor,
363
+ services.map((s) => s.endpoint),
364
+ );
365
+ }
366
+ }
367
+
368
+ async logout(actor: string) {
369
+ try {
370
+ await this.logout_(actor);
371
+ } catch (e) {
372
+ const logoutEvent: GraffitiLogoutEvent = new CustomEvent("logout", {
373
+ detail: {
374
+ error: e instanceof Error ? e : new Error(String(e)),
375
+ actor,
376
+ },
377
+ });
378
+ this.sessionEvents.dispatchEvent(logoutEvent);
379
+ }
380
+ }
381
+ protected async logout_(actor: string) {
382
+ const session = this.loggedInSessions.find(
383
+ (session) => session.actor === actor,
384
+ );
385
+ if (!session) {
386
+ throw new Error(`No session found for actor ${actor}`);
387
+ }
388
+
389
+ // Remove the session(s)
390
+ this.loggedInSessions = this.loggedInSessions.filter(
391
+ (session) => session.actor !== actor,
392
+ );
393
+
394
+ // Begin the logout
395
+ const token = session.tokens.pop();
396
+ if (!token) {
397
+ throw new Error("No tokens found in session");
398
+ }
399
+ // Store the in progress logout
400
+ if (typeof window !== "undefined") {
401
+ window.localStorage.setItem(
402
+ LOCAL_STORAGE_IN_PROGRESS_LOGOUT_KEY,
403
+ JSON.stringify(session),
404
+ );
405
+ } else {
406
+ this.inProgressLogout = session;
407
+ }
408
+ const [authorizationEndpoint, _] =
409
+ session.servicesByAuthorization[session.tokens.length];
410
+ await this.services.authorization.logout(
411
+ authorizationEndpoint,
412
+ actor,
413
+ token,
414
+ );
415
+ }
416
+
417
+ protected async onLogout(event: unknown) {
418
+ if (!(event instanceof CustomEvent)) return;
419
+ const parsed = LogoutEventDetailSchema.safeParse(event.detail);
420
+ if (!parsed.success) return;
421
+
422
+ const actor = parsed.data.logoutId;
423
+
424
+ try {
425
+ await this.onLogout_(parsed.data);
426
+ } catch (e) {
427
+ const logoutEvent: GraffitiLogoutEvent = new CustomEvent("logout", {
428
+ detail: {
429
+ error: e instanceof Error ? e : new Error(String(e)),
430
+ actor,
431
+ },
432
+ });
433
+ this.sessionEvents.dispatchEvent(logoutEvent);
434
+ }
435
+ }
436
+ protected async onLogout_(
437
+ logoutDetail: infer_<typeof LogoutEventDetailSchema>,
438
+ ) {
439
+ if (logoutDetail.error) throw logoutDetail.error;
440
+
441
+ const actor = logoutDetail.logoutId;
442
+
443
+ // Lookup the in-progress session
444
+ let inProgressLogout: infer_<typeof InProgressSchema>;
445
+ if (typeof window !== "undefined") {
446
+ const inProgressLogoutString = window.localStorage.getItem(
447
+ LOCAL_STORAGE_IN_PROGRESS_LOGOUT_KEY,
448
+ );
449
+ if (!inProgressLogoutString) {
450
+ throw new Error("No in-progress logout found");
451
+ }
452
+
453
+ const json = JSON.parse(inProgressLogoutString);
454
+ inProgressLogout = InProgressSchema.parse(json);
455
+ } else {
456
+ if (!this.inProgressLogout) {
457
+ throw new Error("No in-progress logout found");
458
+ }
459
+ inProgressLogout = this.inProgressLogout;
460
+ }
461
+
462
+ if (inProgressLogout.actor !== actor) {
463
+ throw new Error(
464
+ "Actor mismatch in logout response - concurrent logouts?",
465
+ );
466
+ }
467
+
468
+ const token = inProgressLogout.tokens.pop();
469
+ if (!token) {
470
+ // Logout complete
471
+ if (typeof window === "undefined") {
472
+ this.inProgressLogout = undefined;
473
+ } else {
474
+ window.localStorage.removeItem(LOCAL_STORAGE_IN_PROGRESS_LOGOUT_KEY);
475
+ }
476
+
477
+ const logoutEvent: GraffitiLogoutEvent = new CustomEvent("logout", {
478
+ detail: { actor },
479
+ });
480
+ this.sessionEvents.dispatchEvent(logoutEvent);
481
+ } else {
482
+ // Store the in progress and continue
483
+ if (typeof window !== "undefined") {
484
+ window.localStorage.setItem(
485
+ LOCAL_STORAGE_IN_PROGRESS_LOGOUT_KEY,
486
+ JSON.stringify(inProgressLogout),
487
+ );
488
+ } else {
489
+ this.inProgressLogout = inProgressLogout;
490
+ }
491
+
492
+ // Continue to the next authorization endpoint
493
+ const [authorizationEndpoint, _] =
494
+ inProgressLogout.servicesByAuthorization[
495
+ inProgressLogout.tokens.length
496
+ ];
497
+ await this.services.authorization.logout(
498
+ authorizationEndpoint,
499
+ actor,
500
+ token,
501
+ );
502
+ }
503
+ }
504
+
505
+ protected get loggedInSessions(): StoredSession[] {
506
+ if (typeof window === "undefined") return loggedInSessions_;
507
+
508
+ const data = window.localStorage.getItem(
509
+ LOCAL_STORAGE_LOGGED_IN_SESSIONS_KEY,
510
+ );
511
+ if (!data) return [];
512
+
513
+ let json: unknown;
514
+ try {
515
+ json = JSON.parse(data);
516
+ } catch {
517
+ console.error("Error parsing stored session data");
518
+ window.localStorage.removeItem(LOCAL_STORAGE_LOGGED_IN_SESSIONS_KEY);
519
+ return [];
520
+ }
521
+
522
+ const parsed = array(StoredSessionSchema).safeParse(json);
523
+ if (!parsed.success) {
524
+ console.error("Stored session data is invalid");
525
+ window.localStorage.removeItem(LOCAL_STORAGE_LOGGED_IN_SESSIONS_KEY);
526
+ return [];
527
+ }
528
+ return parsed.data;
529
+ }
530
+ protected set loggedInSessions(sessions: StoredSession[]) {
531
+ if (typeof window === "undefined") {
532
+ loggedInSessions_ = sessions;
533
+ return;
534
+ }
535
+
536
+ window.localStorage.setItem(
537
+ LOCAL_STORAGE_LOGGED_IN_SESSIONS_KEY,
538
+ JSON.stringify(sessions),
539
+ );
540
+ }
541
+
542
+ resolveSession(session: GraffitiSession): StoredSession {
543
+ const resolvedSession = this.loggedInSessions.find(
544
+ (s) => s.actor === session.actor,
545
+ );
546
+ if (!resolvedSession) {
547
+ const logoutEvent: GraffitiLogoutEvent = new CustomEvent("logout", {
548
+ detail: { actor: session.actor },
549
+ });
550
+ this.sessionEvents.dispatchEvent(logoutEvent);
551
+ throw new Error("Not logged in");
552
+ }
553
+ return resolvedSession;
554
+ }
555
+ }
556
+ let loggedInSessions_: StoredSession[] = [];
557
+
558
+ const LOCAL_STORAGE_IN_PROGRESS_LOGIN_KEY = "graffiti-login-in-progress";
559
+ const LOCAL_STORAGE_IN_PROGRESS_LOGOUT_KEY = "graffiti-logout-in-progress";
560
+ const LOCAL_STORAGE_LOGGED_IN_SESSIONS_KEY = "graffiti-sessions-logged-in";
561
+
562
+ const GraffitiSessionSchema = object({
563
+ actor: url(),
564
+ });
565
+
566
+ const ServiceSessionSchema = object({
567
+ token: string(),
568
+ serviceEndpoint: url(),
569
+ authorizationEndpoint: url(),
570
+ });
571
+
572
+ const ServicesByAuthorizationSchema = array(
573
+ tuple([
574
+ url(), // Authorization endpoint
575
+ array(
576
+ object({
577
+ endpoint: url(), // Service endpoint
578
+ type: enum_(["bucket", "personal-inbox", "shared-inbox"]),
579
+ }),
580
+ ),
581
+ ]),
582
+ );
583
+
584
+ const InProgressSchema = extend(GraffitiSessionSchema, {
585
+ tokens: array(string()),
586
+ servicesByAuthorization: ServicesByAuthorizationSchema,
587
+ });
588
+
589
+ const StoredSessionSchema = extend(InProgressSchema, {
590
+ storageBucket: ServiceSessionSchema,
591
+ personalInbox: ServiceSessionSchema,
592
+ sharedInboxes: array(ServiceSessionSchema),
593
+ });
594
+
595
+ type StoredSession = infer_<typeof StoredSessionSchema>;
596
+
597
+ function serviceToEndpoint(service: Service): string {
598
+ if (typeof service.serviceEndpoint === "string")
599
+ return service.serviceEndpoint;
600
+ throw new Error(`Service endpoint for ${service.id} is not a string`);
601
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Handles } from "./2-handles";
3
+ import { DecentralizedIdentifiers } from "../1-services/2-dids";
4
+
5
+ export function handleTests(handle: string) {
6
+ describe("Handles", async () => {
7
+ const handles = new Handles({
8
+ dids: new DecentralizedIdentifiers(),
9
+ });
10
+
11
+ test("handleToActor and actorToHandle", async () => {
12
+ const actor = await handles.handleToActor(handle);
13
+ const resolvedHandle = await handles.actorToHandle(actor);
14
+ expect(resolvedHandle).toBe(handle);
15
+ });
16
+ });
17
+ }
@@ -0,0 +1,60 @@
1
+ import type { Graffiti } from "@graffiti-garden/api";
2
+ import { GraffitiErrorNotFound } from "@graffiti-garden/api";
3
+ import { DecentralizedIdentifiers } from "../1-services/2-dids";
4
+
5
+ // Handles used a fixed method
6
+ const HANDLE_DID_PREFIX = "did:web:";
7
+
8
+ export class Handles {
9
+ constructor(
10
+ protected readonly services: { dids: DecentralizedIdentifiers },
11
+ ) {}
12
+
13
+ actorToHandle: Graffiti["actorToHandle"] = async (actor) => {
14
+ const actorDocument = await this.services.dids.resolve(actor);
15
+
16
+ const handleDid = actorDocument.alsoKnownAs?.at(0);
17
+ if (!handleDid) {
18
+ throw new GraffitiErrorNotFound(
19
+ `Handle for actor DID ${actor} not found`,
20
+ );
21
+ }
22
+ if (!handleDid.startsWith(HANDLE_DID_PREFIX)) {
23
+ throw new Error(`Handle DID ${handleDid} is not a valid handle`);
24
+ }
25
+
26
+ const handle = handleDid.slice(HANDLE_DID_PREFIX.length);
27
+
28
+ const handleDocument = await this.services.dids.resolve(handleDid);
29
+ if (
30
+ !handleDocument.alsoKnownAs ||
31
+ !handleDocument.alsoKnownAs.includes(actor)
32
+ ) {
33
+ throw new Error(`Handle ${handle} does not match actor ${actor}`);
34
+ }
35
+
36
+ return handle;
37
+ };
38
+
39
+ handleToActor: Graffiti["handleToActor"] = async (handle) => {
40
+ const handleDid = `${HANDLE_DID_PREFIX}${handle}`;
41
+ const handleDocument = await this.services.dids.resolve(handleDid);
42
+
43
+ const actor = handleDocument.alsoKnownAs?.at(0);
44
+ if (!actor) {
45
+ throw new GraffitiErrorNotFound(
46
+ `Actor for handle DID ${handleDid} not found`,
47
+ );
48
+ }
49
+
50
+ const actorDocument = await this.services.dids.resolve(actor);
51
+ if (
52
+ !actorDocument.alsoKnownAs ||
53
+ !actorDocument.alsoKnownAs.includes(handleDid)
54
+ ) {
55
+ throw new Error(`Actor ${actor} does not match handle ${handle}`);
56
+ }
57
+
58
+ return actor;
59
+ };
60
+ }