@ossy/platform 1.39.3 → 3.0.2
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/package.json +13 -12
- package/src/Definition.js +1 -1
- package/src/PlatformShell.jsx +5 -5
- package/src/accept-invitation.api.js +46 -0
- package/src/actions/action.service.js +6 -1
- package/src/audit/action-invocation-service.js +1 -2
- package/src/audit/action-invocation.aggregate.js +1 -3
- package/src/audit/task-run-list.aggregate.js +3 -5
- package/src/audit/task-run-service.js +18 -3
- package/src/audit/task-run.aggregate.js +1 -3
- package/src/config.service.js +1 -48
- package/src/directory.schema.js +1 -3
- package/src/file.schema.js +1 -3
- package/src/index.js +0 -1
- package/src/mcp/create-ossy-mcp-server.js +1 -1
- package/src/mcp/mount-platform-mcp.js +1 -1
- package/src/metering/metering-service.js +1 -2
- package/src/push/mount-push-sse.js +51 -3
- package/src/request-diagnostics.js +144 -0
- package/src/request-diagnostics.spec.js +40 -0
- package/src/resources/schema.registry.js +1 -26
- package/src/resources/schema.validation.js +6 -90
- package/src/runtime.js +17 -6
- package/src/select-workspace.api.js +50 -0
- package/src/server.js +166 -48
- package/src/tasks/change-stream.js +69 -9
- package/src/users.middleware.js +52 -20
- package/src/schema-ids.js +0 -9
|
@@ -17,6 +17,10 @@ export class ChangeStream {
|
|
|
17
17
|
static _reconnectAttempts = 0
|
|
18
18
|
static _dbUrl = null
|
|
19
19
|
static _client = null
|
|
20
|
+
/** @type {import('mongodb').ChangeStream | null} */
|
|
21
|
+
static _changeStream = null
|
|
22
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
23
|
+
static _reconnectTimer = null
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* Opens the MongoDB changestream and wires up reconnect logic.
|
|
@@ -33,12 +37,35 @@ export class ChangeStream {
|
|
|
33
37
|
})
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
static stop() {
|
|
40
|
+
static async stop () {
|
|
37
41
|
ChangeStream._stopped = true
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ChangeStream.
|
|
42
|
+
|
|
43
|
+
if (ChangeStream._reconnectTimer) {
|
|
44
|
+
clearTimeout(ChangeStream._reconnectTimer)
|
|
45
|
+
ChangeStream._reconnectTimer = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stream = ChangeStream._changeStream
|
|
49
|
+
ChangeStream._changeStream = null
|
|
50
|
+
if (stream) {
|
|
51
|
+
try {
|
|
52
|
+
await stream.close()
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore — stream may already be closed
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const client = ChangeStream._client
|
|
59
|
+
ChangeStream._client = null
|
|
60
|
+
if (client) {
|
|
61
|
+
try {
|
|
62
|
+
await client.close()
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
41
66
|
}
|
|
67
|
+
|
|
68
|
+
log.info('[ChangeStream] Stopped')
|
|
42
69
|
}
|
|
43
70
|
|
|
44
71
|
static async _getMongoClient() {
|
|
@@ -46,7 +73,10 @@ export class ChangeStream {
|
|
|
46
73
|
return MongoClient
|
|
47
74
|
}
|
|
48
75
|
|
|
49
|
-
static async _getClient() {
|
|
76
|
+
static async _getClient () {
|
|
77
|
+
if (ChangeStream._stopped) {
|
|
78
|
+
throw new Error('ChangeStream stopped')
|
|
79
|
+
}
|
|
50
80
|
if (!ChangeStream._client) {
|
|
51
81
|
const MongoClient = await ChangeStream._getMongoClient()
|
|
52
82
|
ChangeStream._client = new MongoClient(ChangeStream._dbUrl, {
|
|
@@ -56,7 +86,8 @@ export class ChangeStream {
|
|
|
56
86
|
return ChangeStream._client
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
static async _resetClient() {
|
|
89
|
+
static async _resetClient () {
|
|
90
|
+
if (ChangeStream._stopped) return
|
|
60
91
|
const MongoClient = await ChangeStream._getMongoClient()
|
|
61
92
|
const prev = ChangeStream._client
|
|
62
93
|
ChangeStream._client = new MongoClient(ChangeStream._dbUrl, {
|
|
@@ -68,7 +99,7 @@ export class ChangeStream {
|
|
|
68
99
|
log.info('[ChangeStream] New MongoClient instance created')
|
|
69
100
|
}
|
|
70
101
|
|
|
71
|
-
static _scheduleReconnect() {
|
|
102
|
+
static _scheduleReconnect () {
|
|
72
103
|
if (ChangeStream._stopped) return
|
|
73
104
|
|
|
74
105
|
const delaySecs = Math.min(Math.pow(2, ChangeStream._reconnectAttempts), 30)
|
|
@@ -76,7 +107,12 @@ export class ChangeStream {
|
|
|
76
107
|
|
|
77
108
|
log.info(`[ChangeStream] Reconnecting in ${delaySecs}s...`)
|
|
78
109
|
|
|
79
|
-
|
|
110
|
+
if (ChangeStream._reconnectTimer) {
|
|
111
|
+
clearTimeout(ChangeStream._reconnectTimer)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ChangeStream._reconnectTimer = setTimeout(() => {
|
|
115
|
+
ChangeStream._reconnectTimer = null
|
|
80
116
|
if (ChangeStream._stopped) return
|
|
81
117
|
ChangeStream._open().catch((error) => {
|
|
82
118
|
log.error('[ChangeStream] Change stream could not be opened', undefined, error)
|
|
@@ -85,16 +121,30 @@ export class ChangeStream {
|
|
|
85
121
|
}, delaySecs * 1000)
|
|
86
122
|
}
|
|
87
123
|
|
|
88
|
-
static async _open() {
|
|
124
|
+
static async _open () {
|
|
125
|
+
if (ChangeStream._stopped) return
|
|
126
|
+
|
|
89
127
|
log.info('[ChangeStream] Watching for changes')
|
|
90
128
|
|
|
91
129
|
const dbName = process.env.DB_NAME || 'test'
|
|
92
130
|
const client = await ChangeStream._getClient()
|
|
131
|
+
if (ChangeStream._stopped) return
|
|
132
|
+
|
|
93
133
|
const collection = client.db(dbName).collection('eventstore')
|
|
94
134
|
|
|
135
|
+
if (ChangeStream._changeStream) {
|
|
136
|
+
try {
|
|
137
|
+
await ChangeStream._changeStream.close()
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore
|
|
140
|
+
}
|
|
141
|
+
ChangeStream._changeStream = null
|
|
142
|
+
}
|
|
143
|
+
|
|
95
144
|
const changeStream = collection.watch([
|
|
96
145
|
{ $match: { operationType: 'insert' } },
|
|
97
146
|
])
|
|
147
|
+
ChangeStream._changeStream = changeStream
|
|
98
148
|
|
|
99
149
|
const wasReconnecting = ChangeStream._reconnectAttempts > 0
|
|
100
150
|
ChangeStream._reconnectAttempts = 0
|
|
@@ -105,6 +155,7 @@ export class ChangeStream {
|
|
|
105
155
|
// Ensures only one reconnect is scheduled if both 'error' and 'close' fire for the same failure.
|
|
106
156
|
let reconnectScheduled = false
|
|
107
157
|
const scheduleOnce = () => {
|
|
158
|
+
if (ChangeStream._stopped) return
|
|
108
159
|
if (!reconnectScheduled) {
|
|
109
160
|
reconnectScheduled = true
|
|
110
161
|
ChangeStream._scheduleReconnect()
|
|
@@ -112,6 +163,7 @@ export class ChangeStream {
|
|
|
112
163
|
}
|
|
113
164
|
|
|
114
165
|
changeStream.on('error', (error) => {
|
|
166
|
+
if (ChangeStream._stopped) return
|
|
115
167
|
log.error('[ChangeStream] Change stream error (server keeps running)', undefined, error)
|
|
116
168
|
if (isMongoTopologyClosedError(error)) {
|
|
117
169
|
ChangeStream._resetClient().catch(() => {})
|
|
@@ -132,11 +184,19 @@ export class ChangeStream {
|
|
|
132
184
|
})
|
|
133
185
|
|
|
134
186
|
changeStream.on('close', () => {
|
|
187
|
+
if (ChangeStream._changeStream === changeStream) {
|
|
188
|
+
ChangeStream._changeStream = null
|
|
189
|
+
}
|
|
190
|
+
if (ChangeStream._stopped) return
|
|
135
191
|
log.info('[ChangeStream] close detected')
|
|
136
192
|
scheduleOnce()
|
|
137
193
|
})
|
|
138
194
|
|
|
139
195
|
changeStream.on('end', () => {
|
|
196
|
+
if (ChangeStream._changeStream === changeStream) {
|
|
197
|
+
ChangeStream._changeStream = null
|
|
198
|
+
}
|
|
199
|
+
if (ChangeStream._stopped) return
|
|
140
200
|
log.info('[ChangeStream] end detected')
|
|
141
201
|
scheduleOnce()
|
|
142
202
|
})
|
package/src/users.middleware.js
CHANGED
|
@@ -5,8 +5,14 @@ import { User } from '@ossy/users/server'
|
|
|
5
5
|
import { Token } from '@ossy/tokens/server'
|
|
6
6
|
import { ConfigService } from './config.service.js'
|
|
7
7
|
import { PoliciesQueries } from '@ossy/policies/server'
|
|
8
|
+
import {
|
|
9
|
+
OperationTimeoutError,
|
|
10
|
+
resolveTimeoutMs,
|
|
11
|
+
withTimeout,
|
|
12
|
+
} from './request-diagnostics.js'
|
|
8
13
|
|
|
9
14
|
const log = createLogger('users')
|
|
15
|
+
const AUTH_TIMEOUT_MS = resolveTimeoutMs('OSSY_AUTH_TIMEOUT_MS', 10_000)
|
|
10
16
|
|
|
11
17
|
function normalizeAuthToken (token) {
|
|
12
18
|
const trimmed = String(token ?? '').trim()
|
|
@@ -58,29 +64,55 @@ export class UsersMiddleware {
|
|
|
58
64
|
log.info('[UsersMiddleware] Authenticating')
|
|
59
65
|
log.debug('[UsersMiddleware] authToken', { authToken })
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.then(({ sub }) => Aggregate.Of(User, sub))
|
|
69
|
-
.then(Aggregate.View())
|
|
70
|
-
.then(user => {
|
|
71
|
-
const workspaces = user.workspaces ?? []
|
|
67
|
+
const authStarted = performance.now()
|
|
68
|
+
let authFinished = false
|
|
69
|
+
const finishAuth = (apply) => {
|
|
70
|
+
if (authFinished) return
|
|
71
|
+
authFinished = true
|
|
72
|
+
apply()
|
|
73
|
+
}
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
withTimeout(
|
|
76
|
+
TokenService.verify(authToken)
|
|
77
|
+
.then(UsersMiddleware.assertApiTokenActive)
|
|
78
|
+
.then(payload => {
|
|
79
|
+
req.authPayload = payload
|
|
80
|
+
req.tokenScopes = payload?.type === 'Api' ? (payload.scopes ?? ['*']) : null
|
|
81
|
+
return payload
|
|
82
|
+
})
|
|
83
|
+
.then(({ sub }) => Aggregate.Of(User, sub))
|
|
84
|
+
.then(Aggregate.View())
|
|
85
|
+
.then(user => {
|
|
86
|
+
const workspaces = user.workspaces ?? []
|
|
87
|
+
|
|
88
|
+
return PoliciesQueries.GetPoliciesForUser(user.id)
|
|
89
|
+
.then(policies => {
|
|
90
|
+
finishAuth(() => {
|
|
91
|
+
log.info('[UsersMiddleware] Resolved user', {
|
|
92
|
+
elapsedMs: Math.round(performance.now() - authStarted),
|
|
93
|
+
})
|
|
94
|
+
req.userId = user.id
|
|
95
|
+
req.user = { ...user, workspaces, policies }
|
|
96
|
+
next()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}),
|
|
100
|
+
AUTH_TIMEOUT_MS,
|
|
101
|
+
'UsersMiddleware.AuthenticateUser',
|
|
102
|
+
)
|
|
81
103
|
.catch(error => {
|
|
104
|
+
if (error instanceof OperationTimeoutError) {
|
|
105
|
+
log.warn('[UsersMiddleware] Auth timed out', {
|
|
106
|
+
timeoutMs: error.timeoutMs,
|
|
107
|
+
elapsedMs: Math.round(performance.now() - authStarted),
|
|
108
|
+
})
|
|
109
|
+
finishAuth(() => {
|
|
110
|
+
res.status(504).json({ error: 'Authentication timed out', code: 'AUTH_TIMEOUT' })
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
82
114
|
log.debug('[UsersMiddleware] Auth failed; anonymous', { error })
|
|
83
|
-
asAnonymous
|
|
115
|
+
finishAuth(asAnonymous)
|
|
84
116
|
})
|
|
85
117
|
}
|
|
86
118
|
|
package/src/schema-ids.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/** Platform envelope schemas (ADR 0008). */
|
|
2
|
-
export const PlatformSchema = Object.freeze({
|
|
3
|
-
config: '@ossy/platform/schema/config',
|
|
4
|
-
directory: '@ossy/platform/schema/directory',
|
|
5
|
-
file: '@ossy/platform/schema/file',
|
|
6
|
-
taskRun: '@ossy/platform/schema/task-run',
|
|
7
|
-
actionInvocation: '@ossy/platform/schema/action-invocation',
|
|
8
|
-
meterEvent: '@ossy/platform/schema/meter-event',
|
|
9
|
-
})
|