@budibase/backend-core 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.js +2181 -1429
  2. package/dist/index.js.map +4 -4
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +4 -4
  5. package/dist/plugins.js.meta.json +1 -1
  6. package/dist/src/environment.d.ts +2 -0
  7. package/dist/src/environment.js +3 -1
  8. package/dist/src/environment.js.map +1 -1
  9. package/dist/src/middleware/contentSecurityPolicy.d.ts +2 -0
  10. package/dist/src/middleware/contentSecurityPolicy.js +128 -0
  11. package/dist/src/middleware/contentSecurityPolicy.js.map +1 -0
  12. package/dist/src/middleware/index.d.ts +1 -0
  13. package/dist/src/middleware/index.js +3 -1
  14. package/dist/src/middleware/index.js.map +1 -1
  15. package/dist/src/queue/inMemoryQueue.d.ts +0 -2
  16. package/dist/src/queue/inMemoryQueue.js +1 -11
  17. package/dist/src/queue/inMemoryQueue.js.map +1 -1
  18. package/dist/src/queue/queue.js +6 -0
  19. package/dist/src/queue/queue.js.map +1 -1
  20. package/dist/tests/core/utilities/index.d.ts +1 -0
  21. package/dist/tests/core/utilities/index.js +2 -1
  22. package/dist/tests/core/utilities/index.js.map +1 -1
  23. package/dist/tests/core/utilities/queue.d.ts +2 -0
  24. package/dist/tests/core/utilities/queue.js +21 -0
  25. package/dist/tests/core/utilities/queue.js.map +1 -0
  26. package/dist/tests/core/utilities/testContainerUtils.d.ts +2 -0
  27. package/dist/tests/core/utilities/testContainerUtils.js +63 -0
  28. package/dist/tests/core/utilities/testContainerUtils.js.map +1 -1
  29. package/dist/tests/core/utilities/utils/index.d.ts +1 -0
  30. package/dist/tests/core/utilities/utils/index.js +2 -1
  31. package/dist/tests/core/utilities/utils/index.js.map +1 -1
  32. package/dist/tests/core/utilities/utils/queue.d.ts +3 -0
  33. package/dist/tests/core/utilities/utils/queue.js +37 -0
  34. package/dist/tests/core/utilities/utils/queue.js.map +1 -0
  35. package/package.json +4 -4
  36. package/src/cache/tests/docWritethrough.spec.ts +26 -24
  37. package/src/environment.ts +4 -0
  38. package/src/middleware/contentSecurityPolicy.ts +118 -0
  39. package/src/middleware/index.ts +1 -0
  40. package/src/middleware/tests/contentSecurityPolicy.spec.ts +75 -0
  41. package/src/queue/inMemoryQueue.ts +2 -12
  42. package/src/queue/queue.ts +10 -3
  43. package/tests/core/utilities/index.ts +1 -0
  44. package/tests/core/utilities/queue.ts +9 -0
  45. package/tests/core/utilities/testContainerUtils.ts +57 -0
  46. package/tests/core/utilities/utils/index.ts +1 -0
  47. package/tests/core/utilities/utils/queue.ts +27 -0
