@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,269 @@
1
+ import { assert, describe, expect, test } from "vitest";
2
+
3
+ import {
4
+ encodeObjectUrl,
5
+ decodeObjectUrl,
6
+ ObjectEncoding,
7
+ } from "./3-object-encoding.js";
8
+ import { randomBytes } from "@noble/hashes/utils.js";
9
+ import {
10
+ STRING_ENCODER_METHOD_BASE64URL,
11
+ StringEncoder,
12
+ } from "../2-primitives/1-string-encoding.js";
13
+ import { ContentAddresses } from "../2-primitives/2-content-addresses.js";
14
+ import { ChannelAttestations } from "../2-primitives/3-channel-attestations.js";
15
+ import {
16
+ ALLOWED_ATTESTATION_METHOD_HMAC_SHA256,
17
+ AllowedAttestations,
18
+ } from "../2-primitives/4-allowed-attestations.js";
19
+ import {
20
+ encode as dagCborEncode,
21
+ decode as dagCborDecode,
22
+ } from "@ipld/dag-cbor";
23
+ import {
24
+ maskGraffitiObject,
25
+ type GraffitiObjectBase,
26
+ } from "@graffiti-garden/api";
27
+
28
+ export function objectEncodingTests() {
29
+ describe("object Urls", () => {
30
+ for (const actor of [
31
+ "did:plc:alsdkjfkdjf",
32
+ "did:web:example.com/someone",
33
+ "did:example:123456789abcdefghi👻",
34
+ ]) {
35
+ test(`encodeObjectUrl encodes and decodes correctly with actor: ${actor}`, async () => {
36
+ const contentAddressBytes = randomBytes();
37
+ const contentAddress = await new StringEncoder().encode(
38
+ STRING_ENCODER_METHOD_BASE64URL,
39
+ contentAddressBytes,
40
+ );
41
+
42
+ const url = encodeObjectUrl(actor, contentAddress);
43
+ const decoded = decodeObjectUrl(url);
44
+ expect(decoded.actor).toBe(actor);
45
+ expect(decoded.contentAddress).toBe(contentAddress);
46
+ });
47
+ }
48
+
49
+ for (const invalidUrl of [
50
+ "http://example.com/not-an-object-url",
51
+ "graffiti:",
52
+ "graffiti:",
53
+ "graffiti:no-content-address",
54
+ "graffiti:too:many:parts",
55
+ ]) {
56
+ test(`Invalid Graffiti URL: ${invalidUrl}`, () => {
57
+ expect(() => decodeObjectUrl(invalidUrl)).toThrow();
58
+ });
59
+ }
60
+ });
61
+
62
+ describe("object encoding and validation", async () => {
63
+ const objectEncoding = new ObjectEncoding({
64
+ stringEncoder: new StringEncoder(),
65
+ allowedAttestations: new AllowedAttestations(),
66
+ channelAttestations: new ChannelAttestations(),
67
+ contentAddresses: new ContentAddresses(),
68
+ });
69
+
70
+ const value = {
71
+ message: "Hello world!",
72
+ nested: {
73
+ foo: {
74
+ bar: 42,
75
+ },
76
+ },
77
+ array: [1, "something 👻", { key: "value" }],
78
+ };
79
+ const channels = ["channel1👻", "channel2"];
80
+ const allowed = ["did:web:noone.example.com", "did:web:someone.else.com"];
81
+ const actor = "did:web:someone.example.com";
82
+
83
+ const { allowedTickets, tags, objectBytes, object } =
84
+ await objectEncoding.encode<{}>(
85
+ {
86
+ value,
87
+ channels,
88
+ allowed,
89
+ },
90
+ actor,
91
+ );
92
+ assert(Array.isArray(allowedTickets));
93
+
94
+ test("validate private", async () => {
95
+ await objectEncoding.validate(object, tags, objectBytes, {
96
+ allowedTickets,
97
+ });
98
+
99
+ for (const [index, recipient] of allowed.entries()) {
100
+ const copy = JSON.parse(JSON.stringify(object)) as GraffitiObjectBase;
101
+ const masked = maskGraffitiObject(copy, [], recipient);
102
+ await objectEncoding.validate(masked, tags, objectBytes, {
103
+ recipient: recipient,
104
+ allowedTicket: allowedTickets[index],
105
+ allowedIndex: index,
106
+ });
107
+ }
108
+ });
109
+
110
+ test("incorrect value", async () => {
111
+ await expect(
112
+ objectEncoding.validate(
113
+ {
114
+ ...object,
115
+ value: {
116
+ ...object.value,
117
+ extra: "field",
118
+ },
119
+ },
120
+ tags,
121
+ objectBytes,
122
+ {
123
+ allowedTickets,
124
+ },
125
+ ),
126
+ ).rejects.toThrow();
127
+ });
128
+
129
+ test("incorrect content address", async () => {
130
+ const url = encodeObjectUrl(
131
+ actor,
132
+ await new StringEncoder().encode(
133
+ STRING_ENCODER_METHOD_BASE64URL,
134
+ randomBytes(),
135
+ ),
136
+ );
137
+
138
+ await expect(
139
+ objectEncoding.validate(
140
+ {
141
+ ...object,
142
+ url,
143
+ },
144
+ [new TextEncoder().encode(url), ...tags.slice(1)],
145
+ objectBytes,
146
+ {
147
+ allowedTickets,
148
+ },
149
+ ),
150
+ ).rejects.toThrow();
151
+ });
152
+
153
+ test("incorrect bytes", async () => {
154
+ const wrongObjectBytes = randomBytes();
155
+ const correctContentAddress = decodeObjectUrl(object.url).contentAddress;
156
+ const correctContentAddressBytes = await new StringEncoder().decode(
157
+ correctContentAddress,
158
+ );
159
+ const contentAddresses = new ContentAddresses();
160
+ const contentAddressMethod = await contentAddresses.getMethod(
161
+ correctContentAddressBytes,
162
+ );
163
+ const wrongContentAddressBytes = await contentAddresses.register(
164
+ contentAddressMethod,
165
+ wrongObjectBytes,
166
+ );
167
+ const wrongContentAddress = await new StringEncoder().encode(
168
+ STRING_ENCODER_METHOD_BASE64URL,
169
+ wrongContentAddressBytes,
170
+ );
171
+ const wrongObjectUrl = encodeObjectUrl(actor, wrongContentAddress);
172
+
173
+ await expect(
174
+ objectEncoding.validate(
175
+ {
176
+ ...object,
177
+ url: wrongObjectUrl,
178
+ },
179
+ [new TextEncoder().encode(wrongObjectUrl), ...tags.slice(1)],
180
+ wrongObjectBytes,
181
+ {
182
+ allowedTickets,
183
+ },
184
+ ),
185
+ ).rejects.toThrow();
186
+ });
187
+
188
+ test("incorrect format", async () => {
189
+ const wrongData = {
190
+ not: "the expected format",
191
+ };
192
+ const wrongObjectBytes = dagCborEncode(wrongData);
193
+ const correctContentAddress = decodeObjectUrl(object.url).contentAddress;
194
+ const correctContentAddressBytes = await new StringEncoder().decode(
195
+ correctContentAddress,
196
+ );
197
+ const contentAddresses = new ContentAddresses();
198
+ const contentAddressMethod = await contentAddresses.getMethod(
199
+ correctContentAddressBytes,
200
+ );
201
+ const wrongContentAddressBytes = await contentAddresses.register(
202
+ contentAddressMethod,
203
+ wrongObjectBytes,
204
+ );
205
+ const wrongContentAddress = await new StringEncoder().encode(
206
+ STRING_ENCODER_METHOD_BASE64URL,
207
+ wrongContentAddressBytes,
208
+ );
209
+ const wrongObjectUrl = encodeObjectUrl(actor, wrongContentAddress);
210
+ await expect(
211
+ objectEncoding.validate(
212
+ {
213
+ ...object,
214
+ url: wrongObjectUrl,
215
+ },
216
+ [new TextEncoder().encode(wrongObjectUrl), ...tags.slice(1)],
217
+ wrongObjectBytes,
218
+ {
219
+ allowedTickets,
220
+ },
221
+ ),
222
+ ).rejects.toThrow();
223
+ });
224
+
225
+ test("missing allowed tickets", async () => {
226
+ await expect(
227
+ objectEncoding.validate(object, tags, objectBytes),
228
+ ).rejects.toThrow();
229
+ });
230
+
231
+ test("wrong allowed tickets", async () => {
232
+ const wrongAllowedTickets = await Promise.all(
233
+ allowedTickets.map(
234
+ async (ticket) =>
235
+ (
236
+ await new AllowedAttestations().attest(
237
+ ALLOWED_ATTESTATION_METHOD_HMAC_SHA256,
238
+ "did:web:not-the-right.actor",
239
+ )
240
+ ).ticket,
241
+ ),
242
+ );
243
+
244
+ await expect(
245
+ objectEncoding.validate(object, tags, objectBytes, {
246
+ allowedTickets: wrongAllowedTickets,
247
+ }),
248
+ ).rejects.toThrow();
249
+ });
250
+
251
+ test("wrong recipients", async () => {
252
+ const wrongRecipients = allowed.map((recipient) => recipient + "-wrong");
253
+
254
+ await expect(
255
+ objectEncoding.validate(
256
+ {
257
+ ...object,
258
+ allowed: wrongRecipients,
259
+ },
260
+ tags,
261
+ objectBytes,
262
+ {
263
+ allowedTickets,
264
+ },
265
+ ),
266
+ ).rejects.toThrow();
267
+ });
268
+ });
269
+ }
@@ -0,0 +1,396 @@
1
+ import type { JSONSchema } from "json-schema-to-ts";
2
+ import type {
3
+ GraffitiObject,
4
+ GraffitiObjectBase,
5
+ GraffitiPostObject,
6
+ } from "@graffiti-garden/api";
7
+ import type { ChannelAttestations } from "../2-primitives/3-channel-attestations";
8
+ import type { AllowedAttestations } from "../2-primitives/4-allowed-attestations";
9
+ import {
10
+ CONTENT_ADDRESS_METHOD_SHA256,
11
+ type ContentAddresses,
12
+ } from "../2-primitives/2-content-addresses";
13
+ import { randomBytes } from "@noble/hashes/utils.js";
14
+ import {
15
+ encode as dagCborEncode,
16
+ decode as dagCborDecode,
17
+ } from "@ipld/dag-cbor";
18
+ import {
19
+ type infer as infer_,
20
+ array,
21
+ custom,
22
+ looseObject,
23
+ optional,
24
+ strictObject,
25
+ } from "zod/mini";
26
+ import { CHANNEL_ATTESTATION_METHOD_SHA256_ED25519 } from "../2-primitives/3-channel-attestations";
27
+ import { ALLOWED_ATTESTATION_METHOD_HMAC_SHA256 } from "../2-primitives/4-allowed-attestations";
28
+ import {
29
+ STRING_ENCODER_METHOD_BASE64URL,
30
+ type StringEncoder,
31
+ } from "../2-primitives/1-string-encoding";
32
+
33
+ // Objects have a max size of 32kb
34
+ // If each channel and allowed actor takes 32 bytes
35
+ // of space (i.e. they are hashed with 256 bit security)
36
+ // then this means that the combined number of channels
37
+ // and recipients of object has cannot exceed one thousand.
38
+ // This seems like a reasonable limit and on par with
39
+ // signal's group chat limit of 1000
40
+ export const MAX_OBJECT_SIZE_BYTES = 32 * 1024;
41
+
42
+ export class ObjectEncoding {
43
+ constructor(
44
+ protected readonly primitives: {
45
+ readonly stringEncoder: StringEncoder;
46
+ readonly channelAttestations: ChannelAttestations;
47
+ readonly allowedAttestations: AllowedAttestations;
48
+ readonly contentAddresses: ContentAddresses;
49
+ },
50
+ ) {}
51
+
52
+ async encode<Schema extends JSONSchema>(
53
+ partialObject: GraffitiPostObject<Schema>,
54
+ actor: string,
55
+ ): Promise<{
56
+ object: GraffitiObject<Schema>;
57
+ tags: Uint8Array[];
58
+ objectBytes: Uint8Array;
59
+ allowedTickets: Uint8Array[] | undefined;
60
+ }> {
61
+ // Clean out any undefineds
62
+ partialObject = cleanUndefined(partialObject);
63
+
64
+ // Create a verifiable attestation that the actor
65
+ // knows the included channels without
66
+ // directly revealing any channel to anyone who doesn't
67
+ // know the channel already
68
+ const channelAttestationAndPublicIds = await Promise.all(
69
+ partialObject.channels.map((channel) =>
70
+ this.primitives.channelAttestations.attest(
71
+ // TODO: get this from the DID document of the actor
72
+ CHANNEL_ATTESTATION_METHOD_SHA256_ED25519,
73
+ actor,
74
+ channel,
75
+ ),
76
+ ),
77
+ );
78
+ const channelAttestations = channelAttestationAndPublicIds.map(
79
+ (c) => c.attestation,
80
+ );
81
+ const channelPublicIds = channelAttestationAndPublicIds.map(
82
+ (c) => c.channelPublicId,
83
+ );
84
+
85
+ const objectData: infer_<typeof ObjectDataSchema> = {
86
+ [VALUE_PROPERTY]: partialObject.value,
87
+ [CHANNEL_ATTESTATIONS_PROPERTY]: channelAttestations,
88
+ [NONCE_PROPERTY]: randomBytes(32),
89
+ };
90
+
91
+ let allowedTickets: Uint8Array[] | undefined = undefined;
92
+
93
+ // If the object is private...
94
+ if (Array.isArray(partialObject.allowed)) {
95
+ // Create an attestation that the object's allowed list
96
+ // includes the given actors, without revealing the
97
+ // presence of an actor on the list to anyone except
98
+ // that actor themselves. Each actor will receive a
99
+ // "ticket" that they can use to verify their own membership
100
+ // on the allowed list.
101
+ const allowedAttestations = await Promise.all(
102
+ partialObject.allowed.map(async (allowedActor) =>
103
+ this.primitives.allowedAttestations.attest(
104
+ // TODO: get this from the DID document of the actor
105
+ ALLOWED_ATTESTATION_METHOD_HMAC_SHA256,
106
+ allowedActor,
107
+ ),
108
+ ),
109
+ );
110
+ objectData[ALLOWED_ATTESTATIONS_PROPERTY] = allowedAttestations.map(
111
+ (a) => a.attestation,
112
+ );
113
+ allowedTickets = allowedAttestations.map((a) => a.ticket);
114
+ }
115
+
116
+ // Encode the mixed JSON/binary data using CBOR
117
+ const objectBytes = dagCborEncode(objectData);
118
+ if (objectBytes.byteLength > MAX_OBJECT_SIZE_BYTES) {
119
+ throw new Error("The object is too large");
120
+ }
121
+
122
+ // Compute a public identifier (hash) of the object data
123
+ const objectContentAddressBytes =
124
+ await this.primitives.contentAddresses.register(
125
+ // TODO: get this from the DID document of the actor
126
+ CONTENT_ADDRESS_METHOD_SHA256,
127
+ objectBytes,
128
+ );
129
+ const objectContentAddress = await this.primitives.stringEncoder.encode(
130
+ STRING_ENCODER_METHOD_BASE64URL,
131
+ objectContentAddressBytes,
132
+ );
133
+ // Use it to compute the object's URL
134
+ const objectUrl = encodeObjectUrl(actor, objectContentAddress);
135
+
136
+ const tags = [new TextEncoder().encode(objectUrl), ...channelPublicIds];
137
+
138
+ const object: GraffitiObject<Schema> = {
139
+ value: partialObject.value,
140
+ channels: partialObject.channels,
141
+ url: objectUrl,
142
+ actor,
143
+ ...(partialObject.allowed
144
+ ? {
145
+ allowed: partialObject.allowed,
146
+ }
147
+ : {}),
148
+ } as GraffitiObject<Schema>;
149
+
150
+ // Return object URL and allowed secrets
151
+ return {
152
+ object,
153
+ tags,
154
+ objectBytes,
155
+ allowedTickets,
156
+ };
157
+ }
158
+
159
+ async validate(
160
+ object: GraffitiObjectBase,
161
+ tags: Uint8Array[],
162
+ objectBytes: Uint8Array,
163
+ privateObjectInfo?:
164
+ | {
165
+ recipient: string;
166
+ allowedTicket: Uint8Array;
167
+ allowedIndex: number;
168
+ }
169
+ | {
170
+ allowedTickets: Uint8Array[];
171
+ },
172
+ ): Promise<void> {
173
+ if (objectBytes.byteLength > MAX_OBJECT_SIZE_BYTES) {
174
+ throw new Error("Object is too big");
175
+ }
176
+ const { actor, contentAddress } = decodeObjectUrl(object.url);
177
+ if (actor !== object.actor) {
178
+ throw new Error("Object actor does not match URL actor");
179
+ }
180
+
181
+ const objectUrlTag = tags.at(0);
182
+ if (!objectUrlTag) {
183
+ throw new Error("No object URL tag");
184
+ }
185
+ if (new TextDecoder().decode(objectUrlTag) !== object.url) {
186
+ throw new Error("Object URL tag does not match object URL");
187
+ }
188
+ const channelPublicIds = tags.slice(1);
189
+
190
+ // Make sure the object content address matches the object content
191
+ const contentAddressBytes =
192
+ await this.primitives.stringEncoder.decode(contentAddress);
193
+ const contentAddressMethod =
194
+ await this.primitives.contentAddresses.getMethod(contentAddressBytes);
195
+ const expectedContentAddress =
196
+ await this.primitives.contentAddresses.register(
197
+ contentAddressMethod,
198
+ objectBytes,
199
+ );
200
+ if (
201
+ expectedContentAddress.length !== contentAddressBytes.length ||
202
+ !expectedContentAddress.every((b, i) => b === contentAddressBytes[i])
203
+ ) {
204
+ throw new Error("Content address is invalid");
205
+ }
206
+
207
+ // Convert the raw object data from CBOR
208
+ // back to a javascript object
209
+ const objectDataUnknown = dagCborDecode(objectBytes);
210
+ const objectData = ObjectDataSchema.parse(objectDataUnknown);
211
+
212
+ // And extract the values
213
+ const value = objectData[VALUE_PROPERTY];
214
+ const channelAttestations = objectData[CHANNEL_ATTESTATIONS_PROPERTY];
215
+ const allowedAttestations = objectData[ALLOWED_ATTESTATIONS_PROPERTY];
216
+
217
+ // Validate that the object's value matches
218
+ const valueBytes = dagCborEncode(value);
219
+ const expectedValueBytes = dagCborEncode(object.value);
220
+ if (
221
+ valueBytes.length !== expectedValueBytes.length ||
222
+ !valueBytes.every((b, i) => b === expectedValueBytes[i])
223
+ ) {
224
+ throw new Error("Object value does not match storage value");
225
+ }
226
+
227
+ // Validate the object's channels
228
+ if (channelAttestations.length !== channelPublicIds.length) {
229
+ throw new Error("Not as many channel attestations and public ids");
230
+ }
231
+ for (const [index, attestation] of channelAttestations.entries()) {
232
+ const channelPublicId = channelPublicIds[index];
233
+ const isValid = await this.primitives.channelAttestations.validate(
234
+ attestation,
235
+ actor,
236
+ channelPublicId,
237
+ );
238
+ if (!isValid) {
239
+ throw new Error("Invalid channel attestation");
240
+ }
241
+ }
242
+ if (object.channels.length) {
243
+ // If any channels are included, they all must be included
244
+ if (object.channels.length !== channelPublicIds.length) {
245
+ throw new Error(
246
+ "Number of claimed channels does not match attestations/public IDs",
247
+ );
248
+ }
249
+ const channelAttestationMethod =
250
+ await this.primitives.channelAttestations.getMethod(
251
+ channelPublicIds[0],
252
+ );
253
+ const expectedChannelPublicIds = await Promise.all(
254
+ object.channels.map((channel) =>
255
+ this.primitives.channelAttestations.register(
256
+ channelAttestationMethod,
257
+ channel,
258
+ ),
259
+ ),
260
+ );
261
+ for (const [
262
+ index,
263
+ expectedPublicId,
264
+ ] of expectedChannelPublicIds.entries()) {
265
+ const actualPublicId = channelPublicIds[index];
266
+ if (
267
+ expectedPublicId.length !== actualPublicId.length ||
268
+ !expectedPublicId.every((b, i) => b === actualPublicId[i])
269
+ ) {
270
+ throw new Error("Channel public id does not match expected");
271
+ }
272
+ }
273
+ }
274
+
275
+ // Validate the recipient
276
+ if (privateObjectInfo) {
277
+ if (!allowedAttestations) {
278
+ throw new Error("Object is public but thought to be private");
279
+ }
280
+
281
+ let recipients: string[];
282
+ let allowedTickets: Uint8Array[];
283
+ let attestations: Uint8Array[];
284
+ if ("recipient" in privateObjectInfo) {
285
+ recipients = [privateObjectInfo.recipient];
286
+ allowedTickets = [privateObjectInfo.allowedTicket];
287
+ attestations = allowedAttestations.filter(
288
+ (_, i) => i === privateObjectInfo.allowedIndex,
289
+ );
290
+ } else {
291
+ recipients = [...(object.allowed ?? [])];
292
+ allowedTickets = privateObjectInfo.allowedTickets;
293
+ attestations = allowedAttestations;
294
+ }
295
+
296
+ // All recipients must be in the allowed list
297
+ if (recipients.length !== object.allowed?.length) {
298
+ throw new Error("Recipient count does not match object allowed list");
299
+ }
300
+ if (!recipients.every((r) => object.allowed?.includes(r))) {
301
+ throw new Error("Recipient not in object allowed list");
302
+ }
303
+
304
+ for (const [index, recipient] of recipients.entries()) {
305
+ const allowedTicket = allowedTickets.at(index);
306
+ const allowedAttestation = attestations.at(index);
307
+ if (!allowedTicket) {
308
+ throw new Error("Missing allowed ticket for recipient");
309
+ }
310
+ if (!allowedAttestation) {
311
+ throw new Error("Missing allowed attestation for recipient");
312
+ }
313
+ const isValid = await this.primitives.allowedAttestations.validate(
314
+ allowedAttestation,
315
+ recipient,
316
+ allowedTicket,
317
+ );
318
+
319
+ if (!isValid) {
320
+ throw new Error("Invalid allowed attestation for recipient");
321
+ }
322
+ }
323
+ } else if (allowedAttestations) {
324
+ throw new Error("Object is private but no recipient info provided");
325
+ }
326
+ }
327
+ }
328
+
329
+ // A compact data representation of the object data
330
+ const VALUE_PROPERTY = "v";
331
+ const CHANNEL_ATTESTATIONS_PROPERTY = "c";
332
+ const ALLOWED_ATTESTATIONS_PROPERTY = "a";
333
+ const NONCE_PROPERTY = "n";
334
+
335
+ const Uint8ArraySchema = custom<Uint8Array>(
336
+ (v): v is Uint8Array => v instanceof Uint8Array,
337
+ );
338
+
339
+ const ObjectDataSchema = strictObject({
340
+ [VALUE_PROPERTY]: looseObject({}),
341
+ [CHANNEL_ATTESTATIONS_PROPERTY]: array(Uint8ArraySchema),
342
+ [ALLOWED_ATTESTATIONS_PROPERTY]: optional(array(Uint8ArraySchema)),
343
+ [NONCE_PROPERTY]: Uint8ArraySchema,
344
+ });
345
+
346
+ export const GRAFFITI_OBJECT_URL_PREFIX = "graffiti:";
347
+
348
+ // Methods to encode and decode object URLs
349
+ export function encodeObjectUrlComponent(value: string) {
350
+ const replaced = value.replace(/:/g, "!").replace(/\//g, "~");
351
+ return encodeURIComponent(replaced);
352
+ }
353
+ export function decodeObjectUrlComponent(value: string) {
354
+ const decoded = decodeURIComponent(value);
355
+ return decoded.replace(/!/g, ":").replace(/~/g, "/");
356
+ }
357
+ export function encodeObjectUrl(actor: string, contentAddress: string) {
358
+ return `${GRAFFITI_OBJECT_URL_PREFIX}${encodeObjectUrlComponent(actor)}:${encodeObjectUrlComponent(contentAddress)}`;
359
+ }
360
+ export function decodeObjectUrl(objectUrl: string) {
361
+ if (!objectUrl.startsWith(GRAFFITI_OBJECT_URL_PREFIX)) {
362
+ throw new Error("Invalid object URL");
363
+ }
364
+
365
+ const rest = objectUrl.slice(GRAFFITI_OBJECT_URL_PREFIX.length);
366
+ const parts = rest.split(":");
367
+
368
+ if (parts.length !== 2) {
369
+ throw new Error("Invalid object URL format");
370
+ }
371
+
372
+ const [actor, contentAddress] = parts;
373
+
374
+ return {
375
+ actor: decodeObjectUrlComponent(actor),
376
+ contentAddress: decodeObjectUrlComponent(contentAddress),
377
+ };
378
+ }
379
+
380
+ function cleanUndefined(value: any): any {
381
+ if (value === undefined) return null;
382
+
383
+ if (Array.isArray(value)) {
384
+ return value.map(cleanUndefined);
385
+ }
386
+
387
+ if (typeof value === "object") {
388
+ return Object.fromEntries(
389
+ Object.entries(value)
390
+ .filter(([, v]) => v !== undefined)
391
+ .map(([k, v]) => [k, cleanUndefined(v)]),
392
+ );
393
+ }
394
+
395
+ return value;
396
+ }