@runlane/realtime 0.0.0-stage → 0.5.0

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/dist/index.mjs ADDED
@@ -0,0 +1,664 @@
1
+ import { n as realtimeTiming, r as invalidOptions, t as realtimeClock } from "./time-sHQYGDPD.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") || url.searchParams.getAll("after").length > 1) throw invalidRequest("Only one after query parameter is 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
+ return {
339
+ runId,
340
+ ...name === void 0 ? {} : { name },
341
+ ...parseResumeCursor(request, url)
342
+ };
343
+ }
344
+ function parseResumeCursor(request, url) {
345
+ const header = request.headers.get("last-event-id");
346
+ const query = url.searchParams.get("after");
347
+ if (header !== null && query !== null && header !== query) throw invalidCursor();
348
+ const raw = header ?? query;
349
+ const parsed = raw === null ? void 0 : runCursorSchema.safeParse(raw);
350
+ if (parsed !== void 0 && !parsed.success) throw invalidCursor();
351
+ return parsed?.success ? { after: parsed.data } : {};
352
+ }
353
+ async function openConnection(options, request, route, grant, token) {
354
+ const started = realtimeClock.now();
355
+ const operation = route.name === void 0 ? "subscribe" : "read";
356
+ const control = new AbortController();
357
+ const expires = Math.min(started + realtimeTiming.connectionDuration, token === void 0 ? Infinity : token.expiresAt * 1e3);
358
+ const stop = () => control.abort();
359
+ request.signal.addEventListener("abort", stop, { once: true });
360
+ if (request.signal.aborted) stop();
361
+ const expiry = setTimeout(stop, Math.max(0, expires - realtimeClock.now()));
362
+ const frames = sourceFrames(options.runlane, route, grant, control.signal);
363
+ let pending;
364
+ let cursor = route.after;
365
+ let closed = false;
366
+ const finish = () => {
367
+ if (closed) return;
368
+ closed = true;
369
+ clearTimeout(expiry);
370
+ request.signal.removeEventListener("abort", stop);
371
+ control.abort();
372
+ frames.return().catch(() => void 0);
373
+ recordRunlaneTelemetryMetric(options, {
374
+ name: RunlaneMetricName.RealtimeConnectionDurationMs,
375
+ value: Math.max(0, realtimeClock.now() - started),
376
+ attributes: { [RunlaneMetricAttribute.Operation]: operation }
377
+ });
378
+ };
379
+ const next = async () => {
380
+ if (control.signal.aborted || realtimeClock.now() >= expires) return void 0;
381
+ pending ??= runWithRunlaneTelemetrySpan(options, { name: route.name === void 0 ? RunlaneSpanName.RealtimeSubscribe : RunlaneSpanName.RealtimeRead }, () => frames.next()).catch((cause) => {
382
+ const { cursor, ...error } = publicSourceError(cause);
383
+ throw new RunlaneError$1({
384
+ ...error,
385
+ cause,
386
+ ...cursor === void 0 ? {} : { meta: { cursor } }
387
+ });
388
+ });
389
+ const timer = new AbortController();
390
+ const abortTimer = () => timer.abort();
391
+ control.signal.addEventListener("abort", abortTimer, { once: true });
392
+ try {
393
+ const result = await Promise.race([pending, waitForAbortableTimeout(realtimeTiming.heartbeatInterval, timer.signal).then(() => void 0)]);
394
+ if (realtimeClock.now() >= expires) return void 0;
395
+ if (result === void 0) return encoder.encode(encode({ comment: "heartbeat" }));
396
+ pending = void 0;
397
+ if (result.done) return void 0;
398
+ const frame = result.value;
399
+ const bytes = encodeFrame(frame, cursor);
400
+ cursor = frame.cursor ?? cursor;
401
+ return bytes;
402
+ } finally {
403
+ timer.abort();
404
+ control.signal.removeEventListener("abort", abortTimer);
405
+ }
406
+ };
407
+ let first;
408
+ try {
409
+ first = await next();
410
+ } catch (cause) {
411
+ finish();
412
+ throw cause;
413
+ }
414
+ const body = new ReadableStream({
415
+ start(controller) {
416
+ const end = () => {
417
+ if (closed) return;
418
+ controller.close();
419
+ finish();
420
+ };
421
+ control.signal.addEventListener("abort", end, { once: true });
422
+ if (control.signal.aborted) end();
423
+ },
424
+ async pull(controller) {
425
+ try {
426
+ const bytes = control.signal.aborted || realtimeClock.now() >= expires ? void 0 : first ?? await next();
427
+ if (closed) return;
428
+ first = void 0;
429
+ if (bytes === void 0) {
430
+ controller.close();
431
+ finish();
432
+ return;
433
+ }
434
+ controller.enqueue(bytes);
435
+ } catch (cause) {
436
+ if (closed) return;
437
+ controller.enqueue(encoder.encode(encode({
438
+ event: "error",
439
+ data: JSON.stringify(publicError(cause, cursor))
440
+ })));
441
+ controller.close();
442
+ finish();
443
+ }
444
+ },
445
+ cancel() {
446
+ finish();
447
+ }
448
+ }, { highWaterMark: 0 });
449
+ return new Response(body, { headers: {
450
+ "content-type": "text/event-stream; charset=utf-8",
451
+ "cache-control": "no-store",
452
+ "x-accel-buffering": "no"
453
+ } });
454
+ }
455
+ function encodeFrame(frame, cursor) {
456
+ const data = JSON.stringify(frame.data);
457
+ const bytes = encoder.encode(encode({
458
+ event: frame.event,
459
+ data,
460
+ ...frame.cursor === void 0 ? {} : { id: frame.cursor }
461
+ }));
462
+ if (bytes.byteLength > realtimeDefaults.maxEventBytes) throw new RunlaneError$1({
463
+ code: ErrorCode$1.EventTooLarge,
464
+ message: "Realtime event exceeds the serialized UTF-8 byte limit.",
465
+ retryable: false,
466
+ ...cursor === void 0 ? {} : { meta: { cursor } }
467
+ });
468
+ return bytes;
469
+ }
470
+ async function* sourceFrames(source, route, grant, signal) {
471
+ if (route.name === void 0) {
472
+ for await (const event of source.runs.subscribe(route.runId, {
473
+ after: route.after,
474
+ signal,
475
+ streams: grant.permissions.includes(RunReadPermission$1.Streams) ? grant.streams : false
476
+ })) {
477
+ if (signal.aborted) return;
478
+ const projected = projectEvent(event, grant);
479
+ yield projected === void 0 ? {
480
+ event: "checkpoint",
481
+ cursor: event.cursor,
482
+ data: {}
483
+ } : {
484
+ event: projected.type,
485
+ cursor: projected.cursor,
486
+ data: projected
487
+ };
488
+ }
489
+ if (!signal.aborted) yield {
490
+ event: "end",
491
+ data: {}
492
+ };
493
+ return;
494
+ }
495
+ yield* namedFrames(source, route.runId, route.name, route.after, signal);
496
+ }
497
+ async function* namedFrames(source, runId, name, initial, signal) {
498
+ let after = initial;
499
+ let terminal = false;
500
+ while (!signal.aborted) {
501
+ const controller = new AbortController();
502
+ const waiting = source.lane?.runSignal?.wait({
503
+ environment: source.environment,
504
+ runId,
505
+ signal: AbortSignal.any([signal, controller.signal]),
506
+ timeoutMs: pollInterval
507
+ }).catch((cause) => {
508
+ throw new RunlaneError$1({
509
+ code: ErrorCode$1.AdapterContractViolation,
510
+ message: "Lane run signal wait must not reject.",
511
+ cause
512
+ });
513
+ });
514
+ waiting?.catch(() => void 0);
515
+ try {
516
+ const page = await source.runs.readStream(runId, name, {
517
+ after,
518
+ limit: contractDefaults.pagination.defaultLimit
519
+ });
520
+ yield* namedPageFrames(page, after);
521
+ after = page.cursor;
522
+ if (page.hasMore) continue;
523
+ if (terminal && page.records.length === 0) {
524
+ yield {
525
+ event: "end",
526
+ data: {}
527
+ };
528
+ return;
529
+ }
530
+ terminal = await readNamedRunTerminal(source, runId);
531
+ if (terminal) continue;
532
+ if (page.records.length === 0) await (waiting ?? waitForAbortableTimeout(pollInterval, signal));
533
+ } finally {
534
+ controller.abort();
535
+ await waiting;
536
+ }
537
+ }
538
+ }
539
+ async function readNamedRunTerminal(source, runId) {
540
+ const run = await source.runs.get(runId);
541
+ if (run === void 0) throw new RunlaneError$1({
542
+ code: ErrorCode$1.RunNotFound,
543
+ message: "Run was not found.",
544
+ retryable: false
545
+ });
546
+ return runStatusValues.isTerminal(run.status);
547
+ }
548
+ function* namedPageFrames(page, after) {
549
+ for (const [index, record] of page.records.entries()) {
550
+ const last = index === page.records.length - 1;
551
+ yield {
552
+ event: "stream",
553
+ ...last ? { cursor: page.cursor } : {},
554
+ data: {
555
+ ...last ? { cursor: page.cursor } : {},
556
+ record
557
+ }
558
+ };
559
+ }
560
+ if (page.records.length === 0 && page.cursor !== after) yield {
561
+ event: "checkpoint",
562
+ cursor: page.cursor,
563
+ data: {}
564
+ };
565
+ }
566
+ function publicError(cause, cursor) {
567
+ const checkpoint = cursor ?? (cause instanceof RunlaneError$1 ? runCursorSchema.safeParse(cause.meta?.cursor).data : void 0);
568
+ const resume = checkpoint === void 0 ? {} : { cursor: checkpoint };
569
+ if (cause instanceof RunlaneError$1) {
570
+ const code = runRealtimeErrorCodeSchema.safeParse(cause.code);
571
+ if (code.success && code.data !== ErrorCode$1.InternalError) return {
572
+ code: code.data,
573
+ message: cause.message,
574
+ retryable: cause.retryable,
575
+ ...resume
576
+ };
577
+ }
578
+ return {
579
+ code: ErrorCode$1.InternalError,
580
+ message: "Realtime request failed.",
581
+ retryable: true,
582
+ ...resume
583
+ };
584
+ }
585
+ function publicSourceError(cause) {
586
+ const error = publicError(cause);
587
+ if (cause instanceof RunlaneError$1) switch (cause.code) {
588
+ case ErrorCode$1.ConfigurationInvalid:
589
+ case ErrorCode$1.ValidationFailed: return {
590
+ ...error,
591
+ code: ErrorCode$1.ValidationFailed,
592
+ message: "Realtime request is invalid.",
593
+ retryable: false
594
+ };
595
+ case ErrorCode$1.RunCursorInvalid: return {
596
+ ...error,
597
+ code: ErrorCode$1.RunCursorInvalid,
598
+ message: "Run cursor is invalid.",
599
+ retryable: false
600
+ };
601
+ case ErrorCode$1.RunNotFound: return {
602
+ ...error,
603
+ code: ErrorCode$1.RunNotFound,
604
+ message: "Run was not found.",
605
+ retryable: false
606
+ };
607
+ case ErrorCode$1.OperationCancelled: return {
608
+ ...error,
609
+ code: ErrorCode$1.Disconnected,
610
+ message: "Realtime runtime is closing; reconnect to resume.",
611
+ retryable: true
612
+ };
613
+ }
614
+ return error;
615
+ }
616
+ function httpStatus(code) {
617
+ if (code === ErrorCode$1.AccessDenied) return 403;
618
+ if (code === ErrorCode$1.RunNotFound) return 404;
619
+ if (code === ErrorCode$1.ValidationFailed || code === ErrorCode$1.RunCursorInvalid) return 400;
620
+ return 500;
621
+ }
622
+ var AccessDenialReason = /* @__PURE__ */ function(AccessDenialReason) {
623
+ AccessDenialReason["ApplicationDenied"] = "application_denied";
624
+ AccessDenialReason["AuthorizationHeader"] = "authorization_header";
625
+ AccessDenialReason["VerificationUnconfigured"] = "verification_unconfigured";
626
+ AccessDenialReason["TokenScope"] = "token_scope";
627
+ return AccessDenialReason;
628
+ }(AccessDenialReason || {});
629
+ const accessDenialReasonSchema = z$1.enum(AccessDenialReason);
630
+ function accessDenied(reason = "application_denied") {
631
+ return new RunlaneError$1({
632
+ code: ErrorCode$1.AccessDenied,
633
+ message: "Run access denied.",
634
+ meta: { reason }
635
+ });
636
+ }
637
+ function invalidRequest(message, cause) {
638
+ return new RunlaneError$1({
639
+ code: ErrorCode$1.ValidationFailed,
640
+ message,
641
+ retryable: false,
642
+ cause
643
+ });
644
+ }
645
+ function invalidCursor() {
646
+ return new RunlaneError$1({
647
+ code: ErrorCode$1.RunCursorInvalid,
648
+ message: "Run cursor is invalid or conflicts with after.",
649
+ retryable: false
650
+ });
651
+ }
652
+ async function requestToken(request, runId, options, verification) {
653
+ const supplied = request.headers.get("authorization");
654
+ if (supplied === null) return void 0;
655
+ const match = /^Bearer ([a-z0-9_.-]+)$/i.exec(supplied);
656
+ if (match?.[1] === void 0) throw accessDenied("authorization_header");
657
+ if (verification === void 0) throw accessDenied("verification_unconfigured");
658
+ const token = await verifyRunToken(match[1], verification, options, new URL(request.url).pathname.endsWith("/events") ? "subscribe" : "read");
659
+ if (token.runId !== runId || token.environment.name !== options.runlane.environment.name) throw accessDenied("token_scope");
660
+ return token;
661
+ }
662
+ export { ErrorCode, RunReadPermission, RunlaneError, createRunlaneRealtime };
663
+
664
+ //# sourceMappingURL=index.mjs.map