@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,399 @@
1
+ import {
2
+ discovery,
3
+ randomState,
4
+ buildAuthorizationUrl,
5
+ authorizationCodeGrant,
6
+ tokenRevocation,
7
+ } from "openid-client";
8
+ import {
9
+ type infer as infer_,
10
+ string,
11
+ url,
12
+ array,
13
+ object,
14
+ optional,
15
+ nullable,
16
+ instanceof as instanceof_,
17
+ undefined as undefined_,
18
+ intersection,
19
+ union,
20
+ } from "zod/mini";
21
+ import type { IncomingMessage, ServerResponse } from "node:http";
22
+ import type { Socket } from "node:net";
23
+
24
+ const AUTHORIZATION_ENDPOINT_METHOD_PREFIX_OAUTH2 = "oauth2:";
25
+ const LOCAL_STORAGE_OAUTH2_KEY = "graffiti-auth-oauth2-data";
26
+
27
+ export class Authorization {
28
+ eventTarget: EventTarget = new EventTarget();
29
+
30
+ constructor() {
31
+ // Extract oauth redirect synchronously so the route
32
+ // can be changed before any SPA routers (e.g. vue router)
33
+ // start messing with things
34
+ const oauthPromise = this.completeOauth();
35
+
36
+ (async () => {
37
+ // Allow listeners to be added first
38
+ await new Promise((resolve) => setTimeout(resolve, 0));
39
+
40
+ // Complete the oauth flow
41
+ await oauthPromise;
42
+
43
+ // Send an initialized event
44
+ const initializedEvent: InitializedEvent = new CustomEvent("initialized");
45
+ this.eventTarget.dispatchEvent(initializedEvent);
46
+ })();
47
+ }
48
+
49
+ async login(...args: Parameters<typeof this.login_>): Promise<void> {
50
+ try {
51
+ await this.login_(...args);
52
+ } catch (e) {
53
+ const error = e instanceof Error ? e : new Error("Unknown error");
54
+ const detail: LoginEvent["detail"] = { loginId: args[1], error };
55
+ this.eventTarget.dispatchEvent(new CustomEvent("login", { detail }));
56
+ }
57
+ }
58
+ protected async login_(
59
+ authorizationEndpoint: string,
60
+ loginId: string,
61
+ serviceEndpoints: string[],
62
+ ): Promise<void> {
63
+ const configuration = await this.getAuthorizationConfiguration(
64
+ authorizationEndpoint,
65
+ );
66
+
67
+ const scope = serviceEndpoints.map(encodeURIComponent).join(" ");
68
+ const state = randomState();
69
+
70
+ let redirectUri: string;
71
+ let waitForCallback: Promise<void> | undefined = undefined;
72
+ if (typeof window !== "undefined") {
73
+ // If in a browser, prepare for a redirect by
74
+ // storing the configuration, expected state,
75
+ // current URL, and endpoints in local storage
76
+ redirectUri = window.location.href;
77
+ const data: infer_<typeof OAuth2LoginDataSchema> = {
78
+ loginId,
79
+ redirectUri,
80
+ authorizationEndpoint,
81
+ state,
82
+ serviceEndpoints,
83
+ };
84
+ window.localStorage.setItem(
85
+ LOCAL_STORAGE_OAUTH2_KEY,
86
+ JSON.stringify(data),
87
+ );
88
+ } else {
89
+ // Otherwise, in node, start a local server to receive the callback
90
+ const http = await import("node:http").catch((e) => {
91
+ throw new Error(
92
+ "Unrecognized environment: cannot find window or node:http",
93
+ );
94
+ });
95
+ const server = http.createServer();
96
+
97
+ try {
98
+ await new Promise<void>((resolve, reject) => {
99
+ server.once("error", reject);
100
+ server.listen(0, "::1", resolve);
101
+ });
102
+ } catch (e) {
103
+ try {
104
+ server.close();
105
+ } catch {}
106
+ throw new Error("Failed to start local oauth callback server.");
107
+ }
108
+
109
+ const address = server.address();
110
+ if (!address) {
111
+ try {
112
+ server.close();
113
+ } catch {}
114
+ throw new Error("Failed to get local oauth callback server address.");
115
+ }
116
+ redirectUri =
117
+ typeof address === "string"
118
+ ? `http://${address}`
119
+ : `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`;
120
+
121
+ // Wait for a callback request
122
+ waitForCallback = new Promise<void>((resolve, reject) => {
123
+ const timeout = setTimeout(
124
+ () => {
125
+ try {
126
+ server.close();
127
+ } catch {}
128
+ reject("Oauth callback timed out.");
129
+ },
130
+ 5 * 60 * 1000, // 5 minutes
131
+ );
132
+
133
+ const sockets = new Set<Socket>();
134
+ server.on("connection", (socket: Socket) => {
135
+ sockets.add(socket);
136
+ socket.on("close", () => {
137
+ sockets.delete(socket);
138
+ });
139
+ });
140
+
141
+ // Set up the actual request handler
142
+ const onRequest = async (req: IncomingMessage, res: ServerResponse) => {
143
+ try {
144
+ const callbackUrl = new URL(req.url ?? "/", redirectUri);
145
+ await this.onCallbackUrl({
146
+ loginId,
147
+ callbackUrl,
148
+ configuration,
149
+ expectedState: state,
150
+ serviceEndpoints,
151
+ });
152
+
153
+ res.statusCode = 200;
154
+ res.setHeader("Content-Type", "text/plain");
155
+ res.end("You may now close this window.");
156
+ } catch (e) {
157
+ res.statusCode = 500;
158
+ res.setHeader("Content-Type", "text/plain");
159
+ res.end("Error processing OAuth callback.");
160
+
161
+ throw e;
162
+ } finally {
163
+ clearTimeout(timeout);
164
+ server.off("request", onRequest);
165
+
166
+ for (const socket of sockets) {
167
+ socket.destroy();
168
+ }
169
+
170
+ server.close(() => resolve());
171
+ }
172
+ };
173
+
174
+ server.on("request", onRequest);
175
+ });
176
+ }
177
+
178
+ // Construct the authorization URL
179
+ const redirectUriStripped = new URL(redirectUri);
180
+ redirectUriStripped.hash = "";
181
+ redirectUriStripped.search = "";
182
+
183
+ const redirectTo = buildAuthorizationUrl(configuration, {
184
+ scope,
185
+ redirect_uri: redirectUriStripped.toString(),
186
+ state,
187
+ });
188
+
189
+ // Either redirect (browser) or print the URL and wait (node)
190
+ if (typeof window !== "undefined") {
191
+ window.location.href = redirectTo.toString();
192
+ } else {
193
+ console.log("Please open the following URL in your browser:");
194
+ console.log(redirectTo.toString());
195
+ await waitForCallback;
196
+ }
197
+ }
198
+
199
+ protected completeOauth() {
200
+ if (typeof window === "undefined") return;
201
+
202
+ // Look in local storage to see if we have a pending login
203
+ const data = window.localStorage.getItem(LOCAL_STORAGE_OAUTH2_KEY);
204
+ if (!data) return;
205
+
206
+ let json: unknown;
207
+ try {
208
+ json = JSON.parse(data);
209
+ } catch {
210
+ console.error("Invalid OAuth2 login data in local storage.");
211
+ window.localStorage.removeItem(LOCAL_STORAGE_OAUTH2_KEY);
212
+ return;
213
+ }
214
+
215
+ const parseResult = OAuth2LoginDataSchema.safeParse(json);
216
+ if (!parseResult.success) {
217
+ console.error(
218
+ "Invalid OAuth2 login data structure in local storage.",
219
+ parseResult.error,
220
+ );
221
+ window.localStorage.removeItem(LOCAL_STORAGE_OAUTH2_KEY);
222
+ return;
223
+ }
224
+
225
+ const {
226
+ loginId,
227
+ redirectUri,
228
+ authorizationEndpoint,
229
+ state,
230
+ serviceEndpoints,
231
+ } = parseResult.data;
232
+
233
+ try {
234
+ // Make sure that we redirected back to the correct page
235
+ const expectedUrl = new URL(redirectUri);
236
+ const callbackUrl = new URL(window.location.href);
237
+ if (expectedUrl.pathname !== callbackUrl.pathname) return;
238
+ // Make sure it is actually an oauth call
239
+ const params = callbackUrl.searchParams;
240
+ if (!params.has("code") && !params.has("error")) return;
241
+
242
+ // Restore the hash and query parameters to the expected URL,
243
+ // removing the code, state, and error parameters
244
+ window.history.replaceState({}, document.title, expectedUrl.toString());
245
+ window.localStorage.removeItem(LOCAL_STORAGE_OAUTH2_KEY);
246
+
247
+ return new Promise<void>((resolve) => setTimeout(resolve, 0))
248
+ .then(() => this.getAuthorizationConfiguration(authorizationEndpoint))
249
+ .then((configuration) =>
250
+ this.onCallbackUrl({
251
+ loginId,
252
+ callbackUrl,
253
+ configuration,
254
+ expectedState: state,
255
+ serviceEndpoints,
256
+ }),
257
+ )
258
+ .catch((e) => {
259
+ const error = e instanceof Error ? e : new Error("Unknown error");
260
+ const detail: LoginEvent["detail"] = { loginId, error };
261
+ this.eventTarget.dispatchEvent(new CustomEvent("login", { detail }));
262
+ });
263
+ } catch (e) {
264
+ console.error(e);
265
+ }
266
+ }
267
+
268
+ protected async onCallbackUrl(args: {
269
+ loginId: string;
270
+ callbackUrl: URL;
271
+ configuration: any;
272
+ expectedState: string;
273
+ serviceEndpoints: string[];
274
+ }) {
275
+ const {
276
+ loginId,
277
+ callbackUrl,
278
+ configuration,
279
+ expectedState,
280
+ serviceEndpoints,
281
+ } = args;
282
+
283
+ const response = await authorizationCodeGrant(configuration, callbackUrl, {
284
+ expectedState,
285
+ });
286
+
287
+ const token = response.access_token;
288
+ const scope = response.scope;
289
+ const grantedEndpoints =
290
+ scope?.split(" ").map(decodeURIComponent) || serviceEndpoints;
291
+
292
+ // Make sure granted endpoints cover the requested endpoints
293
+ if (
294
+ !serviceEndpoints.every((endpoint) => grantedEndpoints.includes(endpoint))
295
+ ) {
296
+ throw new Error("Not all requested service endpoints were granted.");
297
+ }
298
+
299
+ // Send a logged in event
300
+ const loginEvent: LoginEvent = new CustomEvent("login", {
301
+ detail: {
302
+ loginId,
303
+ token,
304
+ },
305
+ });
306
+ this.eventTarget.dispatchEvent(loginEvent);
307
+ }
308
+
309
+ async logout(
310
+ authorizationEndpoint: string,
311
+ logoutId: string,
312
+ token: string,
313
+ ): Promise<void> {
314
+ try {
315
+ await this.logout_(authorizationEndpoint, logoutId, token);
316
+ } catch (e) {
317
+ const error = e instanceof Error ? e : new Error("Unknown error");
318
+ const detail: LogoutEvent["detail"] = { logoutId, error };
319
+ this.eventTarget.dispatchEvent(new CustomEvent("logout", { detail }));
320
+ }
321
+ }
322
+ protected async logout_(
323
+ authorizationEndpoint: string,
324
+ logoutId: string,
325
+ token: string,
326
+ ): Promise<void> {
327
+ const configuration = await this.getAuthorizationConfiguration(
328
+ authorizationEndpoint,
329
+ );
330
+ await tokenRevocation(configuration, token);
331
+ const detail: LogoutEvent["detail"] = { logoutId };
332
+ this.eventTarget.dispatchEvent(new CustomEvent("logout", { detail }));
333
+ }
334
+
335
+ protected async getAuthorizationConfiguration(
336
+ authorizationEndpoint: string,
337
+ ): Promise<any> {
338
+ // Parse the authorization endpoint
339
+ if (
340
+ !authorizationEndpoint.startsWith(
341
+ AUTHORIZATION_ENDPOINT_METHOD_PREFIX_OAUTH2,
342
+ )
343
+ ) {
344
+ throw new Error(
345
+ `Unrecognized authorization endpoint method: ${authorizationEndpoint}`,
346
+ );
347
+ }
348
+ const issuer = authorizationEndpoint.slice(
349
+ AUTHORIZATION_ENDPOINT_METHOD_PREFIX_OAUTH2.length,
350
+ );
351
+
352
+ // Look up the oauth configuration
353
+ let issuerUrl: URL;
354
+ try {
355
+ issuerUrl = new URL(issuer);
356
+ } catch (e) {
357
+ throw new Error("Invalid issuer URL.");
358
+ }
359
+
360
+ return await discovery(issuerUrl, "graffiti-client");
361
+ }
362
+ }
363
+
364
+ export const LoginEventDetailSchema = intersection(
365
+ object({
366
+ loginId: string(),
367
+ }),
368
+ union([
369
+ object({ token: string(), error: optional(undefined_()) }),
370
+ object({ error: instanceof_(Error) }),
371
+ ]),
372
+ );
373
+
374
+ export const LogoutEventDetailSchema = object({
375
+ logoutId: string(),
376
+ error: optional(instanceof_(Error)),
377
+ });
378
+
379
+ export const InitializedEventDetailSchema = optional(
380
+ nullable(
381
+ object({
382
+ error: optional(instanceof_(Error)),
383
+ }),
384
+ ),
385
+ );
386
+
387
+ export type LoginEvent = CustomEvent<infer_<typeof LoginEventDetailSchema>>;
388
+ export type LogoutEvent = CustomEvent<infer_<typeof LogoutEventDetailSchema>>;
389
+ export type InitializedEvent = CustomEvent<
390
+ infer_<typeof InitializedEventDetailSchema>
391
+ >;
392
+
393
+ const OAuth2LoginDataSchema = object({
394
+ loginId: string(),
395
+ redirectUri: url(),
396
+ authorizationEndpoint: url(),
397
+ state: string(),
398
+ serviceEndpoints: array(url()),
399
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { DecentralizedIdentifiers } from "./2-dids";
3
+
4
+ export function didTests() {
5
+ return describe("DecentralizedIdentifiers", () => {
6
+ const dids = new DecentralizedIdentifiers();
7
+
8
+ test("invalid method", async () => {
9
+ await expect(dids.resolve("did:invalid:12345")).rejects.toThrowError();
10
+ });
11
+
12
+ test("did:web", async () => {
13
+ const did = "did:web:identity.foundation";
14
+ const result = await dids.resolve(did);
15
+ expect(result).toHaveProperty("id", did);
16
+ });
17
+
18
+ test("did:plc", async () => {
19
+ const did = "did:plc:44ybard66vv44zksje25o7dz";
20
+ const result = await dids.resolve(did);
21
+ expect(result).toHaveProperty("id", did);
22
+ });
23
+ });
24
+ }
@@ -0,0 +1,30 @@
1
+ import { GraffitiErrorNotFound } from "@graffiti-garden/api";
2
+ import { Resolver, type DIDDocument } from "did-resolver";
3
+ import { getResolver as plcResolver } from "plc-did-resolver";
4
+ import { getResolver as webResolver } from "web-did-resolver";
5
+
6
+ export class DecentralizedIdentifiers {
7
+ protected readonly methods = {
8
+ ...plcResolver(),
9
+ ...webResolver(),
10
+ };
11
+
12
+ protected readonly resolver = new Resolver(this.methods, { cache: true });
13
+
14
+ async resolve(did: string): Promise<DIDDocument> {
15
+ if (
16
+ !Object.keys(this.methods).some((method) =>
17
+ did.startsWith(`did:${method}:`),
18
+ )
19
+ ) {
20
+ throw new Error(`Unrecognized DID method: ${did}`);
21
+ }
22
+
23
+ const { didDocument } = await this.resolver.resolve(did);
24
+ if (!didDocument) {
25
+ throw new GraffitiErrorNotFound(`DID not found: ${did}`);
26
+ }
27
+
28
+ return didDocument;
29
+ }
30
+ }
@@ -0,0 +1,121 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { StorageBuckets } from "./3-storage-buckets";
3
+ import { GraffitiErrorUnauthorized } from "./utilities";
4
+ import { GraffitiErrorNotFound } from "@graffiti-garden/api";
5
+
6
+ export function storageBucketTests(
7
+ storageBucketEndpoint: string,
8
+ storageBucketToken: string,
9
+ ) {
10
+ describe("Storage buckets", async () => {
11
+ const storageBuckets = new StorageBuckets();
12
+
13
+ test("put, get, delete", async () => {
14
+ const key = Math.random().toString(36).substring(2, 15);
15
+ const input = "Hello world";
16
+ const bytes = new TextEncoder().encode(input);
17
+
18
+ await expect(
19
+ storageBuckets.get(storageBucketEndpoint, key),
20
+ ).rejects.toThrow(GraffitiErrorNotFound);
21
+
22
+ await storageBuckets.put(
23
+ storageBucketEndpoint,
24
+ key,
25
+ bytes,
26
+ storageBucketToken,
27
+ );
28
+
29
+ const resultBytes = await storageBuckets.get(
30
+ storageBucketEndpoint,
31
+ key,
32
+ bytes.length,
33
+ );
34
+ const result = new TextDecoder().decode(resultBytes);
35
+ expect(result).toEqual(input);
36
+
37
+ await storageBuckets.delete(
38
+ storageBucketEndpoint,
39
+ key,
40
+ storageBucketToken,
41
+ );
42
+
43
+ await expect(
44
+ storageBuckets.get(storageBucketEndpoint, key),
45
+ ).rejects.toThrow(GraffitiErrorNotFound);
46
+ });
47
+
48
+ test("get with limit less than object", async () => {
49
+ const key = Math.random().toString(36).substring(2, 15);
50
+ const input = "Hello world";
51
+ const bytes = new TextEncoder().encode(input);
52
+
53
+ await storageBuckets.put(
54
+ storageBucketEndpoint,
55
+ key,
56
+ bytes,
57
+ storageBucketToken,
58
+ );
59
+
60
+ await expect(
61
+ storageBuckets.get(storageBucketEndpoint, key, bytes.length - 1),
62
+ ).rejects.toThrow();
63
+ });
64
+
65
+ test("unauthorized", async () => {
66
+ const key = Math.random().toString(36).substring(2, 15);
67
+ const input = "Hello world";
68
+ const bytes = new TextEncoder().encode(input);
69
+
70
+ await expect(
71
+ storageBuckets.put(storageBucketEndpoint, key, bytes, "invalid-token"),
72
+ ).rejects.toThrow(GraffitiErrorUnauthorized);
73
+ await expect(
74
+ storageBuckets.delete(storageBucketEndpoint, key, "invalid-token"),
75
+ ).rejects.toThrow(GraffitiErrorUnauthorized);
76
+ await expect(
77
+ storageBuckets.export(storageBucketEndpoint, "invalid-token").next(),
78
+ ).rejects.toThrow(GraffitiErrorUnauthorized);
79
+ });
80
+
81
+ test("export", async () => {
82
+ // Put a whole bunch of stuff so the export needs to page
83
+ const keys = new Set<string>();
84
+ for (let i = 0; i < 256; i++) {
85
+ const key = Math.random().toString(36).substring(2, 15);
86
+ keys.add(key);
87
+
88
+ const input = "Hello world " + i;
89
+ const bytes = new TextEncoder().encode(input);
90
+ await storageBuckets.put(
91
+ storageBucketEndpoint,
92
+ key,
93
+ bytes,
94
+ storageBucketToken,
95
+ );
96
+ }
97
+
98
+ // Export
99
+ const retrievedKeys = new Set<string>();
100
+ const iterator = storageBuckets.export(
101
+ storageBucketEndpoint,
102
+ storageBucketToken,
103
+ );
104
+ for await (const result of iterator) {
105
+ if (keys.has(result.key)) {
106
+ retrievedKeys.add(result.key);
107
+ }
108
+ }
109
+ expect(retrievedKeys.size).toEqual(keys.size);
110
+
111
+ // Delete all the keys
112
+ for (const key of keys) {
113
+ await storageBuckets.delete(
114
+ storageBucketEndpoint,
115
+ key,
116
+ storageBucketToken,
117
+ );
118
+ }
119
+ }, 1000000);
120
+ });
121
+ }