@@ -0,0 +1,118 @@
1
+ import crypto from "crypto"
2
+ import env from "../environment"
3
+
4
+ const CSP_DIRECTIVES = {
5
+ "default-src": ["'self'"],
6
+ "script-src": [
7
+ "'self'",
8
+ "'unsafe-eval'",
9
+ "https://*.budibase.net",
10
+ "https://cdn.budi.live",
11
+ "https://js.intercomcdn.com",
12
+ "https://widget.intercom.io",
13
+ "https://d2l5prqdbvm3op.cloudfront.net",
14
+ "https://us-assets.i.posthog.com",
15
+ ],
16
+ "style-src": [
17
+ "'self'",
18
+ "'unsafe-inline'",
19
+ "https://cdn.jsdelivr.net",
20
+ "https://fonts.googleapis.com",
21
+ "https://rsms.me",
22
+ "https://maxcdn.bootstrapcdn.com",
23
+ ],
24
+ "object-src": ["'none'"],
25
+ "base-uri": ["'self'"],
26
+ "connect-src": [
27
+ "'self'",
28
+ "https://*.budibase.app",
29
+ "https://*.budibaseqa.app",
30
+ "https://*.budibase.net",
31
+ "https://api-iam.intercom.io",
32
+ "https://api-ping.intercom.io",
33
+ "https://app.posthog.com",
34
+ "https://us.i.posthog.com",
35
+ "wss://nexus-websocket-a.intercom.io",
36
+ "wss://nexus-websocket-b.intercom.io",
37
+ "https://nexus-websocket-a.intercom.io",
38
+ "https://nexus-websocket-b.intercom.io",
39
+ "https://uploads.intercomcdn.com",
40
+ "https://uploads.intercomusercontent.com",
41
+ "https://*.amazonaws.com",
42
+ "https://*.s3.amazonaws.com",
43
+ "https://*.s3.us-east-2.amazonaws.com",
44
+ "https://*.s3.us-east-1.amazonaws.com",
45
+ "https://*.s3.us-west-1.amazonaws.com",
46
+ "https://*.s3.us-west-2.amazonaws.com",
47
+ "https://*.s3.af-south-1.amazonaws.com",
48
+ "https://*.s3.ap-east-1.amazonaws.com",
49
+ "https://*.s3.ap-south-1.amazonaws.com",
50
+ "https://*.s3.ap-northeast-2.amazonaws.com",
51
+ "https://*.s3.ap-southeast-1.amazonaws.com",
52
+ "https://*.s3.ap-southeast-2.amazonaws.com",
53
+ "https://*.s3.ap-northeast-1.amazonaws.com",
54
+ "https://*.s3.ca-central-1.amazonaws.com",
55
+ "https://*.s3.cn-north-1.amazonaws.com",
56
+ "https://*.s3.cn-northwest-1.amazonaws.com",
57
+ "https://*.s3.eu-central-1.amazonaws.com",
58
+ "https://*.s3.eu-west-1.amazonaws.com",
59
+ "https://*.s3.eu-west-2.amazonaws.com",
60
+ "https://*.s3.eu-south-1.amazonaws.com",
61
+ "https://*.s3.eu-west-3.amazonaws.com",
62
+ "https://*.s3.eu-north-1.amazonaws.com",
63
+ "https://*.s3.sa-east-1.amazonaws.com",
64
+ "https://*.s3.me-south-1.amazonaws.com",
65
+ "https://*.s3.us-gov-east-1.amazonaws.com",
66
+ "https://*.s3.us-gov-west-1.amazonaws.com",
67
+ "https://api.github.com",
68
+ ],
69
+ "font-src": [
70
+ "'self'",
71
+ "data:",
72
+ "https://cdn.jsdelivr.net",
73
+ "https://fonts.gstatic.com",
74
+ "https://rsms.me",
75
+ "https://maxcdn.bootstrapcdn.com",
76
+ "https://js.intercomcdn.com",
77
+ "https://fonts.intercomcdn.com",
78
+ ],
79
+ "frame-src": ["'self'", "https:"],
80
+ "img-src": ["http:", "https:", "data:", "blob:"],
81
+ "manifest-src": ["'self'"],
82
+ "media-src": [
83
+ "'self'",
84
+ "https://js.intercomcdn.com",
85
+ "https://cdn.budi.live",
86
+ ],
87
+ "worker-src": ["blob:"],
88
+ }
89
+
90
+ export async function contentSecurityPolicy(ctx: any, next: any) {
91
+ try {
92
+ const nonce = crypto.randomBytes(16).toString("base64")
93
+
94
+ const directives = { ...CSP_DIRECTIVES }
95
+ directives["script-src"] = [
96
+ ...CSP_DIRECTIVES["script-src"],
97
+ `'nonce-${nonce}'`,
98
+ ]
99
+
100
+ if (!env.DISABLE_CSP_UNSAFE_INLINE_SCRIPTS) {
101
+ directives["script-src"].push("'unsafe-inline'")
102
+ }
103
+
104
+ ctx.state.nonce = nonce
105
+
106
+ const cspHeader = Object.entries(directives)
107
+ .map(([key, sources]) => `${key} ${sources.join(" ")}`)
108
+ .join("; ")
109
+ ctx.set("Content-Security-Policy", cspHeader)
110
+ await next()
111
+ } catch (err: any) {
112
+ console.error(
113
+ `Error occurred in Content-Security-Policy middleware: ${err}`
114
+ )
115
+ }
116
+ }
117
+
118
+ export default contentSecurityPolicy
@@ -19,5 +19,6 @@ export { default as pino } from "../logging/pino/middleware"
19
19
  export { default as correlation } from "../logging/correlation/middleware"
