@graffiti-garden/implementation-local 1.0.3 → 1.0.7

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/cjs/index.js CHANGED
@@ -25,7 +25,13 @@ var import_api = require("@graffiti-garden/api");
25
25
  var import_identity = require("./identity");
26
26
  var import_objects = require("./objects");
27
27
  var import_media = require("./media");
28
- class GraffitiLocal {
28
+ class GraffitiLocal extends import_api.GraffitiRuntimeTypes {
29
+ constructor(options) {
30
+ const graffiti = new GraffitiLocal_(options);
31
+ super(graffiti);
32
+ }
33
+ }
34
+ class GraffitiLocal_ {
29
35
  graffitiLocalIdentity = new import_identity.GraffitiLocalIdentity();
30
36
  login = this.graffitiLocalIdentity.login.bind(this.graffitiLocalIdentity);
31
37
  logout = this.graffitiLocalIdentity.logout.bind(this.graffitiLocalIdentity);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts"],
4
- "sourcesContent": ["import { Graffiti, type GraffitiSession } from \"@graffiti-garden/api\";\nimport { GraffitiLocalIdentity } from \"./identity\";\nimport { GraffitiLocalObjects, type GraffitiLocalOptions } from \"./objects\";\nimport { GraffitiLocalMedia } from \"./media\";\n\nexport type { GraffitiLocalOptions };\n\n/**\n * A local implementation of the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * based on [PouchDB](https://pouchdb.com/). PouchDb will automatically persist data in a local\n * database, either in the browser or in Node.js.\n * It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,\n * although using it with a remote server will not be secure.\n */\nexport class GraffitiLocal implements Graffiti {\n protected graffitiLocalIdentity = new GraffitiLocalIdentity();\n login = this.graffitiLocalIdentity.login.bind(this.graffitiLocalIdentity);\n logout = this.graffitiLocalIdentity.logout.bind(this.graffitiLocalIdentity);\n handleToActor = this.graffitiLocalIdentity.handleToActor.bind(\n this.graffitiLocalIdentity,\n );\n actorToHandle = this.graffitiLocalIdentity.actorToHandle.bind(\n this.graffitiLocalIdentity,\n );\n sessionEvents = this.graffitiLocalIdentity.sessionEvents;\n\n protected graffitiLocalObjects: GraffitiLocalObjects;\n post: Graffiti[\"post\"];\n get: Graffiti[\"get\"];\n delete: Graffiti[\"delete\"];\n discover: Graffiti[\"discover\"];\n continueDiscover: Graffiti[\"continueDiscover\"];\n\n protected graffitiLocalMedia: GraffitiLocalMedia;\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n\n constructor(options?: GraffitiLocalOptions) {\n this.graffitiLocalObjects = new GraffitiLocalObjects(options);\n this.post = this.graffitiLocalObjects.post.bind(this.graffitiLocalObjects);\n this.get = this.graffitiLocalObjects.get.bind(this.graffitiLocalObjects);\n this.delete = this.graffitiLocalObjects.delete.bind(\n this.graffitiLocalObjects,\n );\n this.discover = this.graffitiLocalObjects.discover.bind(\n this.graffitiLocalObjects,\n );\n this.continueDiscover = this.graffitiLocalObjects.continueDiscover.bind(\n this.graffitiLocalObjects,\n );\n\n this.graffitiLocalMedia = new GraffitiLocalMedia(this.graffitiLocalObjects);\n this.postMedia = this.graffitiLocalMedia.postMedia.bind(\n this.graffitiLocalMedia,\n );\n this.getMedia = this.graffitiLocalMedia.getMedia.bind(\n this.graffitiLocalMedia,\n );\n this.deleteMedia = this.graffitiLocalMedia.deleteMedia.bind(\n this.graffitiLocalMedia,\n );\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAA+C;AAC/C,sBAAsC;AACtC,qBAAgE;AAChE,mBAAmC;AAW5B,MAAM,cAAkC;AAAA,EACnC,wBAAwB,IAAI,sCAAsB;AAAA,EAC5D,QAAQ,KAAK,sBAAsB,MAAM,KAAK,KAAK,qBAAqB;AAAA,EACxE,SAAS,KAAK,sBAAsB,OAAO,KAAK,KAAK,qBAAqB;AAAA,EAC1E,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB;AAAA,EAEjC;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAgC;AAC1C,SAAK,uBAAuB,IAAI,oCAAqB,OAAO;AAC5D,SAAK,OAAO,KAAK,qBAAqB,KAAK,KAAK,KAAK,oBAAoB;AACzE,SAAK,MAAM,KAAK,qBAAqB,IAAI,KAAK,KAAK,oBAAoB;AACvE,SAAK,SAAS,KAAK,qBAAqB,OAAO;AAAA,MAC7C,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,qBAAqB,SAAS;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,mBAAmB,KAAK,qBAAqB,iBAAiB;AAAA,MACjE,KAAK;AAAA,IACP;AAEA,SAAK,qBAAqB,IAAI,gCAAmB,KAAK,oBAAoB;AAC1E,SAAK,YAAY,KAAK,mBAAmB,UAAU;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,mBAAmB,SAAS;AAAA,MAC/C,KAAK;AAAA,IACP;AACA,SAAK,cAAc,KAAK,mBAAmB,YAAY;AAAA,MACrD,KAAK;AAAA,IACP;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { Graffiti, GraffitiRuntimeTypes } from \"@graffiti-garden/api\";\nimport { GraffitiLocalIdentity } from \"./identity\";\nimport { GraffitiLocalObjects, type GraffitiLocalOptions } from \"./objects\";\nimport { GraffitiLocalMedia } from \"./media\";\n\nexport type { GraffitiLocalOptions };\n\n/**\n * A local implementation of the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * based on [PouchDB](https://pouchdb.com/). PouchDb will automatically persist data in a local\n * database, either in the browser or in Node.js.\n * It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,\n * although using it with a remote server will not be secure.\n */\nexport class GraffitiLocal extends GraffitiRuntimeTypes {\n constructor(options?: GraffitiLocalOptions) {\n const graffiti = new GraffitiLocal_(options);\n super(graffiti);\n }\n}\n\nclass GraffitiLocal_ implements Graffiti {\n protected graffitiLocalIdentity = new GraffitiLocalIdentity();\n login = this.graffitiLocalIdentity.login.bind(this.graffitiLocalIdentity);\n logout = this.graffitiLocalIdentity.logout.bind(this.graffitiLocalIdentity);\n handleToActor = this.graffitiLocalIdentity.handleToActor.bind(\n this.graffitiLocalIdentity,\n );\n actorToHandle = this.graffitiLocalIdentity.actorToHandle.bind(\n this.graffitiLocalIdentity,\n );\n sessionEvents = this.graffitiLocalIdentity.sessionEvents;\n\n protected graffitiLocalObjects: GraffitiLocalObjects;\n post: Graffiti[\"post\"];\n get: Graffiti[\"get\"];\n delete: Graffiti[\"delete\"];\n discover: Graffiti[\"discover\"];\n continueDiscover: Graffiti[\"continueDiscover\"];\n\n protected graffitiLocalMedia: GraffitiLocalMedia;\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n\n constructor(options?: GraffitiLocalOptions) {\n this.graffitiLocalObjects = new GraffitiLocalObjects(options);\n this.post = this.graffitiLocalObjects.post.bind(this.graffitiLocalObjects);\n this.get = this.graffitiLocalObjects.get.bind(this.graffitiLocalObjects);\n this.delete = this.graffitiLocalObjects.delete.bind(\n this.graffitiLocalObjects,\n );\n this.discover = this.graffitiLocalObjects.discover.bind(\n this.graffitiLocalObjects,\n );\n this.continueDiscover = this.graffitiLocalObjects.continueDiscover.bind(\n this.graffitiLocalObjects,\n );\n\n this.graffitiLocalMedia = new GraffitiLocalMedia(this.graffitiLocalObjects);\n this.postMedia = this.graffitiLocalMedia.postMedia.bind(\n this.graffitiLocalMedia,\n );\n this.getMedia = this.graffitiLocalMedia.getMedia.bind(\n this.graffitiLocalMedia,\n );\n this.deleteMedia = this.graffitiLocalMedia.deleteMedia.bind(\n this.graffitiLocalMedia,\n );\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAA+C;AAC/C,sBAAsC;AACtC,qBAAgE;AAChE,mBAAmC;AAW5B,MAAM,sBAAsB,gCAAqB;AAAA,EACtD,YAAY,SAAgC;AAC1C,UAAM,WAAW,IAAI,eAAe,OAAO;AAC3C,UAAM,QAAQ;AAAA,EAChB;AACF;AAEA,MAAM,eAAmC;AAAA,EAC7B,wBAAwB,IAAI,sCAAsB;AAAA,EAC5D,QAAQ,KAAK,sBAAsB,MAAM,KAAK,KAAK,qBAAqB;AAAA,EACxE,SAAS,KAAK,sBAAsB,OAAO,KAAK,KAAK,qBAAqB;AAAA,EAC1E,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB;AAAA,EAEjC;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAgC;AAC1C,SAAK,uBAAuB,IAAI,oCAAqB,OAAO;AAC5D,SAAK,OAAO,KAAK,qBAAqB,KAAK,KAAK,KAAK,oBAAoB;AACzE,SAAK,MAAM,KAAK,qBAAqB,IAAI,KAAK,KAAK,oBAAoB;AACvE,SAAK,SAAS,KAAK,qBAAqB,OAAO;AAAA,MAC7C,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,qBAAqB,SAAS;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,mBAAmB,KAAK,qBAAqB,iBAAiB;AAAA,MACjE,KAAK;AAAA,IACP;AAEA,SAAK,qBAAqB,IAAI,gCAAmB,KAAK,oBAAoB;AAC1E,SAAK,YAAY,KAAK,mBAAmB,UAAU;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,mBAAmB,SAAS;AAAA,MAC/C,KAAK;AAAA,IACP;AACA,SAAK,cAAc,KAAK,mBAAmB,YAAY;AAAA,MACrD,KAAK;AAAA,IACP;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/dist/cjs/media.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
  var media_exports = {};
30
20
  __export(media_exports, {
@@ -33,7 +23,6 @@ __export(media_exports, {
33
23
  module.exports = __toCommonJS(media_exports);
34
24
  var import_api = require("@graffiti-garden/api");
35
25
  var import_utilities = require("./utilities");
36
- var import_negotiator = __toESM(require("negotiator"));
37
26
  const MEDIA_OBJECT_SCHEMA = {
38
27
  properties: {
39
28
  value: {
@@ -67,28 +56,24 @@ class GraffitiLocalMedia {
67
56
  },
68
57
  session
69
58
  );
70
- const { actor, id } = (0, import_utilities.decodeObjectUrl)(url);
71
- return (0, import_utilities.encodeMediaUrl)(actor, id);
59
+ return url;
72
60
  };
73
61
  getMedia = async (...args) => {
74
- const [mediaUrl, requirements, session] = args;
75
- const { actor, id } = (0, import_utilities.decodeMediaUrl)(mediaUrl);
76
- const objectUrl = (0, import_utilities.encodeObjectUrl)(actor, id);
62
+ const [mediaUrl, accept, session] = args;
77
63
  const object = await this.db.get(
78
- objectUrl,
64
+ mediaUrl,
79
65
  MEDIA_OBJECT_SCHEMA,
80
66
  session
81
67
  );
82
68
  const { dataBase64, type, size } = object.value;
83
- if (requirements?.maxBytes && size > requirements.maxBytes) {
69
+ if (accept?.maxBytes && size > accept.maxBytes) {
84
70
  throw new import_api.GraffitiErrorTooLarge("File size exceeds limit");
85
71
  }
86
- if (requirements?.accept) {
87
- const negotiator = new import_negotiator.default({
88
- headers: { accept: requirements.accept }
89
- });
90
- if (negotiator.mediaType([type]) !== type) {
91
- throw new import_api.GraffitiErrorNotAcceptable(`Unsupported media type, ${type}`);
72
+ if (accept?.types) {
73
+ if (!(0, import_api.isMediaAcceptable)(type, accept.types)) {
74
+ throw new import_api.GraffitiErrorNotAcceptable(
75
+ `Unacceptable media type, ${type}`
76
+ );
92
77
  }
93
78
  }
94
79
  const data = await (0, import_utilities.base64ToBlob)(dataBase64);
@@ -103,9 +88,7 @@ class GraffitiLocalMedia {
103
88
  };
104
89
  deleteMedia = async (...args) => {
105
90
  const [mediaUrl, session] = args;
106
- const { actor, id } = (0, import_utilities.decodeMediaUrl)(mediaUrl);
107
- const objectUrl = (0, import_utilities.encodeObjectUrl)(actor, id);
108
- await this.db.delete(objectUrl, session);
91
+ await this.db.delete(mediaUrl, session);
109
92
  };
110
93
  }
111
94
  //# sourceMappingURL=media.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/media.ts"],
4
- "sourcesContent": ["import {\n GraffitiErrorNotAcceptable,\n GraffitiErrorTooLarge,\n type Graffiti,\n type JSONSchema,\n} from \"@graffiti-garden/api\";\nimport {\n decodeObjectUrl,\n encodeObjectUrl,\n decodeMediaUrl,\n encodeMediaUrl,\n blobToBase64,\n base64ToBlob,\n} from \"./utilities\";\nimport Negotiator from \"negotiator\";\n\nconst MEDIA_OBJECT_SCHEMA = {\n properties: {\n value: {\n properties: {\n dataBase64: { type: \"string\" },\n type: { type: \"string\" },\n size: { type: \"number\" },\n },\n required: [\"dataBase64\", \"type\", \"size\"],\n },\n },\n} as const satisfies JSONSchema;\n\nexport class GraffitiLocalMedia {\n protected db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">;\n\n constructor(db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">) {\n this.db = db;\n }\n\n postMedia: Graffiti[\"postMedia\"] = async (...args) => {\n const [media, session] = args;\n\n const dataBase64 = await blobToBase64(media.data);\n const type = media.data.type;\n\n const { url } = await this.db.post<typeof MEDIA_OBJECT_SCHEMA>(\n {\n value: {\n dataBase64,\n type,\n size: media.data.size,\n },\n channels: [],\n allowed: media.allowed,\n },\n session,\n );\n\n const { actor, id } = decodeObjectUrl(url);\n return encodeMediaUrl(actor, id);\n };\n\n getMedia: Graffiti[\"getMedia\"] = async (...args) => {\n const [mediaUrl, requirements, session] = args;\n const { actor, id } = decodeMediaUrl(mediaUrl);\n const objectUrl = encodeObjectUrl(actor, id);\n\n const object = await this.db.get<typeof MEDIA_OBJECT_SCHEMA>(\n objectUrl,\n MEDIA_OBJECT_SCHEMA,\n session,\n );\n\n const { dataBase64, type, size } = object.value;\n\n if (requirements?.maxBytes && size > requirements.maxBytes) {\n throw new GraffitiErrorTooLarge(\"File size exceeds limit\");\n }\n\n // Make sure it adheres to requirements.accept\n if (requirements?.accept) {\n const negotiator = new Negotiator({\n headers: { accept: requirements.accept },\n });\n if (negotiator.mediaType([type]) !== type) {\n throw new GraffitiErrorNotAcceptable(`Unsupported media type, ${type}`);\n }\n }\n\n const data = await base64ToBlob(dataBase64);\n if (data.size !== size || data.type !== type) {\n throw new Error(\"Invalid data\");\n }\n\n return {\n data,\n actor: object.actor,\n allowed: object.allowed,\n };\n };\n\n deleteMedia: Graffiti[\"deleteMedia\"] = async (...args) => {\n const [mediaUrl, session] = args;\n const { actor, id } = decodeMediaUrl(mediaUrl);\n const objectUrl = encodeObjectUrl(actor, id);\n\n await this.db.delete(objectUrl, session);\n };\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAKO;AACP,uBAOO;AACP,wBAAuB;AAEvB,MAAM,sBAAsB;AAAA,EAC1B,YAAY;AAAA,IACV,OAAO;AAAA,MACL,YAAY;AAAA,QACV,YAAY,EAAE,MAAM,SAAS;AAAA,QAC7B,MAAM,EAAE,MAAM,SAAS;AAAA,QACvB,MAAM,EAAE,MAAM,SAAS;AAAA,MACzB;AAAA,MACA,UAAU,CAAC,cAAc,QAAQ,MAAM;AAAA,IACzC;AAAA,EACF;AACF;AAEO,MAAM,mBAAmB;AAAA,EACpB;AAAA,EAEV,YAAY,IAA+C;AACzD,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,YAAmC,UAAU,SAAS;AACpD,UAAM,CAAC,OAAO,OAAO,IAAI;AAEzB,UAAM,aAAa,UAAM,+BAAa,MAAM,IAAI;AAChD,UAAM,OAAO,MAAM,KAAK;AAExB,UAAM,EAAE,IAAI,IAAI,MAAM,KAAK,GAAG;AAAA,MAC5B;AAAA,QACE,OAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,MAAM,MAAM,KAAK;AAAA,QACnB;AAAA,QACA,UAAU,CAAC;AAAA,QACX,SAAS,MAAM;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAEA,UAAM,EAAE,OAAO,GAAG,QAAI,kCAAgB,GAAG;AACzC,eAAO,iCAAe,OAAO,EAAE;AAAA,EACjC;AAAA,EAEA,WAAiC,UAAU,SAAS;AAClD,UAAM,CAAC,UAAU,cAAc,OAAO,IAAI;AAC1C,UAAM,EAAE,OAAO,GAAG,QAAI,iCAAe,QAAQ;AAC7C,UAAM,gBAAY,kCAAgB,OAAO,EAAE;AAE3C,UAAM,SAAS,MAAM,KAAK,GAAG;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,EAAE,YAAY,MAAM,KAAK,IAAI,OAAO;AAE1C,QAAI,cAAc,YAAY,OAAO,aAAa,UAAU;AAC1D,YAAM,IAAI,iCAAsB,yBAAyB;AAAA,IAC3D;AAGA,QAAI,cAAc,QAAQ;AACxB,YAAM,aAAa,IAAI,kBAAAA,QAAW;AAAA,QAChC,SAAS,EAAE,QAAQ,aAAa,OAAO;AAAA,MACzC,CAAC;AACD,UAAI,WAAW,UAAU,CAAC,IAAI,CAAC,MAAM,MAAM;AACzC,cAAM,IAAI,sCAA2B,2BAA2B,IAAI,EAAE;AAAA,MACxE;AAAA,IACF;AAEA,UAAM,OAAO,UAAM,+BAAa,UAAU;AAC1C,QAAI,KAAK,SAAS,QAAQ,KAAK,SAAS,MAAM;AAC5C,YAAM,IAAI,MAAM,cAAc;AAAA,IAChC;AAEA,WAAO;AAAA,MACL;AAAA,MACA,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,cAAuC,UAAU,SAAS;AACxD,UAAM,CAAC,UAAU,OAAO,IAAI;AAC5B,UAAM,EAAE,OAAO,GAAG,QAAI,iCAAe,QAAQ;AAC7C,UAAM,gBAAY,kCAAgB,OAAO,EAAE;AAE3C,UAAM,KAAK,GAAG,OAAO,WAAW,OAAO;AAAA,EACzC;AACF;",
6
- "names": ["Negotiator"]
4
+ "sourcesContent": ["import {\n GraffitiErrorNotAcceptable,\n GraffitiErrorTooLarge,\n isMediaAcceptable,\n type Graffiti,\n type JSONSchema,\n} from \"@graffiti-garden/api\";\nimport { blobToBase64, base64ToBlob } from \"./utilities\";\n\nconst MEDIA_OBJECT_SCHEMA = {\n properties: {\n value: {\n properties: {\n dataBase64: { type: \"string\" },\n type: { type: \"string\" },\n size: { type: \"number\" },\n },\n required: [\"dataBase64\", \"type\", \"size\"],\n },\n },\n} as const satisfies JSONSchema;\n\nexport class GraffitiLocalMedia {\n protected db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">;\n\n constructor(db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">) {\n this.db = db;\n }\n\n postMedia: Graffiti[\"postMedia\"] = async (...args) => {\n const [media, session] = args;\n\n const dataBase64 = await blobToBase64(media.data);\n const type = media.data.type;\n\n const { url } = await this.db.post<typeof MEDIA_OBJECT_SCHEMA>(\n {\n value: {\n dataBase64,\n type,\n size: media.data.size,\n },\n channels: [],\n allowed: media.allowed,\n },\n session,\n );\n\n return url;\n };\n\n getMedia: Graffiti[\"getMedia\"] = async (...args) => {\n const [mediaUrl, accept, session] = args;\n\n const object = await this.db.get<typeof MEDIA_OBJECT_SCHEMA>(\n mediaUrl,\n MEDIA_OBJECT_SCHEMA,\n session,\n );\n\n const { dataBase64, type, size } = object.value;\n\n if (accept?.maxBytes && size > accept.maxBytes) {\n throw new GraffitiErrorTooLarge(\"File size exceeds limit\");\n }\n\n // Make sure it adheres to requirements.accept\n if (accept?.types) {\n if (!isMediaAcceptable(type, accept.types)) {\n throw new GraffitiErrorNotAcceptable(\n `Unacceptable media type, ${type}`,\n );\n }\n }\n\n const data = await base64ToBlob(dataBase64);\n if (data.size !== size || data.type !== type) {\n throw new Error(\"Invalid data\");\n }\n\n return {\n data,\n actor: object.actor,\n allowed: object.allowed,\n };\n };\n\n deleteMedia: Graffiti[\"deleteMedia\"] = async (...args) => {\n const [mediaUrl, session] = args;\n\n await this.db.delete(mediaUrl, session);\n };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAMO;AACP,uBAA2C;AAE3C,MAAM,sBAAsB;AAAA,EAC1B,YAAY;AAAA,IACV,OAAO;AAAA,MACL,YAAY;AAAA,QACV,YAAY,EAAE,MAAM,SAAS;AAAA,QAC7B,MAAM,EAAE,MAAM,SAAS;AAAA,QACvB,MAAM,EAAE,MAAM,SAAS;AAAA,MACzB;AAAA,MACA,UAAU,CAAC,cAAc,QAAQ,MAAM;AAAA,IACzC;AAAA,EACF;AACF;AAEO,MAAM,mBAAmB;AAAA,EACpB;AAAA,EAEV,YAAY,IAA+C;AACzD,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,YAAmC,UAAU,SAAS;AACpD,UAAM,CAAC,OAAO,OAAO,IAAI;AAEzB,UAAM,aAAa,UAAM,+BAAa,MAAM,IAAI;AAChD,UAAM,OAAO,MAAM,KAAK;AAExB,UAAM,EAAE,IAAI,IAAI,MAAM,KAAK,GAAG;AAAA,MAC5B;AAAA,QACE,OAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,MAAM,MAAM,KAAK;AAAA,QACnB;AAAA,QACA,UAAU,CAAC;AAAA,QACX,SAAS,MAAM;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,WAAiC,UAAU,SAAS;AAClD,UAAM,CAAC,UAAU,QAAQ,OAAO,IAAI;AAEpC,UAAM,SAAS,MAAM,KAAK,GAAG;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,EAAE,YAAY,MAAM,KAAK,IAAI,OAAO;AAE1C,QAAI,QAAQ,YAAY,OAAO,OAAO,UAAU;AAC9C,YAAM,IAAI,iCAAsB,yBAAyB;AAAA,IAC3D;AAGA,QAAI,QAAQ,OAAO;AACjB,UAAI,KAAC,8BAAkB,MAAM,OAAO,KAAK,GAAG;AAC1C,cAAM,IAAI;AAAA,UACR,4BAA4B,IAAI;AAAA,QAClC;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,UAAM,+BAAa,UAAU;AAC1C,QAAI,KAAK,SAAS,QAAQ,KAAK,SAAS,MAAM;AAC5C,YAAM,IAAI,MAAM,cAAc;AAAA,IAChC;AAEA,WAAO;AAAA,MACL;AAAA,MACA,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,cAAuC,UAAU,SAAS;AACxD,UAAM,CAAC,UAAU,OAAO,IAAI;AAE5B,UAAM,KAAK,GAAG,OAAO,UAAU,OAAO;AAAA,EACxC;AACF;",
6
+ "names": []
7
7
  }
@@ -35,7 +35,6 @@ var import_api = require("@graffiti-garden/api");
35
35
  var import_utilities = require("./utilities.js");
36
36
  class GraffitiLocalObjects {
37
37
  db_;
38
- ajv_;
39
38
  options;
40
39
  get db() {
41
40
  if (!this.db_) {
@@ -74,15 +73,6 @@ class GraffitiLocalObjects {
74
73
  }
75
74
  return this.db_;
76
75
  }
77
- get ajv() {
78
- if (!this.ajv_) {
79
- this.ajv_ = (async () => {
80
- const { default: Ajv } = await import("ajv");
81
- return new Ajv({ strict: false });
82
- })();
83
- }
84
- return this.ajv_;
85
- }
86
76
  async getOperationClock() {
87
77
  return Number((await (await this.db).info()).update_seq);
88
78
  }
@@ -119,12 +109,12 @@ class GraffitiLocalObjects {
119
109
  "The object you are trying to get either does not exist or you are not allowed to see it"
120
110
  );
121
111
  }
122
- (0, import_api.maskGraffitiObject)(object, [], session);
123
- const validate = (0, import_api.compileGraffitiObjectSchema)(await this.ajv, schema);
124
- if (!validate(object)) {
112
+ const masked = (0, import_api.maskGraffitiObject)(object, [], session?.actor);
113
+ const validate = await (0, import_api.compileGraffitiObjectSchema)(schema);
114
+ if (!validate(masked)) {
125
115
  throw new import_api.GraffitiErrorSchemaMismatch();
126
116
  }
127
- return object;
117
+ return masked;
128
118
  };
129
119
  delete = async (...args) => {
130
120
  const [urlObject, session] = args;
@@ -151,7 +141,15 @@ class GraffitiLocalObjects {
151
141
  } catch {
152
142
  throw new import_api.GraffitiErrorNotFound("Object not found.");
153
143
  }
154
- return;
144
+ const { value, channels, allowed } = doc;
145
+ const object = {
146
+ value,
147
+ channels,
148
+ allowed,
149
+ url,
150
+ actor
151
+ };
152
+ return object;
155
153
  };
156
154
  post = async (...args) => {
157
155
  const [objectPartial, session] = args;
@@ -187,7 +185,7 @@ class GraffitiLocalObjects {
187
185
  }
188
186
  }
189
187
  const [discoverChannels, schema, session] = args;
190
- const validate = (0, import_api.compileGraffitiObjectSchema)(await this.ajv, schema);
188
+ const validate = await (0, import_api.compileGraffitiObjectSchema)(schema);
191
189
  const startKeySuffix = continueParams ? continueParams.ifModifiedSince.toString().padStart(15, "0") : "";
192
190
  const endKeySuffix = "\uFFFF";
193
191
  const processedUrls = /* @__PURE__ */ new Set();
@@ -218,12 +216,16 @@ class GraffitiLocalObjects {
218
216
  actor
219
217
  };
220
218
  if (!(0, import_api.isActorAllowedGraffitiObject)(object, session)) continue;
221
- (0, import_api.maskGraffitiObject)(object, discoverChannels, session);
222
- if (!validate(object)) continue;
219
+ const masked = (0, import_api.maskGraffitiObject)(
220
+ object,
221
+ discoverChannels,
222
+ session?.actor
223
+ );
224
+ if (!validate(masked)) continue;
223
225
  yield tombstone ? {
224
226
  tombstone: true,
225
227
  object: { url }
226
- } : { object };
228
+ } : { object: masked };
227
229
  }
228
230
  }
229
231
  return {
@@ -303,7 +305,7 @@ class GraffitiLocalObjects {
303
305
  );
304
306
  } else {
305
307
  return (async function* () {
306
- throw new import_api.GraffitiErrorNotFound("Cursor not found");
308
+ throw new import_api.GraffitiErrorCursorExpired("Cursor not found");
307
309
  })();
308
310
  }
309
311
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/objects.ts"],
4
- "sourcesContent": ["import type {\n Graffiti,\n GraffitiObjectBase,\n JSONSchema,\n GraffitiSession,\n GraffitiObjectStreamContinue,\n GraffitiObjectStreamContinueEntry,\n} from \"@graffiti-garden/api\";\nimport {\n GraffitiErrorNotFound,\n GraffitiErrorSchemaMismatch,\n GraffitiErrorForbidden,\n unpackObjectUrl,\n maskGraffitiObject,\n isActorAllowedGraffitiObject,\n compileGraffitiObjectSchema,\n} from \"@graffiti-garden/api\";\nimport { randomBase64, decodeObjectUrl, encodeObjectUrl } from \"./utilities.js\";\nimport type Ajv from \"ajv\";\n\n/**\n * Constructor options for the GraffitiPoubchDB class.\n */\nexport interface GraffitiLocalOptions {\n /**\n * Options to pass to the PouchDB constructor.\n * Defaults to `{ name: \"graffitiDb\" }`.\n *\n * See the [PouchDB documentation](https://pouchdb.com/api.html#create_database)\n * for available options.\n */\n pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;\n /**\n * Wait at least this long (in milliseconds) before continuing a stream.\n * A basic form of rate limiting. Defaults to 2 seconds.\n */\n continueBuffer?: number;\n}\n\ntype GraffitiObjectData = {\n tombstone: boolean;\n value: {};\n channels: string[];\n allowed?: string[] | null;\n lastModified: number;\n};\n\ntype ContinueDiscoverParams = {\n lastDiscovered: number;\n ifModifiedSince: number;\n};\n\n/**\n * An implementation of only the database operations of the\n * GraffitiAPI without synchronization or session management.\n */\nexport class GraffitiLocalObjects {\n protected db_: Promise<PouchDB.Database<GraffitiObjectData>> | undefined;\n protected ajv_: Promise<Ajv> | undefined;\n protected readonly options: GraffitiLocalOptions;\n\n get db() {\n if (!this.db_) {\n this.db_ = (async () => {\n const { default: PouchDB } = await import(\"pouchdb\");\n const pouchDbOptions = {\n name: \"graffitiDb\",\n ...this.options.pouchDBOptions,\n };\n const db = new PouchDB<GraffitiObjectData>(\n pouchDbOptions.name,\n pouchDbOptions,\n );\n await db\n //@ts-ignore\n .put({\n _id: \"_design/indexes\",\n views: {\n objectsPerChannelAndLastModified: {\n map: function (object: GraffitiObjectData) {\n const paddedLastModified = object.lastModified\n .toString()\n .padStart(15, \"0\");\n object.channels.forEach(function (channel) {\n const id =\n encodeURIComponent(channel) + \"/\" + paddedLastModified;\n //@ts-ignore\n emit(id);\n });\n }.toString(),\n },\n },\n })\n //@ts-ignore\n .catch((error) => {\n if (\n error &&\n typeof error === \"object\" &&\n \"name\" in error &&\n error.name === \"conflict\"\n ) {\n // Design document already exists\n return;\n } else {\n throw error;\n }\n });\n return db;\n })();\n }\n return this.db_;\n }\n\n protected get ajv() {\n if (!this.ajv_) {\n this.ajv_ = (async () => {\n const { default: Ajv } = await import(\"ajv\");\n return new Ajv({ strict: false });\n })();\n }\n return this.ajv_;\n }\n\n protected async getOperationClock() {\n return Number((await (await this.db).info()).update_seq);\n }\n\n constructor(options?: GraffitiLocalOptions) {\n this.options = options ?? {};\n }\n\n get: Graffiti[\"get\"] = async (...args) => {\n const [urlObject, schema, session] = args;\n const url = unpackObjectUrl(urlObject);\n\n let doc: GraffitiObjectData;\n try {\n doc = await (await this.db).get(url);\n } catch (error) {\n throw new GraffitiErrorNotFound(\n \"The object you are trying to get either does not exist or you are not allowed to see it\",\n );\n }\n\n if (doc.tombstone) {\n throw new GraffitiErrorNotFound(\n \"The object you are trying to get either does not exist or you are not allowed to see it\",\n );\n }\n\n const { actor } = decodeObjectUrl(url);\n const { value, channels, allowed } = doc;\n const object: GraffitiObjectBase = {\n value,\n channels,\n allowed,\n url,\n actor,\n };\n\n if (!isActorAllowedGraffitiObject(object, session)) {\n throw new GraffitiErrorNotFound(\n \"The object you are trying to get either does not exist or you are not allowed to see it\",\n );\n }\n\n // Mask out the allowed list and channels\n // if the user is not the owner\n maskGraffitiObject(object, [], session);\n\n const validate = compileGraffitiObjectSchema(await this.ajv, schema);\n if (!validate(object)) {\n throw new GraffitiErrorSchemaMismatch();\n }\n return object;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const [urlObject, session] = args;\n\n const url = unpackObjectUrl(urlObject);\n const { actor } = decodeObjectUrl(url);\n if (actor !== session.actor) {\n throw new GraffitiErrorForbidden(\n \"You cannot delete an object that you did not create.\",\n );\n }\n\n let doc: GraffitiObjectData & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;\n try {\n doc = await (await this.db).get(url);\n } catch {\n throw new GraffitiErrorNotFound(\"Object not found.\");\n }\n\n if (doc.tombstone) {\n throw new GraffitiErrorNotFound(\"Object not found.\");\n }\n\n // Set the tombstone and update lastModified\n doc.tombstone = true;\n doc.lastModified = await this.getOperationClock();\n try {\n await (await this.db).put(doc);\n } catch {\n throw new GraffitiErrorNotFound(\"Object not found.\");\n }\n\n return;\n };\n\n post: Graffiti[\"post\"] = async (...args) => {\n const [objectPartial, session] = args;\n\n const actor = session.actor;\n const id = randomBase64();\n const url = encodeObjectUrl(actor, id);\n\n const { value, channels, allowed } = objectPartial;\n const object: GraffitiObjectData = {\n value,\n channels,\n allowed,\n lastModified: await this.getOperationClock(),\n tombstone: false,\n };\n\n await (\n await this.db\n ).put({\n _id: url,\n ...object,\n });\n\n return {\n ...objectPartial,\n actor,\n url,\n };\n };\n\n protected async *discoverMeta<Schema extends JSONSchema>(\n args: Parameters<typeof Graffiti.prototype.discover<Schema>>,\n continueParams?: {\n lastDiscovered: number;\n ifModifiedSince: number;\n },\n ): AsyncGenerator<\n GraffitiObjectStreamContinueEntry<Schema>,\n ContinueDiscoverParams\n > {\n // If we are continuing a discover, make sure to wait at\n // least 2 seconds since the last poll to start a new one.\n if (continueParams) {\n const continueBuffer = this.options.continueBuffer ?? 2000;\n const timeElapsedSinceLastDiscover =\n Date.now() - continueParams.lastDiscovered;\n if (timeElapsedSinceLastDiscover < continueBuffer) {\n // Continue was called too soon,\n // wait a bit before continuing\n await new Promise((resolve) =>\n setTimeout(resolve, continueBuffer - timeElapsedSinceLastDiscover),\n );\n }\n }\n\n const [discoverChannels, schema, session] = args;\n const validate = compileGraffitiObjectSchema(await this.ajv, schema);\n const startKeySuffix = continueParams\n ? continueParams.ifModifiedSince.toString().padStart(15, \"0\")\n : \"\";\n const endKeySuffix = \"\\uffff\";\n\n const processedUrls = new Set<string>();\n\n const startTime = await this.getOperationClock();\n\n for (const channel of discoverChannels) {\n const keyPrefix = encodeURIComponent(channel) + \"/\";\n const startkey = keyPrefix + startKeySuffix;\n const endkey = keyPrefix + endKeySuffix;\n\n const result = await (\n await this.db\n ).query<GraffitiObjectData>(\"indexes/objectsPerChannelAndLastModified\", {\n startkey,\n endkey,\n include_docs: true,\n });\n\n for (const row of result.rows) {\n const doc = row.doc;\n if (!doc) continue;\n\n const url = doc._id;\n\n if (processedUrls.has(url)) continue;\n processedUrls.add(url);\n\n // If this is not a continuation, skip tombstones\n if (!continueParams && doc.tombstone) continue;\n\n const { tombstone, value, channels, allowed } = doc;\n const { actor } = decodeObjectUrl(url);\n\n const object: GraffitiObjectBase = {\n url,\n value,\n allowed,\n channels,\n actor,\n };\n\n if (!isActorAllowedGraffitiObject(object, session)) continue;\n\n maskGraffitiObject(object, discoverChannels, session);\n\n if (!validate(object)) continue;\n\n yield tombstone\n ? {\n tombstone: true,\n object: { url },\n }\n : { object };\n }\n }\n\n return {\n lastDiscovered: Date.now(),\n ifModifiedSince: startTime,\n };\n }\n\n protected discoverCursor(\n args: Parameters<typeof Graffiti.prototype.discover<{}>>,\n continueParams: {\n lastDiscovered: number;\n ifModifiedSince: number;\n },\n ): string {\n const [channels, schema, session] = args;\n return (\n \"discover:\" +\n JSON.stringify({\n channels,\n schema,\n continueParams,\n actor: session?.actor,\n })\n );\n }\n\n protected async *discoverContinue<Schema extends JSONSchema>(\n args: Parameters<typeof Graffiti.prototype.discover<Schema>>,\n continueParams: {\n lastDiscovered: number;\n ifModifiedSince: number;\n },\n session?: GraffitiSession | null,\n ): GraffitiObjectStreamContinue<Schema> {\n if (session?.actor !== args[2]?.actor) {\n throw new GraffitiErrorForbidden(\n \"Cannot continue a cursor started by another actor\",\n );\n }\n const iterator = this.discoverMeta<Schema>(args, continueParams);\n\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n return {\n continue: (session) =>\n this.discoverContinue<Schema>(args, result.value, session),\n cursor: this.discoverCursor(args, result.value),\n };\n }\n yield result.value;\n }\n }\n\n discover: Graffiti[\"discover\"] = (...args) => {\n const [channels, schema, session] = args;\n const iterator = this.discoverMeta<(typeof args)[1]>([\n channels,\n schema,\n session,\n ]);\n\n const this_ = this;\n return (async function* () {\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n return {\n continue: (session) =>\n this_.discoverContinue<(typeof args)[1]>(\n args,\n result.value,\n session,\n ),\n cursor: this_.discoverCursor(args, result.value),\n };\n }\n // Make sure to filter out tombstones\n if (result.value.tombstone) continue;\n yield result.value;\n }\n })();\n };\n\n continueDiscover: Graffiti[\"continueDiscover\"] = (...args) => {\n const [cursor, session] = args;\n if (cursor.startsWith(\"discover:\")) {\n // TODO: use AJV here\n const { channels, schema, actor, continueParams } = JSON.parse(\n cursor.slice(\"discover:\".length),\n );\n if (actor && actor !== session?.actor) {\n return (async function* () {\n throw new GraffitiErrorForbidden(\n \"Cannot continue a cursor started by another actor\",\n );\n })();\n }\n return this.discoverContinue<{}>(\n [channels, schema, session],\n continueParams,\n );\n } else {\n return (async function* () {\n throw new GraffitiErrorNotFound(\"Cursor not found\");\n })();\n }\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n Graffiti,\n GraffitiObjectBase,\n JSONSchema,\n GraffitiSession,\n GraffitiObjectStreamContinue,\n GraffitiObjectStreamContinueEntry,\n} from \"@graffiti-garden/api\";\nimport {\n GraffitiErrorNotFound,\n GraffitiErrorSchemaMismatch,\n GraffitiErrorForbidden,\n unpackObjectUrl,\n maskGraffitiObject,\n isActorAllowedGraffitiObject,\n compileGraffitiObjectSchema,\n GraffitiErrorCursorExpired,\n} from \"@graffiti-garden/api\";\nimport { randomBase64, decodeObjectUrl, encodeObjectUrl } from \"./utilities.js\";\n\n/**\n * Constructor options for the GraffitiPoubchDB class.\n */\nexport interface GraffitiLocalOptions {\n /**\n * Options to pass to the PouchDB constructor.\n * Defaults to `{ name: \"graffitiDb\" }`.\n *\n * See the [PouchDB documentation](https://pouchdb.com/api.html#create_database)\n * for available options.\n */\n pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;\n /**\n * Wait at least this long (in milliseconds) before continuing a stream.\n * A basic form of rate limiting. Defaults to 2 seconds.\n */\n continueBuffer?: number;\n}\n\ntype GraffitiObjectData = {\n tombstone: boolean;\n value: {};\n channels: string[];\n allowed?: string[] | null;\n lastModified: number;\n};\n\ntype ContinueDiscoverParams = {\n lastDiscovered: number;\n ifModifiedSince: number;\n};\n\n/**\n * An implementation of only the database operations of the\n * GraffitiAPI without synchronization or session management.\n */\nexport class GraffitiLocalObjects {\n protected db_: Promise<PouchDB.Database<GraffitiObjectData>> | undefined;\n protected readonly options: GraffitiLocalOptions;\n\n get db() {\n if (!this.db_) {\n this.db_ = (async () => {\n const { default: PouchDB } = await import(\"pouchdb\");\n const pouchDbOptions = {\n name: \"graffitiDb\",\n ...this.options.pouchDBOptions,\n };\n const db = new PouchDB<GraffitiObjectData>(\n pouchDbOptions.name,\n pouchDbOptions,\n );\n await db\n //@ts-ignore\n .put({\n _id: \"_design/indexes\",\n views: {\n objectsPerChannelAndLastModified: {\n map: function (object: GraffitiObjectData) {\n const paddedLastModified = object.lastModified\n .toString()\n .padStart(15, \"0\");\n object.channels.forEach(function (channel) {\n const id =\n encodeURIComponent(channel) + \"/\" + paddedLastModified;\n //@ts-ignore\n emit(id);\n });\n }.toString(),\n },\n },\n })\n //@ts-ignore\n .catch((error) => {\n if (\n error &&\n typeof error === \"object\" &&\n \"name\" in error &&\n error.name === \"conflict\"\n ) {\n // Design document already exists\n return;\n } else {\n throw error;\n }\n });\n return db;\n })();\n }\n return this.db_;\n }\n\n protected async getOperationClock() {\n return Number((await (await this.db).info()).update_seq);\n }\n\n constructor(options?: GraffitiLocalOptions) {\n this.options = options ?? {};\n }\n\n get: Graffiti[\"get\"] = async (...args) => {\n const [urlObject, schema, session] = args;\n const url = unpackObjectUrl(urlObject);\n\n let doc: GraffitiObjectData;\n try {\n doc = await (await this.db).get(url);\n } catch (error) {\n throw new GraffitiErrorNotFound(\n \"The object you are trying to get either does not exist or you are not allowed to see it\",\n );\n }\n\n if (doc.tombstone) {\n throw new GraffitiErrorNotFound(\n \"The object you are trying to get either does not exist or you are not allowed to see it\",\n );\n }\n\n const { actor } = decodeObjectUrl(url);\n const { value, channels, allowed } = doc;\n const object: GraffitiObjectBase = {\n value,\n channels,\n allowed,\n url,\n actor,\n };\n\n if (!isActorAllowedGraffitiObject(object, session)) {\n throw new GraffitiErrorNotFound(\n \"The object you are trying to get either does not exist or you are not allowed to see it\",\n );\n }\n\n // Mask out the allowed list and channels\n // if the user is not the owner\n const masked = maskGraffitiObject(object, [], session?.actor);\n\n const validate = await compileGraffitiObjectSchema(schema);\n if (!validate(masked)) {\n throw new GraffitiErrorSchemaMismatch();\n }\n return masked;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const [urlObject, session] = args;\n\n const url = unpackObjectUrl(urlObject);\n const { actor } = decodeObjectUrl(url);\n if (actor !== session.actor) {\n throw new GraffitiErrorForbidden(\n \"You cannot delete an object that you did not create.\",\n );\n }\n\n let doc: GraffitiObjectData & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;\n try {\n doc = await (await this.db).get(url);\n } catch {\n throw new GraffitiErrorNotFound(\"Object not found.\");\n }\n\n if (doc.tombstone) {\n throw new GraffitiErrorNotFound(\"Object not found.\");\n }\n\n // Set the tombstone and update lastModified\n doc.tombstone = true;\n doc.lastModified = await this.getOperationClock();\n try {\n await (await this.db).put(doc);\n } catch {\n throw new GraffitiErrorNotFound(\"Object not found.\");\n }\n\n // Return the output\n const { value, channels, allowed } = doc;\n const object: GraffitiObjectBase = {\n value,\n channels,\n allowed,\n url,\n actor,\n };\n return object;\n };\n\n post: Graffiti[\"post\"] = async (...args) => {\n const [objectPartial, session] = args;\n\n const actor = session.actor;\n const id = randomBase64();\n const url = encodeObjectUrl(actor, id);\n\n const { value, channels, allowed } = objectPartial;\n const object: GraffitiObjectData = {\n value,\n channels,\n allowed,\n lastModified: await this.getOperationClock(),\n tombstone: false,\n };\n\n await (\n await this.db\n ).put({\n _id: url,\n ...object,\n });\n\n return {\n ...objectPartial,\n actor,\n url,\n };\n };\n\n protected async *discoverMeta<Schema extends JSONSchema>(\n args: Parameters<typeof Graffiti.prototype.discover<Schema>>,\n continueParams?: {\n lastDiscovered: number;\n ifModifiedSince: number;\n },\n ): AsyncGenerator<\n GraffitiObjectStreamContinueEntry<Schema>,\n ContinueDiscoverParams\n > {\n // If we are continuing a discover, make sure to wait at\n // least 2 seconds since the last poll to start a new one.\n if (continueParams) {\n const continueBuffer = this.options.continueBuffer ?? 2000;\n const timeElapsedSinceLastDiscover =\n Date.now() - continueParams.lastDiscovered;\n if (timeElapsedSinceLastDiscover < continueBuffer) {\n // Continue was called too soon,\n // wait a bit before continuing\n await new Promise((resolve) =>\n setTimeout(resolve, continueBuffer - timeElapsedSinceLastDiscover),\n );\n }\n }\n\n const [discoverChannels, schema, session] = args;\n const validate = await compileGraffitiObjectSchema(schema);\n const startKeySuffix = continueParams\n ? continueParams.ifModifiedSince.toString().padStart(15, \"0\")\n : \"\";\n const endKeySuffix = \"\\uffff\";\n\n const processedUrls = new Set<string>();\n\n const startTime = await this.getOperationClock();\n\n for (const channel of discoverChannels) {\n const keyPrefix = encodeURIComponent(channel) + \"/\";\n const startkey = keyPrefix + startKeySuffix;\n const endkey = keyPrefix + endKeySuffix;\n\n const result = await (\n await this.db\n ).query<GraffitiObjectData>(\"indexes/objectsPerChannelAndLastModified\", {\n startkey,\n endkey,\n include_docs: true,\n });\n\n for (const row of result.rows) {\n const doc = row.doc;\n if (!doc) continue;\n\n const url = doc._id;\n\n if (processedUrls.has(url)) continue;\n processedUrls.add(url);\n\n // If this is not a continuation, skip tombstones\n if (!continueParams && doc.tombstone) continue;\n\n const { tombstone, value, channels, allowed } = doc;\n const { actor } = decodeObjectUrl(url);\n\n const object: GraffitiObjectBase = {\n url,\n value,\n allowed,\n channels,\n actor,\n };\n\n if (!isActorAllowedGraffitiObject(object, session)) continue;\n\n const masked = maskGraffitiObject(\n object,\n discoverChannels,\n session?.actor,\n );\n\n if (!validate(masked)) continue;\n\n yield tombstone\n ? {\n tombstone: true,\n object: { url },\n }\n : { object: masked };\n }\n }\n\n return {\n lastDiscovered: Date.now(),\n ifModifiedSince: startTime,\n };\n }\n\n protected discoverCursor(\n args: Parameters<typeof Graffiti.prototype.discover<{}>>,\n continueParams: {\n lastDiscovered: number;\n ifModifiedSince: number;\n },\n ): string {\n const [channels, schema, session] = args;\n return (\n \"discover:\" +\n JSON.stringify({\n channels,\n schema,\n continueParams,\n actor: session?.actor,\n })\n );\n }\n\n protected async *discoverContinue<Schema extends JSONSchema>(\n args: Parameters<typeof Graffiti.prototype.discover<Schema>>,\n continueParams: {\n lastDiscovered: number;\n ifModifiedSince: number;\n },\n session?: GraffitiSession | null,\n ): GraffitiObjectStreamContinue<Schema> {\n if (session?.actor !== args[2]?.actor) {\n throw new GraffitiErrorForbidden(\n \"Cannot continue a cursor started by another actor\",\n );\n }\n const iterator = this.discoverMeta<Schema>(args, continueParams);\n\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n return {\n continue: (session) =>\n this.discoverContinue<Schema>(args, result.value, session),\n cursor: this.discoverCursor(args, result.value),\n };\n }\n yield result.value;\n }\n }\n\n discover: Graffiti[\"discover\"] = (...args) => {\n const [channels, schema, session] = args;\n const iterator = this.discoverMeta<(typeof args)[1]>([\n channels,\n schema,\n session,\n ]);\n\n const this_ = this;\n return (async function* () {\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n return {\n continue: (session) =>\n this_.discoverContinue<(typeof args)[1]>(\n args,\n result.value,\n session,\n ),\n cursor: this_.discoverCursor(args, result.value),\n };\n }\n // Make sure to filter out tombstones\n if (result.value.tombstone) continue;\n yield result.value;\n }\n })();\n };\n\n continueDiscover: Graffiti[\"continueDiscover\"] = (...args) => {\n const [cursor, session] = args;\n if (cursor.startsWith(\"discover:\")) {\n // TODO: use AJV here\n const { channels, schema, actor, continueParams } = JSON.parse(\n cursor.slice(\"discover:\".length),\n );\n if (actor && actor !== session?.actor) {\n return (async function* () {\n throw new GraffitiErrorForbidden(\n \"Cannot continue a cursor started by another actor\",\n );\n })();\n }\n return this.discoverContinue<{}>(\n [channels, schema, session],\n continueParams,\n );\n } else {\n return (async function* () {\n throw new GraffitiErrorCursorExpired(\"Cursor not found\");\n })();\n }\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["session"]
7
7
  }
@@ -2,8 +2,8 @@
2
2
  var import_tests = require("@graffiti-garden/api/tests");
3
3
  var import_index = require("./index");
4
4
  const useGraffiti = () => new import_index.GraffitiLocal();
5
- const useSession1 = () => ({ actor: "someone" });
6
- const useSession2 = () => ({ actor: "someoneelse" });
5
+ const useSession1 = () => ({ actor: "did:example:someone" });
6
+ const useSession2 = () => ({ actor: "did:example:someoneelse" });
7
7
  (0, import_tests.graffitiCRUDTests)(useGraffiti, useSession1, useSession2);
8
8
  (0, import_tests.graffitiDiscoverTests)(useGraffiti, useSession1, useSession2);
9
9
  (0, import_tests.graffitiMediaTests)(useGraffiti, useSession1, useSession2);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/tests.spec.ts"],
4
- "sourcesContent": ["import {\n graffitiCRUDTests,\n graffitiDiscoverTests,\n graffitiMediaTests,\n} from \"@graffiti-garden/api/tests\";\nimport { GraffitiLocal } from \"./index\";\n\nconst useGraffiti = () => new GraffitiLocal();\nconst useSession1 = () => ({ actor: \"someone\" });\nconst useSession2 = () => ({ actor: \"someoneelse\" });\n\ngraffitiCRUDTests(useGraffiti, useSession1, useSession2);\ngraffitiDiscoverTests(useGraffiti, useSession1, useSession2);\ngraffitiMediaTests(useGraffiti, useSession1, useSession2);\n"],
5
- "mappings": ";AAAA,mBAIO;AACP,mBAA8B;AAE9B,MAAM,cAAc,MAAM,IAAI,2BAAc;AAC5C,MAAM,cAAc,OAAO,EAAE,OAAO,UAAU;AAC9C,MAAM,cAAc,OAAO,EAAE,OAAO,cAAc;AAAA,IAElD,gCAAkB,aAAa,aAAa,WAAW;AAAA,IACvD,oCAAsB,aAAa,aAAa,WAAW;AAAA,IAC3D,iCAAmB,aAAa,aAAa,WAAW;",
4
+ "sourcesContent": ["import {\n graffitiCRUDTests,\n graffitiDiscoverTests,\n graffitiMediaTests,\n} from \"@graffiti-garden/api/tests\";\nimport { GraffitiLocal } from \"./index\";\n\nconst useGraffiti = () => new GraffitiLocal();\nconst useSession1 = () => ({ actor: \"did:example:someone\" });\nconst useSession2 = () => ({ actor: \"did:example:someoneelse\" });\n\n// @ts-ignore\ngraffitiCRUDTests(useGraffiti, useSession1, useSession2);\ngraffitiDiscoverTests(useGraffiti, useSession1, useSession2);\ngraffitiMediaTests(useGraffiti, useSession1, useSession2);\n"],
5
+ "mappings": ";AAAA,mBAIO;AACP,mBAA8B;AAE9B,MAAM,cAAc,MAAM,IAAI,2BAAc;AAC5C,MAAM,cAAc,OAAO,EAAE,OAAO,sBAAsB;AAC1D,MAAM,cAAc,OAAO,EAAE,OAAO,0BAA0B;AAAA,IAG9D,gCAAkB,aAAa,aAAa,WAAW;AAAA,IACvD,oCAAsB,aAAa,aAAa,WAAW;AAAA,IAC3D,iCAAmB,aAAa,aAAa,WAAW;",
6
6
  "names": []
7
7
  }
@@ -22,12 +22,12 @@ __export(utilities_exports, {
22
22
  blobToBase64: () => blobToBase64,
23
23
  decodeBase64: () => decodeBase64,
24
24
  decodeGraffitiUrl: () => decodeGraffitiUrl,
25
- decodeMediaUrl: () => decodeMediaUrl,
26
25
  decodeObjectUrl: () => decodeObjectUrl,
26
+ decodeObjectUrlComponent: () => decodeObjectUrlComponent,
27
27
  encodeBase64: () => encodeBase64,
28
28
  encodeGraffitiUrl: () => encodeGraffitiUrl,
29
- encodeMediaUrl: () => encodeMediaUrl,
30
29
  encodeObjectUrl: () => encodeObjectUrl,
30
+ encodeObjectUrlComponent: () => encodeObjectUrlComponent,
31
31
  randomBase64: () => randomBase64
32
32
  });
33
33
  module.exports = __toCommonJS(utilities_exports);
@@ -46,17 +46,21 @@ function randomBase64(numBytes = 32) {
46
46
  crypto.getRandomValues(bytes);
47
47
  return encodeBase64(bytes);
48
48
  }
49
- const OBJECT_URL_PREFIX = "graffiti:object:";
50
- const MEDIA_URL_PREFIX = "graffiti:media:";
49
+ const OBJECT_URL_PREFIX = "graffiti:";
50
+ function encodeObjectUrlComponent(value) {
51
+ const replaced = value.replace(/:/g, "!").replace(/\//g, "~");
52
+ return encodeURIComponent(replaced);
53
+ }
54
+ function decodeObjectUrlComponent(value) {
55
+ const decoded = decodeURIComponent(value);
56
+ return decoded.replace(/!/g, ":").replace(/~/g, "/");
57
+ }
51
58
  function encodeGraffitiUrl(actor, id, prefix) {
52
- return `${prefix}${encodeURIComponent(actor)}:${encodeURIComponent(id)}`;
59
+ return `${prefix}${encodeObjectUrlComponent(actor)}:${encodeObjectUrlComponent(id)}`;
53
60
  }
54
61
  function encodeObjectUrl(actor, id) {
55
62
  return encodeGraffitiUrl(actor, id, OBJECT_URL_PREFIX);
56
63
  }
57
- function encodeMediaUrl(actor, id) {
58
- return encodeGraffitiUrl(actor, id, MEDIA_URL_PREFIX);
59
- }
60
64
  function decodeGraffitiUrl(url, prefix) {
61
65
  if (!url.startsWith(prefix)) {
62
66
  throw new import_api.GraffitiErrorNotFound(`URL does not start with ${prefix}`);
@@ -65,15 +69,12 @@ function decodeGraffitiUrl(url, prefix) {
65
69
  if (slices.length !== 2) {
66
70
  throw new import_api.GraffitiErrorNotFound("URL has too many colon-seperated parts");
67
71
  }
68
- const [actor, id] = slices.map(decodeURIComponent);
72
+ const [actor, id] = slices.map(decodeObjectUrlComponent);
69
73
  return { actor, id };
70
74
  }
71
75
  function decodeObjectUrl(url) {
72
76
  return decodeGraffitiUrl(url, OBJECT_URL_PREFIX);
73
77
  }
74
- function decodeMediaUrl(url) {
75
- return decodeGraffitiUrl(url, MEDIA_URL_PREFIX);
76
- }
77
78
  async function blobToBase64(blob) {
78
79
  if (typeof FileReader !== "undefined") {
79
80
  return new Promise((resolve, reject) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utilities.ts"],
4
- "sourcesContent": ["import { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\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"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAsC;AAE/B,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,iCAAsB,2BAA2B,MAAM,EAAE;AAAA,EACrE;AACA,QAAM,SAAS,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,GAAG;AACjD,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,iCAAsB,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;",
4
+ "sourcesContent": ["import { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\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:\";\n\nexport function encodeObjectUrlComponent(value: string) {\n const replaced = value.replace(/:/g, \"!\").replace(/\\//g, \"~\");\n return encodeURIComponent(replaced);\n}\nexport function decodeObjectUrlComponent(value: string) {\n const decoded = decodeURIComponent(value);\n return decoded.replace(/!/g, \":\").replace(/~/g, \"/\");\n}\nexport function encodeGraffitiUrl(actor: string, id: string, prefix: string) {\n return `${prefix}${encodeObjectUrlComponent(actor)}:${encodeObjectUrlComponent(id)}`;\n}\nexport function encodeObjectUrl(actor: string, id: string) {\n return encodeGraffitiUrl(actor, id, OBJECT_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(decodeObjectUrlComponent);\n return { actor, id };\n}\nexport function decodeObjectUrl(url: string) {\n return decodeGraffitiUrl(url, OBJECT_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"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAsC;AAE/B,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;AAEnB,SAAS,yBAAyB,OAAe;AACtD,QAAM,WAAW,MAAM,QAAQ,MAAM,GAAG,EAAE,QAAQ,OAAO,GAAG;AAC5D,SAAO,mBAAmB,QAAQ;AACpC;AACO,SAAS,yBAAyB,OAAe;AACtD,QAAM,UAAU,mBAAmB,KAAK;AACxC,SAAO,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AACrD;AACO,SAAS,kBAAkB,OAAe,IAAY,QAAgB;AAC3E,SAAO,GAAG,MAAM,GAAG,yBAAyB,KAAK,CAAC,IAAI,yBAAyB,EAAE,CAAC;AACpF;AACO,SAAS,gBAAgB,OAAe,IAAY;AACzD,SAAO,kBAAkB,OAAO,IAAI,iBAAiB;AACvD;AAEO,SAAS,kBAAkB,KAAa,QAAgB;AAC7D,MAAI,CAAC,IAAI,WAAW,MAAM,GAAG;AAC3B,UAAM,IAAI,iCAAsB,2BAA2B,MAAM,EAAE;AAAA,EACrE;AACA,QAAM,SAAS,IAAI,MAAM,OAAO,MAAM,EAAE,MAAM,GAAG;AACjD,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,iCAAsB,wCAAwC;AAAA,EAC1E;AACA,QAAM,CAAC,OAAO,EAAE,IAAI,OAAO,IAAI,wBAAwB;AACvD,SAAO,EAAE,OAAO,GAAG;AACrB;AACO,SAAS,gBAAgB,KAAa;AAC3C,SAAO,kBAAkB,KAAK,iBAAiB;AACjD;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;",
6
6
  "names": []
7
7
  }
package/dist/esm/index.js CHANGED
@@ -1,8 +1,14 @@
1
- import { Graffiti } from "@graffiti-garden/api";
1
+ import { Graffiti, GraffitiRuntimeTypes } from "@graffiti-garden/api";
2
2
  import { GraffitiLocalIdentity } from "./identity";
3
3
  import { GraffitiLocalObjects } from "./objects";
4
4
  import { GraffitiLocalMedia } from "./media";
5
- class GraffitiLocal {
5
+ class GraffitiLocal extends GraffitiRuntimeTypes {
6
+ constructor(options) {
7
+ const graffiti = new GraffitiLocal_(options);
8
+ super(graffiti);
9
+ }
10
+ }
11
+ class GraffitiLocal_ {
6
12
  graffitiLocalIdentity = new GraffitiLocalIdentity();
7
13
  login = this.graffitiLocalIdentity.login.bind(this.graffitiLocalIdentity);
8
14
  logout = this.graffitiLocalIdentity.logout.bind(this.graffitiLocalIdentity);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts"],
4
- "sourcesContent": ["import { Graffiti, type GraffitiSession } from \"@graffiti-garden/api\";\nimport { GraffitiLocalIdentity } from \"./identity\";\nimport { GraffitiLocalObjects, type GraffitiLocalOptions } from \"./objects\";\nimport { GraffitiLocalMedia } from \"./media\";\n\nexport type { GraffitiLocalOptions };\n\n/**\n * A local implementation of the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * based on [PouchDB](https://pouchdb.com/). PouchDb will automatically persist data in a local\n * database, either in the browser or in Node.js.\n * It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,\n * although using it with a remote server will not be secure.\n */\nexport class GraffitiLocal implements Graffiti {\n protected graffitiLocalIdentity = new GraffitiLocalIdentity();\n login = this.graffitiLocalIdentity.login.bind(this.graffitiLocalIdentity);\n logout = this.graffitiLocalIdentity.logout.bind(this.graffitiLocalIdentity);\n handleToActor = this.graffitiLocalIdentity.handleToActor.bind(\n this.graffitiLocalIdentity,\n );\n actorToHandle = this.graffitiLocalIdentity.actorToHandle.bind(\n this.graffitiLocalIdentity,\n );\n sessionEvents = this.graffitiLocalIdentity.sessionEvents;\n\n protected graffitiLocalObjects: GraffitiLocalObjects;\n post: Graffiti[\"post\"];\n get: Graffiti[\"get\"];\n delete: Graffiti[\"delete\"];\n discover: Graffiti[\"discover\"];\n continueDiscover: Graffiti[\"continueDiscover\"];\n\n protected graffitiLocalMedia: GraffitiLocalMedia;\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n\n constructor(options?: GraffitiLocalOptions) {\n this.graffitiLocalObjects = new GraffitiLocalObjects(options);\n this.post = this.graffitiLocalObjects.post.bind(this.graffitiLocalObjects);\n this.get = this.graffitiLocalObjects.get.bind(this.graffitiLocalObjects);\n this.delete = this.graffitiLocalObjects.delete.bind(\n this.graffitiLocalObjects,\n );\n this.discover = this.graffitiLocalObjects.discover.bind(\n this.graffitiLocalObjects,\n );\n this.continueDiscover = this.graffitiLocalObjects.continueDiscover.bind(\n this.graffitiLocalObjects,\n );\n\n this.graffitiLocalMedia = new GraffitiLocalMedia(this.graffitiLocalObjects);\n this.postMedia = this.graffitiLocalMedia.postMedia.bind(\n this.graffitiLocalMedia,\n );\n this.getMedia = this.graffitiLocalMedia.getMedia.bind(\n this.graffitiLocalMedia,\n );\n this.deleteMedia = this.graffitiLocalMedia.deleteMedia.bind(\n this.graffitiLocalMedia,\n );\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,gBAAsC;AAC/C,SAAS,6BAA6B;AACtC,SAAS,4BAAuD;AAChE,SAAS,0BAA0B;AAW5B,MAAM,cAAkC;AAAA,EACnC,wBAAwB,IAAI,sBAAsB;AAAA,EAC5D,QAAQ,KAAK,sBAAsB,MAAM,KAAK,KAAK,qBAAqB;AAAA,EACxE,SAAS,KAAK,sBAAsB,OAAO,KAAK,KAAK,qBAAqB;AAAA,EAC1E,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB;AAAA,EAEjC;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAgC;AAC1C,SAAK,uBAAuB,IAAI,qBAAqB,OAAO;AAC5D,SAAK,OAAO,KAAK,qBAAqB,KAAK,KAAK,KAAK,oBAAoB;AACzE,SAAK,MAAM,KAAK,qBAAqB,IAAI,KAAK,KAAK,oBAAoB;AACvE,SAAK,SAAS,KAAK,qBAAqB,OAAO;AAAA,MAC7C,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,qBAAqB,SAAS;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,mBAAmB,KAAK,qBAAqB,iBAAiB;AAAA,MACjE,KAAK;AAAA,IACP;AAEA,SAAK,qBAAqB,IAAI,mBAAmB,KAAK,oBAAoB;AAC1E,SAAK,YAAY,KAAK,mBAAmB,UAAU;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,mBAAmB,SAAS;AAAA,MAC/C,KAAK;AAAA,IACP;AACA,SAAK,cAAc,KAAK,mBAAmB,YAAY;AAAA,MACrD,KAAK;AAAA,IACP;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { Graffiti, GraffitiRuntimeTypes } from \"@graffiti-garden/api\";\nimport { GraffitiLocalIdentity } from \"./identity\";\nimport { GraffitiLocalObjects, type GraffitiLocalOptions } from \"./objects\";\nimport { GraffitiLocalMedia } from \"./media\";\n\nexport type { GraffitiLocalOptions };\n\n/**\n * A local implementation of the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * based on [PouchDB](https://pouchdb.com/). PouchDb will automatically persist data in a local\n * database, either in the browser or in Node.js.\n * It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,\n * although using it with a remote server will not be secure.\n */\nexport class GraffitiLocal extends GraffitiRuntimeTypes {\n constructor(options?: GraffitiLocalOptions) {\n const graffiti = new GraffitiLocal_(options);\n super(graffiti);\n }\n}\n\nclass GraffitiLocal_ implements Graffiti {\n protected graffitiLocalIdentity = new GraffitiLocalIdentity();\n login = this.graffitiLocalIdentity.login.bind(this.graffitiLocalIdentity);\n logout = this.graffitiLocalIdentity.logout.bind(this.graffitiLocalIdentity);\n handleToActor = this.graffitiLocalIdentity.handleToActor.bind(\n this.graffitiLocalIdentity,\n );\n actorToHandle = this.graffitiLocalIdentity.actorToHandle.bind(\n this.graffitiLocalIdentity,\n );\n sessionEvents = this.graffitiLocalIdentity.sessionEvents;\n\n protected graffitiLocalObjects: GraffitiLocalObjects;\n post: Graffiti[\"post\"];\n get: Graffiti[\"get\"];\n delete: Graffiti[\"delete\"];\n discover: Graffiti[\"discover\"];\n continueDiscover: Graffiti[\"continueDiscover\"];\n\n protected graffitiLocalMedia: GraffitiLocalMedia;\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n\n constructor(options?: GraffitiLocalOptions) {\n this.graffitiLocalObjects = new GraffitiLocalObjects(options);\n this.post = this.graffitiLocalObjects.post.bind(this.graffitiLocalObjects);\n this.get = this.graffitiLocalObjects.get.bind(this.graffitiLocalObjects);\n this.delete = this.graffitiLocalObjects.delete.bind(\n this.graffitiLocalObjects,\n );\n this.discover = this.graffitiLocalObjects.discover.bind(\n this.graffitiLocalObjects,\n );\n this.continueDiscover = this.graffitiLocalObjects.continueDiscover.bind(\n this.graffitiLocalObjects,\n );\n\n this.graffitiLocalMedia = new GraffitiLocalMedia(this.graffitiLocalObjects);\n this.postMedia = this.graffitiLocalMedia.postMedia.bind(\n this.graffitiLocalMedia,\n );\n this.getMedia = this.graffitiLocalMedia.getMedia.bind(\n this.graffitiLocalMedia,\n );\n this.deleteMedia = this.graffitiLocalMedia.deleteMedia.bind(\n this.graffitiLocalMedia,\n );\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,UAAU,4BAA4B;AAC/C,SAAS,6BAA6B;AACtC,SAAS,4BAAuD;AAChE,SAAS,0BAA0B;AAW5B,MAAM,sBAAsB,qBAAqB;AAAA,EACtD,YAAY,SAAgC;AAC1C,UAAM,WAAW,IAAI,eAAe,OAAO;AAC3C,UAAM,QAAQ;AAAA,EAChB;AACF;AAEA,MAAM,eAAmC;AAAA,EAC7B,wBAAwB,IAAI,sBAAsB;AAAA,EAC5D,QAAQ,KAAK,sBAAsB,MAAM,KAAK,KAAK,qBAAqB;AAAA,EACxE,SAAS,KAAK,sBAAsB,OAAO,KAAK,KAAK,qBAAqB;AAAA,EAC1E,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB,cAAc;AAAA,IACvD,KAAK;AAAA,EACP;AAAA,EACA,gBAAgB,KAAK,sBAAsB;AAAA,EAEjC;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAgC;AAC1C,SAAK,uBAAuB,IAAI,qBAAqB,OAAO;AAC5D,SAAK,OAAO,KAAK,qBAAqB,KAAK,KAAK,KAAK,oBAAoB;AACzE,SAAK,MAAM,KAAK,qBAAqB,IAAI,KAAK,KAAK,oBAAoB;AACvE,SAAK,SAAS,KAAK,qBAAqB,OAAO;AAAA,MAC7C,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,qBAAqB,SAAS;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,mBAAmB,KAAK,qBAAqB,iBAAiB;AAAA,MACjE,KAAK;AAAA,IACP;AAEA,SAAK,qBAAqB,IAAI,mBAAmB,KAAK,oBAAoB;AAC1E,SAAK,YAAY,KAAK,mBAAmB,UAAU;AAAA,MACjD,KAAK;AAAA,IACP;AACA,SAAK,WAAW,KAAK,mBAAmB,SAAS;AAAA,MAC/C,KAAK;AAAA,IACP;AACA,SAAK,cAAc,KAAK,mBAAmB,YAAY;AAAA,MACrD,KAAK;AAAA,IACP;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/dist/esm/media.js CHANGED
@@ -1,16 +1,9 @@
1
1
  import {
2
2
  GraffitiErrorNotAcceptable,
3
- GraffitiErrorTooLarge
3
+ GraffitiErrorTooLarge,
4
+ isMediaAcceptable
4
5
  } from "@graffiti-garden/api";
5
- import {
6
- decodeObjectUrl,
7
- encodeObjectUrl,
8
- decodeMediaUrl,
9
- encodeMediaUrl,
10
- blobToBase64,
11
- base64ToBlob
12
- } from "./utilities";
13
- import Negotiator from "negotiator";
6
+ import { blobToBase64, base64ToBlob } from "./utilities";
14
7
  const MEDIA_OBJECT_SCHEMA = {
15
8
  properties: {
16
9
  value: {
@@ -44,28 +37,24 @@ class GraffitiLocalMedia {
44
37
  },
45
38
  session
46
39
  );
47
- const { actor, id } = decodeObjectUrl(url);
48
- return encodeMediaUrl(actor, id);
40
+ return url;
49
41
  };
50
42
  getMedia = async (...args) => {
51
- const [mediaUrl, requirements, session] = args;
52
- const { actor, id } = decodeMediaUrl(mediaUrl);
53
- const objectUrl = encodeObjectUrl(actor, id);
43
+ const [mediaUrl, accept, session] = args;
54
44
  const object = await this.db.get(
55
- objectUrl,
45
+ mediaUrl,
56
46
  MEDIA_OBJECT_SCHEMA,
57
47
  session
58
48
  );
59
49
  const { dataBase64, type, size } = object.value;
60
- if (requirements?.maxBytes && size > requirements.maxBytes) {
50
+ if (accept?.maxBytes && size > accept.maxBytes) {
61
51
  throw new GraffitiErrorTooLarge("File size exceeds limit");
62
52
  }
63
- if (requirements?.accept) {
64
- const negotiator = new Negotiator({
65
- headers: { accept: requirements.accept }
66
- });
67
- if (negotiator.mediaType([type]) !== type) {
68
- throw new GraffitiErrorNotAcceptable(`Unsupported media type, ${type}`);
53
+ if (accept?.types) {
54
+ if (!isMediaAcceptable(type, accept.types)) {
55
+ throw new GraffitiErrorNotAcceptable(
56
+ `Unacceptable media type, ${type}`
57
+ );
69
58
  }
70
59
  }
71
60
  const data = await base64ToBlob(dataBase64);
@@ -80,9 +69,7 @@ class GraffitiLocalMedia {
80
69
  };
81
70
  deleteMedia = async (...args) => {
82
71
  const [mediaUrl, session] = args;
83
- const { actor, id } = decodeMediaUrl(mediaUrl);
84
- const objectUrl = encodeObjectUrl(actor, id);
85
- await this.db.delete(objectUrl, session);
72
+ await this.db.delete(mediaUrl, session);
86
73
  };
87
74
  }
88
75
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/media.ts"],
4
- "sourcesContent": ["import {\n GraffitiErrorNotAcceptable,\n GraffitiErrorTooLarge,\n type Graffiti,\n type JSONSchema,\n} from \"@graffiti-garden/api\";\nimport {\n decodeObjectUrl,\n encodeObjectUrl,\n decodeMediaUrl,\n encodeMediaUrl,\n blobToBase64,\n base64ToBlob,\n} from \"./utilities\";\nimport Negotiator from \"negotiator\";\n\nconst MEDIA_OBJECT_SCHEMA = {\n properties: {\n value: {\n properties: {\n dataBase64: { type: \"string\" },\n type: { type: \"string\" },\n size: { type: \"number\" },\n },\n required: [\"dataBase64\", \"type\", \"size\"],\n },\n },\n} as const satisfies JSONSchema;\n\nexport class GraffitiLocalMedia {\n protected db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">;\n\n constructor(db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">) {\n this.db = db;\n }\n\n postMedia: Graffiti[\"postMedia\"] = async (...args) => {\n const [media, session] = args;\n\n const dataBase64 = await blobToBase64(media.data);\n const type = media.data.type;\n\n const { url } = await this.db.post<typeof MEDIA_OBJECT_SCHEMA>(\n {\n value: {\n dataBase64,\n type,\n size: media.data.size,\n },\n channels: [],\n allowed: media.allowed,\n },\n session,\n );\n\n const { actor, id } = decodeObjectUrl(url);\n return encodeMediaUrl(actor, id);\n };\n\n getMedia: Graffiti[\"getMedia\"] = async (...args) => {\n const [mediaUrl, requirements, session] = args;\n const { actor, id } = decodeMediaUrl(mediaUrl);\n const objectUrl = encodeObjectUrl(actor, id);\n\n const object = await this.db.get<typeof MEDIA_OBJECT_SCHEMA>(\n objectUrl,\n MEDIA_OBJECT_SCHEMA,\n session,\n );\n\n const { dataBase64, type, size } = object.value;\n\n if (requirements?.maxBytes && size > requirements.maxBytes) {\n throw new GraffitiErrorTooLarge(\"File size exceeds limit\");\n }\n\n // Make sure it adheres to requirements.accept\n if (requirements?.accept) {\n const negotiator = new Negotiator({\n headers: { accept: requirements.accept },\n });\n if (negotiator.mediaType([type]) !== type) {\n throw new GraffitiErrorNotAcceptable(`Unsupported media type, ${type}`);\n }\n }\n\n const data = await base64ToBlob(dataBase64);\n if (data.size !== size || data.type !== type) {\n throw new Error(\"Invalid data\");\n }\n\n return {\n data,\n actor: object.actor,\n allowed: object.allowed,\n };\n };\n\n deleteMedia: Graffiti[\"deleteMedia\"] = async (...args) => {\n const [mediaUrl, session] = args;\n const { actor, id } = decodeMediaUrl(mediaUrl);\n const objectUrl = encodeObjectUrl(actor, id);\n\n await this.db.delete(objectUrl, session);\n };\n}\n"],
5
- "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,gBAAgB;AAEvB,MAAM,sBAAsB;AAAA,EAC1B,YAAY;AAAA,IACV,OAAO;AAAA,MACL,YAAY;AAAA,QACV,YAAY,EAAE,MAAM,SAAS;AAAA,QAC7B,MAAM,EAAE,MAAM,SAAS;AAAA,QACvB,MAAM,EAAE,MAAM,SAAS;AAAA,MACzB;AAAA,MACA,UAAU,CAAC,cAAc,QAAQ,MAAM;AAAA,IACzC;AAAA,EACF;AACF;AAEO,MAAM,mBAAmB;AAAA,EACpB;AAAA,EAEV,YAAY,IAA+C;AACzD,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,YAAmC,UAAU,SAAS;AACpD,UAAM,CAAC,OAAO,OAAO,IAAI;AAEzB,UAAM,aAAa,MAAM,aAAa,MAAM,IAAI;AAChD,UAAM,OAAO,MAAM,KAAK;AAExB,UAAM,EAAE,IAAI,IAAI,MAAM,KAAK,GAAG;AAAA,MAC5B;AAAA,QACE,OAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,MAAM,MAAM,KAAK;AAAA,QACnB;AAAA,QACA,UAAU,CAAC;AAAA,QACX,SAAS,MAAM;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAEA,UAAM,EAAE,OAAO,GAAG,IAAI,gBAAgB,GAAG;AACzC,WAAO,eAAe,OAAO,EAAE;AAAA,EACjC;AAAA,EAEA,WAAiC,UAAU,SAAS;AAClD,UAAM,CAAC,UAAU,cAAc,OAAO,IAAI;AAC1C,UAAM,EAAE,OAAO,GAAG,IAAI,eAAe,QAAQ;AAC7C,UAAM,YAAY,gBAAgB,OAAO,EAAE;AAE3C,UAAM,SAAS,MAAM,KAAK,GAAG;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,EAAE,YAAY,MAAM,KAAK,IAAI,OAAO;AAE1C,QAAI,cAAc,YAAY,OAAO,aAAa,UAAU;AAC1D,YAAM,IAAI,sBAAsB,yBAAyB;AAAA,IAC3D;AAGA,QAAI,cAAc,QAAQ;AACxB,YAAM,aAAa,IAAI,WAAW;AAAA,QAChC,SAAS,EAAE,QAAQ,aAAa,OAAO;AAAA,MACzC,CAAC;AACD,UAAI,WAAW,UAAU,CAAC,IAAI,CAAC,MAAM,MAAM;AACzC,cAAM,IAAI,2BAA2B,2BAA2B,IAAI,EAAE;AAAA,MACxE;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,aAAa,UAAU;AAC1C,QAAI,KAAK,SAAS,QAAQ,KAAK,SAAS,MAAM;AAC5C,YAAM,IAAI,MAAM,cAAc;AAAA,IAChC;AAEA,WAAO;AAAA,MACL;AAAA,MACA,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,cAAuC,UAAU,SAAS;AACxD,UAAM,CAAC,UAAU,OAAO,IAAI;AAC5B,UAAM,EAAE,OAAO,GAAG,IAAI,eAAe,QAAQ;AAC7C,UAAM,YAAY,gBAAgB,OAAO,EAAE;AAE3C,UAAM,KAAK,GAAG,OAAO,WAAW,OAAO;AAAA,EACzC;AACF;",
4
+ "sourcesContent": ["import {\n GraffitiErrorNotAcceptable,\n GraffitiErrorTooLarge,\n isMediaAcceptable,\n type Graffiti,\n type JSONSchema,\n} from \"@graffiti-garden/api\";\nimport { blobToBase64, base64ToBlob } from \"./utilities\";\n\nconst MEDIA_OBJECT_SCHEMA = {\n properties: {\n value: {\n properties: {\n dataBase64: { type: \"string\" },\n type: { type: \"string\" },\n size: { type: \"number\" },\n },\n required: [\"dataBase64\", \"type\", \"size\"],\n },\n },\n} as const satisfies JSONSchema;\n\nexport class GraffitiLocalMedia {\n protected db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">;\n\n constructor(db: Pick<Graffiti, \"post\" | \"get\" | \"delete\">) {\n this.db = db;\n }\n\n postMedia: Graffiti[\"postMedia\"] = async (...args) => {\n const [media, session] = args;\n\n const dataBase64 = await blobToBase64(media.data);\n const type = media.data.type;\n\n const { url } = await this.db.post<typeof MEDIA_OBJECT_SCHEMA>(\n {\n value: {\n dataBase64,\n type,\n size: media.data.size,\n },\n channels: [],\n allowed: media.allowed,\n },\n session,\n );\n\n return url;\n };\n\n getMedia: Graffiti[\"getMedia\"] = async (...args) => {\n const [mediaUrl, accept, session] = args;\n\n const object = await this.db.get<typeof MEDIA_OBJECT_SCHEMA>(\n mediaUrl,\n MEDIA_OBJECT_SCHEMA,\n session,\n );\n\n const { dataBase64, type, size } = object.value;\n\n if (accept?.maxBytes && size > accept.maxBytes) {\n throw new GraffitiErrorTooLarge(\"File size exceeds limit\");\n }\n\n // Make sure it adheres to requirements.accept\n if (accept?.types) {\n if (!isMediaAcceptable(type, accept.types)) {\n throw new GraffitiErrorNotAcceptable(\n `Unacceptable media type, ${type}`,\n );\n }\n }\n\n const data = await base64ToBlob(dataBase64);\n if (data.size !== size || data.type !== type) {\n throw new Error(\"Invalid data\");\n }\n\n return {\n data,\n actor: object.actor,\n allowed: object.allowed,\n };\n };\n\n deleteMedia: Graffiti[\"deleteMedia\"] = async (...args) => {\n const [mediaUrl, session] = args;\n\n await this.db.delete(mediaUrl, session);\n };\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,cAAc,oBAAoB;AAE3C,MAAM,sBAAsB;AAAA,EAC1B,YAAY;AAAA,IACV,OAAO;AAAA,MACL,YAAY;AAAA,QACV,YAAY,EAAE,MAAM,SAAS;AAAA,QAC7B,MAAM,EAAE,MAAM,SAAS;AAAA,QACvB,MAAM,EAAE,MAAM,SAAS;AAAA,MACzB;AAAA,MACA,UAAU,CAAC,cAAc,QAAQ,MAAM;AAAA,IACzC;AAAA,EACF;AACF;AAEO,MAAM,mBAAmB;AAAA,EACpB;AAAA,EAEV,YAAY,IAA+C;AACzD,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,YAAmC,UAAU,SAAS;AACpD,UAAM,CAAC,OAAO,OAAO,IAAI;AAEzB,UAAM,aAAa,MAAM,aAAa,MAAM,IAAI;AAChD,UAAM,OAAO,MAAM,KAAK;AAExB,UAAM,EAAE,IAAI,IAAI,MAAM,KAAK,GAAG;AAAA,MAC5B;AAAA,QACE,OAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,MAAM,MAAM,KAAK;AAAA,QACnB;AAAA,QACA,UAAU,CAAC;AAAA,QACX,SAAS,MAAM;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,WAAiC,UAAU,SAAS;AAClD,UAAM,CAAC,UAAU,QAAQ,OAAO,IAAI;AAEpC,UAAM,SAAS,MAAM,KAAK,GAAG;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,EAAE,YAAY,MAAM,KAAK,IAAI,OAAO;AAE1C,QAAI,QAAQ,YAAY,OAAO,OAAO,UAAU;AAC9C,YAAM,IAAI,sBAAsB,yBAAyB;AAAA,IAC3D;AAGA,QAAI,QAAQ,OAAO;AACjB,UAAI,CAAC,kBAAkB,MAAM,OAAO,KAAK,GAAG;AAC1C,cAAM,IAAI;AAAA,UACR,4BAA4B,IAAI;AAAA,QAClC;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,aAAa,UAAU;AAC1C,QAAI,KAAK,SAAS,QAAQ,KAAK,SAAS,MAAM;AAC5C,YAAM,IAAI,MAAM,cAAc;AAAA,IAChC;AAEA,WAAO;AAAA,MACL;AAAA,MACA,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,cAAuC,UAAU,SAAS;AACxD,UAAM,CAAC,UAAU,OAAO,IAAI;AAE5B,UAAM,KAAK,GAAG,OAAO,UAAU,OAAO;AAAA,EACxC;AACF;",
6
6
  "names": []
7
7
  }