@teleporthq/teleport-plugin-next-workflows 0.43.46 → 0.43.48
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__/auth-env-secret-preservation.test.ts +35 -0
- package/__tests__/auth-nextauth-url.test.ts +28 -0
- package/__tests__/auth-session-cost.test.ts +614 -0
- package/__tests__/custom-js-params-env-invariance.test.ts +134 -0
- package/__tests__/data-node-not-awaited.test.ts +644 -0
- package/__tests__/element-trigger-static-attr-replace.test.ts +99 -0
- package/__tests__/element-visible-trigger-scoping.test.ts +296 -0
- package/__tests__/payment-charge-user-handler-serialization.test.ts +7 -5
- package/__tests__/payment-redirect-and-order-notification.test.ts +6 -4
- package/__tests__/raw-sql-param-binding.test.ts +82 -0
- package/__tests__/reserved-word-state-binding.test.ts +112 -0
- package/__tests__/runtime-stringified-boolean-truthiness.test.ts +76 -0
- package/__tests__/strip-event-handler-attrs.test.ts +59 -0
- package/__tests__/workflow-auth-guard.test.ts +277 -0
- package/dist/cjs/api-route-generator.d.ts +3 -3
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +136 -11
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +67 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +458 -20
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/await-result.d.ts +31 -0
- package/dist/cjs/await-result.d.ts.map +1 -0
- package/dist/cjs/await-result.js +50 -0
- package/dist/cjs/await-result.js.map +1 -0
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +162 -3
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.js +44 -11
- package/dist/cjs/nodes/account/account-get-current.js.map +1 -1
- package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-login.js +10 -1
- package/dist/cjs/nodes/account/account-login.js.map +1 -1
- package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/cjs/raw-sql-param-binding.js +32 -0
- package/dist/cjs/raw-sql-param-binding.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +15 -7
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-auth-generator.d.ts +26 -0
- package/dist/cjs/workflow-auth-generator.d.ts.map +1 -0
- package/dist/cjs/workflow-auth-generator.js +166 -0
- package/dist/cjs/workflow-auth-generator.js.map +1 -0
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +107 -16
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts +2 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +170 -20
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts +3 -3
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +136 -11
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +67 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +453 -19
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/await-result.d.ts +31 -0
- package/dist/esm/await-result.d.ts.map +1 -0
- package/dist/esm/await-result.js +45 -0
- package/dist/esm/await-result.js.map +1 -0
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +162 -3
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-get-current.js +44 -11
- package/dist/esm/nodes/account/account-get-current.js.map +1 -1
- package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-login.js +10 -1
- package/dist/esm/nodes/account/account-login.js.map +1 -1
- package/dist/esm/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/esm/raw-sql-param-binding.js +32 -0
- package/dist/esm/raw-sql-param-binding.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +15 -7
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-auth-generator.d.ts +26 -0
- package/dist/esm/workflow-auth-generator.d.ts.map +1 -0
- package/dist/esm/workflow-auth-generator.js +161 -0
- package/dist/esm/workflow-auth-generator.js.map +1 -0
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +108 -17
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts +2 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +169 -20
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/api-route-generator.ts +139 -11
- package/src/auth-generator.ts +471 -19
- package/src/await-result.ts +52 -0
- package/src/executor-generator.ts +162 -3
- package/src/nodes/account/account-get-current.ts +44 -9
- package/src/nodes/account/account-login.ts +12 -1
- package/src/raw-sql-param-binding.ts +33 -0
- package/src/segment-splitter.ts +15 -7
- package/src/workflow-auth-generator.ts +187 -0
- package/src/workflow-component-plugin.ts +129 -20
- package/src/workflow-project-plugin.ts +186 -19
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { generateSharedRuntimeUtilsCode } from '../src'
|
|
2
|
+
|
|
3
|
+
// Producers that cannot type their output emit booleans as the strings
|
|
4
|
+
// "true"/"false" — the evaluate-auth custom-js most notably (see
|
|
5
|
+
// is-logged-in-gate.ts), plus any inspector text input. `is-true`/`is-false`
|
|
6
|
+
// and coerceForComparison already read those as booleans, but `is-truthy` was
|
|
7
|
+
// plain `!!left`, and the string "false" is truthy in JS: an is-truthy gate on
|
|
8
|
+
// isLoggedIn admitted EVERY visitor, and an is-falsy check never fired.
|
|
9
|
+
|
|
10
|
+
type SharedUtils = {
|
|
11
|
+
evaluateSingleComparison: (cfg: unknown, ctx: Record<string, unknown>) => boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadSharedRuntime(): SharedUtils {
|
|
15
|
+
const src = generateSharedRuntimeUtilsCode()
|
|
16
|
+
const wrapper: { exports: Record<string, unknown> } = { exports: {} }
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
18
|
+
new Function('module', 'exports', src)(wrapper, wrapper.exports)
|
|
19
|
+
return wrapper.exports as unknown as SharedUtils
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('workflow runtime truthiness of stringified booleans', () => {
|
|
23
|
+
const utils = loadSharedRuntime()
|
|
24
|
+
|
|
25
|
+
const isTruthy = (leftValue: unknown) =>
|
|
26
|
+
utils.evaluateSingleComparison({ leftValue, operator: 'is-truthy' }, {})
|
|
27
|
+
const isFalsy = (leftValue: unknown) =>
|
|
28
|
+
utils.evaluateSingleComparison({ leftValue, operator: 'is-falsy' }, {})
|
|
29
|
+
|
|
30
|
+
it('treats the string "false" as false, matching is-false and coerceForComparison', () => {
|
|
31
|
+
expect(isTruthy('false')).toBe(false)
|
|
32
|
+
expect(isFalsy('false')).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps the string "true" true', () => {
|
|
36
|
+
expect(isTruthy('true')).toBe(true)
|
|
37
|
+
expect(isFalsy('true')).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('agrees with is-true/is-false on the same value', () => {
|
|
41
|
+
for (const value of ['true', 'false', true, false]) {
|
|
42
|
+
expect(isTruthy(value)).toBe(
|
|
43
|
+
utils.evaluateSingleComparison({ leftValue: value, operator: 'is-true' }, {})
|
|
44
|
+
)
|
|
45
|
+
expect(isFalsy(value)).toBe(
|
|
46
|
+
utils.evaluateSingleComparison({ leftValue: value, operator: 'is-false' }, {})
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('leaves ordinary JS truthiness untouched', () => {
|
|
52
|
+
expect(isTruthy(true)).toBe(true)
|
|
53
|
+
expect(isTruthy(false)).toBe(false)
|
|
54
|
+
expect(isTruthy('anything')).toBe(true)
|
|
55
|
+
expect(isTruthy('')).toBe(false)
|
|
56
|
+
expect(isTruthy(0)).toBe(false)
|
|
57
|
+
expect(isTruthy(1)).toBe(true)
|
|
58
|
+
expect(isTruthy(null)).toBe(false)
|
|
59
|
+
expect(isTruthy(undefined)).toBe(false)
|
|
60
|
+
expect(isFalsy('')).toBe(true)
|
|
61
|
+
expect(isFalsy(0)).toBe(true)
|
|
62
|
+
expect(isFalsy('anything')).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('normalizes the snake_case and camelCase spellings the same way', () => {
|
|
66
|
+
expect(utils.evaluateSingleComparison({ leftValue: 'false', operator: 'is_truthy' }, {})).toBe(
|
|
67
|
+
false
|
|
68
|
+
)
|
|
69
|
+
expect(utils.evaluateSingleComparison({ leftValue: 'false', operator: 'isTruthy' }, {})).toBe(
|
|
70
|
+
false
|
|
71
|
+
)
|
|
72
|
+
expect(utils.evaluateSingleComparison({ leftValue: 'false', operator: 'is_falsy' }, {})).toBe(
|
|
73
|
+
true
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { stripEventHandlerAttrs } from '../src/workflow-project-plugin'
|
|
2
|
+
|
|
3
|
+
// UIDL `attrs` hold data, never functions — an `on*`-named attr (the alerts
|
|
4
|
+
// fixture shipped `onClick: ""` on the Resolved pill) becomes a non-function
|
|
5
|
+
// JSX event prop that React invokes on click: `func.apply is not a function`.
|
|
6
|
+
// runBefore strips them so behavior stays on `events` / workflow triggers.
|
|
7
|
+
|
|
8
|
+
const element = (name: string, attrs: Record<string, unknown>, children: unknown[] = []) => ({
|
|
9
|
+
type: 'element',
|
|
10
|
+
content: { elementType: 'button', name, attrs, children },
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const projectUidl = (): any => ({
|
|
14
|
+
root: {
|
|
15
|
+
name: 'Root',
|
|
16
|
+
stateDefinitions: {},
|
|
17
|
+
node: element('Page', { id: { type: 'static', content: 'page' } }, [
|
|
18
|
+
element('ResolvedPill', {
|
|
19
|
+
id: { type: 'static', content: 'status-resolved-btn' },
|
|
20
|
+
'data-value': { type: 'static', content: 'Resolved' },
|
|
21
|
+
onClick: { type: 'static', content: '' },
|
|
22
|
+
}),
|
|
23
|
+
]),
|
|
24
|
+
},
|
|
25
|
+
components: {
|
|
26
|
+
Card: {
|
|
27
|
+
name: 'Card',
|
|
28
|
+
node: element('Card', {
|
|
29
|
+
onMouseEnter: { type: 'static', content: 'highlight()' },
|
|
30
|
+
// lowercase second letter — NOT an event handler pattern, must survive
|
|
31
|
+
onboarding: { type: 'static', content: 'step-1' },
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('stripEventHandlerAttrs', () => {
|
|
38
|
+
it('removes on[A-Z]* attrs from pages and components, keeps everything else', () => {
|
|
39
|
+
const uidl = projectUidl()
|
|
40
|
+
stripEventHandlerAttrs(uidl)
|
|
41
|
+
|
|
42
|
+
const pill = uidl.root.node.content.children[0].content
|
|
43
|
+
expect(pill.attrs.onClick).toBeUndefined()
|
|
44
|
+
expect(pill.attrs.id.content).toBe('status-resolved-btn')
|
|
45
|
+
expect(pill.attrs['data-value'].content).toBe('Resolved')
|
|
46
|
+
|
|
47
|
+
const card = uidl.components.Card.node.content
|
|
48
|
+
expect(card.attrs.onMouseEnter).toBeUndefined()
|
|
49
|
+
expect(card.attrs.onboarding.content).toBe('step-1')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('tolerates projects without components or attrs', () => {
|
|
53
|
+
const uidl: any = {
|
|
54
|
+
root: { name: 'Root', node: element('Bare', undefined as any) },
|
|
55
|
+
}
|
|
56
|
+
delete uidl.root.node.content.attrs
|
|
57
|
+
expect(() => stripEventHandlerAttrs(uidl)).not.toThrow()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateWorkflowAuthHelperFile,
|
|
3
|
+
buildWorkflowAuthInjection,
|
|
4
|
+
} from '../src/workflow-auth-generator'
|
|
5
|
+
import {
|
|
6
|
+
generateServerSegmentAPIRoute,
|
|
7
|
+
generateStreamingServerSegmentAPIRoute,
|
|
8
|
+
} from '../src/api-route-generator'
|
|
9
|
+
import { UIDLWorkflowProtection } from '@teleporthq/teleport-types'
|
|
10
|
+
import { WorkflowSegment } from '../src/types'
|
|
11
|
+
|
|
12
|
+
// The generated workflow-auth.js guard is the single stateless enforcement
|
|
13
|
+
// point. Booting it with a mocked next-auth/jwt proves the runtime behaviour;
|
|
14
|
+
// the route generators prove the guard is wired in BEFORE any node runs.
|
|
15
|
+
|
|
16
|
+
type Guard = (
|
|
17
|
+
req: any,
|
|
18
|
+
context: any,
|
|
19
|
+
policy: any
|
|
20
|
+
) => Promise<{ status: number; message: string } | null>
|
|
21
|
+
|
|
22
|
+
/** Boots the emitted guard file with getToken stubbed to read `req.__token`. */
|
|
23
|
+
function bootGuard(secret: string | undefined): Guard {
|
|
24
|
+
const code = generateWorkflowAuthHelperFile()
|
|
25
|
+
const moduleObj: { exports: any } = { exports: {} }
|
|
26
|
+
const fakeRequire = (name: string): any => {
|
|
27
|
+
if (name === 'next-auth/jwt') {
|
|
28
|
+
return { getToken: async ({ req }: any) => (req && req.__token) || null }
|
|
29
|
+
}
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
31
|
+
return require(name)
|
|
32
|
+
}
|
|
33
|
+
// tslint:disable-next-line:function-constructor
|
|
34
|
+
new Function('require', 'module', 'exports', 'process', code)(
|
|
35
|
+
fakeRequire,
|
|
36
|
+
moduleObj,
|
|
37
|
+
moduleObj.exports,
|
|
38
|
+
{ env: { NEXTAUTH_SECRET: secret } }
|
|
39
|
+
)
|
|
40
|
+
return moduleObj.exports.guardWorkflowRequest as Guard
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const reqWith = (extra: Record<string, unknown> = {}): any => ({ headers: {}, ...extra })
|
|
44
|
+
|
|
45
|
+
describe('guardWorkflowRequest (runtime enforcement)', () => {
|
|
46
|
+
const guard = bootGuard('server-secret')
|
|
47
|
+
|
|
48
|
+
it('is a no-op when there is no policy', async () => {
|
|
49
|
+
const context = { n: { userId: 'victim' } }
|
|
50
|
+
expect(await guard(reqWith(), context, null)).toBeNull()
|
|
51
|
+
expect(context.n.userId).toBe('victim')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('rejects with 401 when auth is required and there is no session', async () => {
|
|
55
|
+
const res = await guard(reqWith(), {}, { requiresAuth: true, allowedRoles: [] })
|
|
56
|
+
expect(res).toEqual({ status: 401, message: 'Unauthenticated' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('allows any authenticated user when allowedRoles is empty', async () => {
|
|
60
|
+
const res = await guard(
|
|
61
|
+
reqWith({ __token: { id: 'u1', role: 'user' } }),
|
|
62
|
+
{},
|
|
63
|
+
{
|
|
64
|
+
requiresAuth: true,
|
|
65
|
+
allowedRoles: [],
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
expect(res).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('rejects with 403 when the session role is not allowed', async () => {
|
|
72
|
+
const res = await guard(
|
|
73
|
+
reqWith({ __token: { id: 'u1', role: 'user' } }),
|
|
74
|
+
{},
|
|
75
|
+
{
|
|
76
|
+
requiresAuth: true,
|
|
77
|
+
allowedRoles: ['admin'],
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
expect(res).toEqual({ status: 403, message: 'Forbidden' })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('allows a matching role (incl. roleName / roles[] spellings)', async () => {
|
|
84
|
+
for (const token of [
|
|
85
|
+
{ id: 'u1', role: 'admin' },
|
|
86
|
+
{ id: 'u1', roleName: 'admin' },
|
|
87
|
+
{ id: 'u1', roles: ['admin'] },
|
|
88
|
+
]) {
|
|
89
|
+
expect(
|
|
90
|
+
await guard(
|
|
91
|
+
reqWith({ __token: token }),
|
|
92
|
+
{},
|
|
93
|
+
{ requiresAuth: true, allowedRoles: ['admin'] }
|
|
94
|
+
)
|
|
95
|
+
).toBeNull()
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('OVERWRITES a forged user id with the session id (the IDOR fix)', async () => {
|
|
100
|
+
const context = { resolver: { userId: 'victim-id', email: 'x' } }
|
|
101
|
+
const res = await guard(reqWith({ __token: { id: 'attacker-real-id' } }), context, {
|
|
102
|
+
requiresAuth: true,
|
|
103
|
+
allowedRoles: [],
|
|
104
|
+
userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'resolver', path: ['userId'] }] },
|
|
105
|
+
})
|
|
106
|
+
expect(res).toBeNull()
|
|
107
|
+
expect(context.resolver.userId).toBe('attacker-real-id') // forced to the caller
|
|
108
|
+
expect(context.resolver.email).toBe('x') // other fields untouched
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('binds a nested path and creates missing context nodes', async () => {
|
|
112
|
+
const context: any = {}
|
|
113
|
+
await guard(reqWith({ __token: { id: 'sess-1' } }), context, {
|
|
114
|
+
requiresAuth: false,
|
|
115
|
+
allowedRoles: [],
|
|
116
|
+
userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['user', 'id'] }] },
|
|
117
|
+
})
|
|
118
|
+
expect(context.n.user.id).toBe('sess-1')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('falls back to token.sub when token.id is absent', async () => {
|
|
122
|
+
const context = { n: { userId: 'victim' } }
|
|
123
|
+
await guard(reqWith({ __token: { sub: 'sub-id' } }), context, {
|
|
124
|
+
requiresAuth: false,
|
|
125
|
+
allowedRoles: [],
|
|
126
|
+
userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['userId'] }] },
|
|
127
|
+
})
|
|
128
|
+
expect(context.n.userId).toBe('sub-id')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('leaves the client value for a GUEST (no session) on a non-required userScoped write', async () => {
|
|
132
|
+
// Guest checkout: no token, requiresAuth false → the anonymous client id is
|
|
133
|
+
// kept (there is no server session to bind), and the request proceeds.
|
|
134
|
+
const context = { n: { userId: 'guest-anon-uuid' } }
|
|
135
|
+
const res = await guard(reqWith(), context, {
|
|
136
|
+
requiresAuth: false,
|
|
137
|
+
allowedRoles: [],
|
|
138
|
+
userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['userId'] }] },
|
|
139
|
+
})
|
|
140
|
+
expect(res).toBeNull()
|
|
141
|
+
expect(context.n.userId).toBe('guest-anon-uuid')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('honours the internal server-to-server secret bypass', async () => {
|
|
145
|
+
const res = await guard(
|
|
146
|
+
reqWith({ headers: { 'x-internal-data-secret': 'server-secret' } }),
|
|
147
|
+
{},
|
|
148
|
+
{ requiresAuth: true, allowedRoles: ['admin'] }
|
|
149
|
+
)
|
|
150
|
+
expect(res).toBeNull()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('does not treat a wrong internal secret as a bypass', async () => {
|
|
154
|
+
const res = await guard(
|
|
155
|
+
reqWith({ headers: { 'x-internal-data-secret': 'wrong' } }),
|
|
156
|
+
{},
|
|
157
|
+
{ requiresAuth: true, allowedRoles: [] }
|
|
158
|
+
)
|
|
159
|
+
expect(res).toEqual({ status: 401, message: 'Unauthenticated' })
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('guardWorkflowRequest with next-auth absent / no secret', () => {
|
|
164
|
+
it('treats a missing NEXTAUTH_SECRET as no session (401 when required)', async () => {
|
|
165
|
+
const guard = bootGuard(undefined)
|
|
166
|
+
const res = await guard(
|
|
167
|
+
reqWith({ __token: { id: 'u1' } }),
|
|
168
|
+
{},
|
|
169
|
+
{ requiresAuth: true, allowedRoles: [] }
|
|
170
|
+
)
|
|
171
|
+
expect(res).toEqual({ status: 401, message: 'Unauthenticated' })
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Route wiring
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
const dataSegment = (): WorkflowSegment =>
|
|
180
|
+
({
|
|
181
|
+
id: 'server-1',
|
|
182
|
+
env: 'server',
|
|
183
|
+
nodes: [
|
|
184
|
+
{
|
|
185
|
+
id: 'w1',
|
|
186
|
+
type: 'data-create-item',
|
|
187
|
+
label: 'Write',
|
|
188
|
+
config: { tableName: 'teleport_favourites' },
|
|
189
|
+
stepNumber: 1,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
edges: [],
|
|
193
|
+
} as unknown as WorkflowSegment)
|
|
194
|
+
|
|
195
|
+
const protection: UIDLWorkflowProtection = {
|
|
196
|
+
requiresAuth: true,
|
|
197
|
+
allowedRoles: ['admin'],
|
|
198
|
+
derivedFrom: 'page',
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
describe('route generators wire the guard in', () => {
|
|
202
|
+
it('injects the guard into a protected non-streaming route BEFORE the node loop', () => {
|
|
203
|
+
const code = generateServerSegmentAPIRoute(dataSegment(), 'Admin CRUD', protection)
|
|
204
|
+
expect(code).toContain("require('../../../utils/workflows/workflow-auth')")
|
|
205
|
+
expect(code).toContain('const __WF_AUTH = {')
|
|
206
|
+
expect(code).toContain('__wfAuth.guardWorkflowRequest(req, context, __WF_AUTH)')
|
|
207
|
+
// The guard (and its early 401/403 return) must precede the node loop.
|
|
208
|
+
expect(code.indexOf('guardWorkflowRequest')).toBeLessThan(
|
|
209
|
+
code.indexOf('SEGMENT_CONFIG.nodes.slice()')
|
|
210
|
+
)
|
|
211
|
+
// The baked policy carries the runtime fields, not the build-only derivedFrom.
|
|
212
|
+
expect(code).toContain('"requiresAuth":true')
|
|
213
|
+
expect(code).toContain('"allowedRoles":["admin"]')
|
|
214
|
+
expect(code).not.toContain('derivedFrom')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('injects the guard into a protected streaming route BEFORE the stream starts', () => {
|
|
218
|
+
const code = generateStreamingServerSegmentAPIRoute(dataSegment(), 'Streaming', {
|
|
219
|
+
requiresAuth: true,
|
|
220
|
+
allowedRoles: [],
|
|
221
|
+
derivedFrom: 'graph',
|
|
222
|
+
})
|
|
223
|
+
expect(code).toContain('__wfAuth.guardWorkflowRequest(req, context, __WF_AUTH)')
|
|
224
|
+
// The guard runs before the node loop. `ensureStream()` (which calls
|
|
225
|
+
// res.writeHead for the event-stream) is only invoked DURING node execution,
|
|
226
|
+
// so a 401/403 is sent as an HTTP status before the stream ever starts.
|
|
227
|
+
expect(code.indexOf('guardWorkflowRequest')).toBeLessThan(
|
|
228
|
+
code.indexOf('SEGMENT_CONFIG.nodes.slice()')
|
|
229
|
+
)
|
|
230
|
+
// The only ensureStream CALL (not its definition) is inside the node loop.
|
|
231
|
+
expect(code.indexOf('guardWorkflowRequest')).toBeLessThan(code.lastIndexOf('ensureStream()'))
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('emits NOTHING guard-related for an unprotected route (byte-compatible)', () => {
|
|
235
|
+
const code = generateServerSegmentAPIRoute(dataSegment(), 'Public')
|
|
236
|
+
expect(code).not.toContain('workflow-auth')
|
|
237
|
+
expect(code).not.toContain('__WF_AUTH')
|
|
238
|
+
expect(code).not.toContain('guardWorkflowRequest')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('bakes the userScoped bindings into the policy', () => {
|
|
242
|
+
const code = generateServerSegmentAPIRoute(dataSegment(), 'Favourites', {
|
|
243
|
+
requiresAuth: true,
|
|
244
|
+
allowedRoles: [],
|
|
245
|
+
userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'resolve', path: ['userId'] }] },
|
|
246
|
+
derivedFrom: 'graph',
|
|
247
|
+
})
|
|
248
|
+
expect(code).toContain('"userScoped"')
|
|
249
|
+
expect(code).toContain('"nodeId":"resolve"')
|
|
250
|
+
expect(code).toContain('"path":["userId"]')
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('buildWorkflowAuthInjection', () => {
|
|
255
|
+
it('returns empty pieces when there is no meaningful policy', () => {
|
|
256
|
+
expect(buildWorkflowAuthInjection(undefined)).toEqual({
|
|
257
|
+
requireLine: '',
|
|
258
|
+
policyConst: '',
|
|
259
|
+
guardCall: '',
|
|
260
|
+
})
|
|
261
|
+
expect(
|
|
262
|
+
buildWorkflowAuthInjection({ requiresAuth: false, allowedRoles: [], derivedFrom: 'default' })
|
|
263
|
+
).toEqual({ requireLine: '', policyConst: '', guardCall: '' })
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('emits pieces for a userScoped-only (guest-capable) policy', () => {
|
|
267
|
+
const injection = buildWorkflowAuthInjection({
|
|
268
|
+
requiresAuth: false,
|
|
269
|
+
allowedRoles: [],
|
|
270
|
+
userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['userId'] }] },
|
|
271
|
+
derivedFrom: 'graph',
|
|
272
|
+
})
|
|
273
|
+
expect(injection.requireLine).toContain('workflow-auth')
|
|
274
|
+
expect(injection.policyConst).toContain('"userScoped"')
|
|
275
|
+
expect(injection.guardCall).toContain('guardWorkflowRequest')
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { UIDLWorkflow, UIDLCustomWorkflowNode } from '@teleporthq/teleport-types';
|
|
1
|
+
import { UIDLWorkflow, UIDLCustomWorkflowNode, UIDLWorkflowProtection } from '@teleporthq/teleport-types';
|
|
2
2
|
import { WorkflowSegment } from './types';
|
|
3
|
-
export declare const generateServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string) => string;
|
|
3
|
+
export declare const generateServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string, protection?: UIDLWorkflowProtection) => string;
|
|
4
4
|
export declare const hasStreamingAINode: (segment: WorkflowSegment) => boolean;
|
|
5
|
-
export declare const generateStreamingServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string) => string;
|
|
5
|
+
export declare const generateStreamingServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string, protection?: UIDLWorkflowProtection) => string;
|
|
6
6
|
export declare const generateCronAPIRoute: (workflow: UIDLWorkflow, customNodes?: Record<string, UIDLCustomWorkflowNode>) => string;
|
|
7
7
|
export declare const getAPIRouteFileName: (workflowId: string, segmentId: string, workflowName?: string) => string;
|
|
8
8
|
export declare const getCronRouteFileName: (workflow: UIDLWorkflow) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAIZ,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAIZ,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAqBzC,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,KAClC,MAgXF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,eACR,sBAAsB,KAClC,MAsbF,CAAA;AA0HD,eAAO,MAAM,oBAAoB,aACrB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MAuFF,CAAA;AAED,eAAO,MAAM,mBAAmB,eAClB,MAAM,aACP,MAAM,iBACF,MAAM,KACpB,MAuBF,CAAA;AAED,eAAO,MAAM,oBAAoB,aAAc,YAAY,KAAG,MAa7D,CAAA;AA4RD,eAAO,MAAM,+BAA+B,aAChC,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MAsMF,CAAA;AAED,eAAO,MAAM,uBAAuB,aAAc,YAAY,KAAG,MAiBhE,CAAA;AAED,eAAO,MAAM,mBAAmB,aAAc,YAAY,KAAG,MAAM,EAqBlE,CAAA"}
|