rebase-git 0.0.2-nightly.20260822.35
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/LICENSE +201 -0
- package/README.md +63 -0
- package/dist/cli.js +14 -0
- package/dist/migrations/20260820194541_create_environment/migration.sql +5 -0
- package/dist/migrations/20260820194541_create_environment/snapshot.json +57 -0
- package/dist/migrations/20260820194552_create_activity/migration.sql +33 -0
- package/dist/migrations/20260820194552_create_activity/snapshot.json +231 -0
- package/dist/migrations/20260821164241_stale_the_fallen/migration.sql +23 -0
- package/dist/migrations/20260821164241_stale_the_fallen/snapshot.json +279 -0
- package/dist/runtime.js +2555 -0
- package/dist/web/assets/index-B0o5MB9r.js +21 -0
- package/dist/web/assets/index-BuR4V6ji.css +2 -0
- package/dist/web/favicon.svg +9 -0
- package/dist/web/index.html +15 -0
- package/package.json +75 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,2555 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/apps/server/cli.ts
|
|
4
|
+
import { existsSync as existsSync2, realpathSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
|
+
|
|
7
|
+
// src/contracts/environment-connection/environment-transport-limits.contract.ts
|
|
8
|
+
import { Schema } from "effect";
|
|
9
|
+
var ByteLimit = Schema.Int.check(
|
|
10
|
+
Schema.isBetween({ minimum: 1024, maximum: 67108864 })
|
|
11
|
+
), QueueLength = Schema.Int.check(
|
|
12
|
+
Schema.isBetween({ minimum: 1, maximum: 4096 })
|
|
13
|
+
), TimeoutMilliseconds = Schema.Int.check(
|
|
14
|
+
Schema.isBetween({ minimum: 100, maximum: 6e4 })
|
|
15
|
+
), TransportLimits = Schema.Struct({
|
|
16
|
+
maxHttpRequestBytes: ByteLimit,
|
|
17
|
+
maxHttpResponseBytes: ByteLimit,
|
|
18
|
+
maxWebSocketRequestBytes: ByteLimit,
|
|
19
|
+
maxWebSocketResponseBytes: ByteLimit,
|
|
20
|
+
maxQueuedEvents: QueueLength,
|
|
21
|
+
maxQueuedEventBytes: ByteLimit,
|
|
22
|
+
maxCapturedOutputBytes: ByteLimit,
|
|
23
|
+
helloTimeoutMilliseconds: TimeoutMilliseconds
|
|
24
|
+
}), ClientReceiveLimits = Schema.Struct({
|
|
25
|
+
maxCapturedOutputBytes: ByteLimit,
|
|
26
|
+
maxHttpResponseBytes: ByteLimit,
|
|
27
|
+
maxWebSocketResponseBytes: ByteLimit
|
|
28
|
+
}), currentTransportLimits = {
|
|
29
|
+
helloTimeoutMilliseconds: 5e3,
|
|
30
|
+
maxCapturedOutputBytes: 1048576,
|
|
31
|
+
maxHttpRequestBytes: 65536,
|
|
32
|
+
maxHttpResponseBytes: 65536,
|
|
33
|
+
maxQueuedEventBytes: 262144,
|
|
34
|
+
maxQueuedEvents: 128,
|
|
35
|
+
maxWebSocketRequestBytes: 16384,
|
|
36
|
+
maxWebSocketResponseBytes: 16384
|
|
37
|
+
}, currentClientReceiveLimits = {
|
|
38
|
+
maxCapturedOutputBytes: currentTransportLimits.maxCapturedOutputBytes,
|
|
39
|
+
maxHttpResponseBytes: currentTransportLimits.maxHttpResponseBytes,
|
|
40
|
+
maxWebSocketResponseBytes: currentTransportLimits.maxWebSocketResponseBytes
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/contracts/environment-connection/negotiation/environment-protocol.contract.ts
|
|
44
|
+
import { Schema as Schema2 } from "effect";
|
|
45
|
+
var ProtocolNumber = Schema2.Natural.check(Schema2.isLessThanOrEqualTo(65535)), CapabilityName = Schema2.String.check(
|
|
46
|
+
Schema2.isMinLength(1),
|
|
47
|
+
Schema2.isMaxLength(64)
|
|
48
|
+
), ProductVersion = Schema2.String.check(
|
|
49
|
+
Schema2.isMinLength(1),
|
|
50
|
+
Schema2.isMaxLength(64)
|
|
51
|
+
), ProtocolRange = Schema2.Struct({
|
|
52
|
+
major: ProtocolNumber,
|
|
53
|
+
minor: ProtocolNumber,
|
|
54
|
+
minimumSupportedMinor: ProtocolNumber
|
|
55
|
+
}).check(
|
|
56
|
+
Schema2.makeFilter(
|
|
57
|
+
(range) => range.minimumSupportedMinor <= range.minor || "minimumSupportedMinor must not exceed minor"
|
|
58
|
+
)
|
|
59
|
+
), EnvironmentCapability = Schema2.Struct({
|
|
60
|
+
name: CapabilityName,
|
|
61
|
+
version: Schema2.Int.check(
|
|
62
|
+
Schema2.isGreaterThan(0),
|
|
63
|
+
Schema2.isLessThanOrEqualTo(65535)
|
|
64
|
+
),
|
|
65
|
+
introducedInMinor: ProtocolNumber
|
|
66
|
+
}), EnvironmentCapabilities = Schema2.Array(
|
|
67
|
+
EnvironmentCapability
|
|
68
|
+
).check(
|
|
69
|
+
Schema2.isMaxLength(32),
|
|
70
|
+
Schema2.makeFilter((capabilities) => new Set(capabilities.map((capability) => capability.name)).size === capabilities.length || "capability names must be unique")
|
|
71
|
+
), ProductVersionSchema = ProductVersion, currentEnvironmentProtocol = {
|
|
72
|
+
major: 1,
|
|
73
|
+
minor: 2,
|
|
74
|
+
minimumSupportedMinor: 0
|
|
75
|
+
}, currentEnvironmentCapabilities = [
|
|
76
|
+
{
|
|
77
|
+
introducedInMinor: 0,
|
|
78
|
+
name: "environment-events",
|
|
79
|
+
version: 1
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
introducedInMinor: 1,
|
|
83
|
+
name: "sequence-resnapshot",
|
|
84
|
+
version: 1
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// src/contracts/environment-connection/websocket/environment-live-connection.contract.ts
|
|
89
|
+
import { Schema as Schema3 } from "effect";
|
|
90
|
+
var EnvironmentHello = Schema3.TaggedStruct("Hello", {
|
|
91
|
+
productVersion: ProductVersionSchema,
|
|
92
|
+
protocol: ProtocolRange,
|
|
93
|
+
capabilities: EnvironmentCapabilities,
|
|
94
|
+
receiveLimits: ClientReceiveLimits,
|
|
95
|
+
lastObservedSequence: Schema3.optionalKey(Schema3.Natural)
|
|
96
|
+
}), SnapshotApplied = Schema3.TaggedStruct("SnapshotApplied", {
|
|
97
|
+
sequence: Schema3.Natural
|
|
98
|
+
}), EnvironmentClientMessage = Schema3.Union([
|
|
99
|
+
EnvironmentHello,
|
|
100
|
+
SnapshotApplied
|
|
101
|
+
]), ProtocolMajorMismatch = Schema3.TaggedStruct("ProtocolMajorMismatch", {
|
|
102
|
+
clientMajor: Schema3.Natural,
|
|
103
|
+
serverMajor: Schema3.Natural,
|
|
104
|
+
requiredUpdate: Schema3.Literals(["client", "server"])
|
|
105
|
+
}), ProtocolMinorMismatch = Schema3.TaggedStruct("ProtocolMinorMismatch", {
|
|
106
|
+
clientMinor: Schema3.Natural,
|
|
107
|
+
clientMinimumSupportedMinor: Schema3.Natural,
|
|
108
|
+
serverMinor: Schema3.Natural,
|
|
109
|
+
serverMinimumSupportedMinor: Schema3.Natural
|
|
110
|
+
}), InvalidMessage = Schema3.TaggedStruct("InvalidMessage", {}), PayloadTooLarge = Schema3.TaggedStruct("PayloadTooLarge", {
|
|
111
|
+
limitBytes: Schema3.Natural
|
|
112
|
+
}), HandshakeRequired = Schema3.TaggedStruct("HandshakeRequired", {}), HandshakeAlreadyCompleted = Schema3.TaggedStruct(
|
|
113
|
+
"HandshakeAlreadyCompleted",
|
|
114
|
+
{}
|
|
115
|
+
), EnvironmentTransportFailure = Schema3.Union([
|
|
116
|
+
ProtocolMajorMismatch,
|
|
117
|
+
ProtocolMinorMismatch,
|
|
118
|
+
InvalidMessage,
|
|
119
|
+
PayloadTooLarge,
|
|
120
|
+
HandshakeRequired,
|
|
121
|
+
HandshakeAlreadyCompleted
|
|
122
|
+
]), HelloAccepted = Schema3.TaggedStruct("HelloAccepted", {
|
|
123
|
+
environmentId: Schema3.String.check(Schema3.isUUID(4)),
|
|
124
|
+
protocol: Schema3.Struct({
|
|
125
|
+
major: Schema3.Natural,
|
|
126
|
+
minor: Schema3.Natural
|
|
127
|
+
}),
|
|
128
|
+
capabilities: EnvironmentCapabilities,
|
|
129
|
+
limits: TransportLimits,
|
|
130
|
+
currentSequence: Schema3.Natural
|
|
131
|
+
}), HelloRejected = Schema3.TaggedStruct("HelloRejected", {
|
|
132
|
+
failure: EnvironmentTransportFailure
|
|
133
|
+
}), EnvironmentChanged = Schema3.TaggedStruct("EnvironmentChanged", {
|
|
134
|
+
sequence: Schema3.Natural
|
|
135
|
+
}), ResnapshotRequired = Schema3.TaggedStruct("ResnapshotRequired", {
|
|
136
|
+
currentSequence: Schema3.Natural,
|
|
137
|
+
reason: Schema3.Literals(["SequenceGap", "OutgoingQueueOverflow"])
|
|
138
|
+
}), EnvironmentHelloResult = Schema3.Union([
|
|
139
|
+
HelloAccepted,
|
|
140
|
+
HelloRejected
|
|
141
|
+
]), EnvironmentServerMessage = Schema3.Union([
|
|
142
|
+
HelloAccepted,
|
|
143
|
+
HelloRejected,
|
|
144
|
+
EnvironmentChanged,
|
|
145
|
+
ResnapshotRequired
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// src/contracts/environment-authorization/environment-authorization.contract.ts
|
|
149
|
+
import { Schema as Schema4 } from "effect";
|
|
150
|
+
var environmentAuthorizationRoles = [
|
|
151
|
+
"viewer",
|
|
152
|
+
"contributor",
|
|
153
|
+
"maintainer",
|
|
154
|
+
"owner",
|
|
155
|
+
"custom"
|
|
156
|
+
], environmentAccessCapabilities = [
|
|
157
|
+
"environment.read",
|
|
158
|
+
"repository.read",
|
|
159
|
+
"repository.write",
|
|
160
|
+
"history.rewrite",
|
|
161
|
+
"worktree.manage",
|
|
162
|
+
"authorization.manage",
|
|
163
|
+
"environment.manage"
|
|
164
|
+
], EnvironmentAuthorizationRole = Schema4.Literals(
|
|
165
|
+
environmentAuthorizationRoles
|
|
166
|
+
), EnvironmentAccessCapability = Schema4.Literals(
|
|
167
|
+
environmentAccessCapabilities
|
|
168
|
+
), AuthorizationId = Schema4.String.check(Schema4.isUUID(4)), SecretMaterial = Schema4.String.check(
|
|
169
|
+
Schema4.isMinLength(32),
|
|
170
|
+
Schema4.isMaxLength(512)
|
|
171
|
+
), PairingCode = Schema4.String.check(Schema4.isPattern(/^\d{3}-\d{3}$/)), DeviceLabel = Schema4.String.check(
|
|
172
|
+
Schema4.isMinLength(1),
|
|
173
|
+
Schema4.isMaxLength(128)
|
|
174
|
+
), IsoDate = Schema4.String, CreateEnvironmentPairing = Schema4.Struct({
|
|
175
|
+
role: EnvironmentAuthorizationRole,
|
|
176
|
+
capabilities: Schema4.Array(EnvironmentAccessCapability).check(
|
|
177
|
+
Schema4.isMaxLength(environmentAccessCapabilities.length)
|
|
178
|
+
)
|
|
179
|
+
}), EnvironmentPairingCreated = Schema4.Struct({
|
|
180
|
+
pairingUrl: Schema4.String,
|
|
181
|
+
expiresAt: IsoDate
|
|
182
|
+
}), ExchangeEnvironmentPairing = Schema4.Struct({
|
|
183
|
+
pairingMaterial: PairingCode,
|
|
184
|
+
label: DeviceLabel
|
|
185
|
+
}), EnvironmentDeviceAuthorization = Schema4.Struct({
|
|
186
|
+
id: AuthorizationId,
|
|
187
|
+
label: DeviceLabel,
|
|
188
|
+
role: EnvironmentAuthorizationRole,
|
|
189
|
+
capabilities: Schema4.Array(EnvironmentAccessCapability)
|
|
190
|
+
}), EnvironmentPairingExchanged = Schema4.Struct({
|
|
191
|
+
authorization: EnvironmentDeviceAuthorization,
|
|
192
|
+
credential: SecretMaterial
|
|
193
|
+
}), EnvironmentWebSocketTicket = Schema4.Struct({
|
|
194
|
+
expiresAt: IsoDate,
|
|
195
|
+
ticket: SecretMaterial
|
|
196
|
+
}), RevokeEnvironmentAuthorization = Schema4.Struct({
|
|
197
|
+
authorizationId: AuthorizationId
|
|
198
|
+
}), EnvironmentAuthorizationRevoked = Schema4.Struct({
|
|
199
|
+
authorizationId: AuthorizationId,
|
|
200
|
+
revokedAt: IsoDate
|
|
201
|
+
}), InvalidHost = Schema4.TaggedStruct("InvalidHost", {}), InvalidOrigin = Schema4.TaggedStruct("InvalidOrigin", {}), InvalidGrant = Schema4.TaggedStruct("InvalidGrant", {}), ExpiredGrant = Schema4.TaggedStruct("ExpiredGrant", {}), RevokedGrant = Schema4.TaggedStruct("RevokedGrant", {}), CapabilityDenied = Schema4.TaggedStruct("CapabilityDenied", {
|
|
202
|
+
capability: EnvironmentAccessCapability
|
|
203
|
+
}), InvalidPairing = Schema4.TaggedStruct("InvalidPairing", {}), ExpiredPairing = Schema4.TaggedStruct("ExpiredPairing", {}), PairingAlreadyUsed = Schema4.TaggedStruct("PairingAlreadyUsed", {}), InvalidTicket = Schema4.TaggedStruct("InvalidTicket", {}), ExpiredTicket = Schema4.TaggedStruct("ExpiredTicket", {}), TicketAlreadyUsed = Schema4.TaggedStruct("TicketAlreadyUsed", {}), EnvironmentAuthorizationFailure = Schema4.Union([
|
|
204
|
+
InvalidHost,
|
|
205
|
+
InvalidOrigin,
|
|
206
|
+
InvalidGrant,
|
|
207
|
+
ExpiredGrant,
|
|
208
|
+
RevokedGrant,
|
|
209
|
+
CapabilityDenied,
|
|
210
|
+
InvalidPairing,
|
|
211
|
+
ExpiredPairing,
|
|
212
|
+
PairingAlreadyUsed,
|
|
213
|
+
InvalidTicket,
|
|
214
|
+
ExpiredTicket,
|
|
215
|
+
TicketAlreadyUsed
|
|
216
|
+
]), EnvironmentGrantHttpFailure = Schema4.Union([
|
|
217
|
+
InvalidHost,
|
|
218
|
+
InvalidOrigin,
|
|
219
|
+
InvalidGrant,
|
|
220
|
+
ExpiredGrant,
|
|
221
|
+
RevokedGrant,
|
|
222
|
+
CapabilityDenied,
|
|
223
|
+
InvalidMessage,
|
|
224
|
+
PayloadTooLarge
|
|
225
|
+
]), EnvironmentPairingExchangeHttpFailure = Schema4.Union([
|
|
226
|
+
InvalidHost,
|
|
227
|
+
InvalidOrigin,
|
|
228
|
+
InvalidPairing,
|
|
229
|
+
ExpiredPairing,
|
|
230
|
+
PairingAlreadyUsed,
|
|
231
|
+
InvalidMessage,
|
|
232
|
+
PayloadTooLarge
|
|
233
|
+
]), EnvironmentAuthorizationHttpFailure = Schema4.Union([
|
|
234
|
+
EnvironmentGrantHttpFailure,
|
|
235
|
+
EnvironmentPairingExchangeHttpFailure
|
|
236
|
+
]), environmentPairingExchangePath = "/api/authorization/pairings/exchange", environmentPairingsPath = "/api/authorization/pairings", environmentWebSocketTicketsPath = "/api/authorization/websocket-tickets", environmentAuthorizationRevocationPath = "/api/authorization/revocations", EnvironmentAuthorizationHttpApi = {
|
|
237
|
+
createPairing: {
|
|
238
|
+
failure: EnvironmentGrantHttpFailure,
|
|
239
|
+
failureStatuses: [400, 401, 403, 410, 413],
|
|
240
|
+
method: "POST",
|
|
241
|
+
path: environmentPairingsPath,
|
|
242
|
+
request: CreateEnvironmentPairing,
|
|
243
|
+
success: EnvironmentPairingCreated,
|
|
244
|
+
successStatus: 201
|
|
245
|
+
},
|
|
246
|
+
exchangePairing: {
|
|
247
|
+
failure: EnvironmentPairingExchangeHttpFailure,
|
|
248
|
+
failureStatuses: [400, 401, 403, 409, 410, 413],
|
|
249
|
+
method: "POST",
|
|
250
|
+
path: environmentPairingExchangePath,
|
|
251
|
+
request: ExchangeEnvironmentPairing,
|
|
252
|
+
success: EnvironmentPairingExchanged,
|
|
253
|
+
successStatus: 201
|
|
254
|
+
},
|
|
255
|
+
mintWebSocketTicket: {
|
|
256
|
+
failure: EnvironmentGrantHttpFailure,
|
|
257
|
+
failureStatuses: [400, 401, 403, 410, 413],
|
|
258
|
+
method: "POST",
|
|
259
|
+
path: environmentWebSocketTicketsPath,
|
|
260
|
+
success: EnvironmentWebSocketTicket,
|
|
261
|
+
successStatus: 201
|
|
262
|
+
},
|
|
263
|
+
revokeAuthorization: {
|
|
264
|
+
failure: EnvironmentGrantHttpFailure,
|
|
265
|
+
failureStatuses: [400, 401, 403, 410, 413],
|
|
266
|
+
method: "POST",
|
|
267
|
+
path: environmentAuthorizationRevocationPath,
|
|
268
|
+
request: RevokeEnvironmentAuthorization,
|
|
269
|
+
success: EnvironmentAuthorizationRevoked,
|
|
270
|
+
successStatus: 200
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/contracts/environment-connection/http/environment-discovery.contract.ts
|
|
275
|
+
import { Schema as Schema5 } from "effect";
|
|
276
|
+
var environmentDiscoveryPath = "/api/discovery", environmentSnapshotPath = "/api/environment/snapshot", environmentLivePath = "/api/environment/live", EnvironmentDiscovery = Schema5.Struct({
|
|
277
|
+
environmentId: Schema5.String.check(Schema5.isUUID(4)),
|
|
278
|
+
productVersion: ProductVersionSchema,
|
|
279
|
+
protocol: ProtocolRange,
|
|
280
|
+
capabilities: EnvironmentCapabilities,
|
|
281
|
+
limits: TransportLimits,
|
|
282
|
+
routes: Schema5.Struct({
|
|
283
|
+
snapshot: Schema5.Literal(environmentSnapshotPath),
|
|
284
|
+
live: Schema5.Literal(environmentLivePath)
|
|
285
|
+
})
|
|
286
|
+
}), EnvironmentSnapshot = Schema5.Struct({
|
|
287
|
+
environmentId: Schema5.String.check(Schema5.isUUID(4)),
|
|
288
|
+
sequence: Schema5.Natural
|
|
289
|
+
}), EnvironmentHttpFailure = Schema5.Union([
|
|
290
|
+
InvalidMessage,
|
|
291
|
+
PayloadTooLarge
|
|
292
|
+
]), EnvironmentDiscoveryHttpFailure = Schema5.Union([
|
|
293
|
+
EnvironmentHttpFailure,
|
|
294
|
+
InvalidHost
|
|
295
|
+
]), EnvironmentHttpApi = {
|
|
296
|
+
discovery: {
|
|
297
|
+
failure: EnvironmentDiscoveryHttpFailure,
|
|
298
|
+
failureStatuses: [400, 403, 413],
|
|
299
|
+
method: "GET",
|
|
300
|
+
path: environmentDiscoveryPath,
|
|
301
|
+
success: EnvironmentDiscovery,
|
|
302
|
+
successStatus: 200
|
|
303
|
+
},
|
|
304
|
+
snapshot: {
|
|
305
|
+
failure: EnvironmentGrantHttpFailure,
|
|
306
|
+
failureStatuses: [400, 401, 403, 410, 413],
|
|
307
|
+
method: "GET",
|
|
308
|
+
path: environmentSnapshotPath,
|
|
309
|
+
success: EnvironmentSnapshot,
|
|
310
|
+
successStatus: 200
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
function createCurrentEnvironmentDiscovery(environmentId, productVersion2) {
|
|
314
|
+
return {
|
|
315
|
+
capabilities: currentEnvironmentCapabilities,
|
|
316
|
+
environmentId,
|
|
317
|
+
limits: currentTransportLimits,
|
|
318
|
+
productVersion: productVersion2,
|
|
319
|
+
protocol: currentEnvironmentProtocol,
|
|
320
|
+
routes: {
|
|
321
|
+
live: environmentLivePath,
|
|
322
|
+
snapshot: environmentSnapshotPath
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/contracts/environment-connection/negotiation/negotiate-environment-protocol.ts
|
|
328
|
+
function negotiateEnvironmentHello(discovery, hello, currentSequence) {
|
|
329
|
+
if (hello.protocol.major !== discovery.protocol.major)
|
|
330
|
+
return {
|
|
331
|
+
_tag: "HelloRejected",
|
|
332
|
+
failure: {
|
|
333
|
+
_tag: "ProtocolMajorMismatch",
|
|
334
|
+
clientMajor: hello.protocol.major,
|
|
335
|
+
requiredUpdate: hello.protocol.major < discovery.protocol.major ? "client" : "server",
|
|
336
|
+
serverMajor: discovery.protocol.major
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
let negotiatedMinor = Math.min(
|
|
340
|
+
hello.protocol.minor,
|
|
341
|
+
discovery.protocol.minor
|
|
342
|
+
), minimumCompatibleMinor = Math.max(
|
|
343
|
+
hello.protocol.minimumSupportedMinor,
|
|
344
|
+
discovery.protocol.minimumSupportedMinor
|
|
345
|
+
);
|
|
346
|
+
return negotiatedMinor < minimumCompatibleMinor ? {
|
|
347
|
+
_tag: "HelloRejected",
|
|
348
|
+
failure: {
|
|
349
|
+
_tag: "ProtocolMinorMismatch",
|
|
350
|
+
clientMinor: hello.protocol.minor,
|
|
351
|
+
clientMinimumSupportedMinor: hello.protocol.minimumSupportedMinor,
|
|
352
|
+
serverMinor: discovery.protocol.minor,
|
|
353
|
+
serverMinimumSupportedMinor: discovery.protocol.minimumSupportedMinor
|
|
354
|
+
}
|
|
355
|
+
} : {
|
|
356
|
+
_tag: "HelloAccepted",
|
|
357
|
+
capabilities: negotiateCapabilities(
|
|
358
|
+
discovery.capabilities,
|
|
359
|
+
hello.capabilities,
|
|
360
|
+
negotiatedMinor
|
|
361
|
+
),
|
|
362
|
+
currentSequence,
|
|
363
|
+
environmentId: discovery.environmentId,
|
|
364
|
+
limits: negotiatedLimits(discovery.limits, hello.receiveLimits),
|
|
365
|
+
protocol: {
|
|
366
|
+
major: discovery.protocol.major,
|
|
367
|
+
minor: negotiatedMinor
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function negotiateCapabilities(serverCapabilities, clientCapabilities, negotiatedMinor) {
|
|
372
|
+
let clientByName = new Map(
|
|
373
|
+
clientCapabilities.map((capability) => [capability.name, capability])
|
|
374
|
+
);
|
|
375
|
+
return serverCapabilities.flatMap((serverCapability) => {
|
|
376
|
+
let clientCapability = clientByName.get(serverCapability.name);
|
|
377
|
+
return clientCapability === void 0 || serverCapability.introducedInMinor > negotiatedMinor || clientCapability.introducedInMinor > negotiatedMinor ? [] : [
|
|
378
|
+
{
|
|
379
|
+
introducedInMinor: Math.max(
|
|
380
|
+
serverCapability.introducedInMinor,
|
|
381
|
+
clientCapability.introducedInMinor
|
|
382
|
+
),
|
|
383
|
+
name: serverCapability.name,
|
|
384
|
+
version: Math.min(serverCapability.version, clientCapability.version)
|
|
385
|
+
}
|
|
386
|
+
];
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function negotiatedLimits(serverLimits, clientLimits) {
|
|
390
|
+
return {
|
|
391
|
+
helloTimeoutMilliseconds: serverLimits.helloTimeoutMilliseconds,
|
|
392
|
+
maxCapturedOutputBytes: Math.min(
|
|
393
|
+
serverLimits.maxCapturedOutputBytes,
|
|
394
|
+
clientLimits.maxCapturedOutputBytes
|
|
395
|
+
),
|
|
396
|
+
maxHttpRequestBytes: serverLimits.maxHttpRequestBytes,
|
|
397
|
+
maxHttpResponseBytes: Math.min(
|
|
398
|
+
serverLimits.maxHttpResponseBytes,
|
|
399
|
+
clientLimits.maxHttpResponseBytes
|
|
400
|
+
),
|
|
401
|
+
maxQueuedEventBytes: serverLimits.maxQueuedEventBytes,
|
|
402
|
+
maxQueuedEvents: serverLimits.maxQueuedEvents,
|
|
403
|
+
maxWebSocketRequestBytes: serverLimits.maxWebSocketRequestBytes,
|
|
404
|
+
maxWebSocketResponseBytes: Math.min(
|
|
405
|
+
serverLimits.maxWebSocketResponseBytes,
|
|
406
|
+
clientLimits.maxWebSocketResponseBytes
|
|
407
|
+
)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/apps/server/cli.ts
|
|
412
|
+
import { Deferred, Effect as Effect20 } from "effect";
|
|
413
|
+
|
|
414
|
+
// src/apps/server/features/browser-client/default-browser.ts
|
|
415
|
+
import { spawn } from "node:child_process";
|
|
416
|
+
import { readFileSync } from "node:fs";
|
|
417
|
+
import { release } from "node:os";
|
|
418
|
+
function openDefaultBrowser(url) {
|
|
419
|
+
if (process.env.BROWSER !== "none")
|
|
420
|
+
try {
|
|
421
|
+
let invocation = browserInvocation(url), child = spawn(invocation.command, invocation.arguments, {
|
|
422
|
+
detached: !0,
|
|
423
|
+
stdio: "ignore",
|
|
424
|
+
windowsHide: !0
|
|
425
|
+
});
|
|
426
|
+
child.once("error", () => {
|
|
427
|
+
}), child.unref();
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function browserInvocation(url) {
|
|
432
|
+
return process.platform === "darwin" ? { arguments: [url], command: "open" } : process.platform === "win32" || isWindowsSubsystemForLinux() ? {
|
|
433
|
+
arguments: ["/d", "/s", "/c", "start", "", url],
|
|
434
|
+
command: "cmd.exe"
|
|
435
|
+
} : { arguments: [url], command: "xdg-open" };
|
|
436
|
+
}
|
|
437
|
+
function isWindowsSubsystemForLinux() {
|
|
438
|
+
if (process.env.WSL_DISTRO_NAME !== void 0) return !0;
|
|
439
|
+
try {
|
|
440
|
+
return readFileSync("/proc/sys/kernel/osrelease", "utf8").toLowerCase().includes("microsoft");
|
|
441
|
+
} catch {
|
|
442
|
+
return release().toLowerCase().includes("microsoft");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/apps/server/features/environment-server/server/start-environment-server.ts
|
|
447
|
+
import { and } from "drizzle-orm";
|
|
448
|
+
import { Effect as Effect19 } from "effect";
|
|
449
|
+
|
|
450
|
+
// src/apps/server/features/environment-authorization/environment-authorization.ts
|
|
451
|
+
import { randomUUID } from "node:crypto";
|
|
452
|
+
import { eq } from "drizzle-orm";
|
|
453
|
+
import { Effect } from "effect";
|
|
454
|
+
|
|
455
|
+
// src/apps/server/features/environment-authorization/environment-authorization.contract.ts
|
|
456
|
+
import { Data } from "effect";
|
|
457
|
+
var EnvironmentAuthorizationError = class extends Data.TaggedError(
|
|
458
|
+
"EnvironmentAuthorizationError"
|
|
459
|
+
) {
|
|
460
|
+
};
|
|
461
|
+
function isEnvironmentAuthorizationError(value) {
|
|
462
|
+
return value instanceof EnvironmentAuthorizationError;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/apps/server/features/environment-authorization/environment-authorization-secret.ts
|
|
466
|
+
import {
|
|
467
|
+
createHash,
|
|
468
|
+
createHmac,
|
|
469
|
+
randomBytes,
|
|
470
|
+
randomInt,
|
|
471
|
+
timingSafeEqual
|
|
472
|
+
} from "node:crypto";
|
|
473
|
+
var credentialPrefix = "rebase.v1";
|
|
474
|
+
function createDeviceCredential(serverSecret, authorizationId) {
|
|
475
|
+
let secret = createSecretMaterial(), unsigned = `${credentialPrefix}.${authorizationId}.${secret}`, signature = signCredential(serverSecret, unsigned);
|
|
476
|
+
return `${unsigned}.${signature}`;
|
|
477
|
+
}
|
|
478
|
+
function verifyDeviceCredential(serverSecret, credential) {
|
|
479
|
+
if (credential === void 0)
|
|
480
|
+
return;
|
|
481
|
+
let [product, version, authorizationId, secret, signature, excess] = credential.split(".");
|
|
482
|
+
if (product !== "rebase" || version !== "v1" || authorizationId === void 0 || !/^[0-9a-f-]{36}$/.test(authorizationId) || secret === void 0 || !/^[A-Za-z0-9_-]{43}$/.test(secret) || signature === void 0 || !/^[A-Za-z0-9_-]{43}$/.test(signature) || excess !== void 0)
|
|
483
|
+
return;
|
|
484
|
+
let unsigned = `${credentialPrefix}.${authorizationId}.${secret}`, expected = Buffer.from(signCredential(serverSecret, unsigned)), received = Buffer.from(signature);
|
|
485
|
+
return expected.byteLength === received.byteLength && timingSafeEqual(expected, received) ? authorizationId : void 0;
|
|
486
|
+
}
|
|
487
|
+
function createSecretMaterial() {
|
|
488
|
+
return randomBytes(32).toString("base64url");
|
|
489
|
+
}
|
|
490
|
+
function createPairingCode() {
|
|
491
|
+
let digits = randomInt(1e6).toString().padStart(6, "0");
|
|
492
|
+
return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
|
493
|
+
}
|
|
494
|
+
function digestSecretMaterial(material) {
|
|
495
|
+
return createHash("sha256").update(material).digest("base64url");
|
|
496
|
+
}
|
|
497
|
+
function signCredential(serverSecret, unsigned) {
|
|
498
|
+
return createHmac("sha256", serverSecret).update(unsigned).digest("base64url");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/apps/server/features/environment-authorization/environment-capabilities.ts
|
|
502
|
+
var viewerCapabilities = [
|
|
503
|
+
"environment.read",
|
|
504
|
+
"repository.read"
|
|
505
|
+
], contributorCapabilities = [
|
|
506
|
+
...viewerCapabilities,
|
|
507
|
+
"repository.write"
|
|
508
|
+
], maintainerCapabilities = [
|
|
509
|
+
...contributorCapabilities,
|
|
510
|
+
"history.rewrite",
|
|
511
|
+
"worktree.manage"
|
|
512
|
+
], ownerCapabilities = [
|
|
513
|
+
...maintainerCapabilities,
|
|
514
|
+
"authorization.manage",
|
|
515
|
+
"environment.manage"
|
|
516
|
+
];
|
|
517
|
+
function capabilitiesForRole(role, customCapabilities) {
|
|
518
|
+
switch (role) {
|
|
519
|
+
case "viewer":
|
|
520
|
+
return viewerCapabilities;
|
|
521
|
+
case "contributor":
|
|
522
|
+
return contributorCapabilities;
|
|
523
|
+
case "maintainer":
|
|
524
|
+
return maintainerCapabilities;
|
|
525
|
+
case "owner":
|
|
526
|
+
return ownerCapabilities;
|
|
527
|
+
case "custom":
|
|
528
|
+
return [...new Set(customCapabilities)];
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/apps/server/persistence/environment-state.schema.ts
|
|
533
|
+
import { sql } from "drizzle-orm";
|
|
534
|
+
import {
|
|
535
|
+
check,
|
|
536
|
+
foreignKey,
|
|
537
|
+
index,
|
|
538
|
+
integer,
|
|
539
|
+
primaryKey,
|
|
540
|
+
sqliteTable,
|
|
541
|
+
text
|
|
542
|
+
} from "drizzle-orm/sqlite-core";
|
|
543
|
+
|
|
544
|
+
// src/apps/server/domain/environment-state.contract.ts
|
|
545
|
+
var operationStatuses = [
|
|
546
|
+
"queued",
|
|
547
|
+
"running",
|
|
548
|
+
"succeeded",
|
|
549
|
+
"failed",
|
|
550
|
+
"cancelled",
|
|
551
|
+
"outcome_unknown"
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
// src/apps/server/persistence/environment-state.schema.ts
|
|
555
|
+
var environmentTable = sqliteTable(
|
|
556
|
+
"environment",
|
|
557
|
+
{
|
|
558
|
+
automaticPort: integer("automatic_port"),
|
|
559
|
+
id: text("id").notNull().unique(),
|
|
560
|
+
singleton: integer("singleton").primaryKey()
|
|
561
|
+
},
|
|
562
|
+
(environment) => [
|
|
563
|
+
check(
|
|
564
|
+
"environment_automatic_port_check",
|
|
565
|
+
sql`${environment.automaticPort} BETWEEN 1 AND 65535`
|
|
566
|
+
),
|
|
567
|
+
check("environment_singleton_check", sql`${environment.singleton} = 1`)
|
|
568
|
+
]
|
|
569
|
+
), authorizationMetadataTable = sqliteTable(
|
|
570
|
+
"authorization_metadata",
|
|
571
|
+
{
|
|
572
|
+
createdAt: text("created_at").notNull(),
|
|
573
|
+
id: text("id").primaryKey(),
|
|
574
|
+
label: text("label").notNull(),
|
|
575
|
+
lastSeenAt: text("last_seen_at"),
|
|
576
|
+
revokedAt: text("revoked_at"),
|
|
577
|
+
role: text("role", { enum: environmentAuthorizationRoles }).notNull()
|
|
578
|
+
},
|
|
579
|
+
(authorization) => [
|
|
580
|
+
check(
|
|
581
|
+
"authorization_metadata_role_check",
|
|
582
|
+
sql`${authorization.role} IN (${sql.raw(sqlValues(environmentAuthorizationRoles))})`
|
|
583
|
+
)
|
|
584
|
+
]
|
|
585
|
+
), authorizationCapabilityTable = sqliteTable(
|
|
586
|
+
"authorization_capability",
|
|
587
|
+
{
|
|
588
|
+
authorizationId: text("authorization_id").notNull(),
|
|
589
|
+
capability: text("capability", {
|
|
590
|
+
enum: environmentAccessCapabilities
|
|
591
|
+
}).notNull()
|
|
592
|
+
},
|
|
593
|
+
(authorizationCapability) => [
|
|
594
|
+
primaryKey({
|
|
595
|
+
columns: [
|
|
596
|
+
authorizationCapability.authorizationId,
|
|
597
|
+
authorizationCapability.capability
|
|
598
|
+
]
|
|
599
|
+
}),
|
|
600
|
+
foreignKey({
|
|
601
|
+
columns: [authorizationCapability.authorizationId],
|
|
602
|
+
foreignColumns: [authorizationMetadataTable.id]
|
|
603
|
+
}).onDelete("cascade"),
|
|
604
|
+
check(
|
|
605
|
+
"authorization_capability_value_check",
|
|
606
|
+
sql`${authorizationCapability.capability} IN (${sql.raw(sqlValues(environmentAccessCapabilities))})`
|
|
607
|
+
)
|
|
608
|
+
]
|
|
609
|
+
), operationActivityTable = sqliteTable(
|
|
610
|
+
"operation_activity",
|
|
611
|
+
{
|
|
612
|
+
finishedAt: text("finished_at"),
|
|
613
|
+
id: text("id").primaryKey(),
|
|
614
|
+
kind: text("kind").notNull(),
|
|
615
|
+
startedAt: text("started_at").notNull(),
|
|
616
|
+
status: text("status", { enum: operationStatuses }).notNull()
|
|
617
|
+
},
|
|
618
|
+
(operation) => [
|
|
619
|
+
check(
|
|
620
|
+
"operation_activity_status_check",
|
|
621
|
+
sql`${operation.status} IN (${sql.raw(sqlValues(operationStatuses))})`
|
|
622
|
+
),
|
|
623
|
+
index("operation_activity_started_at").on(
|
|
624
|
+
sql`${operation.startedAt} DESC`,
|
|
625
|
+
sql`${operation.id} DESC`
|
|
626
|
+
)
|
|
627
|
+
]
|
|
628
|
+
);
|
|
629
|
+
function sqlValues(values) {
|
|
630
|
+
return values.map((value) => `'${value.replaceAll("'", "''")}'`).join(", ");
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/apps/server/features/environment-authorization/environment-authorization.ts
|
|
634
|
+
var pairingLifetimeMilliseconds = 600 * 1e3, ticketLifetimeMilliseconds = 30 * 1e3, authorizationInactivityMilliseconds = 2160 * 60 * 60 * 1e3, retainedMaterialMilliseconds = 1440 * 60 * 1e3;
|
|
635
|
+
function createEnvironmentAuthorization(context, serverSecret, options = {}) {
|
|
636
|
+
let clock = options.clock ?? systemClock, pairings = /* @__PURE__ */ new Map(), tickets = /* @__PURE__ */ new Map();
|
|
637
|
+
return {
|
|
638
|
+
authorize: (credential, capability) => authorizeCredential(context, serverSecret, clock, credential, capability),
|
|
639
|
+
consumeTicket: (ticket) => consumeTicket(context, clock, tickets, ticket),
|
|
640
|
+
createPairing: (pairing) => Effect.sync(() => createPairing(clock, pairings, pairing)),
|
|
641
|
+
exchangePairing: (exchange) => exchangePairing(context, serverSecret, clock, pairings, exchange),
|
|
642
|
+
mintTicket: (credential) => mintTicket(context, serverSecret, clock, tickets, credential),
|
|
643
|
+
revoke: (credential, authorizationId) => revokeAuthorization(
|
|
644
|
+
context,
|
|
645
|
+
serverSecret,
|
|
646
|
+
clock,
|
|
647
|
+
credential,
|
|
648
|
+
authorizationId
|
|
649
|
+
)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function createPairing(clock, pairings, pairing) {
|
|
653
|
+
let now = clock.now().getTime();
|
|
654
|
+
removeOldMaterial(pairings, now);
|
|
655
|
+
let material = createAvailablePairingCode(pairings), expiresAt = now + pairingLifetimeMilliseconds;
|
|
656
|
+
return pairings.set(digestSecretMaterial(material), {
|
|
657
|
+
capabilities: capabilitiesForRole(pairing.role, pairing.capabilities),
|
|
658
|
+
expiresAt,
|
|
659
|
+
role: pairing.role,
|
|
660
|
+
used: !1
|
|
661
|
+
}), { expiresAt: new Date(expiresAt).toISOString(), material };
|
|
662
|
+
}
|
|
663
|
+
function createAvailablePairingCode(pairings) {
|
|
664
|
+
let code = createPairingCode();
|
|
665
|
+
for (; pairings.has(digestSecretMaterial(code)); )
|
|
666
|
+
code = createPairingCode();
|
|
667
|
+
return code;
|
|
668
|
+
}
|
|
669
|
+
function exchangePairing(context, serverSecret, clock, pairings, exchange) {
|
|
670
|
+
return Effect.gen(function* () {
|
|
671
|
+
let now = clock.now(), pairing = yield* consumePairing(
|
|
672
|
+
pairings,
|
|
673
|
+
exchange.pairingMaterial,
|
|
674
|
+
now
|
|
675
|
+
), authorization = {
|
|
676
|
+
capabilities: pairing.capabilities,
|
|
677
|
+
id: randomUUID(),
|
|
678
|
+
label: exchange.label,
|
|
679
|
+
role: pairing.role
|
|
680
|
+
};
|
|
681
|
+
return yield* context.write(
|
|
682
|
+
"Could not save device authorization",
|
|
683
|
+
(database) => database.transaction(
|
|
684
|
+
(transaction) => {
|
|
685
|
+
transaction.insert(authorizationMetadataTable).values({
|
|
686
|
+
createdAt: now.toISOString(),
|
|
687
|
+
id: authorization.id,
|
|
688
|
+
label: authorization.label,
|
|
689
|
+
role: authorization.role
|
|
690
|
+
}).run(), authorization.role === "custom" && authorization.capabilities.length > 0 && transaction.insert(authorizationCapabilityTable).values(
|
|
691
|
+
authorization.capabilities.map((capability) => ({
|
|
692
|
+
authorizationId: authorization.id,
|
|
693
|
+
capability
|
|
694
|
+
}))
|
|
695
|
+
).run();
|
|
696
|
+
},
|
|
697
|
+
{ behavior: "immediate" }
|
|
698
|
+
)
|
|
699
|
+
).pipe(
|
|
700
|
+
Effect.tapError(
|
|
701
|
+
() => Effect.sync(() => {
|
|
702
|
+
pairing.used = !1;
|
|
703
|
+
})
|
|
704
|
+
)
|
|
705
|
+
), {
|
|
706
|
+
authorization,
|
|
707
|
+
credential: createDeviceCredential(serverSecret, authorization.id)
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function consumePairing(pairings, material, now) {
|
|
712
|
+
let pairing = pairings.get(digestSecretMaterial(material));
|
|
713
|
+
return pairing === void 0 ? failAuthorization({ _tag: "InvalidPairing" }) : pairing.used ? failAuthorization({ _tag: "PairingAlreadyUsed" }) : now.getTime() >= pairing.expiresAt ? failAuthorization({ _tag: "ExpiredPairing" }) : (pairing.used = !0, Effect.succeed(pairing));
|
|
714
|
+
}
|
|
715
|
+
function authorizeCredential(context, serverSecret, clock, credential, capability) {
|
|
716
|
+
let authorizationId = verifyDeviceCredential(serverSecret, credential);
|
|
717
|
+
return authorizationId === void 0 ? failAuthorization({ _tag: "InvalidGrant" }) : authorizeStoredGrant(context, clock, authorizationId, capability);
|
|
718
|
+
}
|
|
719
|
+
function authorizeStoredGrant(context, clock, authorizationId, capability) {
|
|
720
|
+
let now = clock.now();
|
|
721
|
+
return context.write("Could not authenticate device authorization", (database) => {
|
|
722
|
+
let metadata = database.select().from(authorizationMetadataTable).where(eq(authorizationMetadataTable.id, authorizationId)).get();
|
|
723
|
+
if (metadata === void 0)
|
|
724
|
+
return authorizationFailure({ _tag: "InvalidGrant" });
|
|
725
|
+
if (metadata.revokedAt !== null)
|
|
726
|
+
return authorizationFailure({ _tag: "RevokedGrant" });
|
|
727
|
+
let activeSince = new Date(
|
|
728
|
+
metadata.lastSeenAt ?? metadata.createdAt
|
|
729
|
+
).getTime();
|
|
730
|
+
if (now.getTime() - activeSince >= authorizationInactivityMilliseconds)
|
|
731
|
+
return authorizationFailure({ _tag: "ExpiredGrant" });
|
|
732
|
+
let capabilities = capabilitiesForRole(
|
|
733
|
+
metadata.role,
|
|
734
|
+
metadata.role === "custom" ? database.select({ capability: authorizationCapabilityTable.capability }).from(authorizationCapabilityTable).where(
|
|
735
|
+
eq(
|
|
736
|
+
authorizationCapabilityTable.authorizationId,
|
|
737
|
+
authorizationId
|
|
738
|
+
)
|
|
739
|
+
).all().map(({ capability: storedCapability }) => storedCapability) : []
|
|
740
|
+
);
|
|
741
|
+
return database.update(authorizationMetadataTable).set({ lastSeenAt: now.toISOString() }).where(eq(authorizationMetadataTable.id, authorizationId)).run(), capabilities.includes(capability) ? authorizationSuccess({
|
|
742
|
+
capabilities,
|
|
743
|
+
id: metadata.id,
|
|
744
|
+
label: metadata.label,
|
|
745
|
+
role: metadata.role
|
|
746
|
+
}) : authorizationFailure({ _tag: "CapabilityDenied", capability });
|
|
747
|
+
}).pipe(
|
|
748
|
+
Effect.flatMap(
|
|
749
|
+
(result) => result.success ? Effect.succeed(result.authorization) : failAuthorization(result.failure)
|
|
750
|
+
)
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
function mintTicket(context, serverSecret, clock, tickets, credential) {
|
|
754
|
+
return Effect.gen(function* () {
|
|
755
|
+
let authorization = yield* authorizeCredential(
|
|
756
|
+
context,
|
|
757
|
+
serverSecret,
|
|
758
|
+
clock,
|
|
759
|
+
credential,
|
|
760
|
+
"environment.read"
|
|
761
|
+
), now = clock.now().getTime();
|
|
762
|
+
removeOldMaterial(tickets, now);
|
|
763
|
+
let ticket = createSecretMaterial(), expiresAt = now + ticketLifetimeMilliseconds;
|
|
764
|
+
return tickets.set(digestSecretMaterial(ticket), {
|
|
765
|
+
authorizationId: authorization.id,
|
|
766
|
+
expiresAt,
|
|
767
|
+
used: !1
|
|
768
|
+
}), { expiresAt: new Date(expiresAt).toISOString(), ticket };
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function consumeTicket(context, clock, tickets, ticket) {
|
|
772
|
+
if (ticket === void 0)
|
|
773
|
+
return failAuthorization({ _tag: "InvalidTicket" });
|
|
774
|
+
let stored = tickets.get(digestSecretMaterial(ticket));
|
|
775
|
+
return stored === void 0 ? failAuthorization({ _tag: "InvalidTicket" }) : stored.used ? failAuthorization({ _tag: "TicketAlreadyUsed" }) : clock.now().getTime() >= stored.expiresAt ? failAuthorization({ _tag: "ExpiredTicket" }) : (stored.used = !0, authorizeStoredGrant(
|
|
776
|
+
context,
|
|
777
|
+
clock,
|
|
778
|
+
stored.authorizationId,
|
|
779
|
+
"environment.read"
|
|
780
|
+
).pipe(
|
|
781
|
+
Effect.tapError(
|
|
782
|
+
(error) => error._tag === "EnvironmentStorageError" ? Effect.sync(() => {
|
|
783
|
+
stored.used = !1;
|
|
784
|
+
}) : Effect.void
|
|
785
|
+
)
|
|
786
|
+
));
|
|
787
|
+
}
|
|
788
|
+
function revokeAuthorization(context, serverSecret, clock, credential, authorizationId) {
|
|
789
|
+
return Effect.gen(function* () {
|
|
790
|
+
yield* authorizeCredential(
|
|
791
|
+
context,
|
|
792
|
+
serverSecret,
|
|
793
|
+
clock,
|
|
794
|
+
credential,
|
|
795
|
+
"authorization.manage"
|
|
796
|
+
);
|
|
797
|
+
let revokedAt = clock.now().toISOString();
|
|
798
|
+
return (yield* context.write(
|
|
799
|
+
"Could not revoke device authorization",
|
|
800
|
+
(database) => database.update(authorizationMetadataTable).set({ revokedAt }).where(eq(authorizationMetadataTable.id, authorizationId)).run().changes > 0
|
|
801
|
+
)) ? { authorizationId, revokedAt } : yield* failAuthorization({ _tag: "InvalidGrant" });
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
function removeOldMaterial(entries, now) {
|
|
805
|
+
for (let [digest, entry] of entries)
|
|
806
|
+
entry.expiresAt + retainedMaterialMilliseconds <= now && entries.delete(digest);
|
|
807
|
+
}
|
|
808
|
+
function authorizationFailure(failure) {
|
|
809
|
+
return { failure, success: !1 };
|
|
810
|
+
}
|
|
811
|
+
function authorizationSuccess(authorization) {
|
|
812
|
+
return { authorization, success: !0 };
|
|
813
|
+
}
|
|
814
|
+
function failAuthorization(failure) {
|
|
815
|
+
return Effect.fail(new EnvironmentAuthorizationError({ failure }));
|
|
816
|
+
}
|
|
817
|
+
var systemClock = {
|
|
818
|
+
now: () => /* @__PURE__ */ new Date()
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// src/apps/server/features/environment-connection/events/environment-event-publisher.ts
|
|
822
|
+
function createEnvironmentEventPublisher() {
|
|
823
|
+
let sequence = 0, subscribers = /* @__PURE__ */ new Set();
|
|
824
|
+
return {
|
|
825
|
+
currentSequence: () => sequence,
|
|
826
|
+
publishChanged: () => {
|
|
827
|
+
sequence += 1;
|
|
828
|
+
for (let subscriber of subscribers)
|
|
829
|
+
subscriber(sequence);
|
|
830
|
+
return sequence;
|
|
831
|
+
},
|
|
832
|
+
subscribe: (subscriber) => (subscribers.add(subscriber), () => {
|
|
833
|
+
subscribers.delete(subscriber);
|
|
834
|
+
})
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/apps/server/features/environment-server/environment-state.specifications.ts
|
|
839
|
+
import { eq as eq2, isNull } from "drizzle-orm";
|
|
840
|
+
function isCurrentEnvironment() {
|
|
841
|
+
return eq2(environmentTable.singleton, 1);
|
|
842
|
+
}
|
|
843
|
+
function hasNoAutomaticPort() {
|
|
844
|
+
return isNull(environmentTable.automaticPort);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/apps/server/features/environment-server/runtime/runtime-marker.ts
|
|
848
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
849
|
+
import { mkdir as mkdir2, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
850
|
+
import { dirname } from "node:path";
|
|
851
|
+
import { Effect as Effect3 } from "effect";
|
|
852
|
+
|
|
853
|
+
// src/apps/server/error-inspection.ts
|
|
854
|
+
function isFileSystemError(error) {
|
|
855
|
+
return error instanceof Error && "code" in error;
|
|
856
|
+
}
|
|
857
|
+
function errorMessage(error) {
|
|
858
|
+
return error instanceof Error ? error.message : String(error);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/apps/server/features/environment-server/runtime/runtime-errors.contract.ts
|
|
862
|
+
import { Data as Data2 } from "effect";
|
|
863
|
+
var RuntimeMarkerError = class extends Data2.TaggedError("RuntimeMarkerError") {
|
|
864
|
+
}, RuntimeRequirementsError = class extends Data2.TaggedError(
|
|
865
|
+
"RuntimeRequirementsError"
|
|
866
|
+
) {
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// src/apps/server/persistence/storage/environment-paths.ts
|
|
870
|
+
import { chmod, mkdir } from "node:fs/promises";
|
|
871
|
+
import { homedir } from "node:os";
|
|
872
|
+
import { join } from "node:path";
|
|
873
|
+
import { Effect as Effect2 } from "effect";
|
|
874
|
+
|
|
875
|
+
// src/apps/server/persistence/storage/storage-error.contract.ts
|
|
876
|
+
import { Data as Data3 } from "effect";
|
|
877
|
+
var EnvironmentStorageError = class extends Data3.TaggedError(
|
|
878
|
+
"EnvironmentStorageError"
|
|
879
|
+
) {
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
// src/apps/server/persistence/storage/environment-paths.ts
|
|
883
|
+
function defaultEnvironmentPaths() {
|
|
884
|
+
return environmentPaths(join(homedir(), ".rebase"));
|
|
885
|
+
}
|
|
886
|
+
function environmentPaths(root) {
|
|
887
|
+
let cacheDirectory = join(root, "cache"), runtimeDirectory = join(root, "runtime"), secretsDirectory = join(root, "secrets"), settingsDirectory = join(root, "settings"), stateDirectory = join(root, "state");
|
|
888
|
+
return {
|
|
889
|
+
cacheDirectory,
|
|
890
|
+
root,
|
|
891
|
+
runtimeDirectory,
|
|
892
|
+
runtimeMarker: join(runtimeDirectory, "runtime.json"),
|
|
893
|
+
secretsDirectory,
|
|
894
|
+
serverSecret: join(secretsDirectory, "server.key"),
|
|
895
|
+
settingsDirectory,
|
|
896
|
+
stateDatabase: join(stateDirectory, "state.sqlite"),
|
|
897
|
+
stateDirectory
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function prepareEnvironmentDirectories(paths) {
|
|
901
|
+
let directories = [
|
|
902
|
+
paths.root,
|
|
903
|
+
paths.stateDirectory,
|
|
904
|
+
paths.runtimeDirectory,
|
|
905
|
+
paths.settingsDirectory,
|
|
906
|
+
paths.secretsDirectory,
|
|
907
|
+
paths.cacheDirectory
|
|
908
|
+
];
|
|
909
|
+
return Effect2.tryPromise({
|
|
910
|
+
try: async () => {
|
|
911
|
+
for (let directory of directories)
|
|
912
|
+
await mkdir(directory, { mode: 448, recursive: !0 }), process.platform !== "win32" && await chmod(directory, 448);
|
|
913
|
+
},
|
|
914
|
+
catch: (cause) => new EnvironmentStorageError({
|
|
915
|
+
cause,
|
|
916
|
+
message: `Could not prepare Environment storage: ${errorMessage(cause)}`
|
|
917
|
+
})
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/apps/server/features/environment-server/runtime/runtime-marker.ts
|
|
922
|
+
function defaultRuntimePath() {
|
|
923
|
+
return defaultEnvironmentPaths().runtimeMarker;
|
|
924
|
+
}
|
|
925
|
+
function acquireRuntimeMarker(marker, runtimePath = defaultRuntimePath()) {
|
|
926
|
+
return Effect3.acquireRelease(
|
|
927
|
+
writeRuntimeMarker(runtimePath, marker),
|
|
928
|
+
() => removeRuntimeMarker(runtimePath, marker.pid).pipe(
|
|
929
|
+
Effect3.catchTag("RuntimeMarkerError", reportCleanupFailure)
|
|
930
|
+
)
|
|
931
|
+
).pipe(Effect3.asVoid);
|
|
932
|
+
}
|
|
933
|
+
function writeRuntimeMarker(runtimePath, marker) {
|
|
934
|
+
let temporaryPath = `${runtimePath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
935
|
+
return Effect3.gen(function* () {
|
|
936
|
+
yield* fileSystemOperation(
|
|
937
|
+
() => mkdir2(dirname(runtimePath), { recursive: !0 })
|
|
938
|
+
), yield* fileSystemOperation(
|
|
939
|
+
() => writeFile(temporaryPath, `${JSON.stringify(marker)}
|
|
940
|
+
`, { mode: 384 })
|
|
941
|
+
), yield* replaceFile(temporaryPath, runtimePath);
|
|
942
|
+
}).pipe(
|
|
943
|
+
Effect3.ensuring(
|
|
944
|
+
fileSystemOperation(() => rm(temporaryPath, { force: !0 })).pipe(
|
|
945
|
+
Effect3.orDie
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
function removeRuntimeMarker(runtimePath, pid) {
|
|
951
|
+
return Effect3.gen(function* () {
|
|
952
|
+
let contents = yield* fileSystemOperation(
|
|
953
|
+
() => readFile(runtimePath, "utf8")
|
|
954
|
+
);
|
|
955
|
+
(yield* Effect3.try({
|
|
956
|
+
try: () => JSON.parse(contents),
|
|
957
|
+
catch: runtimeMarkerError
|
|
958
|
+
})).pid === pid && (yield* fileSystemOperation(() => rm(runtimePath)));
|
|
959
|
+
}).pipe(
|
|
960
|
+
Effect3.catchTag(
|
|
961
|
+
"RuntimeMarkerError",
|
|
962
|
+
(error) => isMissingFile(error.cause) ? Effect3.void : Effect3.fail(error)
|
|
963
|
+
)
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
function replaceFile(source, destination) {
|
|
967
|
+
return fileSystemOperation(() => rename(source, destination)).pipe(
|
|
968
|
+
Effect3.catchTag("RuntimeMarkerError", (error) => isReplaceConflict(error.cause) ? Effect3.gen(function* () {
|
|
969
|
+
yield* fileSystemOperation(() => rm(destination, { force: !0 })), yield* fileSystemOperation(() => rename(source, destination));
|
|
970
|
+
}) : Effect3.fail(error))
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
function fileSystemOperation(operation) {
|
|
974
|
+
return Effect3.tryPromise({
|
|
975
|
+
try: operation,
|
|
976
|
+
catch: runtimeMarkerError
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
function runtimeMarkerError(cause) {
|
|
980
|
+
return new RuntimeMarkerError({
|
|
981
|
+
cause,
|
|
982
|
+
message: `Could not update the runtime marker: ${errorMessage(cause)}`
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function reportCleanupFailure(error) {
|
|
986
|
+
return Effect3.sync(() => {
|
|
987
|
+
process.stderr.write(`${error.message}
|
|
988
|
+
`);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function isMissingFile(error) {
|
|
992
|
+
return isFileSystemError(error) && error.code === "ENOENT";
|
|
993
|
+
}
|
|
994
|
+
function isReplaceConflict(error) {
|
|
995
|
+
return isFileSystemError(error) && (error.code === "EEXIST" || error.code === "EPERM");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/apps/server/features/environment-server/runtime/runtime-requirements.ts
|
|
999
|
+
import { execFile } from "node:child_process";
|
|
1000
|
+
import { Effect as Effect4 } from "effect";
|
|
1001
|
+
var requiredNodeMajor = 24, minimumGitVersion = [2, 34, 0], verifyRuntimeRequirements = Effect4.gen(function* () {
|
|
1002
|
+
yield* captureRequirement(
|
|
1003
|
+
() => assertSupportedNodeVersion(process.versions.node)
|
|
1004
|
+
);
|
|
1005
|
+
let gitVersion = yield* readGitVersion();
|
|
1006
|
+
yield* captureRequirement(() => assertSupportedGitVersion(gitVersion));
|
|
1007
|
+
});
|
|
1008
|
+
function assertSupportedNodeVersion(version) {
|
|
1009
|
+
let [major] = parseVersion(version, "Node");
|
|
1010
|
+
if (major !== requiredNodeMajor)
|
|
1011
|
+
throw new Error(`Node 24 is required. Found Node ${version}.`);
|
|
1012
|
+
}
|
|
1013
|
+
function assertSupportedGitVersion(version) {
|
|
1014
|
+
let parsed = parseVersion(version, "Git");
|
|
1015
|
+
if (compareVersions(parsed, minimumGitVersion) < 0)
|
|
1016
|
+
throw new Error(`Git 2.34 or newer is required. Found Git ${version}.`);
|
|
1017
|
+
}
|
|
1018
|
+
function parseGitVersion(output) {
|
|
1019
|
+
let match = output.match(/^git version (\d+\.\d+\.\d+)/i);
|
|
1020
|
+
if (!match?.[1])
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
`Could not determine the Git version from "${output.trim()}".`
|
|
1023
|
+
);
|
|
1024
|
+
return match[1];
|
|
1025
|
+
}
|
|
1026
|
+
function readGitVersion() {
|
|
1027
|
+
return Effect4.callback((resume, signal) => {
|
|
1028
|
+
execFile(
|
|
1029
|
+
"git",
|
|
1030
|
+
["--version"],
|
|
1031
|
+
{ encoding: "utf8", signal, timeout: 5e3 },
|
|
1032
|
+
(error, stdout) => {
|
|
1033
|
+
if (!error) {
|
|
1034
|
+
resume(captureRequirement(() => parseGitVersion(stdout)));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
let message = error.code === "ENOENT" ? "Git 2.34 or newer is required, but Git was not found." : `Could not run Git: ${error.message}`;
|
|
1038
|
+
resume(
|
|
1039
|
+
Effect4.fail(new RuntimeRequirementsError({ cause: error, message }))
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
function captureRequirement(check2) {
|
|
1046
|
+
return Effect4.try({
|
|
1047
|
+
try: check2,
|
|
1048
|
+
catch: (cause) => new RuntimeRequirementsError({
|
|
1049
|
+
cause,
|
|
1050
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
1051
|
+
})
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
function parseVersion(version, product) {
|
|
1055
|
+
let match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
1056
|
+
if (!match?.[1] || !match[2] || !match[3])
|
|
1057
|
+
throw new Error(`Could not parse ${product} version "${version}".`);
|
|
1058
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
1059
|
+
}
|
|
1060
|
+
function compareVersions(left, right) {
|
|
1061
|
+
for (let index2 of [0, 1, 2]) {
|
|
1062
|
+
let difference = left[index2] - right[index2];
|
|
1063
|
+
if (difference !== 0) return difference;
|
|
1064
|
+
}
|
|
1065
|
+
return 0;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/apps/server/features/environment-server/server/environment-listener.ts
|
|
1069
|
+
import { createServer } from "node:http";
|
|
1070
|
+
import { Effect as Effect14, FiberSet as FiberSet2 } from "effect";
|
|
1071
|
+
|
|
1072
|
+
// src/apps/server/features/environment-connection/http/environment-http-handler.ts
|
|
1073
|
+
import { Effect as Effect9 } from "effect";
|
|
1074
|
+
|
|
1075
|
+
// src/apps/server/features/browser-client/browser-assets.ts
|
|
1076
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1077
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
1078
|
+
import { Effect as Effect5 } from "effect";
|
|
1079
|
+
var contentTypes = {
|
|
1080
|
+
".css": "text/css; charset=utf-8",
|
|
1081
|
+
".html": "text/html; charset=utf-8",
|
|
1082
|
+
".js": "text/javascript; charset=utf-8",
|
|
1083
|
+
".json": "application/json; charset=utf-8",
|
|
1084
|
+
".svg": "image/svg+xml"
|
|
1085
|
+
};
|
|
1086
|
+
function respondWithBrowserAsset(request, response, assetsRoot) {
|
|
1087
|
+
let asset = resolveBrowserAsset(request.url, assetsRoot);
|
|
1088
|
+
return asset === void 0 ? Effect5.succeed(!1) : request.method !== "GET" && request.method !== "HEAD" ? (response.writeHead(405, { allow: "GET, HEAD" }).end(), Effect5.succeed(!0)) : Effect5.promise(async () => {
|
|
1089
|
+
try {
|
|
1090
|
+
let content = await readFile2(asset.path);
|
|
1091
|
+
response.writeHead(
|
|
1092
|
+
200,
|
|
1093
|
+
browserAssetHeaders(asset.extension, asset.cache)
|
|
1094
|
+
), response.end(request.method === "HEAD" ? void 0 : content);
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
let status = isMissingAsset(error) ? 404 : 500;
|
|
1097
|
+
response.writeHead(status).end();
|
|
1098
|
+
}
|
|
1099
|
+
return !0;
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
function resolveBrowserAsset(url, assetsRoot) {
|
|
1103
|
+
let pathname;
|
|
1104
|
+
try {
|
|
1105
|
+
pathname = decodeURIComponent(
|
|
1106
|
+
new URL(url ?? "/", "http://127.0.0.1").pathname
|
|
1107
|
+
);
|
|
1108
|
+
} catch {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
let relativePath = browserAssetPath(pathname);
|
|
1112
|
+
if (relativePath === void 0)
|
|
1113
|
+
return;
|
|
1114
|
+
let path = resolve(assetsRoot, relativePath), pathFromRoot = relative(resolve(assetsRoot), path);
|
|
1115
|
+
if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot))
|
|
1116
|
+
return;
|
|
1117
|
+
let extensionIndex = relativePath.lastIndexOf(".");
|
|
1118
|
+
return {
|
|
1119
|
+
cache: relativePath.startsWith("assets/"),
|
|
1120
|
+
extension: extensionIndex === -1 ? "" : relativePath.slice(extensionIndex),
|
|
1121
|
+
path
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function browserAssetPath(pathname) {
|
|
1125
|
+
if (pathname === "/" || pathname === "/pair" || pathname === "/pair/")
|
|
1126
|
+
return "index.html";
|
|
1127
|
+
if (pathname === "/favicon.svg" || pathname.startsWith("/assets/") && pathname !== "/assets/")
|
|
1128
|
+
return pathname.slice(1);
|
|
1129
|
+
}
|
|
1130
|
+
function browserAssetHeaders(extension, cache) {
|
|
1131
|
+
return {
|
|
1132
|
+
"cache-control": cache ? "public, max-age=31536000, immutable" : "no-store",
|
|
1133
|
+
"content-security-policy": "default-src 'self'; base-uri 'none'; connect-src 'self' ws://127.0.0.1:*; form-action 'none'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'",
|
|
1134
|
+
"content-type": contentTypes[extension] ?? "application/octet-stream",
|
|
1135
|
+
"x-content-type-options": "nosniff"
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function isMissingAsset(error) {
|
|
1139
|
+
return error !== null && typeof error == "object" && "code" in error && (error.code === "ENOENT" || error.code === "EISDIR");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/apps/server/features/environment-authorization/http/environment-authorization-http-handler.ts
|
|
1143
|
+
import { Effect as Effect7, Schema as Schema7 } from "effect";
|
|
1144
|
+
|
|
1145
|
+
// src/apps/server/features/environment-connection/environment-request-authorization.ts
|
|
1146
|
+
import { Effect as Effect6 } from "effect";
|
|
1147
|
+
function validateRequestHost(request) {
|
|
1148
|
+
let port = request.socket.localPort, expectedHost = port === void 0 ? void 0 : `127.0.0.1:${port}`;
|
|
1149
|
+
return request.headers.host === expectedHost ? Effect6.void : failAuthorization2({ _tag: "InvalidHost" });
|
|
1150
|
+
}
|
|
1151
|
+
function validateRequestOrigin(request, required) {
|
|
1152
|
+
let origin = request.headers.origin;
|
|
1153
|
+
return origin === void 0 && !required ? Effect6.void : origin === expectedRequestOrigin(request) ? Effect6.void : failAuthorization2({ _tag: "InvalidOrigin" });
|
|
1154
|
+
}
|
|
1155
|
+
function expectedRequestOrigin(request) {
|
|
1156
|
+
return `http://127.0.0.1:${request.socket.localPort ?? 0}`;
|
|
1157
|
+
}
|
|
1158
|
+
function readBearerCredential(request) {
|
|
1159
|
+
let authorization = request.headers.authorization;
|
|
1160
|
+
return authorization?.startsWith("Bearer ") ? authorization.slice(7) : void 0;
|
|
1161
|
+
}
|
|
1162
|
+
function authorizationFailureStatus(failure) {
|
|
1163
|
+
switch (failure._tag) {
|
|
1164
|
+
case "InvalidHost":
|
|
1165
|
+
case "InvalidOrigin":
|
|
1166
|
+
case "CapabilityDenied":
|
|
1167
|
+
return 403;
|
|
1168
|
+
case "InvalidGrant":
|
|
1169
|
+
case "RevokedGrant":
|
|
1170
|
+
case "InvalidPairing":
|
|
1171
|
+
case "InvalidTicket":
|
|
1172
|
+
return 401;
|
|
1173
|
+
case "ExpiredGrant":
|
|
1174
|
+
case "ExpiredPairing":
|
|
1175
|
+
case "ExpiredTicket":
|
|
1176
|
+
return 410;
|
|
1177
|
+
case "PairingAlreadyUsed":
|
|
1178
|
+
case "TicketAlreadyUsed":
|
|
1179
|
+
return 409;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function failAuthorization2(failure) {
|
|
1183
|
+
return Effect6.fail(new EnvironmentAuthorizationError({ failure }));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/apps/server/features/environment-connection/http/environment-http-request-body.contract.ts
|
|
1187
|
+
import { Data as Data4 } from "effect";
|
|
1188
|
+
var EnvironmentHttpBodyError = class extends Data4.TaggedError(
|
|
1189
|
+
"EnvironmentHttpBodyError"
|
|
1190
|
+
) {
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
// src/apps/server/features/environment-connection/http/environment-http-response.ts
|
|
1194
|
+
import { Schema as Schema6 } from "effect";
|
|
1195
|
+
function writeJson(response, status, schema, value) {
|
|
1196
|
+
writeJsonValue(response, status, Schema6.encodeSync(schema)(value));
|
|
1197
|
+
}
|
|
1198
|
+
function writeJsonValue(response, status, value) {
|
|
1199
|
+
response.writeHead(status, {
|
|
1200
|
+
"cache-control": "no-store",
|
|
1201
|
+
"content-type": "application/json; charset=utf-8"
|
|
1202
|
+
}), response.end(JSON.stringify(value));
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/apps/server/features/environment-authorization/http/environment-authorization-http-handler.ts
|
|
1206
|
+
function respondToEnvironmentAuthorizationRequest(request, response, body, authorization) {
|
|
1207
|
+
return Effect7.gen(function* () {
|
|
1208
|
+
if (request.url === EnvironmentAuthorizationHttpApi.exchangePairing.path) {
|
|
1209
|
+
yield* requireMethod(
|
|
1210
|
+
request,
|
|
1211
|
+
response,
|
|
1212
|
+
EnvironmentAuthorizationHttpApi.exchangePairing.method
|
|
1213
|
+
), yield* validateRequestOrigin(request, !1);
|
|
1214
|
+
let result = yield* authorization.exchangePairing(
|
|
1215
|
+
yield* decodeRequestBody(ExchangeEnvironmentPairing, body)
|
|
1216
|
+
);
|
|
1217
|
+
return writeJson(
|
|
1218
|
+
response,
|
|
1219
|
+
EnvironmentAuthorizationHttpApi.exchangePairing.successStatus,
|
|
1220
|
+
EnvironmentPairingExchanged,
|
|
1221
|
+
result
|
|
1222
|
+
), !0;
|
|
1223
|
+
}
|
|
1224
|
+
if (request.url === EnvironmentAuthorizationHttpApi.createPairing.path) {
|
|
1225
|
+
yield* requireMethod(
|
|
1226
|
+
request,
|
|
1227
|
+
response,
|
|
1228
|
+
EnvironmentAuthorizationHttpApi.createPairing.method
|
|
1229
|
+
), yield* validateRequestOrigin(request, !1), yield* authorization.authorize(
|
|
1230
|
+
readBearerCredential(request),
|
|
1231
|
+
"authorization.manage"
|
|
1232
|
+
);
|
|
1233
|
+
let pairing = yield* authorization.createPairing(
|
|
1234
|
+
yield* decodeRequestBody(CreateEnvironmentPairing, body)
|
|
1235
|
+
);
|
|
1236
|
+
return writeJson(
|
|
1237
|
+
response,
|
|
1238
|
+
EnvironmentAuthorizationHttpApi.createPairing.successStatus,
|
|
1239
|
+
EnvironmentPairingCreated,
|
|
1240
|
+
{
|
|
1241
|
+
expiresAt: pairing.expiresAt,
|
|
1242
|
+
pairingUrl: `${expectedRequestOrigin(request)}/pair#${pairing.material}`
|
|
1243
|
+
}
|
|
1244
|
+
), !0;
|
|
1245
|
+
}
|
|
1246
|
+
if (request.url === EnvironmentAuthorizationHttpApi.mintWebSocketTicket.path) {
|
|
1247
|
+
yield* requireMethod(
|
|
1248
|
+
request,
|
|
1249
|
+
response,
|
|
1250
|
+
EnvironmentAuthorizationHttpApi.mintWebSocketTicket.method
|
|
1251
|
+
), yield* validateRequestOrigin(request, !1), yield* requireEmptyBody(body);
|
|
1252
|
+
let ticket = yield* authorization.mintTicket(
|
|
1253
|
+
readBearerCredential(request)
|
|
1254
|
+
);
|
|
1255
|
+
return writeJson(
|
|
1256
|
+
response,
|
|
1257
|
+
EnvironmentAuthorizationHttpApi.mintWebSocketTicket.successStatus,
|
|
1258
|
+
EnvironmentWebSocketTicket,
|
|
1259
|
+
ticket
|
|
1260
|
+
), !0;
|
|
1261
|
+
}
|
|
1262
|
+
if (request.url === EnvironmentAuthorizationHttpApi.revokeAuthorization.path) {
|
|
1263
|
+
yield* requireMethod(
|
|
1264
|
+
request,
|
|
1265
|
+
response,
|
|
1266
|
+
EnvironmentAuthorizationHttpApi.revokeAuthorization.method
|
|
1267
|
+
), yield* validateRequestOrigin(request, !1);
|
|
1268
|
+
let revocation = yield* decodeRequestBody(
|
|
1269
|
+
RevokeEnvironmentAuthorization,
|
|
1270
|
+
body
|
|
1271
|
+
), revoked = yield* authorization.revoke(
|
|
1272
|
+
readBearerCredential(request),
|
|
1273
|
+
revocation.authorizationId
|
|
1274
|
+
);
|
|
1275
|
+
return writeJson(
|
|
1276
|
+
response,
|
|
1277
|
+
EnvironmentAuthorizationHttpApi.revokeAuthorization.successStatus,
|
|
1278
|
+
EnvironmentAuthorizationRevoked,
|
|
1279
|
+
revoked
|
|
1280
|
+
), !0;
|
|
1281
|
+
}
|
|
1282
|
+
return !1;
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
function requireMethod(request, response, method) {
|
|
1286
|
+
return request.method === method ? Effect7.void : Effect7.sync(
|
|
1287
|
+
() => response.writeHead(405, { allow: method }).end()
|
|
1288
|
+
).pipe(Effect7.andThen(Effect7.interrupt));
|
|
1289
|
+
}
|
|
1290
|
+
function requireEmptyBody(body) {
|
|
1291
|
+
return body.byteLength === 0 ? Effect7.void : Effect7.fail(
|
|
1292
|
+
new EnvironmentHttpBodyError({
|
|
1293
|
+
failure: { _tag: "InvalidMessage" }
|
|
1294
|
+
})
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
function decodeRequestBody(schema, body) {
|
|
1298
|
+
return Effect7.try({
|
|
1299
|
+
try: () => Schema7.decodeUnknownSync(schema)(JSON.parse(body.toString("utf8")), {
|
|
1300
|
+
onExcessProperty: "error"
|
|
1301
|
+
}),
|
|
1302
|
+
catch: () => new EnvironmentHttpBodyError({ failure: { _tag: "InvalidMessage" } })
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/apps/server/features/environment-connection/http/environment-http-request-body.ts
|
|
1307
|
+
import { Effect as Effect8 } from "effect";
|
|
1308
|
+
var maximumRequestBytes = currentTransportLimits.maxHttpRequestBytes;
|
|
1309
|
+
function readEnvironmentHttpRequestBody(request) {
|
|
1310
|
+
return Effect8.callback((resume) => {
|
|
1311
|
+
let state = {
|
|
1312
|
+
chunks: [],
|
|
1313
|
+
receivedBytes: 0
|
|
1314
|
+
}, finish = finishBodyRead(resume), handlers = createBodyHandlers(request, state, finish);
|
|
1315
|
+
if (declaredBodyLength(request) > maximumRequestBytes) {
|
|
1316
|
+
rejectPayloadTooLarge(request, finish);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
return attachBodyHandlers(request, handlers), Effect8.sync(() => detachBodyHandlers(request, handlers));
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
function createBodyHandlers(request, state, finish) {
|
|
1323
|
+
return {
|
|
1324
|
+
end: () => completeBodyRead(state, finish),
|
|
1325
|
+
receive: (chunk) => receiveBodyChunk(
|
|
1326
|
+
state,
|
|
1327
|
+
chunk,
|
|
1328
|
+
() => rejectPayloadTooLarge(request, finish)
|
|
1329
|
+
),
|
|
1330
|
+
rejectInvalidMessage: () => {
|
|
1331
|
+
finish(Effect8.fail(invalidMessageFailure()));
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function finishBodyRead(resume) {
|
|
1336
|
+
let settled = !1;
|
|
1337
|
+
return (effect) => {
|
|
1338
|
+
settled || (settled = !0, resume(effect));
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function rejectPayloadTooLarge(request, finish) {
|
|
1342
|
+
finish(Effect8.fail(payloadTooLargeFailure())), request.resume();
|
|
1343
|
+
}
|
|
1344
|
+
function receiveBodyChunk(state, chunk, rejectPayload) {
|
|
1345
|
+
if (state.receivedBytes += chunk.byteLength, state.receivedBytes > maximumRequestBytes) {
|
|
1346
|
+
rejectPayload();
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
state.chunks.push(chunk);
|
|
1350
|
+
}
|
|
1351
|
+
function completeBodyRead(state, finish) {
|
|
1352
|
+
finish(Effect8.succeed(Buffer.concat(state.chunks, state.receivedBytes)));
|
|
1353
|
+
}
|
|
1354
|
+
function attachBodyHandlers(request, handlers) {
|
|
1355
|
+
request.on("data", handlers.receive), request.on("end", handlers.end), request.on("aborted", handlers.rejectInvalidMessage), request.on("error", handlers.rejectInvalidMessage);
|
|
1356
|
+
}
|
|
1357
|
+
function detachBodyHandlers(request, handlers) {
|
|
1358
|
+
request.off("data", handlers.receive), request.off("end", handlers.end), request.off("aborted", handlers.rejectInvalidMessage), request.off("error", handlers.rejectInvalidMessage);
|
|
1359
|
+
}
|
|
1360
|
+
function declaredBodyLength(request) {
|
|
1361
|
+
return Number(request.headers["content-length"] ?? 0);
|
|
1362
|
+
}
|
|
1363
|
+
function invalidMessageFailure() {
|
|
1364
|
+
return new EnvironmentHttpBodyError({
|
|
1365
|
+
failure: { _tag: "InvalidMessage" }
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
function payloadTooLargeFailure() {
|
|
1369
|
+
return new EnvironmentHttpBodyError({
|
|
1370
|
+
failure: {
|
|
1371
|
+
_tag: "PayloadTooLarge",
|
|
1372
|
+
limitBytes: maximumRequestBytes
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/apps/server/features/environment-connection/http/environment-http-handler.ts
|
|
1378
|
+
function createEnvironmentHttpHandler(state, authorization, ready, runEnvironmentEffect, browserAssetsRoot) {
|
|
1379
|
+
return (request, response) => {
|
|
1380
|
+
let lifetime = startHttpRequestLifetime(request, response);
|
|
1381
|
+
runEnvironmentEffect(
|
|
1382
|
+
createEnvironmentHttpResponse(
|
|
1383
|
+
request,
|
|
1384
|
+
response,
|
|
1385
|
+
state,
|
|
1386
|
+
authorization,
|
|
1387
|
+
ready(),
|
|
1388
|
+
browserAssetsRoot
|
|
1389
|
+
).pipe(Effect9.ensuring(Effect9.sync(lifetime.release))),
|
|
1390
|
+
lifetime.signal
|
|
1391
|
+
);
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function startHttpRequestLifetime(request, response) {
|
|
1395
|
+
let abortController = new AbortController(), abort = () => abortController.abort();
|
|
1396
|
+
return request.once("aborted", abort), response.once("close", abort), {
|
|
1397
|
+
release: () => {
|
|
1398
|
+
request.off("aborted", abort), response.off("close", abort);
|
|
1399
|
+
},
|
|
1400
|
+
signal: abortController.signal
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function createEnvironmentHttpResponse(request, response, state, authorization, ready, browserAssetsRoot) {
|
|
1404
|
+
return respondToEnvironmentRequest(
|
|
1405
|
+
request,
|
|
1406
|
+
response,
|
|
1407
|
+
state,
|
|
1408
|
+
authorization,
|
|
1409
|
+
ready,
|
|
1410
|
+
browserAssetsRoot
|
|
1411
|
+
).pipe(
|
|
1412
|
+
Effect9.catch(
|
|
1413
|
+
(error) => Effect9.sync(() => writeEnvironmentHttpError(response, error))
|
|
1414
|
+
)
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
function respondToEnvironmentRequest(request, response, state, authorization, ready, browserAssetsRoot) {
|
|
1418
|
+
return Effect9.gen(function* () {
|
|
1419
|
+
if (request.url === "/health") {
|
|
1420
|
+
yield* requireMethod2(request, response, "GET"), yield* requireEmptyBody2(yield* readEnvironmentHttpRequestBody(request)), writeJsonValue(response, ready ? 200 : 503, {
|
|
1421
|
+
status: ready ? "ready" : "starting"
|
|
1422
|
+
});
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (yield* validateRequestHost(request), browserAssetsRoot !== void 0 && (yield* respondWithBrowserAsset(request, response, browserAssetsRoot)))
|
|
1426
|
+
return;
|
|
1427
|
+
let body = yield* readEnvironmentHttpRequestBody(request);
|
|
1428
|
+
if (request.url === EnvironmentHttpApi.discovery.path) {
|
|
1429
|
+
yield* requireMethod2(
|
|
1430
|
+
request,
|
|
1431
|
+
response,
|
|
1432
|
+
EnvironmentHttpApi.discovery.method
|
|
1433
|
+
), yield* requireEmptyBody2(body), writeJson(
|
|
1434
|
+
response,
|
|
1435
|
+
EnvironmentHttpApi.discovery.successStatus,
|
|
1436
|
+
EnvironmentDiscovery,
|
|
1437
|
+
state.discovery
|
|
1438
|
+
);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (request.url === EnvironmentHttpApi.snapshot.path) {
|
|
1442
|
+
yield* requireMethod2(
|
|
1443
|
+
request,
|
|
1444
|
+
response,
|
|
1445
|
+
EnvironmentHttpApi.snapshot.method
|
|
1446
|
+
), yield* requireEmptyBody2(body), yield* validateRequestOrigin(request, !1), yield* authorization.authorize(
|
|
1447
|
+
readBearerCredential(request),
|
|
1448
|
+
"environment.read"
|
|
1449
|
+
), writeJson(
|
|
1450
|
+
response,
|
|
1451
|
+
EnvironmentHttpApi.snapshot.successStatus,
|
|
1452
|
+
EnvironmentSnapshot,
|
|
1453
|
+
{
|
|
1454
|
+
environmentId: state.discovery.environmentId,
|
|
1455
|
+
sequence: state.events.currentSequence()
|
|
1456
|
+
}
|
|
1457
|
+
);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
(yield* respondToEnvironmentAuthorizationRequest(
|
|
1461
|
+
request,
|
|
1462
|
+
response,
|
|
1463
|
+
body,
|
|
1464
|
+
authorization
|
|
1465
|
+
)) || response.writeHead(404).end();
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
function requireMethod2(request, response, method) {
|
|
1469
|
+
return request.method === method ? Effect9.void : Effect9.sync(
|
|
1470
|
+
() => response.writeHead(405, { allow: method }).end()
|
|
1471
|
+
).pipe(Effect9.andThen(Effect9.interrupt));
|
|
1472
|
+
}
|
|
1473
|
+
function requireEmptyBody2(body) {
|
|
1474
|
+
return body.byteLength === 0 ? Effect9.void : Effect9.fail(
|
|
1475
|
+
new EnvironmentHttpBodyError({
|
|
1476
|
+
failure: { _tag: "InvalidMessage" }
|
|
1477
|
+
})
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
function writeEnvironmentHttpError(response, error) {
|
|
1481
|
+
if (!response.writableEnded) {
|
|
1482
|
+
if (error._tag === "EnvironmentAuthorizationError") {
|
|
1483
|
+
writeJson(
|
|
1484
|
+
response,
|
|
1485
|
+
authorizationFailureStatus(error.failure),
|
|
1486
|
+
EnvironmentAuthorizationFailure,
|
|
1487
|
+
error.failure
|
|
1488
|
+
);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
if (error._tag === "EnvironmentHttpBodyError") {
|
|
1492
|
+
writeJson(
|
|
1493
|
+
response,
|
|
1494
|
+
error.failure._tag === "PayloadTooLarge" ? 413 : 400,
|
|
1495
|
+
EnvironmentHttpFailure,
|
|
1496
|
+
error.failure
|
|
1497
|
+
);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
response.writeHead(500).end();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-server.ts
|
|
1505
|
+
import { Cause as Cause2, Effect as Effect13 } from "effect";
|
|
1506
|
+
import { WebSocketServer } from "ws";
|
|
1507
|
+
|
|
1508
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-session.ts
|
|
1509
|
+
import { Effect as Effect12, FiberSet, Option, Queue as Queue2, Schema as Schema9 } from "effect";
|
|
1510
|
+
import { WebSocket as WebSocketState } from "ws";
|
|
1511
|
+
|
|
1512
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-error.contract.ts
|
|
1513
|
+
import { Data as Data5 } from "effect";
|
|
1514
|
+
var EnvironmentWebSocketSessionClosed = class extends Data5.TaggedError(
|
|
1515
|
+
"EnvironmentWebSocketSessionClosed"
|
|
1516
|
+
) {
|
|
1517
|
+
}, EnvironmentWebSocketSessionRejected = class extends Data5.TaggedError(
|
|
1518
|
+
"EnvironmentWebSocketSessionRejected"
|
|
1519
|
+
) {
|
|
1520
|
+
}, EnvironmentWebSocketWriteError = class extends Data5.TaggedError(
|
|
1521
|
+
"EnvironmentWebSocketWriteError"
|
|
1522
|
+
) {
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-inbox.ts
|
|
1526
|
+
import { Cause, Effect as Effect10, Queue } from "effect";
|
|
1527
|
+
function acquireEnvironmentWebSocketInbox(socket, state) {
|
|
1528
|
+
return Effect10.gen(function* () {
|
|
1529
|
+
let messages = yield* Queue.bounded(state.discovery.limits.maxQueuedEvents), handlers = createInboxHandlers(socket, messages);
|
|
1530
|
+
return yield* Effect10.acquireRelease(
|
|
1531
|
+
Effect10.sync(() => attachInboxHandlers(socket, handlers)),
|
|
1532
|
+
() => Effect10.sync(() => detachInboxHandlers(socket, handlers)).pipe(
|
|
1533
|
+
Effect10.andThen(Queue.shutdown(messages))
|
|
1534
|
+
)
|
|
1535
|
+
), messages;
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
function createInboxHandlers(socket, messages) {
|
|
1539
|
+
let close = () => {
|
|
1540
|
+
Queue.failCauseUnsafe(
|
|
1541
|
+
messages,
|
|
1542
|
+
Cause.fail(new EnvironmentWebSocketSessionClosed())
|
|
1543
|
+
);
|
|
1544
|
+
};
|
|
1545
|
+
return {
|
|
1546
|
+
close,
|
|
1547
|
+
receive: (data, isBinary) => {
|
|
1548
|
+
Queue.offerUnsafe(messages, { data, isBinary }) || (socket.close(1013, "IncomingQueueOverflow"), close());
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function attachInboxHandlers(socket, handlers) {
|
|
1553
|
+
socket.on("message", handlers.receive), socket.on("close", handlers.close), socket.on("error", handlers.close);
|
|
1554
|
+
}
|
|
1555
|
+
function detachInboxHandlers(socket, handlers) {
|
|
1556
|
+
socket.off("message", handlers.receive), socket.off("close", handlers.close), socket.off("error", handlers.close);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-writer.ts
|
|
1560
|
+
import { Effect as Effect11, Ref, Schema as Schema8, Semaphore } from "effect";
|
|
1561
|
+
import { WebSocket } from "ws";
|
|
1562
|
+
|
|
1563
|
+
// src/apps/server/features/environment-connection/websocket/outgoing-message-queue.ts
|
|
1564
|
+
function createOutgoingMessageQueue() {
|
|
1565
|
+
return { messages: [], overflowed: !1, queuedBytes: 0 };
|
|
1566
|
+
}
|
|
1567
|
+
function enqueueOutgoingMessage(queue, value, overflowMessage, limits) {
|
|
1568
|
+
if (queue.overflowed)
|
|
1569
|
+
return queue;
|
|
1570
|
+
let message = queuedMessage(value), overCount = queue.messages.length + 1 > limits.maxQueuedEvents, overBytes = queue.queuedBytes + message.byteLength > limits.maxQueuedEventBytes;
|
|
1571
|
+
if (overCount || overBytes) {
|
|
1572
|
+
let overflow = queuedMessage(overflowMessage);
|
|
1573
|
+
return {
|
|
1574
|
+
messages: [overflow],
|
|
1575
|
+
overflowed: !0,
|
|
1576
|
+
queuedBytes: overflow.byteLength
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
messages: [...queue.messages, message],
|
|
1581
|
+
overflowed: !1,
|
|
1582
|
+
queuedBytes: queue.queuedBytes + message.byteLength
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
function dequeueOutgoingMessage(queue) {
|
|
1586
|
+
let [message, ...remaining] = queue.messages;
|
|
1587
|
+
return message === void 0 ? { message: void 0, queue } : {
|
|
1588
|
+
message: message.value,
|
|
1589
|
+
queue: {
|
|
1590
|
+
messages: remaining,
|
|
1591
|
+
overflowed: queue.overflowed,
|
|
1592
|
+
queuedBytes: queue.queuedBytes - message.byteLength
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
function resetOutgoingMessageQueue() {
|
|
1597
|
+
return createOutgoingMessageQueue();
|
|
1598
|
+
}
|
|
1599
|
+
function replaceWithResnapshotMessage(queue, value) {
|
|
1600
|
+
let message = queuedMessage(value);
|
|
1601
|
+
return {
|
|
1602
|
+
messages: [message],
|
|
1603
|
+
overflowed: queue.overflowed,
|
|
1604
|
+
queuedBytes: message.byteLength
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function queuedMessage(value) {
|
|
1608
|
+
return { byteLength: Buffer.byteLength(value), value };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-writer.ts
|
|
1612
|
+
function createEnvironmentWebSocketWriter(socket, state) {
|
|
1613
|
+
return Effect11.gen(function* () {
|
|
1614
|
+
let writerState = yield* Ref.make({
|
|
1615
|
+
limits: state.discovery.limits,
|
|
1616
|
+
queue: createOutgoingMessageQueue(),
|
|
1617
|
+
requiredSnapshotSequence: void 0,
|
|
1618
|
+
supportsResnapshot: !1
|
|
1619
|
+
}), sendPermit = yield* Semaphore.make(1), flush = flushOutgoingMessages(socket, writerState, sendPermit), enqueue = (message) => enqueueServerMessage(writerState, state, message);
|
|
1620
|
+
return {
|
|
1621
|
+
acknowledgeSnapshot: (sequence) => acknowledgeSnapshot(writerState, state, sendPermit, sequence),
|
|
1622
|
+
enqueue,
|
|
1623
|
+
flush,
|
|
1624
|
+
send: (message) => sendServerMessage(enqueue, flush, message),
|
|
1625
|
+
setNegotiatedContract: (limits, supportsResnapshot) => setNegotiatedContract(writerState, limits, supportsResnapshot)
|
|
1626
|
+
};
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
function flushOutgoingMessages(socket, writerState, sendPermit) {
|
|
1630
|
+
return sendPermit.withPermit(
|
|
1631
|
+
Effect11.gen(function* () {
|
|
1632
|
+
for (; ; ) {
|
|
1633
|
+
if (socket.readyState !== WebSocket.OPEN)
|
|
1634
|
+
return yield* Effect11.fail(
|
|
1635
|
+
new EnvironmentWebSocketWriteError({
|
|
1636
|
+
closeCode: 1011,
|
|
1637
|
+
reason: "WebSocketNotOpen"
|
|
1638
|
+
})
|
|
1639
|
+
);
|
|
1640
|
+
let next = yield* dequeueServerMessage(writerState);
|
|
1641
|
+
if (next === void 0)
|
|
1642
|
+
return;
|
|
1643
|
+
yield* sendWebSocketMessage(socket, next);
|
|
1644
|
+
}
|
|
1645
|
+
})
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
function dequeueServerMessage(writerState) {
|
|
1649
|
+
return Ref.modify(writerState, (current) => {
|
|
1650
|
+
let dequeued = dequeueOutgoingMessage(current.queue);
|
|
1651
|
+
return [dequeued.message, { ...current, queue: dequeued.queue }];
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
function enqueueServerMessage(writerState, transportState, message) {
|
|
1655
|
+
return Effect11.gen(function* () {
|
|
1656
|
+
let result = yield* Ref.modify(
|
|
1657
|
+
writerState,
|
|
1658
|
+
(current) => enqueueMessage(current, message, transportState.events.currentSequence())
|
|
1659
|
+
);
|
|
1660
|
+
return result._tag === "Rejected" ? yield* result.error : result._tag === "Enqueued";
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
function sendServerMessage(enqueue, flush, message) {
|
|
1664
|
+
return Effect11.gen(function* () {
|
|
1665
|
+
(yield* enqueue(message)) && (yield* flush);
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
function acknowledgeSnapshot(writerState, transportState, sendPermit, sequence) {
|
|
1669
|
+
return sendPermit.withPermit(
|
|
1670
|
+
Ref.modify(writerState, (current) => current.requiredSnapshotSequence !== sequence || sequence !== transportState.events.currentSequence() || current.queue.messages.length > 0 ? [!1, current] : [
|
|
1671
|
+
!0,
|
|
1672
|
+
{
|
|
1673
|
+
...current,
|
|
1674
|
+
queue: resetOutgoingMessageQueue(),
|
|
1675
|
+
requiredSnapshotSequence: void 0
|
|
1676
|
+
}
|
|
1677
|
+
])
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
function setNegotiatedContract(writerState, limits, supportsResnapshot) {
|
|
1681
|
+
return Ref.update(writerState, (current) => ({
|
|
1682
|
+
...current,
|
|
1683
|
+
limits,
|
|
1684
|
+
supportsResnapshot
|
|
1685
|
+
}));
|
|
1686
|
+
}
|
|
1687
|
+
function enqueueMessage(state, message, currentSequence) {
|
|
1688
|
+
if (message._tag === "EnvironmentChanged" && state.requiredSnapshotSequence !== void 0)
|
|
1689
|
+
return [
|
|
1690
|
+
{ _tag: "Ignored" },
|
|
1691
|
+
{
|
|
1692
|
+
...state,
|
|
1693
|
+
requiredSnapshotSequence: Math.max(
|
|
1694
|
+
state.requiredSnapshotSequence,
|
|
1695
|
+
message.sequence
|
|
1696
|
+
)
|
|
1697
|
+
}
|
|
1698
|
+
];
|
|
1699
|
+
let encoded = encodeMessage(message);
|
|
1700
|
+
if (Buffer.byteLength(encoded) > state.limits.maxWebSocketResponseBytes)
|
|
1701
|
+
return [
|
|
1702
|
+
{
|
|
1703
|
+
_tag: "Rejected",
|
|
1704
|
+
error: new EnvironmentWebSocketWriteError({
|
|
1705
|
+
closeCode: 1009,
|
|
1706
|
+
reason: "PayloadTooLarge"
|
|
1707
|
+
})
|
|
1708
|
+
},
|
|
1709
|
+
state
|
|
1710
|
+
];
|
|
1711
|
+
if (message._tag === "ResnapshotRequired" && state.queue.overflowed)
|
|
1712
|
+
return [
|
|
1713
|
+
{ _tag: "Enqueued" },
|
|
1714
|
+
{
|
|
1715
|
+
...state,
|
|
1716
|
+
queue: replaceWithResnapshotMessage(state.queue, encoded),
|
|
1717
|
+
requiredSnapshotSequence: message.currentSequence
|
|
1718
|
+
}
|
|
1719
|
+
];
|
|
1720
|
+
let overflow = encodeMessage({
|
|
1721
|
+
_tag: "ResnapshotRequired",
|
|
1722
|
+
currentSequence,
|
|
1723
|
+
reason: "OutgoingQueueOverflow"
|
|
1724
|
+
}), queue = enqueueOutgoingMessage(
|
|
1725
|
+
state.queue,
|
|
1726
|
+
encoded,
|
|
1727
|
+
overflow,
|
|
1728
|
+
state.limits
|
|
1729
|
+
), overflowed = !state.queue.overflowed && queue.overflowed;
|
|
1730
|
+
return overflowed && !state.supportsResnapshot ? [
|
|
1731
|
+
{
|
|
1732
|
+
_tag: "Rejected",
|
|
1733
|
+
error: new EnvironmentWebSocketWriteError({
|
|
1734
|
+
closeCode: 1013,
|
|
1735
|
+
reason: "OutgoingQueueOverflow"
|
|
1736
|
+
})
|
|
1737
|
+
},
|
|
1738
|
+
{ ...state, queue }
|
|
1739
|
+
] : [
|
|
1740
|
+
{ _tag: "Enqueued" },
|
|
1741
|
+
{
|
|
1742
|
+
...state,
|
|
1743
|
+
queue,
|
|
1744
|
+
requiredSnapshotSequence: message._tag === "ResnapshotRequired" ? message.currentSequence : overflowed ? currentSequence : state.requiredSnapshotSequence
|
|
1745
|
+
}
|
|
1746
|
+
];
|
|
1747
|
+
}
|
|
1748
|
+
function sendWebSocketMessage(socket, message) {
|
|
1749
|
+
return Effect11.callback((resume) => {
|
|
1750
|
+
socket.send(message, (error) => {
|
|
1751
|
+
resume(
|
|
1752
|
+
error == null ? Effect11.void : Effect11.fail(
|
|
1753
|
+
new EnvironmentWebSocketWriteError({
|
|
1754
|
+
closeCode: 1011,
|
|
1755
|
+
reason: "WebSocketWriteFailed"
|
|
1756
|
+
})
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
});
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
function encodeMessage(message) {
|
|
1763
|
+
return JSON.stringify(
|
|
1764
|
+
Schema8.encodeSync(EnvironmentServerMessage)(message)
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-session.ts
|
|
1769
|
+
function runEnvironmentWebSocketSession(socket, state) {
|
|
1770
|
+
return runSession(socket, state).pipe(
|
|
1771
|
+
Effect12.catchTag("EnvironmentWebSocketSessionClosed", () => Effect12.void),
|
|
1772
|
+
Effect12.catchTag(
|
|
1773
|
+
"EnvironmentWebSocketSessionRejected",
|
|
1774
|
+
({ result }) => rejectAndClose(socket, result)
|
|
1775
|
+
),
|
|
1776
|
+
Effect12.catchTag(
|
|
1777
|
+
"EnvironmentWebSocketWriteError",
|
|
1778
|
+
(error) => closeForWriteError(socket, error)
|
|
1779
|
+
),
|
|
1780
|
+
Effect12.scoped
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
function runSession(socket, state) {
|
|
1784
|
+
return Effect12.gen(function* () {
|
|
1785
|
+
let messages = yield* acquireEnvironmentWebSocketInbox(socket, state), writer = yield* createEnvironmentWebSocketWriter(socket, state), runSessionEffect = yield* FiberSet.makeRuntime(), hello = yield* readHello(messages, state), result = yield* negotiateSession(state, hello), supportsResnapshot = yield* initializeNegotiatedSession(
|
|
1786
|
+
socket,
|
|
1787
|
+
state,
|
|
1788
|
+
runSessionEffect,
|
|
1789
|
+
writer,
|
|
1790
|
+
hello,
|
|
1791
|
+
result
|
|
1792
|
+
);
|
|
1793
|
+
yield* processClientMessages(messages, writer, state, supportsResnapshot);
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
function negotiateSession(state, hello) {
|
|
1797
|
+
let result = negotiateEnvironmentHello(
|
|
1798
|
+
state.discovery,
|
|
1799
|
+
hello,
|
|
1800
|
+
state.events.currentSequence()
|
|
1801
|
+
);
|
|
1802
|
+
return result._tag === "HelloRejected" ? Effect12.fail(new EnvironmentWebSocketSessionRejected({ result })) : Effect12.succeed(result);
|
|
1803
|
+
}
|
|
1804
|
+
function initializeNegotiatedSession(socket, state, runSessionEffect, writer, hello, result) {
|
|
1805
|
+
return Effect12.gen(function* () {
|
|
1806
|
+
let capabilities = new Set(
|
|
1807
|
+
result.capabilities.map((capability) => capability.name)
|
|
1808
|
+
), supportsEnvironmentEvents = capabilities.has("environment-events"), supportsResnapshot = capabilities.has("sequence-resnapshot");
|
|
1809
|
+
return yield* writer.setNegotiatedContract(result.limits, supportsResnapshot), yield* writer.enqueue(result), supportsEnvironmentEvents && (yield* acquireEventSubscription(
|
|
1810
|
+
socket,
|
|
1811
|
+
state,
|
|
1812
|
+
runSessionEffect,
|
|
1813
|
+
writer.send
|
|
1814
|
+
)), yield* enqueueInitialResnapshot(writer.enqueue, hello, result, state), yield* writer.flush, supportsResnapshot;
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
function processClientMessages(messages, writer, state, supportsResnapshot) {
|
|
1818
|
+
return Effect12.gen(function* () {
|
|
1819
|
+
for (; ; ) {
|
|
1820
|
+
let message = decodeSocketMessage(yield* Queue2.take(messages));
|
|
1821
|
+
yield* handleClientMessage(message, writer, state, supportsResnapshot);
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
function handleClientMessage(message, writer, state, supportsResnapshot) {
|
|
1826
|
+
return message === void 0 ? rejectSession("InvalidMessage") : message._tag === "Hello" ? rejectSession("HandshakeAlreadyCompleted") : supportsResnapshot ? Effect12.gen(function* () {
|
|
1827
|
+
(yield* writer.acknowledgeSnapshot(message.sequence)) || (yield* writer.send({
|
|
1828
|
+
_tag: "ResnapshotRequired",
|
|
1829
|
+
currentSequence: state.events.currentSequence(),
|
|
1830
|
+
reason: "SequenceGap"
|
|
1831
|
+
}));
|
|
1832
|
+
}) : rejectSession("InvalidMessage");
|
|
1833
|
+
}
|
|
1834
|
+
function readHello(messages, state) {
|
|
1835
|
+
return Effect12.gen(function* () {
|
|
1836
|
+
let first = yield* Queue2.take(messages).pipe(
|
|
1837
|
+
Effect12.timeoutOption(state.discovery.limits.helloTimeoutMilliseconds)
|
|
1838
|
+
);
|
|
1839
|
+
if (Option.isNone(first))
|
|
1840
|
+
return yield* rejectSession("HandshakeRequired");
|
|
1841
|
+
let message = decodeSocketMessage(first.value);
|
|
1842
|
+
return message === void 0 ? yield* rejectSession("InvalidMessage") : message._tag !== "Hello" ? yield* rejectSession("HandshakeRequired") : message;
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
function acquireEventSubscription(socket, state, runSessionEffect, send) {
|
|
1846
|
+
return Effect12.acquireRelease(
|
|
1847
|
+
Effect12.sync(
|
|
1848
|
+
() => state.events.subscribe((sequence) => {
|
|
1849
|
+
runSessionEffect(
|
|
1850
|
+
send({ _tag: "EnvironmentChanged", sequence }).pipe(
|
|
1851
|
+
Effect12.catchTag(
|
|
1852
|
+
"EnvironmentWebSocketWriteError",
|
|
1853
|
+
(error) => closeForWriteError(socket, error)
|
|
1854
|
+
)
|
|
1855
|
+
)
|
|
1856
|
+
);
|
|
1857
|
+
})
|
|
1858
|
+
),
|
|
1859
|
+
(unsubscribe) => Effect12.sync(unsubscribe)
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
function enqueueInitialResnapshot(enqueue, hello, result, state) {
|
|
1863
|
+
return !result.capabilities.some(
|
|
1864
|
+
(capability) => capability.name === "sequence-resnapshot"
|
|
1865
|
+
) || (hello.lastObservedSequence === void 0 || hello.lastObservedSequence === result.currentSequence) && result.currentSequence === state.events.currentSequence() ? Effect12.void : enqueue({
|
|
1866
|
+
_tag: "ResnapshotRequired",
|
|
1867
|
+
currentSequence: state.events.currentSequence(),
|
|
1868
|
+
reason: "SequenceGap"
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
function decodeSocketMessage(message) {
|
|
1872
|
+
if (!message.isBinary)
|
|
1873
|
+
try {
|
|
1874
|
+
return Schema9.decodeUnknownSync(EnvironmentClientMessage)(
|
|
1875
|
+
JSON.parse(message.data.toString()),
|
|
1876
|
+
{ onExcessProperty: "error" }
|
|
1877
|
+
);
|
|
1878
|
+
} catch {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function rejectSession(failure) {
|
|
1883
|
+
return Effect12.fail(
|
|
1884
|
+
new EnvironmentWebSocketSessionRejected({
|
|
1885
|
+
result: { _tag: "HelloRejected", failure: sessionFailure(failure) }
|
|
1886
|
+
})
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
function sessionFailure(failure) {
|
|
1890
|
+
switch (failure) {
|
|
1891
|
+
case "HandshakeAlreadyCompleted":
|
|
1892
|
+
return { _tag: "HandshakeAlreadyCompleted" };
|
|
1893
|
+
case "HandshakeRequired":
|
|
1894
|
+
return { _tag: "HandshakeRequired" };
|
|
1895
|
+
case "InvalidMessage":
|
|
1896
|
+
return { _tag: "InvalidMessage" };
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
function rejectAndClose(socket, result) {
|
|
1900
|
+
if (result._tag !== "HelloRejected")
|
|
1901
|
+
return Effect12.void;
|
|
1902
|
+
let encoded = JSON.stringify(
|
|
1903
|
+
Schema9.encodeSync(EnvironmentHelloResult)(result)
|
|
1904
|
+
);
|
|
1905
|
+
return Effect12.callback((resume) => {
|
|
1906
|
+
if (socket.readyState !== WebSocketState.OPEN) {
|
|
1907
|
+
socket.terminate(), resume(Effect12.void);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
socket.send(encoded, (error) => {
|
|
1911
|
+
error != null ? socket.terminate() : socket.close(1008, result.failure._tag), resume(Effect12.void);
|
|
1912
|
+
});
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
function closeForWriteError(socket, error) {
|
|
1916
|
+
return Effect12.sync(() => {
|
|
1917
|
+
socket.readyState === WebSocketState.OPEN ? socket.close(error.closeCode, error.reason) : socket.terminate();
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/apps/server/features/environment-connection/websocket/environment-websocket-server.ts
|
|
1922
|
+
function attachEnvironmentWebSocketServer(server, state, authorization, runEnvironmentEffect) {
|
|
1923
|
+
let webSocketServer = new WebSocketServer({
|
|
1924
|
+
clientTracking: !0,
|
|
1925
|
+
maxPayload: currentTransportLimits.maxWebSocketRequestBytes,
|
|
1926
|
+
noServer: !0,
|
|
1927
|
+
perMessageDeflate: !1
|
|
1928
|
+
}), upgrade = (request, socket, head) => {
|
|
1929
|
+
let url = readUpgradeUrl(request);
|
|
1930
|
+
if (url?.pathname !== environmentLivePath) {
|
|
1931
|
+
socket.end(`HTTP/1.1 404 Not Found\r
|
|
1932
|
+
Connection: close\r
|
|
1933
|
+
\r
|
|
1934
|
+
`);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
runEnvironmentEffect(
|
|
1938
|
+
Effect13.gen(function* () {
|
|
1939
|
+
yield* validateRequestHost(request), yield* validateRequestOrigin(request, !1);
|
|
1940
|
+
let tickets = url.searchParams.getAll("ticket");
|
|
1941
|
+
yield* authorization.consumeTicket(
|
|
1942
|
+
tickets.length === 1 ? tickets[0] : void 0
|
|
1943
|
+
), yield* Effect13.sync(() => {
|
|
1944
|
+
webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
|
|
1945
|
+
webSocketServer.emit("connection", webSocket, request);
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
}).pipe(
|
|
1949
|
+
Effect13.catchCause(
|
|
1950
|
+
(cause) => Effect13.sync(() => rejectUpgrade(socket, Cause2.squash(cause)))
|
|
1951
|
+
)
|
|
1952
|
+
)
|
|
1953
|
+
);
|
|
1954
|
+
};
|
|
1955
|
+
return server.on("upgrade", upgrade), webSocketServer.on("connection", (socket) => {
|
|
1956
|
+
runEnvironmentEffect(runEnvironmentWebSocketSession(socket, state));
|
|
1957
|
+
}), {
|
|
1958
|
+
close: () => closeWebSocketServer(server, webSocketServer, upgrade)
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function readUpgradeUrl(request) {
|
|
1962
|
+
try {
|
|
1963
|
+
return new URL(request.url ?? "", expectedRequestOrigin(request));
|
|
1964
|
+
} catch {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function rejectUpgrade(socket, error) {
|
|
1969
|
+
let authorizationError = isEnvironmentAuthorizationError(error) ? error : void 0, status = authorizationError === void 0 ? 503 : authorizationFailureStatus(authorizationError.failure), body = JSON.stringify(
|
|
1970
|
+
authorizationError === void 0 ? { _tag: "EnvironmentUnavailable" } : authorizationError.failure
|
|
1971
|
+
);
|
|
1972
|
+
socket.end(
|
|
1973
|
+
`HTTP/1.1 ${status} ${statusText(status)}\r
|
|
1974
|
+
Content-Type: application/json; charset=utf-8\r
|
|
1975
|
+
Cache-Control: no-store\r
|
|
1976
|
+
Content-Length: ${Buffer.byteLength(body)}\r
|
|
1977
|
+
Connection: close\r
|
|
1978
|
+
\r
|
|
1979
|
+
${body}`
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
function statusText(status) {
|
|
1983
|
+
switch (status) {
|
|
1984
|
+
case 401:
|
|
1985
|
+
return "Unauthorized";
|
|
1986
|
+
case 403:
|
|
1987
|
+
return "Forbidden";
|
|
1988
|
+
case 409:
|
|
1989
|
+
return "Conflict";
|
|
1990
|
+
case 410:
|
|
1991
|
+
return "Gone";
|
|
1992
|
+
default:
|
|
1993
|
+
return "Service Unavailable";
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function closeWebSocketServer(server, webSocketServer, upgrade) {
|
|
1997
|
+
server.off("upgrade", upgrade);
|
|
1998
|
+
for (let client of webSocketServer.clients)
|
|
1999
|
+
client.terminate();
|
|
2000
|
+
return new Promise((resolveClosed, rejectClosed) => {
|
|
2001
|
+
webSocketServer.close((error) => {
|
|
2002
|
+
error !== void 0 ? rejectClosed(error) : resolveClosed();
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// src/apps/server/features/environment-server/server/environment-server-error.contract.ts
|
|
2008
|
+
import { Data as Data6 } from "effect";
|
|
2009
|
+
var EnvironmentServerStartError = class extends Data6.TaggedError(
|
|
2010
|
+
"EnvironmentServerStartError"
|
|
2011
|
+
) {
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
// src/apps/server/features/environment-server/server/environment-listener.ts
|
|
2015
|
+
var loopbackHost = "127.0.0.1";
|
|
2016
|
+
function acquireEnvironmentListener(options) {
|
|
2017
|
+
return Effect14.gen(function* () {
|
|
2018
|
+
let port = options.port ?? 0, readiness = { value: !1 }, state = {
|
|
2019
|
+
discovery: createCurrentEnvironmentDiscovery(
|
|
2020
|
+
options.environmentId,
|
|
2021
|
+
options.productVersion
|
|
2022
|
+
),
|
|
2023
|
+
events: options.events
|
|
2024
|
+
}, runFork = yield* FiberSet2.makeRuntime(), runEnvironmentEffect = (effect, signal) => {
|
|
2025
|
+
runFork(effect, signal === void 0 ? void 0 : { signal });
|
|
2026
|
+
}, server = yield* Effect14.acquireRelease(
|
|
2027
|
+
createHttpServer(
|
|
2028
|
+
readiness,
|
|
2029
|
+
state,
|
|
2030
|
+
options.authorization,
|
|
2031
|
+
port,
|
|
2032
|
+
runEnvironmentEffect,
|
|
2033
|
+
options.browserAssetsRoot
|
|
2034
|
+
),
|
|
2035
|
+
(acquiredServer) => Effect14.promise(() => closeServer(acquiredServer)).pipe(Effect14.orDie)
|
|
2036
|
+
);
|
|
2037
|
+
yield* Effect14.acquireRelease(
|
|
2038
|
+
Effect14.try({
|
|
2039
|
+
try: () => attachEnvironmentWebSocketServer(
|
|
2040
|
+
server,
|
|
2041
|
+
state,
|
|
2042
|
+
options.authorization,
|
|
2043
|
+
runEnvironmentEffect
|
|
2044
|
+
),
|
|
2045
|
+
catch: (cause) => environmentServerError(cause, port)
|
|
2046
|
+
}),
|
|
2047
|
+
(webSockets) => Effect14.promise(webSockets.close).pipe(Effect14.orDie)
|
|
2048
|
+
), yield* listen(server, port);
|
|
2049
|
+
let listeningPort = yield* readListeningPort(server, port);
|
|
2050
|
+
return {
|
|
2051
|
+
host: loopbackHost,
|
|
2052
|
+
origin: `http://${loopbackHost}:${listeningPort}`,
|
|
2053
|
+
port: listeningPort,
|
|
2054
|
+
readiness,
|
|
2055
|
+
server
|
|
2056
|
+
};
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
function createHttpServer(readiness, state, authorization, port, runEnvironmentEffect, browserAssetsRoot) {
|
|
2060
|
+
return Effect14.try({
|
|
2061
|
+
try: () => createServer(
|
|
2062
|
+
{ maxHeaderSize: 16384 },
|
|
2063
|
+
createEnvironmentHttpHandler(
|
|
2064
|
+
state,
|
|
2065
|
+
authorization,
|
|
2066
|
+
() => readiness.value,
|
|
2067
|
+
runEnvironmentEffect,
|
|
2068
|
+
browserAssetsRoot
|
|
2069
|
+
)
|
|
2070
|
+
),
|
|
2071
|
+
catch: (cause) => environmentServerError(cause, port)
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
function readListeningPort(server, requestedPort) {
|
|
2075
|
+
return Effect14.try({
|
|
2076
|
+
try: () => {
|
|
2077
|
+
let address = server.address();
|
|
2078
|
+
if (address === null || typeof address == "string")
|
|
2079
|
+
throw new Error("The HTTP listener has no TCP address.");
|
|
2080
|
+
return address.port;
|
|
2081
|
+
},
|
|
2082
|
+
catch: (cause) => environmentServerError(cause, requestedPort)
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
function listen(server, port) {
|
|
2086
|
+
return Effect14.callback(
|
|
2087
|
+
(resume, signal) => {
|
|
2088
|
+
let failed = (cause) => {
|
|
2089
|
+
detach(), resume(Effect14.fail(environmentServerError(cause, port)));
|
|
2090
|
+
}, listening = () => {
|
|
2091
|
+
detach(), resume(Effect14.void);
|
|
2092
|
+
}, detach = () => {
|
|
2093
|
+
server.off("error", failed), server.off("listening", listening);
|
|
2094
|
+
};
|
|
2095
|
+
server.once("error", failed), server.once("listening", listening);
|
|
2096
|
+
try {
|
|
2097
|
+
server.listen({ exclusive: !0, host: loopbackHost, port, signal });
|
|
2098
|
+
} catch (cause) {
|
|
2099
|
+
failed(cause);
|
|
2100
|
+
}
|
|
2101
|
+
return Effect14.sync(detach);
|
|
2102
|
+
}
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
function closeServer(server) {
|
|
2106
|
+
return new Promise((resolveClosed, rejectClosed) => {
|
|
2107
|
+
server.close((error) => {
|
|
2108
|
+
error && !isServerNotRunning(error) ? rejectClosed(error) : resolveClosed();
|
|
2109
|
+
}), server.closeAllConnections();
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
function isServerNotRunning(error) {
|
|
2113
|
+
return isFileSystemError(error) && error.code === "ERR_SERVER_NOT_RUNNING";
|
|
2114
|
+
}
|
|
2115
|
+
function environmentServerError(cause, port) {
|
|
2116
|
+
return new EnvironmentServerStartError({
|
|
2117
|
+
cause,
|
|
2118
|
+
message: listenerErrorMessage(cause, port)
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
function listenerErrorMessage(cause, port) {
|
|
2122
|
+
return isFileSystemError(cause) && cause.code === "EADDRINUSE" && port !== 0 ? `Port ${port} is already in use on ${loopbackHost}.` : `Could not start the Environment server: ${errorMessage(cause)}`;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/apps/server/persistence/environment-context.ts
|
|
2126
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2127
|
+
import { eq as eq3 } from "drizzle-orm";
|
|
2128
|
+
import { drizzle as drizzle2 } from "drizzle-orm/node-sqlite";
|
|
2129
|
+
import { Effect as Effect18, Semaphore as Semaphore2 } from "effect";
|
|
2130
|
+
|
|
2131
|
+
// src/apps/server/persistence/sqlite/database.ts
|
|
2132
|
+
import { chmodSync } from "node:fs";
|
|
2133
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2134
|
+
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
2135
|
+
import { Effect as Effect16, Schedule } from "effect";
|
|
2136
|
+
|
|
2137
|
+
// src/apps/server/persistence/sqlite/migrations.ts
|
|
2138
|
+
import { existsSync } from "node:fs";
|
|
2139
|
+
import { fileURLToPath } from "node:url";
|
|
2140
|
+
import { readMigrationFiles } from "drizzle-orm/migrator";
|
|
2141
|
+
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
2142
|
+
var migrationsFolder = resolveMigrationsFolder(), migrationsTable = "__drizzle_migrations";
|
|
2143
|
+
function migrateEnvironmentState(database) {
|
|
2144
|
+
let localMigrations = readMigrationFiles({ migrationsFolder }), appliedMigrations = readAppliedMigrations(database.$client);
|
|
2145
|
+
if (validateMigrationHistory(localMigrations, appliedMigrations), appliedMigrations.length !== localMigrations.length)
|
|
2146
|
+
try {
|
|
2147
|
+
migrate(database, { migrationsFolder, migrationsTable });
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
let migrationsAfterFailure = readAppliedMigrations(database.$client);
|
|
2150
|
+
if (validateMigrationHistory(localMigrations, migrationsAfterFailure), migrationsAfterFailure.length !== localMigrations.length) throw error;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
function readAppliedMigrations(database) {
|
|
2154
|
+
return database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(migrationsTable) === void 0 ? [] : database.prepare(`SELECT id, name, hash FROM ${migrationsTable} ORDER BY id`).all().map(parseAppliedMigration);
|
|
2155
|
+
}
|
|
2156
|
+
function validateMigrationHistory(localMigrations, appliedMigrations) {
|
|
2157
|
+
for (let [index2, applied] of appliedMigrations.entries()) {
|
|
2158
|
+
let expected = localMigrations[index2];
|
|
2159
|
+
if (expected === void 0)
|
|
2160
|
+
throw new Error(
|
|
2161
|
+
`The state database is at version ${applied.id}, but this Rebase build supports version ${localMigrations.length}.`
|
|
2162
|
+
);
|
|
2163
|
+
if (applied.id !== index2 + 1 || applied.name !== expected.name)
|
|
2164
|
+
throw new Error(
|
|
2165
|
+
`Migration ${applied.id} "${applied.name}" does not match the expected migration "${expected.name}".`
|
|
2166
|
+
);
|
|
2167
|
+
if (applied.hash !== expected.hash)
|
|
2168
|
+
throw new Error(
|
|
2169
|
+
`Migration ${applied.id} "${applied.name}" no longer matches its applied checksum.`
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
function parseAppliedMigration(row) {
|
|
2174
|
+
if (typeof row.id != "number" || typeof row.name != "string" || typeof row.hash != "string")
|
|
2175
|
+
throw new Error("The migration history contains an invalid record.");
|
|
2176
|
+
return {
|
|
2177
|
+
hash: row.hash,
|
|
2178
|
+
id: row.id,
|
|
2179
|
+
name: row.name
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
function resolveMigrationsFolder() {
|
|
2183
|
+
let folder = [
|
|
2184
|
+
new URL("./migrations", import.meta.url),
|
|
2185
|
+
new URL("../migrations", import.meta.url)
|
|
2186
|
+
].find((candidate) => existsSync(candidate));
|
|
2187
|
+
if (folder === void 0)
|
|
2188
|
+
throw new Error("Rebase database migrations are missing.");
|
|
2189
|
+
return fileURLToPath(folder);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// src/apps/server/persistence/sqlite/storage-operation.ts
|
|
2193
|
+
import { Effect as Effect15 } from "effect";
|
|
2194
|
+
function serializedPromise(writer, message, operation) {
|
|
2195
|
+
return writer.withPermit(storagePromise(message, operation));
|
|
2196
|
+
}
|
|
2197
|
+
function storagePromise(message, operation) {
|
|
2198
|
+
return Effect15.tryPromise({
|
|
2199
|
+
try: async () => operation(),
|
|
2200
|
+
catch: (cause) => storageError(message, cause)
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
function storageSync(message, operation) {
|
|
2204
|
+
return Effect15.try({
|
|
2205
|
+
try: operation,
|
|
2206
|
+
catch: (cause) => storageError(message, cause)
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
function storageError(message, cause) {
|
|
2210
|
+
return new EnvironmentStorageError({
|
|
2211
|
+
cause,
|
|
2212
|
+
message: `${message}: ${errorMessage(cause)}`
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/apps/server/persistence/sqlite/database.ts
|
|
2217
|
+
var busyTimeoutMilliseconds = 1e3, databaseLockRetryDelay = "10 millis", databaseLockRetryCount = 10;
|
|
2218
|
+
function openEnvironmentDatabase(paths) {
|
|
2219
|
+
return Effect16.gen(function* () {
|
|
2220
|
+
let database = yield* storageSync(
|
|
2221
|
+
"Could not open Environment state",
|
|
2222
|
+
() => new DatabaseSync(paths.stateDatabase, {
|
|
2223
|
+
allowUnknownNamedParameters: !1,
|
|
2224
|
+
enableForeignKeyConstraints: !0,
|
|
2225
|
+
timeout: busyTimeoutMilliseconds
|
|
2226
|
+
})
|
|
2227
|
+
);
|
|
2228
|
+
return yield* Effect16.gen(function* () {
|
|
2229
|
+
return yield* storageSync(
|
|
2230
|
+
"Could not open Environment state",
|
|
2231
|
+
() => configureDatabase(database)
|
|
2232
|
+
).pipe(retryDatabaseLock), yield* migrateDatabase(database), yield* storageSync(
|
|
2233
|
+
"Could not open Environment state",
|
|
2234
|
+
() => restrictDatabasePermissions(paths.stateDatabase)
|
|
2235
|
+
), database;
|
|
2236
|
+
}).pipe(Effect16.onError(() => closeEnvironmentDatabase(database)));
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
function closeEnvironmentDatabase(database) {
|
|
2240
|
+
return Effect16.sync(() => database.close()).pipe(Effect16.orDie);
|
|
2241
|
+
}
|
|
2242
|
+
function readDatabaseSettings(database) {
|
|
2243
|
+
let foreignKeys = database.prepare("PRAGMA foreign_keys").get();
|
|
2244
|
+
return {
|
|
2245
|
+
busyTimeout: database.prepare("PRAGMA busy_timeout").get().timeout,
|
|
2246
|
+
foreignKeys: foreignKeys.foreign_keys === 1,
|
|
2247
|
+
journalMode: readJournalMode(database)
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function configureDatabase(database) {
|
|
2251
|
+
database.exec(`
|
|
2252
|
+
PRAGMA busy_timeout = ${busyTimeoutMilliseconds};
|
|
2253
|
+
PRAGMA synchronous = NORMAL;
|
|
2254
|
+
`), enableWriteAheadLogging(database);
|
|
2255
|
+
}
|
|
2256
|
+
function migrateDatabase(database) {
|
|
2257
|
+
return storageSync(
|
|
2258
|
+
"Could not open Environment state",
|
|
2259
|
+
() => migrateEnvironmentState(drizzle({ client: database }))
|
|
2260
|
+
).pipe(retryDatabaseLock);
|
|
2261
|
+
}
|
|
2262
|
+
function retryDatabaseLock(effect) {
|
|
2263
|
+
return effect.pipe(
|
|
2264
|
+
Effect16.retry({
|
|
2265
|
+
schedule: Schedule.spaced(databaseLockRetryDelay),
|
|
2266
|
+
times: databaseLockRetryCount,
|
|
2267
|
+
while: isDatabaseLocked
|
|
2268
|
+
})
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
function isDatabaseLocked(error) {
|
|
2272
|
+
return error.message.includes("database is locked");
|
|
2273
|
+
}
|
|
2274
|
+
function enableWriteAheadLogging(database) {
|
|
2275
|
+
try {
|
|
2276
|
+
database.exec("PRAGMA journal_mode = WAL;");
|
|
2277
|
+
} catch (error) {
|
|
2278
|
+
if (readJournalMode(database) !== "wal") throw error;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
function readJournalMode(database) {
|
|
2282
|
+
return database.prepare("PRAGMA journal_mode").get().journal_mode;
|
|
2283
|
+
}
|
|
2284
|
+
function restrictDatabasePermissions(path) {
|
|
2285
|
+
process.platform !== "win32" && chmodSync(path, 384);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/apps/server/persistence/storage/server-secret.ts
|
|
2289
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2290
|
+
import { chmod as chmod2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
2291
|
+
import { setTimeout } from "node:timers/promises";
|
|
2292
|
+
import { Effect as Effect17 } from "effect";
|
|
2293
|
+
var secretCreationRetryCount = 10, secretCreationRetryDelayMilliseconds = 10;
|
|
2294
|
+
function ensureServerSecret(paths) {
|
|
2295
|
+
return Effect17.tryPromise({
|
|
2296
|
+
try: () => readOrCreateServerSecret(paths.serverSecret),
|
|
2297
|
+
catch: (cause) => new EnvironmentStorageError({
|
|
2298
|
+
cause,
|
|
2299
|
+
message: `Could not prepare the server secret: ${errorMessage(cause)}`
|
|
2300
|
+
})
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
async function readOrCreateServerSecret(path) {
|
|
2304
|
+
let secret;
|
|
2305
|
+
try {
|
|
2306
|
+
secret = await readFile3(path, "utf8");
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
if (!isMissingFile2(error))
|
|
2309
|
+
throw error;
|
|
2310
|
+
let generated = `${randomBytes2(32).toString("base64url")}
|
|
2311
|
+
`;
|
|
2312
|
+
try {
|
|
2313
|
+
await writeFile2(path, generated, { flag: "wx", mode: 384 }), secret = generated;
|
|
2314
|
+
} catch (writeError) {
|
|
2315
|
+
if (!isExistingFile(writeError))
|
|
2316
|
+
throw writeError;
|
|
2317
|
+
secret = await readSecretCreatedByAnotherProcess(path);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(secret.trim()))
|
|
2321
|
+
throw new Error("The server secret is invalid.");
|
|
2322
|
+
return process.platform !== "win32" && await chmod2(path, 384), secret.trim();
|
|
2323
|
+
}
|
|
2324
|
+
async function readSecretCreatedByAnotherProcess(path) {
|
|
2325
|
+
for (let attempt = 0; attempt <= secretCreationRetryCount; attempt += 1) {
|
|
2326
|
+
let secret = await readFile3(path, "utf8");
|
|
2327
|
+
if (/^[A-Za-z0-9_-]{43}$/.test(secret.trim()))
|
|
2328
|
+
return secret;
|
|
2329
|
+
if (attempt === secretCreationRetryCount)
|
|
2330
|
+
break;
|
|
2331
|
+
await setTimeout(secretCreationRetryDelayMilliseconds);
|
|
2332
|
+
}
|
|
2333
|
+
throw new Error("The server secret is invalid.");
|
|
2334
|
+
}
|
|
2335
|
+
function isMissingFile2(error) {
|
|
2336
|
+
return isFileSystemError(error) && error.code === "ENOENT";
|
|
2337
|
+
}
|
|
2338
|
+
function isExistingFile(error) {
|
|
2339
|
+
return isFileSystemError(error) && error.code === "EEXIST";
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/apps/server/persistence/environment-context.ts
|
|
2343
|
+
function acquireEnvironmentContext(paths = defaultEnvironmentPaths()) {
|
|
2344
|
+
return Effect18.gen(function* () {
|
|
2345
|
+
yield* prepareEnvironmentDirectories(paths);
|
|
2346
|
+
let serverSecret = yield* ensureServerSecret(paths), database = yield* Effect18.acquireRelease(
|
|
2347
|
+
openEnvironmentDatabase(paths),
|
|
2348
|
+
closeEnvironmentDatabase
|
|
2349
|
+
), context = yield* storageSync(
|
|
2350
|
+
"Could not read Environment state settings",
|
|
2351
|
+
() => createEnvironmentContext(database, serverSecret)
|
|
2352
|
+
);
|
|
2353
|
+
return yield* initializeEnvironment(context), context;
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
function createEnvironmentContext(database, serverSecret) {
|
|
2357
|
+
let drizzleDatabase = drizzle2({ client: database }), writer = Semaphore2.makeUnsafe(1);
|
|
2358
|
+
return {
|
|
2359
|
+
database: drizzleDatabase,
|
|
2360
|
+
databaseSettings: readDatabaseSettings(database),
|
|
2361
|
+
serverSecret,
|
|
2362
|
+
read: (message, operation) => storagePromise(message, () => operation(drizzleDatabase)),
|
|
2363
|
+
write: (message, operation) => serializedPromise(writer, message, () => operation(drizzleDatabase))
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
function initializeEnvironment(context) {
|
|
2367
|
+
return context.write(
|
|
2368
|
+
"Could not initialize Environment state",
|
|
2369
|
+
async (database) => {
|
|
2370
|
+
if (await database.insert(environmentTable).values({ id: randomUUID3(), singleton: 1 }).onConflictDoNothing({ target: environmentTable.singleton }), await database.select({ id: environmentTable.id }).from(environmentTable).where(eq3(environmentTable.singleton, 1)).get() === void 0)
|
|
2371
|
+
throw new Error("The Environment identity is missing.");
|
|
2372
|
+
}
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// src/apps/server/product-version.ts
|
|
2377
|
+
var productVersion = "0.0.2-nightly.20260822.35";
|
|
2378
|
+
|
|
2379
|
+
// src/apps/server/features/environment-server/server/start-environment-server.ts
|
|
2380
|
+
function startEnvironmentServer(options = {}) {
|
|
2381
|
+
return Effect19.gen(function* () {
|
|
2382
|
+
yield* verifyRuntimeRequirements;
|
|
2383
|
+
let paths = defaultEnvironmentPaths(), context = yield* acquireEnvironmentContext(paths), environment = yield* readCurrentEnvironment(context), authorization = createEnvironmentAuthorization(
|
|
2384
|
+
context,
|
|
2385
|
+
context.serverSecret
|
|
2386
|
+
), useAutomaticPort = options.port === void 0 || options.port === 0, requestedPort = useAutomaticPort ? environment.automaticPort ?? 0 : options.port, events = createEnvironmentEventPublisher(), listener = yield* acquireEnvironmentListener({
|
|
2387
|
+
authorization,
|
|
2388
|
+
...options.browserAssetsRoot === void 0 ? {} : { browserAssetsRoot: options.browserAssetsRoot },
|
|
2389
|
+
environmentId: environment.id,
|
|
2390
|
+
events,
|
|
2391
|
+
port: requestedPort,
|
|
2392
|
+
productVersion
|
|
2393
|
+
});
|
|
2394
|
+
useAutomaticPort && environment.automaticPort === null && (yield* claimAutomaticPort(context, listener.port)), yield* acquireRuntimeMarker(runtimeMarker(listener), paths.runtimeMarker);
|
|
2395
|
+
let pairing = yield* authorization.createPairing({
|
|
2396
|
+
capabilities: [],
|
|
2397
|
+
role: "owner"
|
|
2398
|
+
});
|
|
2399
|
+
return yield* markListenerReady(listener), {
|
|
2400
|
+
environmentId: environment.id,
|
|
2401
|
+
origin: listener.origin,
|
|
2402
|
+
pairingUrl: `${listener.origin}/pair#${pairing.material}`,
|
|
2403
|
+
port: listener.port
|
|
2404
|
+
};
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
function readCurrentEnvironment(context) {
|
|
2408
|
+
return context.read("Could not read Environment state", async (database) => {
|
|
2409
|
+
let environment = await database.select().from(environmentTable).where(isCurrentEnvironment()).get();
|
|
2410
|
+
if (environment === void 0)
|
|
2411
|
+
throw new Error("The Environment identity is missing.");
|
|
2412
|
+
return environment;
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
function claimAutomaticPort(context, port) {
|
|
2416
|
+
return context.write(
|
|
2417
|
+
"Could not save the automatic port",
|
|
2418
|
+
async (database) => {
|
|
2419
|
+
await database.update(environmentTable).set({ automaticPort: port }).where(and(isCurrentEnvironment(), hasNoAutomaticPort()));
|
|
2420
|
+
let selected = await database.select({ automaticPort: environmentTable.automaticPort }).from(environmentTable).where(isCurrentEnvironment()).get();
|
|
2421
|
+
if (selected?.automaticPort === null || selected === void 0)
|
|
2422
|
+
throw new Error("The automatic port was not saved.");
|
|
2423
|
+
if (selected.automaticPort !== port)
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
`Another server selected automatic port ${selected.automaticPort}.`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
function runtimeMarker(listener) {
|
|
2431
|
+
return {
|
|
2432
|
+
host: listener.host,
|
|
2433
|
+
origin: listener.origin,
|
|
2434
|
+
pid: process.pid,
|
|
2435
|
+
port: listener.port,
|
|
2436
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
function markListenerReady(listener) {
|
|
2440
|
+
return Effect19.gen(function* () {
|
|
2441
|
+
yield* Effect19.addFinalizer(
|
|
2442
|
+
() => Effect19.sync(() => {
|
|
2443
|
+
listener.readiness.value = !1;
|
|
2444
|
+
})
|
|
2445
|
+
), yield* Effect19.sync(() => {
|
|
2446
|
+
listener.readiness.value = !0;
|
|
2447
|
+
});
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// src/apps/server/cli.ts
|
|
2452
|
+
var usage = "Usage: rebase [serve] [--port <1-65535>]";
|
|
2453
|
+
function runCli(arguments_ = process.argv.slice(2)) {
|
|
2454
|
+
if (arguments_.includes("--help") || arguments_.includes("-h"))
|
|
2455
|
+
return writeOutput(usage).pipe(Effect20.as(0));
|
|
2456
|
+
if (arguments_.includes("--version") || arguments_.includes("-v"))
|
|
2457
|
+
return writeOutput(versionOutput()).pipe(Effect20.as(0));
|
|
2458
|
+
let program = Effect20.try({
|
|
2459
|
+
try: () => parseServeArguments(arguments_),
|
|
2460
|
+
catch: normalizeError
|
|
2461
|
+
}).pipe(Effect20.flatMap(serve));
|
|
2462
|
+
return Effect20.matchEffect(program, {
|
|
2463
|
+
onFailure: (error) => writeFailure(`rebase serve failed: ${error.message}`).pipe(Effect20.as(1)),
|
|
2464
|
+
onSuccess: () => Effect20.succeed(0)
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
async function main() {
|
|
2468
|
+
process.exitCode = await Effect20.runPromise(runCli());
|
|
2469
|
+
}
|
|
2470
|
+
function parseServeArguments(arguments_) {
|
|
2471
|
+
let normalizedArguments = arguments_.length === 0 || arguments_[0] === "--port" ? ["serve", ...arguments_] : arguments_, [command, ...options] = normalizedArguments;
|
|
2472
|
+
if (command !== "serve") throw new Error(usage);
|
|
2473
|
+
let browserAssetsRoot = resolveBrowserAssetsRoot();
|
|
2474
|
+
if (options.length === 0) return { browserAssetsRoot };
|
|
2475
|
+
if (options.length !== 2 || options[0] !== "--port" || !options[1])
|
|
2476
|
+
throw new Error(usage);
|
|
2477
|
+
let port = Number(options[1]);
|
|
2478
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
2479
|
+
throw new Error(
|
|
2480
|
+
`Port must be an integer between 1 and 65535. Found "${options[1]}".`
|
|
2481
|
+
);
|
|
2482
|
+
return { browserAssetsRoot, port };
|
|
2483
|
+
}
|
|
2484
|
+
function serve(options) {
|
|
2485
|
+
return Effect20.scoped(
|
|
2486
|
+
Effect20.gen(function* () {
|
|
2487
|
+
let shutdown = yield* acquireShutdownSignal(), server = yield* Effect20.raceFirst(
|
|
2488
|
+
startEnvironmentServer(options),
|
|
2489
|
+
Deferred.await(shutdown)
|
|
2490
|
+
);
|
|
2491
|
+
server !== void 0 && (yield* Effect20.sync(() => {
|
|
2492
|
+
process.stdout.write(`Listening URL: ${server.origin}
|
|
2493
|
+
`), process.stdout.write(`Pairing URL: ${server.pairingUrl}
|
|
2494
|
+
`), openDefaultBrowser(server.pairingUrl);
|
|
2495
|
+
}), yield* Deferred.await(shutdown));
|
|
2496
|
+
})
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
function acquireShutdownSignal() {
|
|
2500
|
+
return Effect20.gen(function* () {
|
|
2501
|
+
let shutdown = yield* Deferred.make();
|
|
2502
|
+
return yield* Effect20.acquireRelease(
|
|
2503
|
+
Effect20.sync(() => {
|
|
2504
|
+
let stop = () => {
|
|
2505
|
+
Deferred.doneUnsafe(shutdown, Effect20.void);
|
|
2506
|
+
};
|
|
2507
|
+
return process.once("SIGINT", stop), process.once("SIGTERM", stop), stop;
|
|
2508
|
+
}),
|
|
2509
|
+
(stop) => Effect20.sync(() => {
|
|
2510
|
+
process.off("SIGINT", stop), process.off("SIGTERM", stop);
|
|
2511
|
+
})
|
|
2512
|
+
), shutdown;
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
function writeOutput(line) {
|
|
2516
|
+
return Effect20.sync(() => {
|
|
2517
|
+
process.stdout.write(`${line}
|
|
2518
|
+
`);
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
function writeFailure(line) {
|
|
2522
|
+
return Effect20.sync(() => {
|
|
2523
|
+
process.stderr.write(`${line}
|
|
2524
|
+
`);
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
function normalizeError(error) {
|
|
2528
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
2529
|
+
}
|
|
2530
|
+
function resolveBrowserAssetsRoot() {
|
|
2531
|
+
let root = [
|
|
2532
|
+
new URL("./web", import.meta.url),
|
|
2533
|
+
new URL("../web/dist/web", import.meta.url),
|
|
2534
|
+
new URL("../../web/dist/web", import.meta.url)
|
|
2535
|
+
].find((candidate) => existsSync2(candidate));
|
|
2536
|
+
if (root === void 0)
|
|
2537
|
+
throw new Error(
|
|
2538
|
+
"Browser assets are missing. Reinstall Rebase or rebuild the web client."
|
|
2539
|
+
);
|
|
2540
|
+
return fileURLToPath2(root);
|
|
2541
|
+
}
|
|
2542
|
+
function versionOutput() {
|
|
2543
|
+
let protocol = currentEnvironmentProtocol;
|
|
2544
|
+
return [
|
|
2545
|
+
`Rebase ${productVersion}`,
|
|
2546
|
+
`Environment protocol ${protocol.major}.${protocol.minor} (minimum ${protocol.major}.${protocol.minimumSupportedMinor})`
|
|
2547
|
+
].join(`
|
|
2548
|
+
`);
|
|
2549
|
+
}
|
|
2550
|
+
var entryPoint = process.argv[1];
|
|
2551
|
+
entryPoint && realpathSync(fileURLToPath2(import.meta.url)) === realpathSync(entryPoint) && await main();
|
|
2552
|
+
export {
|
|
2553
|
+
main,
|
|
2554
|
+
runCli
|
|
2555
|
+
};
|