Browse Source

fix: add sending out email campaigns

Karol Sójko 3 years ago
parent
commit
bf14ec05f9

+ 53 - 3
.pnp.cjs

@@ -2499,6 +2499,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["jsonwebtoken", "npm:8.5.1"]\
           ],\
           "linkType": "HARD"\
+        }],\
+        ["npm:3.19.4", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-auth-npm-3.19.4-29b35c1352-84ac9e669e.zip/node_modules/@standardnotes/auth/",\
+          "packageDependencies": [\
+            ["@standardnotes/auth", "npm:3.19.4"],\
+            ["@standardnotes/common", "npm:1.23.1"],\
+            ["jsonwebtoken", "npm:8.5.1"]\
+          ],\
+          "linkType": "HARD"\
         }]\
       ]],\
       ["@standardnotes/auth-server", [\
@@ -2511,13 +2520,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@standardnotes/analytics", "npm:1.6.0"],\
             ["@standardnotes/api", "npm:1.1.13"],\
             ["@standardnotes/auth", "npm:3.19.3"],\
-            ["@standardnotes/common", "npm:1.23.0"],\
-            ["@standardnotes/domain-events", "npm:2.32.2"],\
+            ["@standardnotes/common", "npm:1.23.1"],\
+            ["@standardnotes/domain-events", "npm:2.32.4"],\
             ["@standardnotes/domain-events-infra", "npm:1.5.2"],\
             ["@standardnotes/features", "npm:1.45.5"],\
             ["@standardnotes/responses", "npm:1.6.36"],\
             ["@standardnotes/scheduler", "npm:1.1.1"],\
-            ["@standardnotes/settings", "npm:1.14.3"],\
+            ["@standardnotes/settings", "npm:1.15.0"],\
             ["@standardnotes/sncrypto-common", "npm:1.9.0"],\
             ["@standardnotes/sncrypto-node", "npm:1.8.3"],\
             ["@standardnotes/time", "npm:1.7.0"],\
