@fabrika/operations-contract 0.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/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@fabrika/operations-contract",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/contember/fabrika-platform.git",
8
+ "directory": "packages/operations-contract"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "files": [
14
+ "src"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "bun": "./src/index.ts",
19
+ "types": "./src/index.ts",
20
+ "default": "./src/index.ts"
21
+ },
22
+ "./ingest": {
23
+ "bun": "./src/ingest.ts",
24
+ "types": "./src/ingest.ts",
25
+ "default": "./src/ingest.ts"
26
+ },
27
+ "./operator": {
28
+ "bun": "./src/operator.ts",
29
+ "types": "./src/operator.ts",
30
+ "default": "./src/operator.ts"
31
+ },
32
+ "./operator-api": {
33
+ "bun": "./src/operator-api.ts",
34
+ "types": "./src/operator-api.ts",
35
+ "default": "./src/operator-api.ts"
36
+ },
37
+ "./access": {
38
+ "bun": "./src/access.ts",
39
+ "types": "./src/access.ts",
40
+ "default": "./src/access.ts"
41
+ },
42
+ "./catalog": {
43
+ "bun": "./src/catalog.ts",
44
+ "types": "./src/catalog.ts",
45
+ "default": "./src/catalog.ts"
46
+ },
47
+ "./releases": {
48
+ "bun": "./src/releases.ts",
49
+ "types": "./src/releases.ts",
50
+ "default": "./src/releases.ts"
51
+ },
52
+ "./rpc": {
53
+ "bun": "./src/rpc.ts",
54
+ "types": "./src/rpc.ts",
55
+ "default": "./src/rpc.ts"
56
+ }
57
+ },
58
+ "scripts": {
59
+ "typecheck": "tsc --noEmit"
60
+ },
61
+ "dependencies": {
62
+ "@fabrika/app": "^0.0.1"
63
+ }
64
+ }
package/src/access.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** IAM vocabulary shared by the Operations runtime and Fabrika's Access schema. */
2
+
3
+ /**
4
+ * The app id every composition names the Operations HOST by — the Cloudflare proxy Worker's manifest,
5
+ * the shared platform manifest, and `defineApp`. One constant because a shared manifest may not name
6
+ * one app twice, so the two compositions cannot disagree here and stay generalisable.
7
+ */
8
+ export const OPERATIONS_APP_ID = 'operations'
9
+
10
+ export const OPERATIONS_ACTIONS = {
11
+ READ: 'operations.read',
12
+ TRIAGE: 'operations.triage',
13
+ MANAGE: 'operations.manage',
14
+ } as const
15
+
16
+ export type OperationsAction = (typeof OPERATIONS_ACTIONS)[keyof typeof OPERATIONS_ACTIONS]
package/src/catalog.ts ADDED
@@ -0,0 +1,103 @@
1
+ /** Versioned private Control → Operations source-catalog protocol. */
2
+
3
+ export const OPERATIONS_CATALOG_PROTOCOL_VERSION = 2
4
+ export const DEFAULT_OPERATIONS_SERVICE_KEY = 'default'
5
+
6
+ export interface OperationsSourceCoordinateV1 {
7
+ appId: string
8
+ environment: string
9
+ /** Omitted by Control while an app environment has only its default service. */
10
+ serviceKey?: string
11
+ }
12
+
13
+ export interface CanonicalOperationsSourceCoordinateV1 {
14
+ appId: string
15
+ environment: string
16
+ serviceKey: string
17
+ }
18
+
19
+ export interface OperationsCatalogSourceV1 {
20
+ coordinate: OperationsSourceCoordinateV1
21
+ displayName: string
22
+ /** Canonical externally reachable origin when Control owns one; null when it does not. */
23
+ publicOrigin?: string | null
24
+ }
25
+
26
+ export interface CanonicalOperationsCatalogSourceV1 {
27
+ coordinate: CanonicalOperationsSourceCoordinateV1
28
+ displayName: string
29
+ publicOrigin: string | null
30
+ }
31
+
32
+ /** Caller-owned write credential transported only over the private catalog channel. */
33
+ export interface OperationsCatalogIngestCredentialV2 {
34
+ id: string
35
+ publicKey: string
36
+ }
37
+
38
+ export interface OperationsCatalogSourceV2 extends OperationsCatalogSourceV1 {
39
+ ingestCredential: OperationsCatalogIngestCredentialV2
40
+ }
41
+
42
+ export interface CanonicalOperationsCatalogSourceV2 extends CanonicalOperationsCatalogSourceV1 {
43
+ ingestCredential: OperationsCatalogIngestCredentialV2
44
+ }
45
+
46
+ export interface OperationsCatalogReconcileRequestV2 {
47
+ protocolVersion: typeof OPERATIONS_CATALOG_PROTOCOL_VERSION
48
+ revision: number
49
+ snapshotHash: string
50
+ sources: OperationsCatalogSourceV2[]
51
+ }
52
+
53
+ export type OperationsCatalogReconcileOutcome = 'applied' | 'unchanged' | 'stale'
54
+
55
+ export interface OperationsCatalogIngestResultV2 {
56
+ coordinate: CanonicalOperationsSourceCoordinateV1
57
+ credentialId: string
58
+ ingestProjectId: string
59
+ }
60
+
61
+ export interface OperationsCatalogReconcileResponseV2 {
62
+ protocolVersion: typeof OPERATIONS_CATALOG_PROTOCOL_VERSION
63
+ revision: number
64
+ outcome: OperationsCatalogReconcileOutcome
65
+ created: number
66
+ updated: number
67
+ disabled: number
68
+ reenabled: number
69
+ unchanged: number
70
+ ingest: OperationsCatalogIngestResultV2[]
71
+ }
72
+
73
+ export function canonicalOperationsServiceKey(serviceKey: string | undefined): string {
74
+ return serviceKey ?? DEFAULT_OPERATIONS_SERVICE_KEY
75
+ }
76
+
77
+ export function canonicalOperationsCatalogSources(
78
+ sources: readonly OperationsCatalogSourceV2[],
79
+ ): CanonicalOperationsCatalogSourceV2[] {
80
+ return sources.map((source) => ({
81
+ coordinate: {
82
+ appId: source.coordinate.appId,
83
+ environment: source.coordinate.environment,
84
+ serviceKey: canonicalOperationsServiceKey(source.coordinate.serviceKey),
85
+ },
86
+ displayName: source.displayName,
87
+ publicOrigin: source.publicOrigin ?? null,
88
+ ingestCredential: source.ingestCredential,
89
+ })).sort((left, right) => {
90
+ const app = left.coordinate.appId.localeCompare(right.coordinate.appId)
91
+ if (app !== 0) return app
92
+ const environment = left.coordinate.environment.localeCompare(right.coordinate.environment)
93
+ if (environment !== 0) return environment
94
+ return left.coordinate.serviceKey.localeCompare(right.coordinate.serviceKey)
95
+ })
96
+ }
97
+
98
+ /** Hash the canonical source array only; revision is transport ordering, not catalog content. */
99
+ export async function operationsCatalogSnapshotHash(sources: readonly OperationsCatalogSourceV2[]): Promise<string> {
100
+ const bytes = new TextEncoder().encode(JSON.stringify(canonicalOperationsCatalogSources(sources)))
101
+ const digest = await crypto.subtle.digest('SHA-256', bytes)
102
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('')
103
+ }
package/src/index.ts ADDED
@@ -0,0 +1,120 @@
1
+ export { OPERATIONS_ACTIONS, OPERATIONS_APP_ID } from './access.js'
2
+ export type { OperationsAction } from './access.js'
3
+ export {
4
+ canonicalOperationsCatalogSources,
5
+ canonicalOperationsServiceKey,
6
+ DEFAULT_OPERATIONS_SERVICE_KEY,
7
+ OPERATIONS_CATALOG_PROTOCOL_VERSION,
8
+ operationsCatalogSnapshotHash,
9
+ } from './catalog.js'
10
+ export type {
11
+ CanonicalOperationsCatalogSourceV1,
12
+ CanonicalOperationsCatalogSourceV2,
13
+ CanonicalOperationsSourceCoordinateV1,
14
+ OperationsCatalogIngestCredentialV2,
15
+ OperationsCatalogIngestResultV2,
16
+ OperationsCatalogReconcileOutcome,
17
+ OperationsCatalogReconcileRequestV2,
18
+ OperationsCatalogReconcileResponseV2,
19
+ OperationsCatalogSourceV1,
20
+ OperationsCatalogSourceV2,
21
+ OperationsSourceCoordinateV1,
22
+ } from './catalog.js'
23
+ export {
24
+ buildOperationsDsn,
25
+ FABRIKA_APP_ID,
26
+ FABRIKA_ENVIRONMENT,
27
+ FABRIKA_OPERATIONS_DSN,
28
+ FABRIKA_SERVICE_KEY,
29
+ OPERATIONS_MANAGED_ENVIRONMENT_KEYS,
30
+ operationsEnvelopeUrl,
31
+ operationsManagedEnvironment,
32
+ operationsManagedEnvironmentCollisions,
33
+ } from './ingest.js'
34
+ export type {
35
+ IngestMessage,
36
+ IngestRejectReason,
37
+ OperationsIngestConfiguration,
38
+ OperationsManagedEnvironmentKey,
39
+ ParsedEvent,
40
+ ParsedException,
41
+ StackFrame,
42
+ } from './ingest.js'
43
+ export type {
44
+ OperationsAlertKind,
45
+ OperationsAlertRuleDto,
46
+ OperationsAlertRuleRequestDto,
47
+ OperationsAlertSettingsResponseDto,
48
+ OperationsAssigneeListResponseDto,
49
+ OperationsBulkIssueStatusRequestDto,
50
+ OperationsBulkIssueStatusResponseDto,
51
+ OperationsCurrentHttpHealthDto,
52
+ OperationsEventDetailResponseDto,
53
+ OperationsHealthCheckDto,
54
+ OperationsHealthCheckUpsertRequestDto,
55
+ OperationsHealthObservationDto,
56
+ OperationsHealthResponseDto,
57
+ OperationsHealthState,
58
+ OperationsIssueDetailDto,
59
+ OperationsIssueDetailResponseDto,
60
+ OperationsIssueListResponseDto,
61
+ OperationsIssueMutationRequestDto,
62
+ OperationsIssueOccurrenceDto,
63
+ OperationsIssueOccurrenceListResponseDto,
64
+ OperationsIssueSummaryDto,
65
+ OperationsNotificationAttemptDto,
66
+ OperationsNotificationChannelDto,
67
+ OperationsNotificationChannelRequestDto,
68
+ OperationsNotificationDeliveryDto,
69
+ OperationsReleaseDetailResponseDto,
70
+ OperationsReleaseListResponseDto,
71
+ OperationsReleaseSummaryDto,
72
+ OperationsSourceDetailResponseDto,
73
+ OperationsSourceHealthDto,
74
+ OperationsSourceListResponseDto,
75
+ OperationsSourceSummaryDto,
76
+ OperationsSpikeAlertRequestDto,
77
+ } from './operator-api.js'
78
+ export type {
79
+ ActivityDraft,
80
+ ActivityItem,
81
+ ActivityKind,
82
+ DisplayFrame,
83
+ EventBreadcrumb,
84
+ EventDetail,
85
+ EventException,
86
+ IssueAlertType,
87
+ IssueListItem,
88
+ IssueMutation,
89
+ IssueMutationDecision,
90
+ IssueStatus,
91
+ Occurrence,
92
+ OccurrenceTransition,
93
+ PriorIssueState,
94
+ RawEventFrame,
95
+ SourceContext,
96
+ } from './operator.js'
97
+ export {
98
+ FABRIKA_RELEASE,
99
+ normalizeOperationsCommit,
100
+ OPERATIONS_ARTIFACT_HEADERS,
101
+ OPERATIONS_RELEASE_PROTOCOL_VERSION,
102
+ OPERATIONS_RELEASE_RECONCILE_PATH,
103
+ OPERATIONS_SOURCE_MAP_UPLOAD_PATH,
104
+ operationsReleaseName,
105
+ operationsSourceMapUploadUrl,
106
+ } from './releases.js'
107
+ export type {
108
+ OperationsArtifactState,
109
+ OperationsArtifactUploadConfiguration,
110
+ OperationsArtifactUploadCredentialV1,
111
+ OperationsReleaseAvailabilityV1,
112
+ OperationsReleaseCoordinateV1,
113
+ OperationsReleaseOutcome,
114
+ OperationsReleasePhase,
115
+ OperationsReleaseReconcileOutcome,
116
+ OperationsReleaseReconcileRequestV1,
117
+ OperationsReleaseReconcileResponseV1,
118
+ OperationsReleaseUnavailableReason,
119
+ } from './releases.js'
120
+ export type { OperationsIssueQuery, OperationsReleaseQuery, OperationsRpcContract } from './rpc.js'
package/src/ingest.ts ADDED
@@ -0,0 +1,130 @@
1
+ /** Browser- and runtime-safe shapes at the direct Sentry-compatible ingest boundary. */
2
+
3
+ export const FABRIKA_OPERATIONS_DSN = 'FABRIKA_OPERATIONS_DSN'
4
+ export const FABRIKA_APP_ID = 'FABRIKA_APP_ID'
5
+ export const FABRIKA_ENVIRONMENT = 'FABRIKA_ENVIRONMENT'
6
+ export const FABRIKA_SERVICE_KEY = 'FABRIKA_SERVICE_KEY'
7
+
8
+ export type OperationsManagedEnvironmentKey =
9
+ | typeof FABRIKA_OPERATIONS_DSN
10
+ | typeof FABRIKA_APP_ID
11
+ | typeof FABRIKA_ENVIRONMENT
12
+ | typeof FABRIKA_SERVICE_KEY
13
+
14
+ export const OPERATIONS_MANAGED_ENVIRONMENT_KEYS: readonly OperationsManagedEnvironmentKey[] = [
15
+ FABRIKA_OPERATIONS_DSN,
16
+ FABRIKA_APP_ID,
17
+ FABRIKA_ENVIRONMENT,
18
+ FABRIKA_SERVICE_KEY,
19
+ ]
20
+
21
+ export interface OperationsIngestConfiguration {
22
+ dsn: string
23
+ appId: string
24
+ environment: string
25
+ serviceKey: string
26
+ }
27
+
28
+ const PUBLIC_KEY_PATTERN = /^[0-9a-f]{32}$/
29
+ const INGEST_PROJECT_ID_PATTERN = /^[1-9][0-9]{0,18}$/
30
+
31
+ export function buildOperationsDsn(origin: string, publicKey: string, ingestProjectId: string): string {
32
+ if (!PUBLIC_KEY_PATTERN.test(publicKey)) throw new Error('invalid Operations ingest public key')
33
+ if (!INGEST_PROJECT_ID_PATTERN.test(ingestProjectId)) throw new Error('invalid Operations ingest project id')
34
+ const url = new URL(origin)
35
+ if (
36
+ (url.protocol !== 'https:' && url.protocol !== 'http:')
37
+ || url.origin !== origin
38
+ || url.username !== ''
39
+ || url.password !== ''
40
+ ) {
41
+ throw new Error('Operations ingest origin must be an HTTP origin')
42
+ }
43
+ url.username = publicKey
44
+ url.pathname = `/${ingestProjectId}`
45
+ return url.toString()
46
+ }
47
+
48
+ export function operationsEnvelopeUrl(origin: string, ingestProjectId: string): string {
49
+ if (!INGEST_PROJECT_ID_PATTERN.test(ingestProjectId)) throw new Error('invalid Operations ingest project id')
50
+ const url = new URL(origin)
51
+ if (
52
+ (url.protocol !== 'https:' && url.protocol !== 'http:')
53
+ || url.origin !== origin
54
+ || url.username !== ''
55
+ || url.password !== ''
56
+ ) {
57
+ throw new Error('Operations ingest origin must be an HTTP origin')
58
+ }
59
+ url.pathname = `/api/${ingestProjectId}/envelope/`
60
+ return url.toString()
61
+ }
62
+
63
+ export function operationsManagedEnvironment(input: OperationsIngestConfiguration): Record<OperationsManagedEnvironmentKey, string> {
64
+ return {
65
+ [FABRIKA_OPERATIONS_DSN]: input.dsn,
66
+ [FABRIKA_APP_ID]: input.appId,
67
+ [FABRIKA_ENVIRONMENT]: input.environment,
68
+ [FABRIKA_SERVICE_KEY]: input.serviceKey,
69
+ }
70
+ }
71
+
72
+ export function operationsManagedEnvironmentCollisions(keys: Iterable<string>): OperationsManagedEnvironmentKey[] {
73
+ const occupied = new Set(keys)
74
+ return OPERATIONS_MANAGED_ENVIRONMENT_KEYS.filter((key) => occupied.has(key))
75
+ }
76
+
77
+ export interface StackFrame {
78
+ function?: string
79
+ filename?: string
80
+ module?: string
81
+ absPath?: string
82
+ line?: number
83
+ column?: number
84
+ inApp?: boolean
85
+ preContext?: string[]
86
+ contextLine?: string
87
+ postContext?: string[]
88
+ }
89
+
90
+ export interface ParsedException {
91
+ type: string
92
+ value: string
93
+ frames: StackFrame[]
94
+ }
95
+
96
+ export interface ParsedEvent {
97
+ eventId: string
98
+ projectId: string
99
+ exception: ParsedException
100
+ level: string
101
+ release?: string
102
+ environment?: string
103
+ platform?: string
104
+ tags: Record<string, string>
105
+ sdkFingerprint: string[] | null
106
+ receivedAt: number
107
+ raw: Record<string, unknown>
108
+ }
109
+
110
+ export interface IngestMessage {
111
+ projectId: string
112
+ fingerprint: string
113
+ eventId: string
114
+ title: string
115
+ culprit: string | null
116
+ level: string
117
+ release?: string
118
+ environment?: string
119
+ receivedAt: number
120
+ payload: Record<string, unknown>
121
+ }
122
+
123
+ export type IngestRejectReason =
124
+ | 'unauthorized'
125
+ | 'forbidden'
126
+ | 'too_large'
127
+ | 'malformed'
128
+ | 'unsupported'
129
+ | 'rate_limited'
130
+ | 'unavailable'
@@ -0,0 +1,250 @@
1
+ import type { ActivityItem, EventDetail, IssueStatus } from './operator.js'
2
+ import type { OperationsArtifactState } from './releases.js'
3
+
4
+ export type OperationsAlertKind =
5
+ | 'new_issue'
6
+ | 'regression'
7
+ | 'spike'
8
+ | 'failed_check'
9
+ | 'recovery'
10
+ | 'unhealthy_telemetry'
11
+
12
+ export type OperationsHealthState = 'healthy' | 'degraded' | 'failed' | 'stale' | 'unavailable'
13
+
14
+ export interface OperationsSourceSummaryDto {
15
+ id: string
16
+ appId: string
17
+ environment: string
18
+ serviceKey: string
19
+ displayName: string
20
+ publicOrigin: string | null
21
+ enabled: boolean
22
+ }
23
+
24
+ export interface OperationsSourceListResponseDto {
25
+ items: OperationsSourceSummaryDto[]
26
+ }
27
+
28
+ export interface OperationsSourceDetailResponseDto {
29
+ source: OperationsSourceSummaryDto
30
+ }
31
+
32
+ export interface OperationsIssueSummaryDto {
33
+ id: string
34
+ source: OperationsSourceSummaryDto
35
+ title: string
36
+ culprit: string | null
37
+ level: string
38
+ status: IssueStatus
39
+ assignedTo: { id: string; label: string | null } | null
40
+ regressedAt: number | null
41
+ firstSeen: number
42
+ lastSeen: number
43
+ count: number
44
+ trend: number[]
45
+ }
46
+
47
+ export interface OperationsIssueOccurrenceDto {
48
+ id: string
49
+ eventId: string
50
+ receivedAt: number
51
+ release: string | null
52
+ }
53
+
54
+ export interface OperationsIssueListResponseDto {
55
+ items: OperationsIssueSummaryDto[]
56
+ nextCursor: string | null
57
+ summary: {
58
+ total: number
59
+ open: number
60
+ resolved: number
61
+ ignored: number
62
+ }
63
+ }
64
+
65
+ export interface OperationsIssueDetailDto extends OperationsIssueSummaryDto {
66
+ snoozeUntil: number | null
67
+ snoozeUntilCount: number | null
68
+ resolvedInRelease: { id: string; name: string } | null
69
+ mergedIntoIssueId: string | null
70
+ activity: ActivityItem[]
71
+ latestOccurrence: OperationsIssueOccurrenceDto | null
72
+ occurrences: OperationsIssueOccurrenceDto[]
73
+ occurrencesNextCursor: string | null
74
+ }
75
+
76
+ export interface OperationsIssueDetailResponseDto {
77
+ issue: OperationsIssueDetailDto
78
+ }
79
+
80
+ export interface OperationsEventDetailResponseDto {
81
+ occurrenceId: string
82
+ receivedAt: number
83
+ detail: EventDetail
84
+ }
85
+
86
+ export interface OperationsIssueOccurrenceListResponseDto {
87
+ items: OperationsIssueOccurrenceDto[]
88
+ nextCursor: string | null
89
+ }
90
+
91
+ export type OperationsIssueMutationRequestDto =
92
+ | { kind: 'status'; status: IssueStatus }
93
+ | { kind: 'comment'; text: string }
94
+ | { kind: 'assign'; principalId: string | null }
95
+ | { kind: 'snooze_until'; until: number }
96
+ | { kind: 'snooze_count'; additional: number }
97
+ | { kind: 'resolve_in_release'; releaseId: string | null }
98
+ | { kind: 'merge'; targetIssueId: string }
99
+
100
+ export interface OperationsBulkIssueStatusRequestDto {
101
+ issueIds: string[]
102
+ status: IssueStatus
103
+ }
104
+
105
+ export interface OperationsBulkIssueStatusResponseDto {
106
+ items: OperationsIssueSummaryDto[]
107
+ }
108
+
109
+ export interface OperationsAssigneeListResponseDto {
110
+ items: {
111
+ id: string
112
+ label: string
113
+ }[]
114
+ }
115
+
116
+ export interface OperationsReleaseSummaryDto {
117
+ id: string
118
+ source: OperationsSourceSummaryDto
119
+ runId: string
120
+ commitSha: string
121
+ state: string
122
+ artifactState: OperationsArtifactState
123
+ releaseName: string
124
+ createdAt: number
125
+ finishedAt: number | null
126
+ newIssueCount: number
127
+ regressionCount: number
128
+ }
129
+
130
+ export interface OperationsReleaseListResponseDto {
131
+ items: OperationsReleaseSummaryDto[]
132
+ nextCursor: string | null
133
+ }
134
+
135
+ export interface OperationsReleaseDetailResponseDto {
136
+ release: OperationsReleaseSummaryDto
137
+ issues: OperationsIssueSummaryDto[]
138
+ }
139
+
140
+ export interface OperationsCurrentHttpHealthDto {
141
+ state: Exclude<OperationsHealthState, 'unavailable'>
142
+ observedAt: number
143
+ latencyMs: number | null
144
+ detailCode: string
145
+ consecutiveFailures: number
146
+ consecutiveSuccesses: number
147
+ }
148
+
149
+ export interface OperationsHealthObservationDto {
150
+ id: string
151
+ state: OperationsHealthState
152
+ observedAt: number
153
+ latencyMs: number | null
154
+ detailCode: string | null
155
+ successful: boolean
156
+ statusCode: number | null
157
+ }
158
+
159
+ export interface OperationsHealthCheckDto {
160
+ id: string
161
+ sourceId: string
162
+ path: string
163
+ enabled: boolean
164
+ intervalMs: number
165
+ timeoutMs: number
166
+ expectedStatus: number
167
+ failureThreshold: number
168
+ recoveryThreshold: number
169
+ staleAfterMs: number
170
+ current: OperationsCurrentHttpHealthDto | null
171
+ history: OperationsHealthObservationDto[]
172
+ }
173
+
174
+ export interface OperationsSourceHealthDto {
175
+ source: OperationsSourceSummaryDto
176
+ httpChecks: OperationsHealthCheckDto[]
177
+ telemetryState: OperationsHealthState
178
+ }
179
+
180
+ export interface OperationsHealthResponseDto {
181
+ sources: OperationsSourceHealthDto[]
182
+ }
183
+
184
+ export interface OperationsHealthCheckUpsertRequestDto {
185
+ path: string
186
+ enabled: boolean
187
+ intervalMs: number
188
+ timeoutMs: number
189
+ expectedStatus: number
190
+ failureThreshold: number
191
+ recoveryThreshold: number
192
+ staleAfterMs: number
193
+ }
194
+
195
+ export interface OperationsAlertRuleDto {
196
+ kind: OperationsAlertKind
197
+ enabled: boolean
198
+ }
199
+
200
+ export interface OperationsNotificationChannelDto {
201
+ id: string
202
+ scope: OperationsAlertKind
203
+ type: 'webhook'
204
+ targetDisplay: string
205
+ hasTarget: boolean
206
+ enabled: boolean
207
+ }
208
+
209
+ export interface OperationsNotificationAttemptDto {
210
+ id: string
211
+ attemptedAt: number
212
+ delivered: boolean
213
+ errorCode: string | null
214
+ }
215
+
216
+ export interface OperationsNotificationDeliveryDto {
217
+ id: string
218
+ channelId: string
219
+ kind: OperationsAlertKind
220
+ createdAt: number
221
+ attemptCount: number
222
+ maxAttempts: number
223
+ status: 'pending' | 'delivered' | 'abandoned'
224
+ deliveredAt: number | null
225
+ abandonedAt: number | null
226
+ attempts: OperationsNotificationAttemptDto[]
227
+ }
228
+
229
+ export interface OperationsAlertSettingsResponseDto {
230
+ spike: { threshold: number; enabled: boolean } | null
231
+ rules: OperationsAlertRuleDto[]
232
+ channels: OperationsNotificationChannelDto[]
233
+ deliveries: OperationsNotificationDeliveryDto[]
234
+ }
235
+
236
+ export interface OperationsSpikeAlertRequestDto {
237
+ threshold: number
238
+ enabled: boolean
239
+ }
240
+
241
+ export interface OperationsAlertRuleRequestDto {
242
+ enabled: boolean
243
+ }
244
+
245
+ export interface OperationsNotificationChannelRequestDto {
246
+ scope: OperationsAlertKind
247
+ type: 'webhook'
248
+ target?: string
249
+ enabled: boolean
250
+ }
@@ -0,0 +1,123 @@
1
+ import type { StackFrame } from './ingest.js'
2
+
3
+ export type IssueStatus = 'open' | 'resolved' | 'ignored'
4
+ export type ActivityKind = 'comment' | 'status' | 'assigned' | 'regressed' | 'snoozed' | 'unsnoozed' | 'merged'
5
+ export type IssueAlertType = 'new_issue' | 'regression'
6
+
7
+ export interface IssueListItem {
8
+ fingerprint: string
9
+ projectId: string
10
+ title: string
11
+ culprit: string | null
12
+ level: string
13
+ status: IssueStatus
14
+ assignedTo: string | null
15
+ regressedAt: number | null
16
+ firstSeen: number
17
+ lastSeen: number
18
+ count: number
19
+ trend: number[]
20
+ }
21
+
22
+ export interface ActivityItem {
23
+ id: string
24
+ kind: ActivityKind
25
+ actorLabel: string | null
26
+ data: Record<string, unknown> | null
27
+ at: number
28
+ }
29
+
30
+ export interface SourceContext {
31
+ lines: string[]
32
+ errorIndex: number
33
+ startLine: number
34
+ }
35
+
36
+ export interface DisplayFrame {
37
+ file: string
38
+ function: string | null
39
+ line: number | null
40
+ column: number | null
41
+ inApp: boolean
42
+ resolved: boolean
43
+ source?: SourceContext
44
+ }
45
+
46
+ export interface EventException {
47
+ type: string | null
48
+ value: string | null
49
+ handled: boolean | null
50
+ frames: DisplayFrame[]
51
+ }
52
+
53
+ export interface EventBreadcrumb {
54
+ timestamp?: number | string
55
+ type?: string
56
+ category?: string
57
+ level?: string
58
+ message?: string
59
+ }
60
+
61
+ export interface EventDetail {
62
+ eventId: string | null
63
+ platform: string | null
64
+ release: string | null
65
+ environment: string | null
66
+ tags: { key: string; value: string }[]
67
+ request: { url: string | null; method: string | null } | null
68
+ user: { id: string | null; email: string | null; username: string | null } | null
69
+ breadcrumbs: EventBreadcrumb[]
70
+ runtime: string | null
71
+ os: string | null
72
+ serverName: string | null
73
+ traceId: string | null
74
+ exceptions: EventException[]
75
+ rawEvent: string
76
+ }
77
+
78
+ export interface Occurrence {
79
+ release?: string
80
+ receivedAt: number
81
+ }
82
+
83
+ export interface PriorIssueState {
84
+ status: IssueStatus
85
+ resolvedInRelease: string | null
86
+ snoozeUntil: number | null
87
+ snoozeUntilCount: number | null
88
+ }
89
+
90
+ export interface OccurrenceTransition {
91
+ reopen: boolean
92
+ regression: boolean
93
+ at: number | null
94
+ release: string | null
95
+ activity: ActivityKind | null
96
+ }
97
+
98
+ export type IssueMutation =
99
+ | { kind: 'status'; status: IssueStatus }
100
+ | { kind: 'comment'; text: string }
101
+ | { kind: 'assign'; principalId: string | null; principalLabel: string | null }
102
+ | { kind: 'snooze_until'; until: number }
103
+ | { kind: 'snooze_count'; additional: number; currentCount: number }
104
+ | { kind: 'resolve_in_release'; release: string | null }
105
+ | { kind: 'merge'; target: string }
106
+
107
+ export interface ActivityDraft {
108
+ kind: ActivityKind
109
+ data: Record<string, unknown>
110
+ }
111
+
112
+ export interface IssueMutationDecision {
113
+ status: IssueStatus
114
+ assignedTo?: string | null
115
+ assignedToLabel?: string | null
116
+ snoozeUntil?: number | null
117
+ snoozeUntilCount?: number | null
118
+ resolvedInRelease?: string | null
119
+ mergedInto?: string
120
+ activity: ActivityDraft | null
121
+ }
122
+
123
+ export type RawEventFrame = StackFrame
@@ -0,0 +1,118 @@
1
+ /** Versioned private Delivery → Operations release and artifact protocol. */
2
+
3
+ import { DEFAULT_OPERATIONS_SERVICE_KEY } from './catalog.js'
4
+
5
+ export const OPERATIONS_RELEASE_PROTOCOL_VERSION = 1
6
+ export const FABRIKA_RELEASE = 'FABRIKA_RELEASE'
7
+ export const OPERATIONS_RELEASE_RECONCILE_PATH = '/private/releases/reconcile'
8
+ export const OPERATIONS_SOURCE_MAP_UPLOAD_PATH = '/api/artifacts/source-maps/'
9
+
10
+ export const OPERATIONS_ARTIFACT_HEADERS = {
11
+ appId: 'X-Fabrika-App-Id',
12
+ environment: 'X-Fabrika-Environment',
13
+ serviceKey: 'X-Fabrika-Service-Key',
14
+ release: 'X-Fabrika-Release',
15
+ runId: 'X-Fabrika-Run-Id',
16
+ logicalPath: 'X-Fabrika-Artifact-Path',
17
+ digest: 'X-Fabrika-Artifact-Sha256',
18
+ }
19
+
20
+ export type OperationsReleasePhase = 'started' | 'provider_accepted' | 'terminal'
21
+ export type OperationsReleaseOutcome = 'succeeded' | 'failed' | null
22
+ export type OperationsArtifactState = 'pending' | 'complete' | 'incomplete' | 'not_applicable'
23
+ export type OperationsReleaseUnavailableReason = 'dry_run' | 'missing_commit'
24
+
25
+ export interface OperationsReleaseCoordinateV1 {
26
+ appId: string
27
+ environment: string
28
+ serviceKey?: string
29
+ }
30
+
31
+ export type OperationsReleaseAvailabilityV1 =
32
+ | {
33
+ kind: 'available'
34
+ name: string
35
+ commitSha: string
36
+ }
37
+ | {
38
+ kind: 'unavailable'
39
+ reason: OperationsReleaseUnavailableReason
40
+ }
41
+
42
+ export interface OperationsArtifactUploadCredentialV1 {
43
+ /** SHA-256 verifier. The bearer itself never crosses the private projection boundary. */
44
+ verifier: string
45
+ expiresAt: number
46
+ }
47
+
48
+ export interface OperationsReleaseReconcileRequestV1 {
49
+ protocolVersion: typeof OPERATIONS_RELEASE_PROTOCOL_VERSION
50
+ revision: number
51
+ runId: string
52
+ coordinate: OperationsReleaseCoordinateV1
53
+ phase: OperationsReleasePhase
54
+ providerRunId: string | null
55
+ outcome: OperationsReleaseOutcome
56
+ artifactState: OperationsArtifactState
57
+ release: OperationsReleaseAvailabilityV1
58
+ uploadCredential?: OperationsArtifactUploadCredentialV1
59
+ observedAt: number
60
+ }
61
+
62
+ export type OperationsReleaseReconcileOutcome = 'applied' | 'unchanged' | 'stale'
63
+
64
+ export interface OperationsReleaseReconcileResponseV1 {
65
+ protocolVersion: typeof OPERATIONS_RELEASE_PROTOCOL_VERSION
66
+ revision: number
67
+ outcome: OperationsReleaseReconcileOutcome
68
+ releaseId: string | null
69
+ }
70
+
71
+ export interface OperationsArtifactUploadConfiguration {
72
+ url: string
73
+ bearer: string
74
+ appId: string
75
+ environment: string
76
+ serviceKey: string
77
+ release: string
78
+ runId: string
79
+ }
80
+
81
+ const COMMIT_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/
82
+
83
+ export function normalizeOperationsCommit(value: string | null | undefined): string | null {
84
+ if (value === null || value === undefined) return null
85
+ const normalized = value.toLowerCase()
86
+ return COMMIT_PATTERN.test(normalized) ? normalized : null
87
+ }
88
+
89
+ export function operationsReleaseName(input: {
90
+ appId: string
91
+ environment: string
92
+ serviceKey?: string
93
+ commitSha: string
94
+ }): string {
95
+ const commitSha = normalizeOperationsCommit(input.commitSha)
96
+ if (commitSha === null) throw new Error('release commit must be a complete immutable git object id')
97
+ return [
98
+ 'fabrika',
99
+ encodeURIComponent(input.appId),
100
+ encodeURIComponent(input.environment),
101
+ encodeURIComponent(input.serviceKey ?? DEFAULT_OPERATIONS_SERVICE_KEY),
102
+ commitSha,
103
+ ].join('/')
104
+ }
105
+
106
+ export function operationsSourceMapUploadUrl(origin: string): string {
107
+ const url = new URL(origin)
108
+ if (
109
+ (url.protocol !== 'https:' && url.protocol !== 'http:')
110
+ || url.origin !== origin
111
+ || url.username !== ''
112
+ || url.password !== ''
113
+ ) {
114
+ throw new Error('Operations artifact origin must be an HTTP origin')
115
+ }
116
+ url.pathname = OPERATIONS_SOURCE_MAP_UPLOAD_PATH
117
+ return url.toString()
118
+ }
package/src/rpc.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { RpcProcedureContract } from '@fabrika/app'
2
+ import type {
3
+ OperationsAlertKind,
4
+ OperationsAlertRuleRequestDto,
5
+ OperationsAlertSettingsResponseDto,
6
+ OperationsAssigneeListResponseDto,
7
+ OperationsBulkIssueStatusRequestDto,
8
+ OperationsBulkIssueStatusResponseDto,
9
+ OperationsEventDetailResponseDto,
10
+ OperationsHealthCheckUpsertRequestDto,
11
+ OperationsHealthResponseDto,
12
+ OperationsIssueDetailResponseDto,
13
+ OperationsIssueListResponseDto,
14
+ OperationsIssueMutationRequestDto,
15
+ OperationsIssueOccurrenceListResponseDto,
16
+ OperationsNotificationChannelRequestDto,
17
+ OperationsReleaseDetailResponseDto,
18
+ OperationsReleaseListResponseDto,
19
+ OperationsSourceDetailResponseDto,
20
+ OperationsSourceHealthDto,
21
+ OperationsSourceListResponseDto,
22
+ OperationsSpikeAlertRequestDto,
23
+ } from './operator-api.js'
24
+
25
+ export interface OperationsIssueQuery {
26
+ sourceId?: string
27
+ status?: 'open' | 'resolved' | 'ignored'
28
+ level?: 'fatal' | 'error' | 'warning' | 'info'
29
+ window?: 'all' | '24h' | '7d' | '30d'
30
+ query?: string
31
+ assignee?: 'all' | 'me' | 'none'
32
+ sort?: 'recent' | 'new' | 'frequency'
33
+ cursor?: string
34
+ limit?: number
35
+ }
36
+
37
+ export interface OperationsReleaseQuery {
38
+ sourceId?: string
39
+ cursor?: string
40
+ limit?: number
41
+ }
42
+
43
+ /** Portable operator API implemented by Operations and consumed by its console. */
44
+ export interface OperationsRpcContract {
45
+ sources: RpcProcedureContract<void, OperationsSourceListResponseDto>
46
+ source: RpcProcedureContract<{ sourceId: string }, OperationsSourceDetailResponseDto>
47
+ issues: RpcProcedureContract<OperationsIssueQuery, OperationsIssueListResponseDto>
48
+ issue: RpcProcedureContract<{ issueId: string }, OperationsIssueDetailResponseDto>
49
+ issueOccurrences: RpcProcedureContract<{ issueId: string; cursor?: string; limit?: number }, OperationsIssueOccurrenceListResponseDto>
50
+ latestEvent: RpcProcedureContract<{ issueId: string }, OperationsEventDetailResponseDto>
51
+ event: RpcProcedureContract<{ issueId: string; occurrenceId: string }, OperationsEventDetailResponseDto>
52
+ mutateIssue: RpcProcedureContract<{ issueId: string; mutation: OperationsIssueMutationRequestDto }, OperationsIssueDetailResponseDto>
53
+ bulkIssueStatus: RpcProcedureContract<OperationsBulkIssueStatusRequestDto, OperationsBulkIssueStatusResponseDto>
54
+ assignees: RpcProcedureContract<{ sourceId: string }, OperationsAssigneeListResponseDto>
55
+ releases: RpcProcedureContract<OperationsReleaseQuery, OperationsReleaseListResponseDto>
56
+ release: RpcProcedureContract<{ releaseId: string }, OperationsReleaseDetailResponseDto>
57
+ health: RpcProcedureContract<void, OperationsHealthResponseDto>
58
+ sourceHealth: RpcProcedureContract<{ sourceId: string }, OperationsSourceHealthDto>
59
+ createHealthCheck: RpcProcedureContract<
60
+ { sourceId: string; input: OperationsHealthCheckUpsertRequestDto },
61
+ { id: string }
62
+ >
63
+ updateHealthCheck: RpcProcedureContract<
64
+ { sourceId: string; checkId: string; input: OperationsHealthCheckUpsertRequestDto },
65
+ { id: string }
66
+ >
67
+ deleteHealthCheck: RpcProcedureContract<{ sourceId: string; checkId: string }, null>
68
+ alerts: RpcProcedureContract<{ sourceId: string }, OperationsAlertSettingsResponseDto>
69
+ updateSpikeAlert: RpcProcedureContract<
70
+ { sourceId: string; input: OperationsSpikeAlertRequestDto },
71
+ { threshold: number; enabled: boolean }
72
+ >
73
+ updateAlertRule: RpcProcedureContract<
74
+ { sourceId: string; kind: OperationsAlertKind; input: OperationsAlertRuleRequestDto },
75
+ { kind: OperationsAlertKind; enabled: boolean }
76
+ >
77
+ createAlertChannel: RpcProcedureContract<
78
+ { sourceId: string; input: OperationsNotificationChannelRequestDto },
79
+ { id: string }
80
+ >
81
+ updateAlertChannel: RpcProcedureContract<
82
+ { sourceId: string; channelId: string; input: OperationsNotificationChannelRequestDto },
83
+ { id: string }
84
+ >
85
+ deleteAlertChannel: RpcProcedureContract<{ sourceId: string; channelId: string }, null>
86
+ }