@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
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": "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
- "@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": "^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": "2b8745d57fee8b6c08787e2755b4df489291a5cd"
76
+ "gitHead": "4a70ec216448680d2fddc57b2a8a0077489720ae"
68
77
  }
package/src/Definition.js CHANGED
@@ -3,5 +3,6 @@ export const Definition = {
3
3
  title: 'Platform',
4
4
  description: 'Deployment platform configuration aligned with deployment-tools S3 platform-config.json.',
5
5
  icon: 'controller',
6
- statuses: ['beta'],
6
+ status: ['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 view="app:system-messages" />
29
+ <Slot view="app:header" />
30
+ <Slot view="app:sidebar" />
31
+ <Slot view="app:toolbar" />
32
32
  {children}
33
- <Slot name="shell:notifications" />
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
- * 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,15 +51,91 @@ 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
- * @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
- 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
+ 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,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
+ }