20
20
  export { default as errorHandling } from "./errorHandling"
21
21
  export { default as querystringToBody } from "./querystringToBody"
22
+ export { default as csp } from "./contentSecurityPolicy"
22
23
  export * as joiValidator from "./joi-validator"
23
24
  export { default as ip } from "./ip"
@@ -0,0 +1,75 @@
1
+ import crypto from "crypto"
2
+ import contentSecurityPolicy from "../contentSecurityPolicy"
3
+
4
+ jest.mock("crypto", () => ({
5
+ randomBytes: jest.fn(),
6
+ randomUUID: jest.fn(),
7
+ }))
8
+
9
+ describe("contentSecurityPolicy middleware", () => {
10
+ let ctx: any
11
+ let next: any
12
+ const mockNonce = "mocked/nonce"
13
+
14
+ beforeEach(() => {
15
+ ctx = {
16
+ state: {},
17
+ set: jest.fn(),
18
+ }
19
+ next = jest.fn()
20
+ // @ts-ignore
21
+ crypto.randomBytes.mockReturnValue(Buffer.from(mockNonce, "base64"))
22
+ })
23
+
24
+ afterEach(() => {
25
+ jest.clearAllMocks()
26
+ })
27
+
28
+ it("should generate a nonce and set it in the script-src directive", async () => {
29
+ await contentSecurityPolicy(ctx, next)
30
+
31
+ expect(ctx.state.nonce).toBe(mockNonce)
32
+ expect(ctx.set).toHaveBeenCalledWith(
33
+ "Content-Security-Policy",
34
+ expect.stringContaining(
35
+ `script-src 'self' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io https://d2l5prqdbvm3op.cloudfront.net https://us-assets.i.posthog.com 'nonce-${mockNonce}'`
36
+ )
37
+ )
38
+ expect(next).toHaveBeenCalled()
39
+ })
40
+
41
+ it("should include all CSP directives in the header", async () => {
42
+ await contentSecurityPolicy(ctx, next)
43
+
44
+ const cspHeader = ctx.set.mock.calls[0][1]
45
+ expect(cspHeader).toContain("default-src 'self'")
46
+ expect(cspHeader).toContain("script-src 'self' 'unsafe-eval'")
47
+ expect(cspHeader).toContain("style-src 'self' 'unsafe-inline'")
48
+ expect(cspHeader).toContain("object-src 'none'")
49
+ expect(cspHeader).toContain("base-uri 'self'")
50
+ expect(cspHeader).toContain("connect-src 'self'")
51
+ expect(cspHeader).toContain("font-src 'self'")
52
+ expect(cspHeader).toContain("frame-src 'self'")
53
+ expect(cspHeader).toContain("img-src http: https: data: blob:")
54
+ expect(cspHeader).toContain("manifest-src 'self'")
55
+ expect(cspHeader).toContain("media-src 'self'")
56
+ expect(cspHeader).toContain("worker-src blob:")
57
+ })
58
+
59
+ it("should handle errors and log an error message", async () => {
60
+ const consoleSpy = jest.spyOn(console, "error").mockImplementation()
61
+ const error = new Error("Test error")
62
+ // @ts-ignore
63
+ crypto.randomBytes.mockImplementation(() => {
64
+ throw error
65
+ })
66
+
67
+ await contentSecurityPolicy(ctx, next)
68
+
69
+ expect(consoleSpy).toHaveBeenCalledWith(
70
+ `Error occurred in Content-Security-Policy middleware: ${error}`
71
+ )
72
+ expect(next).not.toHaveBeenCalled()
73
+ consoleSpy.mockRestore()
74
+ })
75
+ })
@@ -1,5 +1,5 @@
1
1
  import events from "events"
