@teleporthq/teleport-plugin-next-workflows 0.45.5 → 0.45.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/account-delete-password-check.test.ts +39 -0
- package/__tests__/analytics-track-outcome.test.ts +56 -0
- package/__tests__/public-step-guards.test.ts +26 -0
- package/__tests__/server-job-self-calls-absolute.test.ts +430 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +17 -8
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/nodes/analytics/analytics-track-outcome.d.ts.map +1 -1
- package/dist/cjs/nodes/analytics/analytics-track-outcome.js +12 -0
- package/dist/cjs/nodes/analytics/analytics-track-outcome.js.map +1 -1
- package/dist/cjs/public-step-guards.d.ts +2 -2
- package/dist/cjs/public-step-guards.d.ts.map +1 -1
- package/dist/cjs/public-step-guards.js +11 -8
- package/dist/cjs/public-step-guards.js.map +1 -1
- package/dist/cjs/publish-refusals.js +1 -1
- package/dist/cjs/publish-refusals.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +17 -8
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/nodes/analytics/analytics-track-outcome.d.ts.map +1 -1
- package/dist/esm/nodes/analytics/analytics-track-outcome.js +12 -0
- package/dist/esm/nodes/analytics/analytics-track-outcome.js.map +1 -1
- package/dist/esm/public-step-guards.d.ts +2 -2
- package/dist/esm/public-step-guards.d.ts.map +1 -1
- package/dist/esm/public-step-guards.js +11 -8
- package/dist/esm/public-step-guards.js.map +1 -1
- package/dist/esm/publish-refusals.js +1 -1
- package/dist/esm/publish-refusals.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/account-delete-route-generator.ts +17 -8
- package/src/nodes/analytics/analytics-track-outcome.ts +12 -0
- package/src/public-step-guards.ts +14 -8
- package/src/publish-refusals.ts +1 -1
|
@@ -86,6 +86,45 @@ describe('deleting an account with a password', () => {
|
|
|
86
86
|
expect(withoutPepper.status).toBe(403)
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
+
it('checks with the project’s own pepper first, and still with the old one, as sign-in does', async () => {
|
|
90
|
+
const env = { AUTH_PEPPER: 'pepper-1', AUTH_PEPPER_SECRET: 'secret-1' }
|
|
91
|
+
// Signed up, or signed in (rehashed), after the deploy provisioned the secret.
|
|
92
|
+
const current = await runAccountDeleteRoute({
|
|
93
|
+
userRow: userRow(storedHashOf(PASSWORD, 'secret-1')),
|
|
94
|
+
tables: TABLES,
|
|
95
|
+
requestBody: { password: PASSWORD },
|
|
96
|
+
env,
|
|
97
|
+
})
|
|
98
|
+
expect(current.status).toBe(200)
|
|
99
|
+
|
|
100
|
+
// Not signed in since: the hash is still on the old pepper.
|
|
101
|
+
const earlier = await runAccountDeleteRoute({
|
|
102
|
+
userRow: userRow(storedHashOf(PASSWORD, 'pepper-1')),
|
|
103
|
+
tables: TABLES,
|
|
104
|
+
requestBody: { password: PASSWORD },
|
|
105
|
+
env,
|
|
106
|
+
})
|
|
107
|
+
expect(earlier.status).toBe(200)
|
|
108
|
+
|
|
109
|
+
const wrong = await runAccountDeleteRoute({
|
|
110
|
+
userRow: userRow(storedHashOf(PASSWORD, 'secret-1')),
|
|
111
|
+
tables: TABLES,
|
|
112
|
+
requestBody: { password: 'not it' },
|
|
113
|
+
env,
|
|
114
|
+
})
|
|
115
|
+
expect(wrong.status).toBe(403)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('takes an unresolved secret placeholder for no secret', async () => {
|
|
119
|
+
const run = await runAccountDeleteRoute({
|
|
120
|
+
userRow: userRow(storedHashOf(PASSWORD, 'pepper-1')),
|
|
121
|
+
tables: TABLES,
|
|
122
|
+
requestBody: { password: PASSWORD },
|
|
123
|
+
env: { AUTH_PEPPER: 'pepper-1', AUTH_PEPPER_SECRET: 'teleporthq.secrets.AUTH_PEPPER_SECRET' },
|
|
124
|
+
})
|
|
125
|
+
expect(run.status).toBe(200)
|
|
126
|
+
})
|
|
127
|
+
|
|
89
128
|
it('refuses any password for an account that has none stored (a social sign-in)', async () => {
|
|
90
129
|
const run = await runAccountDeleteRoute({
|
|
91
130
|
userRow: userRow(null),
|
|
@@ -176,6 +176,62 @@ describe('analytics-track-outcome node', () => {
|
|
|
176
176
|
})
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
+
describe('server: an outcome the analytics service takes only from a browser', () => {
|
|
180
|
+
// The service's server endpoint counts `purchase` and `booking_made` alone;
|
|
181
|
+
// a lead, sign-up, newsletter, search or custom outcome comes from the page.
|
|
182
|
+
const configured = (calls: Array<{ body: string }>) =>
|
|
183
|
+
evalHandler(generator.generateServerHandler?.() ?? '', {
|
|
184
|
+
globalThis: {
|
|
185
|
+
process: {
|
|
186
|
+
env: {
|
|
187
|
+
NEXT_PUBLIC_TELEPORT_ANALYTICS_URL: 'https://analytics.example',
|
|
188
|
+
NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY: 'tpa_key',
|
|
189
|
+
TELEPORT_ANALYTICS_SERVER_KEY: 'tpas_secret',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
fetch: (_url: string, init: { body: string }) => {
|
|
194
|
+
calls.push(init)
|
|
195
|
+
return Promise.resolve({ status: 202 })
|
|
196
|
+
},
|
|
197
|
+
AbortController: undefined,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
let warn: jest.SpyInstance
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
203
|
+
})
|
|
204
|
+
afterEach(() => warn.mockRestore())
|
|
205
|
+
|
|
206
|
+
it('is refused as browser-only, with the reason in the logs, and never sent', async () => {
|
|
207
|
+
const calls: Array<{ body: string }> = []
|
|
208
|
+
const report = configured(calls)
|
|
209
|
+
for (const name of ['lead', 'signup', 'newsletter', 'search', 'custom:demo_booked']) {
|
|
210
|
+
expect(await report({ name }, {})).toEqual({ sent: false, reason: 'browser-only' })
|
|
211
|
+
expect(await report({ name, outcomeId: 'row-1' }, {})).toEqual({
|
|
212
|
+
sent: false,
|
|
213
|
+
reason: 'browser-only',
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
expect(calls).toEqual([])
|
|
217
|
+
expect(warn).toHaveBeenCalledWith(
|
|
218
|
+
'[analytics] the "lead" outcome was not reported: only a visitor\'s browser can report it, never a server step'
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('says so in the logs when a money outcome has no id to report', async () => {
|
|
223
|
+
const calls: Array<{ body: string }> = []
|
|
224
|
+
expect(await configured(calls)({ name: 'booking_made', value: '40' }, {})).toEqual({
|
|
225
|
+
sent: false,
|
|
226
|
+
reason: 'no-outcome-id',
|
|
227
|
+
})
|
|
228
|
+
expect(calls).toEqual([])
|
|
229
|
+
expect(warn).toHaveBeenCalledWith(
|
|
230
|
+
'[analytics] the "booking_made" outcome was not reported: it has no order or booking id'
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
179
235
|
it('server: answers not-configured without the key and never throws on a network failure', async () => {
|
|
180
236
|
const unconfigured = evalHandler(generator.generateServerHandler?.() ?? '', {
|
|
181
237
|
globalThis: { process: { env: {} } },
|
|
@@ -3,6 +3,7 @@ import adminFileWorkflows from './fixtures/admin-file-workflows.json'
|
|
|
3
3
|
import {
|
|
4
4
|
PUBLIC_STEP_REFUSAL_CODE,
|
|
5
5
|
PublicStepRefusalError,
|
|
6
|
+
assertNoPublicRestrictedSteps,
|
|
6
7
|
findPublicRestrictedSteps,
|
|
7
8
|
} from '../src/public-step-guards'
|
|
8
9
|
|
|
@@ -234,6 +235,31 @@ describe('restricted steps on a public route', () => {
|
|
|
234
235
|
expect(error).toBeInstanceOf(PublicStepRefusalError)
|
|
235
236
|
})
|
|
236
237
|
|
|
238
|
+
it('on a site without sign-in, says to enable authentication before any role can be asked', () => {
|
|
239
|
+
const workflows = {
|
|
240
|
+
workflows: { w1: pageWorkflow([node('n1', 'file-storage-delete', 'Delete Upload')]) },
|
|
241
|
+
}
|
|
242
|
+
const refusedWith = (authentication: unknown): string => {
|
|
243
|
+
try {
|
|
244
|
+
assertNoPublicRestrictedSteps(workflows, authentication as never)
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return shownSentence(error as Error)
|
|
247
|
+
}
|
|
248
|
+
throw new Error('not refused')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const withoutSignIn of [refusedWith(null), refusedWith({ enabled: false, roles: [] })]) {
|
|
252
|
+
expect(withoutSignIn).toContain('"Delete Upload"')
|
|
253
|
+
expect(withoutSignIn).toContain('has no sign-in')
|
|
254
|
+
expect(withoutSignIn).toContain('enable authentication')
|
|
255
|
+
expect(withoutSignIn).not.toContain('let only the role')
|
|
256
|
+
expect(withoutSignIn.length).toBeLessThanOrEqual(400)
|
|
257
|
+
}
|
|
258
|
+
const withSignIn = refusedWith({ enabled: true, roles: ['admin', 'user'] })
|
|
259
|
+
expect(withSignIn).toContain('let only the role "admin" start it')
|
|
260
|
+
expect(withSignIn).not.toContain('has no sign-in')
|
|
261
|
+
})
|
|
262
|
+
|
|
237
263
|
it('stays one short sentence for many steps', () => {
|
|
238
264
|
const nodes = Array.from({ length: 12 }, (_, i) =>
|
|
239
265
|
node(
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor no-any */
|
|
2
|
+
import * as http from 'http'
|
|
3
|
+
import { AddressInfo } from 'net'
|
|
4
|
+
import * as nodePath from 'path'
|
|
5
|
+
import * as vm from 'vm'
|
|
6
|
+
import { NextWorkflowProjectPlugin } from '../src/workflow-project-plugin'
|
|
7
|
+
import { generateHandlerFunction } from '../src/api-route-generator'
|
|
8
|
+
import { withEnv } from './_helpers/fake-fetch'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A scheduled workflow on a published site failed every run with HTTP 500
|
|
12
|
+
* "Failed to parse URL from /api/data/<id>/raw-query": its data step called the
|
|
13
|
+
* site's own data API by a relative address, which a browser resolves against
|
|
14
|
+
* the page and Node's fetch refuses. A server job has no page, so each route
|
|
15
|
+
* that runs one hands its steps the absolute origin (`context.__baseUrl`).
|
|
16
|
+
*
|
|
17
|
+
* Every kind of server job is run here as the deployment runs it — the
|
|
18
|
+
* generated files, Node's own fetch, and a real HTTP server standing in for the
|
|
19
|
+
* site — so a relative self-call fails exactly as it did in production.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const DS = 'ds-store'
|
|
23
|
+
const ref = (nodeId: string, path: string[]) => ({ type: 'workflowContext', nodeId, path })
|
|
24
|
+
|
|
25
|
+
const CART_ROWS = [{ cart_id: 'cart-1', email: 'a@shop.test' }]
|
|
26
|
+
|
|
27
|
+
/** The predefined "Abandoned Cart Reminder", reduced to its database steps. */
|
|
28
|
+
const CART_REMINDER = {
|
|
29
|
+
id: 'wf-cart-reminder',
|
|
30
|
+
name: 'Abandoned Cart Reminder',
|
|
31
|
+
trigger: {
|
|
32
|
+
type: 'event-cron-triggered',
|
|
33
|
+
nodeId: 't',
|
|
34
|
+
scope: 'global',
|
|
35
|
+
config: { schedule: '0 * * * *', urlPath: 'cron_cartreminder' },
|
|
36
|
+
},
|
|
37
|
+
nodes: [
|
|
38
|
+
{
|
|
39
|
+
id: 'scan',
|
|
40
|
+
type: 'data-raw-query',
|
|
41
|
+
label: 'Scan Abandoned Carts',
|
|
42
|
+
config: {
|
|
43
|
+
dataSourceId: DS,
|
|
44
|
+
query:
|
|
45
|
+
'SELECT c.id AS cart_id FROM teleport_cart c WHERE c.updated_at < NOW() - make_interval(hours => $1::int)',
|
|
46
|
+
params: ['2'],
|
|
47
|
+
},
|
|
48
|
+
stepNumber: 1,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'loop',
|
|
52
|
+
type: 'general-loop',
|
|
53
|
+
label: 'Loop Over Each Abandoned Cart',
|
|
54
|
+
config: { loopType: 'forEach', collection: ref('scan', ['result']), itemVariable: 'cart' },
|
|
55
|
+
stepNumber: 2,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'record',
|
|
59
|
+
type: 'data-create-item',
|
|
60
|
+
label: 'Record Abandoned Cart',
|
|
61
|
+
config: {
|
|
62
|
+
dataSourceId: DS,
|
|
63
|
+
tableName: 'teleport_abandoned_carts',
|
|
64
|
+
columnMappings: [{ column: 'cart_id', value: ref('loop', ['currentItem', 'cart_id']) }],
|
|
65
|
+
},
|
|
66
|
+
stepNumber: 3,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
edges: [
|
|
70
|
+
{ id: 'e1', source: 't', target: 'scan' },
|
|
71
|
+
{ id: 'e2', source: 'scan', target: 'loop' },
|
|
72
|
+
{
|
|
73
|
+
id: 'e3',
|
|
74
|
+
source: 'loop',
|
|
75
|
+
target: 'record',
|
|
76
|
+
sourceHandle: 'loop',
|
|
77
|
+
targetHandle: 'loop-body-in',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'e4',
|
|
81
|
+
source: 'record',
|
|
82
|
+
target: 'loop',
|
|
83
|
+
sourceHandle: 'loop-body-out',
|
|
84
|
+
targetHandle: 'loop-back',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A shared step the store's jobs call: its database read runs in its own server segment route. */
|
|
90
|
+
const RELEASE_NODE = {
|
|
91
|
+
id: 'cn-release',
|
|
92
|
+
name: 'Release Expired Holds',
|
|
93
|
+
parameters: [] as unknown[],
|
|
94
|
+
nodes: [
|
|
95
|
+
{
|
|
96
|
+
id: 'release-query',
|
|
97
|
+
type: 'data-raw-query',
|
|
98
|
+
label: 'Release Expired Holds',
|
|
99
|
+
config: { dataSourceId: DS, query: "UPDATE teleport_orders SET status = 'expired'" },
|
|
100
|
+
stepNumber: 0,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
edges: [] as unknown[],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const callRelease = (id: string, stepNumber: number) => ({
|
|
107
|
+
id,
|
|
108
|
+
type: 'general-custom-node',
|
|
109
|
+
label: 'Release Expired Holds',
|
|
110
|
+
config: { customNodeId: 'cn-release', parameters: [] as unknown[] },
|
|
111
|
+
stepNumber,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const RELEASE_CRON = {
|
|
115
|
+
id: 'wf-release',
|
|
116
|
+
name: 'Release Abandoned Checkouts',
|
|
117
|
+
trigger: {
|
|
118
|
+
type: 'event-cron-triggered',
|
|
119
|
+
nodeId: 't',
|
|
120
|
+
scope: 'global',
|
|
121
|
+
config: { schedule: '*/15 * * * *', urlPath: 'cron_release' },
|
|
122
|
+
},
|
|
123
|
+
nodes: [callRelease('release-call', 1)],
|
|
124
|
+
edges: [{ id: 'r1', source: 't', target: 'release-call' }],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const SYNC_WEBHOOK = {
|
|
128
|
+
id: 'wf-sync',
|
|
129
|
+
name: 'Inventory Sync',
|
|
130
|
+
trigger: { type: 'event-webhook-received', nodeId: 't', scope: 'global', config: {} },
|
|
131
|
+
webhookConfig: { urlPath: 'webhooks/inventory-sync', httpMethod: 'POST' },
|
|
132
|
+
nodes: [
|
|
133
|
+
{
|
|
134
|
+
id: 'sync-query',
|
|
135
|
+
type: 'data-raw-query',
|
|
136
|
+
label: 'Read Stock',
|
|
137
|
+
config: { dataSourceId: DS, query: 'SELECT id, stock FROM teleport_products' },
|
|
138
|
+
stepNumber: 1,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
edges: [{ id: 's1', source: 't', target: 'sync-query' }],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ORDER_AUTOMATION = {
|
|
145
|
+
id: 'wf-order-placed',
|
|
146
|
+
name: 'When an order is placed',
|
|
147
|
+
trigger: { type: 'event-business-event', nodeId: 't', scope: 'global', config: {} },
|
|
148
|
+
nodes: [
|
|
149
|
+
{
|
|
150
|
+
id: 'auto-query',
|
|
151
|
+
type: 'data-raw-query',
|
|
152
|
+
label: 'Read The Order',
|
|
153
|
+
config: { dataSourceId: DS, query: 'SELECT id FROM teleport_orders LIMIT 1' },
|
|
154
|
+
stepNumber: 1,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
edges: [{ id: 'a1', source: 't', target: 'auto-query' }],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ENV: Record<string, string | undefined> = {
|
|
161
|
+
NEXTAUTH_SECRET: 'app-secret',
|
|
162
|
+
NEXTAUTH_URL: undefined,
|
|
163
|
+
TELEPORT_CRON_ALLOW_UNSIGNED: 'true',
|
|
164
|
+
TELEPORT_CRON_SECRET: undefined,
|
|
165
|
+
VERCEL: undefined,
|
|
166
|
+
VERCEL_AUTOMATION_BYPASS_SECRET: undefined,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const generateFiles = async (): Promise<Map<string, string>> => {
|
|
170
|
+
const structure: any = {
|
|
171
|
+
uidl: {
|
|
172
|
+
name: 'Store',
|
|
173
|
+
globals: { settings: { title: 'Store', language: 'en' }, env: { NEXTAUTH_SECRET: 'k' } },
|
|
174
|
+
root: {
|
|
175
|
+
name: 'Root',
|
|
176
|
+
stateDefinitions: {
|
|
177
|
+
route: { type: 'string', defaultValue: 'Home', values: [{ value: 'Home' }] },
|
|
178
|
+
},
|
|
179
|
+
node: { type: 'element', content: { elementType: 'container', children: [] } },
|
|
180
|
+
},
|
|
181
|
+
components: {},
|
|
182
|
+
dataSources: { [DS]: { id: DS, type: 'teleport', name: 'Store', config: {} } },
|
|
183
|
+
workflows: {
|
|
184
|
+
workflows: JSON.parse(
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
[CART_REMINDER.id]: CART_REMINDER,
|
|
187
|
+
[RELEASE_CRON.id]: RELEASE_CRON,
|
|
188
|
+
[SYNC_WEBHOOK.id]: SYNC_WEBHOOK,
|
|
189
|
+
})
|
|
190
|
+
),
|
|
191
|
+
customNodes: JSON.parse(JSON.stringify({ [RELEASE_NODE.id]: RELEASE_NODE })),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
strategy: { pages: { options: {} } },
|
|
195
|
+
files: new Map(),
|
|
196
|
+
dependencies: {},
|
|
197
|
+
devDependencies: {},
|
|
198
|
+
}
|
|
199
|
+
await new NextWorkflowProjectPlugin().runAfter(structure)
|
|
200
|
+
const files = new Map<string, string>()
|
|
201
|
+
for (const entry of structure.files.values()) {
|
|
202
|
+
for (const file of entry.files) {
|
|
203
|
+
files.set(
|
|
204
|
+
[...entry.path, `${file.name}.${file.fileType || 'js'}`].join('/'),
|
|
205
|
+
String(file.content)
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
files.set(
|
|
210
|
+
'lib/automations/handlers/wf-order-placed.js',
|
|
211
|
+
generateHandlerFunction(JSON.parse(JSON.stringify(ORDER_AUTOMATION)), {
|
|
212
|
+
[RELEASE_NODE.id]: RELEASE_NODE,
|
|
213
|
+
} as any)
|
|
214
|
+
)
|
|
215
|
+
return files
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface Reply {
|
|
219
|
+
status: number
|
|
220
|
+
body: any
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface Site {
|
|
224
|
+
origin: string
|
|
225
|
+
/** Every request the site received: its path and the Host it was sent to. */
|
|
226
|
+
received: Array<{ path: string; host: string }>
|
|
227
|
+
call: (routeFile: string, body?: unknown) => Promise<Reply>
|
|
228
|
+
load: (path: string) => any
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The generated files served by a real HTTP server: the data API answers with
|
|
233
|
+
* rows, and a call to one of the site's own workflow routes runs that route.
|
|
234
|
+
*/
|
|
235
|
+
const startSite = async (files: Map<string, string>): Promise<Site & { close: () => void }> => {
|
|
236
|
+
let origin = ''
|
|
237
|
+
const received: Site['received'] = []
|
|
238
|
+
const cache = new Map<string, { exports: any }>()
|
|
239
|
+
const overrides: Record<string, unknown> = {
|
|
240
|
+
'lib/automations/store.js': {
|
|
241
|
+
appBaseUrl: () => origin,
|
|
242
|
+
saveWait: async (): Promise<void> => undefined,
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const load = (path: string): any => {
|
|
247
|
+
if (Object.prototype.hasOwnProperty.call(overrides, path)) {
|
|
248
|
+
return overrides[path]
|
|
249
|
+
}
|
|
250
|
+
const cached = cache.get(path)
|
|
251
|
+
if (cached) {
|
|
252
|
+
return cached.exports
|
|
253
|
+
}
|
|
254
|
+
const source = files.get(path)
|
|
255
|
+
if (source === undefined) {
|
|
256
|
+
throw new Error(`Cannot find module '${path}'`)
|
|
257
|
+
}
|
|
258
|
+
const module = { exports: {} as any }
|
|
259
|
+
cache.set(path, module)
|
|
260
|
+
const dir = nodePath.posix.dirname(path)
|
|
261
|
+
const localRequire = (id: string): unknown => {
|
|
262
|
+
if (id.startsWith('.')) {
|
|
263
|
+
const joined = nodePath.posix.normalize(nodePath.posix.join(dir, id))
|
|
264
|
+
const found = [joined, `${joined}.js`, `${joined}/index.js`].find(
|
|
265
|
+
(candidate) =>
|
|
266
|
+
files.has(candidate) || Object.prototype.hasOwnProperty.call(overrides, candidate)
|
|
267
|
+
)
|
|
268
|
+
return load(found || `${joined}.js`)
|
|
269
|
+
}
|
|
270
|
+
if (id === 'next-auth/jwt') {
|
|
271
|
+
return { getToken: async (): Promise<null> => null }
|
|
272
|
+
}
|
|
273
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
274
|
+
return require(id)
|
|
275
|
+
}
|
|
276
|
+
new Function('module', 'exports', 'require', source)(module, module.exports, localRequire)
|
|
277
|
+
return module.exports
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const run = async (
|
|
281
|
+
routeFile: string,
|
|
282
|
+
headers: Record<string, string | string[] | undefined>,
|
|
283
|
+
body: unknown
|
|
284
|
+
): Promise<Reply> => {
|
|
285
|
+
const reply: Reply = { status: 200, body: null }
|
|
286
|
+
const res: any = {
|
|
287
|
+
setHeader: (): any => res,
|
|
288
|
+
status(code: number) {
|
|
289
|
+
reply.status = code
|
|
290
|
+
return res
|
|
291
|
+
},
|
|
292
|
+
json(payload: unknown) {
|
|
293
|
+
reply.body = payload
|
|
294
|
+
return res
|
|
295
|
+
},
|
|
296
|
+
end() {
|
|
297
|
+
return res
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
const handler = load(routeFile)
|
|
301
|
+
await (handler.default || handler)(
|
|
302
|
+
{
|
|
303
|
+
method: 'POST',
|
|
304
|
+
url: '/' + routeFile.replace(/^pages\//, '').replace(/\.js$/, ''),
|
|
305
|
+
query: {},
|
|
306
|
+
headers,
|
|
307
|
+
body,
|
|
308
|
+
},
|
|
309
|
+
res
|
|
310
|
+
)
|
|
311
|
+
return reply
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const server = http.createServer((req, res) => {
|
|
315
|
+
const chunks: Buffer[] = []
|
|
316
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
317
|
+
req.on('end', async () => {
|
|
318
|
+
const path = new URL(req.url || '/', 'http://site').pathname
|
|
319
|
+
received.push({ path, host: String(req.headers.host) })
|
|
320
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
321
|
+
const body = text ? JSON.parse(text) : undefined
|
|
322
|
+
let reply: Reply = { status: 404, body: { error: 'not found' } }
|
|
323
|
+
if (/^\/api\/data\/[^/]+\/[^/]+$/.test(path)) {
|
|
324
|
+
reply = { status: 200, body: { success: true, rows: CART_ROWS, data: CART_ROWS[0] } }
|
|
325
|
+
} else if (files.has(`pages${path}.js`)) {
|
|
326
|
+
reply = await run(`pages${path}.js`, req.headers, body)
|
|
327
|
+
}
|
|
328
|
+
res.statusCode = reply.status
|
|
329
|
+
res.setHeader('content-type', 'application/json')
|
|
330
|
+
res.end(JSON.stringify(reply.body))
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
334
|
+
const { port } = server.address() as AddressInfo
|
|
335
|
+
origin = `http://127.0.0.1:${port}`
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
origin,
|
|
339
|
+
received,
|
|
340
|
+
load,
|
|
341
|
+
call: (routeFile, body = {}) =>
|
|
342
|
+
run(routeFile, { host: `127.0.0.1:${port}`, 'content-type': 'application/json' }, body),
|
|
343
|
+
close: () => server.close(),
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Node's own fetch (undici), as the deployment has it: the one that refuses a
|
|
348
|
+
// relative address. Jest's sandbox has no global fetch of its own.
|
|
349
|
+
const nodeFetch = vm.runInThisContext('fetch')
|
|
350
|
+
|
|
351
|
+
const silenced = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
352
|
+
const sink = console as unknown as Record<string, unknown>
|
|
353
|
+
const levels = ['warn', 'error', 'log', 'info']
|
|
354
|
+
const saved = levels.map((level) => sink[level])
|
|
355
|
+
levels.forEach((level) => {
|
|
356
|
+
sink[level] = (): undefined => undefined
|
|
357
|
+
})
|
|
358
|
+
try {
|
|
359
|
+
return await run()
|
|
360
|
+
} finally {
|
|
361
|
+
levels.forEach((level, index) => {
|
|
362
|
+
sink[level] = saved[index]
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const onSite = async <T>(run: (site: Site) => Promise<T>): Promise<T> => {
|
|
368
|
+
const site = await startSite(await generateFiles())
|
|
369
|
+
const scope = globalThis as any
|
|
370
|
+
const previous = scope.fetch
|
|
371
|
+
scope.fetch = nodeFetch
|
|
372
|
+
try {
|
|
373
|
+
return await withEnv(ENV, () => silenced(() => run(site)))
|
|
374
|
+
} finally {
|
|
375
|
+
scope.fetch = previous
|
|
376
|
+
site.close()
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The paths the site received, each asserted to have come to its own address. */
|
|
381
|
+
const selfCalls = (site: Site): string[] => {
|
|
382
|
+
const host = new URL(site.origin).host
|
|
383
|
+
site.received.forEach((call) => expect(call.host).toBe(host))
|
|
384
|
+
return site.received.map((call) => call.path)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const webhookDelivery = (payload: unknown) => Buffer.from(JSON.stringify(payload))
|
|
388
|
+
|
|
389
|
+
describe("a server job calls the site's own routes by their absolute address", () => {
|
|
390
|
+
it('a scheduled workflow reads with a raw query, then records each row', async () => {
|
|
391
|
+
await onSite(async (site) => {
|
|
392
|
+
const reply = await site.call('pages/api/workflows/cron_cartreminder.js')
|
|
393
|
+
|
|
394
|
+
expect(reply).toEqual({ status: 200, body: expect.objectContaining({ success: true }) })
|
|
395
|
+
expect(selfCalls(site)).toEqual([`/api/data/${DS}/raw-query`, `/api/data/${DS}/create`])
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it("a scheduled workflow runs a shared step whose query is in that step's own route", async () => {
|
|
400
|
+
await onSite(async (site) => {
|
|
401
|
+
const reply = await site.call('pages/api/workflows/cron_release.js')
|
|
402
|
+
|
|
403
|
+
expect(reply).toEqual({ status: 200, body: expect.objectContaining({ success: true }) })
|
|
404
|
+
const [segmentRoute, ...rest] = selfCalls(site)
|
|
405
|
+
expect(segmentRoute).toMatch(/^\/api\/workflows\/release-expired-holds-/)
|
|
406
|
+
expect(rest).toEqual([`/api/data/${DS}/raw-query`])
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('a webhook reads with a raw query', async () => {
|
|
411
|
+
await onSite(async (site) => {
|
|
412
|
+
const reply = await site.call(
|
|
413
|
+
'pages/api/webhooks/inventory-sync.js',
|
|
414
|
+
webhookDelivery({ event: 'stock' })
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
expect(reply).toEqual({ status: 200, body: { received: true } })
|
|
418
|
+
expect(selfCalls(site)).toEqual([`/api/data/${DS}/raw-query`])
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('an automation reads with a raw query', async () => {
|
|
423
|
+
await onSite(async (site) => {
|
|
424
|
+
const handler = site.load('lib/automations/handlers/wf-order-placed.js')
|
|
425
|
+
await handler.run({ event: { id: 'e1', type: 'order-placed' }, order: { id: 'o1' } }, {})
|
|
426
|
+
|
|
427
|
+
expect(selfCalls(site)).toEqual([`/api/data/${DS}/raw-query`])
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAcxE,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAGtB,kBAAkB,CAAC,EAAE,2BAA2B,CAAA;IAEhD,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAG5B,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,
|
|
1
|
+
{"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAcxE,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAGtB,kBAAkB,CAAC,EAAE,2BAA2B,CAAA;IAEhD,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAG5B,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,MAicpF,CAAA"}
|
|
@@ -275,19 +275,28 @@ function readRequestPassword(req) {
|
|
|
275
275
|
return String(body.password);
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
// The sign-in check (utils/auth/hash-password.js
|
|
279
|
-
// the SHA-256 of the password and
|
|
280
|
-
//
|
|
278
|
+
// The sign-in check (utils/auth/hash-password.js checkPassword): bcrypt over
|
|
279
|
+
// the SHA-256 of the password and a pepper, the project's own
|
|
280
|
+
// (AUTH_PEPPER_SECRET) first, then the one every earlier hash used
|
|
281
|
+
// (AUTH_PEPPER). An account without a stored password (a social sign-in)
|
|
282
|
+
// matches no password.
|
|
281
283
|
function passwordMatches(password, storedHash) {
|
|
282
284
|
if (!storedHash || password.length > 1000) { return false; }
|
|
283
285
|
var bcrypt = require('bcryptjs');
|
|
284
286
|
var crypto = require('crypto');
|
|
285
|
-
var
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
var secret = process.env.AUTH_PEPPER_SECRET;
|
|
288
|
+
var peppers = typeof secret === 'string' && secret && secret.indexOf('teleporthq.secrets.') !== 0
|
|
289
|
+
? [secret, process.env.AUTH_PEPPER || '']
|
|
290
|
+
: [process.env.AUTH_PEPPER || ''];
|
|
291
|
+
for (var i = 0; i < peppers.length; i++) {
|
|
292
|
+
var prepared = crypto.createHash('sha256').update(password + peppers[i]).digest('base64');
|
|
293
|
+
try {
|
|
294
|
+
if (bcrypt.compareSync(prepared, String(storedHash))) { return true; }
|
|
295
|
+
} catch (e) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
290
298
|
}
|
|
299
|
+
return false;
|
|
291
300
|
}
|
|
292
301
|
|
|
293
302
|
${(0, transactional_email_code_1.generateFillTemplateFn)()}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":";;;AA2BA,iEAA0D;AAC1D,qDAAuD;AACvD,uEAAoF;AACpF,yEAOmC;AACnC,qDAAsF;AA0BtF,+EAA+E;AAC/E,oFAAoF;AACvE,QAAA,8BAA8B,GAAG,yDAA8B,CAAA;AAErE,MAAM,0BAA0B,GAAG,CAAC,UAAqC,EAAE,EAAU,EAAE;IAC5F,MAAM,cAAc,GAAG,OAAO,CAAC,kBAAkB,IAAI,OAAO,CAAA;IAC5D,MAAM,QAAQ,GACZ,OAAO,CAAC,aAAa,IAAI,oDAAyB,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;QAC3E,CAAC,CAAC,OAAO,CAAC,aAAa;QACvB,CAAC,CAAC,IAAI,CAAA;IACV,MAAM,mBAAmB,GACvB,OAAO,CAAC,mBAAmB,IAAI,yCAAyC,CAAA;IAC1E,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,KAAK,IAAI,CAAA;IAExD,OAAO,GAAG,IAAA,qCAAoB,GAAE;EAChC,IAAA,kEAAwC,GAAE;2BACjB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;oBACrC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;gCAC1B,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC;yBAC1C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;qBAC5B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;0BAClC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;gCAChC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;wBACxD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,YAAY,IAAI,+BAA+B,CAAC;0BACrE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;0BAC3C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;EACxE,IAAA,qDAA0B,EAAC,UAAU,CAAC;;;;;;;;;;;KAWnC,sCAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyHxB,8BAAY,CAAC,+BAA+B,EAAE
|
|
1
|
+
{"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics-track-outcome.d.ts","sourceRoot":"","sources":["../../../../src/nodes/analytics/analytics-track-outcome.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"analytics-track-outcome.d.ts","sourceRoot":"","sources":["../../../../src/nodes/analytics/analytics-track-outcome.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AA8H/C,eAAO,MAAM,qBAAqB,EAAE,oBASnC,CAAA"}
|
|
@@ -21,6 +21,13 @@ exports.analyticsTrackOutcome = void 0;
|
|
|
21
21
|
* the visitor id is the tracker's alone: a browser may still hold a persistent
|
|
22
22
|
* id from before its consent was withdrawn.
|
|
23
23
|
*
|
|
24
|
+
* The service takes exactly the money outcomes from a server, each with the
|
|
25
|
+
* order or booking id it dedupes a retried report on, and every other outcome
|
|
26
|
+
* from a browser only. So on the server any other name is refused as
|
|
27
|
+
* `browser-only` (an automation, a webhook or a cron has no visitor's page to
|
|
28
|
+
* report it from), and a money outcome without an id as `no-outcome-id`: an id
|
|
29
|
+
* made up here would count a retried report twice. Both say why in the logs.
|
|
30
|
+
*
|
|
24
31
|
* Never throws: an outcome that cannot be reported answers `{ sent: false }`
|
|
25
32
|
* and the workflow continues — analytics must never cost a sale.
|
|
26
33
|
*/
|
|
@@ -56,6 +63,10 @@ const SERVER_BODY = `async function analytics_track_outcome(config, context) {
|
|
|
56
63
|
if (!name) {
|
|
57
64
|
return { sent: false, reason: 'no-name' };
|
|
58
65
|
}
|
|
66
|
+
if (${JSON.stringify(SERVER_ONLY_OUTCOMES)}.indexOf(name) === -1) {
|
|
67
|
+
console.warn('[analytics] the "' + name + '" outcome was not reported: only a visitor\\'s browser can report it, never a server step');
|
|
68
|
+
return { sent: false, reason: 'browser-only' };
|
|
69
|
+
}
|
|
59
70
|
// \`globalThis.process\` (member access): webpack rewrites a bare \`process\`.
|
|
60
71
|
var env = globalThis.process && globalThis.process.env;
|
|
61
72
|
var baseUrl = (env && env.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL) || '';
|
|
@@ -66,6 +77,7 @@ const SERVER_BODY = `async function analytics_track_outcome(config, context) {
|
|
|
66
77
|
}
|
|
67
78
|
// The service dedupes on this id and drops an outcome without one.
|
|
68
79
|
if (config.outcomeId == null || config.outcomeId === '') {
|
|
80
|
+
console.warn('[analytics] the "' + name + '" outcome was not reported: it has no order or booking id');
|
|
69
81
|
return { sent: false, reason: 'no-outcome-id' };
|
|
70
82
|
}
|
|
71
83
|
var ids = context && context.__tpa && typeof context.__tpa === 'object' ? context.__tpa : {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics-track-outcome.js","sourceRoot":"","sources":["../../../../src/nodes/analytics/analytics-track-outcome.ts"],"names":[],"mappings":";;;AAEA
|
|
1
|
+
{"version":3,"file":"analytics-track-outcome.js","sourceRoot":"","sources":["../../../../src/nodes/analytics/analytics-track-outcome.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,cAAc,CAAC,CAAA;AAEzD,MAAM,WAAW,GAAG;;;;;QAKZ,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;EAoB1C,CAAA;AAEF,MAAM,WAAW,GAAG;;;;;QAKZ,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0D1C,CAAA;AAEW,QAAA,qBAAqB,GAAyB;IACzD,QAAQ,EAAE,yBAAyB;IACnC,YAAY,EAAE,WAAW;IACzB,eAAe;QACb,OAAO,WAAW,CAAA;IACpB,CAAC;IACD,qBAAqB;QACnB,OAAO,WAAW,CAAA;IACpB,CAAC;CACF,CAAA"}
|