From 726cd980f0ed5588cfe8cbb2d994d5e4aef6e292 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Fri, 22 Dec 2023 13:03:30 -0800 Subject: [PATCH] redeem proffered tickets, db schema 1.1.0 --- .eslintrc.json | 2 +- README.md | 12 +- bin/publishTickets.js | 30 + bin/ticket-queue-profile.js | 13 + config/default.js | 6 +- config/test.js | 2 +- documentation/media/postgres-er.svg | 275 +++--- documentation/media/sqlite-er.svg | 273 +++--- package-lock.json | 814 +++++++++--------- package.json | 16 +- src/chores.js | 40 +- src/db/abstract.js | 53 +- src/db/postgres/index.js | 76 +- src/db/postgres/sql/schema/1.1.0/apply.sql | 23 + src/db/postgres/sql/schema/1.1.0/er.dot | 120 +++ src/db/postgres/sql/schema/1.1.0/revert.sql | 5 + src/db/postgres/sql/ticket-redeemed.sql | 5 + .../sql/ticket-token-get-unpublished.sql | 4 + .../postgres/sql/ticket-token-published.sql | 11 + src/db/sqlite/index.js | 92 +- src/db/sqlite/sql/schema/1.0.0/revert.sql | 2 + src/db/sqlite/sql/schema/1.1.0/apply.sql | 20 + src/db/sqlite/sql/schema/1.1.0/er.dot | 120 +++ src/db/sqlite/sql/schema/1.1.0/revert.sql | 5 + src/db/sqlite/sql/ticket-redeemed.sql | 6 + .../sql/ticket-token-get-unpublished.sql | 4 + src/db/sqlite/sql/ticket-token-published.sql | 11 + src/enum.js | 7 + src/manager.js | 136 ++- src/service.js | 11 +- src/template/admin-html.js | 2 +- src/template/root-html.js | 3 + test/src/chores.js | 71 +- test/src/db/integration.js | 59 ++ test/src/db/postgres.js | 132 +++ test/src/db/sqlite.js | 142 +++ test/src/manager.js | 81 +- test/stub-db.js | 8 +- 38 files changed, 1961 insertions(+), 731 deletions(-) create mode 100644 bin/publishTickets.js create mode 100644 bin/ticket-queue-profile.js create mode 100644 src/db/postgres/sql/schema/1.1.0/apply.sql create mode 100644 src/db/postgres/sql/schema/1.1.0/er.dot create mode 100644 src/db/postgres/sql/schema/1.1.0/revert.sql create mode 100644 src/db/postgres/sql/ticket-redeemed.sql create mode 100644 src/db/postgres/sql/ticket-token-get-unpublished.sql create mode 100644 src/db/postgres/sql/ticket-token-published.sql create mode 100644 src/db/sqlite/sql/schema/1.1.0/apply.sql create mode 100644 src/db/sqlite/sql/schema/1.1.0/er.dot create mode 100644 src/db/sqlite/sql/schema/1.1.0/revert.sql create mode 100644 src/db/sqlite/sql/ticket-redeemed.sql create mode 100644 src/db/sqlite/sql/ticket-token-get-unpublished.sql create mode 100644 src/db/sqlite/sql/ticket-token-published.sql diff --git a/.eslintrc.json b/.eslintrc.json index b3ffbe2..a5481e7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,7 @@ "extends": [ "eslint:recommended", "plugin:node/recommended", - "plugin:security/recommended", + "plugin:security/recommended-legacy", "plugin:sonarjs/recommended" ], "parserOptions": { diff --git a/README.md b/README.md index e37aea1..b5e6fb2 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,25 @@ A ```user``` is an entity known to this service, with a credential (currently a Authentication of a ```user``` is handled by either a [hashed password](https://en.wikipedia.org/wiki/Argon2) stored securely in one of the available database engines, or by optionally delegating to the host machine's [PAM subsystem](https://en.wikipedia.org/wiki/Pluggable_Authentication_Modules). PAM can be used to leverage, exempli gratia, LDAP integration for user authentication. -A ```profile``` is a URL (under control of a ```user```) which contents includes the necessary meta-data informing an application to contact this server for identification validation. Each ```user``` may have one or more ```profile```s. +A ```profile``` is a URL (under control of a ```user```) which contents includes the necessary meta-data informing an application to contact this service for identification validation. Each ```user``` may have one or more ```profile```s. Each ```profile``` may also be associated with a customizable list of additional [scopes](https://www.oauth.com/oauth2-servers/scope/) which may be added to any application client grant for convenience. An example of the user-interface when granting consent to a client application: ![Consent page](./documentation/media/consent-page.png) -A rudimentary ticket-sending UI is also available: -![Ticket Offer page](./documentation/media/ticket-page.png) - ## Resource Service Integration Other services (resources) may make calls to validate token grants by configuring a pre-shared secret, and authenticating to this server using [an HMAC-style bearer token scheme](https://git.squeep.com/?p=squeep-resource-authentication-module;a=blob_plain;f=README.md;hb=HEAD). ## Ticket Auth -This service can accept proffered [authentication tickets](https://indieweb.org/IndieAuth_Ticket_Auth). It will simply publish any proffered tickets for valid profiles to a configured AMQP/RabbitMQ queue for some other service to redeem and make use of. +This service can accept proffered [authentication tickets](https://indieweb.org/IndieAuth_Ticket_Auth). It will attempt to redeem any proffered tickets, then publish the resulting tokens to a configured AMQP/RabbitMQ queue for other services to make use of. If no AMQP server is configured, the ticket endpoint will be disabled and not advertised. + +Ensure the output of the script `bin/ticket-queue-profile.js` is executed on RabbitMQ server to install the needed queue profile. + +A rudimentary ticket-sending UI is also available: +![Ticket Offer page](./documentation/media/ticket-page.png) ## Architecture diff --git a/bin/publishTickets.js b/bin/publishTickets.js new file mode 100644 index 0000000..c8de1f8 --- /dev/null +++ b/bin/publishTickets.js @@ -0,0 +1,30 @@ +'use strict'; + +const DB = require('../src/db'); +const Logger = require('../src/logger'); +const Config = require('../config'); +const config = new Config(process.env.NODE_ENV, false); +const Chores = require('../src/chores'); +const { Publisher: QueuePublisher } = require('@squeep/amqp-helper'); + +const logger = new Logger(config); +const db = new DB(logger, config); + +(async () => { + if (!config.queues.amqp.url) { + console.log('no queue configured, nothing to do'); + return; + } + await db.initialize(); + const queuePublisher = new QueuePublisher(logger, config.queues.amqp); + // no automatic chores + config.chores.tokenCleanupMs = 0; + config.chores.scopeCleanupMs = 0; + config.chores.publishTicketsMs = 0; + const chores = new Chores(logger, db, queuePublisher, config); + + await chores.publishTickets(); + + console.log('done'); + await db._closeConnection(); +})(); diff --git a/bin/ticket-queue-profile.js b/bin/ticket-queue-profile.js new file mode 100644 index 0000000..b32d2d2 --- /dev/null +++ b/bin/ticket-queue-profile.js @@ -0,0 +1,13 @@ +'use strict'; + +/** + * Generates the required command line to configure rabbitmq queue profile. + */ + +const { Publisher } = require('@squeep/amqp-helper'); +const Config = require('../config'); + +const config = new Config(process.env.NODE_ENV); +const publisher = new Publisher(console, config.queues.amqp); +const result = publisher.policyCommand(config.queues.ticketPublishName); +console.log(result); diff --git a/config/default.js b/config/default.js index 55945fd..3b3fdcb 100644 --- a/config/default.js +++ b/config/default.js @@ -52,9 +52,10 @@ const defaultOptions = { queues: { amqp: { url: undefined, // AMQP endpoint, e.g. 'amqp://user:pass@rmq.host:5672' If not specified, ticket endpoint will be disabled - prefix: undefined, + prefix: 'indieauth', }, - ticketPublishName: 'indieauth.ticket.proffered', // exchange to publish proffered tickets to + ticketPublishName: 'ticket.proffered', // exchange to publish proffered tickets to + ticketRedeemedName: 'ticket.redeemed', // exchange to publish redeemed ticket tokens to }, // Logging options @@ -77,6 +78,7 @@ const defaultOptions = { chores: { scopeCleanupMs: 0, // how often to clean up unreferenced scopes, 0 for never tokenCleanupMs: 0, // how often to clean up no-longer-valid scopes, 0 for never + publishTicketsMs: 0, // how often to try to re-publish unpublished redeemed ticket tokens }, // Outgoing request UA header. Setting these here to override helper defaults. diff --git a/config/test.js b/config/test.js index b92272f..59d7d29 100644 --- a/config/test.js +++ b/config/test.js @@ -10,7 +10,7 @@ module.exports = { }, queues: { amqp: { - url: 'ampq://localhost:5432', + url: 'amqp://localhost:5432', }, }, }; diff --git a/documentation/media/postgres-er.svg b/documentation/media/postgres-er.svg index 670582a..ac8658e 100644 --- a/documentation/media/postgres-er.svg +++ b/documentation/media/postgres-er.svg @@ -4,181 +4,204 @@ - + indieAutherERD - -IndieAuther Entity-Relations -Postgres -Schema 1.0.0 - + +IndieAuther Entity-Relations +Postgres +Schema 1.1.0 + +redeemed_ticket + + +REDEEMED_TICKET + +ticket_id + +created + +subject + +resource + +iss + +token + +ticket + +published + + + token - - -TOKEN - -code_id - -profile_id - -created - -expires - -refresh_expires - -refreshed - -duration - -refresh_duration - -refresh_count - -is_revoked - -is_token - -client_id - -resource - -profile_data + + +TOKEN + +code_id + +profile_id + +created + +expires + +refresh_expires + +refreshed + +duration + +refresh_duration + +refresh_count + +is_revoked + +is_token + +client_id + +resource + +profile_data - + token_scope - - -TOKEN_SCOPE - -code_id - -scope_id + + +TOKEN_SCOPE + +code_id + +scope_id token:pk_code_id->token_scope:fk_code_id - - + + - + profile - - -PROFILE - -profile_id - -identifier_id - -profile + + +PROFILE + +profile_id + +identifier_id + +profile profile:pk_profile_id->token:fk_profile_id - - + + - + profile_scope - - -PROFILE_SCOPE - -profile_id - -scope_id + + +PROFILE_SCOPE + +profile_id + +scope_id profile:pk_profile_id->profile_scope:fk_profile_id - - + + - + scope - - -SCOPE - -scope_id - -scope - -description - -application - -is_permanent - -is_manually_added + + +SCOPE + +scope_id + +scope + +description + +application + +is_permanent + +is_manually_added scope:pk_scope_id->token_scope:fk_scope_id - - + + scope:pk_scope_id->profile_scope:fk_scope_id - - + + - + authentication - - -AUTHENTICATION - -identifier_id - -created - -last_authenticated - -identifier - -credential + + +AUTHENTICATION + +identifier_id + +created + +last_authenticated + +identifier + +credential authentication:pk_identifier_id->profile:fk_identifier_id - - + + - + resource - - -RESOURCE - -resource_id - -description - -created - -secret + + +RESOURCE + +resource_id + +description + +created + +secret - + almanac - - -ALMANAC - -event - -date + + +ALMANAC + +event + +date diff --git a/documentation/media/sqlite-er.svg b/documentation/media/sqlite-er.svg index 8d036ed..ca8ee1c 100644 --- a/documentation/media/sqlite-er.svg +++ b/documentation/media/sqlite-er.svg @@ -4,180 +4,203 @@ - + indieAutherERD - -IndieAuther Entity-RelationsSQLite -Schema 1.0.0 - + +IndieAuther Entity-RelationsSQLite +Schema 1.1.0 + +redeemed_ticket + + +REDEEMED_TICKET + +ticket_id + +created + +subject + +resource + +iss + +token + +ticket + +published + + + token - - -TOKEN - -code_id - -profile_id - -created - -expires - -refresh_expires - -refreshed - -duration - -refresh_duration - -refresh_count - -is_revoked - -is_token - -client_id - -resource - -profile_data + + +TOKEN + +code_id + +profile_id + +created + +expires + +refresh_expires + +refreshed + +duration + +refresh_duration + +refresh_count + +is_revoked + +is_token + +client_id + +resource + +profile_data - + token_scope - - -TOKEN_SCOPE - -code_id - -scope_id + + +TOKEN_SCOPE + +code_id + +scope_id token:pk_code_id->token_scope:fk_code_id - - + + - + profile - - -PROFILE - -profile_id - -identifier_id - -profile + + +PROFILE + +profile_id + +identifier_id + +profile profile:pk_profile_id->token:fk_profile_id - - + + - + profile_scope - - -PROFILE_SCOPE - -profile_id - -scope_id + + +PROFILE_SCOPE + +profile_id + +scope_id profile:pk_profile_id->profile_scope:fk_profile_id - - + + - + scope - - -SCOPE - -scope_id - -scope - -description - -application - -is_permanent - -is_manually_added + + +SCOPE + +scope_id + +scope + +description + +application + +is_permanent + +is_manually_added scope:pk_scope_id->token_scope:fk_scope_id - - + + scope:pk_scope_id->profile_scope:fk_scope_id - - + + - + authentication - - -AUTHENTICATION - -identifier_id - -created - -last_authenticated - -identifier - -credential + + +AUTHENTICATION + +identifier_id + +created + +last_authenticated + +identifier + +credential authentication:pk_identifier_id->profile:fk_identifier_id - - + + - + resource - - -RESOURCE - -resource_id - -description - -created - -secret + + +RESOURCE + +resource_id + +description + +created + +secret - + almanac - - -ALMANAC - -event - -epoch + + +ALMANAC + +event + +epoch diff --git a/package-lock.json b/package-lock.json index f93c9a7..63ffa54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,26 +10,26 @@ "license": "ISC", "dependencies": { "@squeep/amqp-helper": "git+https://git.squeep.com/squeep-amqp-helper#v1.0.0", - "@squeep/api-dingus": "^2.0.0", + "@squeep/api-dingus": "^2.0.1", "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.3.2", - "@squeep/chores": "git+https://git.squeep.com/squeep-chores/#v1.0.0", + "@squeep/chores": "git+https://git.squeep.com/squeep-chores/#v1.0.1", "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0", - "@squeep/indieauth-helper": "^1.3.0", - "@squeep/logger-json-console": "^2.0.1", + "@squeep/indieauth-helper": "^1.4.1", + "@squeep/logger-json-console": "^3.0.1", "@squeep/mystery-box": "^2.0.1", "@squeep/resource-authentication-module": "git+https://git.squeep.com/squeep-resource-authentication-module#v1.0.1", "@squeep/roman": "^1.0.0", "@squeep/web-linking": "^1.0.8", - "better-sqlite3": "^9.1.1", + "better-sqlite3": "^9.2.2", "pg-promise": "^11.5.4", "uuid": "^9.0.1" }, "devDependencies": { - "@squeep/test-helper": "git+https://git.squeep.com/squeep-test-helper#v1.0.0", - "eslint": "^8.53.0", + "@squeep/test-helper": "git+https://git.squeep.com/squeep-test-helper#v1.0.1", + "eslint": "^8.56.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-security": "^1.7.1", + "eslint-plugin-security": "^2.1.0", "eslint-plugin-sonarjs": "^0.23.0", "html-minifier-lint": "^2.0.0", "mocha": "^10.2.0", @@ -78,12 +78,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -162,30 +162,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz", + "integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", + "@babel/helpers": "^7.23.6", + "@babel/parser": "^7.23.6", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -216,12 +216,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.3", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -231,14 +231,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -345,9 +345,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -363,32 +363,32 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", + "integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -471,9 +471,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -497,20 +497,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", - "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", + "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.3", - "@babel/types": "^7.23.3", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -527,12 +527,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -565,9 +565,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -588,9 +588,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -922,12 +922,13 @@ } }, "node_modules/@squeep/api-dingus": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.0.tgz", - "integrity": "sha512-HKz/yB1KNmEcHB92KIvrNvwMph5fSdJBrxKSgERYfyQkLFl2vSwDV+IlFvi68DYmMBP3lWKzQcTXWBMYlW3c0g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.1.tgz", + "integrity": "sha512-b4FWPyHNpn8JtvrTQszukz6mF5OmqhJba0czVffCzhOdbfUk6PKejDRjAtSj4m8fgn4QnvvtAOTHBDvQwNAftw==", "dependencies": { + "@squeep/log-helper": "^1.0.0", "mime-db": "^1.52.0", - "uuid": "^9.0.0" + "uuid": "^9.0.1" }, "engines": { "node": ">=14" @@ -951,10 +952,25 @@ "node-linux-pam": "^0.2.1" } }, + "node_modules/@squeep/authentication-module/node_modules/@squeep/api-dingus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.0.tgz", + "integrity": "sha512-HKz/yB1KNmEcHB92KIvrNvwMph5fSdJBrxKSgERYfyQkLFl2vSwDV+IlFvi68DYmMBP3lWKzQcTXWBMYlW3c0g==", + "dependencies": { + "mime-db": "^1.52.0", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@squeep/chores": { - "version": "1.0.0", - "resolved": "git+https://git.squeep.com/squeep-chores/#d98a3c114eb33bd68477c0ca750e6f82b4d02964", - "license": "ISC" + "version": "1.0.1", + "resolved": "git+https://git.squeep.com/squeep-chores/#a77e8814cbba0ad751e249850d1f7b144da6446b", + "license": "ISC", + "dependencies": { + "@squeep/log-helper": "^1.0.0" + } }, "node_modules/@squeep/html-template-helper": { "version": "1.4.0", @@ -968,15 +984,16 @@ } }, "node_modules/@squeep/indieauth-helper": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@squeep/indieauth-helper/-/indieauth-helper-1.3.0.tgz", - "integrity": "sha512-v7tae925KePhAmtpisQjSQ/xUi3XOGGdruxkhcy8sZpQ0/DOwRdwsuKEDT7aNKVB/STm0rYn8kgiH4SOBRDvWA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@squeep/indieauth-helper/-/indieauth-helper-1.4.1.tgz", + "integrity": "sha512-x/yqrjrbp0vTdvIW8e9CKsr5+YwmDp/x4nK4H5LInt07rk4nthxv7e7qIgy97OFtvFnoNee+N9gyi3TzIIiGoQ==", "dependencies": { + "@squeep/log-helper": "^1.0.0", "@squeep/web-linking": "^1.0.8", "got": "^13.0.0", "iconv": "^3.0.1", - "ip-address": "^8.1.0", - "microformats-parser": "^1.4.1" + "ip-address": "^9.0.5", + "microformats-parser": "^2.0.2" }, "engines": { "node": "^14 >=14.18 || >=15.7" @@ -990,14 +1007,22 @@ "node": ">=14" } }, - "node_modules/@squeep/logger-json-console": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@squeep/logger-json-console/-/logger-json-console-2.0.1.tgz", - "integrity": "sha512-JDlyevhn/JPMPUU6ItJpou3537gXzkSLn4cGm5VvZPrxb6od3I0ls6ywQC3dtaxgH2ZJmFUZwBTXRhZojwvukQ==", + "node_modules/@squeep/log-helper": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@squeep/log-helper/-/log-helper-1.0.0.tgz", + "integrity": "sha512-i61ECZLWQI2rhkXj9pDzH1Md5ICghL9zvh5QFVo0BTayuSrdS9SWkJ6gV1qWki/Xz6SuE0y0y145NyHlvOuVaw==", "engines": { "node": ">=14" } }, + "node_modules/@squeep/logger-json-console": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@squeep/logger-json-console/-/logger-json-console-3.0.1.tgz", + "integrity": "sha512-Po4PPtKHoYHuDyx/PMdHdIByCeArVh2McGCw8IxTaFBf+PCdAosZmDNpreKb2Xw7OVpbkX1u3iOmrVgQWv6aBg==", + "engines": { + "node": ">=17" + } + }, "node_modules/@squeep/mystery-box": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@squeep/mystery-box/-/mystery-box-2.0.1.tgz", @@ -1018,94 +1043,46 @@ "node": "^14 >=14.18 || >=15.7" } }, + "node_modules/@squeep/resource-authentication-module/node_modules/@squeep/api-dingus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.0.tgz", + "integrity": "sha512-HKz/yB1KNmEcHB92KIvrNvwMph5fSdJBrxKSgERYfyQkLFl2vSwDV+IlFvi68DYmMBP3lWKzQcTXWBMYlW3c0g==", + "dependencies": { + "mime-db": "^1.52.0", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@squeep/roman": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@squeep/roman/-/roman-1.0.0.tgz", "integrity": "sha512-D02jDw/we8tc6QiCPor7tWVviY8MLITyp/egqp3XqrrKtsFMYdguAhaFKUmIBu1ZL1uPKgoLBOy8hIptmh8cWA==" }, "node_modules/@squeep/test-helper": { - "version": "1.0.0", - "resolved": "git+https://git.squeep.com/squeep-test-helper#7a5a384abb99757b53c8898c508023f0ba9e94b1", + "version": "1.0.1", + "resolved": "git+https://git.squeep.com/squeep-test-helper#cc0f69b40de9ae3342f1b7a1784d37769e7f1e84", "dev": true, "license": "ISC", "dependencies": { - "eslint": "^8.23.1", + "eslint": "^8.53.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-security": "^1.5.0", - "eslint-plugin-sonarjs": "^0.15.0", - "mocha": "^10.0.0", + "eslint-plugin-security": "^1.7.1", + "eslint-plugin-sonarjs": "^0.23.0", + "mocha": "^10.2.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", - "sinon": "^14.0.0" - } - }, - "node_modules/@squeep/test-helper/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@squeep/test-helper/node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@squeep/test-helper/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@squeep/test-helper/node_modules/@sinonjs/samsam": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", - "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "node_modules/@squeep/test-helper/node_modules/eslint-plugin-sonarjs": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.15.0.tgz", - "integrity": "sha512-LuxHdAe6VqSbi1phsUvNjbmXLuvlobmryQJJNyQYbdubCfz6K8tmgoqNiJPnz0pP2AbYDbtuPm0ajOMgMrC+dQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "sinon": "^17.0.1" } }, - "node_modules/@squeep/test-helper/node_modules/sinon": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", - "integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==", - "deprecated": "16.1.1", + "node_modules/@squeep/test-helper/node_modules/eslint-plugin-security": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz", + "integrity": "sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==", "dev": true, "dependencies": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^7.0.1", - "diff": "^5.0.0", - "nise": "^5.1.2", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" + "safe-regex": "^2.1.1" } }, "node_modules/@squeep/web-linking": { @@ -1401,9 +1378,9 @@ ] }, "node_modules/better-sqlite3": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.1.1.tgz", - "integrity": "sha512-FhW7bS7cXwkB2SFnPJrSGPmQerVSCzwBgmQ1cIRcYKxLsyiKjljzCbyEqqhYXo5TTBqt5BISiBj2YE2Sy2ynaA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.2.2.tgz", + "integrity": "sha512-qwjWB46il0lsDkeB4rSRI96HyDQr8sxeu1MkBVLMrwusq1KRu4Bpt1TMI+8zIJkDUtZ3umjAkaEjIlokZKWCQw==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", @@ -1506,9 +1483,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -1525,9 +1502,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -1648,9 +1625,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "version": "1.0.30001571", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz", + "integrity": "sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==", "dev": true, "funding": [ { @@ -2016,9 +1993,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.580", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.580.tgz", - "integrity": "sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==", + "version": "1.4.616", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", + "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==", "dev": true }, "node_modules/emoji-regex": { @@ -2035,6 +2012,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -2063,15 +2051,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -2178,9 +2166,9 @@ } }, "node_modules/eslint-plugin-security": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz", - "integrity": "sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-2.1.0.tgz", + "integrity": "sha512-ywxclP954bf8d3gr6KOQ/AFc+PRvWuhOxtPOEtiHmVYiZr/mcgQtmSJq6+hTEXC5ylTjHnPPG+PEnzlDiWMXbQ==", "dev": true, "dependencies": { "safe-regex": "^2.1.1" @@ -2349,9 +2337,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -2429,9 +2417,9 @@ } }, "node_modules/flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { "flatted": "^3.2.9", @@ -2439,7 +2427,7 @@ "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -2649,9 +2637,9 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2808,9 +2796,9 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -2861,9 +2849,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "engines": { "node": ">= 4" @@ -2924,12 +2912,12 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/ip-address": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz", - "integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dependencies": { "jsbn": "1.1.0", - "sprintf-js": "1.1.2" + "sprintf-js": "^1.1.3" }, "engines": { "node": ">= 12" @@ -3384,14 +3372,14 @@ } }, "node_modules/microformats-parser": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.5.2.tgz", - "integrity": "sha512-EcHm8zxEm3CggOLgILfxCo2wDiJEOnACzpV/FXWGLaRk24ECei+JkoWNdKdo2vzo/Pww9EvrQNeQsdv4JuHy7Q==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-2.0.2.tgz", + "integrity": "sha512-tUf9DmN4Jq/tGyp1YH2V6D/Cud+9Uc0WhjjUFirqVeHTRkkfLDacv6BQFT7h7HFsD0Z8wja5eKkRgzZU8bv0Fw==", "dependencies": { - "parse5": "^6.0.0" + "parse5": "^7.1.2" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/mime-db": { @@ -3656,9 +3644,9 @@ } }, "node_modules/node-abi": { - "version": "3.51.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", - "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.52.0.tgz", + "integrity": "sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==", "dependencies": { "semver": "^7.3.5" }, @@ -4038,9 +4026,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/nopt": { @@ -4411,9 +4399,15 @@ } }, "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } }, "node_modules/path-exists": { "version": "4.0.0", @@ -5282,9 +5276,9 @@ } }, "node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/string_decoder": { "version": "0.10.31", @@ -5877,12 +5871,12 @@ } }, "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "requires": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "dependencies": { @@ -5945,27 +5939,27 @@ } }, "@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true }, "@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz", + "integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", + "@babel/helpers": "^7.23.6", + "@babel/parser": "^7.23.6", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -5988,26 +5982,26 @@ } }, "@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "requires": { - "@babel/types": "^7.23.3", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" } }, "@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -6086,9 +6080,9 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true }, "@babel/helper-validator-identifier": { @@ -6098,26 +6092,26 @@ "dev": true }, "@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true }, "@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", + "integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", "dev": true, "requires": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6" } }, "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.22.20", @@ -6184,9 +6178,9 @@ } }, "@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true }, "@babel/template": { @@ -6201,20 +6195,20 @@ } }, "@babel/traverse": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", - "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", + "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.3", - "@babel/types": "^7.23.3", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "dependencies": { @@ -6227,12 +6221,12 @@ } }, "@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } @@ -6253,9 +6247,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -6270,9 +6264,9 @@ } }, "@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true }, "@humanwhocodes/config-array": { @@ -6534,12 +6528,13 @@ } }, "@squeep/api-dingus": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.0.tgz", - "integrity": "sha512-HKz/yB1KNmEcHB92KIvrNvwMph5fSdJBrxKSgERYfyQkLFl2vSwDV+IlFvi68DYmMBP3lWKzQcTXWBMYlW3c0g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.1.tgz", + "integrity": "sha512-b4FWPyHNpn8JtvrTQszukz6mF5OmqhJba0czVffCzhOdbfUk6PKejDRjAtSj4m8fgn4QnvvtAOTHBDvQwNAftw==", "requires": { + "@squeep/log-helper": "^1.0.0", "mime-db": "^1.52.0", - "uuid": "^9.0.0" + "uuid": "^9.0.1" } }, "@squeep/authentication-module": { @@ -6552,11 +6547,25 @@ "@squeep/mystery-box": "^2.0.1", "argon2": "^0.31.0", "node-linux-pam": "^0.2.1" + }, + "dependencies": { + "@squeep/api-dingus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.0.tgz", + "integrity": "sha512-HKz/yB1KNmEcHB92KIvrNvwMph5fSdJBrxKSgERYfyQkLFl2vSwDV+IlFvi68DYmMBP3lWKzQcTXWBMYlW3c0g==", + "requires": { + "mime-db": "^1.52.0", + "uuid": "^9.0.0" + } + } } }, "@squeep/chores": { - "version": "git+https://git.squeep.com/squeep-chores/#d98a3c114eb33bd68477c0ca750e6f82b4d02964", - "from": "@squeep/chores@git+https://git.squeep.com/squeep-chores/#v1.0.0" + "version": "git+https://git.squeep.com/squeep-chores/#a77e8814cbba0ad751e249850d1f7b144da6446b", + "from": "@squeep/chores@git+https://git.squeep.com/squeep-chores/#v1.0.1", + "requires": { + "@squeep/log-helper": "^1.0.0" + } }, "@squeep/html-template-helper": { "version": "git+https://git.squeep.com/squeep-html-template-helper#100046316a87631fb8814f80b35647709e6c7319", @@ -6566,15 +6575,16 @@ } }, "@squeep/indieauth-helper": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@squeep/indieauth-helper/-/indieauth-helper-1.3.0.tgz", - "integrity": "sha512-v7tae925KePhAmtpisQjSQ/xUi3XOGGdruxkhcy8sZpQ0/DOwRdwsuKEDT7aNKVB/STm0rYn8kgiH4SOBRDvWA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@squeep/indieauth-helper/-/indieauth-helper-1.4.1.tgz", + "integrity": "sha512-x/yqrjrbp0vTdvIW8e9CKsr5+YwmDp/x4nK4H5LInt07rk4nthxv7e7qIgy97OFtvFnoNee+N9gyi3TzIIiGoQ==", "requires": { + "@squeep/log-helper": "^1.0.0", "@squeep/web-linking": "^1.0.8", "got": "^13.0.0", "iconv": "^3.0.1", - "ip-address": "^8.1.0", - "microformats-parser": "^1.4.1" + "ip-address": "^9.0.5", + "microformats-parser": "^2.0.2" } }, "@squeep/lazy-property": { @@ -6582,10 +6592,15 @@ "resolved": "https://registry.npmjs.org/@squeep/lazy-property/-/lazy-property-1.1.2.tgz", "integrity": "sha512-wRdR4IOqWXoDMArx0HPo5MtM2Wk5wemAULbZ6PabVw1ylSQekkzKfoAUuupxsKuzjcRPjZvbpGDv+i04hBMnQw==" }, + "@squeep/log-helper": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@squeep/log-helper/-/log-helper-1.0.0.tgz", + "integrity": "sha512-i61ECZLWQI2rhkXj9pDzH1Md5ICghL9zvh5QFVo0BTayuSrdS9SWkJ6gV1qWki/Xz6SuE0y0y145NyHlvOuVaw==" + }, "@squeep/logger-json-console": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@squeep/logger-json-console/-/logger-json-console-2.0.1.tgz", - "integrity": "sha512-JDlyevhn/JPMPUU6ItJpou3537gXzkSLn4cGm5VvZPrxb6od3I0ls6ywQC3dtaxgH2ZJmFUZwBTXRhZojwvukQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@squeep/logger-json-console/-/logger-json-console-3.0.1.tgz", + "integrity": "sha512-Po4PPtKHoYHuDyx/PMdHdIByCeArVh2McGCw8IxTaFBf+PCdAosZmDNpreKb2Xw7OVpbkX1u3iOmrVgQWv6aBg==" }, "@squeep/mystery-box": { "version": "2.0.1", @@ -6598,6 +6613,17 @@ "requires": { "@squeep/api-dingus": "v2.0.0", "uuid": "^9.0.0" + }, + "dependencies": { + "@squeep/api-dingus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.0.0.tgz", + "integrity": "sha512-HKz/yB1KNmEcHB92KIvrNvwMph5fSdJBrxKSgERYfyQkLFl2vSwDV+IlFvi68DYmMBP3lWKzQcTXWBMYlW3c0g==", + "requires": { + "mime-db": "^1.52.0", + "uuid": "^9.0.0" + } + } } }, "@squeep/roman": { @@ -6606,79 +6632,27 @@ "integrity": "sha512-D02jDw/we8tc6QiCPor7tWVviY8MLITyp/egqp3XqrrKtsFMYdguAhaFKUmIBu1ZL1uPKgoLBOy8hIptmh8cWA==" }, "@squeep/test-helper": { - "version": "git+https://git.squeep.com/squeep-test-helper#7a5a384abb99757b53c8898c508023f0ba9e94b1", + "version": "git+https://git.squeep.com/squeep-test-helper#cc0f69b40de9ae3342f1b7a1784d37769e7f1e84", "dev": true, - "from": "@squeep/test-helper@git+https://git.squeep.com/squeep-test-helper#v1.0.0", + "from": "@squeep/test-helper@git+https://git.squeep.com/squeep-test-helper#v1.0.1", "requires": { - "eslint": "^8.23.1", + "eslint": "^8.53.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-security": "^1.5.0", - "eslint-plugin-sonarjs": "^0.15.0", - "mocha": "^10.0.0", + "eslint-plugin-security": "^1.7.1", + "eslint-plugin-sonarjs": "^0.23.0", + "mocha": "^10.2.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", - "sinon": "^14.0.0" + "sinon": "^17.0.1" }, "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "@sinonjs/samsam": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", - "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "eslint-plugin-sonarjs": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.15.0.tgz", - "integrity": "sha512-LuxHdAe6VqSbi1phsUvNjbmXLuvlobmryQJJNyQYbdubCfz6K8tmgoqNiJPnz0pP2AbYDbtuPm0ajOMgMrC+dQ==", - "dev": true, - "requires": {} - }, - "sinon": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", - "integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==", + "eslint-plugin-security": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz", + "integrity": "sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==", "dev": true, "requires": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^7.0.1", - "diff": "^5.0.0", - "nise": "^5.1.2", - "supports-color": "^7.2.0" + "safe-regex": "^2.1.1" } } } @@ -6892,9 +6866,9 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "better-sqlite3": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.1.1.tgz", - "integrity": "sha512-FhW7bS7cXwkB2SFnPJrSGPmQerVSCzwBgmQ1cIRcYKxLsyiKjljzCbyEqqhYXo5TTBqt5BISiBj2YE2Sy2ynaA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.2.2.tgz", + "integrity": "sha512-qwjWB46il0lsDkeB4rSRI96HyDQr8sxeu1MkBVLMrwusq1KRu4Bpt1TMI+8zIJkDUtZ3umjAkaEjIlokZKWCQw==", "requires": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -6975,14 +6949,14 @@ "dev": true }, "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" } }, @@ -7065,9 +7039,9 @@ "devOptional": true }, "caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "version": "1.0.30001571", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz", + "integrity": "sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==", "dev": true }, "chalk": { @@ -7337,9 +7311,9 @@ } }, "electron-to-chromium": { - "version": "1.4.580", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.580.tgz", - "integrity": "sha512-T5q3pjQon853xxxHUq3ZP68ZpvJHuSMY2+BZaW3QzjS4HvNuvsMmZ/+lU+nCrftre1jFZ+OSlExynXWBihnXzw==", + "version": "1.4.616", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", + "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==", "dev": true }, "emoji-regex": { @@ -7356,6 +7330,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -7375,15 +7354,15 @@ "dev": true }, "eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -7460,9 +7439,9 @@ "requires": {} }, "eslint-plugin-security": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz", - "integrity": "sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-2.1.0.tgz", + "integrity": "sha512-ywxclP954bf8d3gr6KOQ/AFc+PRvWuhOxtPOEtiHmVYiZr/mcgQtmSJq6+hTEXC5ylTjHnPPG+PEnzlDiWMXbQ==", "dev": true, "requires": { "safe-regex": "^2.1.1" @@ -7579,9 +7558,9 @@ "dev": true }, "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -7638,9 +7617,9 @@ "dev": true }, "flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "requires": { "flatted": "^3.2.9", @@ -7794,9 +7773,9 @@ } }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -7913,9 +7892,9 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "requires": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -7942,9 +7921,9 @@ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true }, "import-fresh": { @@ -7990,12 +7969,12 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "ip-address": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz", - "integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "requires": { "jsbn": "1.1.0", - "sprintf-js": "1.1.2" + "sprintf-js": "^1.1.3" } }, "is-binary-path": { @@ -8338,11 +8317,11 @@ } }, "microformats-parser": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.5.2.tgz", - "integrity": "sha512-EcHm8zxEm3CggOLgILfxCo2wDiJEOnACzpV/FXWGLaRk24ECei+JkoWNdKdo2vzo/Pww9EvrQNeQsdv4JuHy7Q==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-2.0.2.tgz", + "integrity": "sha512-tUf9DmN4Jq/tGyp1YH2V6D/Cud+9Uc0WhjjUFirqVeHTRkkfLDacv6BQFT7h7HFsD0Z8wja5eKkRgzZU8bv0Fw==", "requires": { - "parse5": "^6.0.0" + "parse5": "^7.1.2" } }, "mime-db": { @@ -8559,9 +8538,9 @@ } }, "node-abi": { - "version": "3.51.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", - "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.52.0.tgz", + "integrity": "sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==", "requires": { "semver": "^7.3.5" } @@ -8868,9 +8847,9 @@ } }, "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "nopt": { @@ -9156,9 +9135,12 @@ } }, "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } }, "path-exists": { "version": "4.0.0", @@ -9791,9 +9773,9 @@ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "string_decoder": { "version": "0.10.31", diff --git a/package.json b/package.json index 85ec0be..f594e63 100644 --- a/package.json +++ b/package.json @@ -33,26 +33,26 @@ "license": "ISC", "dependencies": { "@squeep/amqp-helper": "git+https://git.squeep.com/squeep-amqp-helper#v1.0.0", - "@squeep/api-dingus": "^2.0.0", + "@squeep/api-dingus": "^2.0.1", "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.3.2", - "@squeep/chores": "git+https://git.squeep.com/squeep-chores/#v1.0.0", + "@squeep/chores": "git+https://git.squeep.com/squeep-chores/#v1.0.1", "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0", - "@squeep/indieauth-helper": "^1.3.0", - "@squeep/logger-json-console": "^2.0.1", + "@squeep/indieauth-helper": "^1.4.1", + "@squeep/logger-json-console": "^3.0.1", "@squeep/mystery-box": "^2.0.1", "@squeep/resource-authentication-module": "git+https://git.squeep.com/squeep-resource-authentication-module#v1.0.1", "@squeep/roman": "^1.0.0", "@squeep/web-linking": "^1.0.8", - "better-sqlite3": "^9.1.1", + "better-sqlite3": "^9.2.2", "pg-promise": "^11.5.4", "uuid": "^9.0.1" }, "devDependencies": { - "@squeep/test-helper": "git+https://git.squeep.com/squeep-test-helper#v1.0.0", - "eslint": "^8.53.0", + "@squeep/test-helper": "git+https://git.squeep.com/squeep-test-helper#v1.0.1", + "eslint": "^8.56.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-security": "^1.7.1", + "eslint-plugin-security": "^2.1.0", "eslint-plugin-sonarjs": "^0.23.0", "html-minifier-lint": "^2.0.0", "mocha": "^10.2.0", diff --git a/src/chores.js b/src/chores.js index 0f3d379..4002158 100644 --- a/src/chores.js +++ b/src/chores.js @@ -10,13 +10,15 @@ const _fileScope = common.fileScope(__filename); */ class Chores extends BaseChores { - constructor(logger, db, options) { + constructor(logger, db, queuePublisher, options) { super(logger); this.options = options; this.db = db; + this.queuePublisher = queuePublisher; this.establishChore(Enum.Chore.CleanTokens, this.cleanTokens.bind(this), options?.chores?.tokenCleanupMs); this.establishChore(Enum.Chore.CleanScopes, this.cleanScopes.bind(this), options?.chores?.scopeCleanupMs); + this.establishChore(Enum.Chore.PublishTickets, this.publishTickets.bind(this), options?.chores?.publishTicketsMs); } /** @@ -25,7 +27,7 @@ class Chores extends BaseChores { */ async cleanTokens(atLeastMsSinceLast = this.options?.chores?.tokenCleanupMs || 0) { const _scope = _fileScope('cleanTokens'); - this.logger.debug(_scope, 'called', atLeastMsSinceLast); + this.logger.debug(_scope, 'called', { atLeastMsSinceLast }); let tokensCleaned; try { @@ -49,7 +51,7 @@ class Chores extends BaseChores { */ async cleanScopes(atLeastMsSinceLast = this.options?.chores?.scopeCleanupMs || 0) { const _scope = _fileScope('cleanScopes'); - this.logger.debug(_scope, 'called', atLeastMsSinceLast); + this.logger.debug(_scope, 'called', { atLeastMsSinceLast }); let scopesCleaned; try { @@ -65,6 +67,34 @@ class Chores extends BaseChores { } } -} // IAChores -module.exports = Chores; \ No newline at end of file + /** + * Attempt to deliver any redeemed but un-delivered ticket tokens. + */ + async publishTickets() { + const _scope = _fileScope('publishTickets'); + this.logger.debug(_scope, 'called'); + + try { + const queueName = this.options.queues.ticketRedeemedName; + await this.db.context(async (dbCtx) => { + const ticketTokens = await this.db.ticketTokenGetUnpublished(dbCtx); + for await (const data of ticketTokens) { + try { + const result = await this.queuePublisher.publish(queueName, data); + this.logger.info(_scope, 'published ticket token', { queueName, result, ...data }); + const redeemedData = common.pick(data, ['resource', 'subject', 'iss', 'ticket', 'token']); + await this.db.ticketTokenPublished(dbCtx, redeemedData); + } catch (e) { + this.logger.error(_scope, 'publish failed', { error: e, data }); + } + } + }); // dbCtx + } catch (e) { + this.logger.error(_scope, 'failed', { error: e }); + throw e; + } + } +} // Chores + +module.exports = Chores; diff --git a/src/db/abstract.js b/src/db/abstract.js index 2e9f46d..d5367df 100644 --- a/src/db/abstract.js +++ b/src/db/abstract.js @@ -47,7 +47,7 @@ class Database { /** * Query the current schema version. * This is a standalone query function, as it is called before statements are loaded. - * @returns {SchemaVersionObject} + * @returns {Promise} */ async _currentSchema() { this._notImplemented('_currentSchema', arguments); @@ -251,6 +251,17 @@ class Database { } + /** + * Insert or update an almanac entry. + * @param {*} dbCtx + * @param {String} event + * @param {Date=} date + */ + async almanacUpsert(dbCtx, event, date) { + this._notImplemented('almanacUpsert', arguments); + } + + /** * Fetch the authentication record for an identifier. * @param {*} dbCtx @@ -580,6 +591,44 @@ class Database { this._notImplemented('tokensGetByIdentifier', arguments); } + + /** @typedef {Object} RedeemedTicketData + * @property {String} subject + * @property {String} resource + * @property {String=} iss + * @property {String} ticket + * @property {String} token + */ + /** + * Persist details of a redeemed ticket. + * @param {*} dbCtx + * @param {RedeemedTicketData} redeemedData + * @returns {Promise} + */ + async ticketRedeemed(dbCtx, redeemedData) { + this._notImplemented('ticketRedeemed', arguments); + } + + + /** + * Update details of a redeemed ticket that it has been published. + * @param {*} dbCtx + * @param {RedeemedTicketData} redeemedData + * @returns {Promise} + */ + async ticketTokenPublished(dbCtx, redeemedData) { + this._notImplemented('ticketTokenPublished', arguments); + } + + /** + * Retrieve redeemed tokens which have not yet been published to queue. + * @param {Number} limit + * @returns {Promise} + */ + async ticketTokenGetUnpublished(dbCtx, limit) { + this._notImplemented('ticketTokenGetUnpublished', arguments); + } + } -module.exports = Database; \ No newline at end of file +module.exports = Database; diff --git a/src/db/postgres/index.js b/src/db/postgres/index.js index 3a536f4..535a901 100644 --- a/src/db/postgres/index.js +++ b/src/db/postgres/index.js @@ -11,6 +11,7 @@ const { unappliedSchemaVersions } = require('../schema-version-helper'); const Database = require('../abstract'); const DBErrors = require('../errors'); const common = require('../../common'); +const Enum = require('../../enum'); const _fileScope = common.fileScope(__filename); @@ -28,7 +29,7 @@ const schemaVersionsSupported = { }, max: { major: 1, - minor: 0, + minor: 1, patch: 0, }, }; @@ -197,9 +198,11 @@ class DatabasePostgres extends Database { if (really) { await this.db.tx(async (t) => { await t.batch([ + 'almanac', 'authentication', - 'resource', 'profile', + 'redeemed_ticket', + 'resource', 'token', ].map(async (table) => t.query('TRUNCATE TABLE $(table:name) CASCADE', { table }))); }); @@ -235,6 +238,22 @@ class DatabasePostgres extends Database { } + async almanacUpsert(dbCtx, event, date) { + const _scope = _fileScope('almanacUpsert'); + this.logger.debug(_scope, 'called', { event, date }); + + try { + const result = await dbCtx.result(this.statement.almanacUpsert, { event, date: date ?? new Date() }); + if (result.rowCount != 1) { + throw new DBErrors.UnexpectedResult('did not upsert almanac event'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, event, date }); + throw e; + } + } + + async authenticationGet(dbCtx, identifier) { const _scope = _fileScope('authenticationGet'); this.logger.debug(_scope, 'called', { identifier }); @@ -464,7 +483,7 @@ class DatabasePostgres extends Database { const _scope = _fileScope('scopeCleanup'); this.logger.debug(_scope, 'called', { atLeastMsSinceLast }); - const almanacEvent = 'scopeCleanup'; + const almanacEvent = Enum.AlmanacEntry.ScopeCleanup; try { return await this.transaction(dbCtx, async (txCtx) => { @@ -543,7 +562,7 @@ class DatabasePostgres extends Database { const _scope = _fileScope('tokenCleanup'); this.logger.debug(_scope, 'called', { codeLifespanSeconds, atLeastMsSinceLast }); - const almanacEvent = 'tokenCleanup'; + const almanacEvent = Enum.AlmanacEntry.TokenCleanup; try { return await this.transaction(dbCtx, async (txCtx) => { @@ -633,6 +652,55 @@ class DatabasePostgres extends Database { } } + + async ticketRedeemed(dbCtx, redeemedData) { + const _scope = _fileScope('ticketRedeemed'); + this.logger.debug(_scope, 'called', { ...redeemedData }); + + try { + const result = await dbCtx.result(this.statement.ticketRedeemed, redeemedData); + if (result.rowCount != 1) { + throw new DBErrors.UnexpectedResult('did not store redeemed ticket'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, ...redeemedData }); + throw e; + } + } + + + async ticketTokenPublished(dbCtx, redeemedData) { + const _scope = _fileScope('ticketRedeemed'); + this.logger.debug(_scope, 'called', { ...redeemedData }); + + const almanacEvent = Enum.AlmanacEntry.TicketPublished; + try { + const result = await dbCtx.result(this.statement.ticketTokenPublished, redeemedData); + if (result.rowCount != 1) { + throw new DBErrors.UnexpectedResult('did not store redeemed ticket'); + } + const almanacResult = await dbCtx.result(this.statement.almanacUpsert, { event: almanacEvent, date: new Date() }); + if (almanacResult.rowCount != 1) { + throw new DBErrors.UnexpectedResult('did not update almanac'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, ...redeemedData }); + throw e; + } + } + + async ticketTokenGetUnpublished(dbCtx) { + const _scope = _fileScope('ticketTokenGetUnpublished'); + this.logger.debug(_scope, 'called'); + + try { + return await dbCtx.manyOrNone(this.statement.ticketTokenGetUnpublished); + } catch (e) { + this.logger.error(_scope, 'failed', { error: e }); + throw e; + } + } + } module.exports = DatabasePostgres; diff --git a/src/db/postgres/sql/schema/1.1.0/apply.sql b/src/db/postgres/sql/schema/1.1.0/apply.sql new file mode 100644 index 0000000..8ccc2a5 --- /dev/null +++ b/src/db/postgres/sql/schema/1.1.0/apply.sql @@ -0,0 +1,23 @@ +BEGIN; + + CREATE TABLE redeemed_ticket ( + ticket_id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + subject TEXT NOT NULL, + resource TEXT NOT NULL, + iss TEXT, + token TEXT NOT NULL, + ticket TEXT NOT NULL, + published TIMESTAMP WITH TIME ZONE + ); + COMMENT ON TABLE redeemed_ticket IS $docstring$ +Tickets which have been redeemed for tokens and published. + $docstring$; + CREATE INDEX redeemed_ticket_created_idx ON redeemed_ticket(created); + CREATE INDEX redeemed_ticket_published_idx ON redeemed_ticket(published) WHERE published IS NOT NULL; + CREATE INDEX redeemed_ticket_ref_idx ON redeemed_ticket(subject, resource, iss, ticket); + + -- Update schema version + INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 1, 0); + +COMMIT; diff --git a/src/db/postgres/sql/schema/1.1.0/er.dot b/src/db/postgres/sql/schema/1.1.0/er.dot new file mode 100644 index 0000000..087c21a --- /dev/null +++ b/src/db/postgres/sql/schema/1.1.0/er.dot @@ -0,0 +1,120 @@ +digraph indieAutherERD { + graph[ + rankdir=LR, + overlap=false, + splines=true, + label="IndieAuther Entity-Relations\nPostgres\nSchema 1.1.0", + labelloc="t", + fontsize=26, + ]; + // layout=neato; + node[shape=plain]; + edge[arrowhead=crow]; + + redeemed_ticket [label=< + + + + + + + + + + +
REDEEMED_TICKET
ticket_id
created
subject
resource
iss
token
ticket
published
+ >] + + token [label=< + + + + + + + + + + + + + + + + +
TOKEN
code_id
profile_id
created
expires
refresh_expires
refreshed
duration
refresh_duration
refresh_count
is_revoked
is_token
client_id
resource
profile_data
+ >]; + profile:pk_profile_id -> token:fk_profile_id; + + scope [label=< + + + + + + + + +
SCOPE
scope_id
scope
description
application
is_permanent
is_manually_added
+ >]; + + token_scope [label=< + + + + +
TOKEN_SCOPE
code_id
scope_id
+ >]; + token:pk_code_id -> token_scope:fk_code_id; + scope:pk_scope_id -> token_scope:fk_scope_id; + + profile [label=< + + + + + +
PROFILE
profile_id
identifier_id
profile
+ >]; + authentication:pk_identifier_id -> profile:fk_identifier_id; + + profile_scope [label=< + + + + +
PROFILE_SCOPE
profile_id
scope_id
+ >]; + profile:pk_profile_id -> profile_scope:fk_profile_id; + scope:pk_scope_id -> profile_scope:fk_scope_id; + + authentication [label=< + + + + + + + +
AUTHENTICATION
identifier_id
created
last_authenticated
identifier
credential
+ >]; + + resource [label=< + + + + + + +
RESOURCE
resource_id
description
created
secret
+ >]; + + almanac [label=< + + + + +
ALMANAC
event
date
+ >]; + +} diff --git a/src/db/postgres/sql/schema/1.1.0/revert.sql b/src/db/postgres/sql/schema/1.1.0/revert.sql new file mode 100644 index 0000000..8d70e0c --- /dev/null +++ b/src/db/postgres/sql/schema/1.1.0/revert.sql @@ -0,0 +1,5 @@ +BEGIN; + DROP TABLE redeemed_ticket CASCADE; + + DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0; +COMMIT; diff --git a/src/db/postgres/sql/ticket-redeemed.sql b/src/db/postgres/sql/ticket-redeemed.sql new file mode 100644 index 0000000..06e49fc --- /dev/null +++ b/src/db/postgres/sql/ticket-redeemed.sql @@ -0,0 +1,5 @@ +-- +INSERT INTO redeemed_ticket + (subject, resource, iss, token, ticket) +VALUES + ($(subject), $(resource), $(iss), $(token), $(ticket)) diff --git a/src/db/postgres/sql/ticket-token-get-unpublished.sql b/src/db/postgres/sql/ticket-token-get-unpublished.sql new file mode 100644 index 0000000..425fc3e --- /dev/null +++ b/src/db/postgres/sql/ticket-token-get-unpublished.sql @@ -0,0 +1,4 @@ +-- +SELECT * FROM redeemed_ticket +WHERE + published IS NULL diff --git a/src/db/postgres/sql/ticket-token-published.sql b/src/db/postgres/sql/ticket-token-published.sql new file mode 100644 index 0000000..9c2e542 --- /dev/null +++ b/src/db/postgres/sql/ticket-token-published.sql @@ -0,0 +1,11 @@ +-- +UPDATE redeemed_ticket SET + published = now() +WHERE + subject = $(subject) +AND + resource = $(resource) +AND + iss = $(iss) +AND + ticket = $(ticket) diff --git a/src/db/sqlite/index.js b/src/db/sqlite/index.js index 1858a65..453dbf8 100644 --- a/src/db/sqlite/index.js +++ b/src/db/sqlite/index.js @@ -1,6 +1,7 @@ 'use strict'; const common = require('../../common'); +const Enum = require('../../enum'); const Database = require('../abstract'); const DBErrors = require('../errors'); const { unappliedSchemaVersions } = require('../schema-version-helper'); @@ -20,7 +21,7 @@ const schemaVersionsSupported = { }, max: { major: 1, - minor: 0, + minor: 1, patch: 0, }, }; @@ -233,10 +234,13 @@ class DatabaseSQLite extends Database { _purgeTables(really) { if (really) { [ + 'almanac', 'authentication', 'profile', + 'redeemed_ticket', + 'resource', 'token', - ].map((table) => { + ].forEach((table) => { const result = this.db.prepare(`DELETE FROM ${table}`).run(); this.logger.debug(_fileScope('_purgeTables'), 'success', { table, result }); }); @@ -262,6 +266,7 @@ class DatabaseSQLite extends Database { }; } + almanacGetAll(dbCtx) { // eslint-disable-line no-unused-vars const _scope = _fileScope('almanacGetAll'); this.logger.debug(_scope, 'called'); @@ -276,6 +281,23 @@ class DatabaseSQLite extends Database { } + almanacUpsert(dbCtx, event, date) { + const _scope = _fileScope('almanacUpsert'); + this.logger.debug(_scope, 'called', { event, date }); + + try { + const epoch = common.dateToEpoch(date); + const result = this.statement.almanacUpsert.run({ event, epoch }); + if (result.changes != 1) { + throw new DBErrors.UnexpectedResult('did not upsert almanac event'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, event, date }); + throw e; + } + } + + static _authenticationToNative(authentication) { if (authentication) { authentication.created = new Date(Number(authentication.created) * 1000); @@ -539,7 +561,7 @@ class DatabaseSQLite extends Database { const _scope = _fileScope('scopeCleanup'); this.logger.debug(_scope, 'called', { atLeastMsSinceLast }); - const almanacEvent = 'scopeCleanup'; + const almanacEvent = Enum.AlmanacEntry.ScopeCleanup; try { return this.db.transaction(() => { @@ -617,7 +639,7 @@ class DatabaseSQLite extends Database { const _scope = _fileScope('tokenCleanup'); this.logger.debug(_scope, 'called', { codeLifespanSeconds, atLeastMsSinceLast }); - const almanacEvent = 'tokenCleanup'; + const almanacEvent = Enum.AlmanacEntry.TokenCleanup; try { return this.db.transaction(() => { @@ -734,6 +756,66 @@ class DatabaseSQLite extends Database { } } + + ticketRedeemed(dbCtx, redeemedData) { + const _scope = _fileScope('ticketRedeemed'); + this.logger.debug(_scope, 'called', { ...redeemedData }); + + try { + const result = this.statement.ticketRedeemed.run(redeemedData); + if (result.changes != 1) { + throw new DBErrors.UnexpectedResult('did not store redeemed ticket'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e }); + throw e; + } + } + + + ticketTokenPublished(dbCtx, redeemedData) { + const _scope = _fileScope('ticketRedeemed'); + this.logger.debug(_scope, 'called', { ...redeemedData }); + + const almanacEvent = Enum.AlmanacEntry.TicketPublished; + try { + const result = this.statement.ticketTokenPublished.run(redeemedData); + if (result.changes != 1) { + throw new DBErrors.UnexpectedResult('did not store redeemed ticket'); + } + const epoch = common.dateToEpoch(); + const almanacResult = this.statement.almanacUpsert.run({ event: almanacEvent, epoch }); + if (almanacResult.changes != 1) { + throw new DBErrors.UnexpectedResult('did not update almanac'); + } + + } catch (e) { + this.logger.error(_scope, 'failed', { error: e }); + throw e; + } + } + + static _redeemedTicketToNative(redeemedTicket) { + redeemedTicket.created = new Date(Number(redeemedTicket.created) * 1000); + if (redeemedTicket.published) { + redeemedTicket.published = new Date(Number(redeemedTicket.published) * 1000); + } + return redeemedTicket; + } + + ticketTokenGetUnpublished() { + const _scope = _fileScope('ticketTokenGetUnpublished'); + this.logger.debug(_scope, 'called'); + + try { + const unpublished = this.statement.ticketTokenGetUnpublished.all(); + return unpublished.map((x) => DatabaseSQLite._redeemedTicketToNative(x)); + } catch (e) { + this.logger.error(_scope, 'failed', { error: e }); + throw e; + } + } + } -module.exports = DatabaseSQLite; \ No newline at end of file +module.exports = DatabaseSQLite; diff --git a/src/db/sqlite/sql/schema/1.0.0/revert.sql b/src/db/sqlite/sql/schema/1.0.0/revert.sql index 20ccf68..9ef50c2 100644 --- a/src/db/sqlite/sql/schema/1.0.0/revert.sql +++ b/src/db/sqlite/sql/schema/1.0.0/revert.sql @@ -4,4 +4,6 @@ BEGIN; DROP TABLE token; DROP TABLE scope; DROP TABLE profile_scope; + + DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 0; COMMIT; \ No newline at end of file diff --git a/src/db/sqlite/sql/schema/1.1.0/apply.sql b/src/db/sqlite/sql/schema/1.1.0/apply.sql new file mode 100644 index 0000000..81ebc0d --- /dev/null +++ b/src/db/sqlite/sql/schema/1.1.0/apply.sql @@ -0,0 +1,20 @@ +BEGIN; + + CREATE TABLE redeemed_ticket ( + ticket_id INTEGER NOT NULL PRIMARY KEY CHECK (typeof(ticket_id) = 'integer'), + created INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) CHECK (typeof(created) = 'integer'), + subject TEXT NOT NULL CHECK (typeof(subject) = 'text'), + resource TEXT NOT NULL CHECK (typeof(resource) = 'text'), + iss TEXT CHECK (typeof(iss) = 'text'), + token TEXT NOT NULL CHECK (typeof(token) = 'text'), + ticket TEXT NOT NULL CHECK (typeof(ticket) = 'text'), + published INTEGER CHECK (typeof(published) IN ('integer', 'null')) + ); + CREATE INDEX redeemed_ticket_created_idx ON redeemed_ticket(created); + CREATE INDEX redeemed_ticket_published_idx ON redeemed_ticket(published) WHERE published IS NOT NULL; + CREATE INDEX redeemed_ticket_ref_idx ON redeemed_ticket(subject, resource, iss, ticket); + + -- Update schema version + INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 1, 0); + +COMMIT; diff --git a/src/db/sqlite/sql/schema/1.1.0/er.dot b/src/db/sqlite/sql/schema/1.1.0/er.dot new file mode 100644 index 0000000..1925993 --- /dev/null +++ b/src/db/sqlite/sql/schema/1.1.0/er.dot @@ -0,0 +1,120 @@ +digraph indieAutherERD { + graph[ + rankdir=LR, + overlap=false, + splines=true, + label="IndieAuther Entity-Relations\SQLite\nSchema 1.1.0", + labelloc="t", + fontsize=26, + ]; + // layout=neato; + node[shape=plain]; + edge[arrowhead=crow]; + + redeemed_ticket [label=< + + + + + + + + + + +
REDEEMED_TICKET
ticket_id
created
subject
resource
iss
token
ticket
published
+ >] + + token [label=< + + + + + + + + + + + + + + + + +
TOKEN
code_id
profile_id
created
expires
refresh_expires
refreshed
duration
refresh_duration
refresh_count
is_revoked
is_token
client_id
resource
profile_data
+ >]; + profile:pk_profile_id -> token:fk_profile_id; + + scope [label=< + + + + + + + + +
SCOPE
scope_id
scope
description
application
is_permanent
is_manually_added
+ >]; + + token_scope [label=< + + + + +
TOKEN_SCOPE
code_id
scope_id
+ >]; + token:pk_code_id -> token_scope:fk_code_id; + scope:pk_scope_id -> token_scope:fk_scope_id; + + profile [label=< + + + + + +
PROFILE
profile_id
identifier_id
profile
+ >]; + authentication:pk_identifier_id -> profile:fk_identifier_id; + + profile_scope [label=< + + + + +
PROFILE_SCOPE
profile_id
scope_id
+ >]; + profile:pk_profile_id -> profile_scope:fk_profile_id; + scope:pk_scope_id -> profile_scope:fk_scope_id; + + authentication [label=< + + + + + + + +
AUTHENTICATION
identifier_id
created
last_authenticated
identifier
credential
+ >]; + + resource [label=< + + + + + + +
RESOURCE
resource_id
description
created
secret
+ >]; + + almanac [label=< + + + + +
ALMANAC
event
epoch
+ >]; + +} diff --git a/src/db/sqlite/sql/schema/1.1.0/revert.sql b/src/db/sqlite/sql/schema/1.1.0/revert.sql new file mode 100644 index 0000000..8d70e0c --- /dev/null +++ b/src/db/sqlite/sql/schema/1.1.0/revert.sql @@ -0,0 +1,5 @@ +BEGIN; + DROP TABLE redeemed_ticket CASCADE; + + DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0; +COMMIT; diff --git a/src/db/sqlite/sql/ticket-redeemed.sql b/src/db/sqlite/sql/ticket-redeemed.sql new file mode 100644 index 0000000..6e00cbe --- /dev/null +++ b/src/db/sqlite/sql/ticket-redeemed.sql @@ -0,0 +1,6 @@ +-- +INSERT INTO redeemed_ticket + (subject, resource, iss, ticket, token) +VALUES + (:subject, :resource, :iss, :ticket, :token) + diff --git a/src/db/sqlite/sql/ticket-token-get-unpublished.sql b/src/db/sqlite/sql/ticket-token-get-unpublished.sql new file mode 100644 index 0000000..425fc3e --- /dev/null +++ b/src/db/sqlite/sql/ticket-token-get-unpublished.sql @@ -0,0 +1,4 @@ +-- +SELECT * FROM redeemed_ticket +WHERE + published IS NULL diff --git a/src/db/sqlite/sql/ticket-token-published.sql b/src/db/sqlite/sql/ticket-token-published.sql new file mode 100644 index 0000000..077c24f --- /dev/null +++ b/src/db/sqlite/sql/ticket-token-published.sql @@ -0,0 +1,11 @@ +-- +UPDATE redeemed_ticket SET + published = (strftime('%s', 'now')) +WHERE + subject = :subject +AND + resource = :resource +AND + iss = :iss +AND + ticket = :ticket diff --git a/src/enum.js b/src/enum.js index 8179941..8e8a4f1 100644 --- a/src/enum.js +++ b/src/enum.js @@ -21,6 +21,13 @@ common.mergeEnum(Enum, { Chore: { CleanTokens: 'cleanTokens', CleanScopes: 'cleanScopes', + PublishTickets: 'publishTickets', + }, + + AlmanacEntry: { + TokenCleanup: 'tokenCleanup', + ScopeCleanup: 'scopeCleanup', + TicketPublished: 'ticketPublished', }, }); diff --git a/src/manager.js b/src/manager.js index 2b64ed1..c79c752 100644 --- a/src/manager.js +++ b/src/manager.js @@ -8,7 +8,7 @@ const Template = require('./template'); const { MysteryBox } = require('@squeep/mystery-box'); const DBErrors = require('./db/errors'); const Chores = require('./chores'); -const { Publisher: QueuePublisher } = require('@squeep/amqp-helper'); +const { Publisher: QueuePublisher, Consumer: QueueConsumer } = require('@squeep/amqp-helper'); const _fileScope = common.fileScope(__filename); @@ -23,11 +23,12 @@ class Manager { this.options = options; this.logger = logger; this.db = db; - this.chores = new Chores(logger, db, options); this.communication = new Communication(logger, options); if (options.queues.amqp.url) { this.queuePublisher = new QueuePublisher(logger, options.queues.amqp); + this.queueConsumer = new QueueConsumer(logger, options.queues.amqp); } + this.chores = new Chores(logger, db, this.queuePublisher, options); this.mysteryBox = new MysteryBox(options); this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name))); @@ -51,6 +52,12 @@ class Manager { async _connectQueues() { await this.queuePublisher.connect(); await this.queuePublisher.establishAMQPPlumbing(this.options.queues.ticketPublishName); + await this.queuePublisher.establishAMQPPlumbing(this.options.queues.ticketRedeemedName); + + await this.queueConsumer.connect(); + await this.queueConsumer.establishAMQPPlumbing(this.options.queues.ticketPublishName); + const boundTicketProcessor = this.queuedTicketProcessor.bind(this); + await this.queueConsumer.consume(this.options.queues.ticketPublishName, boundTicketProcessor); } @@ -578,7 +585,7 @@ class Manager { /** * Get up-to-date profile data from selected profile endpoint. * @param {Object} ctx - * @returns {Object} + * @returns {Promise} */ async _fetchConsentProfileData(ctx) { const _scope = _fileScope('_fetchConsentProfileData'); @@ -1188,7 +1195,7 @@ class Manager { * @param {String} payload.identifier user generating ticket * @param {} payload.profile profile of user generating ticket * @param {Number} payload.ticketLifespanSeconds ticket redeemable for this long - * @returns {String} + * @returns {Promise} */ async _mintTicket({ subject, resource, scopes, identifier, profile, ticketLifespanSeconds }) { const _scope = _fileScope('_mintTicket'); @@ -1222,7 +1229,7 @@ class Manager { /** * * @param {String} ticket - * @returns {Ticket} + * @returns {Promise} */ async _unpackTicket(ticket) { const ticketObj = await this.mysteryBox.unpack(ticket); @@ -1309,7 +1316,7 @@ class Manager { this.logger.debug(_scope, 'called', { ctx }); await this._restoreSessionFromCode(ctx); - await this._checkSessionMatchingRedirectUri(ctx); + this._checkSessionMatchingRedirectUri(ctx); if (ctx.session.error) { throw new ResponseError(Enum.ErrorResponse.BadRequest); @@ -1510,8 +1517,16 @@ class Manager { } const queueName = this.options.queues.ticketPublishName; - const { ticket, resource, subject } = ctx.parsedBody; + const { ticket, resource, subject, iss } = ctx.parsedBody; + if (iss) { + try { + new URL(iss); + } catch (e) { + this.logger.debug(_scope, 'unparsable issuer', { ticket, resource, subject, iss, ctx }); + // continue, will try resource for metadata + } + } try { new URL(resource); } catch (e) { @@ -1527,7 +1542,7 @@ class Manager { } try { - const result = await this.queuePublisher.publish(queueName, { ticket, resource, subject, epochMs: Date.now() }); + const result = await this.queuePublisher.publish(queueName, { ticket, resource, subject, iss, epochMs: Date.now() }); this.logger.debug(_scope, 'accepted ticket offer', { queueName, ticket, resource, subject, ctx, result }); } catch (e) { this.logger.error(_scope, 'failed to publish ticket to queue', { error: e, queueName, ticket, resource, subject, ctx }); @@ -1541,6 +1556,104 @@ class Manager { } + /** + * Process messages from proffered ticket queue. + * Attempt to redeem ticket and publish to redeemed token queue. + * @param {AMQPChannel} channel + * @param {Buffer} message + */ + async queuedTicketProcessor(channel, message) { + const _scope = _fileScope('queuedTicketProcessor'); + + const queueName = this.options.queues.ticketRedeemedName; + let payload, ticket, resource, subject, iss; + try { + payload = message.content.toString(); + + this.logger.debug(_scope, 'processing ticket', { payload }); + ({ + ticket, + resource, + subject, + iss, + } = JSON.parse(payload)); + } catch (e) { + this.logger.error(_scope, 'could not parse message, discarding', { error: e, message }); + channel.ack(message); + return; + } + + let issuerUrlObj; + try { + if (iss) { + issuerUrlObj = new URL(iss); + } + } catch (e) { + this.logger.debug(_scope, 'unparsable issuer, falling back to resource discovery', { error: e, payload }); + } + + let resourceUrlObj; + try { + resourceUrlObj = new URL(resource); + } catch (e) { + this.logger.error(_scope, 'unparsable resource, discarding', { payload }); + channel.ack(message); + return; + } + + let isNotRetryable = false; + try { + await this.db.context(async (dbCtx) => { + + let token; + try { + token = await this.communication.redeemTicket(ticket, resourceUrlObj, issuerUrlObj); + isNotRetryable = true; // assume we cannot redeem a ticket more than once + this.logger.debug(_scope, 'successfully redeemed ticket', { token, payload }); + channel.ack(message); + } catch (e) { + this.logger.error(_scope, 'failed to redeem ticket', { error: e, payload }); + throw e; + } + + // persist our redemption + const redeemedData = { + subject, + resource, + iss, + ticket, + token, + }; + await this.db.ticketRedeemed(dbCtx, redeemedData); + + try { + const result = await this.queuePublisher.publish(queueName, redeemedData); + this.logger.info(_scope, 'published ticket token', { queueName, ticket, resource, subject, iss, result }); + } catch (e) { + this.logger.error(_scope, 'failed to publish token to queue', { error: e, queueName, ticket, token, resource, subject }); + throw e; // return a 500 + } + + await this.db.ticketTokenPublished(dbCtx, redeemedData); + + }); // dbCtx + + } catch (e) { + isNotRetryable = isNotRetryable + || (e instanceof CommunicationErrors.ValidationError) + || (e?.response?.statusCode < 500) + ; + if (isNotRetryable) { + this.logger.error(_scope, 'failed to process ticket, not requeuing', { error: e, payload }); + channel.ack(message); + return; + } + this.logger.error(_scope, 'failed to process ticket, requeuing', { error: e, payload }); + throw e; + } + } + + /** * Validate a token and return data about it. * @param {http.ServerResponse} res @@ -1933,9 +2046,10 @@ class Manager { this.logger.debug(_scope, 'ticket created', { ctx, ticketData, subjectData }); try { - const result = await this.communication.deliverTicket(ctx.ticketEndpointUrl, ctx.ticketResourceUrl, ctx.ticketSubjectUrl, ticket); + const issuerUrl = new URL(this.options.dingus.selfBaseUrl); + const result = await this.communication.deliverTicket(ctx.ticketEndpointUrl, ctx.ticketResourceUrl, ctx.ticketSubjectUrl, ticket, issuerUrl); ctx.notifications.push(`Success! Ticket was delivered. (${result?.statusMessage})`); - this.logger.info(_scope, 'ticket delivered', { ctx, result }); + this.logger.info(_scope, 'ticket delivered', { ctx }); } catch (e) { this.logger.error(_scope, 'failed to deliver ticket', { ctx, error: e }); ctx.errors.push(`Failed to deliver ticket. (${e})`); @@ -2017,4 +2131,4 @@ class Manager { } -module.exports = Manager; \ No newline at end of file +module.exports = Manager; diff --git a/src/service.js b/src/service.js index 2a159a9..c1e6006 100644 --- a/src/service.js +++ b/src/service.js @@ -101,12 +101,19 @@ class Service extends Dingus { * @param {Object} ctx */ async preHandler(req, res, ctx) { + const _scope = _fileScope('preHandler'); + await super.preHandler(req, res, ctx); ctx.url = req.url; // Persist this for logout redirect const logObject = this.asyncLocalStorage.getStore(); - logObject.requestId = ctx.requestId; - delete ctx.requestId; + // istanbul ignore else + if (logObject) { // debugging in vscode seems to kill ALS, work around + logObject.requestId = ctx.requestId; + delete ctx.requestId; + } else { + this.logger.debug(_scope, 'no async local store'); + } } diff --git a/src/template/admin-html.js b/src/template/admin-html.js index 085b95a..a6a83a0 100644 --- a/src/template/admin-html.js +++ b/src/template/admin-html.js @@ -102,7 +102,7 @@ function tokenTable(tokens) { \t \t\t Type -\t\t\tClient Identifier +\t\t\tClient Identifier / Ticket Subject \t\t\tProfile Scopes \t\t\tCode diff --git a/src/template/root-html.js b/src/template/root-html.js index 7baaa41..1c3f4eb 100644 --- a/src/template/root-html.js +++ b/src/template/root-html.js @@ -54,6 +54,9 @@ module.exports = (ctx, options) => { href: 'admin/ticket', }, ], + headElements: [ + ``, + ], }; const content = [ aboutSection(), diff --git a/test/src/chores.js b/test/src/chores.js index 51c015c..0272990 100644 --- a/test/src/chores.js +++ b/test/src/chores.js @@ -13,12 +13,15 @@ const snooze = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const expectedException = new Error('oh no'); describe('Chores', function () { - let chores, stubLogger, stubDb, options; + let chores, stubLogger, stubDb, stubQueuePublisher, options; beforeEach(function () { stubLogger = new StubLogger(); stubLogger._reset(); stubDb = new StubDB(); stubDb._reset(); + stubQueuePublisher = { + publish: sinon.stub(), + }; }); afterEach(function () { chores?.stopAllChores(); @@ -26,12 +29,13 @@ describe('Chores', function () { }); describe('constructor', function () { - + this.slow(200); it('empty options, no cleaning', async function () { options = undefined; - chores = new Chores(stubLogger, stubDb, options); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); assert.strictEqual(chores.chores.cleanTokens.timeoutObj, undefined); assert.strictEqual(chores.chores.cleanScopes.timeoutObj, undefined); + assert.strictEqual(chores.chores.publishTickets.timeoutObj, undefined); }); it('cleans scopes', async function () { @@ -40,7 +44,7 @@ describe('Chores', function () { scopeCleanupMs: 1, }, }; - chores = new Chores(stubLogger, stubDb, options); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); await snooze(50); assert(chores.chores.cleanScopes.timeoutObj); assert(chores.db.scopeCleanup.called); @@ -55,12 +59,27 @@ describe('Chores', function () { codeValidityTimeoutMs: 10, }, }; - chores = new Chores(stubLogger, stubDb, options); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); await snooze(50); assert(chores.chores.cleanTokens.timeoutObj); assert(chores.db.tokenCleanup.called); }); + it('publishes tickets', async function () { + options = { + chores: { + publishTicketsMs: 1, + }, + queues: { + ticketRedeemedName: 'queue', + }, + }; + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); + await snooze(50); + assert(chores.chores.publishTickets.timeoutObj); + assert(chores.db.ticketTokenGetUnpublished.called); + }); + }); // constructor describe('cleanTokens', function () { @@ -75,7 +94,7 @@ describe('Chores', function () { }, }; stubDb.tokenCleanup.resolves(cleaned); - chores = new Chores(stubLogger, stubDb, options); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); clearTimeout(chores.cleanTokensTimeout); await chores.cleanTokens(); assert(stubLogger.info.called); @@ -90,12 +109,12 @@ describe('Chores', function () { }, }; stubDb.tokenCleanup.rejects(expectedException); - chores = new Chores(stubLogger, stubDb, options); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); await assert.rejects(() => chores.cleanTokens(), expectedException); }); it('covers default', async function () { stubDb.tokenCleanup.resolves(0); - chores = new Chores(stubLogger, stubDb, { + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, { manager: { codeValidityTimeoutMs: 10, }, @@ -114,7 +133,7 @@ describe('Chores', function () { }, }; stubDb.scopeCleanup.resolves(cleaned); - chores = new Chores(stubLogger, stubDb, options); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); clearTimeout(chores.cleanScopesTimeout); await chores.cleanScopes(); assert(stubLogger.info.called); @@ -126,15 +145,43 @@ describe('Chores', function () { }, }; stubDb.scopeCleanup.rejects(expectedException); - chores = new Chores(stubLogger, stubDb, options); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); await assert.rejects(() => chores.cleanScopes(), expectedException); }); it('covers default', async function () { stubDb.scopeCleanup.resolves(0); - chores = new Chores(stubLogger, stubDb, {}); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, {}); await chores.cleanScopes(); assert(stubDb.scopeCleanup.called); }); }); // cleanScopes -}); // Chores \ No newline at end of file + describe('publishTickets', function () { + beforeEach(function () { + options = { + queues: { + ticketRedeemedName: 'queue', + }, + }; + stubDb.ticketTokenGetUnpublished.resolves([{ + ticket: 'xxxTICKETxxx', + resource: 'https://resource.example.com/', + subject: 'https://subject.example.com/', + iss: null, + }]); + chores = new Chores(stubLogger, stubDb, stubQueuePublisher, options); + }); + it('publishes a ticket', async function () { + await chores.publishTickets(); + assert(stubQueuePublisher.publish.called); + assert(stubDb.ticketTokenPublished.called); + }); + it('covers error', async function () { + stubQueuePublisher.publish.rejects(expectedException); + await chores.publishTickets(); + assert(stubQueuePublisher.publish.called); + assert(stubDb.ticketTokenPublished.notCalled); + }); + }); // publishTickets + +}); // Chores diff --git a/test/src/db/integration.js b/test/src/db/integration.js index 7634b30..d866af6 100644 --- a/test/src/db/integration.js +++ b/test/src/db/integration.js @@ -405,6 +405,65 @@ describe('Database Integration', function () { }); }); // Token + describe('Ticket Token Tracking', function () { + let redeemedData; + beforeEach(function () { + redeemedData = { + subject: 'https://entity.example.com/', + resource: 'https://blog.example.com/secret_entry', + iss: 'https://idp.example.com/', + ticket: 'xxxTICKETxxx', + token: 'xxxTOKENxxx', + }; + }); + step('stores redeemed ticket data', async function () { + await db.context(async (dbCtx) => { + await db.ticketRedeemed(dbCtx, redeemedData); + }); + }); + step('gets one pending-publish ticket tokens', async function () { + await db.context(async (dbCtx) => { + const unpublished = await db.ticketTokenGetUnpublished(dbCtx); + assert.strictEqual(unpublished.length, 1); + const record = unpublished[0]; + assert(record.created); + assert(!record.published); + assert(record.ticketId); + delete record.created; + delete record.published; + delete record.ticketId; + assert.deepStrictEqual(record, redeemedData); + }); + }); + step('stores published ticket token data', async function () { + await db.context(async (dbCtx) => { + await db.ticketTokenPublished(dbCtx, redeemedData); + }); + }); + step('gets no pending-publish ticket tokens', async function () { + await db.context(async (dbCtx) => { + const unpublished = await db.ticketTokenGetUnpublished(dbCtx); + assert.strictEqual(unpublished.length, 0); + }); + }); + }); // Ticket Token Tracking + + describe('Bookkeeping', function () { + let event, date; + beforeEach(function () { + event = 'integrationTestEvent'; + date = new Date('Fri Dec 22 03:27 UTC 2023'); + }); + step('inserts event', async function () { + await db.context(async (dbCtx) => { + await db.almanacUpsert(dbCtx, event, date); + const result = await db.almanacGetAll(dbCtx); + const [storedEvent] = result.filter((e) => e.event === event); + assert.deepStrictEqual(storedEvent.date, date); + }); + }); + }); // Bookkeeping + describe('Refreshable Token', function () { let created, codeId, scopes, clientId, profileData, lifespanSeconds, refreshLifespanSeconds, removeScopes; beforeEach(function () { diff --git a/test/src/db/postgres.js b/test/src/db/postgres.js index 7311210..3920d34 100644 --- a/test/src/db/postgres.js +++ b/test/src/db/postgres.js @@ -314,6 +314,42 @@ describe('DatabasePostgres', function () { }); }); // almanacGetAll + describe('almanacUpsert', function () { + let event, date; + beforeEach(function () { + event = 'test_event'; + date = new Date('Fri Dec 22 03:27 UTC 2023') + }); + it('success', async function () { + const dbResult = { + rowCount: 1, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + await db.almanacUpsert(dbCtx, event, date); + }); + it('success with default date', async function () { + const dbResult = { + rowCount: 1, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + await db.almanacUpsert(dbCtx, event); + }); + it('failure', async function () { + const dbResult = { + rowCount: 0, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + await assert.rejects(() => db.almanacUpsert(dbCtx, event, date), DBErrors.UnexpectedResult); + }); + }); // almanacUpsert + + describe('authenticationSuccess', function () { let identifier; beforeEach(function () { @@ -949,5 +985,101 @@ describe('DatabasePostgres', function () { }); }); // tokensGetByIdentifier + describe('ticketRedeemed', function () { + let redeemedData; + beforeEach(function () { + redeemedData = { + resource: 'https://resource.example.com/', + subject: 'https://subject.example.com/', + iss: 'https://idp.example.com/', + ticket: 'xxxTICKETxxx', + token: 'xxxTOKENxxx', + }; + }); + it('success', async function () { + const dbResult = { + rowCount: 1, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + await db.ticketRedeemed(dbCtx, redeemedData); + }); + it('failure', async function () { + const dbResult = { + rowCount: 0, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + await assert.rejects(() => db.ticketRedeemed(dbCtx, redeemedData), DBErrors.UnexpectedResult); + }); + }); // ticketRedeemed + + describe('ticketTokenPublished', function () { + let redeemedData; + beforeEach(function () { + redeemedData = { + resource: 'https://resource.example.com/', + subject: 'https://subject.example.com/', + iss: 'https://idp.example.com/', + ticket: 'xxxTICKETxxx', + token: 'xxxTOKENxxx', + }; + sinon.stub(db.db, 'result'); + }); + it('success', async function () { + const dbResult = { + rowCount: 1, + rows: undefined, + duration: 22, + }; + db.db.result.resolves(dbResult); + await db.ticketTokenPublished(dbCtx, redeemedData); + }); + it('failure', async function () { + const dbResult = { + rowCount: 0, + rows: undefined, + duration: 22, + }; + db.db.result.resolves(dbResult); + await assert.rejects(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult); + }); + it('failure of almanac', async function () { + const dbResult = { + rowCount: 1, + rows: undefined, + duration: 22, + }; + const dbResultAlmanac = { + ...dbResult, + rowCount: 0, + }; + db.db.result.resolves(dbResult).onCall(1).resolves(dbResultAlmanac); + await assert.rejects(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult); + }); + }); // ticketTokenPublished + + describe('ticketTokenGetUnpublished', function () { + it('success', async function () { + const expected = [{ + resource: 'https://resource.example.com/', + subject: 'https://subject.example.com/', + iss: 'https://idp.example.com/', + ticket: 'xxxTICKETxxx', + token: 'xxxTOKENxxx', + created: new Date(), + published: null, + }]; + sinon.stub(db.db, 'manyOrNone').resolves(expected); + const result = await db.ticketTokenGetUnpublished(dbCtx); + assert.deepStrictEqual(result, expected); + }); + it('failure', async function () { + sinon.stub(db.db, 'manyOrNone').rejects(expectedException); + await assert.rejects(() => db.ticketTokenGetUnpublished(dbCtx), expectedException); + }); + }); // ticketTokenGetUnpublished }); // DatabasePostgres diff --git a/test/src/db/sqlite.js b/test/src/db/sqlite.js index f693594..e79e75f 100644 --- a/test/src/db/sqlite.js +++ b/test/src/db/sqlite.js @@ -216,6 +216,32 @@ describe('DatabaseSQLite', function () { }); }); // almanacGetAll + describe('almanacUpsert', function () { + let event, date, dbResult; + beforeEach(function () { + event = 'test_event'; + date = new Date('Fri Dec 22 03:27 UTC 2023') + sinon.stub(db.statement.almanacUpsert, 'run'); + dbResult = { + changes: 1, + lastInsertRowid: undefined, + }; + }); + it('success', function () { + db.statement.almanacUpsert.run.returns(dbResult); + db.almanacUpsert(dbCtx, event, date); + }); + it('success with default date', function () { + db.statement.almanacUpsert.run.returns(dbResult); + db.almanacUpsert(dbCtx, event); + }); + it('failure', function () { + dbResult.changes = 0; + db.statement.almanacUpsert.run.returns(dbResult); + assert.throws(() => db.almanacUpsert(dbCtx, { event, date }), DBErrors.UnexpectedResult); + }); + }); // almanacUpsert + describe('authenticationGet', function () { let identifier, credential; beforeEach(function () { @@ -915,4 +941,120 @@ describe('DatabaseSQLite', function () { }); }); // tokensGetByIdentifier + describe('ticketRedeemed', function () { + let redeemedData, dbResult; + beforeEach(function () { + redeemedData = { + resource: 'https://resource.example.com/', + subject: 'https://subject.example.com/', + iss: 'https://idp.example.com/', + ticket: 'xxxTICKETxxx', + token: 'xxxTOKENxxx', + }; + sinon.stub(db.statement.ticketRedeemed, 'run'); + dbResult = { + changes: 1, + lastInsertRowid: undefined, + }; + }); + it('success', function () { + db.statement.ticketRedeemed.run.returns(dbResult); + db.ticketRedeemed(dbCtx, redeemedData); + }); + it('failure', function () { + dbResult.changes = 0; + db.statement.ticketRedeemed.run.returns(dbResult); + assert.throws(() => db.ticketRedeemed(dbCtx, redeemedData), DBErrors.UnexpectedResult); + }); + }); // ticketRedeemed + + describe('ticketTokenPublished', function () { + let redeemedData, dbResult; + beforeEach(function () { + redeemedData = { + resource: 'https://resource.example.com/', + subject: 'https://subject.example.com/', + iss: 'https://idp.example.com/', + ticket: 'xxxTICKETxxx', + token: 'xxxTOKENxxx', + }; + sinon.stub(db.statement.ticketTokenPublished, 'run'); + sinon.stub(db.statement.almanacUpsert, 'run'); + dbResult = { + changes: 1, + lastInsertRowid: undefined, + }; + }); + it('success', function () { + db.statement.ticketTokenPublished.run.returns(dbResult); + db.statement.almanacUpsert.run.returns(dbResult); + db.ticketTokenPublished(dbCtx, redeemedData); + assert(db.statement.ticketTokenPublished.run.called); + assert(db.statement.almanacUpsert.run.called); + }); + it('failure', function () { + dbResult.changes = 0; + db.statement.ticketTokenPublished.run.returns(dbResult); + assert.throws(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult); + }); + it('failure of almanac', function () { + const dbResultAlmanac = { + ...dbResult, + changes: 0, + } + db.statement.ticketTokenPublished.run.returns(dbResult); + db.statement.almanacUpsert.run.returns(dbResultAlmanac); + assert.throws(() => db.ticketTokenPublished(dbCtx, redeemedData), DBErrors.UnexpectedResult); + }); + }); // ticketTokenPublished + + describe('ticketTokenGetUnpublished', function () { + beforeEach(function () { + sinon.stub(db.statement.ticketTokenGetUnpublished, 'all'); + }); + it('success', function () { + db.statement.ticketTokenGetUnpublished.all.returns([]); + const result = db.ticketTokenGetUnpublished(); + assert.deepStrictEqual(result, []); + }); + it('failure', function () { + db.statement.ticketTokenGetUnpublished.all.throws(expectedException); + assert.throws(() => db.ticketTokenGetUnpublished(), expectedException); + }); + }); // ticketTokenGetUnpublished + + describe('_redeemedTicketToNative', function () { + let redeemedData; + beforeEach(function () { + redeemedData = { + resource: 'https://resource.example.com/', + subject: 'https://subject.example.com/', + iss: 'https://idp.example.com/', + ticket: 'xxxTICKETxxx', + token: 'xxxTOKENxxx', + created: 1701970607n, + published: 1701970670n, + }; + }); + it('covers', function () { + const expected = { + ...redeemedData, + created: new Date('2023-12-07T17:36:47.000Z'), + published: new Date('2023-12-07T17:37:50.000Z'), + }; + const result = DB._redeemedTicketToNative(redeemedData); + assert.deepStrictEqual(result, expected); + }); + it('covers no published', function () { + redeemedData.published = null; + const expected = { + ...redeemedData, + created: new Date('2023-12-07T17:36:47.000Z'), + published: null, + }; + const result = DB._redeemedTicketToNative(redeemedData); + assert.deepStrictEqual(result, expected); + }); + }); // _redeemedTicketToNative + }); // DatabaseSQLite diff --git a/test/src/manager.js b/test/src/manager.js index f3c38b4..5acb5bb 100644 --- a/test/src/manager.js +++ b/test/src/manager.js @@ -49,10 +49,14 @@ describe('Manager', function () { sinon.stub(manager.communication, 'fetchProfile'); sinon.stub(manager.communication, 'fetchClientIdentifier'); sinon.stub(manager.communication, 'deliverTicket'); + sinon.stub(manager.communication, 'redeemTicket'); sinon.stub(dns.promises, 'lookup').resolves([{ family: 4, address: '10.11.12.13' }]); sinon.stub(manager.queuePublisher, 'connect'); sinon.stub(manager.queuePublisher, 'establishAMQPPlumbing'); sinon.stub(manager.queuePublisher, 'publish'); + sinon.stub(manager.queueConsumer, 'connect'); + sinon.stub(manager.queueConsumer, 'establishAMQPPlumbing'); + sinon.stub(manager.queueConsumer, 'consume'); }); afterEach(function () { @@ -425,7 +429,8 @@ describe('Manager', function () { assert.strictEqual(res.statusCode, 302); assert.strictEqual(ctx.session.error, 'invalid_request'); assert.strictEqual(ctx.session.errorDescriptions.length, 1); - }); }); // getAuthorization + }); + }); // getAuthorization describe('_setError', function () { it('covers', function () { @@ -2276,6 +2281,7 @@ describe('Manager', function () { ticket: 'ticket123', resource: 'https://blog.example.com/', subject: 'https://otheruser.example.com/', + iss: 'https://ia.example.com/', }; }); it('accepts a ticket for a known profile', async function () { @@ -2300,12 +2306,81 @@ describe('Manager', function () { it('covers no ticket queue', async function () { delete options.queues.amqp.url; manager = new Manager(logger, stubDb, options); - await assert.rejects(() => manager.postTicket(req, res, ctx), ResponseError); }); + it('covers no issuer', async function () { + delete ctx.parsedBody.iss; + manager.db.profileIsValid.resolves(true); + await manager.postTicket(req, res, ctx); + assert(res.end.called); + assert.strictEqual(res.statusCode, 202); + }); + it('covers bad issuer', async function () { + ctx.parsedBody.iss = 'not a url'; + manager.db.profileIsValid.resolves(true); + await manager.postTicket(req, res, ctx); + assert(res.end.called); + assert.strictEqual(res.statusCode, 202); + }); }); // postTicket + describe('queuedTicketProcessor', function () { + let channel, content; + const message = () => ({ + content: Buffer.from(JSON.stringify(content)), + }); + beforeEach(function () { + channel = { + ack: sinon.stub(), + }; + content = { + ticket: 'XXXticketXXX', + resource: 'https://blog.example.com/', + subject: 'https://otheruser.exmaple.com/', + iss: 'https://ia.example.com/', + epochMs: Date.now(), + }; + }); + it('redeems a ticket', async function () { + await manager.queuedTicketProcessor(channel, message()); + assert(manager.queuePublisher.publish.called); + assert(channel.ack.called); + }); + it('redeems a ticket, missing issuer', async function () { + delete content.iss; + await manager.queuedTicketProcessor(channel, message()); + assert(manager.queuePublisher.publish.called); + assert(channel.ack.called); + }); + it('covers bad message', async function () { + await manager.queuedTicketProcessor(channel, { content: 'diddly' }); + assert(channel.ack.called); + }); + it('covers bad issuer', async function () { + content.iss = 'not a url'; + await manager.queuedTicketProcessor(channel, message()); + assert(manager.queuePublisher.publish.called); + }); + it('covers bad resource', async function () { + content.resource = 'not a url'; + await manager.queuedTicketProcessor(channel, message()); + assert(manager.communication.redeemTicket.notCalled); + assert(manager.queuePublisher.publish.notCalled); + assert(channel.ack.called); + }); + it('covers failed redemption', async function () { + const expectedException = new Error('oh no'); + manager.communication.redeemTicket.rejects(expectedException); + assert.rejects(() => manager.queuedTicketProcessor(channel, message()), expectedException); + }); + it('covers failed publish', async function () { + const expectedException = new Error('oh no'); + manager.queuePublisher.publish.rejects(expectedException); + assert.rejects(() => manager.queuedTicketProcessor(channel, message()), expectedException); + }); + }); // queuedTicketProcessor + describe('getAdminMaintenance', function () { it('covers information', async function () { await manager.getAdminMaintenance(res, ctx); @@ -2320,4 +2395,4 @@ describe('Manager', function () { }); }); // getAdminMaintenance -}); // Manager \ No newline at end of file +}); // Manager diff --git a/test/stub-db.js b/test/stub-db.js index f973b8d..d8cb0cd 100644 --- a/test/stub-db.js +++ b/test/stub-db.js @@ -8,6 +8,7 @@ class StubDatabase extends Base { return [ ...super._stubFns, 'almanacGetAll', + 'almanacUpsert', 'authenticationGet', 'authenticationSuccess', 'authenticationUpsert', @@ -27,9 +28,12 @@ class StubDatabase extends Base { 'tokenGetByCodeId', 'tokenRefreshRevokeByCodeId', 'tokenRevokeByCodeId', - 'tokensGetByIdentifier', + 'tokensGetByIdentifier', + 'ticketRedeemed', + 'ticketTokenPublished', + 'ticketTokenGetUnpublished', ]; } } -module.exports = StubDatabase; \ No newline at end of file +module.exports = StubDatabase; -- 2.45.2