@runlane/realtime 0.0.0-stage → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/client.cjs +534 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +33 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +33 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +508 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.cjs +686 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +62 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +667 -0
- package/dist/index.mjs.map +1 -0
- package/dist/wire-BEevjo-K.mjs +53 -0
- package/dist/wire-BEevjo-K.mjs.map +1 -0
- package/dist/wire-xCr4QuVn.cjs +115 -0
- package/dist/wire-xCr4QuVn.cjs.map +1 -0
- package/package.json +83 -3
- package/README.md +0 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { a as invalidOptions, i as realtimeTiming, n as streamSelectionQuery, r as realtimeClock } from "./wire-BEevjo-K.mjs";
|
|
2
|
+
import { ErrorCode, ErrorCode as ErrorCode$1, RunReadPermission, RunReadPermission as RunReadPermission$1, RunSubscriptionEventType, RunlaneError, RunlaneError as RunlaneError$1, RunlaneLogAttribute, RunlaneLogEventType, RunlaneLogLevel, RunlaneMetricAttribute, RunlaneMetricName, RunlaneSpanName, contractDefaults, createDurationSchema, createRunlaneIdSchema, realtimeDefaults, recordRunlaneTelemetryLog, recordRunlaneTelemetryMetric, runAccessGrantSchema, runCursorSchema, runRealtimeErrorCodeSchema, runRealtimeSnapshotSchema, runStatusValues, runStreamNameSchema, runWithRunlaneTelemetrySpan, verifiedRunTokenSchema } from "@runlane/contracts/realtime";
|
|
3
|
+
import { createFunctionSchema, objectSurfaceSchema, waitForAbortableTimeout } from "@runlane/utilities/browser";
|
|
4
|
+
import { encode } from "eventsource-encoder";
|
|
5
|
+
import * as z$1 from "zod";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { CompactSign, base64url, compactVerify } from "jose";
|
|
8
|
+
// src/access.ts
|
|
9
|
+
function intersectGrants(app, token) {
|
|
10
|
+
if (token === void 0) return app;
|
|
11
|
+
const permissions = app.permissions.filter((permission) => token.permissions.includes(permission));
|
|
12
|
+
let streams = app.streams ?? token.streams;
|
|
13
|
+
if (app.streams !== void 0 && token.streams !== void 0) streams = app.streams.filter((name) => token.streams?.includes(name));
|
|
14
|
+
return {
|
|
15
|
+
permissions,
|
|
16
|
+
...streams === void 0 ? {} : { streams }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function permitsStream(grant, name) {
|
|
20
|
+
return grant.permissions.includes(RunReadPermission$1.Streams) && (grant.streams === void 0 || grant.streams.includes(name));
|
|
21
|
+
}
|
|
22
|
+
function projectEvent(event, grant) {
|
|
23
|
+
if (event.type === RunSubscriptionEventType.Stream) return permitsStream(grant, event.record.name) ? event : void 0;
|
|
24
|
+
if (event.type === RunSubscriptionEventType.Meta) return grant.permissions.includes(RunReadPermission$1.Meta) ? event : void 0;
|
|
25
|
+
if (!grant.permissions.includes(RunReadPermission$1.Status)) return void 0;
|
|
26
|
+
if (event.type === RunSubscriptionEventType.Status) {
|
|
27
|
+
const status = { ...event };
|
|
28
|
+
if (!grant.permissions.includes(RunReadPermission$1.Output)) {
|
|
29
|
+
delete status.output;
|
|
30
|
+
delete status.failure;
|
|
31
|
+
}
|
|
32
|
+
if (status.failure !== void 0) status.failure = runRealtimeSnapshotSchema.shape.failure.parse(status.failure);
|
|
33
|
+
return status;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
...event,
|
|
37
|
+
run: projectSnapshot(event.run, grant)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function projectSnapshot(source, grant) {
|
|
41
|
+
const { meta, output, failure, ...status } = runRealtimeSnapshotSchema.parse(source);
|
|
42
|
+
return {
|
|
43
|
+
...status,
|
|
44
|
+
...grant.permissions.includes(RunReadPermission$1.Meta) && meta !== void 0 ? { meta } : {},
|
|
45
|
+
...grant.permissions.includes(RunReadPermission$1.Output) ? {
|
|
46
|
+
...output === void 0 ? {} : { output },
|
|
47
|
+
...failure === void 0 ? {} : { failure }
|
|
48
|
+
} : {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// src/tokens.ts
|
|
52
|
+
const tokenPartsSchema = z.tuple([
|
|
53
|
+
z.string(),
|
|
54
|
+
z.string(),
|
|
55
|
+
z.string()
|
|
56
|
+
]);
|
|
57
|
+
const headerSchema = z.strictObject({
|
|
58
|
+
alg: z.literal("HS256"),
|
|
59
|
+
typ: z.literal("JWT"),
|
|
60
|
+
kid: z.string().min(1).max(128)
|
|
61
|
+
});
|
|
62
|
+
const algorithmSchema = z.object({
|
|
63
|
+
name: z.literal("HMAC"),
|
|
64
|
+
hash: z.object({ name: z.literal("SHA-256") }),
|
|
65
|
+
length: z.number().int().min(256)
|
|
66
|
+
});
|
|
67
|
+
const encoder$1 = new TextEncoder();
|
|
68
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
69
|
+
const maximumTokenLength = 16384;
|
|
70
|
+
/** Strict construction schema; claim and header constraints remain the canonical owners. */
|
|
71
|
+
const realtimeTokenOptionsSchema = z.strictObject({
|
|
72
|
+
audience: verifiedRunTokenSchema.shape.audience,
|
|
73
|
+
keyId: headerSchema.shape.kid,
|
|
74
|
+
key: z.instanceof(CryptoKey),
|
|
75
|
+
verifyKeys: z.record(headerSchema.shape.kid, z.instanceof(CryptoKey)).readonly().optional()
|
|
76
|
+
}).refine((tokens) => !Object.hasOwn(tokens.verifyKeys ?? {}, tokens.keyId), "tokens.keyId must not also appear in verifyKeys.");
|
|
77
|
+
const createTokenOptionsSchema = z.strictObject({
|
|
78
|
+
runId: z.string(),
|
|
79
|
+
grant: runAccessGrantSchema.optional(),
|
|
80
|
+
expiresIn: createDurationSchema("expiresIn must be whole seconds between 1s and realtime.maxTokenTTL.").optional()
|
|
81
|
+
});
|
|
82
|
+
/** Signs one run/environment/audience grant using an HMAC-SHA256 key of at least 256 bits. */
|
|
83
|
+
async function signRunToken(environment, tokens, input) {
|
|
84
|
+
try {
|
|
85
|
+
const options = createTokenOptionsSchema.parse(input);
|
|
86
|
+
const header = headerSchema.parse({
|
|
87
|
+
alg: "HS256",
|
|
88
|
+
typ: "JWT",
|
|
89
|
+
kid: tokens.keyId
|
|
90
|
+
});
|
|
91
|
+
requireHmacKey(tokens.key, "sign");
|
|
92
|
+
const ttl = tokenLifetime(options.expiresIn?.milliseconds);
|
|
93
|
+
const issuedAt = Math.floor(realtimeClock.now() / 1e3);
|
|
94
|
+
const claims = verifiedRunTokenSchema.parse({
|
|
95
|
+
environment,
|
|
96
|
+
runId: options.runId,
|
|
97
|
+
grant: options.grant ?? { permissions: Object.values(RunReadPermission$1) },
|
|
98
|
+
audience: tokens.audience,
|
|
99
|
+
issuedAt,
|
|
100
|
+
expiresAt: issuedAt + ttl / 1e3,
|
|
101
|
+
version: 1
|
|
102
|
+
});
|
|
103
|
+
const token = await new CompactSign(encoder$1.encode(JSON.stringify(claims))).setProtectedHeader(header).sign(tokens.key);
|
|
104
|
+
if (token.length > maximumTokenLength) throw invalidToken();
|
|
105
|
+
return token;
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
if (cause instanceof RunlaneError$1) throw cause;
|
|
108
|
+
const message = cause instanceof z.ZodError ? `Token ${cause.issues[0]?.path.join(".") ?? "expiresIn"} is invalid: ${cause.issues[0]?.message ?? "Check the token options."}` : "Token signing failed; check key and claims.";
|
|
109
|
+
const field = cause instanceof z.ZodError ? cause.issues[0]?.path[0] : void 0;
|
|
110
|
+
let code = ErrorCode$1.InternalError;
|
|
111
|
+
if (cause instanceof z.ZodError) code = field === "environment" || field === "runId" || field === "grant" ? ErrorCode$1.ValidationFailed : ErrorCode$1.ConfigurationInvalid;
|
|
112
|
+
throw new RunlaneError$1({
|
|
113
|
+
code,
|
|
114
|
+
message,
|
|
115
|
+
retryable: false,
|
|
116
|
+
cause
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function tokenLifetime(ttl = realtimeTiming.tokenTTL) {
|
|
121
|
+
if (ttl < 1e3 || ttl > realtimeTiming.maxTokenTTL || ttl % 1e3 !== 0) throw new RunlaneError$1({
|
|
122
|
+
code: ErrorCode$1.ConfigurationInvalid,
|
|
123
|
+
message: "expiresIn must be whole seconds between 1s and realtime.maxTokenTTL.",
|
|
124
|
+
retryable: false
|
|
125
|
+
});
|
|
126
|
+
return ttl;
|
|
127
|
+
}
|
|
128
|
+
/** Verifies a scoped token and records only bounded rejection reasons through the supplied telemetry. */
|
|
129
|
+
async function verifyRunToken(token, options, diagnostics = {}, operation = "verify") {
|
|
130
|
+
let reason = "malformed";
|
|
131
|
+
try {
|
|
132
|
+
if (token.length > maximumTokenLength) throw invalidToken();
|
|
133
|
+
const [head, body, signature] = tokenPartsSchema.parse(token.split("."));
|
|
134
|
+
reason = "header";
|
|
135
|
+
const header = headerSchema.parse(JSON.parse(decoder.decode(decodeBase64(head))));
|
|
136
|
+
reason = "unknown_key";
|
|
137
|
+
const key = Object.hasOwn(options.keys, header.kid) ? options.keys[header.kid] : void 0;
|
|
138
|
+
if (key === void 0) throw invalidToken();
|
|
139
|
+
reason = "key";
|
|
140
|
+
requireHmacKey(key, "verify");
|
|
141
|
+
reason = "signature";
|
|
142
|
+
decodeBase64(signature);
|
|
143
|
+
const verified = await compactVerify(token, key, { algorithms: ["HS256"] });
|
|
144
|
+
reason = "claims";
|
|
145
|
+
decodeBase64(body);
|
|
146
|
+
const claims = verifiedRunTokenSchema.parse(JSON.parse(decoder.decode(verified.payload)));
|
|
147
|
+
const now = Math.floor(realtimeClock.now() / 1e3);
|
|
148
|
+
const rejection = tokenClaimRejection(claims, options.audience, now);
|
|
149
|
+
if (rejection !== void 0) {
|
|
150
|
+
reason = rejection;
|
|
151
|
+
throw invalidToken();
|
|
152
|
+
}
|
|
153
|
+
return claims;
|
|
154
|
+
} catch (cause) {
|
|
155
|
+
recordRunlaneTelemetryMetric(diagnostics, {
|
|
156
|
+
name: RunlaneMetricName.RealtimeTokenRejectedCount,
|
|
157
|
+
value: 1,
|
|
158
|
+
attributes: {
|
|
159
|
+
[RunlaneMetricAttribute.Operation]: operation,
|
|
160
|
+
[RunlaneMetricAttribute.Reason]: reason
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
recordRunlaneTelemetryLog(diagnostics, {
|
|
164
|
+
type: RunlaneLogEventType.RealtimeTokenRejected,
|
|
165
|
+
level: RunlaneLogLevel.Warn,
|
|
166
|
+
attributes: {
|
|
167
|
+
[RunlaneLogAttribute.Operation]: operation,
|
|
168
|
+
[RunlaneLogAttribute.Reason]: reason
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
throw invalidToken(cause);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function tokenClaimRejection(claims, audience, now) {
|
|
175
|
+
if (claims.audience !== audience) return "audience";
|
|
176
|
+
if (claims.issuedAt > now + realtimeTiming.issuedAtLeeway / 1e3) return "issued_at";
|
|
177
|
+
if (claims.expiresAt <= now) return "expired";
|
|
178
|
+
if (claims.expiresAt <= claims.issuedAt || (claims.expiresAt - claims.issuedAt) * 1e3 > realtimeTiming.maxTokenTTL) return "lifetime";
|
|
179
|
+
}
|
|
180
|
+
/** Shared key policy for signing, verification, and handler startup checks. */
|
|
181
|
+
function requireHmacKey(key, usage) {
|
|
182
|
+
if (!(key instanceof CryptoKey) || key.type !== "secret" || !algorithmSchema.safeParse(key.algorithm).success || !key.usages.includes(usage)) throw new RunlaneError$1({
|
|
183
|
+
code: ErrorCode$1.ConfigurationInvalid,
|
|
184
|
+
message: `key must be an HMAC-SHA256 key of at least 256 bits with ${usage} usage.`,
|
|
185
|
+
retryable: false
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function decodeBase64(value) {
|
|
189
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) throw invalidToken();
|
|
190
|
+
const bytes = base64url.decode(value);
|
|
191
|
+
if (base64url.encode(bytes) !== value) throw invalidToken();
|
|
192
|
+
return bytes;
|
|
193
|
+
}
|
|
194
|
+
function invalidToken(cause) {
|
|
195
|
+
return new RunlaneError$1({
|
|
196
|
+
code: ErrorCode$1.AccessDenied,
|
|
197
|
+
message: "Run access denied.",
|
|
198
|
+
retryable: false,
|
|
199
|
+
cause
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// src/handler.ts
|
|
203
|
+
const encoder = new TextEncoder();
|
|
204
|
+
const runIdSchema = createRunlaneIdSchema("Run id");
|
|
205
|
+
const pollInterval = createDurationSchema().parse(contractDefaults.streams.pollInterval).milliseconds;
|
|
206
|
+
const handlerOptionsSchema = z$1.strictObject({
|
|
207
|
+
runlane: objectSurfaceSchema,
|
|
208
|
+
authorize: createFunctionSchema("authorize must be a function.").optional(),
|
|
209
|
+
tokens: realtimeTokenOptionsSchema.optional(),
|
|
210
|
+
telemetry: objectSurfaceSchema.optional(),
|
|
211
|
+
onTelemetryError: createFunctionSchema("onTelemetryError must be a function.").optional()
|
|
212
|
+
}).refine((options) => options.authorize !== void 0 || options.tokens !== void 0, "Configure authorize for session access, tokens for bearer access, or both.");
|
|
213
|
+
/** Mounts authorized, bounded SSE reads without starting workers or adding runtime storage work. */
|
|
214
|
+
function createRunlaneRealtime(options) {
|
|
215
|
+
const checked = handlerOptionsSchema.safeParse(options);
|
|
216
|
+
if (!checked.success) throw invalidOptions(checked.error, "handler");
|
|
217
|
+
const tokens = checked.data.tokens;
|
|
218
|
+
if (tokens !== void 0) {
|
|
219
|
+
requireHmacKey(tokens.key, "sign");
|
|
220
|
+
requireHmacKey(tokens.key, "verify");
|
|
221
|
+
for (const key of Object.values(tokens.verifyKeys ?? {})) requireHmacKey(key, "verify");
|
|
222
|
+
}
|
|
223
|
+
const verification = tokens === void 0 ? void 0 : {
|
|
224
|
+
audience: tokens.audience,
|
|
225
|
+
keys: {
|
|
226
|
+
...tokens.verifyKeys,
|
|
227
|
+
[tokens.keyId]: tokens.key
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
async createToken(input) {
|
|
232
|
+
if (tokens === void 0) throw new RunlaneError$1({
|
|
233
|
+
code: ErrorCode$1.ConfigurationInvalid,
|
|
234
|
+
message: "Configure tokens before calling createToken."
|
|
235
|
+
});
|
|
236
|
+
return signRunToken(options.runlane.environment, tokens, input);
|
|
237
|
+
},
|
|
238
|
+
async handle(request) {
|
|
239
|
+
let operation = "subscribe";
|
|
240
|
+
try {
|
|
241
|
+
const route = parseRoute(request);
|
|
242
|
+
operation = route.name === void 0 ? "subscribe" : "read";
|
|
243
|
+
const token = await requestToken(request, route.runId, options, verification);
|
|
244
|
+
return await openConnection(options, request, route, await authorizeGrant(options, request, route, token, operation), token);
|
|
245
|
+
} catch (cause) {
|
|
246
|
+
const error = publicError(cause);
|
|
247
|
+
if (error.code === ErrorCode$1.AccessDenied) recordDenial(options, cause, operation);
|
|
248
|
+
return Response.json({ error }, {
|
|
249
|
+
status: request.method === "GET" ? httpStatus(error.code) : 405,
|
|
250
|
+
headers: {
|
|
251
|
+
"cache-control": "no-store",
|
|
252
|
+
...request.method === "GET" ? {} : { allow: "GET" }
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async function authorizeGrant(options, request, route, token, operation) {
|
|
260
|
+
let app;
|
|
261
|
+
try {
|
|
262
|
+
app = options.authorize === void 0 ? token?.grant : await options.authorize({
|
|
263
|
+
request,
|
|
264
|
+
runId: route.runId,
|
|
265
|
+
...token === void 0 ? {} : { token }
|
|
266
|
+
});
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
recordRunlaneTelemetryMetric(options, {
|
|
269
|
+
name: RunlaneMetricName.RealtimeAuthorizationErrorCount,
|
|
270
|
+
value: 1,
|
|
271
|
+
attributes: {
|
|
272
|
+
[RunlaneMetricAttribute.Operation]: operation,
|
|
273
|
+
[RunlaneMetricAttribute.Reason]: "authorize_exception"
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
recordRunlaneTelemetryLog(options, {
|
|
277
|
+
type: RunlaneLogEventType.RealtimeAuthorizationFailed,
|
|
278
|
+
level: RunlaneLogLevel.Error,
|
|
279
|
+
attributes: {
|
|
280
|
+
[RunlaneLogAttribute.Operation]: operation,
|
|
281
|
+
[RunlaneLogAttribute.Reason]: "authorize_exception"
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
throw new RunlaneError$1({
|
|
285
|
+
code: ErrorCode$1.InternalError,
|
|
286
|
+
message: "Realtime authorization failed.",
|
|
287
|
+
cause
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (app === void 0) throw accessDenied();
|
|
291
|
+
const parsed = runAccessGrantSchema.safeParse(app);
|
|
292
|
+
if (!parsed.success) throw invalidRequest("Realtime grant is invalid.", parsed.error);
|
|
293
|
+
const grant = intersectGrants(parsed.data, token?.grant);
|
|
294
|
+
if (grant.permissions.length === 0 || route.name !== void 0 && !permitsStream(grant, route.name)) throw accessDenied();
|
|
295
|
+
return grant;
|
|
296
|
+
}
|
|
297
|
+
function recordDenial(options, cause, operation) {
|
|
298
|
+
const reason = accessDenialReasonSchema.safeParse(cause instanceof RunlaneError$1 ? cause.meta?.reason : void 0).data ?? "token_rejected";
|
|
299
|
+
if (reason !== "token_rejected") recordRunlaneTelemetryLog(options, {
|
|
300
|
+
type: RunlaneLogEventType.RealtimeAuthorizationDenied,
|
|
301
|
+
level: RunlaneLogLevel.Warn,
|
|
302
|
+
attributes: {
|
|
303
|
+
[RunlaneLogAttribute.Operation]: operation,
|
|
304
|
+
[RunlaneLogAttribute.Reason]: reason
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
recordRunlaneTelemetryMetric(options, {
|
|
308
|
+
name: RunlaneMetricName.RealtimeAuthorizationDeniedCount,
|
|
309
|
+
value: 1,
|
|
310
|
+
attributes: {
|
|
311
|
+
[RunlaneMetricAttribute.Operation]: operation,
|
|
312
|
+
[RunlaneMetricAttribute.Reason]: reason
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function parseRoute(request) {
|
|
317
|
+
if (request.method !== "GET") throw new RunlaneError$1({
|
|
318
|
+
code: ErrorCode$1.ValidationFailed,
|
|
319
|
+
message: "Realtime routes require GET.",
|
|
320
|
+
retryable: false
|
|
321
|
+
});
|
|
322
|
+
const url = new URL(request.url);
|
|
323
|
+
if (Array.from(url.searchParams.keys()).some((key) => key !== "after" && key !== streamSelectionQuery.name) || url.searchParams.getAll("after").length > 1) throw invalidRequest("Only one after and one streams query parameter are accepted; use Authorization for credentials.");
|
|
324
|
+
const match = /\/runs\/([^/]+)\/(events|streams\/([^/]+))$/.exec(url.pathname);
|
|
325
|
+
if (match?.[1] === void 0) throw new RunlaneError$1({
|
|
326
|
+
code: ErrorCode$1.RunNotFound,
|
|
327
|
+
message: "Realtime route was not found.",
|
|
328
|
+
retryable: false
|
|
329
|
+
});
|
|
330
|
+
let runId;
|
|
331
|
+
let name;
|
|
332
|
+
try {
|
|
333
|
+
runId = runIdSchema.parse(decodeURIComponent(match[1]));
|
|
334
|
+
name = match[3] === void 0 ? void 0 : runStreamNameSchema.parse(decodeURIComponent(match[3]));
|
|
335
|
+
} catch (cause) {
|
|
336
|
+
throw invalidRequest("Run id or stream name is invalid.", cause);
|
|
337
|
+
}
|
|
338
|
+
const selections = url.searchParams.getAll(streamSelectionQuery.name);
|
|
339
|
+
if (selections.length > 1 || selections.length === 1 && (name !== void 0 || selections[0] !== streamSelectionQuery.none)) throw invalidRequest("Stream selection requires one streams=none parameter on the events route.");
|
|
340
|
+
return {
|
|
341
|
+
runId,
|
|
342
|
+
...name === void 0 ? {} : { name },
|
|
343
|
+
...selections.length === 1 ? { streams: false } : {},
|
|
344
|
+
...parseResumeCursor(request, url)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function parseResumeCursor(request, url) {
|
|
348
|
+
const header = request.headers.get("last-event-id");
|
|
349
|
+
const query = url.searchParams.get("after");
|
|
350
|
+
if (header !== null && query !== null && header !== query) throw invalidCursor();
|
|
351
|
+
const raw = header ?? query;
|
|
352
|
+
const parsed = raw === null ? void 0 : runCursorSchema.safeParse(raw);
|
|
353
|
+
if (parsed !== void 0 && !parsed.success) throw invalidCursor();
|
|
354
|
+
return parsed?.success ? { after: parsed.data } : {};
|
|
355
|
+
}
|
|
356
|
+
async function openConnection(options, request, route, grant, token) {
|
|
357
|
+
const started = realtimeClock.now();
|
|
358
|
+
const operation = route.name === void 0 ? "subscribe" : "read";
|
|
359
|
+
const control = new AbortController();
|
|
360
|
+
const expires = Math.min(started + realtimeTiming.connectionDuration, token === void 0 ? Infinity : token.expiresAt * 1e3);
|
|
361
|
+
const stop = () => control.abort();
|
|
362
|
+
request.signal.addEventListener("abort", stop, { once: true });
|
|
363
|
+
if (request.signal.aborted) stop();
|
|
364
|
+
const expiry = setTimeout(stop, Math.max(0, expires - realtimeClock.now()));
|
|
365
|
+
const frames = sourceFrames(options.runlane, route, grant, control.signal);
|
|
366
|
+
let pending;
|
|
367
|
+
let cursor = route.after;
|
|
368
|
+
let closed = false;
|
|
369
|
+
const finish = () => {
|
|
370
|
+
if (closed) return;
|
|
371
|
+
closed = true;
|
|
372
|
+
clearTimeout(expiry);
|
|
373
|
+
request.signal.removeEventListener("abort", stop);
|
|
374
|
+
control.abort();
|
|
375
|
+
frames.return().catch(() => void 0);
|
|
376
|
+
recordRunlaneTelemetryMetric(options, {
|
|
377
|
+
name: RunlaneMetricName.RealtimeConnectionDurationMs,
|
|
378
|
+
value: Math.max(0, realtimeClock.now() - started),
|
|
379
|
+
attributes: { [RunlaneMetricAttribute.Operation]: operation }
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
const next = async () => {
|
|
383
|
+
if (control.signal.aborted || realtimeClock.now() >= expires) return void 0;
|
|
384
|
+
pending ??= runWithRunlaneTelemetrySpan(options, { name: route.name === void 0 ? RunlaneSpanName.RealtimeSubscribe : RunlaneSpanName.RealtimeRead }, () => frames.next()).catch((cause) => {
|
|
385
|
+
const { cursor, ...error } = publicSourceError(cause);
|
|
386
|
+
throw new RunlaneError$1({
|
|
387
|
+
...error,
|
|
388
|
+
cause,
|
|
389
|
+
...cursor === void 0 ? {} : { meta: { cursor } }
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
const timer = new AbortController();
|
|
393
|
+
const abortTimer = () => timer.abort();
|
|
394
|
+
control.signal.addEventListener("abort", abortTimer, { once: true });
|
|
395
|
+
try {
|
|
396
|
+
const result = await Promise.race([pending, waitForAbortableTimeout(realtimeTiming.heartbeatInterval, timer.signal).then(() => void 0)]);
|
|
397
|
+
if (realtimeClock.now() >= expires) return void 0;
|
|
398
|
+
if (result === void 0) return encoder.encode(encode({ comment: "heartbeat" }));
|
|
399
|
+
pending = void 0;
|
|
400
|
+
if (result.done) return void 0;
|
|
401
|
+
const frame = result.value;
|
|
402
|
+
const bytes = encodeFrame(frame, cursor);
|
|
403
|
+
cursor = frame.cursor ?? cursor;
|
|
404
|
+
return bytes;
|
|
405
|
+
} finally {
|
|
406
|
+
timer.abort();
|
|
407
|
+
control.signal.removeEventListener("abort", abortTimer);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
let first;
|
|
411
|
+
try {
|
|
412
|
+
first = await next();
|
|
413
|
+
} catch (cause) {
|
|
414
|
+
finish();
|
|
415
|
+
throw cause;
|
|
416
|
+
}
|
|
417
|
+
const body = new ReadableStream({
|
|
418
|
+
start(controller) {
|
|
419
|
+
const end = () => {
|
|
420
|
+
if (closed) return;
|
|
421
|
+
controller.close();
|
|
422
|
+
finish();
|
|
423
|
+
};
|
|
424
|
+
control.signal.addEventListener("abort", end, { once: true });
|
|
425
|
+
if (control.signal.aborted) end();
|
|
426
|
+
},
|
|
427
|
+
async pull(controller) {
|
|
428
|
+
try {
|
|
429
|
+
const bytes = control.signal.aborted || realtimeClock.now() >= expires ? void 0 : first ?? await next();
|
|
430
|
+
if (closed) return;
|
|
431
|
+
first = void 0;
|
|
432
|
+
if (bytes === void 0) {
|
|
433
|
+
controller.close();
|
|
434
|
+
finish();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
controller.enqueue(bytes);
|
|
438
|
+
} catch (cause) {
|
|
439
|
+
if (closed) return;
|
|
440
|
+
controller.enqueue(encoder.encode(encode({
|
|
441
|
+
event: "error",
|
|
442
|
+
data: JSON.stringify(publicError(cause, cursor))
|
|
443
|
+
})));
|
|
444
|
+
controller.close();
|
|
445
|
+
finish();
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
cancel() {
|
|
449
|
+
finish();
|
|
450
|
+
}
|
|
451
|
+
}, { highWaterMark: 0 });
|
|
452
|
+
return new Response(body, { headers: {
|
|
453
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
454
|
+
"cache-control": "no-store",
|
|
455
|
+
"x-accel-buffering": "no"
|
|
456
|
+
} });
|
|
457
|
+
}
|
|
458
|
+
function encodeFrame(frame, cursor) {
|
|
459
|
+
const data = JSON.stringify(frame.data);
|
|
460
|
+
const bytes = encoder.encode(encode({
|
|
461
|
+
event: frame.event,
|
|
462
|
+
data,
|
|
463
|
+
...frame.cursor === void 0 ? {} : { id: frame.cursor }
|
|
464
|
+
}));
|
|
465
|
+
if (bytes.byteLength > realtimeDefaults.maxEventBytes) throw new RunlaneError$1({
|
|
466
|
+
code: ErrorCode$1.EventTooLarge,
|
|
467
|
+
message: "Realtime event exceeds the serialized UTF-8 byte limit.",
|
|
468
|
+
retryable: false,
|
|
469
|
+
...cursor === void 0 ? {} : { meta: { cursor } }
|
|
470
|
+
});
|
|
471
|
+
return bytes;
|
|
472
|
+
}
|
|
473
|
+
async function* sourceFrames(source, route, grant, signal) {
|
|
474
|
+
if (route.name === void 0) {
|
|
475
|
+
for await (const event of source.runs.subscribe(route.runId, {
|
|
476
|
+
after: route.after,
|
|
477
|
+
signal,
|
|
478
|
+
streams: route.streams === false || !grant.permissions.includes(RunReadPermission$1.Streams) ? false : grant.streams
|
|
479
|
+
})) {
|
|
480
|
+
if (signal.aborted) return;
|
|
481
|
+
const projected = projectEvent(event, grant);
|
|
482
|
+
yield projected === void 0 ? {
|
|
483
|
+
event: "checkpoint",
|
|
484
|
+
cursor: event.cursor,
|
|
485
|
+
data: {}
|
|
486
|
+
} : {
|
|
487
|
+
event: projected.type,
|
|
488
|
+
cursor: projected.cursor,
|
|
489
|
+
data: projected
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (!signal.aborted) yield {
|
|
493
|
+
event: "end",
|
|
494
|
+
data: {}
|
|
495
|
+
};
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
yield* namedFrames(source, route.runId, route.name, route.after, signal);
|
|
499
|
+
}
|
|
500
|
+
async function* namedFrames(source, runId, name, initial, signal) {
|
|
501
|
+
let after = initial;
|
|
502
|
+
let terminal = false;
|
|
503
|
+
while (!signal.aborted) {
|
|
504
|
+
const controller = new AbortController();
|
|
505
|
+
const waiting = source.lane?.runSignal?.wait({
|
|
506
|
+
environment: source.environment,
|
|
507
|
+
runId,
|
|
508
|
+
signal: AbortSignal.any([signal, controller.signal]),
|
|
509
|
+
timeoutMs: pollInterval
|
|
510
|
+
}).catch((cause) => {
|
|
511
|
+
throw new RunlaneError$1({
|
|
512
|
+
code: ErrorCode$1.AdapterContractViolation,
|
|
513
|
+
message: "Lane run signal wait must not reject.",
|
|
514
|
+
cause
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
waiting?.catch(() => void 0);
|
|
518
|
+
try {
|
|
519
|
+
const page = await source.runs.readStream(runId, name, {
|
|
520
|
+
after,
|
|
521
|
+
limit: contractDefaults.pagination.defaultLimit
|
|
522
|
+
});
|
|
523
|
+
yield* namedPageFrames(page, after);
|
|
524
|
+
after = page.cursor;
|
|
525
|
+
if (page.hasMore) continue;
|
|
526
|
+
if (terminal && page.records.length === 0) {
|
|
527
|
+
yield {
|
|
528
|
+
event: "end",
|
|
529
|
+
data: {}
|
|
530
|
+
};
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
terminal = await readNamedRunTerminal(source, runId);
|
|
534
|
+
if (terminal) continue;
|
|
535
|
+
if (page.records.length === 0) await (waiting ?? waitForAbortableTimeout(pollInterval, signal));
|
|
536
|
+
} finally {
|
|
537
|
+
controller.abort();
|
|
538
|
+
await waiting;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function readNamedRunTerminal(source, runId) {
|
|
543
|
+
const run = await source.runs.get(runId);
|
|
544
|
+
if (run === void 0) throw new RunlaneError$1({
|
|
545
|
+
code: ErrorCode$1.RunNotFound,
|
|
546
|
+
message: "Run was not found.",
|
|
547
|
+
retryable: false
|
|
548
|
+
});
|
|
549
|
+
return runStatusValues.isTerminal(run.status);
|
|
550
|
+
}
|
|
551
|
+
function* namedPageFrames(page, after) {
|
|
552
|
+
for (const [index, record] of page.records.entries()) {
|
|
553
|
+
const last = index === page.records.length - 1;
|
|
554
|
+
yield {
|
|
555
|
+
event: "stream",
|
|
556
|
+
...last ? { cursor: page.cursor } : {},
|
|
557
|
+
data: {
|
|
558
|
+
...last ? { cursor: page.cursor } : {},
|
|
559
|
+
record
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (page.records.length === 0 && page.cursor !== after) yield {
|
|
564
|
+
event: "checkpoint",
|
|
565
|
+
cursor: page.cursor,
|
|
566
|
+
data: {}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function publicError(cause, cursor) {
|
|
570
|
+
const checkpoint = cursor ?? (cause instanceof RunlaneError$1 ? runCursorSchema.safeParse(cause.meta?.cursor).data : void 0);
|
|
571
|
+
const resume = checkpoint === void 0 ? {} : { cursor: checkpoint };
|
|
572
|
+
if (cause instanceof RunlaneError$1) {
|
|
573
|
+
const code = runRealtimeErrorCodeSchema.safeParse(cause.code);
|
|
574
|
+
if (code.success && code.data !== ErrorCode$1.InternalError) return {
|
|
575
|
+
code: code.data,
|
|
576
|
+
message: cause.message,
|
|
577
|
+
retryable: cause.retryable,
|
|
578
|
+
...resume
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
code: ErrorCode$1.InternalError,
|
|
583
|
+
message: "Realtime request failed.",
|
|
584
|
+
retryable: true,
|
|
585
|
+
...resume
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function publicSourceError(cause) {
|
|
589
|
+
const error = publicError(cause);
|
|
590
|
+
if (cause instanceof RunlaneError$1) switch (cause.code) {
|
|
591
|
+
case ErrorCode$1.ConfigurationInvalid:
|
|
592
|
+
case ErrorCode$1.ValidationFailed: return {
|
|
593
|
+
...error,
|
|
594
|
+
code: ErrorCode$1.ValidationFailed,
|
|
595
|
+
message: "Realtime request is invalid.",
|
|
596
|
+
retryable: false
|
|
597
|
+
};
|
|
598
|
+
case ErrorCode$1.RunCursorInvalid: return {
|
|
599
|
+
...error,
|
|
600
|
+
code: ErrorCode$1.RunCursorInvalid,
|
|
601
|
+
message: "Run cursor is invalid.",
|
|
602
|
+
retryable: false
|
|
603
|
+
};
|
|
604
|
+
case ErrorCode$1.RunNotFound: return {
|
|
605
|
+
...error,
|
|
606
|
+
code: ErrorCode$1.RunNotFound,
|
|
607
|
+
message: "Run was not found.",
|
|
608
|
+
retryable: false
|
|
609
|
+
};
|
|
610
|
+
case ErrorCode$1.OperationCancelled: return {
|
|
611
|
+
...error,
|
|
612
|
+
code: ErrorCode$1.Disconnected,
|
|
613
|
+
message: "Realtime runtime is closing; reconnect to resume.",
|
|
614
|
+
retryable: true
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return error;
|
|
618
|
+
}
|
|
619
|
+
function httpStatus(code) {
|
|
620
|
+
if (code === ErrorCode$1.AccessDenied) return 403;
|
|
621
|
+
if (code === ErrorCode$1.RunNotFound) return 404;
|
|
622
|
+
if (code === ErrorCode$1.ValidationFailed || code === ErrorCode$1.RunCursorInvalid) return 400;
|
|
623
|
+
return 500;
|
|
624
|
+
}
|
|
625
|
+
var AccessDenialReason = /* @__PURE__ */ function(AccessDenialReason) {
|
|
626
|
+
AccessDenialReason["ApplicationDenied"] = "application_denied";
|
|
627
|
+
AccessDenialReason["AuthorizationHeader"] = "authorization_header";
|
|
628
|
+
AccessDenialReason["VerificationUnconfigured"] = "verification_unconfigured";
|
|
629
|
+
AccessDenialReason["TokenScope"] = "token_scope";
|
|
630
|
+
return AccessDenialReason;
|
|
631
|
+
}(AccessDenialReason || {});
|
|
632
|
+
const accessDenialReasonSchema = z$1.enum(AccessDenialReason);
|
|
633
|
+
function accessDenied(reason = "application_denied") {
|
|
634
|
+
return new RunlaneError$1({
|
|
635
|
+
code: ErrorCode$1.AccessDenied,
|
|
636
|
+
message: "Run access denied.",
|
|
637
|
+
meta: { reason }
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
function invalidRequest(message, cause) {
|
|
641
|
+
return new RunlaneError$1({
|
|
642
|
+
code: ErrorCode$1.ValidationFailed,
|
|
643
|
+
message,
|
|
644
|
+
retryable: false,
|
|
645
|
+
cause
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function invalidCursor() {
|
|
649
|
+
return new RunlaneError$1({
|
|
650
|
+
code: ErrorCode$1.RunCursorInvalid,
|
|
651
|
+
message: "Run cursor is invalid or conflicts with after.",
|
|
652
|
+
retryable: false
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
async function requestToken(request, runId, options, verification) {
|
|
656
|
+
const supplied = request.headers.get("authorization");
|
|
657
|
+
if (supplied === null) return void 0;
|
|
658
|
+
const match = /^Bearer ([a-z0-9_.-]+)$/i.exec(supplied);
|
|
659
|
+
if (match?.[1] === void 0) throw accessDenied("authorization_header");
|
|
660
|
+
if (verification === void 0) throw accessDenied("verification_unconfigured");
|
|
661
|
+
const token = await verifyRunToken(match[1], verification, options, new URL(request.url).pathname.endsWith("/events") ? "subscribe" : "read");
|
|
662
|
+
if (token.runId !== runId || token.environment.name !== options.runlane.environment.name) throw accessDenied("token_scope");
|
|
663
|
+
return token;
|
|
664
|
+
}
|
|
665
|
+
export { ErrorCode, RunReadPermission, RunlaneError, createRunlaneRealtime };
|
|
666
|
+
|
|
667
|
+
//# sourceMappingURL=index.mjs.map
|