2
- import { newid, timeout } from "../utils"
2
+ import { newid } from "../utils"
3
3
  import { Queue, QueueOptions, JobOptions } from "./queue"
4
4
 
5
5
  interface JobMessage {
@@ -141,7 +141,7 @@ class InMemoryQueue implements Partial<Queue> {
141
141
  } else {
142
142
  pushMessage()
143
143
  }
144
- return {} as any
144
+ return { id: jobId } as any
145
145
  }
146
146
 
147
147
  /**
@@ -184,16 +184,6 @@ class InMemoryQueue implements Partial<Queue> {
184
184
  // do nothing
185
185
  return this as any
186
186
  }
187
-
188
- async waitForCompletion() {
189
- do {
190
- await timeout(50)
191
- } while (this.hasRunningJobs())
192
- }
193
-
194
- hasRunningJobs() {
195
- return this._addCount > this._runCount
196
- }
197
187
  }
198
188
 
199
189
  export default InMemoryQueue
@@ -15,7 +15,7 @@ const QUEUE_LOCK_MS = Duration.fromMinutes(5).toMs()
15
15
  const QUEUE_LOCK_RENEW_INTERNAL_MS = Duration.fromSeconds(30).toMs()
16
16
  // cleanup the queue every 60 seconds
17
17
  const CLEANUP_PERIOD_MS = Duration.fromSeconds(60).toMs()
18
- let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = []
18
+ let QUEUES: BullQueue.Queue[] = []
19
19
  let cleanupInterval: NodeJS.Timeout
20
20
 
21
21
  async function cleanup() {
@@ -45,11 +45,18 @@ export function createQueue<T>(
45
45
  if (opts.jobOptions) {
46
46
  queueConfig.defaultJobOptions = opts.jobOptions
47
47
  }
48
- let queue: any
48
+ let queue: BullQueue.Queue<T>
49
49
  if (!env.isTest()) {
50
50
  queue = new BullQueue(jobQueue, queueConfig)
51
+ } else if (
52
+ process.env.BULL_TEST_REDIS_PORT &&
53
+ !isNaN(+process.env.BULL_TEST_REDIS_PORT)
54
+ ) {
55
+ queue = new BullQueue(jobQueue, {
56
+ redis: { host: "localhost", port: +process.env.BULL_TEST_REDIS_PORT },
57
+ })
51
58
  } else {
52
- queue = new InMemoryQueue(jobQueue, queueConfig)
59
+ queue = new InMemoryQueue(jobQueue, queueConfig) as any
53
60
  }
54
61
  addListeners(queue, jobQueue, opts?.removeStalledCb)
55
62
  QUEUES.push(queue)
@@ -4,3 +4,4 @@ export { generator } from "./structures"
4
4
  export * as testContainerUtils from "./testContainerUtils"
5
5
  export * as utils from "./utils"
6
6
  export * from "./jestUtils"
7
+ export * as queue from "./queue"
@@ -0,0 +1,9 @@
1
+ import { Queue } from "bull"
2
+
3
+ export async function processMessages(queue: Queue) {
4
+ do {
5
+ await queue.whenCurrentJobsFinished()
6
+ } while (await queue.count())
7
+
8
+ await queue.whenCurrentJobsFinished()
9
+ }
@@ -1,4 +1,6 @@
1
1
  import { execSync } from "child_process"
2
+ import { cloneDeep } from "lodash"
3
+ import { GenericContainer, StartedTestContainer } from "testcontainers"
2
4
 
3
5
  const IPV4_PORT_REGEX = new RegExp(`0\\.0\\.0\\.0:(\\d+)->(\\d+)/tcp`, "g")
4
6
 
@@ -106,3 +108,58 @@ export function setupEnv(...envs: any[]) {
106
108
  }
107
109
  }
108
110
  }
111
+
112
+ export async function startContainer(container: GenericContainer) {
113
+ const imageName = (container as any).imageName.string as string
114
+ let key: string = imageName
115
+ if (imageName.includes("@sha256")) {
116
+ key = imageName.split("@")[0]
117
+ }
118
+ key = key.replace(/\//g, "-").replace(/:/g, "-")
119
+
120
+ container = container
121
+ .withReuse()
122
+ .withLabels({ "com.budibase": "true" })
123
+ .withName(`${key}_testcontainer`)
124
+
125
+ let startedContainer: StartedTestContainer | undefined = undefined
126
+ let lastError = undefined
127
+ for (let i = 0; i < 10; i++) {
128
+ try {
129
+ // container.start() is not an idempotent operation, calling `start`
130
+ // modifies the internal state of a GenericContainer instance such that
131
+ // the hash it uses to determine reuse changes. We need to clone the
132
+ // container before calling start to ensure that we're using the same
133
+ // reuse hash every time.
134
+ const containerCopy = cloneDeep(container)
135
+ startedContainer = await containerCopy.start()
136
+ lastError = undefined
137
+ break
138
+ } catch (e: any) {
139
+ lastError = e
140
+ await new Promise(resolve => setTimeout(resolve, 1000))
141
+ }
142
+ }
143
+
144
+ if (!startedContainer) {
145
+ if (lastError) {
146
+ throw lastError
147
+ }
148
+ throw new Error(`failed to start container: ${imageName}`)
149
+ }
150
+
151
+ const info = getContainerById(startedContainer.getId())
152
+ if (!info) {
153
+ throw new Error("Container not found")
154
+ }
155
+
156
+ // Some Docker runtimes, when you expose a port, will bind it to both
157
+ // 127.0.0.1 and ::1, so ipv4 and ipv6. The port spaces of ipv4 and ipv6
158
+ // addresses are not shared, and testcontainers will sometimes give you back
159
+ // the ipv6 port. There's no way to know that this has happened, and if you
160
+ // try to then connect to `localhost:port` you may attempt to bind to the v4
161
+ // address which could be unbound or even an entirely different container. For
162
+ // that reason, we don't use testcontainers' `getExposedPort` function,
163
+ // preferring instead our own method that guaranteed v4 ports.
164
+ return getExposedV4Ports(info)
165
+ }
@@ -1 +1,2 @@
1
1
  export * as time from "./time"
2
+ export * as queue from "./queue"
@@ -0,0 +1,27 @@
1
+ import { Queue } from "bull"
2
+ import { GenericContainer, Wait } from "testcontainers"
3
+ import { startContainer } from "../testContainerUtils"
4
+
5
+ export async function useRealQueues() {
6
+ const ports = await startContainer(
7
+ new GenericContainer("redis")
8
+ .withExposedPorts(6379)
9
+ .withWaitStrategy(
10
+ Wait.forSuccessfulCommand(`redis-cli`).withStartupTimeout(10000)
11
+ )
12
+ )
13
+
14
+ const port = ports.find(x => x.container === 6379)?.host
15
+ if (!port) {
16
+ throw new Error("Redis port not found")
17
+ }
18
+ process.env.BULL_TEST_REDIS_PORT = port.toString()
19
+ }
20
+
21
+ export async function processMessages(queue: Queue) {
22
+ do {
23
+ await queue.whenCurrentJobsFinished()
24
+ } while (await queue.count())
25
+
26
+ await queue.whenCurrentJobsFinished()
27
+ }