@ossy/platform 3.0.9 → 3.2.0
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/README.md +2 -0
- package/package.json +6 -6
- package/src/Definition.js +1 -1
- package/src/platform-config.schema.js +21 -28
- package/src/tasks/task-service.js +10 -1
- package/src/test/flow-context.js +44 -0
- package/src/test/flow-context.spec.js +44 -0
- package/src/test/flow-runner.js +15 -8
- package/src/test/test.util.js +25 -1
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ The server reads configuration from:
|
|
|
53
53
|
- `build/manifest.json` — produced by `@ossy/app build`.
|
|
54
54
|
- `process.env` — used by integrations for their credentials and by startup hooks.
|
|
55
55
|
|
|
56
|
+
Platform integrations are **boot-time and process-global** via `IntegrationService`. Per-workspace credentials are a separate design — see [WORKSPACE-INTEGRATION-SECRETS.md](../../docs/concepts/WORKSPACE-INTEGRATION-SECRETS.md) (SPEC only; not implemented).
|
|
57
|
+
|
|
56
58
|
Optional environment variables used by the platform itself:
|
|
57
59
|
|
|
58
60
|
| Variable | Description |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,16 +45,16 @@
|
|
|
45
45
|
"@aws-sdk/util-format-url": "^3.972.17",
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
47
47
|
"@ossy/config": "^3.0.9",
|
|
48
|
-
"@ossy/event-store": "^3.0
|
|
48
|
+
"@ossy/event-store": "^3.2.0",
|
|
49
49
|
"@ossy/locale": "^3.0.9",
|
|
50
50
|
"@ossy/manifest": "^3.0.9",
|
|
51
51
|
"@ossy/observability": "^3.0.9",
|
|
52
52
|
"@ossy/policies": "^3.0.9",
|
|
53
53
|
"@ossy/schema": "^3.0.9",
|
|
54
|
-
"@ossy/sdk": "^3.0
|
|
54
|
+
"@ossy/sdk": "^3.2.0",
|
|
55
55
|
"@ossy/tokens": "^3.0.9",
|
|
56
|
-
"@ossy/users": "^3.0
|
|
57
|
-
"@ossy/workspaces": "^3.0
|
|
56
|
+
"@ossy/users": "^3.2.0",
|
|
57
|
+
"@ossy/workspaces": "^3.2.0",
|
|
58
58
|
"cookie-parser": "^1.4.7",
|
|
59
59
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
60
60
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -74,5 +74,5 @@
|
|
|
74
74
|
"src",
|
|
75
75
|
"Dockerfile"
|
|
76
76
|
],
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "b045f1fd7f2fe00c776b9ea96f76083b93fd8556"
|
|
78
78
|
}
|
package/src/Definition.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const Definition = {
|
|
2
2
|
id: 'platform',
|
|
3
3
|
title: 'Platform',
|
|
4
|
-
description: 'Deployment platform configuration aligned with deployment-tools
|
|
4
|
+
description: 'Deployment platform configuration aligned with deployment-tools platforms.json / PlatformConfig.',
|
|
5
5
|
icon: 'controller',
|
|
6
6
|
status: ['beta'],
|
|
7
7
|
entitlementRequired: false,
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Matches the
|
|
3
|
-
* (`BucketDeployment` → `Source.jsonData('platform-config.json', { ...config, awsRoleToAssume: undefined })`).
|
|
2
|
+
* Matches the `platforms.json` / PlatformConfig shape used by `@ossy/deployment-tools`.
|
|
4
3
|
*
|
|
5
4
|
* @see ossy/packages/deployment-tools/src/config/platform-config.js (PlatformConfig typedef)
|
|
6
|
-
* @see ossy/packages/deployment-tools/src/infrastructure/container-deployment-target/container-deployment-target.js
|
|
7
5
|
*
|
|
8
|
-
* Note: `
|
|
9
|
-
* Optional `sesDomains` / `dnsRecords` are JSON text fields
|
|
6
|
+
* Note: `env` secret values live in `platforms.json` and Secrets Manager; do not store them here.
|
|
7
|
+
* Optional `domains` / `services` / `sesDomains` / `dnsRecords` / `githubDeployRepos` are JSON text fields.
|
|
10
8
|
*/
|
|
11
9
|
export default {
|
|
12
10
|
name: 'Platform config',
|
|
@@ -34,29 +32,29 @@ export default {
|
|
|
34
32
|
type: 'text',
|
|
35
33
|
},
|
|
36
34
|
{
|
|
37
|
-
name: '
|
|
38
|
-
label: '
|
|
39
|
-
type: '
|
|
35
|
+
name: 'domains',
|
|
36
|
+
label: 'domains (JSON array — Route53 → CloudFront + ACM)',
|
|
37
|
+
type: 'textarea',
|
|
40
38
|
},
|
|
41
39
|
{
|
|
42
|
-
name: '
|
|
43
|
-
label: '
|
|
44
|
-
type: '
|
|
40
|
+
name: 'services',
|
|
41
|
+
label: 'services (JSON array — HTTP → ECS/ALB; TCP ignored)',
|
|
42
|
+
type: 'textarea',
|
|
45
43
|
},
|
|
46
44
|
{
|
|
47
|
-
name: '
|
|
48
|
-
label: '
|
|
49
|
-
type: '
|
|
45
|
+
name: 'githubDeployRepos',
|
|
46
|
+
label: 'githubDeployRepos (JSON array of owner/repo for OIDC)',
|
|
47
|
+
type: 'textarea',
|
|
50
48
|
},
|
|
51
49
|
{
|
|
52
|
-
name: '
|
|
53
|
-
label: '
|
|
54
|
-
type: '
|
|
50
|
+
name: 'sesDomains',
|
|
51
|
+
label: 'sesDomains (JSON array of strings, optional)',
|
|
52
|
+
type: 'textarea',
|
|
55
53
|
},
|
|
56
54
|
{
|
|
57
|
-
name: '
|
|
58
|
-
label: '
|
|
59
|
-
type: '
|
|
55
|
+
name: 'dnsRecords',
|
|
56
|
+
label: 'dnsRecords (JSON object, optional)',
|
|
57
|
+
type: 'textarea',
|
|
60
58
|
},
|
|
61
59
|
{
|
|
62
60
|
name: 'ciDockerNetworkName',
|
|
@@ -64,14 +62,9 @@ export default {
|
|
|
64
62
|
type: 'text',
|
|
65
63
|
},
|
|
66
64
|
{
|
|
67
|
-
name: '
|
|
68
|
-
label: '
|
|
69
|
-
type: '
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'dnsRecords',
|
|
73
|
-
label: 'dnsRecords (JSON object, optional)',
|
|
74
|
-
type: 'textarea',
|
|
65
|
+
name: 'awsKeyPairName',
|
|
66
|
+
label: 'awsKeyPairName (unused after EC2 decommission)',
|
|
67
|
+
type: 'text',
|
|
75
68
|
},
|
|
76
69
|
{
|
|
77
70
|
name: 'Notes',
|
|
@@ -99,10 +99,19 @@ export class TaskService {
|
|
|
99
99
|
|
|
100
100
|
const audit = context.audit !== false && task.metadata.audit !== false
|
|
101
101
|
|
|
102
|
+
// GET/API handlers often omit integrations; default like dispatch/scheduler so
|
|
103
|
+
// tasks can always use integrations.get('email') (including local delivery).
|
|
104
|
+
const invokeContext = {
|
|
105
|
+
...context,
|
|
106
|
+
sdk: context.sdk ?? TaskService._sdk,
|
|
107
|
+
integrations: context.integrations ?? IntegrationService,
|
|
108
|
+
log: context.log ?? createLogger(id),
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
return TaskRunService.execute({
|
|
103
112
|
taskId: id,
|
|
104
113
|
handler: task.handler,
|
|
105
|
-
context,
|
|
114
|
+
context: invokeContext,
|
|
106
115
|
trigger: 'invoke',
|
|
107
116
|
triggeredBy: {
|
|
108
117
|
channel: context.req ? undefined : 'api',
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow-run context helpers — `$email`-style placeholders for declarative steps.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escape a value for use inside a double-quoted CSS attribute selector.
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function escapeCssAttrValue (value) {
|
|
11
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a whole-string `$token` placeholder from the run context.
|
|
16
|
+
* Non-strings and strings that do not start with `$` are returned as-is.
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown} value
|
|
19
|
+
* @param {{ fields?: Record<string, unknown>, [key: string]: unknown }} context
|
|
20
|
+
*/
|
|
21
|
+
export function resolveContextValue (value, context) {
|
|
22
|
+
if (typeof value !== 'string' || !value.startsWith('$')) return value
|
|
23
|
+
const key = value.slice(1)
|
|
24
|
+
return context[key] ?? context.fields?.[key]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Replace `$token` placeholders embedded in a string (e.g. selectors).
|
|
29
|
+
* Unknown tokens are left unchanged.
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} template
|
|
32
|
+
* @param {{ fields?: Record<string, unknown>, [key: string]: unknown }} context
|
|
33
|
+
* @param {{ escape?: (value: unknown) => string }} [options]
|
|
34
|
+
* @returns {unknown}
|
|
35
|
+
*/
|
|
36
|
+
export function interpolateContextString (template, context, options = {}) {
|
|
37
|
+
if (typeof template !== 'string' || !template.includes('$')) return template
|
|
38
|
+
const escape = typeof options.escape === 'function' ? options.escape : (value) => String(value)
|
|
39
|
+
return template.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, key) => {
|
|
40
|
+
const value = context[key] ?? context.fields?.[key]
|
|
41
|
+
if (value == null) return match
|
|
42
|
+
return escape(value)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
escapeCssAttrValue,
|
|
4
|
+
interpolateContextString,
|
|
5
|
+
resolveContextValue,
|
|
6
|
+
} from './flow-context.js'
|
|
7
|
+
|
|
8
|
+
describe('flow-context', () => {
|
|
9
|
+
const context = {
|
|
10
|
+
email: 'new@example.com',
|
|
11
|
+
fields: { firstName: 'Ada', email: 'field@example.com' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
it('resolves whole-string $placeholders from context then fields', () => {
|
|
15
|
+
expect(resolveContextValue('$email', context)).toBe('new@example.com')
|
|
16
|
+
expect(resolveContextValue('$firstName', context)).toBe('Ada')
|
|
17
|
+
expect(resolveContextValue('literal', context)).toBe('literal')
|
|
18
|
+
expect(resolveContextValue('$missing', context)).toBeUndefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('interpolates embedded $placeholders in selectors', () => {
|
|
22
|
+
expect(
|
|
23
|
+
interpolateContextString('[data-user-email="$email"]', context),
|
|
24
|
+
).toBe('[data-user-email="new@example.com"]')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('escapes quotes and backslashes when an escape fn is provided', () => {
|
|
28
|
+
const tricky = {
|
|
29
|
+
email: 'a"b\\c@example.com',
|
|
30
|
+
fields: {},
|
|
31
|
+
}
|
|
32
|
+
expect(
|
|
33
|
+
interpolateContextString('[data-user-email="$email"]', tricky, {
|
|
34
|
+
escape: escapeCssAttrValue,
|
|
35
|
+
}),
|
|
36
|
+
).toBe('[data-user-email="a\\"b\\\\c@example.com"]')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('leaves unknown embedded tokens unchanged', () => {
|
|
40
|
+
expect(
|
|
41
|
+
interpolateContextString('[data-x="$missing"]', context),
|
|
42
|
+
).toBe('[data-x="$missing"]')
|
|
43
|
+
})
|
|
44
|
+
})
|
package/src/test/flow-runner.js
CHANGED
|
@@ -14,6 +14,17 @@ import { faker } from '@faker-js/faker'
|
|
|
14
14
|
import { Router } from '@ossy/router'
|
|
15
15
|
import { Schema } from '@ossy/schema'
|
|
16
16
|
import { test, expect } from '@playwright/test'
|
|
17
|
+
import {
|
|
18
|
+
escapeCssAttrValue,
|
|
19
|
+
interpolateContextString,
|
|
20
|
+
resolveContextValue,
|
|
21
|
+
} from './flow-context.js'
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
escapeCssAttrValue,
|
|
25
|
+
interpolateContextString,
|
|
26
|
+
resolveContextValue,
|
|
27
|
+
} from './flow-context.js'
|
|
17
28
|
|
|
18
29
|
const FLOW_PATTERN = /\.flow\.(mjs|cjs|js)$/
|
|
19
30
|
|
|
@@ -110,13 +121,6 @@ function resolveActionService (action, fallback) {
|
|
|
110
121
|
return fallback
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
/** Resolve `$token`-style placeholders from the run context. */
|
|
114
|
-
function resolveContextValue (value, context) {
|
|
115
|
-
if (typeof value !== 'string' || !value.startsWith('$')) return value
|
|
116
|
-
const key = value.slice(1)
|
|
117
|
-
return context[key] ?? context.fields?.[key]
|
|
118
|
-
}
|
|
119
|
-
|
|
120
124
|
function resolveContextObject (obj, context) {
|
|
121
125
|
if (!obj || typeof obj !== 'object') return obj
|
|
122
126
|
const out = {}
|
|
@@ -312,7 +316,10 @@ export async function runFlow (flow, options = {}) {
|
|
|
312
316
|
await expectFn(page.locator(actionSelector(actionId, service)).first()).toBeVisible({ timeout })
|
|
313
317
|
}
|
|
314
318
|
if (selector != null) {
|
|
315
|
-
|
|
319
|
+
const resolvedSelector = interpolateContextString(selector, context, {
|
|
320
|
+
escape: escapeCssAttrValue,
|
|
321
|
+
})
|
|
322
|
+
await expectFn(page.locator(resolvedSelector).first()).toBeVisible({ timeout })
|
|
316
323
|
}
|
|
317
324
|
if (text != null) {
|
|
318
325
|
const pattern = text instanceof RegExp ? text : new RegExp(text, 'i')
|
package/src/test/test.util.js
CHANGED
|
@@ -173,6 +173,30 @@ export class TestUtil {
|
|
|
173
173
|
return ev.payload.token
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
static async getLatestEmailChangeJwtForSubject(subjectId) {
|
|
177
|
+
const ev = await EventStore.Collection.findOne(
|
|
178
|
+
{
|
|
179
|
+
type: TOKEN_SCHEMA,
|
|
180
|
+
event: 'Created',
|
|
181
|
+
'payload.type': 'EmailChange',
|
|
182
|
+
'payload.subject': subjectId,
|
|
183
|
+
},
|
|
184
|
+
{ sort: { created: -1 } },
|
|
185
|
+
)
|
|
186
|
+
if (!ev?.payload?.token) {
|
|
187
|
+
return Promise.reject(new Error('No EmailChange token found for subject'))
|
|
188
|
+
}
|
|
189
|
+
return ev.payload.token
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static countEmailChangeTokenEvents() {
|
|
193
|
+
return EventStore.Collection.countDocuments({
|
|
194
|
+
type: TOKEN_SCHEMA,
|
|
195
|
+
event: 'Created',
|
|
196
|
+
'payload.type': 'EmailChange',
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
176
200
|
static GetVerificationToken() {
|
|
177
201
|
const email = `${casual.email}`
|
|
178
202
|
|
|
@@ -192,7 +216,7 @@ export class TestUtil {
|
|
|
192
216
|
|
|
193
217
|
await TestUtil.AssertActionResponse({
|
|
194
218
|
actionId: '@ossy/authentication/actions/sign-up',
|
|
195
|
-
|
|
219
|
+
payload: { email, firstName: 'Test', lastName: 'User' },
|
|
196
220
|
expectedResponseStatus: 200,
|
|
197
221
|
expectedResponseBody: { ok: true },
|
|
198
222
|
})
|