@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.
Files changed (66) hide show
  1. package/README.md +21 -17
  2. package/package.json +20 -11
  3. package/src/Definition.js +2 -1
  4. package/src/PlatformShell.jsx +10 -10
  5. package/src/actions/action.service.js +85 -7
  6. package/src/audit/action-invocation-service.js +143 -0
  7. package/src/audit/action-invocation.aggregate.js +57 -0
  8. package/src/audit/audit-helpers.js +61 -0
  9. package/src/audit/detect-channel.js +19 -0
  10. package/src/audit/list-task-runs.action.js +5 -0
  11. package/src/audit/list-task-runs.task.js +17 -0
  12. package/src/audit/task-run-list.aggregate.js +76 -0
  13. package/src/audit/task-run-service.js +208 -0
  14. package/src/audit/task-run.aggregate.js +58 -0
  15. package/src/auth/action-scopes.js +3 -0
  16. package/src/capability-schemas/action-capability.schema.js +1 -0
  17. package/src/capability-schemas/action-meta.schema.js +1 -0
  18. package/src/capability-schemas/component-capability.schema.js +1 -0
  19. package/src/capability-schemas/page-capability.schema.js +1 -0
  20. package/src/capability-schemas/page-meta.schema.js +1 -0
  21. package/src/capability-schemas/task-capability.schema.js +1 -0
  22. package/src/capability-schemas/task-meta.schema.js +1 -0
  23. package/src/capability-schemas/task-output.schema.js +1 -0
  24. package/src/capability-schemas/task-trigger-authoring.schema.js +1 -0
  25. package/src/capability-schemas/task-trigger.schema.js +1 -0
  26. package/src/directory.schema.js +7 -0
  27. package/src/entitlements/action-entitlement.js +69 -0
  28. package/src/file.schema.js +11 -0
  29. package/src/index.js +12 -3
  30. package/src/mcp/create-ossy-mcp-server.js +97 -0
  31. package/src/mcp/json-schema-to-zod.js +64 -0
  32. package/src/mcp/mount-ossy-mcp.js +72 -0
  33. package/src/mcp/mount-platform-mcp.js +101 -0
  34. package/src/mcp/upload-file-tool.js +62 -0
  35. package/src/metering/metering-service.js +91 -0
  36. package/src/{platform-config.resource.js → platform-config.schema.js} +1 -1
  37. package/src/proxy-internal.js +13 -16
  38. package/src/push/mount-push-sse.js +91 -0
  39. package/src/request-diagnostics.js +144 -0
  40. package/src/request-diagnostics.spec.js +40 -0
  41. package/src/resources/index.js +3 -2
  42. package/src/resources/schema.registry.js +26 -0
  43. package/src/resources/schema.service.js +54 -0
  44. package/src/resources/schema.validation.js +90 -0
  45. package/src/runtime.js +20 -6
  46. package/src/server.js +281 -62
  47. package/src/storage/filesystem-storage.client.js +109 -0
  48. package/src/storage/local-storage.client.js +2 -65
  49. package/src/storage/resource-read-url.js +36 -0
  50. package/src/storage/resource-read-url.spec.js +22 -0
  51. package/src/storage/s3-storage.client.js +102 -0
  52. package/src/storage/s3.client.js +27 -23
  53. package/src/storage/storage-keys.js +37 -0
  54. package/src/storage/storage-keys.spec.js +16 -0
  55. package/src/storage/storage.client.js +52 -8
  56. package/src/storage/storage.integration.js +40 -0
  57. package/src/tasks/change-stream.js +78 -11
  58. package/src/tasks/task-service.js +211 -34
  59. package/src/tasks/task-service.spec.js +187 -0
  60. package/src/test/e2e.util.js +18 -39
  61. package/src/test/flow-runner.js +476 -0
  62. package/src/test/test.util.js +30 -29
  63. package/src/user-app-settings.js +44 -0
  64. package/src/users.middleware.js +61 -17
  65. package/src/resources/resource-template.registry.js +0 -29
  66. package/src/resources/resource-template.validation.js +0 -232
