@ossy/platform 1.39.2 → 3.0.1
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 +21 -17
- package/package.json +20 -11
- package/src/Definition.js +2 -1
- package/src/PlatformShell.jsx +10 -10
- package/src/actions/action.service.js +85 -7
- package/src/audit/action-invocation-service.js +143 -0
- package/src/audit/action-invocation.aggregate.js +57 -0
- package/src/audit/audit-helpers.js +61 -0
- package/src/audit/detect-channel.js +19 -0
- package/src/audit/list-task-runs.action.js +5 -0
- package/src/audit/list-task-runs.task.js +17 -0
- package/src/audit/task-run-list.aggregate.js +76 -0
- package/src/audit/task-run-service.js +208 -0
- package/src/audit/task-run.aggregate.js +58 -0
- package/src/auth/action-scopes.js +3 -0
- package/src/capability-schemas/action-capability.schema.js +1 -0
- package/src/capability-schemas/action-meta.schema.js +1 -0
- package/src/capability-schemas/component-capability.schema.js +1 -0
- package/src/capability-schemas/page-capability.schema.js +1 -0
- package/src/capability-schemas/page-meta.schema.js +1 -0
- package/src/capability-schemas/task-capability.schema.js +1 -0
- package/src/capability-schemas/task-meta.schema.js +1 -0
- package/src/capability-schemas/task-output.schema.js +1 -0
- package/src/capability-schemas/task-trigger-authoring.schema.js +1 -0
- package/src/capability-schemas/task-trigger.schema.js +1 -0
- package/src/directory.schema.js +7 -0
- package/src/entitlements/action-entitlement.js +69 -0
- package/src/file.schema.js +11 -0
- package/src/index.js +12 -3
- package/src/mcp/create-ossy-mcp-server.js +97 -0
- package/src/mcp/json-schema-to-zod.js +64 -0
- package/src/mcp/mount-ossy-mcp.js +72 -0
- package/src/mcp/mount-platform-mcp.js +101 -0
- package/src/mcp/upload-file-tool.js +62 -0
- package/src/metering/metering-service.js +91 -0
- package/src/{platform-config.resource.js → platform-config.schema.js} +1 -1
- package/src/proxy-internal.js +13 -16
- package/src/push/mount-push-sse.js +91 -0
- package/src/request-diagnostics.js +144 -0
- package/src/request-diagnostics.spec.js +40 -0
- package/src/resources/index.js +3 -2
- package/src/resources/schema.registry.js +26 -0
- package/src/resources/schema.service.js +54 -0
- package/src/resources/schema.validation.js +90 -0
- package/src/runtime.js +20 -6
- package/src/server.js +281 -62
- package/src/storage/filesystem-storage.client.js +109 -0
- package/src/storage/local-storage.client.js +2 -65
- package/src/storage/resource-read-url.js +36 -0
- package/src/storage/resource-read-url.spec.js +22 -0
- package/src/storage/s3-storage.client.js +102 -0
- package/src/storage/s3.client.js +27 -23
- package/src/storage/storage-keys.js +37 -0
- package/src/storage/storage-keys.spec.js +16 -0
- package/src/storage/storage.client.js +52 -8
- package/src/storage/storage.integration.js +40 -0
- package/src/tasks/change-stream.js +78 -11
- package/src/tasks/task-service.js +211 -34
- package/src/tasks/task-service.spec.js +187 -0
- package/src/test/e2e.util.js +18 -39
- package/src/test/flow-runner.js +476 -0
- package/src/test/test.util.js +30 -29
- package/src/user-app-settings.js +44 -0
- package/src/users.middleware.js +61 -17
- package/src/resources/resource-template.registry.js +0 -29
- package/src/resources/resource-template.validation.js +0 -232
package/src/users.middleware.js
CHANGED
|
@@ -5,8 +5,20 @@ import { User } from '@ossy/users/server'
|
|
|
5
5
|
import { Token } from '@ossy/tokens/server'
|
|
6
6
|
import { ConfigService } from './config.service.js'
|
|
7
7
|
import { PoliciesQueries } from '@ossy/policies/server'
|
|
8
|
+
import {
|
|
9
|
+
OperationTimeoutError,
|
|
10
|
+
resolveTimeoutMs,
|
|
11
|
+
withTimeout,
|
|
12
|
+
} from './request-diagnostics.js'
|
|
8
13
|
|
|
9
14
|
const log = createLogger('users')
|
|
15
|
+
const AUTH_TIMEOUT_MS = resolveTimeoutMs('OSSY_AUTH_TIMEOUT_MS', 10_000)
|
|
16
|
+
|
|
17
|
+
function normalizeAuthToken (token) {
|
|
18
|
+
const trimmed = String(token ?? '').trim()
|
|
19
|
+
if (!trimmed) return ''
|
|
20
|
+
return trimmed.replace(/^Bearer\s+/i, '').trim()
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Express middleware for user authentication and context resolution.
|
|
@@ -34,8 +46,9 @@ export class UsersMiddleware {
|
|
|
34
46
|
* or to the anonymous principal. Authorization / role checks belong in a separate step.
|
|
35
47
|
*/
|
|
36
48
|
static AuthenticateUser (req, res, next) {
|
|
37
|
-
const authToken =
|
|
38
|
-
|| req.get('Authorization')
|
|
49
|
+
const authToken = normalizeAuthToken(
|
|
50
|
+
req.signedCookies.auth || req.get('Authorization'),
|
|
51
|
+
)
|
|
39
52
|
|
|
40
53
|
const asAnonymous = () => {
|
|
41
54
|
req.userId = ConfigService.AnonymousUserId
|
|
@@ -51,24 +64,55 @@ export class UsersMiddleware {
|
|
|
51
64
|
log.info('[UsersMiddleware] Authenticating')
|
|
52
65
|
log.debug('[UsersMiddleware] authToken', { authToken })
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
const authStarted = performance.now()
|
|
68
|
+
let authFinished = false
|
|
69
|
+
const finishAuth = (apply) => {
|
|
70
|
+
if (authFinished) return
|
|
71
|
+
authFinished = true
|
|
72
|
+
apply()
|
|
73
|
+
}
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
withTimeout(
|
|
76
|
+
TokenService.verify(authToken)
|
|
77
|
+
.then(UsersMiddleware.assertApiTokenActive)
|
|
78
|
+
.then(payload => {
|
|
79
|
+
req.authPayload = payload
|
|
80
|
+
req.tokenScopes = payload?.type === 'Api' ? (payload.scopes ?? ['*']) : null
|
|
81
|
+
return payload
|
|
82
|
+
})
|
|
83
|
+
.then(({ sub }) => Aggregate.Of(User, sub))
|
|
84
|
+
.then(Aggregate.View())
|
|
85
|
+
.then(user => {
|
|
86
|
+
const workspaces = user.workspaces ?? []
|
|
87
|
+
|
|
88
|
+
return PoliciesQueries.GetPoliciesForUser(user.id)
|
|
89
|
+
.then(policies => {
|
|
90
|
+
finishAuth(() => {
|
|
91
|
+
log.info('[UsersMiddleware] Resolved user', {
|
|
92
|
+
elapsedMs: Math.round(performance.now() - authStarted),
|
|
93
|
+
})
|
|
94
|
+
req.userId = user.id
|
|
95
|
+
req.user = { ...user, workspaces, policies }
|
|
96
|
+
next()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}),
|
|
100
|
+
AUTH_TIMEOUT_MS,
|
|
101
|
+
'UsersMiddleware.AuthenticateUser',
|
|
102
|
+
)
|
|
69
103
|
.catch(error => {
|
|
104
|
+
if (error instanceof OperationTimeoutError) {
|
|
105
|
+
log.warn('[UsersMiddleware] Auth timed out', {
|
|
106
|
+
timeoutMs: error.timeoutMs,
|
|
107
|
+
elapsedMs: Math.round(performance.now() - authStarted),
|
|
108
|
+
})
|
|
109
|
+
finishAuth(() => {
|
|
110
|
+
res.status(504).json({ error: 'Authentication timed out', code: 'AUTH_TIMEOUT' })
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
70
114
|
log.debug('[UsersMiddleware] Auth failed; anonymous', { error })
|
|
71
|
-
asAnonymous
|
|
115
|
+
finishAuth(asAnonymous)
|
|
72
116
|
})
|
|
73
117
|
}
|
|
74
118
|
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '@ossy/observability'
|
|
2
|
-
|
|
3
|
-
const log = createLogger('platform')
|
|
4
|
-
|
|
5
|
-
/** @type {object[]} */
|
|
6
|
-
const systemTemplates = []
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Register a single resource template POJO (as inlined in `build/manifest.json`).
|
|
10
|
-
* Silently skips duplicates (same `id`) and invalid entries.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} template
|
|
13
|
-
*/
|
|
14
|
-
export function registerResourceTemplate(template) {
|
|
15
|
-
if (!template || typeof template !== 'object' || Array.isArray(template)) return
|
|
16
|
-
if (typeof template.id !== 'string' || template.id.trim() === '') return
|
|
17
|
-
if (systemTemplates.find(t => t.id === template.id)) return
|
|
18
|
-
systemTemplates.push(template)
|
|
19
|
-
log.info(`[ResourceTemplateRegistry] Registered system template: ${template.id}`)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Returns all registered system resource templates.
|
|
24
|
-
*
|
|
25
|
-
* @returns {object[]}
|
|
26
|
-
*/
|
|
27
|
-
export function getSystemResourceTemplates() {
|
|
28
|
-
return systemTemplates
|
|
29
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/** Field input types accepted in resource template definitions (UI + API contract). */
|
|
2
|
-
export const ALLOWED_FIELD_TYPES = new Set([
|
|
3
|
-
'text',
|
|
4
|
-
'textarea',
|
|
5
|
-
'richtext',
|
|
6
|
-
'number',
|
|
7
|
-
'select',
|
|
8
|
-
'multiselect',
|
|
9
|
-
'image',
|
|
10
|
-
'boolean',
|
|
11
|
-
'date',
|
|
12
|
-
])
|
|
13
|
-
|
|
14
|
-
function isNonEmptyString(value) {
|
|
15
|
-
return typeof value === 'string' && value.trim().length > 0
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isEmptyValue(value) {
|
|
19
|
-
if (value === undefined || value === null) return true
|
|
20
|
-
if (typeof value === 'string' && value.trim() === '') return true
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {unknown} template
|
|
26
|
-
* @param {{ reservedIds: Set<string> }} ctx
|
|
27
|
-
* @returns {{ ok: true } | { ok: false, code: string, message: string }}
|
|
28
|
-
*/
|
|
29
|
-
function validateOneTemplateShape(template, ctx) {
|
|
30
|
-
if (template === null || typeof template !== 'object' || Array.isArray(template)) {
|
|
31
|
-
return { ok: false, code: 'INVALID_TEMPLATE', message: 'Each template must be a non-array object' }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!isNonEmptyString(template.id)) {
|
|
35
|
-
return { ok: false, code: 'INVALID_TEMPLATE', message: 'Each template must have a non-empty string id' }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!isNonEmptyString(template.name)) {
|
|
39
|
-
return { ok: false, code: 'INVALID_TEMPLATE', message: `Template "${template.id}" must have a non-empty string name` }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (ctx.reservedIds.has(template.id)) {
|
|
43
|
-
return {
|
|
44
|
-
ok: false,
|
|
45
|
-
code: 'RESERVED_TEMPLATE_ID',
|
|
46
|
-
message: `Template id "${template.id}" is reserved by a system template`,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!Array.isArray(template.fields)) {
|
|
51
|
-
return {
|
|
52
|
-
ok: false,
|
|
53
|
-
code: 'INVALID_TEMPLATE',
|
|
54
|
-
message: `Template "${template.id}" must have a fields array`,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const fieldNames = new Set()
|
|
59
|
-
for (let i = 0; i < template.fields.length; i++) {
|
|
60
|
-
const field = template.fields[i]
|
|
61
|
-
if (field === null || typeof field !== 'object' || Array.isArray(field)) {
|
|
62
|
-
return {
|
|
63
|
-
ok: false,
|
|
64
|
-
code: 'INVALID_FIELD',
|
|
65
|
-
message: `Template "${template.id}" fields[${i}] must be an object`,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (!isNonEmptyString(field.name)) {
|
|
69
|
-
return {
|
|
70
|
-
ok: false,
|
|
71
|
-
code: 'INVALID_FIELD',
|
|
72
|
-
message: `Template "${template.id}" fields[${i}] must have a non-empty string name`,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (fieldNames.has(field.name)) {
|
|
76
|
-
return {
|
|
77
|
-
ok: false,
|
|
78
|
-
code: 'DUPLICATE_FIELD_NAME',
|
|
79
|
-
message: `Template "${template.id}" has duplicate field name "${field.name}"`,
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
fieldNames.add(field.name)
|
|
83
|
-
|
|
84
|
-
if (typeof field.type !== 'string' || !isNonEmptyString(field.type)) {
|
|
85
|
-
return {
|
|
86
|
-
ok: false,
|
|
87
|
-
code: 'INVALID_FIELD_TYPE',
|
|
88
|
-
message: `Template "${template.id}" field "${field.name}" must have a non-empty string type`,
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const fieldType = field.type.trim()
|
|
93
|
-
if (!ALLOWED_FIELD_TYPES.has(fieldType)) {
|
|
94
|
-
return {
|
|
95
|
-
ok: false,
|
|
96
|
-
code: 'INVALID_FIELD_TYPE',
|
|
97
|
-
message: `Template "${template.id}" field "${field.name}" has unsupported type "${fieldType}"`,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (fieldType === 'select' || fieldType === 'multiselect') {
|
|
102
|
-
if (!Array.isArray(field.options) || field.options.length === 0) {
|
|
103
|
-
return {
|
|
104
|
-
ok: false,
|
|
105
|
-
code: 'INVALID_FIELD_OPTIONS',
|
|
106
|
-
message: `Template "${template.id}" field "${field.name}" of type ${fieldType} requires a non-empty options array`,
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const badOption = field.options.find(o => typeof o !== 'string' || o.trim() === '')
|
|
110
|
-
if (badOption !== undefined) {
|
|
111
|
-
return {
|
|
112
|
-
ok: false,
|
|
113
|
-
code: 'INVALID_FIELD_OPTIONS',
|
|
114
|
-
message: `Template "${template.id}" field "${field.name}" options must be non-empty strings`,
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return { ok: true }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Validates a batch import of workspace resource templates.
|
|
125
|
-
*
|
|
126
|
-
* @param {unknown} templates
|
|
127
|
-
* @param {Set<string>} reservedIds - System template ids that must not be redefined
|
|
128
|
-
* @returns {{ ok: true } | { ok: false, code: string, message: string }}
|
|
129
|
-
*/
|
|
130
|
-
export function validateResourceTemplatesForImport(templates, reservedIds) {
|
|
131
|
-
if (!Array.isArray(templates)) {
|
|
132
|
-
return { ok: false, code: 'INVALID_PAYLOAD', message: 'Body must be a JSON array of resource templates' }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const seenIds = new Set()
|
|
136
|
-
for (let i = 0; i < templates.length; i++) {
|
|
137
|
-
const template = templates[i]
|
|
138
|
-
const shape = validateOneTemplateShape(template, { reservedIds })
|
|
139
|
-
if (!shape.ok) return shape
|
|
140
|
-
|
|
141
|
-
if (seenIds.has(template.id)) {
|
|
142
|
-
return {
|
|
143
|
-
ok: false,
|
|
144
|
-
code: 'DUPLICATE_TEMPLATE_ID',
|
|
145
|
-
message: `Duplicate template id "${template.id}" in import payload`,
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
seenIds.add(template.id)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return { ok: true }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Strips content to template field names and validates required fields and basic value shapes.
|
|
156
|
-
*
|
|
157
|
-
* @param {unknown} rawContent
|
|
158
|
-
* @param {{ fields?: { name: string, type?: string, required?: boolean, options?: string[] }[] }} template
|
|
159
|
-
* @returns {{ ok: true, content: Record<string, unknown> } | { ok: false, code: string, message: string }}
|
|
160
|
-
*/
|
|
161
|
-
export function normalizeAndValidateDocumentContent(rawContent, template) {
|
|
162
|
-
const fields = template?.fields || []
|
|
163
|
-
|
|
164
|
-
let base
|
|
165
|
-
if (rawContent === undefined || rawContent === null) {
|
|
166
|
-
base = {}
|
|
167
|
-
} else if (typeof rawContent !== 'object' || Array.isArray(rawContent)) {
|
|
168
|
-
return { ok: false, code: 'INVALID_CONTENT', message: 'Content must be a plain object' }
|
|
169
|
-
} else {
|
|
170
|
-
base = rawContent
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const allowedNames = new Set(fields.map(f => f.name))
|
|
174
|
-
const content = {}
|
|
175
|
-
for (const name of allowedNames) {
|
|
176
|
-
if (Object.prototype.hasOwnProperty.call(base, name)) {
|
|
177
|
-
content[name] = base[name]
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
for (const field of fields) {
|
|
182
|
-
const v = content[field.name]
|
|
183
|
-
const t = typeof field.type === 'string' ? field.type.trim() : field.type
|
|
184
|
-
|
|
185
|
-
if (field.required && isEmptyValue(v)) {
|
|
186
|
-
return {
|
|
187
|
-
ok: false,
|
|
188
|
-
code: 'REQUIRED_FIELD_MISSING',
|
|
189
|
-
message: `Field "${field.name}" is required`,
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (v === undefined) continue
|
|
194
|
-
|
|
195
|
-
if (t === 'number' && v !== null && (typeof v !== 'number' || Number.isNaN(v))) {
|
|
196
|
-
return {
|
|
197
|
-
ok: false,
|
|
198
|
-
code: 'INVALID_FIELD_VALUE',
|
|
199
|
-
message: `Field "${field.name}" must be a number`,
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (t === 'select' && v !== null && v !== undefined && !field.options?.includes(v)) {
|
|
204
|
-
return {
|
|
205
|
-
ok: false,
|
|
206
|
-
code: 'INVALID_FIELD_VALUE',
|
|
207
|
-
message: `Field "${field.name}" must be one of the defined options`,
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (t === 'multiselect' && v !== null && v !== undefined) {
|
|
212
|
-
if (!Array.isArray(v)) {
|
|
213
|
-
return {
|
|
214
|
-
ok: false,
|
|
215
|
-
code: 'INVALID_FIELD_VALUE',
|
|
216
|
-
message: `Field "${field.name}" must be an array of option values`,
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const opts = field.options || []
|
|
220
|
-
const invalid = v.some(item => typeof item !== 'string' || !opts.includes(item))
|
|
221
|
-
if (invalid) {
|
|
222
|
-
return {
|
|
223
|
-
ok: false,
|
|
224
|
-
code: 'INVALID_FIELD_VALUE',
|
|
225
|
-
message: `Field "${field.name}" must only contain values from options`,
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return { ok: true, content }
|
|
232
|
-
}
|