@graffiti-garden/implementation-local 1.0.2 → 1.0.4
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.
- package/dist/browser/index.js +1 -12
- package/dist/browser/index.js.map +4 -4
- package/dist/cjs/media.js +7 -19
- package/dist/cjs/media.js.map +3 -3
- package/dist/cjs/objects.js +29 -13
- package/dist/cjs/objects.js.map +2 -2
- package/dist/cjs/utilities.js +13 -2
- package/dist/cjs/utilities.js.map +2 -2
- package/dist/esm/media.js +9 -10
- package/dist/esm/media.js.map +2 -2
- package/dist/esm/objects.js +34 -14
- package/dist/esm/objects.js.map +2 -2
- package/dist/esm/utilities.js +16 -2
- package/dist/esm/utilities.js.map +2 -2
- package/dist/media.d.ts.map +1 -1
- package/dist/objects.d.ts.map +1 -1
- package/dist/utilities.d.ts +3 -0
- package/dist/utilities.d.ts.map +1 -1
- package/package.json +8 -9
- package/src/media.ts +8 -9
- package/src/objects.ts +34 -13
- package/src/utilities.ts +30 -2
package/dist/esm/utilities.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GraffitiErrorInvalidSchema,
|
|
3
|
+
GraffitiErrorNotFound
|
|
4
|
+
} from "@graffiti-garden/api";
|
|
1
5
|
function encodeBase64(bytes) {
|
|
2
6
|
const base64 = btoa(String.fromCodePoint(...bytes));
|
|
3
7
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=+$/, "");
|
|
@@ -25,11 +29,11 @@ function encodeMediaUrl(actor, id) {
|
|
|
25
29
|
}
|
|
26
30
|
function decodeGraffitiUrl(url, prefix) {
|
|
27
31
|
if (!url.startsWith(prefix)) {
|
|
28
|
-
throw new
|
|
32
|
+
throw new GraffitiErrorNotFound(`URL does not start with ${prefix}`);
|
|
29
33
|
}
|
|
30
34
|
const slices = url.slice(prefix.length).split(":");
|
|
31
35
|
if (slices.length !== 2) {
|
|
32
|
-
throw new
|
|
36
|
+
throw new GraffitiErrorNotFound("URL has too many colon-seperated parts");
|
|
33
37
|
}
|
|
34
38
|
const [actor, id] = slices.map(decodeURIComponent);
|
|
35
39
|
return { actor, id };
|
|
@@ -65,9 +69,19 @@ async function base64ToBlob(dataUrl) {
|
|
|
65
69
|
const response = await fetch(dataUrl);
|
|
66
70
|
return await response.blob();
|
|
67
71
|
}
|
|
72
|
+
function compileGraffitiObjectSchema(ajv, schema) {
|
|
73
|
+
try {
|
|
74
|
+
return ajv.compile(schema);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new GraffitiErrorInvalidSchema(
|
|
77
|
+
error instanceof Error ? error.message : void 0
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
68
81
|
export {
|
|
69
82
|
base64ToBlob,
|
|
70
83
|
blobToBase64,
|
|
84
|
+
compileGraffitiObjectSchema,
|
|
71
85
|
decodeBase64,
|
|
72
86
|
decodeGraffitiUrl,
|
|
73
87
|
decodeMediaUrl,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utilities.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import {\n GraffitiErrorInvalidSchema,\n GraffitiErrorNotFound,\n type GraffitiObject,\n type GraffitiObjectBase,\n type JSONSchema,\n} from \"@graffiti-garden/api\";\nimport type Ajv from \"ajv\";\n\nexport function encodeBase64(bytes: Uint8Array): string {\n // Convert it to base64\n const base64 = btoa(String.fromCodePoint(...bytes));\n // Make sure it is url safe\n return base64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/\\=+$/, \"\");\n}\n\nexport function decodeBase64(base64Url: string): Uint8Array {\n // Undo url-safe base64\n let base64 = base64Url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n // Add padding if necessary\n while (base64.length % 4 !== 0) base64 += \"=\";\n // Decode\n return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));\n}\n\nexport function randomBase64(numBytes: number = 32): string {\n // Generate random bytes\n const bytes = new Uint8Array(numBytes);\n crypto.getRandomValues(bytes);\n return encodeBase64(bytes);\n}\n\nconst OBJECT_URL_PREFIX = \"graffiti:object:\";\nconst MEDIA_URL_PREFIX = \"graffiti:media:\";\n\nexport function encodeGraffitiUrl(actor: string, id: string, prefix: string) {\n return `${prefix}${encodeURIComponent(actor)}:${encodeURIComponent(id)}`;\n}\nexport function encodeObjectUrl(actor: string, id: string) {\n return encodeGraffitiUrl(actor, id, OBJECT_URL_PREFIX);\n}\nexport function encodeMediaUrl(actor: string, id: string) {\n return encodeGraffitiUrl(actor, id, MEDIA_URL_PREFIX);\n}\n\nexport function decodeGraffitiUrl(url: string, prefix: string) {\n if (!url.startsWith(prefix)) {\n throw new GraffitiErrorNotFound(`URL does not start with ${prefix}`);\n }\n const slices = url.slice(prefix.length).split(\":\");\n if (slices.length !== 2) {\n throw new GraffitiErrorNotFound(\"URL has too many colon-seperated parts\");\n }\n const [actor, id] = slices.map(decodeURIComponent);\n return { actor, id };\n}\nexport function decodeObjectUrl(url: string) {\n return decodeGraffitiUrl(url, OBJECT_URL_PREFIX);\n}\nexport function decodeMediaUrl(url: string) {\n return decodeGraffitiUrl(url, MEDIA_URL_PREFIX);\n}\n\nexport async function blobToBase64(blob: Blob): Promise<string> {\n if (typeof FileReader !== \"undefined\") {\n return new Promise((resolve, reject) => {\n const r = new FileReader();\n r.onload = () => {\n if (typeof r.result === \"string\") {\n resolve(r.result);\n } else {\n reject(new Error(\"Unexpected result type\"));\n }\n };\n r.onerror = reject;\n r.readAsDataURL(blob);\n });\n }\n\n if (typeof Buffer !== \"undefined\") {\n const ab = await blob.arrayBuffer();\n return `data:${blob.type};base64,${Buffer.from(ab).toString(\"base64\")}`;\n }\n\n throw new Error(\"Unsupported environment\");\n}\n\nexport async function base64ToBlob(dataUrl: string) {\n const response = await fetch(dataUrl);\n return await response.blob();\n}\n\nexport function compileGraffitiObjectSchema<Schema extends JSONSchema>(\n ajv: Ajv,\n schema: Schema,\n) {\n try {\n // Force the validation guard because\n // it is too big for the type checker.\n // Fortunately json-schema-to-ts is\n // well tested against ajv.\n return ajv.compile(schema) as (\n data: GraffitiObjectBase,\n ) => data is GraffitiObject<Schema>;\n } catch (error) {\n throw new GraffitiErrorInvalidSchema(\n error instanceof Error ? error.message : undefined,\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAGA,SAAS,aAAa,OAA2B;AAEtD,QAAM,SAAS,KAAK,OAAO,cAAc,GAAG,KAAK,CAAC;AAElD,SAAO,OAAO,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,QAAQ,EAAE;AAC1E;AAEO,SAAS,aAAa,WAA+B;AAE1D,MAAI,SAAS,UAAU,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AAE3D,SAAO,OAAO,SAAS,MAAM,EAAG,WAAU;AAE1C,SAAO,WAAW,KAAK,KAAK,MAAM,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAC7D;AAEO,SAAS,aAAa,WAAmB,IAAY;AAE1D,QAAM,QAAQ,IAAI,WAAW,QAAQ;AACrC,SAAO,gBAAgB,KAAK;AAC5B,SAAO,aAAa,KAAK;AAC3B;AAEA,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AAElB,SAAS,kBAAkB,OAAe,IAAY,QAAgB;AAC3E,SAAO,GAAG,MAAM,GAAG,mBAAmB,KAAK,CAAC,IAAI,mBAAmB,EAAE,CAAC;AACxE;AACO,SAAS,gBAAgB,OAAe,IAAY;AACzD,SAAO,kBAAkB,OAAO,IAAI,iBAAiB;AACvD;AACO,SAAS,eAAe,OAAe,IAAY;AACxD,SAAO,kBAAkB,OAAO,IAAI,gBAAgB;AACtD;AAEO,SAAS,kBAAkB,KAAa,QAAgB;AAC7D,MAAI,CAAC,IAAI,WAAW,MAAM,GAAG;AAC3B,UAAM,IAAI,sBAAsB,2BAA2B,MAAM,EAAE;AAAA,EACrE;AACA,QAAM,SAAS,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,GAAG;AACjD,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,sBAAsB,wCAAwC;AAAA,EAC1E;AACA,QAAM,CAAC,OAAO,EAAE,IAAI,OAAO,IAAI,kBAAkB;AACjD,SAAO,EAAE,OAAO,GAAG;AACrB;AACO,SAAS,gBAAgB,KAAa;AAC3C,SAAO,kBAAkB,KAAK,iBAAiB;AACjD;AACO,SAAS,eAAe,KAAa;AAC1C,SAAO,kBAAkB,KAAK,gBAAgB;AAChD;AAEA,eAAsB,aAAa,MAA6B;AAC9D,MAAI,OAAO,eAAe,aAAa;AACrC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,IAAI,IAAI,WAAW;AACzB,QAAE,SAAS,MAAM;AACf,YAAI,OAAO,EAAE,WAAW,UAAU;AAChC,kBAAQ,EAAE,MAAM;AAAA,QAClB,OAAO;AACL,iBAAO,IAAI,MAAM,wBAAwB,CAAC;AAAA,QAC5C;AAAA,MACF;AACA,QAAE,UAAU;AACZ,QAAE,cAAc,IAAI;AAAA,IACtB,CAAC;AAAA,EACH;AAEA,MAAI,OAAO,WAAW,aAAa;AACjC,UAAM,KAAK,MAAM,KAAK,YAAY;AAClC,WAAO,QAAQ,KAAK,IAAI,WAAW,OAAO,KAAK,EAAE,EAAE,SAAS,QAAQ,CAAC;AAAA,EACvE;AAEA,QAAM,IAAI,MAAM,yBAAyB;AAC3C;AAEA,eAAsB,aAAa,SAAiB;AAClD,QAAM,WAAW,MAAM,MAAM,OAAO;AACpC,SAAO,MAAM,SAAS,KAAK;AAC7B;AAEO,SAAS,4BACd,KACA,QACA;AACA,MAAI;AAKF,WAAO,IAAI,QAAQ,MAAM;AAAA,EAG3B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/media.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,QAAQ,EAEd,MAAM,sBAAsB,CAAC;AAuB9B,qBAAa,kBAAkB;IAC7B,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC,CAAC;gBAE5C,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAIzD,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAqB9B;IAEF,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAoC5B;IAEF,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC,CAMlC;CACH"}
|
package/dist/objects.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"objects.d.ts","sourceRoot":"","sources":["../src/objects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAER,UAAU,EACV,eAAe,EACf,4BAA4B,EAC5B,iCAAiC,EAClC,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"objects.d.ts","sourceRoot":"","sources":["../src/objects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAER,UAAU,EACV,eAAe,EACf,4BAA4B,EAC5B,iCAAiC,EAClC,MAAM,sBAAsB,CAAC;AAe9B,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAE3B;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC;IAC7D;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,KAAK,kBAAkB,GAAG;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,EAAE,CAAC;IACV,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,sBAAsB,GAAG;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC,GAAG,SAAS,CAAC;IACzE,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACzC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IAEjD,IAAI,EAAE,kDAkDL;IAED,SAAS,KAAK,GAAG,iBAQhB;cAEe,iBAAiB;gBAIrB,OAAO,CAAC,EAAE,oBAAoB;IAI1C,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CA4ClB;IAEF,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAyCxB;IAEF,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CA4BpB;cAEe,YAAY,CAAC,MAAM,SAAS,UAAU,EACrD,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAC5D,cAAc,CAAC,EAAE;QACf,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,GACA,cAAc,CACf,iCAAiC,CAAC,MAAM,CAAC,EACzC,sBAAsB,CACvB;IAwFD,SAAS,CAAC,cAAc,CACtB,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EACxD,cAAc,EAAE;QACd,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,GACA,MAAM;cAaQ,gBAAgB,CAAC,MAAM,SAAS,UAAU,EACzD,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAC5D,cAAc,EAAE;QACd,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,EACD,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAC/B,4BAA4B,CAAC,MAAM,CAAC;IAqBvC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CA4B5B;IAEF,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAuB5C;CACH"}
|
package/dist/utilities.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type GraffitiObject, type GraffitiObjectBase, type JSONSchema } from "@graffiti-garden/api";
|
|
2
|
+
import type Ajv from "ajv";
|
|
1
3
|
export declare function encodeBase64(bytes: Uint8Array): string;
|
|
2
4
|
export declare function decodeBase64(base64Url: string): Uint8Array;
|
|
3
5
|
export declare function randomBase64(numBytes?: number): string;
|
|
@@ -18,4 +20,5 @@ export declare function decodeMediaUrl(url: string): {
|
|
|
18
20
|
};
|
|
19
21
|
export declare function blobToBase64(blob: Blob): Promise<string>;
|
|
20
22
|
export declare function base64ToBlob(dataUrl: string): Promise<Blob>;
|
|
23
|
+
export declare function compileGraffitiObjectSchema<Schema extends JSONSchema>(ajv: Ajv, schema: Schema): (data: GraffitiObjectBase) => data is GraffitiObject<Schema>;
|
|
21
24
|
//# sourceMappingURL=utilities.d.ts.map
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAKtD;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAO1D;AAED,wBAAgB,YAAY,CAAC,QAAQ,GAAE,MAAW,GAAG,MAAM,CAK1D;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAE1E;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAExD;AACD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAEvD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;;EAU5D;AACD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM;;;EAE1C;AACD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM;;;EAEzC;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB9D;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,iBAGjD"}
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAE3B,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAKtD;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAO1D;AAED,wBAAgB,YAAY,CAAC,QAAQ,GAAE,MAAW,GAAG,MAAM,CAK1D;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAE1E;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAExD;AACD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAEvD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;;EAU5D;AACD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM;;;EAE1C;AACD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM;;;EAEzC;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB9D;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,iBAGjD;AAED,wBAAgB,2BAA2B,CAAC,MAAM,SAAS,UAAU,EACnE,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,UAQJ,kBAAkB,KACrB,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,CAMtC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graffiti-garden/implementation-local",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A local implementation of the Graffiti API using PouchDB",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
-
"test": "vitest run src
|
|
63
|
-
"test:watch": "vitest --watch",
|
|
64
|
-
"test:coverage": "vitest --coverage",
|
|
62
|
+
"test": "vitest run src",
|
|
63
|
+
"test:watch": "vitest --watch src",
|
|
64
|
+
"test:coverage": "vitest --coverage src",
|
|
65
65
|
"build:types": "tsc --declaration --emitDeclarationOnly",
|
|
66
66
|
"build:js": "tsx esbuild.config.mts",
|
|
67
67
|
"build": "rm -rf dist && npm run build:types && npm run build:js",
|
|
@@ -84,19 +84,18 @@
|
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@types/negotiator": "^0.6.4",
|
|
87
|
-
"@types/node": "^25.0.
|
|
87
|
+
"@types/node": "^25.0.6",
|
|
88
88
|
"@types/pouchdb": "^6.4.2",
|
|
89
|
-
"@vitest/coverage-v8": "^4.0.
|
|
89
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
90
90
|
"esbuild": "^0.27.2",
|
|
91
91
|
"esbuild-plugin-polyfill-node": "^0.3.0",
|
|
92
92
|
"tsx": "^4.19.2",
|
|
93
93
|
"typescript": "^5.7.3",
|
|
94
|
-
"vitest": "^4.0.
|
|
94
|
+
"vitest": "^4.0.17"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@graffiti-garden/api": "^1.0.
|
|
97
|
+
"@graffiti-garden/api": "^1.0.4",
|
|
98
98
|
"ajv": "^8.17.1",
|
|
99
|
-
"negotiator": "^1.0.0",
|
|
100
99
|
"pouchdb": "^9.0.0"
|
|
101
100
|
}
|
|
102
101
|
}
|
package/src/media.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GraffitiErrorNotAcceptable,
|
|
3
3
|
GraffitiErrorTooLarge,
|
|
4
|
+
isMediaAcceptable,
|
|
4
5
|
type Graffiti,
|
|
5
6
|
type JSONSchema,
|
|
6
7
|
} from "@graffiti-garden/api";
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
blobToBase64,
|
|
13
14
|
base64ToBlob,
|
|
14
15
|
} from "./utilities";
|
|
15
|
-
import Negotiator from "negotiator";
|
|
16
16
|
|
|
17
17
|
const MEDIA_OBJECT_SCHEMA = {
|
|
18
18
|
properties: {
|
|
@@ -58,7 +58,7 @@ export class GraffitiLocalMedia {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
getMedia: Graffiti["getMedia"] = async (...args) => {
|
|
61
|
-
const [mediaUrl,
|
|
61
|
+
const [mediaUrl, accept, session] = args;
|
|
62
62
|
const { actor, id } = decodeMediaUrl(mediaUrl);
|
|
63
63
|
const objectUrl = encodeObjectUrl(actor, id);
|
|
64
64
|
|
|
@@ -70,17 +70,16 @@ export class GraffitiLocalMedia {
|
|
|
70
70
|
|
|
71
71
|
const { dataBase64, type, size } = object.value;
|
|
72
72
|
|
|
73
|
-
if (
|
|
73
|
+
if (accept?.maxBytes && size > accept.maxBytes) {
|
|
74
74
|
throw new GraffitiErrorTooLarge("File size exceeds limit");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Make sure it adheres to requirements.accept
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new GraffitiErrorNotAcceptable(`Unsupported media type, ${type}`);
|
|
78
|
+
if (accept?.types) {
|
|
79
|
+
if (!isMediaAcceptable(type, accept.types)) {
|
|
80
|
+
throw new GraffitiErrorNotAcceptable(
|
|
81
|
+
`Unacceptable media type, ${type}`,
|
|
82
|
+
);
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
package/src/objects.ts
CHANGED
|
@@ -13,9 +13,13 @@ import {
|
|
|
13
13
|
unpackObjectUrl,
|
|
14
14
|
maskGraffitiObject,
|
|
15
15
|
isActorAllowedGraffitiObject,
|
|
16
|
-
compileGraffitiObjectSchema,
|
|
17
16
|
} from "@graffiti-garden/api";
|
|
18
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
randomBase64,
|
|
19
|
+
decodeObjectUrl,
|
|
20
|
+
encodeObjectUrl,
|
|
21
|
+
compileGraffitiObjectSchema,
|
|
22
|
+
} from "./utilities.js";
|
|
19
23
|
import type Ajv from "ajv";
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -166,13 +170,13 @@ export class GraffitiLocalObjects {
|
|
|
166
170
|
|
|
167
171
|
// Mask out the allowed list and channels
|
|
168
172
|
// if the user is not the owner
|
|
169
|
-
maskGraffitiObject(object, [], session);
|
|
173
|
+
const masked = maskGraffitiObject(object, [], session?.actor);
|
|
170
174
|
|
|
171
175
|
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
172
|
-
if (!validate(
|
|
176
|
+
if (!validate(masked)) {
|
|
173
177
|
throw new GraffitiErrorSchemaMismatch();
|
|
174
178
|
}
|
|
175
|
-
return
|
|
179
|
+
return masked;
|
|
176
180
|
};
|
|
177
181
|
|
|
178
182
|
delete: Graffiti["delete"] = async (...args) => {
|
|
@@ -206,7 +210,16 @@ export class GraffitiLocalObjects {
|
|
|
206
210
|
throw new GraffitiErrorNotFound("Object not found.");
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
|
|
213
|
+
// Return the output
|
|
214
|
+
const { value, channels, allowed } = doc;
|
|
215
|
+
const object: GraffitiObjectBase = {
|
|
216
|
+
value,
|
|
217
|
+
channels,
|
|
218
|
+
allowed,
|
|
219
|
+
url,
|
|
220
|
+
actor,
|
|
221
|
+
};
|
|
222
|
+
return object;
|
|
210
223
|
};
|
|
211
224
|
|
|
212
225
|
post: Graffiti["post"] = async (...args) => {
|
|
@@ -313,16 +326,20 @@ export class GraffitiLocalObjects {
|
|
|
313
326
|
|
|
314
327
|
if (!isActorAllowedGraffitiObject(object, session)) continue;
|
|
315
328
|
|
|
316
|
-
maskGraffitiObject(
|
|
329
|
+
const masked = maskGraffitiObject(
|
|
330
|
+
object,
|
|
331
|
+
discoverChannels,
|
|
332
|
+
session?.actor,
|
|
333
|
+
);
|
|
317
334
|
|
|
318
|
-
if (!validate(
|
|
335
|
+
if (!validate(masked)) continue;
|
|
319
336
|
|
|
320
337
|
yield tombstone
|
|
321
338
|
? {
|
|
322
339
|
tombstone: true,
|
|
323
340
|
object: { url },
|
|
324
341
|
}
|
|
325
|
-
: { object };
|
|
342
|
+
: { object: masked };
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
345
|
|
|
@@ -417,16 +434,20 @@ export class GraffitiLocalObjects {
|
|
|
417
434
|
cursor.slice("discover:".length),
|
|
418
435
|
);
|
|
419
436
|
if (actor && actor !== session?.actor) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
437
|
+
return (async function* () {
|
|
438
|
+
throw new GraffitiErrorForbidden(
|
|
439
|
+
"Cannot continue a cursor started by another actor",
|
|
440
|
+
);
|
|
441
|
+
})();
|
|
423
442
|
}
|
|
424
443
|
return this.discoverContinue<{}>(
|
|
425
444
|
[channels, schema, session],
|
|
426
445
|
continueParams,
|
|
427
446
|
);
|
|
428
447
|
} else {
|
|
429
|
-
|
|
448
|
+
return (async function* () {
|
|
449
|
+
throw new GraffitiErrorNotFound("Cursor not found");
|
|
450
|
+
})();
|
|
430
451
|
}
|
|
431
452
|
};
|
|
432
453
|
}
|
package/src/utilities.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GraffitiErrorInvalidSchema,
|
|
3
|
+
GraffitiErrorNotFound,
|
|
4
|
+
type GraffitiObject,
|
|
5
|
+
type GraffitiObjectBase,
|
|
6
|
+
type JSONSchema,
|
|
7
|
+
} from "@graffiti-garden/api";
|
|
8
|
+
import type Ajv from "ajv";
|
|
9
|
+
|
|
1
10
|
export function encodeBase64(bytes: Uint8Array): string {
|
|
2
11
|
// Convert it to base64
|
|
3
12
|
const base64 = btoa(String.fromCodePoint(...bytes));
|
|
@@ -36,11 +45,11 @@ export function encodeMediaUrl(actor: string, id: string) {
|
|
|
36
45
|
|
|
37
46
|
export function decodeGraffitiUrl(url: string, prefix: string) {
|
|
38
47
|
if (!url.startsWith(prefix)) {
|
|
39
|
-
throw new
|
|
48
|
+
throw new GraffitiErrorNotFound(`URL does not start with ${prefix}`);
|
|
40
49
|
}
|
|
41
50
|
const slices = url.slice(prefix.length).split(":");
|
|
42
51
|
if (slices.length !== 2) {
|
|
43
|
-
throw new
|
|
52
|
+
throw new GraffitiErrorNotFound("URL has too many colon-seperated parts");
|
|
44
53
|
}
|
|
45
54
|
const [actor, id] = slices.map(decodeURIComponent);
|
|
46
55
|
return { actor, id };
|
|
@@ -80,3 +89,22 @@ export async function base64ToBlob(dataUrl: string) {
|
|
|
80
89
|
const response = await fetch(dataUrl);
|
|
81
90
|
return await response.blob();
|
|
82
91
|
}
|
|
92
|
+
|
|
93
|
+
export function compileGraffitiObjectSchema<Schema extends JSONSchema>(
|
|
94
|
+
ajv: Ajv,
|
|
95
|
+
schema: Schema,
|
|
96
|
+
) {
|
|
97
|
+
try {
|
|
98
|
+
// Force the validation guard because
|
|
99
|
+
// it is too big for the type checker.
|
|
100
|
+
// Fortunately json-schema-to-ts is
|
|
101
|
+
// well tested against ajv.
|
|
102
|
+
return ajv.compile(schema) as (
|
|
103
|
+
data: GraffitiObjectBase,
|
|
104
|
+
) => data is GraffitiObject<Schema>;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new GraffitiErrorInvalidSchema(
|
|
107
|
+
error instanceof Error ? error.message : undefined,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|