ldn-inbox-server 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env-example CHANGED
@@ -9,4 +9,7 @@ LDN_SERVER_INBOX_PATH=./inbox
9
9
  LDN_SERVER_ERROR_PATH=./error
10
10
  LDN_SERVER_OUTBOX_PATH=./outbox
11
11
  LDN_SERVER_PUBLIC_PATH=./public
12
- LDN_SERVER_JSON_SCHEMA=./config/notification_schema.json
12
+ LDN_SERVER_JSON_SCHEMA=./config/notification_schema.json
13
+ LDN_SERVER_INBOX_CONFIG=./config/inbox_config.json
14
+ LDN_SERVER_OUTBOX_CONFIG=./config/outbox_config.json
15
+ LDN_SERVER_HAS_PUBLIC_INBOX=0
package/README.md CHANGED
@@ -33,13 +33,13 @@ curl -X POST -H 'Content-Type: application/ld+json' --data-binary '@examples/off
33
33
  Start an inbox handler with a demo handler (that creates an `Accept` message in the `./outbox`).
34
34
 
35
35
  ```
36
- npx ldn-inbox-server handle @inbox -hn ./handler/demo_notification_handler.js
36
+ npx ldn-inbox-server handler @inbox -hn ./handler/accept_notification_handler.js
37
37
  ```
38
38
 
39
- Send the notifications in the outbox:
39
+ Start an outbox handler that send the notifications that are available outbox:
40
40
 
41
41
  ```
42
- npx ldn-inbox-server handle @outbox
42
+ npx ldn-inbox-server handler @outbox -hn ./handler/send_notification_handler.js
43
43
  ```
44
44
 
45
45
  ## Environment
@@ -62,7 +62,7 @@ npx ldn-inbox-server handle @outbox
62
62
  Server extensions are possible by providing custom inbox and notification handlers. E.g.
63
63
 
64
64
  ```
65
- npx ldn-inbox-server handle-inbox -hn handler/demo_notification_handler.js
65
+ npx ldn-inbox-server handler @inbox -hn handler/my_handler.js
66
66
  ```
67
67
 
68
68
  Or, in JavaScript:
@@ -74,12 +74,19 @@ main();
74
74
 
75
75
  async function main() {
76
76
  await handle_inbox('./inbox', {
77
- 'notification_handler': 'handler/worker.js'
77
+ 'inbox': './inbox',
78
+ 'outbox': './outbox',
79
+ 'public': './public',
80
+ 'error': './error',
81
+ 'batch_size': 5,
82
+ 'glob': '^.*\\.jsonld$',
83
+ 'config': './config/inbox_config.json',
84
+ 'notification_handler': 'handler/my_handler.js'
78
85
  });
79
86
  }
80
87
  ```
81
88
 
82
- with `worker.js` :
89
+ with `my_handler.js` :
83
90
 
84
91
  ```
