@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
@@ -1,11 +1,21 @@
1
1
  import { matchesGlob } from './glob.js'
2
2
  import { matchesCron } from './cron.js'
3
3
  import { IntegrationService } from '../integration.service.js'
4
- import { createLogger, metrics } from '@ossy/observability'
4
+ import { createLogger } from '@ossy/observability'
5
+ import { isPrimaryTaskForAction, taskIdFromActionId } from '@ossy/schema'
6
+ import { TaskRunService } from '../audit/task-run-service.js'
5
7
 
6
8
  const SCHEDULER_INTERVAL_MS = 60_000
7
9
  const _serviceLog = createLogger('TaskService')
8
10
 
11
+ function buildTaskContext (baseContext, extras = {}) {
12
+ return {
13
+ ...baseContext,
14
+ ...extras,
15
+ payload: baseContext.payload ?? extras.payload,
16
+ }
17
+ }
18
+
9
19
  export class TaskService {
10
20
 
11
21
  static _stopped = false
@@ -80,13 +90,44 @@ export class TaskService {
80
90
  * Synchronously invoke a task by id (HTTP / sdk.invoke path).
81
91
  *
82
92
  * @param {string} id
83
- * @param {{ payload?: unknown, sdk?: unknown, log?: unknown, integrations?: unknown, req?: unknown }} context
93
+ * @param {{ payload?: unknown, sdk?: unknown, log?: unknown, integrations?: unknown, req?: unknown, actionInvocationId?: string }} context
84
94
  * @returns {Promise<unknown>}
85
95
  */
86
96
  static async invoke(id, context = {}) {
87
97
  const task = TaskService._tasksById.get(id)
88
98
  if (!task) throw new Error(`[TaskService] Task not found: "${id}"`)
89
- return task.handler(context)
99
+
100
+ const audit = context.audit !== false && task.metadata.audit !== false
101
+
102
+ return TaskRunService.execute({
103
+ taskId: id,
104
+ handler: task.handler,
105
+ context,
106
+ trigger: 'invoke',
107
+ triggeredBy: {
108
+ channel: context.req ? undefined : 'api',
109
+ actionInvocationId: context.actionInvocationId ?? null,
110
+ },
111
+ executionEnv: 'ossy_server',
112
+ audit,
113
+ })
114
+ }
115
+
116
+ /** @param {string} id */
117
+ static has (id) {
118
+ return TaskService._tasksById.has(id)
119
+ }
120
+
121
+ /**
122
+ * @param {string} actionId
123
+ * @returns {boolean}
124
+ */
125
+ static hasTaskForAction (actionId) {
126
+ try {
127
+ return TaskService.has(taskIdFromActionId(actionId))
128
+ } catch {
129
+ return false
130
+ }
90
131
  }
91
132
 
92
133
  /**
@@ -101,23 +142,107 @@ export class TaskService {
101
142
 
102
143
  for (const { metadata, handler } of TaskService._tasks) {
103
144
  const triggers = metadata.triggers ?? []
104
- const matched = triggers.some(trigger => TaskService._matchesTrigger(trigger, event))
145
+ const matched = triggers.some(trigger => TaskService._matchesEventTrigger(trigger, event))
105
146
 
106
147
  if (!matched) continue
107
148
 
108
149
  _serviceLog.info(`Dispatching task "${metadata.id}"`)
109
150
 
151
+ const taskContext = buildTaskContext(
152
+ { sdk: effectiveSdk, integrations: IntegrationService, log: createLogger(metadata.id) },
153
+ {
154
+ event,
155
+ payload: { workspaceId: event.payload?.belongsTo },
156
+ },
157
+ )
158
+
110
159
  const _taskId = metadata.id
111
- const _start = Date.now()
112
- Promise.resolve()
113
- .then(() => handler({ event, sdk: effectiveSdk, integrations: IntegrationService, log: createLogger(metadata.id) }))
114
- .catch(error =>
115
- _serviceLog.error(`Task "${metadata.id}" failed`, undefined, error),
116
- )
117
- .finally(() => {
118
- try { metrics.timing('task.duration', Date.now() - _start, { task: _taskId }) } catch {}
119
- try { metrics.increment('task.run', { task: _taskId }) } catch {}
120
- })
160
+ TaskRunService.executeAsync({
161
+ taskId: _taskId,
162
+ handler,
163
+ context: taskContext,
164
+ trigger: 'on_event',
165
+ triggeredBy: {
166
+ eventId: event.id,
167
+ resourceId: event.resourceId,
168
+ type: event.type,
169
+ event: event.event,
170
+ },
171
+ executionEnv: 'ossy_server',
172
+ audit: metadata.audit !== false,
173
+ })
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Dispatches follow-up tasks registered with `on_action` triggers after an action completes.
179
+ *
180
+ * @param {{
181
+ * actionId: string,
182
+ * actionInvocationId?: string,
183
+ * payload?: unknown,
184
+ * result?: unknown,
185
+ * success: boolean,
186
+ * error?: Error,
187
+ * req?: import('express').Request,
188
+ * sdk?: unknown,
189
+ * }} actionContext
190
+ */
191
+ static dispatchOnAction (actionContext) {
192
+ const {
193
+ actionId,
194
+ actionInvocationId,
195
+ payload,
196
+ result,
197
+ success,
198
+ error,
199
+ req,
200
+ sdk,
201
+ } = actionContext
202
+
203
+ for (const { metadata, handler } of TaskService._tasks) {
204
+ if (isPrimaryTaskForAction(actionId, metadata.id)) continue
205
+
206
+ const triggers = metadata.triggers ?? []
207
+ const matched = triggers.some(trigger => TaskService._matchesActionTrigger(trigger, actionId))
208
+
209
+ if (!matched) continue
210
+
211
+ _serviceLog.info(`Dispatching on_action task "${metadata.id}" for action "${actionId}"`)
212
+
213
+ const taskContext = buildTaskContext(
214
+ {
215
+ sdk: sdk ?? TaskService._sdk,
216
+ integrations: IntegrationService,
217
+ log: createLogger(metadata.id),
218
+ req,
219
+ actionInvocationId,
220
+ payload,
221
+ },
222
+ {
223
+ action: {
224
+ id: actionId,
225
+ success,
226
+ result,
227
+ error: error ? { message: error.message } : undefined,
228
+ },
229
+ },
230
+ )
231
+
232
+ const _taskId = metadata.id
233
+ TaskRunService.executeAsync({
234
+ taskId: _taskId,
235
+ handler,
236
+ context: taskContext,
237
+ trigger: 'on_action',
238
+ triggeredBy: {
239
+ actionId,
240
+ actionInvocationId: actionInvocationId ?? null,
241
+ success,
242
+ },
243
+ executionEnv: 'ossy_server',
244
+ audit: metadata.audit !== false,
245
+ })
121
246
  }
122
247
  }
123
248
 
@@ -143,17 +268,23 @@ export class TaskService {
143
268
 
144
269
  _serviceLog.info(`Schedule fired for task "${metadata.id}"`)
145
270
 
271
+ const taskContext = {
272
+ event: { type: 'scheduled', taskId: metadata.id },
273
+ sdk: TaskService._sdk,
274
+ integrations: IntegrationService,
275
+ log: createLogger(metadata.id),
276
+ }
277
+
146
278
  const _taskId = metadata.id
147
- const _start = Date.now()
148
- Promise.resolve()
149
- .then(() => handler({ event: { type: 'scheduled', taskId: metadata.id }, sdk: TaskService._sdk, integrations: IntegrationService, log: createLogger(metadata.id) }))
150
- .catch(error =>
151
- _serviceLog.error(`Scheduled task "${metadata.id}" failed`, undefined, error),
152
- )
153
- .finally(() => {
154
- try { metrics.timing('task.duration', Date.now() - _start, { task: _taskId }) } catch {}
155
- try { metrics.increment('task.run', { task: _taskId }) } catch {}
156
- })
279
+ TaskRunService.executeAsync({
280
+ taskId: _taskId,
281
+ handler,
282
+ context: taskContext,
283
+ trigger: 'on_schedule',
284
+ triggeredBy: { schedule: metadata.schedule },
285
+ executionEnv: 'ossy_server',
286
+ audit: metadata.audit !== false,
287
+ })
157
288
  }
158
289
  }, SCHEDULER_INTERVAL_MS)
