@ossy/platform 1.39.2 → 1.39.3

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 (65) hide show
  1. package/README.md +21 -17
  2. package/package.json +20 -11
  3. package/src/Definition.js +1 -0
  4. package/src/PlatformShell.jsx +10 -10
  5. package/src/actions/action.service.js +79 -6
  6. package/src/audit/action-invocation-service.js +144 -0
  7. package/src/audit/action-invocation.aggregate.js +59 -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 +78 -0
  13. package/src/audit/task-run-service.js +193 -0
  14. package/src/audit/task-run.aggregate.js +60 -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 +9 -0
  27. package/src/entitlements/action-entitlement.js +69 -0
  28. package/src/file.schema.js +13 -0
  29. package/src/index.js +13 -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 +92 -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 +43 -0
  39. package/src/resources/index.js +3 -2
  40. package/src/resources/schema.registry.js +26 -0
  41. package/src/resources/schema.service.js +54 -0
  42. package/src/resources/schema.validation.js +90 -0
  43. package/src/runtime.js +4 -1
  44. package/src/schema-ids.js +9 -0
  45. package/src/server.js +121 -20
  46. package/src/storage/filesystem-storage.client.js +109 -0
  47. package/src/storage/local-storage.client.js +2 -65
  48. package/src/storage/resource-read-url.js +36 -0
  49. package/src/storage/resource-read-url.spec.js +22 -0
  50. package/src/storage/s3-storage.client.js +102 -0
  51. package/src/storage/s3.client.js +27 -23
  52. package/src/storage/storage-keys.js +37 -0
  53. package/src/storage/storage-keys.spec.js +16 -0
  54. package/src/storage/storage.client.js +52 -8
  55. package/src/storage/storage.integration.js +40 -0
  56. package/src/tasks/change-stream.js +9 -2
  57. package/src/tasks/task-service.js +211 -34
  58. package/src/tasks/task-service.spec.js +187 -0
  59. package/src/test/e2e.util.js +18 -39
  60. package/src/test/flow-runner.js +476 -0
  61. package/src/test/test.util.js +30 -29
  62. package/src/user-app-settings.js +44 -0
  63. package/src/users.middleware.js +14 -2
  64. package/src/resources/resource-template.registry.js +0 -29
  65. package/src/resources/resource-template.validation.js +0 -232
package/README.md CHANGED
@@ -7,14 +7,18 @@ Express-based application server runtime for the Ossy platform. It reads the bui
7
7
  At startup `@ossy/platform`:
8
8
 
9
9
  1. Loads `build/manifest.json` produced by `@ossy/app build`.
10
- 2. Registers and runs all **startup hooks** (`*.startup.js`) in order.
11
- 3. Connects all **integrations** (`*.integration.js`) by calling `connect({ env })`.
12
- 4. Registers all **tasks** (`*.task.js`) with `TaskService` and starts the cron scheduler.
13
- 5. Registers all **resource templates** (`*.resource.js`) with `registerResourceTemplate`.
14
- 6. Rebuilds all **aggregates** (`*.aggregate.js`) from the event store.
15
- 7. Registers all **actions** (`*.action.js`) with `ActionService`.
16
- 8. Starts an Express server that routes requests to pages (`*.page.jsx`) and API handlers (`*.api.js`).
17
- 9. Auto-mounts every action at `POST /actions` (`{ action, payload }`).
10
+ 2. Registers the manifest's toggleable packages with `@ossy/workspaces/entitlements` (`setEnableablePackages`) so workspace service toggles and action entitlement checks use scoped npm names (`@ossy/booking`, not slug-only keys).
11
+ 3. Registers and runs all **startup hooks** (`*.startup.js`) in order.
12
+ 4. Connects all **integrations** (`*.integration.js`) by calling `connect({ env })`.
13
+ 5. Registers all **tasks** (`*.task.js`) with `TaskService` and starts the cron scheduler.
14
+ 6. Registers all **schemas** (`*.schema.js`) with `registerSchema`.
15
+ 7. Rebuilds all **aggregates** (`*.aggregate.js`) from the event store.
16
+ 8. Registers all **actions** (`*.action.js`) with `ActionService`.
17
+ 9. Mounts **MCP** at `POST /mcp` and serves `GET /capabilities.json`.
18
+ 10. Starts an Express server that routes requests to pages (`*.page.jsx`) and API handlers (`*.api.js`).
19
+ 11. Auto-mounts every action at `POST /actions` (`{ action, payload }`).
20
+
21
+ Page SSR fills the `app:content` slot (see `PlatformShell` and `resolve-app-slots` in `@ossy/app`). App chrome uses namespaced keys such as `app:header` mapped from `export const slots` in `*.layout.jsx`.
18
22
 