@@ -0,0 +1,26 @@
1
+ import { createLogger } from '@ossy/observability'
2
+
3
+ const log = createLogger('platform')
4
+
5
+ /** @type {object[]} */
6
+ const systemSchemas = []
7
+
8
+ /**
9
+ * Register a single schema POJO (as inlined in `build/manifest.json`).
10
+ *
11
+ * @param {object} schema
12
+ */
13
+ export function registerSchema (schema) {
14
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return
15
+ if (typeof schema.id !== 'string' || schema.id.trim() === '') return
16
+ if (systemSchemas.find(s => s.id === schema.id)) return
17
+ systemSchemas.push(schema)
18
+ log.info(`[SchemaRegistry] Registered system schema: ${schema.id}`)
19
+ }
20
+
21
+ /**
22
+ * @returns {object[]}
23
+ */
24
+ export function getSystemSchemas () {
25
+ return systemSchemas
26
+ }
@@ -0,0 +1,54 @@
1
+ import { Schema } from '@ossy/schema'
2
+
3
+ /** @type {Schema | null} */
4
+ let platformSchema = null
5
+
6
+ /**
7
+ * Platform-wide schema engine from build manifest (no workspace overlays).
8
+ *
9
+ * @param {{ schemas?: object[], actions?: { id: string }[], tasks?: { id: string }[] }} manifest
10
+ */
11
+ export function initPlatformSchema (manifest) {
12
+ platformSchema = Schema.of(manifest, {
13
+ actionIds: (manifest.actions ?? []).map((a) => a.id),
14
+ taskIds: (manifest.tasks ?? []).map((t) => t.id),
15
+ strictRefs: false,
16
+ })
17
+ return platformSchema
18
+ }
19
+
20
+ /**
21
+ * @returns {Schema}
22
+ */
23
+ export function getPlatformSchema () {
24
+ if (!platformSchema) {
25
+ throw new Error('[@ossy/platform] Platform Schema not initialized — call initPlatformSchema after loadManifest')
26
+ }
27
+ return platformSchema
28
+ }
29
+
30
+ /**
31
+ * Build-time or per-request engine with optional strict ref checks.
32
+ *
33
+ * @param {{ schemas?: object[], actions?: { id: string }[], tasks?: { id: string }[] }} manifest
34
+ * @param {{ strictRefs?: boolean, actionIds?: string[], taskIds?: string[] }} [options]
35
+ */
36
+ export function createSchemaEngine (manifest, options = {}) {
37
+ return Schema.of(manifest, {
38
+ actionIds: options.actionIds ?? (manifest.actions ?? []).map((a) => a.id),
39
+ taskIds: options.taskIds ?? (manifest.tasks ?? []).map((t) => t.id),
40
+ strictRefs: options.strictRefs ?? false,
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Platform schema engine with optional workspace schema overlays.
46
+ *
47
+ * @param {object[]} [workspaceSchemas]
48
+ */
49
+ export function schemaForWorkspace (workspaceSchemas = []) {
50
+ const platform = getPlatformSchema()
51
+ const extra = (workspaceSchemas ?? []).filter((s) => s?.id && !platform.has(s.id))
52
+ if (!extra.length) return platform
53
+ return platform.withWorkspaceSchemas(extra)
54
+ }
@@ -0,0 +1,90 @@
1
+ import { Schema } from '@ossy/schema'
2
+
3
+ /** Field input types accepted in schema definitions (UI + API contract). */
4
+ export const ALLOWED_FIELD_TYPES = new Set([
5
+ 'text',
6
+ 'email',
7
+ 'textarea',
8
+ 'richtext',
9
+ 'number',
10
+ 'select',
11
+ 'multiselect',
12
+ 'file',
13
+ 'image',
14
+ 'boolean',
15
+ 'date',
16
+ 'timestamp',
17
+ 'date-range',
18
+ 'date-ranges',
19
+ 'id',
20
+ 'path',
21
+ ])
22
+
23
+ /**
24
+ * Normalize template field `type` to the canonical editor type.
25
+ * `image` is an alias for `file` with default `accept: 'image/*'` when accept is omitted.
26
+ *
27
+ * @param {string | undefined} type
28
+ * @returns {{ type: string, defaultAccept?: string }}
29
+ */
30
+ export function normalizeFieldType (type) {
31
+ const raw = typeof type === 'string' ? type.trim() : ''
32
+ if (raw === 'image') return { type: 'file', defaultAccept: 'image/*' }
33
+ return { type: raw }
34
+ }
35
+
36
+ /**
37
+ * @param {{ type?: string, accept?: string, min?: number, max?: number } & Record<string, unknown>} field
38
+ */
39
+ export function resolveFieldDef (field) {
40
+ const { type: canonical, defaultAccept } = normalizeFieldType(field?.type)
41
+ return {
42
+ ...field,
43
+ type: canonical,
44
+ accept: field?.accept ?? defaultAccept,
45
+ max: field?.max ?? 1,
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Validates a batch import of workspace schemas.
51
+ *
52
+ * @param {unknown} templates
53
+ * @param {Set<string>} reservedIds - System schema ids that must not be redefined
54
+ * @returns {{ ok: true } | { ok: false, code: string, message: string }}
55
+ */
56
+ export function validateSchemasForImport (templates, reservedIds) {
57
+ if (!Array.isArray(templates)) {
58
+ return { ok: false, code: 'INVALID_PAYLOAD', message: 'Body must be a JSON array of schemas' }
59
+ }
60
+
61
+ const engine = Schema.of({ schemas: templates })
62
+ const seenIds = new Set()
63
+
64
+ for (const template of templates) {
65
+ if (reservedIds?.has(template?.id)) {
66
+ return {
67
+ ok: false,
68
+ code: 'RESERVED_SCHEMA_ID',
69
+ message: `Template id "${template.id}" is reserved by a system template`,
70
+ }
71
+ }
72
+
73
+ const result = engine.validate(template)
74
+ if (!result.ok) {
75
+ const first = result.errors[0]
76
+ return { ok: false, code: first.code, message: first.message }
77
+ }
78
+
79
+ if (seenIds.has(template.id)) {
80
+ return {
81
+ ok: false,
82
+ code: 'DUPLICATE_SCHEMA_ID',
83
+ message: `Duplicate schema id "${template.id}" in import payload`,
84
+ }
85
+ }
86
+ seenIds.add(template.id)
87
+ }
88
+
89
+ return { ok: true }
90
+ }
package/src/runtime.js CHANGED
@@ -4,7 +4,7 @@ import express from 'express'
4
4
  import cookieParser from 'cookie-parser'
5
5
  import morgan from 'morgan'
6
6
  import { Router as OssyRouter } from '@ossy/router'
7
- import { loadManifest, resolveEntryUrl } from './server.js'
7
+ import { loadManifest, resolveEntryUrl, loadLayoutsById, resolvePageLayoutRender } from './server.js'
8
8
  import { resolveRequestLocale } from './locale.js'
9
9
  import { buildManifestSummary } from '@ossy/app/manifest/build-manifest-summary'
10
10
  import { ProxyInternal } from './proxy-internal.js'
@@ -31,7 +31,7 @@ function cloneSerializable (value) {
31
31
  */
32
32
  const siteContextCache = new Map()
33
33
 
34
- function buildSiteContext (manifest, buildDir) {
34
+ function buildSiteContext (manifest, buildDir, layoutsById) {
35
35
  const config = manifest.config
36
36
  const supportedLanguages = Array.isArray(config.supportedLanguages) ? config.supportedLanguages : []
37
37
 
@@ -50,14 +50,15 @@ function buildSiteContext (manifest, buildDir) {
50
50
  return promise
51
51
  }
52
52
 
53
- return { pageRouter, apiRouter, manifest, manifestSummary: buildManifestSummary(manifest), loadEntry, buildDir }
53
+ return { pageRouter, apiRouter, manifest, manifestSummary: buildManifestSummary(manifest), loadEntry, buildDir, layoutsById }
54
54
  }
55
55
 
56
56
  async function getSiteContext (domain) {
57
57
  if (siteContextCache.has(domain)) return siteContextCache.get(domain)
58
58
  const buildDir = await loadSite(domain)
59
59
  const manifest = loadManifest(buildDir)
60
- const context = buildSiteContext(manifest, buildDir)
60
+ const layoutsById = await loadLayoutsById(manifest.layouts, buildDir, log)
61
+ const context = buildSiteContext(manifest, buildDir, layoutsById)
61
62
  siteContextCache.set(domain, context)
62
63
  return context
63
64
  }
@@ -129,7 +130,7 @@ export async function startRuntime ({ port } = {}) {
129
130
  return
130
131
  }
131
132
 
132
- const { pageRouter, apiRouter, manifest, manifestSummary, loadEntry, buildDir } = context
133
+ const { pageRouter, apiRouter, manifest, manifestSummary, loadEntry, buildDir, layoutsById } = context
133
134
 
134
135
  try {
135
136
  const apiRoute = apiRouter.getPageByUrl(requestUrl)
@@ -169,13 +170,21 @@ export async function startRuntime ({ port } = {}) {
169
170
 
170
171
  const config = manifest.config
171
172
  const { language, messages, fallbackMessages } = resolveRequestLocale(pageRouter, requestUrl, buildDir, config)
173
+ const {
174
+ Layout,
175
+ layoutEntry,
176
+ layoutSlots,
177
+ } = resolvePageLayoutRender(layoutsById, pageEntry)
172
178
  const props = {
173
179
  ...config,
174
180
  ...(req.userAppSettings || {}),
175
181
  theme: req.userAppSettings?.theme || cloneSerializable(config?.theme),
176
182
  themes: cloneSerializable(config?.themes),
177
- resourceTemplates: cloneSerializable(manifest.resourceTemplates),
183
+ schemas: cloneSerializable(manifest.schemas),
178
184
  manifestSummary: cloneSerializable(manifestSummary),
185
+ actionRoutes: cloneSerializable(manifest.actionRoutes || {}),
186
+ taskCatalog: cloneSerializable(manifest.taskCatalog || []),
187
+ taskGraphEdges: cloneSerializable(manifest.taskGraphEdges || []),
179
188
  url: requestUrl,
180
189
  isAuthenticated: !!req.isAuthenticated,
181
190
  language,
@@ -187,6 +196,11 @@ export async function startRuntime ({ port } = {}) {
187
196
  ...(page.package ? { package: page.package } : {}),
188
197
  })),
189
198
  pageId: pageRoute.id,
199
+ componentEntries: manifest.components,
200
+ resolveComponentEntry: (entry) => resolveEntryUrl(entry, buildDir),
201
+ layoutSlots,
202
+ Layout,
203
+ layoutEntry,
190
204
  }
191
205
  const html = await mod.render(props)
192
206
  res.status(200).type('html').send(html)