@@ -2568,6 +2577,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@standardnotes/common", "npm:1.23.0"]\
           ],\
           "linkType": "HARD"\
+        }],\
+        ["npm:1.23.1", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-common-npm-1.23.1-ed73dbb679-f498f4c469.zip/node_modules/@standardnotes/common/",\
+          "packageDependencies": [\
+            ["@standardnotes/common", "npm:1.23.1"]\
+          ],\
+          "linkType": "HARD"\
         }]\
       ]],\
       ["@standardnotes/config", [\
@@ -2610,6 +2626,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@standardnotes/scheduler", "npm:1.1.1"]\
           ],\
           "linkType": "HARD"\
+        }],\
+        ["npm:2.32.4", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-domain-events-npm-2.32.4-0b0cc85196-9f8a1637aa.zip/node_modules/@standardnotes/domain-events/",\
+          "packageDependencies": [\
+            ["@standardnotes/domain-events", "npm:2.32.4"],\
+            ["@standardnotes/auth", "npm:3.19.4"],\
+            ["@standardnotes/features", "npm:1.45.6"],\
+            ["@standardnotes/scheduler", "npm:1.1.2"]\
+          ],\
+          "linkType": "HARD"\
         }]\
       ]],\
       ["@standardnotes/domain-events-infra", [\
@@ -2663,6 +2689,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@standardnotes/common", "npm:1.23.0"]\
           ],\
           "linkType": "HARD"\
+        }],\
+        ["npm:1.45.6", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.45.6-6f88b4fd2a-572780d6e2.zip/node_modules/@standardnotes/features/",\
+          "packageDependencies": [\
+            ["@standardnotes/features", "npm:1.45.6"],\
+            ["@standardnotes/auth", "npm:3.19.4"],\
+            ["@standardnotes/common", "npm:1.23.1"]\
+          ],\
+          "linkType": "HARD"\
         }]\
       ]],\
       ["@standardnotes/files-server", [\
@@ -2760,6 +2795,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@standardnotes/common", "npm:1.23.0"]\
           ],\
           "linkType": "HARD"\
+        }],\
+        ["npm:1.1.2", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-scheduler-npm-1.1.2-78f89314fb-68642a0874.zip/node_modules/@standardnotes/scheduler/",\
+          "packageDependencies": [\
+            ["@standardnotes/scheduler", "npm:1.1.2"],\
+            ["@standardnotes/common", "npm:1.23.1"]\
+          ],\
+          "linkType": "HARD"\
         }]\
       ]],\
       ["@standardnotes/scheduler-server", [\
@@ -2842,6 +2885,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@standardnotes/settings", "npm:1.14.3"]\
           ],\
           "linkType": "HARD"\
+        }],\
+        ["npm:1.15.0", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-settings-npm-1.15.0-bfec86ee49-4397d453a1.zip/node_modules/@standardnotes/settings/",\
+          "packageDependencies": [\
+            ["@standardnotes/settings", "npm:1.15.0"]\
+          ],\
+          "linkType": "HARD"\
         }]\
       ]],\
       ["@standardnotes/sncrypto-common", [\

BIN
.yarn/cache/@standardnotes-auth-npm-3.19.4-29b35c1352-84ac9e669e.zip


BIN
.yarn/cache/@standardnotes-common-npm-1.23.1-ed73dbb679-f498f4c469.zip


BIN
.yarn/cache/@standardnotes-domain-events-npm-2.32.4-0b0cc85196-9f8a1637aa.zip


BIN
.yarn/cache/@standardnotes-features-npm-1.45.6-6f88b4fd2a-572780d6e2.zip


BIN
.yarn/cache/@standardnotes-scheduler-npm-1.1.2-78f89314fb-68642a0874.zip


BIN
.yarn/cache/@standardnotes-settings-npm-1.15.0-bfec86ee49-4397d453a1.zip


+ 139 - 0
packages/auth/bin/email.ts

@@ -0,0 +1,139 @@
+import 'reflect-metadata'
+
+import 'newrelic'
+
+import { Stream } from 'stream'
+
+import { Logger } from 'winston'
+import * as dayjs from 'dayjs'
+import * as utc from 'dayjs/plugin/utc'
+
+import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
+import { ContainerConfigLoader } from '../src/Bootstrap/Container'
+import TYPES from '../src/Bootstrap/Types'
+import { Env } from '../src/Bootstrap/Env'
+import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
+import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
+import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
+import { EmailMessageIdentifier } from '@standardnotes/common'
+import { User } from '../src/Domain/User/User'
+import { EncryptionVersion } from '../src/Domain/Encryption/EncryptionVersion'
+import { TimerInterface } from '@standardnotes/time'
+
+const inputArgs = process.argv.slice(2)
+const emailMessageIdentifier = inputArgs[0]
+
+const sendEmailCampaign = async (
+  userRepository: UserRepositoryInterface,
+  settingService: SettingServiceInterface,
+  userSubscriptionRepository: UserSubscriptionRepositoryInterface,
+  timer: TimerInterface,
+  domainEventFactory: DomainEventFactoryInterface,
+  domainEventPublisher: DomainEventPublisherInterface,
+): Promise<void> => {
+  const stream = await userRepository.streamAll()
+
+  return new Promise((resolve, reject) => {
+    stream
+      .pipe(
+        new Stream.Transform({
+          objectMode: true,
+          transform: async (rawUserData, _encoding, callback) => {
+            const emailsMutedSetting = await settingService.findSettingWithDecryptedValue({
+              userUuid: rawUserData.user_uuid,
+              settingName: SettingName.MuteMarketingEmails,
+            })
+
+            if (emailsMutedSetting === null) {
+              const user = (await userRepository.findOneByUuid(rawUserData.user_uuid)) as User
+
+              await settingService.createOrReplace({
+                user,
+                props: {
+                  name: SettingName.MuteMarketingEmails,
+                  unencryptedValue: MuteMarketingEmailsOption.NotMuted,
+                  serverEncryptionVersion: EncryptionVersion.Default,
+                  sensitive: false,
+                },
+              })
+            }
+
+            if (emailsMutedSetting?.value === MuteMarketingEmailsOption.Muted) {
+              callback()
+
+              return
+            }
+
+            let activeSubscription = false
+            let subscriptionPlanName = null
+
+            const userSubscription = await userSubscriptionRepository.findOneByUserUuid(rawUserData.user_uuid)
+            if (userSubscription !== null) {
+              activeSubscription =
+                !userSubscription.cancelled && userSubscription.endsAt > timer.getTimestampInMicroseconds()
+              subscriptionPlanName = userSubscription.planName
+            }
+
+            await domainEventPublisher.publish(
+              domainEventFactory.createEmailMessageRequestedEvent({
+                userEmail: rawUserData.user_email,
+                messageIdentifier: emailMessageIdentifier as EmailMessageIdentifier,
+                context: {
+                  activeSubscription,
+                  subscriptionPlanName,
+                },
+              }),
+            )
+
+            callback()
+          },
+        }),
+      )
+      .on('finish', resolve)
+      .on('error', reject)
+  })
+}
+
+const container = new ContainerConfigLoader()
+void container.load().then((container) => {
+  dayjs.extend(utc)
+
+  const env: Env = new Env()
+  env.load()
+
+  const logger: Logger = container.get(TYPES.Logger)
+
+  logger.info(`Starting email campaign for email ${emailMessageIdentifier} ...`)
+
+  const userRepository: UserRepositoryInterface = container.get(TYPES.UserRepository)
+  const settingService: SettingServiceInterface = container.get(TYPES.SettingService)
+  const userSubscriptionRepository: UserSubscriptionRepositoryInterface = container.get(
+    TYPES.UserSubscriptionRepository,
+  )
+  const timer: TimerInterface = container.get(TYPES.Timer)
+  const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
+  const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
+
+  Promise.resolve(
+    sendEmailCampaign(
+      userRepository,
+      settingService,
+      userSubscriptionRepository,
+      timer,
+      domainEventFactory,
+      domainEventPublisher,
+    ),
+  )
+    .then(() => {
+      logger.info(`${emailMessageIdentifier} email campaign complete.`)
+
+      process.exit(0)
+    })
+    .catch((error) => {
+      logger.error(`Could not finish ${emailMessageIdentifier} email campaign: ${error.message}`)
+
+      process.exit(1)
+    })
+})

+ 3 - 3
packages/auth/package.json

@@ -33,13 +33,13 @@
     "@standardnotes/analytics": "^1.6.0",
     "@standardnotes/api": "^1.1.13",
     "@standardnotes/auth": "^3.19.2",
-    "@standardnotes/common": "^1.23.0",
-    "@standardnotes/domain-events": "^2.31.1",
+    "@standardnotes/common": "^1.23.1",
+    "@standardnotes/domain-events": "^2.32.4",
     "@standardnotes/domain-events-infra": "^1.4.135",
     "@standardnotes/features": "^1.45.2",
     "@standardnotes/responses": "^1.6.15",
     "@standardnotes/scheduler": "^1.1.1",
-    "@standardnotes/settings": "^1.14.2",
+    "@standardnotes/settings": "^1.15.0",
     "@standardnotes/sncrypto-common": "^1.8.1",
     "@standardnotes/sncrypto-node": "^1.8.1",
     "@standardnotes/time": "^1.6.8",

+ 30 - 1
packages/auth/src/Domain/Event/DomainEventFactory.spec.ts

@@ -1,6 +1,6 @@
 import 'reflect-metadata'
 
-import { RoleName } from '@standardnotes/common'
+import { EmailMessageIdentifier, RoleName } from '@standardnotes/common'
 import { PredicateName, PredicateAuthority, PredicateVerificationResult } from '@standardnotes/scheduler'
 import { TimerInterface } from '@standardnotes/time'
 
@@ -18,6 +18,35 @@ describe('DomainEventFactory', () => {
     timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
   })
 
+  it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
+    expect(
+      createFactory().createEmailMessageRequestedEvent({
+        userEmail: 'test@test.te',
+        messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
+        context: {
+          foo: 'bar',
+        },
+      }),
+    ).toEqual({
+      createdAt: expect.any(Date),
+      meta: {
+        correlation: {
+          userIdentifier: 'test@test.te',
+          userIdentifierType: 'email',
+        },
+        origin: 'auth',
+      },
+      payload: {
+        messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
+        userEmail: 'test@test.te',
+        context: {
+          foo: 'bar',
+        },
+      },
+      type: 'EMAIL_MESSAGE_REQUESTED',
+    })
+  })
+
   it('should create a PREDICATE_VERIFIED event', () => {
     expect(
       createFactory().createPredicateVerifiedEvent({

+ 21 - 1
packages/auth/src/Domain/Event/DomainEventFactory.ts

@@ -1,4 +1,4 @@
-import { RoleName, Uuid } from '@standardnotes/common'
+import { EmailMessageIdentifier, RoleName, Uuid } from '@standardnotes/common'
 import {
   AccountDeletionRequestedEvent,
   UserEmailChangedEvent,
@@ -14,6 +14,7 @@ import {
   SharedSubscriptionInvitationCanceledEvent,
   PredicateVerifiedEvent,
   DomainEventService,
+  EmailMessageRequestedEvent,
 } from '@standardnotes/domain-events'
 import { Predicate, PredicateVerificationResult } from '@standardnotes/scheduler'
 import { TimerInterface } from '@standardnotes/time'
@@ -26,6 +27,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 export class DomainEventFactory implements DomainEventFactoryInterface {
   constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
 
+  createEmailMessageRequestedEvent(dto: {
+    userEmail: string
+    messageIdentifier: EmailMessageIdentifier
+    context: Record<string, unknown>
+  }): EmailMessageRequestedEvent {
+    return {
+      type: 'EMAIL_MESSAGE_REQUESTED',
+      createdAt: this.timer.getUTCDate(),
+      meta: {
+        correlation: {
+          userIdentifier: dto.userEmail,
+          userIdentifierType: 'email',
+        },
+        origin: DomainEventService.Auth,
+      },
+      payload: dto,
+    }
+  }
+
   createPredicateVerifiedEvent(dto: {
     userUuid: Uuid
     predicate: Predicate

+ 7 - 1
packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -1,4 +1,4 @@
-import { Uuid, RoleName } from '@standardnotes/common'
+import { Uuid, RoleName, EmailMessageIdentifier } from '@standardnotes/common'
 import { Predicate, PredicateVerificationResult } from '@standardnotes/scheduler'
 import {
   AccountDeletionRequestedEvent,
@@ -14,10 +14,16 @@ import {
   SharedSubscriptionInvitationCreatedEvent,
   SharedSubscriptionInvitationCanceledEvent,
   PredicateVerifiedEvent,
+  EmailMessageRequestedEvent,
 } from '@standardnotes/domain-events'
 import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
 
 export interface DomainEventFactoryInterface {
+  createEmailMessageRequestedEvent(dto: {
+    userEmail: string
+    messageIdentifier: EmailMessageIdentifier
+    context: Record<string, unknown>
+  }): EmailMessageRequestedEvent
   createUserSignedInEvent(dto: {
     userUuid: string
     userEmail: string

+ 2 - 2
packages/auth/src/Domain/Setting/SettingsAssociationService.spec.ts

@@ -39,13 +39,13 @@ describe('SettingsAssociationService', () => {
   it('should return the default set of settings for a newly registered user', () => {
     const settings = createService().getDefaultSettingsAndValuesForNewUser()
     const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()]
-    expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'LOG_SESSION_USER_AGENT'])
+    expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
   })
 
   it('should return the default set of settings for a newly registered vault account', () => {
     const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount()
     const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()]
-    expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'LOG_SESSION_USER_AGENT'])
+    expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
 
     expect(settings.get(SettingName.LogSessionUserAgent)?.value).toEqual('disabled')
   })

+ 16 - 1
packages/auth/src/Domain/Setting/SettingsAssociationService.ts

@@ -1,5 +1,10 @@
 import { PermissionName } from '@standardnotes/features'
-import { LogSessionUserAgentOption, MuteSignInEmailsOption, SettingName } from '@standardnotes/settings'
+import {
+  LogSessionUserAgentOption,
+  MuteMarketingEmailsOption,
+  MuteSignInEmailsOption,
+  SettingName,
+} from '@standardnotes/settings'
 import { injectable } from 'inversify'
 
 import { EncryptionVersion } from '../Encryption/EncryptionVersion'
@@ -14,6 +19,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
     SettingName.MuteFailedBackupsEmails,
     SettingName.MuteFailedCloudBackupsEmails,
     SettingName.MuteSignInEmails,
+    SettingName.MuteMarketingEmails,
     SettingName.DropboxBackupFrequency,
     SettingName.GoogleDriveBackupFrequency,
     SettingName.OneDriveBackupFrequency,
@@ -28,6 +34,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
     SettingName.MuteFailedBackupsEmails,
     SettingName.MuteFailedCloudBackupsEmails,
     SettingName.MuteSignInEmails,
+    SettingName.MuteMarketingEmails,
     SettingName.ListedAuthorSecrets,
     SettingName.LogSessionUserAgent,
   ]
@@ -47,6 +54,14 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
         value: MuteSignInEmailsOption.NotMuted,
       },
     ],
+    [
+      SettingName.MuteMarketingEmails,
+      {
+        sensitive: false,
+        serverEncryptionVersion: EncryptionVersion.Unencrypted,
+        value: MuteMarketingEmailsOption.NotMuted,
+      },
+    ],
     [
       SettingName.LogSessionUserAgent,
       {

+ 59 - 5
yarn.lock

@@ -1820,13 +1820,13 @@ __metadata:
     "@standardnotes/analytics": ^1.6.0
     "@standardnotes/api": ^1.1.13
     "@standardnotes/auth": ^3.19.2
-    "@standardnotes/common": ^1.23.0
-    "@standardnotes/domain-events": ^2.31.1
+    "@standardnotes/common": ^1.23.1
+    "@standardnotes/domain-events": ^2.32.4
     "@standardnotes/domain-events-infra": ^1.4.135
     "@standardnotes/features": ^1.45.2
     "@standardnotes/responses": ^1.6.15
     "@standardnotes/scheduler": ^1.1.1
-    "@standardnotes/settings": ^1.14.2
+    "@standardnotes/settings": ^1.15.0
     "@standardnotes/sncrypto-common": ^1.8.1
     "@standardnotes/sncrypto-node": ^1.8.1
     "@standardnotes/time": ^1.6.8
@@ -1889,6 +1889,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@standardnotes/auth@npm:^3.19.4":
+  version: 3.19.4
+  resolution: "@standardnotes/auth@npm:3.19.4"
+  dependencies:
+    "@standardnotes/common": ^1.23.1
+    jsonwebtoken: ^8.5.1
+  checksum: 84ac9e669ee393a8ac04c49729cd732280ab7ad9c6ad780e1f8cc7e02bdd1d15b8446d84134e775d805e6fa04e9500d9e62d7f857e6b1f5bb36f10a0c31640ff
+  languageName: node
+  linkType: hard
+
 "@standardnotes/common@npm:^1.19.1, @standardnotes/common@npm:^1.19.4, @standardnotes/common@npm:^1.22.0, @standardnotes/common@npm:^1.23.0":
   version: 1.23.0
   resolution: "@standardnotes/common@npm:1.23.0"
@@ -1896,6 +1906,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@standardnotes/common@npm:^1.23.1":
+  version: 1.23.1
+  resolution: "@standardnotes/common@npm:1.23.1"
+  checksum: f498f4c469f7b9cdfc08b7648077832a2a214d844f012d02d73c2c418fab221af3302570cc6b1957b5ed481b55237f95bc24fc5c231162bfcdf8730333d38405
+  languageName: node
+  linkType: hard
+
 "@standardnotes/config@npm:2.0.1":
   version: 2.0.1
   resolution: "@standardnotes/config@npm:2.0.1"
@@ -1946,7 +1963,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@standardnotes/domain-events@npm:^2.27.6, @standardnotes/domain-events@npm:^2.29.0, @standardnotes/domain-events@npm:^2.31.1, @standardnotes/domain-events@npm:^2.32.2":
+"@standardnotes/domain-events@npm:^2.27.6, @standardnotes/domain-events@npm:^2.29.0, @standardnotes/domain-events@npm:^2.32.2":
   version: 2.32.2
   resolution: "@standardnotes/domain-events@npm:2.32.2"
   dependencies:
@@ -1968,6 +1985,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@standardnotes/domain-events@npm:^2.32.4":
+  version: 2.32.4
+  resolution: "@standardnotes/domain-events@npm:2.32.4"
+  dependencies:
+    "@standardnotes/auth": ^3.19.4
+    "@standardnotes/features": ^1.45.6
+    "@standardnotes/scheduler": ^1.1.2
+  checksum: 9f8a1637aa63d04f2c60e1a4c75e5fc5f4013f4a5e39b9cd55d56a894220cee47390e8b37422b4c128db05d231ab371e8a1caa4e1af3395bf04319697dae7303
+  languageName: node
+  linkType: hard
+
 "@standardnotes/encryption@npm:^1.8.19":
   version: 1.8.19
   resolution: "@standardnotes/encryption@npm:1.8.19"
@@ -1989,6 +2017,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@standardnotes/features@npm:^1.45.6":
+  version: 1.45.6
+  resolution: "@standardnotes/features@npm:1.45.6"
+  dependencies:
+    "@standardnotes/auth": ^3.19.4
+    "@standardnotes/common": ^1.23.1
+  checksum: 572780d6e24efa664f316f6cf56c01907e00263db6204fee17f7ce9e43fa376411f373863dda59f8fb039250a224b7f8637fbd0bcbd7241112e061accbc05360
+  languageName: node
+  linkType: hard
+
 "@standardnotes/files-server@workspace:packages/files":
   version: 0.0.0-use.local
   resolution: "@standardnotes/files-server@workspace:packages/files"
@@ -2112,6 +2150,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@standardnotes/scheduler@npm:^1.1.2":
+  version: 1.1.2
+  resolution: "@standardnotes/scheduler@npm:1.1.2"
+  dependencies:
+    "@standardnotes/common": ^1.23.1
+  checksum: 68642a08741aeb4936e4a69e43e2b85e5cbfa3ce60006d292cee517d33093edaf35a6a7af61343d85ab3474d6e980a073a2779f6e0b1f11805ab06a407b15ce7
+  languageName: node
+  linkType: hard
+
 "@standardnotes/server-monorepo@workspace:.":
   version: 0.0.0-use.local
   resolution: "@standardnotes/server-monorepo@workspace:."
@@ -2148,13 +2195,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@standardnotes/settings@npm:1.14.3, @standardnotes/settings@npm:^1.14.2":
+"@standardnotes/settings@npm:1.14.3":
   version: 1.14.3
   resolution: "@standardnotes/settings@npm:1.14.3"
   checksum: 60fbb2ca856083b1afdf3a93cfb2729151c8a4a34f02564a8a814a7caad9982285303ad79ffb1da7643046061a6efe39410e57e8e4703a8963be8dc0ffabeaa6
   languageName: node
   linkType: hard
 
+"@standardnotes/settings@npm:^1.15.0":
+  version: 1.15.0
+  resolution: "@standardnotes/settings@npm:1.15.0"
+  checksum: 4397d453a1a0cd5554012a5bf5ee2025c102ffc6d742841eecd2ab9cd46b0fbfe78da61b401451646274887d92801cafcef4c283bbfc6074b4d152103e15f861
+  languageName: node
+  linkType: hard
+
 "@standardnotes/sncrypto-common@npm:^1.3.0, @standardnotes/sncrypto-common@npm:^1.8.1, @standardnotes/sncrypto-common@npm:^1.9.0":
   version: 1.9.0
   resolution: "@standardnotes/sncrypto-common@npm:1.9.0"