19
23
  ## Quick start
20
24
 
@@ -69,9 +73,9 @@ import {
69
73
  StorageClient,
70
74
  S3Client,
71
75
  LocalStorageClient,
72
- getSystemResourceTemplates,
73
- normalizeAndValidateDocumentContent,
74
- validateResourceTemplatesForImport,
76
+ getSystemSchemas,
77
+ schemaForWorkspace,
78
+ validateSchemasForImport,
75
79
  } from '@ossy/platform'
76
80
  ```
77
81
 
@@ -95,14 +99,14 @@ const action = ActionService.get('orders/create') // { id, access, run } | null
95
99
  const all = ActionService.all()
96
100
  ```
97
101
 
98
- ### `getSystemResourceTemplates`
102
+ ### `getSystemSchemas`
99
103
 
100
- Returns all resource templates registered from `*.resource.js` files.
104
+ Returns all system schemas registered from `*.schema.js` files.
101
105
 
102
106
  ```js
103
- import { getSystemResourceTemplates } from '@ossy/platform'
107
+ import { getSystemSchemas } from '@ossy/platform'
104
108
 
105
- const templates = getSystemResourceTemplates()
109
+ const schemas = getSystemSchemas()
106
110
  // [{ id: '@ossy/tool/doc', name: 'Tool Doc', fields: [...] }, ...]
107
111
  ```
108
112
 
@@ -121,7 +125,7 @@ The platform is built around file conventions called **primitives**. Each primit
121
125
  | Integration | `*.integration.js` | Third-party client connected at startup |
122
126
  | Email | `*.email.jsx` | Transactional React email template |
123
127
  | Component | `*.component.jsx` | Injectable UI fragment |
124
- | Resource | `*.resource.js` | Custom document-type schema |
128
+ | Resource | `*.schema.js` | Custom document-type schema |
125
129
  | Aggregate | `*.aggregate.js` | Event-sourced domain object |
126
130
  | Startup | `*.startup.js` | One-time boot hook |
127
131
 