85
92
  async function handle({path,options}) {
@@ -102,6 +109,54 @@ A handler can be started on any directory. E.g. a workflow might be:
102
109
  - processed LDN messages will end up in the "outbox" box
103
110
  - invalid processing will be saved into the "error" box
104
111
 
112
+ ## Handlers
113
+
114
+ ### Accept handler
115
+
116
+ A handler that creates for an incoming notification an `Accept` notification in the `@outbox`.
117
+
118
+ ### Evenlog handler
119
+
120
+ A handler that updates an event log with the incoming notification.
121
+
122
+ Requires a configuration file with property `notification_handler.eventlog`:
123
+
124
+ - `log`: the path to the event log (starting from the `public` directory)
125
+ - `dir`: the path to a container to store the events (starting from the `public` directory)
126
+
127
+ The `log` and `dir` path may contain a `@artifact(:strip)?@` directive to fill in the
128
+ path of the current artifact. The `:strip` filter is used to strip an artifact path of a file extension. E.g. `path/artifact.html` becomes `path/artifact` when using a `:strip`.
129
+
130
+ ### Multi handler
131
+
132
+ A `handler/multi_notification_handler.js` is available to start multiple handler for each notification messages. The handlers to start are specified in a configuraton file that can be passed via the `config` parameter of an `handle_inbox`. In the commmand line tool `bin/ldn-inbox-server` the default location of such config file is `config/inbox_config.json` when processing an `@inbox`, and `config/outbox_config.json` when processing an `@outbox`.
133
+
134
+ ### Offer memento handler
135
+
136
+ A handler to `Offer` an event log to a memento server.
137
+
138
+ Requires a configuration file with property `notification_handler.offer_memento`:
139
+
140
+ - `actor`: the LDN+AS2 `actor` to use in a notification
141
+ - `target`: the LDN+AS2 `target` to use in a notification
142
+
143
+ ### Send handler
144
+
145
+ A hanlder to send notification that live in the `@outbox` via the LDN protocol to the LDN `target`.
146
+
147
+ ### Valid artifact
148
+
149
+ A handler that validates the incoming notification and checks if the `object` or `context` contains an artifact that is part of the `public` resources. See 'Artifact support' below.
150
+
151
+ ## Artifact support
152
+
153
+ This code base contains Event Notifications support for Data Node artifacts. See the examples
154
+ in `public/artifacts-example`. Move this directory tp `public/artifacts` to get a running example.
155
+
156
+ - Each artifact requires at least a `.meta` file with the `X-Artifact` header set to `true` to be recognized by the software as an artifact
157
+ - Each artifact should update the `Link-Template` header in the `.meta` file
158
+ - The config files in `config/inbox_config.json` and `config/outbox_config.json` define the location of the possible event logs for the artifact
159
+
105
160
  ## See also
106
161
 
107
162
  - [mellon-server](https://www.npmjs.com/package/mellon-server)
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const path = require('path');
3
+ const fs = require('fs');
4
4
  const { program } = require('commander');
5
5
  const { inbox_server } = require('../lib/index');
6
- const { handle_inbox , defaultSendNotificationHandler } = require('../lib/handler');
6
+ const { handle_inbox } = require('../lib/handler');
7
7
  require('dotenv').config();
8
8
 
9
9
  const HOST = process.env.LDN_SERVER_HOST ?? 'localhost';
@@ -17,11 +17,12 @@ const INBOX_PATH = process.env.LDN_SERVER_INBOX_PATH ?? './inbox';
17
17
  const ERROR_PATH = process.env.LDN_SERVER_ERROR_PATH ?? './error';
18
18
  const OUTBOX_PATH = process.env.LDN_SERVER_OUTBOX_PATH ?? './outbox';
19
19
  const JSON_SCHEMA_PATH = process.env.LDN_SERVER_JSON_SCHEMA ?? './config/notification_schema.json';
20
+ const INBOX_CONFIG = process.env.LDN_SERVER_INBOX_CONFIG;
21
+ const OUTBOX_CONFIG = process.env.LDN_SERVER_OUTBOX_CONFIG;
20
22
  const HAS_PUBLIC = process.env.LDN_SERVER_HAS_PUBLIC_INBOX ?? 0;
21
23
 
22
24
  program
23
25
  .name('lnd-inbox-server')
24
- .version('1.2.2')
25
26
  .description('A demonstration Event Notifications Inbox server');
26
27
 
27
28
  program
@@ -41,6 +42,7 @@ program
41
42
 
42
43
  program
43
44
  .command('handler')
45
+ .option('--base <url>','base url',INBOX_BASE_URL)
44
46
  .option('--inbox <inbox>','inbox',INBOX_PATH)
45
47
  .option('--outbox <outbox>','outbox',OUTBOX_PATH)
46
48
  .option('--public <public>','public',PUBLIC_PATH)
@@ -48,6 +50,7 @@ program
48
50
  .option('--loop <seconds>', 'run in a loop',0)
49
51
  .option('--batch_size <num>','batch size to process',INBOX_BATCH_SIZE)
50
52
  .option('--glob <glob>','files to process in inbox',INBOX_GLOB)
53
+ .option('--config <path>','config file for handlers')
51
54
  .option('-hi,--inbox_handler <handler>','inbox handler')
52
55
  .option('-hn,--notification_handler <handler>','notification handler')
53
56
  .argument('<box>','box to process')
@@ -55,11 +58,15 @@ program
55
58
  switch (box) {
56
59
  case '@inbox':
57
60
  box = INBOX_PATH;
61
+ if (!options['config'] && fs.existsSync(INBOX_CONFIG)) {
62
+ options['config'] = INBOX_CONFIG
63
+ }
58
64
  break;
59
65
  case '@outbox':
60
66
  box = OUTBOX_PATH;
61
- options['notification_handler'] =
62
- options['notification_handler'] ?? path.join(__dirname,'..','lib','sendNotificationHandler.js');
67
+ if (!options['config'] && fs.existsSync(OUTBOX_CONFIG)) {
68
+ options['config'] = OUTBOX_CONFIG
69
+ }
63
70
  break;
64
71
  }
65
72
  if (options['loop']) {
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const { program } = require('commander');
5
+ const { sendNotification, getLogger } = require('../lib/util.js');
6
+ require('dotenv').config();
7
+
8
+ const logger = getLogger();
9
+
10
+ const INBOX_URL = process.env.LDN_SERVER_INBOX_URL ?? 'inbox/';
11
+ const INBOX_BASE_URL = process.env.LDN_SERVER_BASEURL ?? 'http://localhost:8000';
12
+
13
+ program
14
+ .name('post-notification')
15
+ .description('A LDN notification sender')
16
+ .argument('<url>','notification')
17
+ .argument('<file>','notification')
18
+ .action( async(url,file) => {
19
+ const to = url === '@me' ? `${INBOX_BASE_URL}/${INBOX_URL}` : url;
20
+ const json = JSON.parse(fs.readFileSync(file, { encoding: 'utf-8'}));
21
+ await sendNotification(to,json);
22
+ });
23
+
24
+ program.parse();
@@ -0,0 +1,31 @@
1
+ {
2
+ "notification_handler": {
3
+ "eventlog": {
4
+ "log": "@artifact:strip@.jsonld",
5
+ "dir": "@artifact:strip@"
6
+ },
7
+ "multi": {
8
+ "handlers": [
9
+ "handler/type_filter_notification_handler.js",
10
+ "handler/valid_artifact_notification_handler.js" ,
11
+ "handler/eventlog_notification_handler.js",
12
+ "handler/offer_memento_notification_handler.js"
13
+ ]
14
+ },
15
+ "offer_memento": {
16
+ "actor": {
17
+ "id": "http://localhost:8000/profile/card#me" ,
18
+ "inbox": "http://localhost:8000/inbox/" ,
19
+ "type": "Service"
20
+ },
21
+ "target": {
22
+ "id": "http://localhost:8001/profile/card#me" ,
23
+ "inbox": "http://localhost:8001/inbox/" ,
24
+ "type": "Service"
25
+ }
26
+ },
27
+ "type_filter": {
28
+ "anyOf": [ "Offer" ]
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "notification_handler": {
3
+ "eventlog": {
4
+ "log": "@artifact:strip@.jsonld",
5
+ "dir": "@artifact:strip@"
6
+ },
7
+ "multi": {
8
+ "handlers": [
9
+ "handler/valid_artifact_notification_handler.js" ,
10
+ "handler/send_notification_handler.js",
11
+ "handler/eventlog_notification_handler.js"
12
+ ]
13
+ }
14
+ }
15
+ }
@@ -7,11 +7,11 @@
7
7
  "type": "Offer",
8
8
  "actor": {
9
9
  "id": "https://acme.org.xyz/profile/card#us",
10
- "inbox": "https://acme.org.xyz/inbox/",
10
+ "inbox": "http://localhost:8000/inbox/",
11
11
  "type": "Organization"
12
12
  },
13
13
  "object": {
14
- "id": "http://acme.org.xyz/artifacts/alice/data-set-2022-01-19.zip",
14
+ "id": "http://localhost:8000/artifacts/artifact1.html",
15
15
  "type": [ "Document" , "schema:Dataset" ]
16
16
  },
17
17
  "target": {
@@ -1,23 +1,26 @@
1
1
  const fs = require('fs');
2
+ const md5 = require('md5');
3
+ const { parseAsJSON } = require('../lib/util');
2
4
  const logger = require('../lib/util.js').getLogger();
3
5
 
6
+ /**
7
+ * Demonstration notification handler, that creates an 'Accept'
8
+ * notification for each incoming notification message and stores
9
+ * it in the outbox container.
10
+ */
4
11
  async function handle({path,options}) {
5
12
  logger.info(`parsing notification ${path}`);
6
13
 
7
14
  try {
8
- const json = JSON.parse(fs.readFileSync(path, { encoding: 'utf-8'}));
15
+ const json = parseAsJSON(path);
9
16
 
10
- const outboxFile = options['outbox'] + '/' + path.split('/').pop();
11
-
12
17
  const id = json['id'];
13
18
  const object = json['object'];
14
19
  const actor_id = json['actor']['id'];
15
20
  const actor_type = json['actor']['type'];
16
21
  const actor_inbox = json['actor']['inbox'];
17
22
 
18
- logger.info(`storing Accept to ${outboxFile}`);
19
-
20
- fs.writeFileSync(outboxFile, JSON.stringify({
23
+ const data = JSON.stringify({
21
24
  type: 'Accept',
22
25
  actor: {
23
26
  id: 'http://my.server' ,
@@ -32,13 +35,20 @@ async function handle({path,options}) {
32
35
  type: actor_type ,
33
36
  inbox: actor_inbox
34
37
  }
35
- },null,2));
36
- return { path,options, success: true };
38
+ },null,4);
39
+
40
+ const outboxFile = options['outbox'] + '/' + md5(data) + '.jsonld';
41
+
42
+ logger.info(`storing Accept to ${outboxFile}`);
43
+
44
+ fs.writeFileSync(outboxFile,data);
45
+
46
+ return { path, options, success: true };
37
47
  }
38
48
  catch(e) {
39
49
  logger.error(`failed to process ${path}`);
40
50
  logger.debug(e);
41
- return { path,options, success: false };
51
+ return { path, options, success: false };
42
52
  }
43
53
  }
44
54
 
@@ -1,7 +1,11 @@
1
1
  const fs = require('fs');
2
2
 
3
- // Demonstration of an inbox handler (don't use it the one in lib/handler.js
4
- // is much more mature)
3
+ /**
4
+ * Demonstration of an alternatve inbox handler
5
+ * (don't use it the one in lib/handler.js is much more mature)
6
+ * To use it set it via the `--hi` option of `bin/ldn-inbox-server.js`, or
7
+ * the `inbox_handler` option of the `handle_inbox` function.
8
+ */
5
9
  async function handle({path,options}) {
6
10
  console.log(`handleInbox(${path},..)`);
7
11
  fs.readdir(path, (err,files) => {
@@ -1,45 +1,121 @@
1
1
  const fs = require('fs');
2
+ const md5 = require('md5');
2
3
  const fsPath = require('path');
3
4
  const lockfile = require('proper-lockfile');
5
+ const { parseAsJSON } = require('../lib/util');
4
6
  const logger = require('../lib/util.js').getLogger();
5
7
 
6
- const EVENT_DIR = 'events';
7
- const EVENT_LOG = 'events.jsonld';
8
-
8
+ /**
9
+ * Demonstration event log handler
10
+ */
9
11
  async function handle({path,options}) {
10
12
  logger.info(`parsing notification ${path}`);
11
-
13
+
14
+ const config = parseAsJSON(options['config']);
15
+
16
+ if (! config) {
17
+ logger.error('no configuration found for eventlog_notification_handler');
18
+ return { path, options, success: false };
19
+ }
20
+
21
+ const thisConfig = config['notification_handler']?.['eventlog'];
22
+
23
+ if (! thisConfig || !thisConfig['log'] || !thisConfig['dir']) {
24
+ logger.error('no log/dir entry for notification_handler.eventlog configuration');
25
+ return { path, options, success: false };
26
+ }
27
+
28
+ const base = options['base'] ? options['base'] :
29
+ options['host'] && options['port'] ?
30
+ `http://${options['host']}:${options['port']}` :
31
+ 'http://localhost:8000';
32
+
33
+ let eventLog = thisConfig['log'];
34
+ let eventDir = thisConfig['dir'];
35
+
36
+ let artifactPath = undefined;
37
+
38
+ if (options['artifact']) {
39
+ artifactPath = options['artifact']['id'].substring(base.length+1);
40
+ logger.info(artifactPath);
41
+ }
42
+
43
+ if (eventLog.match(/@artifact(:[a-zA-Z]+)?@/)) {
44
+ if (options['artifact']) {
45
+ if (eventLog.match(/@artifact:strip@/)) {
46
+ eventLog = eventLog.replaceAll(/@artifact(:[a-zA-Z]+)?@/g,artifactPath.replaceAll(/\.\w+$/g,''));
47
+ }
48
+ else {
49
+ eventLog = eventLog.replaceAll(/@artifact(:[a-zA-Z]+)?@/g,artifactPath);
50
+ }
51
+ }
52
+ else {
53
+ logger.error(`requested to parse ${eventLog} but no artifact information available`);
54
+ return { path, options, success: false };
55
+ }
56
+ }
57
+
58
+ if (eventDir.match(/@artifact(:[a-zA-Z]+)?@/)) {
59
+ if (options['artifact']) {
60
+ if (eventDir.match(/@artifact:strip@/)) {
61
+ eventDir = eventDir.replaceAll(/@artifact(:[a-zA-Z]+)?@/g,artifactPath.replaceAll(/\.\w+$/g,''));
62
+ }
63
+ else {
64
+ eventDir = eventDir.replaceAll(/@artifact(:[a-zA-Z]+)?@/g,artifactPath);
65
+ }
66
+ }
67
+ else {
68
+ logger.error(`requested to parse ${eventDir} but no artifact information available`);
69
+ return { path, options, success: false };
70
+ }
71
+ }
72
+
12
73
  try {
13
- const json = JSON.parse(fs.readFileSync(path, { encoding: 'utf-8'}));
74
+ const json = fs.readFileSync(path, { encoding: 'utf-8' });
14
75
 
15
76
  const fileName = path.split('/').pop();
16
- const logDir = fsPath.join(options['public'],EVENT_DIR,'log');
17
77
 
18
- if (! fs.existsSync(logDir)) {
19
- logger.info(`creating ${logDir}`);
20
- fs.mkdirSync(logDir, { recursive : true });
78
+ if (! fs.existsSync(`${options['public']}/${eventDir}`)) {
79
+ logger.info(`creating ${options['public']}/${eventDir}`);
80
+ fs.mkdirSync(`${options['public']}/${eventDir}`, { recursive : true });
21
81
  }
22
-
23
- const outboxFile = fsPath.join(logDir,fileName);
24
82
 
25
- fs.writeFileSync(outboxFile, JSON.stringify(json));
83
+ const eventFile = `${options['public']}/${eventDir}/${fileName}`;
84
+
85
+ fs.writeFileSync(eventFile, json);
26
86
 
27
87
  // Updating metadata file
28
- const metaFile = outboxFile + '.meta';
88
+ const metaFile = `${options['public']}/${eventLog}.meta`;
29
89
 
30
90
  fs.writeFileSync(metaFile, JSON.stringify({
31
91
  'Content-Type': 'application/ld+json',
32
92
  'Last-Modified': nowISO()
33
93
  },null,4));
34
94
 
95
+ // Store the path in the options .. yeah yeah we know ugly but it works for now
96
+
97
+ const eventPath = `${eventDir}/${fileName}`;
98
+ const eventId = `${base}/${eventPath}`;
99
+ const eventLogId = `${base}/${eventLog}`;
100
+
101
+ options['eventlog'] = {
102
+ 'id': eventLogId ,
103
+ 'file': `${options['public']}/${eventLog}` ,
104
+ 'dir': `${options['public']}/${eventDir}` ,
105
+ 'item': {
106
+ 'id' : eventId ,
107
+ 'file' : eventFile
108
+ }
109
+ };
110
+
35
111
  await updateEventLog({path,options});
36
112
 
37
- return { path,options, success: true };
113
+ return { path, options, success: true };
38
114
  }
39
115
  catch(e) {
40
116
  logger.error(`failed to process ${path}`);
41
117
  logger.error(e);
42
- return { path,options, success: false };
118
+ return { path, options, success: false };
43
119
  }
44
120
  }
45
121
 
@@ -47,11 +123,11 @@ async function updateEventLog({path,options}) {
47
123
  logger.info(`updating eventlog for ${path}`);
48
124
 
49
125
  try {
50
- const baseUrl = process.env.LDN_SERVER_BASEURL ?? "";
51
- const fileName = path.split('/').pop();
52
- const entry = `${baseUrl}/${EVENT_DIR}/log/${fileName}`;
126
+ const notification = fs.readFileSync(path, { encoding: 'utf-8'});
127
+ const notification_checksum = md5(notification);
53
128
 
54
- const eventLog = fsPath.join(options['public'],EVENT_DIR,EVENT_LOG);
129
+ const entry = options['eventlog']['item']['id'];
130
+ const eventLog = options['eventlog']['file'];
55
131
 
56
132
  let json;
57
133
 
@@ -66,13 +142,20 @@ async function updateEventLog({path,options}) {
66
142
  };
67
143
  }
68
144
 
69
- if (json['member'].findIndex( (e) => e === entry) >= 0) {
145
+ if (json['member'].findIndex( (e) => e['id'] === entry) >= 0) {
70
146
  logger.info(`${entry} already in ${eventLog}`);
71
147
  }
72
148
  else {
73
149
  logger.info(`updating ${eventLog}`);
74
150
 
75
- json['member'].push(entry);
151
+ json['member'].push({
152
+ "id": entry ,
153
+ "checksum": {
154
+ "type": "Checksum",
155
+ "algorithm": "spdx:checksumAlgorithm_md5",
156
+ "checksumValue": notification_checksum
157
+ }
158
+ });
76
159
 
77
160
  if (fs.existsSync(eventLog)) {
78
161
  try {
@@ -0,0 +1,61 @@
1
+ const { dynamic_handler , parseAsJSON } = require('../lib/util');
2
+ const logger = require('../lib/util.js').getLogger();
3
+
4
+ /**
5
+ * Demonstration notification handler that start multiple notification handlers.
6
+ * Requires a config file that specifies which handlers to start.
7
+ */
8
+ async function handle({path,options}) {
9
+ let success = false;
10
+
11
+ const config = parseAsJSON(options['config']);
12
+
13
+ if (! config) {
14
+ logger.error('no configuration found for multi_notification_handler');
15
+ return { path, options, success: false };
16
+ }
17
+
18
+ const handlers = config['notification_handler']?.['multi']?.['handlers'];
19
+
20
+ if (! handlers) {
21
+ logger.error('no notification_handler.multi.handlers key in configuration file');
22
+ return { path, options, success: false };
23
+ }
24
+
25
+ try {
26
+ logger.info(`starting multi handler`);
27
+
28
+ for (let i = 0 ; i < handlers.length ; i++) {
29
+ logger.info(`starting ${handlers[i]}`);
30
+
31
+ const handler = dynamic_handler(handlers[i],null);
32
+
33
+ if (! handler) {
34
+ throw new Error(`failed to load ${handlers[i]}`);
35
+ }
36
+
37
+ const result = await handler({path,options});
38
+
39
+ if (result['success']) {
40
+ logger.info(`finished ${handlers[i]}`);
41
+ }
42
+ else {
43
+ throw new Error(`Eek! ${handlers[i]} failed`);
44
+ }
45
+ }
46
+
47
+ success = true;
48
+ }
49
+ catch (e) {
50
+ logger.error(`failed to process ${path}`);
51
+ logger.error(e);
52
+
53
+ success = false;
54
+ }
55
+
56
+ logger.info(`finished multi handler`);
57
+
58
+ return { path, options, success: success };
59
+ }
60
+
61
+ module.exports = { handle };
@@ -0,0 +1,68 @@
1
+ const fs = require('fs');
2
+ const md5 = require('md5');
3
+ const { parseAsJSON } = require('../lib/util.js');
4
+ const logger = require('../lib/util.js').getLogger();
5
+
6
+ /**
7
+ * Demonstration notification handler, that creates an 'Offer'
8
+ * notification for each incoming notificaation to request the
9
+ * archivation of the event log.
10
+ */
11
+ async function handle({path,options}) {
12
+ logger.info(`parsing notification ${path}`);
13
+
14
+ const config = parseAsJSON(options['config']);
15
+
16
+ if (! config) {
17
+ logger.error('no configuration found for offer_memento_notification_handler');
18
+ return { path, options, success: false };
19
+ }
20
+
21
+ const thisConfig = config['notification_handler']?.['offer_memento'];
22
+
23
+ if (! thisConfig || !thisConfig['actor'] || !thisConfig['target']) {
24
+ logger.error('no actor/target entry for notification_handler.eventlog configuration');
25
+ return { path, options, success: false };
26
+ }
27
+
28
+ if (!options['artifact']) {
29
+ logger.info(`no artifact found (ignonring this request)`);
30
+ return { path, options, success: true };
31
+ }
32
+
33
+ if (!options['eventlog']) {
34
+ logger.info(`no artifact found (ignonring this request)`);
35
+ return { path, options, success: true };
36
+ }
37
+
38
+ try {
39
+ const data = JSON.stringify({
40
+ '@context': [
41
+ "https://www.w3.org/ns/activitystreams" ,
42
+ {"schema": "https://schema.org/"}
43
+ ],
44
+ type: 'Offer',
45
+ actor: thisConfig['actor'],
46
+ object: {
47
+ id: options['eventlog']['id'],
48
+ type: [ "Document", "schema:Dataset" ]
49
+ },
50
+ target: thisConfig['target']
51
+ },null,4);
52
+
53
+ const outboxFile = options['outbox'] + '/' + md5(data) + '.jsonld';
54
+
55
+ logger.info(`storing Offer to ${outboxFile}`);
56
+
57
+ fs.writeFileSync(outboxFile,data);
58
+
59
+ return { path, options, success: true };
60
+ }
61
+ catch(e) {
62
+ logger.error(`failed to process ${path}`);
63
+ logger.debug(e);
64
+ return { path, options, success: false };
65
+ }
66
+ }
67
+
68
+ module.exports = { handle };
@@ -1,10 +1,13 @@
1
- const fs = require('fs');
2
1
  const logger = require('../lib/util.js').getLogger();
3
- const { sendNotification } = require('../lib/util.js');
2
+ const { sendNotification , parseAsJSON } = require('../lib/util.js');
4
3
 
4
+ /**
5
+ * Demonstration notification handler that sends a notification to a
6
+ * target inbox.
7
+ */
5
8
  async function handle({path,options}) {
6
9
  try {
7
- const json = fs.readFileSync(path, { encoding: 'utf-8'});
10
+ const json = parseAsJSON(path);
8
11
  const data = JSON.parse(json);
9
12
  const type = data['type'];
10
13
  let inbox;
@@ -0,0 +1,65 @@
1
+ const fs = require('fs');
2
+ const { parseAsJSON } = require('../lib/util');
3
+ const logger = require('../lib/util.js').getLogger();
4
+
5
+ /**
6
+ * Demonstration notification handler, that checks if the notification
7
+ * matches a configurable list
8
+ */
9
+ async function handle({path,options}) {
10
+ logger.info(`parsing notification ${path}`);
11
+
12
+ const config = parseAsJSON(options['config']);
13
+
14
+ if (! config) {
15
+ logger.error('no configuration found for eventlog_notification_handler');
16
+ return { path, options, success: false };
17
+ }
18
+
19
+ const thisConfig = config['notification_handler']?.['type_filter'];
20
+
21
+ if (! thisConfig) {
22
+ logger.error('no notification_handler.type_filer configuration');
23
+ return { path, options, success: false };
24
+ }
25
+
26
+ if (! (thisConfig['anyOf'] && Array.isArray(thisConfig['anyOf']))) {
27
+ logger.error('no anyOf entry in notification_handler.type_filer should be an array');
28
+ return { path, options, success: false };
29
+ }
30
+
31
+ try {
32
+ const json = parseAsJSON(path);
33
+
34
+ const type = json['type'];
35
+
36
+ const typeArray = Array.isArray(type) ? type : [type];
37
+
38
+ let isOk = true ;
39
+
40
+ for (let i = 0 ; i < typeArray.length ; i++) {
41
+ if (thisConfig['anyOf'].includes(typeArray[i])) {
42
+ // We are ok
43
+ }
44
+ else {
45
+ logger.error(`${typeArray[i]} does not pass type_filter check`);
46
+ isOk = false;
47
+ break;
48
+ }
49
+ }
50
+
51
+ if (isOk) {
52
+ return { path, options, success: true };
53
+ }
54
+ else {
55
+ return { path, options, success: false };
56
+ }
57
+ }
58
+ catch(e) {
59
+ logger.error(`failed to process ${path}`);
60
+ logger.debug(e);
61
+ return { path, options, success: false };
62
+ }
63
+ }
64
+
65
+ module.exports = { handle };
@@ -0,0 +1,55 @@
1
+ const { ldPropertyAsId , parseArtifact, parseAsJSON } = require('../lib/util.js');
2
+ const logger = require('../lib/util.js').getLogger();
3
+
4
+ /**
5
+ * Demonstration notification handler, that checks if the notification
6
+ * message contains an artifact that is known to the data node
7
+ */
8
+ async function handle({path,options}) {
9
+ logger.info(`parsing notification ${path}`);
10
+
11
+ try {
12
+ const json = parseAsJSON(path);
13
+
14
+ let artifact = undefined;
15
+
16
+ if (ldPropertyAsId(json['context'])) {
17
+ artifact = ldPropertyAsId(json['context']);
18
+ }
19
+ else if (ldPropertyAsId(json['object'])) {
20
+ artifact = ldPropertyAsId(json['object']);
21
+ }
22
+ else {
23
+ logger.error(`failed to find valid context or object`);
24
+ return { path, options, success: false };
25
+ }
26
+
27
+ if (!artifact) {
28
+ logger.error(`failed to find artifact`);
29
+ return { path, options, success: false };
30
+ }
31
+
32
+ const artifactPath = parseArtifact(artifact,options);
33
+
34
+ if (artifactPath) {
35
+ // Storing the artifact path to the options.
36
+ // Maybe bad practice..but it is a workflow attribute like in Nifi :P
37
+ options['artifact'] = {
38
+ 'id': artifact ,
39
+ 'path': artifactPath
40
+ };
41
+ return { path, options, success: true };
42
+ }
43
+ else {
44
+ logger.error(`artifact ${artifact} is not known here...`);
45
+ return { path, options, success: false };
46
+ }
47
+ }
48
+ catch(e) {
49
+ logger.error(`failed to process ${path}`);
50
+ logger.debug(e);
51
+ return { path, options, success: false };
52
+ }
53
+ }
54
+
55
+ module.exports = { handle };
package/lib/handler.js CHANGED
@@ -17,6 +17,7 @@ async function defaultInboxHandler({path,options}) {
17
17
 
18
18
  const worker = options['notification_handler'] ?? fsPath.resolve(__dirname,'..','lib','notification.js');
19
19
 
20
+ // Run the notifications using a node.js worker pool
20
21
  const pool = new piscina({
21
22
  filename: worker,
22
23
  maxQueue: queue_size
package/lib/index.js CHANGED
@@ -42,16 +42,17 @@ function inbox_server(options) {
42
42
  start_server({
43
43
  host: options['host'],
44
44
  port: options['port'],
45
+ base: options['base'],
45
46
  public: options['public'],
46
47
  registry: registry
47
48
  });
48
49
  }
49
50
 
50
51
  function doInbox(req,res) {
51
- if (req.method === 'GET' && INBOX_PUBLIC_READABLE) {
52
+ if (req.method === 'GET' && INBOX_PUBLIC_READABLE == 1) {
52
53
  doInboxGET(req,res);
53
54
  }
54
- if (req.method == 'HEAD' && INBOX_PUBLIC_READABLE) {
55
+ else if (req.method == 'HEAD' && INBOX_PUBLIC_READABLE == 1) {
55
56
  doInboxHEAD(req,res);
56
57
  }
57
58
  else if (req.method === 'POST') {
@@ -61,7 +62,6 @@ function doInbox(req,res) {
61
62
  logger.error(`tried method ${req.method} on inbox : forbidden`);
62
63
  res.writeHead(403);
63
64
  res.end('Forbidden');
64
- return;
65
65
  }
66
66
  }
67
67
 
package/lib/util.js CHANGED
@@ -67,7 +67,7 @@ async function sendNotification(url,json,options) {
67
67
  'Accept': 'application/json',
68
68
  'Content-Type': 'application/json'
69
69
  },
70
- body: JSON.stringify(json)
70
+ body: JSON.stringify(json,null,4)
71
71
  });
72
72
 
73
73
  if (! response.ok) {
@@ -125,11 +125,78 @@ function dynamic_handler(handler,fallback) {
125
125
  }
126
126
  }
127
127
 
128
+ function parseAsJSON(path) {
129
+ try {
130
+ return JSON.parse(fs.readFileSync(path, { encoding: 'utf-8'}));
131
+ }
132
+ catch (e) {
133
+ logger.error(`failed to parse ${path}`);
134
+ return null;
135
+ }
136
+ }
137
+
138
+ function ldPropertyAsId(object_or_string) {
139
+ if (!object_or_string) {
140
+ return null;
141
+ }
142
+
143
+ if (typeof object_or_string === 'string' || object_or_string instanceof String) {
144
+ return object_or_string;
145
+ }
146
+ else if (typeof object_or_string === 'object' && object_or_string['id']) {
147
+ return object_or_string['id'];
148
+ }
149
+ else if (typeof object_or_string === 'object' && object_or_string['@id']) {
150
+ return object_or_string['@id'];
151
+ }
152
+ else {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function parseArtifact(url,options) {
158
+ logger.debug(`isArtifact(${url})?`);
159
+ const base = options['base'] ? options['base'] :
160
+ options['host'] && options['port'] ?
161
+ `http://${options['host']}:${options['port']}` :
162
+ 'http://localhost:8000';
163
+
164
+ const public = options['public'];
165
+
166
+ const filePath = url.substring(base.length + 1);
167
+
168
+ const metaPath = `${public}/${filePath}.meta`;
169
+
170
+ logger.debug(`searching ${metaPath}`);
171
+
172
+ if (! fs.existsSync(metaPath)) {
173
+ logger.debug(`no such file ${metaPath}`);
174
+ return null;
175
+ }
176
+
177
+ const json = parseAsJSON(metaPath);
178
+
179
+ if (json === null) {
180
+ logger.debug(`no json at ${metaPath}`);
181
+ return null;
182
+ }
183
+
184
+ if (json['X-Artifact']) {
185
+ return `${public}/${filePath}`;
186
+ }
187
+ else {
188
+ return null;
189
+ }
190
+ }
191
+
128
192
  module.exports = {
129
193
  getLogger ,
130
194
  backOff_fetch ,
131
195
  fetchOriginal ,
132
196
  moveTo ,
133
197
  sendNotification ,
134
- dynamic_handler
198
+ dynamic_handler ,
199
+ parseAsJSON ,
200
+ parseArtifact ,
201
+ ldPropertyAsId
135
202
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldn-inbox-server",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "A demonstration Event Notifications Inbox server",
5
5
  "main": "lib/index.js",
6
6
  "author": "Patrick Hochstenbach <Patrick.Hochstenbach@UGent.be>",
@@ -8,10 +8,12 @@
8
8
  "scripts": {
9
9
  "server": "npx ldn-inbox-server start-server",
10
10
  "demo-post": "curl -X POST -H 'Content-Type: application/ld+json' --data-binary '@examples/offer.jsonld' http://localhost:8000/inbox/",
11
- "handle-inbox": "npx ldn-inbox-server handler @inbox -hn ./handler/demo_notification_handler.js",
12
- "handle-outbox": "npx ldn-inbox-server handler @outbox",
11
+ "handle-inbox": "npx ldn-inbox-server handler @inbox -hn ./handler/accept_notification_handler.js",
12
+ "handle-outbox": "npx ldn-inbox-server handler @outbox -hn ./handler/send_notification_handler.js",
13
13
  "handle-eventlog": "npx ldn-inbox-server handler @inbox -hn handler/eventlog_notification_handler.js",
14
- "clean": "rm error/* inbox/* outbox/*"
14
+ "handle-inbox-multi": "npx ldn-inbox-server handler @inbox -hn ./handler/multi_notification_handler.js",
15
+ "handle-outbox-multi": "npx ldn-inbox-server handler @outbox -hn ./handler/multi_notification_handler.js",
16
+ "clean": "rm -rf error/* inbox/* outbox/* public/events/* public/events/log/* public/artifacts/artifact1 public/artifacts/artifact2 public/artifacts/*.jsonld*"
15
17
  },
16
18
  "bin": "./bin/ldn-inbox-server.js",
17
19
  "keywords": [
@@ -25,10 +27,11 @@
25
27
  "exponential-backoff": "^3.1.1",
26
28
  "jsonschema": "^1.4.1",
27
29
  "md5": "^2.3.0",
28
- "mellon-server": "^1.0.4",
30
+ "mellon-server": "^1.0.8",
29
31
  "node-fetch": "1.7.3",
30
32
  "piscina": "^4.4.0",
31
33
  "proper-lockfile": "^4.1.2",
34
+ "upath": "^2.0.1",
32
35
  "uuid": "^9.0.1"
33
36
  }
34
37
  }
@@ -0,0 +1,17 @@
1
+ <html>
2
+ <head>
3
+ <link
4
+ rel="stylesheet"
5
+ href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
6
+ />
7
+ <title>Artifact 1</title>
8
+ </head>
9
+ <body>
10
+ <main class="container">
11
+ <h1>Artifact 1</h1>
12
+ <p>
13
+ <a href="artifact1.jsonld">Event Log</a>
14
+ </p>
15
+ </main>
16
+ </body>
17
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "Content-Type": "text/html",
3
+ "X-Artifact": true,
4
+ "Link-Template": "\"https://{YOUR-SERVER}/artifacts/artifact1.jsonld\"; rel=\"eventlog\"",
5
+ "Link": "</inbox/> ; rel=\"http://www.w3.org/ns/ldp#inbox\""
6
+ }
@@ -0,0 +1,17 @@
1
+ <html>
2
+ <head>
3
+ <link
4
+ rel="stylesheet"
5
+ href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
6
+ />
7
+ <title>Artifact 2</title>
8
+ </head>
9
+ <body>
10
+ <main class="container">
11
+ <h1>Artifact 2</h1>
12
+ <p>
13
+ <a href="artifact1.jsonld">Event Log</a>
14
+ </p>
15
+ </main>
16
+ </body>
17
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "Content-Type": "text/html",
3
+ "X-Artifact": true,
4
+ "Link-Template": "\"https://{YOUR-SERVER}/artifacts/artifact2.jsonld\"; rel=\"eventlog\"",
5
+ "Link": "</inbox/> ; rel=\"http://www.w3.org/ns/ldp#inbox\""
6
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "Content-Type": "text/html"
3
+ }