@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,1265 @@
1
+ import type { JSONSchema } from "json-schema-to-ts";
2
+ import {
3
+ GraffitiErrorNotFound,
4
+ maskGraffitiObject,
5
+ type Graffiti,
6
+ type GraffitiLoginEvent,
7
+ type GraffitiObjectBase,
8
+ type GraffitiSession,
9
+ type GraffitiObject,
10
+ unpackObjectUrl,
11
+ compileGraffitiObjectSchema,
12
+ GraffitiErrorSchemaMismatch,
13
+ GraffitiErrorForbidden,
14
+ GraffitiErrorTooLarge,
15
+ isMediaAcceptable,
16
+ GraffitiErrorNotAcceptable,
17
+ GraffitiErrorCursorExpired,
18
+ GraffitiErrorInvalidSchema,
19
+ type GraffitiObjectStream,
20
+ } from "@graffiti-garden/api";
21
+ import { randomBytes } from "@noble/hashes/utils.js";
22
+ import {
23
+ encode as dagCborEncode,
24
+ decode as dagCborDecode,
25
+ } from "@ipld/dag-cbor";
26
+
27
+ import { DecentralizedIdentifiers } from "../1-services/2-dids";
28
+ import { Authorization } from "../1-services/1-authorization";
29
+ import { StorageBuckets } from "../1-services/3-storage-buckets";
30
+ import {
31
+ Inboxes,
32
+ LABELED_MESSAGE_MESSAGE_KEY,
33
+ MESSAGE_METADATA_KEY,
34
+ MESSAGE_OBJECT_KEY,
35
+ MESSAGE_TAGS_KEY,
36
+ type MessageStream,
37
+ } from "../1-services/4-inboxes";
38
+
39
+ import {
40
+ StringEncoder,
41
+ STRING_ENCODER_METHOD_BASE64URL,
42
+ } from "../2-primitives/1-string-encoding";
43
+ import { ContentAddresses } from "../2-primitives/2-content-addresses";
44
+ import {
45
+ CHANNEL_ATTESTATION_METHOD_SHA256_ED25519,
46
+ ChannelAttestations,
47
+ } from "../2-primitives/3-channel-attestations";
48
+ import { AllowedAttestations } from "../2-primitives/4-allowed-attestations";
49
+
50
+ import { Handles } from "./2-handles";
51
+ import {
52
+ Sessions,
53
+ DID_SERVICE_ID_GRAFFITI_PERSONAL_INBOX,
54
+ DID_SERVICE_TYPE_GRAFFITI_INBOX,
55
+ DID_SERVICE_ID_GRAFFITI_STORAGE_BUCKET,
56
+ DID_SERVICE_TYPE_GRAFFITI_STORAGE_BUCKET,
57
+ } from "./1-sessions";
58
+ import {
59
+ decodeObjectUrl,
60
+ MAX_OBJECT_SIZE_BYTES,
61
+ ObjectEncoding,
62
+ } from "./3-object-encoding";
63
+
64
+ import { GraffitiModal } from "@graffiti-garden/modal";
65
+ import {
66
+ type infer as infer_,
67
+ custom,
68
+ string,
69
+ boolean,
70
+ strictObject,
71
+ array,
72
+ int,
73
+ nonnegative,
74
+ optional,
75
+ extend,
76
+ union,
77
+ record,
78
+ url,
79
+ } from "zod/mini";
80
+
81
+ const Uint8ArraySchema = custom<Uint8Array>(
82
+ (v): v is Uint8Array => v instanceof Uint8Array,
83
+ );
84
+ const MESSAGE_DATA_STORAGE_BUCKET_KEY = "k";
85
+ const MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY = "t";
86
+ const MessageMetadataBaseSchema = strictObject({
87
+ [MESSAGE_DATA_STORAGE_BUCKET_KEY]: string(),
88
+ [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: optional(string()),
89
+ });
90
+ const MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY = "id";
91
+ const MESSAGE_DATA_ANNOUNCEMENT_ENDPOINT_KEY = "e";
92
+ const MESSAGE_DATA_ANNOUNCEMENT_ACTOR_KEY = "a";
93
+ const MessageMetadataAnnouncementsSchema = array(
94
+ strictObject({
95
+ [MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY]: string(),
96
+ [MESSAGE_DATA_ANNOUNCEMENT_ENDPOINT_KEY]: optional(url()),
97
+ [MESSAGE_DATA_ANNOUNCEMENT_ACTOR_KEY]: optional(url()),
98
+ }),
99
+ );
100
+ const MESSAGE_DATA_ALLOWED_TICKETS_KEY = "s";
101
+ const MESSAGE_DATA_ANNOUNCEMENTS_KEY = "n";
102
+ const MessageMetaDataSelfSchema = extend(MessageMetadataBaseSchema, {
103
+ [MESSAGE_DATA_ALLOWED_TICKETS_KEY]: optional(array(Uint8ArraySchema)),
104
+ [MESSAGE_DATA_ANNOUNCEMENTS_KEY]: MessageMetadataAnnouncementsSchema,
105
+ });
106
+ const MESSAGE_DATA_ALLOWED_TICKET_KEY = "a";
107
+ const MESSAGE_DATA_ALLOWED_TICKET_INDEX_KEY = "i";
108
+ const MessageMetadataPrivateSchema = extend(MessageMetadataBaseSchema, {
109
+ [MESSAGE_DATA_ALLOWED_TICKET_KEY]: Uint8ArraySchema,
110
+ [MESSAGE_DATA_ALLOWED_TICKET_INDEX_KEY]: int().check(nonnegative()),
111
+ });
112
+ const MessageMetadataSchema = union([
113
+ MessageMetadataBaseSchema,
114
+ MessageMetaDataSelfSchema,
115
+ MessageMetadataPrivateSchema,
116
+ ]);
117
+ type MessageMetadataBase = infer_<typeof MessageMetadataBaseSchema>;
118
+ type MessageMetadata = infer_<typeof MessageMetadataSchema>;
119
+ type MessageMetadataAnnouncements = infer_<
120
+ typeof MessageMetadataAnnouncementsSchema
121
+ >;
122
+
123
+ const MESSAGE_LABEL_UNLABELED = 0;
124
+ const MESSAGE_LABEL_VALID = 1;
125
+ const MESSAGE_LABEL_TRASH = 2;
126
+ const MESSAGE_LABEL_INVALID = 3;
127
+
128
+ export interface GraffitiDecentralizedOptions {
129
+ identityCreatorEndpoint?: string;
130
+ defaultInboxEndpoints?: string[];
131
+ }
132
+
133
+ export class GraffitiDecentralized implements Graffiti {
134
+ protected readonly dids = new DecentralizedIdentifiers();
135
+ protected readonly authorization = new Authorization();
136
+ protected readonly storageBuckets = new StorageBuckets();
137
+ protected readonly inboxes = new Inboxes();
138
+
139
+ protected readonly stringEncoder = new StringEncoder();
140
+ protected readonly contentAddresses = new ContentAddresses();
141
+ protected readonly channelAttestations = new ChannelAttestations();
142
+ protected readonly allowedAttestations = new AllowedAttestations();
143
+
144
+ protected readonly sessions = new Sessions({
145
+ dids: this.dids,
146
+ authorization: this.authorization,
147
+ storageBuckets: this.storageBuckets,
148
+ inboxes: this.inboxes,
149
+ });
150
+ protected readonly handles = new Handles({ dids: this.dids });
151
+ protected readonly objectEncoding = new ObjectEncoding({
152
+ stringEncoder: this.stringEncoder,
153
+ contentAddresses: this.contentAddresses,
154
+ channelAttestations: this.channelAttestations,
155
+ allowedAttestations: this.allowedAttestations,
156
+ });
157
+
158
+ protected readonly modal: GraffitiModal | undefined =
159
+ typeof window === "undefined"
160
+ ? undefined
161
+ : new GraffitiModal({
162
+ useTemplateHTML: () =>
163
+ import("./login-dialog.html").then(({ template }) => template),
164
+ onManualClose: () => {
165
+ const event = new CustomEvent("login", {
166
+ detail: {
167
+ error: new Error("User cancelled login"),
168
+ manual: true,
169
+ },
170
+ });
171
+ this.sessionEvents.dispatchEvent(event);
172
+ },
173
+ });
174
+
175
+ protected readonly defaultInboxEndpoints: string[];
176
+ protected readonly identityCreatorEndpoint: string;
177
+ constructor(options?: GraffitiDecentralizedOptions) {
178
+ this.defaultInboxEndpoints = options?.defaultInboxEndpoints ?? [
179
+ "https://graffiti.actor/i/shared",
180
+ ];
181
+ this.identityCreatorEndpoint =
182
+ options?.identityCreatorEndpoint ?? "https://graffiti.actor/create";
183
+
184
+ this.sessionEvents.addEventListener("login", async (event) => {
185
+ if (!(event instanceof CustomEvent)) return;
186
+ const detail = event.detail as GraffitiLoginEvent["detail"];
187
+ if (
188
+ detail.error !== undefined &&
189
+ !("manual" in detail && detail.manual)
190
+ ) {
191
+ alert("Login failed: " + detail.error.message);
192
+ const actor = detail.session?.actor;
193
+ let handle: string | undefined;
194
+ if (actor) {
195
+ try {
196
+ handle = await this.actorToHandle(actor);
197
+ } catch (error) {
198
+ console.error("Failed to handle actor:", error);
199
+ }
200
+ }
201
+ this.login_(handle);
202
+ }
203
+ });
204
+ }
205
+
206
+ readonly actorToHandle: Graffiti["actorToHandle"] =
207
+ this.handles.actorToHandle.bind(this.handles);
208
+ readonly handleToActor: Graffiti["handleToActor"] =
209
+ this.handles.handleToActor.bind(this.handles);
210
+ readonly sessionEvents: Graffiti["sessionEvents"] =
211
+ this.sessions.sessionEvents;
212
+
213
+ login: Graffiti["login"] = async (actor?: string) => {
214
+ try {
215
+ let proposedHandle: string | undefined;
216
+ try {
217
+ proposedHandle = actor ? await this.actorToHandle(actor) : undefined;
218
+ } catch (error) {
219
+ console.error("Error fetching handle for actor:", error);
220
+ }
221
+
222
+ await this.login_(proposedHandle);
223
+ } catch (e) {
224
+ const loginError: GraffitiLoginEvent = new CustomEvent("login", {
225
+ detail: {
226
+ error: e instanceof Error ? e : new Error(String(e)),
227
+ },
228
+ });
229
+ this.sessionEvents.dispatchEvent(loginError);
230
+ }
231
+ };
232
+ protected async login_(proposedHandle?: string) {
233
+ if (typeof window !== "undefined") {
234
+ let template: HTMLElement | undefined;
235
+ if (proposedHandle !== undefined) {
236
+ template = await this.modal?.displayTemplate("graffiti-login-handle");
237
+ const input = template?.querySelector(
238
+ "#username",
239
+ ) as HTMLInputElement | null;
240
+ input?.setAttribute("value", proposedHandle);
241
+ input?.addEventListener("focus", () => input?.select());
242
+ new Promise<void>((r) => {
243
+ setTimeout(() => r(), 0);
244
+ }).then(() => {
245
+ input?.focus();
246
+ });
247
+
248
+ template
249
+ ?.querySelector("#graffiti-login-handle-form")
250
+ ?.addEventListener("submit", async (e) => {
251
+ e.preventDefault();
252
+ input?.setAttribute("disabled", "true");
253
+ const submitButton = template?.querySelector(
254
+ "#graffiti-login-handle-submit",
255
+ ) as HTMLButtonElement | null;
256
+ submitButton?.setAttribute("disabled", "true");
257
+ submitButton && (submitButton.innerHTML = "Logging in...");
258
+
259
+ if (!input?.value) {
260
+ alert("No handle provided");
261
+ this.login_("");
262
+ return;
263
+ }
264
+
265
+ let handle = input.value;
266
+ if (!handle.includes(".") && !handle.startsWith("localhost")) {
267
+ const defaultHost = new URL(this.identityCreatorEndpoint).host;
268
+ handle = `${handle}.${defaultHost}`;
269
+ }
270
+
271
+ let actor: string;
272
+ try {
273
+ actor = await this.handleToActor(handle);
274
+ } catch (e) {
275
+ alert("Could not find an identity associated with that handle.");
276
+ this.login_(handle);
277
+ return;
278
+ }
279
+
280
+ try {
281
+ await this.sessions.login(actor);
282
+ } catch (e) {
283
+ alert("Error logging in.");
284
+ console.error(e);
285
+ this.login_(handle);
286
+ }
287
+ });
288
+ } else {
289
+ template = await this.modal?.displayTemplate("graffiti-login-welcome");
290
+ template
291
+ ?.querySelector("#graffiti-login-existing")
292
+ ?.addEventListener("click", (e) => {
293
+ e.preventDefault();
294
+ this.login_("");
295
+ });
296
+ new Promise<void>((r) => {
297
+ setTimeout(() => r(), 0);
298
+ }).then(() => {
299
+ (
300
+ template?.querySelector("#graffiti-login-new") as HTMLAnchorElement
301
+ )?.focus();
302
+ });
303
+ }
304
+
305
+ const createUrl = new URL(this.identityCreatorEndpoint);
306
+ createUrl.searchParams.set(
307
+ "redirect_uri",
308
+ encodeURIComponent(window.location.toString()),
309
+ );
310
+ template
311
+ ?.querySelector("#graffiti-login-new")
312
+ ?.setAttribute("href", createUrl.toString());
313
+
314
+ await this.modal?.open();
315
+ } else {
316
+ // Node.js environment
317
+ const readline = await import("readline").catch((e) => {
318
+ throw new Error(
319
+ "Unrecognized environment: neither window nor readline",
320
+ );
321
+ });
322
+
323
+ console.log(
324
+ "If you do not already have a Graffiti handle, you can create one here:",
325
+ );
326
+ console.log(this.identityCreatorEndpoint);
327
+ const rl = readline.createInterface({
328
+ input: process.stdin,
329
+ output: process.stdout,
330
+ });
331
+
332
+ const handle: string | undefined = await new Promise((resolve) => {
333
+ rl.question(
334
+ `Please enter your handle${proposedHandle ? ` (default: ${proposedHandle})` : ""}: `,
335
+ (input) => {
336
+ rl.close();
337
+ resolve(input || proposedHandle);
338
+ },
339
+ );
340
+ });
341
+
342
+ if (!handle) {
343
+ throw new Error("No handle provided");
344
+ }
345
+
346
+ // Convert the handle to an actor
347
+ const actor = await this.handleToActor(handle);
348
+
349
+ await this.sessions.login(actor);
350
+ }
351
+ }
352
+
353
+ logout: Graffiti["logout"] = async (session) => {
354
+ await this.sessions.logout(session.actor);
355
+ };
356
+
357
+ // @ts-ignore
358
+ post: Graffiti["post"] = async (...args) => {
359
+ const [partialObject, session] = args;
360
+ const resolvedSession = this.sessions.resolveSession(session);
361
+
362
+ // Encode the object
363
+ const { object, tags, objectBytes, allowedTickets } =
364
+ await this.objectEncoding.encode<{}>(partialObject, session.actor);
365
+
366
+ // Generate a random key under which to store the object
367
+ // If the object is private, this means no one will be able to
368
+ // fetch the object, even if they know its URL.
369
+ // If the object is public but in some secret channel, the storage
370
+ // location means the object can be moved around or "rotated"
371
+ // without changing its URL.
372
+ const storageBucketKeyBytes = randomBytes();
373
+ const storageBucketKey = await this.stringEncoder.encode(
374
+ STRING_ENCODER_METHOD_BASE64URL,
375
+ storageBucketKeyBytes,
376
+ );
377
+
378
+ // Store the object at the random key
379
+ await this.storageBuckets.put(
380
+ resolvedSession.storageBucket.serviceEndpoint,
381
+ storageBucketKey,
382
+ objectBytes,
383
+ resolvedSession.storageBucket.token,
384
+ );
385
+
386
+ // Announce the object, its key,
387
+ // and other metadata to appropriate inboxes
388
+ await this.announceObject(
389
+ object,
390
+ tags,
391
+ allowedTickets,
392
+ storageBucketKey,
393
+ session,
394
+ );
395
+
396
+ return object;
397
+ };
398
+
399
+ get: Graffiti["get"] = async (...args) => {
400
+ const [url, schema, session] = args;
401
+ let services: { token?: string; serviceEndpoint: string }[];
402
+ const validator = await compileGraffitiObjectSchema(schema);
403
+
404
+ if (session) {
405
+ // If logged in, first search one's
406
+ // personal inbox, then any shared inboxes
407
+ const resolvedSession = this.sessions.resolveSession(session);
408
+ services = [
409
+ resolvedSession.personalInbox,
410
+ ...resolvedSession.sharedInboxes,
411
+ ];
412
+ } else {
413
+ // Otherwise, search the default inboxes
414
+ services = this.defaultInboxEndpoints.map((s) => ({
415
+ serviceEndpoint: s,
416
+ }));
417
+ }
418
+
419
+ // Search the inboxes for all objects
420
+ // matching the tag, object.url
421
+ const objectUrl = unpackObjectUrl(url);
422
+ const tags = [new TextEncoder().encode(objectUrl)];
423
+ for (const service of services) {
424
+ let object: GraffitiObjectBase | undefined = undefined;
425
+
426
+ const iterator = this.querySingleEndpoint<{}>(
427
+ service.serviceEndpoint,
428
+ {
429
+ tags,
430
+ objectSchema: {},
431
+ },
432
+ service.token,
433
+ session?.actor,
434
+ );
435
+
436
+ for await (const result of iterator) {
437
+ if (result.object.url !== objectUrl) continue;
438
+ if (result.tombstone) {
439
+ object = undefined;
440
+ } else {
441
+ object = result.object;
442
+ }
443
+ }
444
+
445
+ if (object) {
446
+ if (!validator(object)) {
447
+ throw new GraffitiErrorSchemaMismatch(
448
+ "Object exists but does not match the supplied schema",
449
+ );
450
+ }
451
+
452
+ return object;
453
+ }
454
+ }
455
+
456
+ throw new GraffitiErrorNotFound("Object not found");
457
+ };
458
+
459
+ delete: Graffiti["delete"] = async (url, session) => {
460
+ const resolvedSession = this.sessions.resolveSession(session);
461
+
462
+ const objectUrl = unpackObjectUrl(url);
463
+
464
+ const { actor } = decodeObjectUrl(objectUrl);
465
+ if (actor !== session.actor) {
466
+ throw new GraffitiErrorForbidden("Cannot delete someone else's actor");
467
+ }
468
+
469
+ // Look in one's personal inbox for the object
470
+ const iterator = this.querySingleEndpoint<{}>(
471
+ resolvedSession.personalInbox.serviceEndpoint,
472
+ {
473
+ tags: [new TextEncoder().encode(objectUrl)],
474
+ objectSchema: {},
475
+ },
476
+ resolvedSession.personalInbox.token,
477
+ );
478
+ let existing: SingleEndpointQueryResult<{}> | undefined;
479
+ for await (const result of iterator) {
480
+ if (result.object.url !== objectUrl) continue;
481
+ if (result.tombstone) {
482
+ existing = undefined;
483
+ } else {
484
+ existing = result;
485
+ }
486
+ }
487
+ if (!existing) {
488
+ throw new GraffitiErrorNotFound(`Object ${objectUrl} not found`);
489
+ }
490
+ const {
491
+ object,
492
+ storageBucketKey,
493
+ tags,
494
+ allowedTickets,
495
+ announcements,
496
+ messageId,
497
+ } = existing;
498
+
499
+ // Delete the object from the actor's own storage bucket
500
+ await this.storageBuckets.delete(
501
+ resolvedSession.storageBucket.serviceEndpoint,
502
+ storageBucketKey,
503
+ resolvedSession.storageBucket.token,
504
+ );
505
+
506
+ // Announce the deletion to all inboxes
507
+ await this.announceObject(
508
+ object,
509
+ tags,
510
+ allowedTickets,
511
+ storageBucketKey,
512
+ session,
513
+ [
514
+ ...(announcements ?? []),
515
+ // Make sure we delete from our own inbox too
516
+ {
517
+ [MESSAGE_DATA_ANNOUNCEMENT_ACTOR_KEY]: session.actor,
518
+ [MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY]: messageId,
519
+ },
520
+ ],
521
+ );
522
+
523
+ return object;
524
+ };
525
+
526
+ postMedia: Graffiti["postMedia"] = async (...args) => {
527
+ const [media, session] = args;
528
+
529
+ const type = media.data.type;
530
+
531
+ const resolvedSession = this.sessions.resolveSession(session);
532
+
533
+ // Generate a random storage key
534
+ const keyBytes = randomBytes();
535
+ const key = await this.stringEncoder.encode(
536
+ STRING_ENCODER_METHOD_BASE64URL,
537
+ keyBytes,
538
+ );
539
+
540
+ // Store the media at that key
541
+ await this.storageBuckets.put(
542
+ resolvedSession.storageBucket.serviceEndpoint,
543
+ key,
544
+ new Uint8Array(await media.data.arrayBuffer()),
545
+ resolvedSession.storageBucket.token,
546
+ );
547
+
548
+ // Create an object
549
+ const { url } = await this.post<typeof MEDIA_OBJECT_SCHEMA>(
550
+ {
551
+ value: {
552
+ key,
553
+ type,
554
+ size: media.data.size,
555
+ },
556
+ channels: [],
557
+ allowed: media.allowed,
558
+ },
559
+ session,
560
+ );
561
+
562
+ return url;
563
+ };
564
+
565
+ getMedia: Graffiti["getMedia"] = async (...args) => {
566
+ const [mediaUrl, accept, session] = args;
567
+
568
+ const object = await this.get<typeof MEDIA_OBJECT_SCHEMA>(
569
+ mediaUrl,
570
+ MEDIA_OBJECT_SCHEMA,
571
+ session,
572
+ );
573
+
574
+ const { key, type, size } = object.value;
575
+
576
+ if (accept?.maxBytes && size > accept.maxBytes) {
577
+ throw new GraffitiErrorTooLarge("File size exceeds limit");
578
+ }
579
+
580
+ // Make sure it adheres to requirements.accept
581
+ if (accept?.types) {
582
+ if (!isMediaAcceptable(type, accept.types)) {
583
+ throw new GraffitiErrorNotAcceptable(
584
+ `Unacceptable media type, ${type}`,
585
+ );
586
+ }
587
+ }
588
+
589
+ // Get the actor's storage bucket endpoint
590
+ const actorDocument = await this.dids.resolve(object.actor);
591
+ const storageBucketService = actorDocument?.service?.find(
592
+ (service) =>
593
+ service.id === DID_SERVICE_ID_GRAFFITI_STORAGE_BUCKET &&
594
+ service.type === DID_SERVICE_TYPE_GRAFFITI_STORAGE_BUCKET,
595
+ );
596
+ if (!storageBucketService) {
597
+ throw new GraffitiErrorNotFound(
598
+ `Actor ${object.actor} has no storage bucket service`,
599
+ );
600
+ }
601
+ if (typeof storageBucketService.serviceEndpoint !== "string") {
602
+ throw new GraffitiErrorNotFound(
603
+ `Actor ${object.actor} does not have a valid storage bucket endpoint`,
604
+ );
605
+ }
606
+ const storageBucketEndpoint = storageBucketService.serviceEndpoint;
607
+
608
+ const data = await this.storageBuckets.get(
609
+ storageBucketEndpoint,
610
+ key,
611
+ size,
612
+ );
613
+
614
+ const blob = new Blob([data.slice()], { type });
615
+
616
+ return {
617
+ data: blob,
618
+ actor: object.actor,
619
+ allowed: object.allowed,
620
+ };
621
+ };
622
+
623
+ deleteMedia: Graffiti["deleteMedia"] = async (...args) => {
624
+ const [mediaUrl, session] = args;
625
+
626
+ const resolvedSession = this.sessions.resolveSession(session);
627
+
628
+ const result = await this.delete(mediaUrl, session);
629
+
630
+ if (!("key" in result.value && typeof result.value.key === "string"))
631
+ throw new Error(
632
+ "Deleted object was not media: " + JSON.stringify(result, null, 2),
633
+ );
634
+
635
+ await this.storageBuckets.delete(
636
+ resolvedSession.storageBucket.serviceEndpoint,
637
+ result.value.key,
638
+ resolvedSession.storageBucket.token,
639
+ );
640
+ };
641
+
642
+ async *discoverMeta<Schema extends JSONSchema>(
643
+ channels: string[],
644
+ schema: Schema,
645
+ cursors: {
646
+ [endpoint: string]: string;
647
+ },
648
+ session?: GraffitiSession | null,
649
+ ): GraffitiObjectStream<Schema> {
650
+ const tombstones = new Map<string, boolean>();
651
+
652
+ let allInboxes: { serviceEndpoint: string; token?: string }[];
653
+ if (session) {
654
+ const resolvedSession = this.sessions.resolveSession(session);
655
+ allInboxes = [
656
+ resolvedSession.personalInbox,
657
+ ...resolvedSession.sharedInboxes,
658
+ ];
659
+ } else {
660
+ allInboxes = this.defaultInboxEndpoints.map((e) => ({
661
+ serviceEndpoint: e,
662
+ }));
663
+ }
664
+
665
+ // Make sure all cursors are represented by an inbox
666
+ for (const endpoint in cursors) {
667
+ if (!allInboxes.some((i) => i.serviceEndpoint === endpoint)) {
668
+ throw new GraffitiErrorForbidden(
669
+ "Cursor does not match actor's inboxes",
670
+ );
671
+ }
672
+ }
673
+
674
+ // Turn the channels into tags
675
+ const tags = await Promise.all(
676
+ channels.map((c) =>
677
+ this.channelAttestations.register(
678
+ CHANNEL_ATTESTATION_METHOD_SHA256_ED25519,
679
+ c,
680
+ ),
681
+ ),
682
+ );
683
+
684
+ const iterators: SingleEndpointQueryIterator<Schema>[] = allInboxes.map(
685
+ (i) => {
686
+ const cursor = cursors[i.serviceEndpoint];
687
+ return this.querySingleEndpoint<Schema>(
688
+ i.serviceEndpoint,
689
+ cursor
690
+ ? {
691
+ cursor,
692
+ }
693
+ : {
694
+ tags,
695
+ objectSchema: schema,
696
+ },
697
+ i.token,
698
+ session?.actor,
699
+ );
700
+ },
701
+ );
702
+
703
+ let indexedIteratorNexts = iterators.map<
704
+ Promise<IndexedSingleEndpointQueryResult<Schema>>
705
+ >(async (it, index) => indexedSingleEndpointQueryNext<Schema>(it, index));
706
+ let active = indexedIteratorNexts.length;
707
+
708
+ while (active > 0) {
709
+ const next: IndexedSingleEndpointQueryResult<Schema> =
710
+ await Promise.race<any>(indexedIteratorNexts);
711
+ if (next.error !== undefined) {
712
+ // Remove it from the race
713
+ indexedIteratorNexts[next.index] = new Promise(() => {});
714
+ active--;
715
+ yield {
716
+ error: next.error,
717
+ origin: allInboxes[next.index].serviceEndpoint,
718
+ };
719
+ } else if (next.result.done) {
720
+ // Store the cursor for future use
721
+ const inbox = allInboxes[next.index];
722
+ cursors[inbox.serviceEndpoint] = next.result.value;
723
+ // Remove it from the race
724
+ indexedIteratorNexts[next.index] = new Promise(() => {});
725
+ active--;
726
+ } else {
727
+ // Re-arm the iterator
728
+ indexedIteratorNexts[next.index] =
729
+ indexedSingleEndpointQueryNext<Schema>(
730
+ iterators[next.index],
731
+ next.index,
732
+ );
733
+ const { object, tombstone, tags: receivedTags } = next.result.value;
734
+ if (tombstone) {
735
+ if (tombstones.get(object.url) === true) continue;
736
+ tombstones.set(object.url, true);
737
+ yield {
738
+ tombstone,
739
+ object: { url: object.url },
740
+ };
741
+ } else {
742
+ // Filter already seen
743
+ if (tombstones.get(object.url) === false) continue;
744
+
745
+ // Fill in the matched channels
746
+ const matchedTagIndices = tags.reduce<number[]>(
747
+ (acc, tag, tagIndex) => {
748
+ for (const receivedTag of receivedTags) {
749
+ if (
750
+ tag.length === receivedTag.length &&
751
+ tag.every((b, i) => receivedTag[i] === b)
752
+ ) {
753
+ acc.push(tagIndex);
754
+ break;
755
+ }
756
+ }
757
+ return acc;
758
+ },
759
+ [],
760
+ );
761
+ const matchedChannels = matchedTagIndices.map(
762
+ (index) => channels[index],
763
+ );
764
+ if (matchedChannels.length === 0) {
765
+ yield {
766
+ error: new Error(
767
+ "Inbox returned object without matching channels",
768
+ ),
769
+ origin: allInboxes[next.index].serviceEndpoint,
770
+ };
771
+ }
772
+ tombstones.set(object.url, false);
773
+ yield {
774
+ object: {
775
+ ...object,
776
+ channels: matchedChannels,
777
+ },
778
+ };
779
+ }
780
+ }
781
+ }
782
+
783
+ return {
784
+ cursor: JSON.stringify({
785
+ channels,
786
+ cursors,
787
+ } satisfies infer_<typeof CursorSchema>),
788
+ continue: (session) =>
789
+ this.discoverMeta<Schema>(channels, schema, cursors, session),
790
+ };
791
+ }
792
+
793
+ discover: Graffiti["discover"] = (...args) => {
794
+ const [channels, schema, session] = args;
795
+ return this.discoverMeta<(typeof args)[1]>(channels, schema, {}, session);
796
+ };
797
+
798
+ continueDiscover: Graffiti["continueDiscover"] = (...args) => {
799
+ const [cursor, session] = args;
800
+ // Extract the channels from the cursor
801
+ let channels: string[];
802
+ let cursors: { [endpoint: string]: string };
803
+ try {
804
+ const json = JSON.parse(cursor);
805
+ const parsed = CursorSchema.parse(json);
806
+ channels = parsed.channels;
807
+ cursors = parsed.cursors;
808
+ } catch (error) {
809
+ return (async function* () {
810
+ throw new GraffitiErrorCursorExpired("Invalid cursor");
811
+ })();
812
+ }
813
+ return this.discoverMeta<{}>(channels, {}, cursors, session);
814
+ };
815
+
816
+ async announceObject(
817
+ object: GraffitiObjectBase,
818
+ tags: Uint8Array[],
819
+ allowedTickets: Uint8Array[] | undefined,
820
+ storageBucketKey: string,
821
+ session: GraffitiSession,
822
+ priorAnnouncements?: MessageMetadataAnnouncements,
823
+ ): Promise<void> {
824
+ const resolvedSession = this.sessions.resolveSession(session);
825
+
826
+ const metadataBase: MessageMetadataBase = {
827
+ [MESSAGE_DATA_STORAGE_BUCKET_KEY]: storageBucketKey,
828
+ };
829
+
830
+ const announcements: MessageMetadataAnnouncements = [];
831
+ const allowed = object.allowed;
832
+ if (Array.isArray(allowed)) {
833
+ if (!allowedTickets || allowedTickets.length !== allowed.length) {
834
+ throw new Error(
835
+ "If allowed actors are specified, there must be a corresponding ticket for each allowed actor",
836
+ );
837
+ }
838
+
839
+ // Send the object to each allowed recipient's personal inbox
840
+ const results = await Promise.allSettled(
841
+ allowed.map(async (recipient, recipientIndex) => {
842
+ // Mask the object to not include any channels
843
+ // and only include the recipient actor on the allowed list
844
+ const copy = JSON.parse(JSON.stringify(object)) as GraffitiObjectBase;
845
+ const masked = maskGraffitiObject(copy, [], recipient);
846
+
847
+ // Get the recipient's inbox
848
+ const actorDocument = await this.dids.resolve(recipient);
849
+ const personalInbox = actorDocument.service?.find(
850
+ (service) =>
851
+ service.type === DID_SERVICE_TYPE_GRAFFITI_INBOX &&
852
+ service.id === DID_SERVICE_ID_GRAFFITI_PERSONAL_INBOX,
853
+ );
854
+ if (!personalInbox) {
855
+ throw new Error(
856
+ `Recipient ${recipient} does not have a personal inbox`,
857
+ );
858
+ }
859
+ if (typeof personalInbox.serviceEndpoint !== "string") {
860
+ throw new Error(
861
+ `Recipient ${recipient} does not have a valid personal inbox endpoint`,
862
+ );
863
+ }
864
+
865
+ const tombstonedMessageId = priorAnnouncements
866
+ ? priorAnnouncements.find(
867
+ (a) => a[MESSAGE_DATA_ANNOUNCEMENT_ACTOR_KEY] === recipient,
868
+ )?.[MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY]
869
+ : undefined;
870
+
871
+ // Announce to the inbox
872
+ const privateMetadata: MessageMetadata = {
873
+ ...metadataBase,
874
+ ...(tombstonedMessageId
875
+ ? {
876
+ [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: tombstonedMessageId,
877
+ }
878
+ : {}),
879
+ [MESSAGE_DATA_ALLOWED_TICKET_KEY]: allowedTickets[recipientIndex],
880
+ [MESSAGE_DATA_ALLOWED_TICKET_INDEX_KEY]: recipientIndex,
881
+ };
882
+ const messageId = await this.inboxes.send(
883
+ personalInbox.serviceEndpoint,
884
+ {
885
+ [MESSAGE_TAGS_KEY]: tags,
886
+ [MESSAGE_OBJECT_KEY]: masked,
887
+ [MESSAGE_METADATA_KEY]: dagCborEncode(privateMetadata),
888
+ },
889
+ );
890
+
891
+ announcements.push({
892
+ [MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY]: messageId,
893
+ [MESSAGE_DATA_ANNOUNCEMENT_ACTOR_KEY]: recipient,
894
+ });
895
+ }),
896
+ );
897
+
898
+ for (const [index, result] of results.entries()) {
899
+ if (result.status === "rejected") {
900
+ const recipient = allowed[index];
901
+ console.error("Error sending to recipient:", recipient);
902
+ console.error(result.reason);
903
+ }
904
+ }
905
+ } else {
906
+ // Mask the object to not include any channels
907
+ // and only include the recipient actor on the allowed list
908
+ const copy = JSON.parse(JSON.stringify(object)) as GraffitiObjectBase;
909
+ const masked = maskGraffitiObject(copy, []);
910
+
911
+ // Send the object to each shared inbox
912
+ const sharedInboxes = resolvedSession.sharedInboxes;
913
+ const results = await Promise.allSettled(
914
+ sharedInboxes.map(async (inbox) => {
915
+ const tombstonedMessageId = priorAnnouncements
916
+ ? priorAnnouncements.find(
917
+ (a) =>
918
+ a[MESSAGE_DATA_ANNOUNCEMENT_ENDPOINT_KEY] ===
919
+ inbox.serviceEndpoint,
920
+ )?.[MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY]
921
+ : undefined;
922
+ const metadata: MessageMetadata = {
923
+ ...metadataBase,
924
+ ...(tombstonedMessageId
925
+ ? {
926
+ [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: tombstonedMessageId,
927
+ }
928
+ : {}),
929
+ };
930
+
931
+ const messageId = await this.inboxes.send(inbox.serviceEndpoint, {
932
+ ...(tombstonedMessageId
933
+ ? {
934
+ [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: tombstonedMessageId,
935
+ }
936
+ : {}),
937
+ [MESSAGE_TAGS_KEY]: tags,
938
+ [MESSAGE_OBJECT_KEY]: masked,
939
+ [MESSAGE_METADATA_KEY]: dagCborEncode(metadata),
940
+ });
941
+ announcements.push({
942
+ [MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY]: messageId,
943
+ [MESSAGE_DATA_ANNOUNCEMENT_ENDPOINT_KEY]: inbox.serviceEndpoint,
944
+ });
945
+ }),
946
+ );
947
+
948
+ for (const [index, result] of results.entries()) {
949
+ if (result.status === "rejected") {
950
+ const inbox = sharedInboxes[index];
951
+ console.error("Error sending to inbox:", inbox);
952
+ console.error(result.reason);
953
+ }
954
+ }
955
+ }
956
+
957
+ // Send the complete object to my own personal inbox
958
+ // along with its key and allowed tickets
959
+ const tombstonedMessageId = priorAnnouncements
960
+ ? priorAnnouncements.find(
961
+ (a) => a[MESSAGE_DATA_ANNOUNCEMENT_ACTOR_KEY] === session.actor,
962
+ )?.[MESSAGE_DATA_ANNOUNCEMENT_MESSAGE_ID_KEY]
963
+ : undefined;
964
+ const selfMetadata: MessageMetadata = {
965
+ ...metadataBase,
966
+ ...(allowedTickets
967
+ ? {
968
+ [MESSAGE_DATA_ALLOWED_TICKETS_KEY]: allowedTickets,
969
+ }
970
+ : {}),
971
+ ...(tombstonedMessageId
972
+ ? {
973
+ [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: tombstonedMessageId,
974
+ }
975
+ : {}),
976
+ [MESSAGE_DATA_ANNOUNCEMENTS_KEY]: announcements,
977
+ };
978
+ await this.inboxes.send(resolvedSession.personalInbox.serviceEndpoint, {
979
+ [MESSAGE_TAGS_KEY]: tags,
980
+ [MESSAGE_OBJECT_KEY]: object,
981
+ [MESSAGE_METADATA_KEY]: dagCborEncode(selfMetadata),
982
+ });
983
+ }
984
+
985
+ protected async *querySingleEndpoint<Schema extends JSONSchema>(
986
+ inboxEndpoint: string,
987
+ queryArguments:
988
+ | {
989
+ tags: Uint8Array[];
990
+ objectSchema: Schema;
991
+ }
992
+ | {
993
+ cursor: string;
994
+ },
995
+ inboxToken?: string | null,
996
+ recipient?: string | null,
997
+ ): SingleEndpointQueryIterator<Schema> {
998
+ const iterator: MessageStream<Schema> =
999
+ "tags" in queryArguments
1000
+ ? this.inboxes.query<Schema>(
1001
+ inboxEndpoint,
1002
+ queryArguments.tags,
1003
+ queryArguments.objectSchema,
1004
+ inboxToken,
1005
+ )
1006
+ : (this.inboxes.continueQuery(
1007
+ inboxEndpoint,
1008
+ queryArguments.cursor,
1009
+ inboxToken,
1010
+ ) as unknown as MessageStream<Schema>);
1011
+
1012
+ while (true) {
1013
+ const itResult = await iterator.next();
1014
+ // Return the cursor if done
1015
+ if (itResult.done) return itResult.value;
1016
+
1017
+ const result = itResult.value;
1018
+
1019
+ const label = result.l;
1020
+ if (label !== MESSAGE_LABEL_VALID && label !== MESSAGE_LABEL_UNLABELED)
1021
+ continue;
1022
+
1023
+ const messageId = result.id;
1024
+ const { o: object, m: metadataBytes, t: receivedTags } = result.m;
1025
+
1026
+ let metadata: MessageMetadata;
1027
+ try {
1028
+ const metadataRaw = dagCborDecode(metadataBytes);
1029
+ metadata = MessageMetadataSchema.parse(metadataRaw);
1030
+ } catch (e) {
1031
+ this.inboxes.label(
1032
+ inboxEndpoint,
1033
+ messageId,
1034
+ MESSAGE_LABEL_INVALID,
1035
+ inboxToken,
1036
+ );
1037
+ continue;
1038
+ }
1039
+
1040
+ const {
1041
+ [MESSAGE_DATA_STORAGE_BUCKET_KEY]: storageBucketKey,
1042
+ [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: tombstonedMessageId,
1043
+ } = metadata;
1044
+
1045
+ const allowedTickets =
1046
+ MESSAGE_DATA_ALLOWED_TICKETS_KEY in metadata
1047
+ ? metadata[MESSAGE_DATA_ALLOWED_TICKETS_KEY]
1048
+ : undefined;
1049
+ const announcements =
1050
+ MESSAGE_DATA_ANNOUNCEMENTS_KEY in metadata
1051
+ ? metadata[MESSAGE_DATA_ANNOUNCEMENTS_KEY]
1052
+ : undefined;
1053
+
1054
+ if (label === MESSAGE_LABEL_VALID) {
1055
+ yield {
1056
+ messageId,
1057
+ object,
1058
+ storageBucketKey,
1059
+ allowedTickets,
1060
+ tags: receivedTags,
1061
+ announcements,
1062
+ };
1063
+ continue;
1064
+ }
1065
+
1066
+ // Try to validate the object
1067
+ let validationError: unknown | undefined = undefined;
1068
+ try {
1069
+ const actor = object.actor;
1070
+ const actorDocument = await this.dids.resolve(actor);
1071
+ const storageBucketService = actorDocument?.service?.find(
1072
+ (service) =>
1073
+ service.id === DID_SERVICE_ID_GRAFFITI_STORAGE_BUCKET &&
1074
+ service.type === DID_SERVICE_TYPE_GRAFFITI_STORAGE_BUCKET,
1075
+ );
1076
+ if (!storageBucketService) {
1077
+ throw new GraffitiErrorNotFound(
1078
+ `Actor ${actor} has no storage bucket service`,
1079
+ );
1080
+ }
1081
+ if (typeof storageBucketService.serviceEndpoint !== "string") {
1082
+ throw new GraffitiErrorNotFound(
1083
+ `Actor ${actor} does not have a valid storage bucket endpoint`,
1084
+ );
1085
+ }
1086
+ const storageBucketEndpoint = storageBucketService.serviceEndpoint;
1087
+
1088
+ const objectBytes = await this.storageBuckets.get(
1089
+ storageBucketEndpoint,
1090
+ storageBucketKey,
1091
+ MAX_OBJECT_SIZE_BYTES,
1092
+ );
1093
+
1094
+ if (MESSAGE_DATA_ALLOWED_TICKET_KEY in metadata && !recipient) {
1095
+ throw new GraffitiErrorForbidden(
1096
+ `Recipient is required when allowed ticket is present`,
1097
+ );
1098
+ }
1099
+ const privateObjectInfo = allowedTickets
1100
+ ? { allowedTickets }
1101
+ : MESSAGE_DATA_ALLOWED_TICKET_KEY in metadata
1102
+ ? {
1103
+ recipient: recipient ?? "null",
1104
+ allowedTicket: metadata[MESSAGE_DATA_ALLOWED_TICKET_KEY],
1105
+ allowedIndex: metadata[MESSAGE_DATA_ALLOWED_TICKET_INDEX_KEY],
1106
+ }
1107
+ : undefined;
1108
+
1109
+ await this.objectEncoding.validate(
1110
+ object,
1111
+ receivedTags,
1112
+ objectBytes,
1113
+ privateObjectInfo,
1114
+ );
1115
+ } catch (e) {
1116
+ validationError = e;
1117
+ }
1118
+
1119
+ if (tombstonedMessageId) {
1120
+ if (validationError instanceof GraffitiErrorNotFound) {
1121
+ // Not found == The tombstone is correct
1122
+ this.inboxes
1123
+ // Get the referenced message
1124
+ .get(inboxEndpoint, tombstonedMessageId, inboxToken)
1125
+ .then((result) => {
1126
+ // Make sure that it actually references the object being deleted
1127
+ if (
1128
+ result &&
1129
+ result[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY].url ===
1130
+ object.url
1131
+ ) {
1132
+ // If it does, label the message as trash, it is no longer needed
1133
+ this.inboxes.label(
1134
+ inboxEndpoint,
1135
+ tombstonedMessageId,
1136
+ MESSAGE_LABEL_TRASH,
1137
+ inboxToken,
1138
+ );
1139
+ }
1140
+
1141
+ // Then, label the tombstone message as trash
1142
+ this.inboxes.label(
1143
+ inboxEndpoint,
1144
+ messageId,
1145
+ MESSAGE_LABEL_TRASH,
1146
+ inboxToken,
1147
+ );
1148
+ });
1149
+
1150
+ yield {
1151
+ messageId,
1152
+ tombstone: true,
1153
+ object,
1154
+ storageBucketKey,
1155
+ allowedTickets,
1156
+ tags: receivedTags,
1157
+ announcements,
1158
+ };
1159
+ } else {
1160
+ console.error("Recieved an incorrect object");
1161
+ console.error(validationError);
1162
+ this.inboxes.label(
1163
+ inboxEndpoint,
1164
+ messageId,
1165
+ MESSAGE_LABEL_INVALID,
1166
+ inboxToken,
1167
+ );
1168
+ }
1169
+ } else {
1170
+ if (validationError === undefined) {
1171
+ this.inboxes.label(
1172
+ inboxEndpoint,
1173
+ messageId,
1174
+ MESSAGE_LABEL_VALID,
1175
+ inboxToken,
1176
+ );
1177
+ yield {
1178
+ messageId,
1179
+ object,
1180
+ storageBucketKey,
1181
+ tags: receivedTags,
1182
+ allowedTickets,
1183
+ announcements,
1184
+ };
1185
+ } else {
1186
+ console.error("Recieved an incorrect object");
1187
+ console.error(validationError);
1188
+ this.inboxes.label(
1189
+ inboxEndpoint,
1190
+ messageId,
1191
+ MESSAGE_LABEL_INVALID,
1192
+ inboxToken,
1193
+ );
1194
+ }
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ const MEDIA_OBJECT_SCHEMA = {
1201
+ properties: {
1202
+ value: {
1203
+ properties: {
1204
+ type: { type: "string" },
1205
+ size: { type: "number" },
1206
+ key: { type: "string" },
1207
+ },
1208
+ required: ["type", "size", "key"],
1209
+ },
1210
+ },
1211
+ } as const satisfies JSONSchema;
1212
+
1213
+ const CursorSchema = strictObject({
1214
+ cursors: record(url(), string()),
1215
+ channels: array(string()),
1216
+ });
1217
+
1218
+ interface SingleEndpointQueryResult<Schema extends JSONSchema> {
1219
+ messageId: string;
1220
+ object: GraffitiObject<Schema>;
1221
+ storageBucketKey: string;
1222
+ tags: Uint8Array[];
1223
+ allowedTickets: Uint8Array[] | undefined;
1224
+ tombstone?: boolean;
1225
+ announcements?: MessageMetadataAnnouncements | undefined;
1226
+ }
1227
+ interface SingleEndpointQueryIterator<
1228
+ Schema extends JSONSchema,
1229
+ > extends AsyncGenerator<SingleEndpointQueryResult<Schema>, string> {}
1230
+ type IndexedSingleEndpointQueryResult<Schema extends JSONSchema> =
1231
+ | {
1232
+ index: number;
1233
+ error?: undefined;
1234
+ result: IteratorResult<SingleEndpointQueryResult<Schema>, string>;
1235
+ }
1236
+ | {
1237
+ index: number;
1238
+ error: Error;
1239
+ result?: undefined;
1240
+ };
1241
+
1242
+ async function indexedSingleEndpointQueryNext<Schema extends JSONSchema>(
1243
+ it: SingleEndpointQueryIterator<Schema>,
1244
+ index: number,
1245
+ ): Promise<IndexedSingleEndpointQueryResult<Schema>> {
1246
+ try {
1247
+ return {
1248
+ index: index,
1249
+ result: await it.next(),
1250
+ };
1251
+ } catch (e) {
1252
+ if (
1253
+ e instanceof GraffitiErrorCursorExpired ||
1254
+ e instanceof GraffitiErrorInvalidSchema
1255
+ ) {
1256
+ // Propogate these errors to the root
1257
+ throw e;
1258
+ }
1259
+ // Otherwise, silently pass them in the stream
1260
+ return {
1261
+ index,
1262
+ error: e instanceof Error ? e : new Error(String(e)),
1263
+ };
1264
+ }
1265
+ }