@@ -130,7 +134,7 @@ The platform is built around file conventions called **primitives**. Each primit
130
134
  ```
131
135
  Incoming request
132
136
 
133
- ├─ POST /actions ──► ActionService.invoke() ──► TaskService.invoke() ──► task.run({ payload, sdk, log, integrations, req })
137
+ ├─ POST /actions ──► ActionService.invoke(actionId) ──► TaskService.invoke(taskIdFromActionId) ──► task.run(...)
134
138
 
135
139
  ├─ Match API route ──► api.handle(req, res)
136
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/platform",
3
- "version": "1.39.2",
3
+ "version": "1.39.3",
4
4
  "description": "Ossy application server runtime",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,8 +24,11 @@
24
24
  "./test": "./src/test/index.js",
25
25
  "./test/jest.setup.js": "./src/test/jest.setup.js",
26
26
  "./test/e2e-runner.js": "./src/test/e2e-runner.js",
27
+ "./test/flow-runner.js": "./src/test/flow-runner.js",
27
28
  "./test/playwright.config.js": "./src/test/playwright.config.js",
28
- "./locale": "./src/locale.js"
29
+ "./locale": "./src/locale.js",
30
+ "./storage-keys": "./src/storage/storage-keys.js",
31
+ "./mcp": "./src/mcp/mount-platform-mcp.js"
29
32
  },
30
33
  "scripts": {
31
34
  "start": "PORT=3003 node -e \"import('./src/server.js').then(m => m.startServer())\"",
@@ -40,21 +43,27 @@
40
43
  "@aws-sdk/s3-request-presigner": "^3.1057.0",
41
44
  "@aws-sdk/util-create-request": "^3.972.26",
42
45
  "@aws-sdk/util-format-url": "^3.972.17",
43
- "@ossy/event-store": "^1.8.2",
44
- "@ossy/locale": "^1.40.2",
45
- "@ossy/observability": "^1.8.2",
46
- "@ossy/policies": "^1.13.2",
47
- "@ossy/sdk": "^1.40.2",
48
- "@ossy/tokens": "^1.13.2",
49
- "@ossy/users": "^1.13.2",
46
+ "@modelcontextprotocol/sdk": "^1.12.1",
47
+ "@ossy/app": "^1.40.3",
48
+ "@ossy/event-store": "^1.8.3",
49
+ "@ossy/locale": "^1.40.3",
50
+ "@ossy/observability": "^1.8.3",
51
+ "@ossy/policies": "^1.13.3",
52
+ "@ossy/schema": "^1.0.1",
53
+ "@ossy/sdk": "^1.40.3",
54
+ "@ossy/tokens": "^1.13.3",
55
+ "@ossy/users": "^1.13.3",
56
+ "@ossy/workspaces": "^1.17.3",
50
57
  "cookie-parser": "^1.4.7",
51
58
  "dotenv": ">=16.0.0 <18.0.0",
52
59
  "express": ">=5.0.0 <6.0.0",
53
60
  "jsonwebtoken": "^9.0.0",
54
61
  "mongodb": "^7.2.0",
55
- "morgan": ">=1.10.1 <2.0.0"
62
+ "morgan": ">=1.10.1 <2.0.0",
63
+ "zod": "^3.25.0"
56
64
  },
57
65
  "devDependencies": {
66
+ "@faker-js/faker": "^9.9.0",
58
67
  "@jest/globals": "^30.2.0",
59
68
  "@playwright/test": ">=1.40.0",
60
69
  "casual": "^1.6.2",
@@ -64,5 +73,5 @@
64
73
  "src",
65
74
  "Dockerfile"
66
75
  ],
67
- "gitHead": "2b8745d57fee8b6c08787e2755b4df489291a5cd"
76
+ "gitHead": "a0d89185a17f8de8ce328c3a648c108ff1d61d8f"
68
77
  }
package/src/Definition.js CHANGED
@@ -4,4 +4,5 @@ export const Definition = {
4
4
  description: 'Deployment platform configuration aligned with deployment-tools S3 platform-config.json.',
5
5
  icon: 'controller',
6
6
  statuses: ['beta'],
7
+ entitlementRequired: false,
7
8
  }
@@ -14,23 +14,23 @@ import { ComponentSlotsProvider, Slot } from '@ossy/design-system'
14
14
  * provider shallow-merges on top.
15
15
  *
16
16
  * Slot regions rendered here (namespaced keys from app `export const slots`):
17
- * shell:header — top bar across full width
18
- * shell:sidebar — left column, full height
19
- * shell:toolbar — below header, above content (contextual actions)
20
- * shell:notifications — floating / bottom corner (toasts, badges)
21
- * shell:system-messages — banner above content (maintenance, errors)
17
+ * app:header — top bar across full width
18
+ * app:sidebar — left column, full height
19
+ * app:toolbar — below header, above content (contextual actions)
20
+ * app:notifications — floating / bottom corner (toasts, badges)
21
+ * app:system-messages — banner above content (maintenance, errors)
22
22
  *
23
23
  * @param {{ slots?: Record<string, import('react').ComponentType | null>, children: import('react').ReactNode }} props
24
24
  */
25
25
  export function PlatformShell({ slots = {}, children }) {
26
26
  return (
27
27
  <ComponentSlotsProvider slots={slots}>
28
- <Slot name="shell:system-messages" />
29
- <Slot name="shell:header" />
30
- <Slot name="shell:sidebar" />
31
- <Slot name="shell:toolbar" />
28
+ <Slot name="app:system-messages" />
29
+ <Slot name="app:header" />
30
+ <Slot name="app:sidebar" />
31
+ <Slot name="app:toolbar" />
32
32
  {children}
33
- <Slot name="shell:notifications" />
33
+ <Slot name="app:notifications" />
34
34
  </ComponentSlotsProvider>
35
35
  )
36
36
  }
@@ -1,5 +1,8 @@
1
1
  import { createLogger } from '@ossy/observability'
2
+ import { taskIdFromActionId } from '@ossy/schema'
2
3
  import { TaskService } from '../tasks/task-service.js'
4
+ import { ActionInvocationService } from '../audit/action-invocation-service.js'
5
+ import { assertActionScoped } from '../auth/action-scopes.js'
3
6
 
4
7
  const log = createLogger('platform/actions')
5
8
 
@@ -9,8 +12,7 @@ const _actions = new Map()
9
12
  /**
10
13
  * Registry for action intent (`*.action.js` metadata only).
11
14
  *
12
- * Actions name what callers want: `{ id, access }`. Implementation lives in
13
- * a task with the same id — see `TaskService.invoke`.
15
+ * Implementation lives in `{feature}/tasks/{intent}` derived from the action id.
14
16
  */
15
17
  export const ActionService = {
16
18
  /**
@@ -19,7 +21,7 @@ export const ActionService = {
19
21
  * @param {{ metadata: { id: string, access?: string } }} mod
20
22
  */
21
23
  register (mod) {
22
- const { id, access = 'authenticated' } = mod?.metadata ?? {}
24
+ const { id, access = 'authenticated', audit = true } = mod?.metadata ?? {}
23
25
 
24
26
  if (typeof id !== 'string' || id.trim() === '') {
25
27
  throw new Error(`[ActionService] Action module must export a non-empty string "metadata.id" (got ${JSON.stringify(id)})`)
@@ -29,7 +31,7 @@ export const ActionService = {
29
31
  log.warn(`[ActionService] Action "${id}" already registered — overwriting`)
30
32
  }
31
33
 
32
- _actions.set(id, { id, access })
34
+ _actions.set(id, { id, access, audit })
33
35
  log.info(`[ActionService] Registered action "${id}" (access: ${access})`)
34
36
  },
35
37
 
@@ -49,7 +51,15 @@ export const ActionService = {
49
51
  },
50
52
 
51
53
  /**
52
- * Invoke an action by id — enforces registration, delegates execution to TaskService.
54
+ * @param {string} actionId
55
+ * @returns {boolean}
56
+ */
57
+ hasHandler (actionId) {
58
+ return TaskService.hasTaskForAction(actionId)
59
+ },
60
+
61
+ /**
62
+ * Invoke an action by id — records audit, runs the derived primary task, dispatches on_action follow-ups.
53
63
  *
54
64
  * @param {string} id
55
65
  * @param {{ payload?: unknown, sdk?: unknown, log?: unknown, integrations?: unknown, req?: unknown }} context
@@ -58,6 +68,69 @@ export const ActionService = {
58
68
  async invoke (id, context = {}) {
59
69
  const action = _actions.get(id)
60
70
  if (!action) throw new Error(`[ActionService] Action not found: "${id}"`)
61
- return TaskService.invoke(id, context)
71
+
72
+ const taskId = taskIdFromActionId(id)
73
+ if (!TaskService.has(taskId)) {
74
+ throw new Error(`[ActionService] No primary task for action "${id}" (expected "${taskId}")`)
75
+ }
76
+
77
+ if (context.req?.tokenScopes) {
78
+ assertActionScoped(id, context.req.tokenScopes)
79
+ }
80
+
81
+ const skipAudit = action.audit === false
82
+ const invocationId = skipAudit
83
+ ? null
84
+ : await ActionInvocationService.recordStart(id, context)
85
+
86
+ const invokeContext = {
87
+ ...context,
88
+ actionInvocationId: invocationId,
89
+ actionId: id,
90
+ audit: skipAudit ? false : undefined,
91
+ }
92
+
93
+ try {
94
+ const result = await TaskService.invoke(taskId, invokeContext)
95
+ const taskRunId = invokeContext.audit?.taskRunId ?? null
96
+
97
+ if (!skipAudit) {
98
+ await ActionInvocationService.recordSuccess(invocationId, context, {
99
+ taskRunId,
100
+ })
101
+
102
+ TaskService.dispatchOnAction({
103
+ actionId: id,
104
+ actionInvocationId: invocationId,
105
+ payload: context.payload,
106
+ result,
107
+ success: true,
108
+ req: context.req,
109
+ sdk: context.sdk,
110
+ })
111
+ }
112
+
113
+ return result
114
+ } catch (error) {
115
+ const taskRunId = invokeContext.audit?.taskRunId ?? null
116
+
117
+ if (!skipAudit) {
118
+ await ActionInvocationService.recordFailure(invocationId, context, error, {
119
+ taskRunId,
120
+ })
121
+
122
+ TaskService.dispatchOnAction({
123
+ actionId: id,
124
+ actionInvocationId: invocationId,
125
+ payload: context.payload,
126
+ success: false,
127
+ error,
128
+ req: context.req,
129
+ sdk: context.sdk,
130
+ })
131
+ }
132
+
133
+ throw error
134
+ }
62
135
  },
63
136
  }
@@ -0,0 +1,144 @@
1
+ import { nanoid } from 'nanoid'
2
+ import { Aggregate } from '@ossy/event-store'
3
+ import { createLogger } from '@ossy/observability'
4
+ import { taskIdFromActionId } from '@ossy/schema'
5
+ import { ActionInvocation } from './action-invocation.aggregate.js'
6
+ import { PlatformSchema } from '../schema-ids.js'
7
+ import { MeteringService } from '../metering/metering-service.js'
8
+ import { detectChannel } from './detect-channel.js'
9
+ import {
10
+ hashPayload,
11
+ moduleIdFromTaskId,
12
+ resolveActorId,
13
+ resolveWorkspaceId,
14
+ summarizeError,
15
+ } from './audit-helpers.js'
16
+
17
+ const log = createLogger('platform/action-invocation')
18
+
19
+ /**
20
+ * Audit trail for action invocations (Wave 3 — agent trust / billing envelope).
21
+ */
22
+ export const ActionInvocationService = {
23
+
24
+ /**
25
+ * @param {string} actionId
26
+ * @param {object} context
27
+ * @returns {Promise<string>} invocationId
28
+ */
29
+ async recordStart (actionId, context = {}) {
30
+ const invocationId = nanoid()
31
+ const workspaceId = resolveWorkspaceId(context)
32
+ const actorId = resolveActorId(context)
33
+ const channel = detectChannel(context.req)
34
+ const payloadHash = hashPayload(context.payload)
35
+
36
+ context.actionInvocationId = invocationId
37
+ context.audit = context.audit ?? {}
38
+ context.audit.invocationId = invocationId
39
+ context.audit.startedAt = Date.now()
40
+ context.audit.actionId = actionId
41
+
42
+ try {
43
+ await Aggregate.Of(ActionInvocation, {
44
+ resourceId: invocationId,
45
+ type: PlatformSchema.actionInvocation,
46
+ event: 'Invoked',
47
+ createdBy: actorId ?? undefined,
48
+ payload: {
49
+ actionId,
50
+ moduleId: moduleIdFromTaskId(actionId),
51
+ channel,
52
+ payloadHash,
53
+ actorId,
54
+ belongsTo: workspaceId,
55
+ location: '/@ossy/audit/actions/',
56
+ },
57
+ })
58
+ } catch (err) {
59
+ log.warn('[ActionInvocationService] Failed to record Invoked', { actionId, invocationId }, err)
60
+ }
61
+
62
+ return invocationId
63
+ },
64
+
65
+ /**
66
+ * @param {string} invocationId
67
+ * @param {object} context
68
+ * @param {{ taskRunId?: string | null }} meta
69
+ */
70
+ async recordSuccess (invocationId, context, meta = {}) {
71
+ const durationMs = Date.now() - (context.audit?.startedAt ?? Date.now())
72
+ const actorId = resolveActorId(context)
73
+
74
+ try {
75
+ await Aggregate.Of(ActionInvocation, invocationId)
76
+ .then(Aggregate.Add({
77
+ event: 'Completed',
78
+ createdBy: actorId ?? undefined,
79
+ payload: {
80
+ durationMs,
81
+ taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
82
+ },
83
+ }))
84
+ } catch (err) {
85
+ log.warn('[ActionInvocationService] Failed to record Completed', { invocationId }, err)
86
+ }
87
+
88
+ const actionId = context.audit?.actionId
89
+ const taskId = actionId ? taskIdFromActionId(actionId) : null
90
+ await MeteringService.record({
91
+ kind: 'action',
92
+ actionId: actionId ?? null,
93
+ taskId,
94
+ channel: detectChannel(context.req),
95
+ workspaceId: resolveWorkspaceId(context),
96
+ actorId,
97
+ durationMs,
98
+ success: true,
99
+ actionInvocationId: invocationId,
100
+ taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
101
+ })
102
+ },
103
+
104
+ /**
105
+ * @param {string} invocationId
106
+ * @param {object} context
107
+ * @param {Error} error
108
+ * @param {{ taskRunId?: string | null }} meta
109
+ */
110
+ async recordFailure (invocationId, context, error, meta = {}) {
111
+ const durationMs = Date.now() - (context.audit?.startedAt ?? Date.now())
112
+ const actorId = resolveActorId(context)
113
+
114
+ try {
115
+ await Aggregate.Of(ActionInvocation, invocationId)
116
+ .then(Aggregate.Add({
117
+ event: 'Failed',
118
+ createdBy: actorId ?? undefined,
119
+ payload: {
120
+ durationMs,
121
+ taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
122
+ error: summarizeError(error),
123
+ },
124
+ }))
125
+ } catch (err) {
126
+ log.warn('[ActionInvocationService] Failed to record Failed', { invocationId }, err)
127
+ }
128
+
129
+ const actionId = context.audit?.actionId
130
+ const taskId = actionId ? taskIdFromActionId(actionId) : null
131
+ await MeteringService.record({
132
+ kind: 'action',
133
+ actionId: actionId ?? null,
134
+ taskId,
135
+ channel: detectChannel(context.req),
136
+ workspaceId: resolveWorkspaceId(context),
137
+ actorId,
138
+ durationMs,
139
+ success: false,
140
+ actionInvocationId: invocationId,
141
+ taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
142
+ })
143
+ },
144
+ }
@@ -0,0 +1,59 @@
1
+ import { PlatformSchema } from '../schema-ids.js'
2
+
3
+ /**
4
+ * ADR 0008 entity — audit record for each action invocation (POST /actions, MCP, SDK).
5
+ */
6
+ export class ActionInvocation {
7
+
8
+ static kind = 'entity'
9
+ static SchemaId = PlatformSchema.actionInvocation
10
+ static AggregateType = 'ActionInvocation'
11
+
12
+ static View (events, state = {}) {
13
+ return events.reduce((invocation, event) => {
14
+ switch (event.event) {
15
+
16
+ case 'Invoked':
17
+ return {
18
+ ...invocation,
19
+ id: event.resourceId,
20
+ type: event.type,
21
+ actionId: event.payload.actionId,
22
+ moduleId: event.payload.moduleId,
23
+ channel: event.payload.channel,
24
+ payloadHash: event.payload.payloadHash,
25
+ actorId: event.payload.actorId ?? event.createdBy ?? null,
26
+ workspaceId: event.payload.belongsTo ?? null,
27
+ status: 'in progress',
28
+ startedAt: event.created,
29
+ created: event.created,
30
+ }
31
+
32
+ case 'Completed':
33
+ return {
34
+ ...invocation,
35
+ status: 'success',
36
+ completedAt: event.created,
37
+ durationMs: event.payload.durationMs,
38
+ taskRunId: event.payload.taskRunId ?? null,
39
+ }
40
+
41
+ case 'Failed':
42
+ return {
43
+ ...invocation,
44
+ status: 'failed',
45
+ completedAt: event.created,
46
+ durationMs: event.payload.durationMs,
47
+ taskRunId: event.payload.taskRunId ?? null,
48
+ error: event.payload.error ?? null,
49
+ }
50
+
51
+ default:
52
+ return invocation
53
+ }
54
+ }, state)
55
+ }
56
+ }
57
+
58
+ export { ActionInvocation as Aggregate }
59
+ export const id = 'platform/schema/action-invocation'
@@ -0,0 +1,61 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { featureFromTaskId } from '@ossy/schema'
3
+
4
+ /** @param {string} taskId e.g. `@ossy/booking/tasks/create` */
5
+ export function moduleIdFromTaskId (taskId) {
6
+ return featureFromTaskId(taskId)
7
+ }
8
+
9
+ /** @param {unknown} payload */
10
+ export function hashPayload (payload) {
11
+ try {
12
+ const json = JSON.stringify(payload ?? null)
13
+ return createHash('sha256').update(json).digest('hex')
14
+ } catch {
15
+ return createHash('sha256').update(String(payload)).digest('hex')
16
+ }
17
+ }
18
+
19
+ /** Keep event payloads small — full task results stay outside the audit stream. */
20
+ export function summarizeResult (result) {
21
+ if (result == null) return null
22
+ if (typeof result !== 'object') {
23
+ return { kind: typeof result, preview: String(result).slice(0, 200) }
24
+ }
25
+ if (Array.isArray(result)) {
26
+ return { kind: 'array', length: result.length }
27
+ }
28
+ const keys = Object.keys(result)
29
+ return { kind: 'object', keys: keys.slice(0, 20), keyCount: keys.length }
30
+ }
31
+
32
+ /** @param {Error | { message?: string, code?: string }} error */
33
+ export function summarizeError (error) {
34
+ if (!error) return { message: 'Unknown error' }
35
+ return {
36
+ message: String(error.message ?? error).slice(0, 500),
37
+ code: error.code ?? undefined,
38
+ }
39
+ }
40
+
41
+ /**
42
+ * @param {object} context
43
+ * @returns {string | null}
44
+ */
45
+ export function resolveWorkspaceId (context) {
46
+ return context.workspaceId
47
+ ?? context.payload?.workspaceId
48
+ ?? context.req?.workspaceId
49
+ ?? null
50
+ }
51
+
52
+ /**
53
+ * @param {object} context
54
+ * @returns {string | null}
55
+ */
56
+ export function resolveActorId (context) {
57
+ return context.userId
58
+ ?? context.payload?.userId
59
+ ?? context.req?.userId
60
+ ?? null
61
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve billing/audit channel from an Express request (or similar).
3
+ *
4
+ * @param {import('express').Request | undefined} req
5
+ * @returns {'web_ui' | 'sdk' | 'api' | 'mcp' | 'cli'}
6
+ */
7
+ export function detectChannel (req) {
8
+ if (!req) return 'api'
9
+
10
+ const url = req.originalUrl ?? req.url ?? ''
11
+ if (url.includes('/mcp')) return 'mcp'
12
+
13
+ const authHeader = req.get?.('Authorization') ?? req.headers?.authorization
14
+ if (authHeader && !req.signedCookies?.auth) return 'api'
15
+
16
+ if (req.signedCookies?.auth) return 'web_ui'
17
+
18
+ return 'api'
19
+ }
@@ -0,0 +1,5 @@
1
+ export const metadata = {
2
+ id: '@ossy/platform/actions/list-task-runs',
3
+ access: 'workspace',
4
+ audit: false,
5
+ }
@@ -0,0 +1,17 @@
1
+ import { getProjection } from '@ossy/event-store'
2
+ import { TaskRunListProjection } from '../audit/task-run-list.aggregate.js'
3
+
4
+ export const metadata = {
5
+ id: '@ossy/platform/tasks/list-task-runs',
6
+ audit: false,
7
+ }
8
+
9
+ export async function run ({ payload, req }) {
10
+ const workspaceId = payload?.workspaceId ?? req?.workspaceId
11
+ if (!workspaceId) {
12
+ throw Object.assign(new Error('workspaceId required'), { status: 400 })
13
+ }
14
+
15
+ const projection = await getProjection(TaskRunListProjection.ProjectionId, workspaceId)
16
+ return projection?.state ?? TaskRunListProjection.initialState(workspaceId)
17
+ }