@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/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
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
13
|
-
5. Registers all **
|
|
14
|
-
6.
|
|
15
|
-
7.
|
|
16
|
-
8.
|
|
17
|
-
9.
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
### `
|
|
102
|
+
### `getSystemSchemas`
|
|
99
103
|
|
|
100
|
-
Returns all
|
|
104
|
+
Returns all system schemas registered from `*.schema.js` files.
|
|
101
105
|
|
|
102
106
|
```js
|
|
103
|
-
import {
|
|
107
|
+
import { getSystemSchemas } from '@ossy/platform'
|
|
104
108
|
|
|
105
|
-
const
|
|
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 | `*.
|
|
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(
|
|
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": "
|
|
3
|
+
"version": "3.0.1",
|
|
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
|
-
"@
|
|
44
|
-
"@ossy/
|
|
45
|
-
"@ossy/
|
|
46
|
-
"@ossy/
|
|
47
|
-
"@ossy/
|
|
48
|
-
"@ossy/
|
|
49
|
-
"@ossy/
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
47
|
+
"@ossy/app": "^3.0.1",
|
|
48
|
+
"@ossy/event-store": "^3.0.1",
|
|
49
|
+
"@ossy/locale": "^3.0.1",
|
|
50
|
+
"@ossy/observability": "^3.0.1",
|
|
51
|
+
"@ossy/policies": "^3.0.1",
|
|
52
|
+
"@ossy/schema": "^3.0.1",
|
|
53
|
+
"@ossy/sdk": "^3.0.1",
|
|
54
|
+
"@ossy/tokens": "^3.0.1",
|
|
55
|
+
"@ossy/users": "^3.0.1",
|
|
56
|
+
"@ossy/workspaces": "^3.0.1",
|
|
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": "
|
|
76
|
+
"gitHead": "4a70ec216448680d2fddc57b2a8a0077489720ae"
|
|
68
77
|
}
|
package/src/Definition.js
CHANGED
package/src/PlatformShell.jsx
CHANGED
|
@@ -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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
29
|
-
<Slot
|
|
30
|
-
<Slot
|
|
31
|
-
<Slot
|
|
28
|
+
<Slot view="app:system-messages" />
|
|
29
|
+
<Slot view="app:header" />
|
|
30
|
+
<Slot view="app:sidebar" />
|
|
31
|
+
<Slot view="app:toolbar" />
|
|
32
32
|
{children}
|
|
33
|
-
<Slot
|
|
33
|
+
<Slot view="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
|
-
*
|
|
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,15 +51,91 @@ export const ActionService = {
|
|
|
49
51
|
},
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
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
|
-
* @param {{ payload?: unknown, sdk?: unknown, log?: unknown, integrations?: unknown, req?: unknown }} context
|
|
65
|
+
* @param {{ payload?: unknown, sdk?: unknown, log?: unknown, integrations?: unknown, req?: unknown, signal?: AbortSignal }} context
|
|
56
66
|
* @returns {Promise<unknown>}
|
|
57
67
|
*/
|
|
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
|
-
|
|
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
|
+
if (context.signal?.aborted) {
|
|
96
|
+
throw context.signal.reason instanceof Error
|
|
97
|
+
? context.signal.reason
|
|
98
|
+
: new Error(`[ActionService] Action aborted: "${id}"`)
|
|
99
|
+
}
|
|
100
|
+
const taskRunId = invokeContext.audit?.taskRunId ?? null
|
|
101
|
+
|
|
102
|
+
if (!skipAudit) {
|
|
103
|
+
await ActionInvocationService.recordSuccess(invocationId, context, {
|
|
104
|
+
taskRunId,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
TaskService.dispatchOnAction({
|
|
108
|
+
actionId: id,
|
|
109
|
+
actionInvocationId: invocationId,
|
|
110
|
+
payload: context.payload,
|
|
111
|
+
result,
|
|
112
|
+
success: true,
|
|
113
|
+
req: context.req,
|
|
114
|
+
sdk: context.sdk,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const taskRunId = invokeContext.audit?.taskRunId ?? null
|
|
121
|
+
|
|
122
|
+
if (!skipAudit) {
|
|
123
|
+
await ActionInvocationService.recordFailure(invocationId, context, error, {
|
|
124
|
+
taskRunId,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
TaskService.dispatchOnAction({
|
|
128
|
+
actionId: id,
|
|
129
|
+
actionInvocationId: invocationId,
|
|
130
|
+
payload: context.payload,
|
|
131
|
+
success: false,
|
|
132
|
+
error,
|
|
133
|
+
req: context.req,
|
|
134
|
+
sdk: context.sdk,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
62
140
|
},
|
|
63
141
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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 { MeteringService } from '../metering/metering-service.js'
|
|
7
|
+
import { detectChannel } from './detect-channel.js'
|
|
8
|
+
import {
|
|
9
|
+
hashPayload,
|
|
10
|
+
moduleIdFromTaskId,
|
|
11
|
+
resolveActorId,
|
|
12
|
+
resolveWorkspaceId,
|
|
13
|
+
summarizeError,
|
|
14
|
+
} from './audit-helpers.js'
|
|
15
|
+
|
|
16
|
+
const log = createLogger('platform/action-invocation')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Audit trail for action invocations (Wave 3 — agent trust / billing envelope).
|
|
20
|
+
*/
|
|
21
|
+
export const ActionInvocationService = {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} actionId
|
|
25
|
+
* @param {object} context
|
|
26
|
+
* @returns {Promise<string>} invocationId
|
|
27
|
+
*/
|
|
28
|
+
async recordStart (actionId, context = {}) {
|
|
29
|
+
const invocationId = nanoid()
|
|
30
|
+
const workspaceId = resolveWorkspaceId(context)
|
|
31
|
+
const actorId = resolveActorId(context)
|
|
32
|
+
const channel = detectChannel(context.req)
|
|
33
|
+
const payloadHash = hashPayload(context.payload)
|
|
34
|
+
|
|
35
|
+
context.actionInvocationId = invocationId
|
|
36
|
+
context.audit = context.audit ?? {}
|
|
37
|
+
context.audit.invocationId = invocationId
|
|
38
|
+
context.audit.startedAt = Date.now()
|
|
39
|
+
context.audit.actionId = actionId
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await Aggregate.Of(ActionInvocation, {
|
|
43
|
+
resourceId: invocationId,
|
|
44
|
+
type: '@ossy/platform/schema/action-invocation',
|
|
45
|
+
event: 'Invoked',
|
|
46
|
+
createdBy: actorId ?? undefined,
|
|
47
|
+
payload: {
|
|
48
|
+
actionId,
|
|
49
|
+
moduleId: moduleIdFromTaskId(actionId),
|
|
50
|
+
channel,
|
|
51
|
+
payloadHash,
|
|
52
|
+
actorId,
|
|
53
|
+
belongsTo: workspaceId,
|
|
54
|
+
location: '/@ossy/audit/actions/',
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.warn('[ActionInvocationService] Failed to record Invoked', { actionId, invocationId }, err)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return invocationId
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} invocationId
|
|
66
|
+
* @param {object} context
|
|
67
|
+
* @param {{ taskRunId?: string | null }} meta
|
|
68
|
+
*/
|
|
69
|
+
async recordSuccess (invocationId, context, meta = {}) {
|
|
70
|
+
const durationMs = Date.now() - (context.audit?.startedAt ?? Date.now())
|
|
71
|
+
const actorId = resolveActorId(context)
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await Aggregate.Of(ActionInvocation, invocationId)
|
|
75
|
+
.then(Aggregate.Add({
|
|
76
|
+
event: 'Completed',
|
|
77
|
+
createdBy: actorId ?? undefined,
|
|
78
|
+
payload: {
|
|
79
|
+
durationMs,
|
|
80
|
+
taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
|
|
81
|
+
},
|
|
82
|
+
}))
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log.warn('[ActionInvocationService] Failed to record Completed', { invocationId }, err)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const actionId = context.audit?.actionId
|
|
88
|
+
const taskId = actionId ? taskIdFromActionId(actionId) : null
|
|
89
|
+
await MeteringService.record({
|
|
90
|
+
kind: 'action',
|
|
91
|
+
actionId: actionId ?? null,
|
|
92
|
+
taskId,
|
|
93
|
+
channel: detectChannel(context.req),
|
|
94
|
+
workspaceId: resolveWorkspaceId(context),
|
|
95
|
+
actorId,
|
|
96
|
+
durationMs,
|
|
97
|
+
success: true,
|
|
98
|
+
actionInvocationId: invocationId,
|
|
99
|
+
taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} invocationId
|
|
105
|
+
* @param {object} context
|
|
106
|
+
* @param {Error} error
|
|
107
|
+
* @param {{ taskRunId?: string | null }} meta
|
|
108
|
+
*/
|
|
109
|
+
async recordFailure (invocationId, context, error, meta = {}) {
|
|
110
|
+
const durationMs = Date.now() - (context.audit?.startedAt ?? Date.now())
|
|
111
|
+
const actorId = resolveActorId(context)
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await Aggregate.Of(ActionInvocation, invocationId)
|
|
115
|
+
.then(Aggregate.Add({
|
|
116
|
+
event: 'Failed',
|
|
117
|
+
createdBy: actorId ?? undefined,
|
|
118
|
+
payload: {
|
|
119
|
+
durationMs,
|
|
120
|
+
taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
|
|
121
|
+
error: summarizeError(error),
|
|
122
|
+
},
|
|
123
|
+
}))
|
|
124
|
+
} catch (err) {
|
|
125
|
+
log.warn('[ActionInvocationService] Failed to record Failed', { invocationId }, err)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const actionId = context.audit?.actionId
|
|
129
|
+
const taskId = actionId ? taskIdFromActionId(actionId) : null
|
|
130
|
+
await MeteringService.record({
|
|
131
|
+
kind: 'action',
|
|
132
|
+
actionId: actionId ?? null,
|
|
133
|
+
taskId,
|
|
134
|
+
channel: detectChannel(context.req),
|
|
135
|
+
workspaceId: resolveWorkspaceId(context),
|
|
136
|
+
actorId,
|
|
137
|
+
durationMs,
|
|
138
|
+
success: false,
|
|
139
|
+
actionInvocationId: invocationId,
|
|
140
|
+
taskRunId: meta.taskRunId ?? context.audit?.taskRunId ?? null,
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR 0008 entity — audit record for each action invocation (POST /actions, MCP, SDK).
|
|
3
|
+
*/
|
|
4
|
+
export class ActionInvocation {
|
|
5
|
+
|
|
6
|
+
static kind = 'entity'
|
|
7
|
+
static SchemaId = '@ossy/platform/schema/action-invocation'
|
|
8
|
+
static AggregateType = 'ActionInvocation'
|
|
9
|
+
|
|
10
|
+
static View (events, state = {}) {
|
|
11
|
+
return events.reduce((invocation, event) => {
|
|
12
|
+
switch (event.event) {
|
|
13
|
+
|
|
14
|
+
case 'Invoked':
|
|
15
|
+
return {
|
|
16
|
+
...invocation,
|
|
17
|
+
id: event.resourceId,
|
|
18
|
+
type: event.type,
|
|
19
|
+
actionId: event.payload.actionId,
|
|
20
|
+
moduleId: event.payload.moduleId,
|
|
21
|
+
channel: event.payload.channel,
|
|
22
|
+
payloadHash: event.payload.payloadHash,
|
|
23
|
+
actorId: event.payload.actorId ?? event.createdBy ?? null,
|
|
24
|
+
workspaceId: event.payload.belongsTo ?? null,
|
|
25
|
+
status: 'in progress',
|
|
26
|
+
startedAt: event.created,
|
|
27
|
+
created: event.created,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case 'Completed':
|
|
31
|
+
return {
|
|
32
|
+
...invocation,
|
|
33
|
+
status: 'success',
|
|
34
|
+
completedAt: event.created,
|
|
35
|
+
durationMs: event.payload.durationMs,
|
|
36
|
+
taskRunId: event.payload.taskRunId ?? null,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case 'Failed':
|
|
40
|
+
return {
|
|
41
|
+
...invocation,
|
|
42
|
+
status: 'failed',
|
|
43
|
+
completedAt: event.created,
|
|
44
|
+
durationMs: event.payload.durationMs,
|
|
45
|
+
taskRunId: event.payload.taskRunId ?? null,
|
|
46
|
+
error: event.payload.error ?? null,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
default:
|
|
50
|
+
return invocation
|
|
51
|
+
}
|
|
52
|
+
}, state)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { ActionInvocation as Aggregate }
|
|
57
|
+
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,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
|
+
}
|