159
290
  }
@@ -170,19 +301,65 @@ export class TaskService {
170
301
  }
171
302
 
172
303
  /**
173
- * Returns true when a trigger descriptor matches the given eventstore document.
304
+ * Returns true when an `on_action` trigger matches the given action id.
174
305
  *
175
- * Matching rules:
176
- * trigger.aggregateType — exact match against event.aggregateType
177
- * trigger.event — exact match against event.type
178
- * trigger.resource.type — glob match (supports *) against event.payload.type
179
- * trigger.location.startsWith — prefix match against event.payload.location
306
+ * @param {object} trigger
307
+ * @param {string} actionId
180
308
  */
181
- static _matchesTrigger(trigger, event) {
182
- if (trigger.aggregateType && trigger.aggregateType !== event.aggregateType) return false
183
- if (trigger.event && trigger.event !== event.type) return false
184
- if (trigger.resource?.type && !matchesGlob(trigger.resource.type, event.payload?.type)) return false
309
+ static _matchesActionTrigger (trigger, actionId) {
310
+ const kind = trigger.trigger ?? trigger.on
311
+ if (kind !== 'on_action') return false
312
+
313
+ const pattern = trigger.action
314
+ if (!pattern) return false
315
+
316
+ return matchesGlob(pattern, actionId)
317
+ }
318
+
319
+ /**
320
+ * Returns true when an event trigger descriptor matches the given eventstore document.
321
+ *
322
+ * ADR 0008: `trigger.type` = schemaId, `trigger.event` = lifecycle/custom name.
323
+ * Skips `on_action` triggers (handled by {@link TaskService.dispatchOnAction}).
324
+ */
325
+ static _matchesEventTrigger (trigger, event) {
326
+ const kind = trigger.trigger ?? trigger.on
327
+ if (kind === 'on_action') return false
328
+
329
+ if (trigger.excludeTypes?.includes(event.type)) return false
330
+
331
+ const hasPositiveFilter = Boolean(
332
+ trigger.type ||
333
+ trigger.event ||
334
+ trigger.match ||
335
+ trigger.resource ||
336
+ trigger.location,
337
+ )
338
+ if (!hasPositiveFilter && trigger.excludeTypes?.length) return true
339
+
340
+ if (trigger.type && trigger.type !== event.type) return false
341
+ if (trigger.event && trigger.event !== event.event) return false
342
+
343
+ if (trigger.match) {
344
+ for (const [path, pattern] of Object.entries(trigger.match)) {
345
+ const fullPath = path.startsWith('payload.') ? path : `payload.${path}`
346
+ const parts = fullPath.split('.')
347
+ let cur = event
348
+ for (const part of parts) {
349
+ if (cur == null || typeof cur !== 'object') { cur = undefined; break }
350
+ cur = cur[part]
351
+ }
352
+ if (!matchesGlob(pattern, cur)) return false
353
+ }
354
+ }
355
+
356
+ if (trigger.resource?.type) {
357
+ const mime = event.payload?.content?.ContentType ?? event.type
358
+ if (!matchesGlob(trigger.resource.type, mime)) return false
359
+ }
360
+
185
361
  if (trigger.location?.startsWith && !event.payload?.location?.startsWith?.(trigger.location.startsWith)) return false
362
+
186
363
  return true
187
364
  }
188
365
 
@@ -0,0 +1,187 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+ import { TaskService } from './task-service.js'
3
+ import { TaskRun } from '../audit/task-run.aggregate.js'
4
+ import { ActionInvocation } from '../audit/action-invocation.aggregate.js'
5
+ import { TaskRunListProjection } from '../audit/task-run-list.aggregate.js'
6
+ import { detectChannel } from '../audit/detect-channel.js'
7
+ import { hashPayload, moduleIdFromTaskId } from '../audit/audit-helpers.js'
8
+ import { isPrimaryTaskForAction, taskIdFromActionId } from '@ossy/schema'
9
+
10
+ describe('TaskService triggers', () => {
11
+ it('matches on_action triggers with glob patterns', () => {
12
+ expect(TaskService._matchesActionTrigger(
13
+ { trigger: 'on_action', action: '@ossy/booking/actions/*' },
14
+ '@ossy/booking/actions/create',
15
+ )).toBe(true)
16
+
17
+ expect(TaskService._matchesActionTrigger(
18
+ { trigger: 'on_action', action: '@ossy/booking/actions/*' },
19
+ '@ossy/users/actions/create',
20
+ )).toBe(false)
21
+ })
22
+
23
+ it('ignores on_action triggers in event dispatch', () => {
24
+ const event = {
25
+ type: '@ossy/booking/schema/booking',
26
+ event: 'Created',
27
+ resourceId: 'b1',
28
+ payload: { belongsTo: 'ws1' },
29
+ }
30
+
31
+ expect(TaskService._matchesEventTrigger(
32
+ { trigger: 'on_action', action: '@ossy/booking/actions/*' },
33
+ event,
34
+ )).toBe(false)
35
+
36
+ expect(TaskService._matchesEventTrigger(
37
+ { type: '@ossy/booking/schema/booking', event: 'Created' },
38
+ event,
39
+ )).toBe(true)
40
+ })
41
+
42
+ it('supports excludeTypes to skip platform audit envelopes', () => {
43
+ const taskRunEvent = {
44
+ type: '@ossy/platform/schema/task-run',
45
+ event: 'Started',
46
+ resourceId: 'run1',
47
+ payload: { belongsTo: 'ws1' },
48
+ }
49
+
50
+ expect(TaskService._matchesEventTrigger(
51
+ { excludeTypes: ['@ossy/platform/schema/task-run'] },
52
+ taskRunEvent,
53
+ )).toBe(false)
54
+
55
+ expect(TaskService._matchesEventTrigger(
56
+ { excludeTypes: ['@ossy/platform/schema/task-run'] },
57
+ {
58
+ type: '@ossy/booking/schema/booking',
59
+ event: 'Created',
60
+ resourceId: 'b1',
61
+ payload: { belongsTo: 'ws1' },
62
+ },
63
+ )).toBe(true)
64
+ })
65
+
66
+ it('skips primary derived task in on_action dispatch', () => {
67
+ expect(isPrimaryTaskForAction('@ossy/booking/actions/create', '@ossy/booking/tasks/create')).toBe(true)
68
+ expect(isPrimaryTaskForAction('@ossy/booking/actions/create', '@ossy/booking/tasks/send-reminder')).toBe(false)
69
+ })
70
+ })
71
+
72
+ describe('capability id derivation', () => {
73
+ it('derives task id from action id', () => {
74
+ expect(taskIdFromActionId('@ossy/booking/actions/create')).toBe('@ossy/booking/tasks/create')
75
+ })
76
+ })
77
+
78
+ describe('TaskRun entity fold', () => {
79
+ it('derives status lifecycle from events', () => {
80
+ const started = {
81
+ resourceId: 'run1',
82
+ type: '@ossy/platform/schema/task-run',
83
+ event: 'Started',
84
+ created: 1000,
85
+ payload: {
86
+ taskId: '@ossy/booking/tasks/create',
87
+ moduleId: 'booking',
88
+ trigger: 'invoke',
89
+ belongsTo: 'ws1',
90
+ executionEnv: 'ossy_server',
91
+ },
92
+ }
93
+
94
+ let state = TaskRun.View([started])
95
+ expect(state.status).toBe('in progress')
96
+ expect(state.taskId).toBe('@ossy/booking/tasks/create')
97
+
98
+ state = TaskRun.View([
99
+ started,
100
+ {
101
+ event: 'Completed',
102
+ created: 2500,
103
+ payload: { durationMs: 1500, resultSummary: { kind: 'object', keyCount: 1 } },
104
+ },
105
+ ])
106
+ expect(state.status).toBe('success')
107
+ expect(state.durationMs).toBe(1500)
108
+ })
109
+ })
110
+
111
+ describe('TaskRunList projection', () => {
112
+ it('lists runs per workspace and updates status', () => {
113
+ const started = {
114
+ resourceId: 'run1',
115
+ type: '@ossy/platform/schema/task-run',
116
+ event: 'Started',
117
+ created: 1000,
118
+ payload: {
119
+ taskId: '@ossy/media-tasks/tasks/resize',
120
+ moduleId: 'media-tasks',
121
+ trigger: 'on_event',
122
+ belongsTo: 'ws1',
123
+ },
124
+ }
125
+
126
+ let state = TaskRunListProjection.initialState('ws1')
127
+ state = TaskRunListProjection.Apply(started, state)
128
+ expect(state.runs).toHaveLength(1)
129
+ expect(state.runs[0].status).toBe('in progress')
130
+
131
+ state = TaskRunListProjection.Apply({
132
+ resourceId: 'run1',
133
+ event: 'Completed',
134
+ created: 2000,
135
+ payload: { durationMs: 1000, resultSummary: null },
136
+ }, state)
137
+
138
+ expect(state.runs[0].status).toBe('success')
139
+ expect(state.runs[0].durationMs).toBe(1000)
140
+ })
141
+ })
142
+
143
+ describe('ActionInvocation entity fold', () => {
144
+ it('records invoke lifecycle', () => {
145
+ const invoked = {
146
+ resourceId: 'inv1',
147
+ type: '@ossy/platform/schema/action-invocation',
148
+ event: 'Invoked',
149
+ created: 500,
150
+ payload: {
151
+ actionId: '@ossy/booking/actions/create',
152
+ moduleId: 'booking',
153
+ channel: 'mcp',
154
+ payloadHash: 'abc',
155
+ belongsTo: 'ws1',
156
+ },
157
+ }
158
+
159
+ let state = ActionInvocation.View([invoked])
160
+ expect(state.status).toBe('in progress')
161
+ expect(state.channel).toBe('mcp')
162
+
163
+ state = ActionInvocation.View([
164
+ invoked,
165
+ { event: 'Completed', created: 700, payload: { durationMs: 200, taskRunId: 'run1' } },
166
+ ])
167
+ expect(state.status).toBe('success')
168
+ expect(state.taskRunId).toBe('run1')
169
+ })
170
+ })
171
+
172
+ describe('audit helpers', () => {
173
+ it('detects channel from request shape', () => {
174
+ expect(detectChannel({ originalUrl: '/mcp', signedCookies: {} })).toBe('mcp')
175
+ expect(detectChannel({ signedCookies: { auth: 'token' } })).toBe('web_ui')
176
+ expect(detectChannel({ get: () => 'Bearer x' })).toBe('api')
177
+ })
178
+
179
+ it('derives module id from task id', () => {
180
+ expect(moduleIdFromTaskId('@ossy/booking/tasks/create')).toBe('booking')
181
+ })
182
+
183
+ it('hashes payloads deterministically', () => {
184
+ expect(hashPayload({ a: 1 })).toBe(hashPayload({ a: 1 }))
185
+ expect(hashPayload({ a: 1 })).not.toBe(hashPayload({ a: 2 }))
186
+ })
187
+ })
@@ -2,25 +2,22 @@ import { MongoClient } from 'mongodb'
2
2
 
3
3
  const DB_URL = process.env.DB_URL ?? 'mongodb://localhost:27017/'
4
4
  const DB_NAME = process.env.DB_NAME ?? 'ossy-local'
5
+ const USER_SCHEMA = '@ossy/users/schema/user'
6
+ const TOKEN_SCHEMA = '@ossy/tokens/schema/token'
7
+ const WORKSPACE_SCHEMA = '@ossy/workspaces/schema/workspace'
8
+
9
+ export { DB_URL, DB_NAME }
5
10
  export const API_URL = process.env.OSSY_API_URL ?? process.env.API_URL ?? 'http://localhost:3001/api/v0'
6
11
  export const ACTIONS_URL = API_URL.replace(/\/api\/v0\/?$/, '')
7
12
  export const APP_URL = process.env.OSSY_APP_URL ?? process.env.BASE_URL ?? 'http://localhost:3002'
8
13
 
9
- /**
10
- * Polls MongoDB until a Verification token appears for the given userId,
11
- * or throws if it doesn't arrive within the timeout.
12
- *
13
- * Uses directConnection=true so the driver doesn't follow the replica-set
14
- * member list (which uses Docker-internal hostnames like "mongodb:27017"
15
- * even when connecting via localhost port-forwarding).
16
- */
17
- async function getVerificationToken(db, userId, timeoutMs = 10000) {
14
+ async function getVerificationToken (db, userId, timeoutMs = 10000) {
18
15
  const eventstore = db.collection('eventstore')
19
16
  const deadline = Date.now() + timeoutMs
20
17
  while (Date.now() < deadline) {
21
18
  const event = await eventstore.findOne({
22
- aggregateType: 'Token',
23
- type: 'Created',
19
+ type: TOKEN_SCHEMA,
20
+ event: 'Created',
24
21
  'payload.type': 'Verification',
25
22
  createdBy: userId,
26
23
  }, { sort: { _id: -1 } })
@@ -30,35 +27,27 @@ async function getVerificationToken(db, userId, timeoutMs = 10000) {
30
27
  throw new Error(`Verification token for user ${userId} not found within ${timeoutMs}ms`)
31
28
  }
32
29
 
33
- /**
34
- * Polls MongoDB until a SignedUp event for the given email is found.
35
- */
36
- async function getUserIdByEmail(db, email, timeoutMs = 10000) {
30
+ async function getUserIdByEmail (db, email, timeoutMs = 10000) {
37
31
  const eventstore = db.collection('eventstore')
38
32
  const deadline = Date.now() + timeoutMs
39
33
  while (Date.now() < deadline) {
40
34
  const event = await eventstore.findOne({
41
- aggregateType: 'User',
42
- type: 'SignedUp',
35
+ type: USER_SCHEMA,
36
+ event: 'Created',
43
37
  'payload.email': email,
44
38
  }, { sort: { _id: -1 } })
45
- if (event?.aggregateId) return event.aggregateId
39
+ if (event?.resourceId) return event.resourceId
46
40
  await new Promise(r => setTimeout(r, 200))
47
41
  }
48
- throw new Error(`SignedUp event for ${email} not found within ${timeoutMs}ms`)
42
+ throw new Error(`Created user event for ${email} not found within ${timeoutMs}ms`)
49
43
  }
50
44
 
51
- /**
52
- * Polls MongoDB until the workspace aggregate for the given userId appears.
53
- * Not fatal if it doesn't arrive — tests that rely on workspace state should
54
- * handle the null return.
55
- */
56
- async function waitForWorkspaceAggregate(db, userId, timeoutMs = 15000) {
45
+ async function waitForWorkspaceAggregate (db, userId, timeoutMs = 15000) {
57
46
  const aggregates = db.collection('aggregates')
58
47
  const deadline = Date.now() + timeoutMs
59
48
  while (Date.now() < deadline) {
60
49
  const workspace = await aggregates.findOne({
61
- type: 'Workspace',
50
+ type: WORKSPACE_SCHEMA,
62
51
  'state.users': userId,
63
52
  })
64
53
  if (workspace) return workspace
@@ -67,14 +56,7 @@ async function waitForWorkspaceAggregate(db, userId, timeoutMs = 15000) {
67
56
  return null
68
57
  }
69
58
 
70
- /**
71
- * Signs up a new user via the API and returns `{ email, userId, token }`.
72
- *
73
- * Waits for the workspace aggregate to be built so tests that rely on
74
- * `useWorkspaces()` for the post-login redirect will find a workspace
75
- * immediately. Uses a unique email per call to avoid cross-run conflicts.
76
- */
77
- export async function signUpAndGetToken(firstName = 'Test', lastName = 'User') {
59
+ export async function signUpAndGetToken (firstName = 'Test', lastName = 'User') {
78
60
  const email = `e2e-${Date.now()}@ossy.local`
79
61
  const client = new MongoClient(DB_URL, { directConnection: true })
80
62
  try {
@@ -84,7 +66,7 @@ export async function signUpAndGetToken(firstName = 'Test', lastName = 'User') {
84
66
  const res = await fetch(`${ACTIONS_URL}/actions`, {
85
67
  method: 'POST',
86
68
  headers: { 'content-type': 'application/json' },
87
- body: JSON.stringify({ action: 'authentication/sign-up', payload: { email, firstName, lastName } }),
69
+ body: JSON.stringify({ action: '@ossy/authentication/actions/sign-up', payload: { email, firstName, lastName } }),
88
70
  })
89
71
  if (!res.ok) throw new Error(`Sign-up failed with status ${res.status}`)
90
72
 
@@ -98,10 +80,7 @@ export async function signUpAndGetToken(firstName = 'Test', lastName = 'User') {
98
80
  }
99
81
  }
100
82
 
101
- /**
102
- * Calls the verify-sign-in endpoint and returns the raw Set-Cookie header.
103
- */
104
- export async function verifySignIn(token) {
83
+ export async function verifySignIn (token) {
105
84
  const res = await fetch(`${API_URL}/users/verify-sign-in?token=${token}`)
106
85
  if (!res.ok) throw new Error(`Verify sign-in failed with status ${res.status}`)
107
86
  const cookie = res.headers.get('set-cookie')