@teleporthq/teleport-plugin-next-workflows 0.44.6 → 0.44.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__/credential-columns-reply.test.ts +286 -0
- package/__tests__/credential-result-reply.test.ts +509 -0
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +6 -2
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/credential-columns-code.d.ts +17 -0
- package/dist/cjs/credential-columns-code.d.ts.map +1 -0
- package/dist/cjs/credential-columns-code.js +61 -0
- package/dist/cjs/credential-columns-code.js.map +1 -0
- package/dist/cjs/credential-results.d.ts +18 -0
- package/dist/cjs/credential-results.d.ts.map +1 -0
- package/dist/cjs/credential-results.js +72 -0
- package/dist/cjs/credential-results.js.map +1 -0
- package/dist/cjs/data-api-route-generator.d.ts +1 -0
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +13 -5
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +20 -3
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/account/account-hash-password.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-hash-password.js +1 -0
- package/dist/cjs/nodes/account/account-hash-password.js.map +1 -1
- package/dist/cjs/nodes/types.d.ts +6 -0
- package/dist/cjs/nodes/types.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.d.ts +8 -0
- package/dist/cjs/server-runtime-code.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.js +106 -0
- package/dist/cjs/server-runtime-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +6 -2
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/credential-columns-code.d.ts +17 -0
- package/dist/esm/credential-columns-code.d.ts.map +1 -0
- package/dist/esm/credential-columns-code.js +57 -0
- package/dist/esm/credential-columns-code.js.map +1 -0
- package/dist/esm/credential-results.d.ts +18 -0
- package/dist/esm/credential-results.d.ts.map +1 -0
- package/dist/esm/credential-results.js +68 -0
- package/dist/esm/credential-results.js.map +1 -0
- package/dist/esm/data-api-route-generator.d.ts +1 -0
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +11 -3
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +20 -3
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/account/account-hash-password.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-hash-password.js +1 -0
- package/dist/esm/nodes/account/account-hash-password.js.map +1 -1
- package/dist/esm/nodes/types.d.ts +6 -0
- package/dist/esm/nodes/types.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.d.ts +8 -0
- package/dist/esm/server-runtime-code.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.js +106 -0
- package/dist/esm/server-runtime-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/api-route-generator.ts +6 -2
- package/src/credential-columns-code.ts +59 -0
- package/src/credential-results.ts +71 -0
- package/src/data-api-route-generator.ts +11 -3
- package/src/executor-generator.ts +20 -3
- package/src/nodes/account/account-hash-password.ts +1 -0
- package/src/nodes/types.ts +6 -0
- package/src/server-runtime-code.ts +106 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { TableAccess } from '@teleporthq/teleport-shared'
|
|
3
|
+
import { generateDataAPIRoute } from '../src/data-api-route-generator'
|
|
4
|
+
import {
|
|
5
|
+
generateServerSegmentAPIRoute,
|
|
6
|
+
generateStreamingServerSegmentAPIRoute,
|
|
7
|
+
} from '../src/api-route-generator'
|
|
8
|
+
import { generateSharedRuntimeUtilsCode } from '../src/executor-generator'
|
|
9
|
+
import type { WorkflowSegment } from '../src/types'
|
|
10
|
+
import { loadServerRuntime } from './_helpers/load-server-runtime'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The auth users table's credentials — the password hash and the OAuth tokens
|
|
14
|
+
* (`TableAccess.AUTH_CREDENTIAL_COLUMNS`) — never leave the server in a row.
|
|
15
|
+
* A browser read of the data route was already served without them; a create
|
|
16
|
+
* or an update answered the whole row it wrote, and a server segment replied
|
|
17
|
+
* its data nodes' rows to the browser as they came.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const SECRET = 'server-secret'
|
|
21
|
+
|
|
22
|
+
const USER_ROW = {
|
|
23
|
+
id: 'u-1',
|
|
24
|
+
email: 'member@example.com',
|
|
25
|
+
name: 'Member',
|
|
26
|
+
password: '$2a$10$hash',
|
|
27
|
+
access_token: 'oauth-access',
|
|
28
|
+
refresh_token: 'oauth-refresh',
|
|
29
|
+
id_token: 'oauth-id',
|
|
30
|
+
session_state: 'state',
|
|
31
|
+
token_type: 'Bearer',
|
|
32
|
+
scope: 'openid email',
|
|
33
|
+
expires_at: 1767225600,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PROFILE = { id: 'u-1', email: 'member@example.com', name: 'Member' }
|
|
37
|
+
|
|
38
|
+
function bootDataRoute(rows: Array<Record<string, unknown>>) {
|
|
39
|
+
const code = generateDataAPIRoute({ authUsersTableName: 'users' })
|
|
40
|
+
class FakeClient {
|
|
41
|
+
public async connect(): Promise<void> {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
public async end(): Promise<void> {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
public async query(sql: string): Promise<{ rows: unknown[]; rowCount: number }> {
|
|
48
|
+
if (/information_schema/i.test(sql)) {
|
|
49
|
+
return { rows: [], rowCount: 0 }
|
|
50
|
+
}
|
|
51
|
+
return { rows, rowCount: rows.length }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const moduleObj: { exports: any } = { exports: {} }
|
|
55
|
+
const fakeRequire = (name: string): unknown => {
|
|
56
|
+
if (name === 'pg') {
|
|
57
|
+
return { Client: FakeClient }
|
|
58
|
+
}
|
|
59
|
+
if (name === 'next-auth/jwt') {
|
|
60
|
+
return { getToken: async () => ({ id: 'u-1' }) }
|
|
61
|
+
}
|
|
62
|
+
throw new Error('unexpected require: ' + name)
|
|
63
|
+
}
|
|
64
|
+
new Function('require', 'module', 'exports', 'process', 'console', code)(
|
|
65
|
+
fakeRequire,
|
|
66
|
+
moduleObj,
|
|
67
|
+
moduleObj.exports,
|
|
68
|
+
{ env: { NEXTAUTH_SECRET: SECRET, TELEPORT_DB_CONNECTION_STRING: 'postgresql://x' } },
|
|
69
|
+
{ error: (): void => undefined, warn: (): void => undefined, log: (): void => undefined }
|
|
70
|
+
)
|
|
71
|
+
return async (operation: string, body: Record<string, unknown>, headers = {}) => {
|
|
72
|
+
let status = 0
|
|
73
|
+
let payload: any = null
|
|
74
|
+
const res = {
|
|
75
|
+
status(statusCode: number) {
|
|
76
|
+
status = statusCode
|
|
77
|
+
return this
|
|
78
|
+
},
|
|
79
|
+
json(value: unknown) {
|
|
80
|
+
payload = value
|
|
81
|
+
return this
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
await moduleObj.exports(
|
|
85
|
+
{ method: 'POST', query: { params: ['ds-1', operation] }, headers, body },
|
|
86
|
+
res
|
|
87
|
+
)
|
|
88
|
+
return { status, body: payload }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const internal = { 'x-internal-data-secret': SECRET }
|
|
93
|
+
|
|
94
|
+
describe('data route — the row a create or update answers with', () => {
|
|
95
|
+
it('leaves the credentials out of a created users row, for the server caller too', async () => {
|
|
96
|
+
const route = bootDataRoute([USER_ROW])
|
|
97
|
+
const reply = await route(
|
|
98
|
+
'create',
|
|
99
|
+
{ tableName: 'users', columnMappings: [{ column: 'email', value: 'member@example.com' }] },
|
|
100
|
+
internal
|
|
101
|
+
)
|
|
102
|
+
expect(reply.status).toBe(200)
|
|
103
|
+
expect(reply.body).toEqual({ item: PROFILE, id: 'u-1' })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('leaves them out of an updated users row, however the table is spelled', async () => {
|
|
107
|
+
const route = bootDataRoute([USER_ROW])
|
|
108
|
+
for (const tableName of ['users', 'public.users', '"users"']) {
|
|
109
|
+
const reply = await route(
|
|
110
|
+
'update',
|
|
111
|
+
{
|
|
112
|
+
tableName,
|
|
113
|
+
filters: [{ field: 'id', operator: '=', value: 'u-1' }],
|
|
114
|
+
columnMappings: [{ column: 'name', value: 'Member' }],
|
|
115
|
+
},
|
|
116
|
+
internal
|
|
117
|
+
)
|
|
118
|
+
expect(reply.status).toBe(200)
|
|
119
|
+
expect(reply.body).toEqual({ updatedCount: 1, item: PROFILE, id: 'u-1' })
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("keeps another table's columns, even ones named like a credential elsewhere", async () => {
|
|
124
|
+
const card = { id: 'g-1', code: 'GIFT', expires_at: '2027-01-01', scope: 'store' }
|
|
125
|
+
const route = bootDataRoute([card])
|
|
126
|
+
const reply = await route(
|
|
127
|
+
'create',
|
|
128
|
+
{ tableName: 'teleport_gift_cards', columnMappings: [{ column: 'code', value: 'GIFT' }] },
|
|
129
|
+
internal
|
|
130
|
+
)
|
|
131
|
+
expect(reply.body).toEqual({ item: card, id: 'g-1' })
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
interface SegmentReply {
|
|
136
|
+
status: number
|
|
137
|
+
body: { success: boolean; results?: Record<string, any>; error?: string }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function bootSegment(segment: WorkflowSegment, dataReplies: Record<string, unknown>) {
|
|
141
|
+
const source = generateServerSegmentAPIRoute(segment, 'Credentials')
|
|
142
|
+
const runtime = loadServerRuntime()
|
|
143
|
+
const routeModule: { exports: unknown } = { exports: {} }
|
|
144
|
+
new Function('module', 'exports', 'require', source)(
|
|
145
|
+
routeModule,
|
|
146
|
+
routeModule.exports,
|
|
147
|
+
(id: string) => (id.endsWith('server-runtime') ? runtime : {})
|
|
148
|
+
)
|
|
149
|
+
const handler = routeModule.exports as (req: unknown, res: unknown) => Promise<void>
|
|
150
|
+
return async (): Promise<SegmentReply> => {
|
|
151
|
+
const originalFetch = globalThis.fetch
|
|
152
|
+
globalThis.fetch = (async (url: string) => ({
|
|
153
|
+
ok: true,
|
|
154
|
+
status: 200,
|
|
155
|
+
json: async () => dataReplies[String(url).split('/').pop() as string],
|
|
156
|
+
})) as unknown as typeof fetch
|
|
157
|
+
try {
|
|
158
|
+
const reply: SegmentReply = { status: 0, body: { success: false } }
|
|
159
|
+
const res = {
|
|
160
|
+
status(code: number) {
|
|
161
|
+
reply.status = code
|
|
162
|
+
return this
|
|
163
|
+
},
|
|
164
|
+
json(payload: SegmentReply['body']) {
|
|
165
|
+
reply.body = payload
|
|
166
|
+
return this
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
await handler(
|
|
170
|
+
{ method: 'POST', body: { context: {} }, headers: { host: 'shop.example' } },
|
|
171
|
+
res
|
|
172
|
+
)
|
|
173
|
+
return reply
|
|
174
|
+
} finally {
|
|
175
|
+
globalThis.fetch = originalFetch
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const step = (id: string, type: string, stepNumber: number, config: Record<string, unknown>) => ({
|
|
181
|
+
id,
|
|
182
|
+
type,
|
|
183
|
+
label: id,
|
|
184
|
+
executionEnv: 'server' as const,
|
|
185
|
+
stepNumber,
|
|
186
|
+
config,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const segmentOf = (nodes: Array<ReturnType<typeof step>>): WorkflowSegment => ({
|
|
190
|
+
id: 'server-1',
|
|
191
|
+
env: 'server',
|
|
192
|
+
nodeIds: nodes.map((node) => node.id),
|
|
193
|
+
nodes,
|
|
194
|
+
edges: nodes.slice(1).map((node, index) => ({
|
|
195
|
+
id: `e${index}`,
|
|
196
|
+
source: nodes[index].id,
|
|
197
|
+
target: node.id,
|
|
198
|
+
})) as WorkflowSegment['edges'],
|
|
199
|
+
entryNodeIds: [nodes[0].id],
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('server segment — the rows it replies to the browser with', () => {
|
|
203
|
+
const secret = process.env.NEXTAUTH_SECRET
|
|
204
|
+
beforeAll(() => {
|
|
205
|
+
process.env.NEXTAUTH_SECRET = SECRET
|
|
206
|
+
})
|
|
207
|
+
afterAll(() => {
|
|
208
|
+
process.env.NEXTAUTH_SECRET = secret
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('replies a data node of the users table without its credentials', async () => {
|
|
212
|
+
const segment = segmentOf([
|
|
213
|
+
step('read', 'data-select', 1, { dataSourceId: 'ds-1', tableName: 'users' }),
|
|
214
|
+
step('save', 'data-update-item', 2, {
|
|
215
|
+
dataSourceId: 'ds-1',
|
|
216
|
+
tableName: 'users',
|
|
217
|
+
filters: [{ field: 'id', operator: '=', value: 'u-1' }],
|
|
218
|
+
columnMappings: [{ column: 'name', value: 'Member' }],
|
|
219
|
+
}),
|
|
220
|
+
])
|
|
221
|
+
const reply = await bootSegment(segment, {
|
|
222
|
+
select: { rows: [USER_ROW], count: 1 },
|
|
223
|
+
update: { item: USER_ROW, id: 'u-1', updatedCount: 1 },
|
|
224
|
+
})()
|
|
225
|
+
expect(reply.status).toBe(200)
|
|
226
|
+
const results = reply.body.results || {}
|
|
227
|
+
expect(results.read).toEqual({ rows: [PROFILE], count: 1 })
|
|
228
|
+
expect(results.save).toEqual({ ...PROFILE, updatedCount: 1, affected: 1 })
|
|
229
|
+
expect(JSON.stringify(results)).not.toMatch(/hash|oauth-/)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("strips a raw statement's rows of the credentials no other table uses", async () => {
|
|
233
|
+
const segment = segmentOf([
|
|
234
|
+
step('raw', 'data-raw-query', 1, { dataSourceId: 'ds-1', query: 'SELECT * FROM users' }),
|
|
235
|
+
])
|
|
236
|
+
const reply = await bootSegment(segment, { 'raw-query': { rows: [USER_ROW] } })()
|
|
237
|
+
const row = { ...PROFILE, scope: 'openid email', expires_at: 1767225600 }
|
|
238
|
+
expect(reply.body.results?.raw).toEqual({ rows: [row], result: [row] })
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("leaves another table's rows, and every node that reads no rows, as they are", async () => {
|
|
242
|
+
const card = { id: 'g-1', expires_at: '2027-01-01', scope: 'store' }
|
|
243
|
+
const segment = segmentOf([
|
|
244
|
+
step('cards', 'data-select', 1, { dataSourceId: 'ds-1', tableName: 'teleport_gift_cards' }),
|
|
245
|
+
step('form', 'general-custom-js', 2, {
|
|
246
|
+
code: 'function customHandler(params) { return { password: "typed" } }',
|
|
247
|
+
context: 'server',
|
|
248
|
+
}),
|
|
249
|
+
])
|
|
250
|
+
const reply = await bootSegment(segment, { select: { rows: [card], count: 1 } })()
|
|
251
|
+
expect(reply.body.results?.cards).toEqual({ rows: [card], count: 1 })
|
|
252
|
+
expect(reply.body.results?.form).toEqual({ password: 'typed' })
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('signs the result it replies, so a later segment can verify what the browser holds', async () => {
|
|
256
|
+
const segment = segmentOf([
|
|
257
|
+
step('read', 'data-select', 1, { dataSourceId: 'ds-1', tableName: 'users' }),
|
|
258
|
+
])
|
|
259
|
+
const reply = await bootSegment(segment, { select: { rows: [USER_ROW], count: 1 } })()
|
|
260
|
+
const later = loadServerRuntime()
|
|
261
|
+
const context: Record<string, unknown> = { ...reply.body.results }
|
|
262
|
+
later.claimSegmentContext({ signedNodeIds: ['read'], nodes: [], edges: [] }, context)
|
|
263
|
+
expect(context.read).toEqual({ rows: [PROFILE], count: 1 })
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('one credential list', () => {
|
|
268
|
+
it('is the shared AUTH_CREDENTIAL_COLUMNS, in the runtime and in a streamed node result', () => {
|
|
269
|
+
const runtime = generateSharedRuntimeUtilsCode()
|
|
270
|
+
expect(runtime).toContain(JSON.stringify(TableAccess.AUTH_CREDENTIAL_COLUMNS))
|
|
271
|
+
expect(runtime).toContain(JSON.stringify(TableAccess.UNAMBIGUOUS_CREDENTIAL_COLUMNS))
|
|
272
|
+
const streaming = generateStreamingServerSegmentAPIRoute(
|
|
273
|
+
segmentOf([step('ai', 'ai-custom-prompt', 1, { streaming: true })])
|
|
274
|
+
)
|
|
275
|
+
expect(streaming).toContain('utils.withoutCredentialColumns(kept, result)')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('strips a streamed data node the same way the reply does', () => {
|
|
279
|
+
const utils = loadServerRuntime()
|
|
280
|
+
const node = { id: 'read', type: 'data-select', config: { tableName: 'users' } }
|
|
281
|
+
expect(utils.withoutCredentialColumns(node, { rows: [USER_ROW], count: 1 })).toEqual({
|
|
282
|
+
rows: [PROFILE],
|
|
283
|
+
count: 1,
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
})
|