@starcite/sdk 0.0.3 → 0.0.4
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/README.md +244 -182
- package/dist/index.cjs +1640 -645
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +589 -204
- package/dist/index.d.ts +589 -204
- package/dist/index.js +1618 -641
- package/dist/index.js.map +1 -1
- package/package.json +15 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,106 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/auth.ts
|
|
2
|
+
import { decodeJwt } from "jose";
|
|
2
3
|
import { z as z2 } from "zod";
|
|
3
4
|
|
|
5
|
+
// src/identity.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var AGENT_PREFIX = "agent:";
|
|
8
|
+
var USER_PREFIX = "user:";
|
|
9
|
+
var PrincipalTypeSchema = z.enum(["user", "agent"]);
|
|
10
|
+
var SessionCreatorPrincipalSchema = z.object({
|
|
11
|
+
tenant_id: z.string().min(1),
|
|
12
|
+
id: z.string().min(1),
|
|
13
|
+
type: PrincipalTypeSchema
|
|
14
|
+
});
|
|
15
|
+
var SessionTokenPrincipalSchema = z.object({
|
|
16
|
+
type: PrincipalTypeSchema,
|
|
17
|
+
id: z.string().min(1)
|
|
18
|
+
});
|
|
19
|
+
var StarciteIdentity = class {
|
|
20
|
+
tenantId;
|
|
21
|
+
id;
|
|
22
|
+
type;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.tenantId = options.tenantId;
|
|
25
|
+
this.id = options.id;
|
|
26
|
+
this.type = options.type;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Serializes to the `creator_principal` wire format expected by the API.
|
|
30
|
+
*/
|
|
31
|
+
toCreatorPrincipal() {
|
|
32
|
+
return { tenant_id: this.tenantId, id: this.id, type: this.type };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Serializes to the `principal` wire format used in session token requests.
|
|
36
|
+
*/
|
|
37
|
+
toTokenPrincipal() {
|
|
38
|
+
return { id: this.id, type: this.type };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the actor string derived from this identity (e.g. `agent:planner`, `user:alice`).
|
|
42
|
+
*/
|
|
43
|
+
toActor() {
|
|
44
|
+
if (this.id.startsWith(AGENT_PREFIX) || this.id.startsWith(USER_PREFIX)) {
|
|
45
|
+
return this.id;
|
|
46
|
+
}
|
|
47
|
+
return `${this.type}:${this.id}`;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function agentFromActor(actor) {
|
|
51
|
+
return actor.startsWith(AGENT_PREFIX) ? actor.slice(AGENT_PREFIX.length) : void 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/auth.ts
|
|
55
|
+
var ApiKeyClaimsSchema = z2.object({
|
|
56
|
+
iss: z2.string().min(1).optional(),
|
|
57
|
+
sub: z2.string().min(1).optional(),
|
|
58
|
+
tenant_id: z2.string().min(1).optional(),
|
|
59
|
+
principal_id: z2.string().min(1).optional(),
|
|
60
|
+
principal_type: PrincipalTypeSchema.optional()
|
|
61
|
+
});
|
|
62
|
+
var SessionTokenClaimsSchema = z2.object({
|
|
63
|
+
session_id: z2.string().min(1).optional(),
|
|
64
|
+
sub: z2.string().min(1).optional(),
|
|
65
|
+
tenant_id: z2.string().min(1),
|
|
66
|
+
principal_id: z2.string().min(1).optional(),
|
|
67
|
+
principal_type: PrincipalTypeSchema.optional()
|
|
68
|
+
});
|
|
69
|
+
function inferIssuerAuthorityFromApiKey(apiKey) {
|
|
70
|
+
const claims = ApiKeyClaimsSchema.parse(decodeJwt(apiKey));
|
|
71
|
+
if (!claims.iss) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
const url = new URL(claims.iss);
|
|
75
|
+
return url.origin;
|
|
76
|
+
}
|
|
77
|
+
function inferIdentityFromApiKey(apiKey) {
|
|
78
|
+
const claims = ApiKeyClaimsSchema.parse(decodeJwt(apiKey));
|
|
79
|
+
const id = claims.principal_id ?? claims.sub;
|
|
80
|
+
const tenantId = claims.tenant_id;
|
|
81
|
+
if (!(tenantId && id && claims.principal_type)) {
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
return new StarciteIdentity({
|
|
85
|
+
tenantId,
|
|
86
|
+
id,
|
|
87
|
+
type: claims.principal_type
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function decodeSessionToken(token) {
|
|
91
|
+
const claims = SessionTokenClaimsSchema.parse(decodeJwt(token));
|
|
92
|
+
const principalId = claims.principal_id ?? claims.sub ?? "session-user";
|
|
93
|
+
const principalType = claims.principal_type ?? "user";
|
|
94
|
+
return {
|
|
95
|
+
sessionId: claims.session_id,
|
|
96
|
+
identity: new StarciteIdentity({
|
|
97
|
+
tenantId: claims.tenant_id,
|
|
98
|
+
id: principalId,
|
|
99
|
+
type: principalType
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
4
104
|
// src/errors.ts
|
|
5
105
|
var StarciteError = class extends Error {
|
|
6
106
|
constructor(message) {
|
|
@@ -29,797 +129,1674 @@ var StarciteConnectionError = class extends StarciteError {
|
|
|
29
129
|
this.name = "StarciteConnectionError";
|
|
30
130
|
}
|
|
31
131
|
};
|
|
132
|
+
var StarciteTailError = class extends StarciteConnectionError {
|
|
133
|
+
/** Session id tied to this tail stream. */
|
|
134
|
+
sessionId;
|
|
135
|
+
/** Failure stage in the tail lifecycle. */
|
|
136
|
+
stage;
|
|
137
|
+
/** Reconnect attempts observed before failing. */
|
|
138
|
+
attempts;
|
|
139
|
+
/** WebSocket close code when available. */
|
|
140
|
+
closeCode;
|
|
141
|
+
/** WebSocket close reason when available. */
|
|
142
|
+
closeReason;
|
|
143
|
+
constructor(message, options) {
|
|
144
|
+
super(message);
|
|
145
|
+
this.name = "StarciteTailError";
|
|
146
|
+
this.sessionId = options.sessionId;
|
|
147
|
+
this.stage = options.stage;
|
|
148
|
+
this.attempts = options.attempts ?? 0;
|
|
149
|
+
this.closeCode = options.closeCode;
|
|
150
|
+
this.closeReason = options.closeReason;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var StarciteTokenExpiredError = class extends StarciteTailError {
|
|
154
|
+
constructor(message, options) {
|
|
155
|
+
super(message, { ...options, stage: "stream" });
|
|
156
|
+
this.name = "StarciteTokenExpiredError";
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var StarciteBackpressureError = class extends StarciteTailError {
|
|
160
|
+
constructor(message, options) {
|
|
161
|
+
super(message, { ...options, stage: "consumer_backpressure" });
|
|
162
|
+
this.name = "StarciteBackpressureError";
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var StarciteRetryLimitError = class extends StarciteTailError {
|
|
166
|
+
constructor(message, options) {
|
|
167
|
+
super(message, { ...options, stage: "retry_limit" });
|
|
168
|
+
this.name = "StarciteRetryLimitError";
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/session.ts
|
|
173
|
+
import EventEmitter3 from "eventemitter3";
|
|
174
|
+
|
|
175
|
+
// src/session-log.ts
|
|
176
|
+
import EventEmitter from "eventemitter3";
|
|
177
|
+
var SessionLogGapError = class extends StarciteError {
|
|
178
|
+
constructor(message) {
|
|
179
|
+
super(message);
|
|
180
|
+
this.name = "SessionLogGapError";
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var SessionLogConflictError = class extends StarciteError {
|
|
184
|
+
constructor(message) {
|
|
185
|
+
super(message);
|
|
186
|
+
this.name = "SessionLogConflictError";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var SessionLog = class {
|
|
190
|
+
history = [];
|
|
191
|
+
emitter = new EventEmitter();
|
|
192
|
+
canonicalBySeq = /* @__PURE__ */ new Map();
|
|
193
|
+
maxEvents;
|
|
194
|
+
appliedSeq = 0;
|
|
195
|
+
constructor(options = {}) {
|
|
196
|
+
this.setMaxEvents(options.maxEvents);
|
|
197
|
+
}
|
|
198
|
+
setMaxEvents(maxEvents) {
|
|
199
|
+
if (maxEvents !== void 0 && (!Number.isInteger(maxEvents) || maxEvents <= 0)) {
|
|
200
|
+
throw new StarciteError(
|
|
201
|
+
"Session log maxEvents must be a positive integer"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
this.maxEvents = maxEvents;
|
|
205
|
+
this.enforceRetention();
|
|
206
|
+
}
|
|
207
|
+
applyBatch(batch) {
|
|
208
|
+
const applied = [];
|
|
209
|
+
for (const event of batch) {
|
|
210
|
+
if (this.apply(event)) {
|
|
211
|
+
applied.push(event);
|
|
212
|
+
this.emitter.emit("event", event);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return applied;
|
|
216
|
+
}
|
|
217
|
+
hydrate(state) {
|
|
218
|
+
if (!Number.isInteger(state.cursor) || state.cursor < 0) {
|
|
219
|
+
throw new StarciteError(
|
|
220
|
+
"Session store cursor must be a non-negative integer"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
this.history.length = 0;
|
|
224
|
+
this.canonicalBySeq.clear();
|
|
225
|
+
this.appliedSeq = state.cursor;
|
|
226
|
+
let previousSeq;
|
|
227
|
+
for (const event of state.events) {
|
|
228
|
+
if (event.seq > state.cursor) {
|
|
229
|
+
throw new StarciteError(
|
|
230
|
+
`Session store contains event seq ${event.seq} above cursor ${state.cursor}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (previousSeq !== void 0 && event.seq !== previousSeq + 1) {
|
|
234
|
+
throw new StarciteError(
|
|
235
|
+
`Session store events must be contiguous; saw seq ${event.seq} after ${previousSeq}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
this.history.push(event);
|
|
239
|
+
this.canonicalBySeq.set(event.seq, JSON.stringify(event));
|
|
240
|
+
previousSeq = event.seq;
|
|
241
|
+
}
|
|
242
|
+
this.enforceRetention();
|
|
243
|
+
}
|
|
244
|
+
subscribe(listener, options = {}) {
|
|
245
|
+
const shouldReplay = options.replay ?? true;
|
|
246
|
+
if (shouldReplay) {
|
|
247
|
+
for (const event of this.history) {
|
|
248
|
+
listener(event);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this.emitter.on("event", listener);
|
|
252
|
+
return () => {
|
|
253
|
+
this.emitter.off("event", listener);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
apply(event) {
|
|
257
|
+
const existingCanonical = this.canonicalBySeq.get(event.seq);
|
|
258
|
+
if (event.seq <= this.appliedSeq) {
|
|
259
|
+
const incomingCanonical = JSON.stringify(event);
|
|
260
|
+
if (!existingCanonical) {
|
|
261
|
+
const oldestRetainedSeq = this.history[0]?.seq;
|
|
262
|
+
if (oldestRetainedSeq === void 0 || event.seq < oldestRetainedSeq) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
throw new SessionLogConflictError(
|
|
266
|
+
`Session log has no canonical payload for retained seq ${event.seq}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (incomingCanonical !== existingCanonical) {
|
|
270
|
+
throw new SessionLogConflictError(
|
|
271
|
+
`Session log conflict for seq ${event.seq}: received different payload for an already-applied event`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const expectedSeq = this.appliedSeq + 1;
|
|
277
|
+
if (event.seq !== expectedSeq) {
|
|
278
|
+
throw new SessionLogGapError(
|
|
279
|
+
`Session log gap detected: expected seq ${expectedSeq} but received ${event.seq}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
this.history.push(event);
|
|
283
|
+
this.canonicalBySeq.set(event.seq, JSON.stringify(event));
|
|
284
|
+
this.appliedSeq = event.seq;
|
|
285
|
+
this.enforceRetention();
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
getSnapshot(syncing) {
|
|
289
|
+
return {
|
|
290
|
+
events: this.history.slice(),
|
|
291
|
+
lastSeq: this.appliedSeq,
|
|
292
|
+
syncing
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
get events() {
|
|
296
|
+
return this.history.slice();
|
|
297
|
+
}
|
|
298
|
+
get cursor() {
|
|
299
|
+
return this.appliedSeq;
|
|
300
|
+
}
|
|
301
|
+
get lastSeq() {
|
|
302
|
+
return this.appliedSeq;
|
|
303
|
+
}
|
|
304
|
+
enforceRetention() {
|
|
305
|
+
if (this.maxEvents === void 0) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
while (this.history.length > this.maxEvents) {
|
|
309
|
+
const removed = this.history.shift();
|
|
310
|
+
if (!removed) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.canonicalBySeq.delete(removed.seq);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/tail/frame.ts
|
|
319
|
+
import { z as z4 } from "zod";
|
|
32
320
|
|
|
33
321
|
// src/types.ts
|
|
34
|
-
import { z } from "zod";
|
|
35
|
-
var
|
|
36
|
-
var
|
|
37
|
-
var
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
322
|
+
import { z as z3 } from "zod";
|
|
323
|
+
var SessionCreatorPrincipalSchema2 = SessionCreatorPrincipalSchema;
|
|
324
|
+
var SessionTokenPrincipalSchema2 = SessionTokenPrincipalSchema;
|
|
325
|
+
var ArbitraryObjectSchema = z3.record(z3.unknown());
|
|
326
|
+
var SessionTokenScopeSchema = z3.enum(["session:read", "session:append"]);
|
|
327
|
+
var IssueSessionTokenInputSchema = z3.object({
|
|
328
|
+
session_id: z3.string().min(1),
|
|
329
|
+
principal: SessionTokenPrincipalSchema2,
|
|
330
|
+
scopes: z3.array(SessionTokenScopeSchema).min(1),
|
|
331
|
+
ttl_seconds: z3.number().int().positive().max(24 * 60 * 60).optional()
|
|
41
332
|
});
|
|
42
|
-
var
|
|
43
|
-
|
|
44
|
-
|
|
333
|
+
var IssueSessionTokenResponseSchema = z3.object({
|
|
334
|
+
token: z3.string().min(1),
|
|
335
|
+
expires_in: z3.number().int().positive()
|
|
336
|
+
});
|
|
337
|
+
var CreateSessionInputSchema = z3.object({
|
|
338
|
+
id: z3.string().optional(),
|
|
339
|
+
title: z3.string().optional(),
|
|
45
340
|
metadata: ArbitraryObjectSchema.optional(),
|
|
46
|
-
creator_principal:
|
|
341
|
+
creator_principal: SessionCreatorPrincipalSchema2.optional()
|
|
47
342
|
});
|
|
48
|
-
var SessionRecordSchema =
|
|
49
|
-
id:
|
|
50
|
-
title:
|
|
343
|
+
var SessionRecordSchema = z3.object({
|
|
344
|
+
id: z3.string(),
|
|
345
|
+
title: z3.string().nullable().optional(),
|
|
51
346
|
metadata: ArbitraryObjectSchema,
|
|
52
|
-
last_seq:
|
|
53
|
-
created_at:
|
|
54
|
-
updated_at:
|
|
347
|
+
last_seq: z3.number().int().nonnegative(),
|
|
348
|
+
created_at: z3.string(),
|
|
349
|
+
updated_at: z3.string()
|
|
55
350
|
});
|
|
56
|
-
var SessionListItemSchema =
|
|
57
|
-
id:
|
|
58
|
-
title:
|
|
351
|
+
var SessionListItemSchema = z3.object({
|
|
352
|
+
id: z3.string(),
|
|
353
|
+
title: z3.string().nullable().optional(),
|
|
59
354
|
metadata: ArbitraryObjectSchema,
|
|
60
|
-
created_at:
|
|
355
|
+
created_at: z3.string()
|
|
61
356
|
});
|
|
62
|
-
var SessionListPageSchema =
|
|
63
|
-
sessions:
|
|
64
|
-
next_cursor:
|
|
357
|
+
var SessionListPageSchema = z3.object({
|
|
358
|
+
sessions: z3.array(SessionListItemSchema),
|
|
359
|
+
next_cursor: z3.string().nullable()
|
|
65
360
|
});
|
|
66
|
-
var AppendEventRequestSchema =
|
|
67
|
-
type:
|
|
361
|
+
var AppendEventRequestSchema = z3.object({
|
|
362
|
+
type: z3.string().min(1),
|
|
68
363
|
payload: ArbitraryObjectSchema,
|
|
69
|
-
actor:
|
|
70
|
-
producer_id:
|
|
71
|
-
producer_seq:
|
|
72
|
-
source:
|
|
364
|
+
actor: z3.string().min(1).optional(),
|
|
365
|
+
producer_id: z3.string().min(1),
|
|
366
|
+
producer_seq: z3.number().int().positive(),
|
|
367
|
+
source: z3.string().optional(),
|
|
73
368
|
metadata: ArbitraryObjectSchema.optional(),
|
|
74
369
|
refs: ArbitraryObjectSchema.optional(),
|
|
75
|
-
idempotency_key:
|
|
76
|
-
expected_seq:
|
|
370
|
+
idempotency_key: z3.string().optional(),
|
|
371
|
+
expected_seq: z3.number().int().nonnegative().optional()
|
|
77
372
|
});
|
|
78
|
-
var AppendEventResponseSchema =
|
|
79
|
-
seq:
|
|
80
|
-
last_seq:
|
|
81
|
-
deduped:
|
|
373
|
+
var AppendEventResponseSchema = z3.object({
|
|
374
|
+
seq: z3.number().int().nonnegative(),
|
|
375
|
+
last_seq: z3.number().int().nonnegative(),
|
|
376
|
+
deduped: z3.boolean()
|
|
82
377
|
});
|
|
83
|
-
var TailEventSchema =
|
|
84
|
-
seq:
|
|
85
|
-
type:
|
|
378
|
+
var TailEventSchema = z3.object({
|
|
379
|
+
seq: z3.number().int().nonnegative(),
|
|
380
|
+
type: z3.string().min(1),
|
|
86
381
|
payload: ArbitraryObjectSchema,
|
|
87
|
-
actor:
|
|
88
|
-
producer_id:
|
|
89
|
-
producer_seq:
|
|
90
|
-
source:
|
|
382
|
+
actor: z3.string().min(1),
|
|
383
|
+
producer_id: z3.string().min(1),
|
|
384
|
+
producer_seq: z3.number().int().positive(),
|
|
385
|
+
source: z3.string().optional(),
|
|
91
386
|
metadata: ArbitraryObjectSchema.optional(),
|
|
92
387
|
refs: ArbitraryObjectSchema.optional(),
|
|
93
|
-
idempotency_key:
|
|
94
|
-
inserted_at:
|
|
95
|
-
});
|
|
96
|
-
var SessionEventInternalSchema = TailEventSchema.extend({
|
|
97
|
-
agent: z.string().optional(),
|
|
98
|
-
text: z.string().optional()
|
|
388
|
+
idempotency_key: z3.string().nullable().optional(),
|
|
389
|
+
inserted_at: z3.string().optional()
|
|
99
390
|
});
|
|
100
|
-
var SessionAppendInputSchema =
|
|
101
|
-
|
|
102
|
-
producerId: z.string().trim().min(1),
|
|
103
|
-
producerSeq: z.number().int().positive(),
|
|
104
|
-
text: z.string().optional(),
|
|
391
|
+
var SessionAppendInputSchema = z3.object({
|
|
392
|
+
text: z3.string().optional(),
|
|
105
393
|
payload: ArbitraryObjectSchema.optional(),
|
|
106
|
-
type:
|
|
107
|
-
|
|
394
|
+
type: z3.string().optional(),
|
|
395
|
+
actor: z3.string().trim().min(1).optional(),
|
|
396
|
+
source: z3.string().optional(),
|
|
108
397
|
metadata: ArbitraryObjectSchema.optional(),
|
|
109
398
|
refs: ArbitraryObjectSchema.optional(),
|
|
110
|
-
idempotencyKey:
|
|
111
|
-
expectedSeq:
|
|
399
|
+
idempotencyKey: z3.string().optional(),
|
|
400
|
+
expectedSeq: z3.number().int().nonnegative().optional()
|
|
112
401
|
}).refine((value) => !!(value.text || value.payload), {
|
|
113
402
|
message: "append() requires either 'text' or an object 'payload'"
|
|
114
403
|
});
|
|
115
|
-
var
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
404
|
+
var SessionListOptionsSchema = z3.object({
|
|
405
|
+
limit: z3.number().int().positive().optional(),
|
|
406
|
+
cursor: z3.string().trim().min(1).optional(),
|
|
407
|
+
metadata: z3.record(z3.string().trim().min(1), z3.string().trim().min(1)).optional()
|
|
408
|
+
});
|
|
119
409
|
|
|
120
|
-
// src/
|
|
121
|
-
var
|
|
122
|
-
var
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
410
|
+
// src/tail/frame.ts
|
|
411
|
+
var MIN_TAIL_BATCH_SIZE = 1;
|
|
412
|
+
var TailFramePayloadSchema = z4.union([
|
|
413
|
+
TailEventSchema,
|
|
414
|
+
z4.array(TailEventSchema).min(MIN_TAIL_BATCH_SIZE)
|
|
415
|
+
]);
|
|
416
|
+
function toFrameText(data) {
|
|
417
|
+
if (typeof data === "string") {
|
|
418
|
+
return data;
|
|
419
|
+
}
|
|
420
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
421
|
+
return new TextDecoder().decode(data);
|
|
422
|
+
}
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
425
|
+
function parseTailFrame(data) {
|
|
426
|
+
const frameText = toFrameText(data);
|
|
427
|
+
if (!frameText) {
|
|
428
|
+
throw new StarciteConnectionError(
|
|
429
|
+
"Tail frame payload must be a UTF-8 string or binary buffer"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
let framePayload;
|
|
131
433
|
try {
|
|
132
|
-
|
|
434
|
+
framePayload = JSON.parse(frameText);
|
|
133
435
|
} catch {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
436
|
+
throw new StarciteConnectionError("Tail frame was not valid JSON");
|
|
437
|
+
}
|
|
438
|
+
const result = TailFramePayloadSchema.safeParse(framePayload);
|
|
439
|
+
if (!result.success) {
|
|
440
|
+
const reason = result.error.issues[0]?.message ?? "Tail frame did not match schema";
|
|
441
|
+
throw new StarciteConnectionError(reason);
|
|
442
|
+
}
|
|
443
|
+
return Array.isArray(result.data) ? result.data : [result.data];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/tail/managed-websocket.ts
|
|
447
|
+
import EventEmitter2 from "eventemitter3";
|
|
448
|
+
var NORMAL_CLOSE_CODE = 1e3;
|
|
449
|
+
var INACTIVITY_CLOSE_CODE = 4e3;
|
|
450
|
+
var CONNECTION_TIMEOUT_CLOSE_CODE = 4100;
|
|
451
|
+
var ManagedWebSocket = class extends EventEmitter2 {
|
|
452
|
+
options;
|
|
453
|
+
socket;
|
|
454
|
+
cancelReconnectWait;
|
|
455
|
+
// Set while a socket run is active so `close()` can synchronously settle it.
|
|
456
|
+
forceCloseSocket;
|
|
457
|
+
started = false;
|
|
458
|
+
closed = false;
|
|
459
|
+
// Tracks reconnect attempts for backoff and retry-limit accounting.
|
|
460
|
+
reconnectAttempts = 0;
|
|
461
|
+
// Shared deferred completion for all callers of waitForClose(), even late ones.
|
|
462
|
+
donePromise;
|
|
463
|
+
// Set to `undefined` after resolution so finish() remains idempotent.
|
|
464
|
+
resolveDone;
|
|
465
|
+
constructor(options) {
|
|
466
|
+
super();
|
|
467
|
+
this.options = options;
|
|
468
|
+
this.donePromise = new Promise((resolve) => {
|
|
469
|
+
this.resolveDone = resolve;
|
|
137
470
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
var AsyncQueue = class {
|
|
142
|
-
items = [];
|
|
143
|
-
waiters = [];
|
|
144
|
-
settled = false;
|
|
145
|
-
push(value) {
|
|
146
|
-
if (this.settled) {
|
|
471
|
+
}
|
|
472
|
+
close(code = NORMAL_CLOSE_CODE, reason = "closed") {
|
|
473
|
+
if (this.closed) {
|
|
147
474
|
return;
|
|
148
475
|
}
|
|
149
|
-
this.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (this.
|
|
476
|
+
this.closed = true;
|
|
477
|
+
this.cancelReconnectWait?.();
|
|
478
|
+
this.cancelReconnectWait = void 0;
|
|
479
|
+
if (!this.started) {
|
|
480
|
+
this.finish({
|
|
481
|
+
closeCode: code,
|
|
482
|
+
closeReason: reason,
|
|
483
|
+
aborted: this.options.signal?.aborted ?? false,
|
|
484
|
+
graceful: code === NORMAL_CLOSE_CODE
|
|
485
|
+
});
|
|
153
486
|
return;
|
|
154
487
|
}
|
|
155
|
-
this.
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
fail(error) {
|
|
159
|
-
if (this.settled) {
|
|
488
|
+
if (this.forceCloseSocket) {
|
|
489
|
+
this.forceCloseSocket(code, reason);
|
|
160
490
|
return;
|
|
161
491
|
}
|
|
162
|
-
this.
|
|
163
|
-
this.enqueue({ type: "error", error: toError(error) });
|
|
492
|
+
this.socket?.close(code, reason);
|
|
164
493
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.waiters.push(resolve);
|
|
168
|
-
});
|
|
169
|
-
if (item.type === "value") {
|
|
170
|
-
return { value: item.value, done: false };
|
|
171
|
-
}
|
|
172
|
-
if (item.type === "done") {
|
|
173
|
-
return { value: void 0, done: true };
|
|
174
|
-
}
|
|
175
|
-
throw item.error;
|
|
494
|
+
resetReconnectAttempts() {
|
|
495
|
+
this.reconnectAttempts = 0;
|
|
176
496
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
497
|
+
waitForClose() {
|
|
498
|
+
this.start();
|
|
499
|
+
return this.donePromise;
|
|
500
|
+
}
|
|
501
|
+
start() {
|
|
502
|
+
if (this.started || this.resolveDone === void 0) {
|
|
181
503
|
return;
|
|
182
504
|
}
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
if (apiBaseUrl.startsWith("http://")) {
|
|
195
|
-
return `ws://${apiBaseUrl.slice("http://".length)}`;
|
|
505
|
+
this.started = true;
|
|
506
|
+
this.run().catch((error) => {
|
|
507
|
+
this.emitSafe("fatal", error);
|
|
508
|
+
this.finish({
|
|
509
|
+
closeCode: void 0,
|
|
510
|
+
closeReason: "run failed",
|
|
511
|
+
aborted: this.options.signal?.aborted ?? false,
|
|
512
|
+
graceful: false
|
|
513
|
+
});
|
|
514
|
+
});
|
|
196
515
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
516
|
+
async run() {
|
|
517
|
+
const finalState = {
|
|
518
|
+
closeCode: void 0,
|
|
519
|
+
closeReason: void 0,
|
|
520
|
+
aborted: this.options.signal?.aborted ?? false,
|
|
521
|
+
graceful: false
|
|
522
|
+
};
|
|
523
|
+
while (!this.closed) {
|
|
524
|
+
if (this.options.signal?.aborted) {
|
|
525
|
+
this.closed = true;
|
|
526
|
+
finalState.aborted = true;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
const attempt = this.reconnectAttempts + 1;
|
|
530
|
+
if (!this.emitSafe("connect_attempt", { attempt })) {
|
|
531
|
+
this.closed = true;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
if (this.closed) {
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
let socket;
|
|
538
|
+
try {
|
|
539
|
+
const url = this.options.url();
|
|
540
|
+
socket = this.options.websocketFactory(url);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
const shouldContinue2 = await this.handleConnectFailure(attempt, error);
|
|
543
|
+
if (shouldContinue2) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
if (this.closed) {
|
|
549
|
+
closeQuietly(socket, NORMAL_CLOSE_CODE, "closed");
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
const result = await this.runSocket(socket);
|
|
553
|
+
const shouldContinue = await this.handleSocketResult(
|
|
554
|
+
attempt,
|
|
555
|
+
result,
|
|
556
|
+
finalState
|
|
557
|
+
);
|
|
558
|
+
if (shouldContinue) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
this.finish(finalState);
|
|
206
564
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
565
|
+
async handleConnectFailure(attempt, error) {
|
|
566
|
+
const rootCause = toErrorMessage(error);
|
|
567
|
+
if (!this.emitSafe("connect_failed", { attempt, rootCause })) {
|
|
568
|
+
this.closed = true;
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
return await this.scheduleReconnect({
|
|
572
|
+
attempt,
|
|
573
|
+
trigger: "connect_failed",
|
|
574
|
+
rootCause
|
|
575
|
+
});
|
|
210
576
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
577
|
+
async handleSocketResult(attempt, result, finalState) {
|
|
578
|
+
finalState.closeCode = result.closeCode;
|
|
579
|
+
finalState.closeReason = result.closeReason;
|
|
580
|
+
finalState.aborted = result.aborted || (this.options.signal?.aborted ?? false);
|
|
581
|
+
finalState.graceful = !result.sawTransportError && result.closeCode === NORMAL_CLOSE_CODE;
|
|
582
|
+
if (result.listenerError !== void 0) {
|
|
583
|
+
this.emitSafe("fatal", result.listenerError);
|
|
584
|
+
this.closed = true;
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
if (this.closed || finalState.aborted || finalState.graceful) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (!this.emitSafe("dropped", {
|
|
591
|
+
attempt,
|
|
592
|
+
closeCode: result.closeCode,
|
|
593
|
+
closeReason: result.closeReason
|
|
594
|
+
})) {
|
|
595
|
+
this.closed = true;
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
return await this.scheduleReconnect({
|
|
599
|
+
attempt,
|
|
600
|
+
trigger: "dropped",
|
|
601
|
+
closeCode: result.closeCode,
|
|
602
|
+
closeReason: result.closeReason
|
|
603
|
+
});
|
|
218
604
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
605
|
+
async scheduleReconnect(input) {
|
|
606
|
+
if (!this.options.shouldReconnect || this.closed || this.options.signal?.aborted) {
|
|
607
|
+
this.closed = true;
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
if (input.attempt > this.options.reconnectPolicy.maxAttempts) {
|
|
611
|
+
this.emitSafe("retry_limit", {
|
|
612
|
+
attempt: input.attempt,
|
|
613
|
+
trigger: input.trigger,
|
|
614
|
+
closeCode: input.closeCode,
|
|
615
|
+
closeReason: input.closeReason,
|
|
616
|
+
rootCause: input.rootCause
|
|
617
|
+
});
|
|
618
|
+
this.closed = true;
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
const delayMs = reconnectDelayForAttempt(
|
|
622
|
+
input.attempt,
|
|
623
|
+
this.options.reconnectPolicy
|
|
224
624
|
);
|
|
625
|
+
if (!this.emitSafe("reconnect_scheduled", {
|
|
626
|
+
attempt: input.attempt,
|
|
627
|
+
delayMs,
|
|
628
|
+
trigger: input.trigger,
|
|
629
|
+
closeCode: input.closeCode,
|
|
630
|
+
closeReason: input.closeReason,
|
|
631
|
+
rootCause: input.rootCause
|
|
632
|
+
})) {
|
|
633
|
+
this.closed = true;
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
this.reconnectAttempts = input.attempt;
|
|
637
|
+
const reconnectWait = waitForDelay(delayMs, this.options.signal);
|
|
638
|
+
this.cancelReconnectWait = reconnectWait.cancel;
|
|
639
|
+
await reconnectWait.promise;
|
|
640
|
+
if (this.cancelReconnectWait === reconnectWait.cancel) {
|
|
641
|
+
this.cancelReconnectWait = void 0;
|
|
642
|
+
}
|
|
643
|
+
return !(this.closed || (this.options.signal?.aborted ?? false));
|
|
225
644
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
645
|
+
runSocket(socket) {
|
|
646
|
+
return new Promise((resolve) => {
|
|
647
|
+
this.socket = socket;
|
|
648
|
+
let settled = false;
|
|
649
|
+
let socketOpen = false;
|
|
650
|
+
let sawTransportError = false;
|
|
651
|
+
let aborted = false;
|
|
652
|
+
let listenerError;
|
|
653
|
+
let closeCode;
|
|
654
|
+
let closeReason;
|
|
655
|
+
let connectionTimeout;
|
|
656
|
+
let inactivityTimeout;
|
|
657
|
+
const clearTimers = () => {
|
|
658
|
+
if (connectionTimeout) {
|
|
659
|
+
clearTimeout(connectionTimeout);
|
|
660
|
+
connectionTimeout = void 0;
|
|
661
|
+
}
|
|
662
|
+
if (inactivityTimeout) {
|
|
663
|
+
clearTimeout(inactivityTimeout);
|
|
664
|
+
inactivityTimeout = void 0;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
const armConnectionTimeout = () => {
|
|
668
|
+
clearTimeout(connectionTimeout);
|
|
669
|
+
if (this.options.connectionTimeoutMs <= 0) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
connectionTimeout = setTimeout(() => {
|
|
673
|
+
if (settled || socketOpen || this.closed) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
closeAndSettle(CONNECTION_TIMEOUT_CLOSE_CODE, "connection timeout");
|
|
677
|
+
}, this.options.connectionTimeoutMs);
|
|
678
|
+
};
|
|
679
|
+
const armInactivityTimeout = () => {
|
|
680
|
+
clearTimeout(inactivityTimeout);
|
|
681
|
+
const timeoutMs = this.options.inactivityTimeoutMs;
|
|
682
|
+
if (!timeoutMs || timeoutMs <= 0 || this.closed) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
inactivityTimeout = setTimeout(() => {
|
|
686
|
+
if (settled || this.closed) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
closeAndSettle(INACTIVITY_CLOSE_CODE, "inactivity timeout");
|
|
690
|
+
}, timeoutMs);
|
|
691
|
+
};
|
|
692
|
+
const closeAndSettle = (code, reason, markAborted = false) => {
|
|
693
|
+
if (settled) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
aborted = aborted || markAborted;
|
|
697
|
+
closeCode = code;
|
|
698
|
+
closeReason = reason;
|
|
699
|
+
try {
|
|
700
|
+
socket.close(code, reason);
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
settle();
|
|
704
|
+
};
|
|
705
|
+
const settle = () => {
|
|
706
|
+
if (settled) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
settled = true;
|
|
710
|
+
clearTimers();
|
|
711
|
+
socket.removeEventListener("open", onOpen);
|
|
712
|
+
socket.removeEventListener("message", onMessage);
|
|
713
|
+
socket.removeEventListener("error", onError);
|
|
714
|
+
socket.removeEventListener("close", onClose);
|
|
715
|
+
this.options.signal?.removeEventListener("abort", onAbort);
|
|
716
|
+
if (this.socket === socket) {
|
|
717
|
+
this.socket = void 0;
|
|
718
|
+
}
|
|
719
|
+
this.forceCloseSocket = void 0;
|
|
720
|
+
resolve({
|
|
721
|
+
closeCode,
|
|
722
|
+
closeReason,
|
|
723
|
+
sawTransportError,
|
|
724
|
+
aborted,
|
|
725
|
+
listenerError
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
const onOpen = () => {
|
|
729
|
+
socketOpen = true;
|
|
730
|
+
clearTimeout(connectionTimeout);
|
|
731
|
+
armInactivityTimeout();
|
|
732
|
+
if (!this.emitSafe("open")) {
|
|
733
|
+
listenerError = new Error("Managed websocket open listener failed");
|
|
734
|
+
closeAndSettle(NORMAL_CLOSE_CODE, "listener failed");
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
const onMessage = (event) => {
|
|
738
|
+
armInactivityTimeout();
|
|
739
|
+
if (!this.emitSafe("message", event.data)) {
|
|
740
|
+
listenerError = new Error(
|
|
741
|
+
"Managed websocket message listener failed"
|
|
742
|
+
);
|
|
743
|
+
closeAndSettle(NORMAL_CLOSE_CODE, "listener failed");
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
const onError = (_event) => {
|
|
747
|
+
sawTransportError = true;
|
|
748
|
+
};
|
|
749
|
+
const onClose = (event) => {
|
|
750
|
+
closeCode = event.code;
|
|
751
|
+
closeReason = event.reason;
|
|
752
|
+
settle();
|
|
753
|
+
};
|
|
754
|
+
const onAbort = () => {
|
|
755
|
+
this.closed = true;
|
|
756
|
+
closeAndSettle(NORMAL_CLOSE_CODE, "aborted", true);
|
|
757
|
+
};
|
|
758
|
+
this.forceCloseSocket = (code, reason, markAborted) => {
|
|
759
|
+
closeAndSettle(code, reason, markAborted ?? false);
|
|
760
|
+
};
|
|
761
|
+
socket.addEventListener("open", onOpen);
|
|
762
|
+
socket.addEventListener("message", onMessage);
|
|
763
|
+
socket.addEventListener("error", onError);
|
|
764
|
+
socket.addEventListener("close", onClose);
|
|
765
|
+
this.options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
766
|
+
armConnectionTimeout();
|
|
767
|
+
});
|
|
231
768
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
769
|
+
emitSafe(eventName, payload) {
|
|
770
|
+
const emitter = this;
|
|
771
|
+
try {
|
|
772
|
+
if (payload === void 0) {
|
|
773
|
+
emitter.emit(eventName);
|
|
774
|
+
} else {
|
|
775
|
+
emitter.emit(eventName, payload);
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (eventName !== "fatal") {
|
|
780
|
+
emitter.emit("fatal", error);
|
|
781
|
+
}
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
237
784
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
785
|
+
finish(event) {
|
|
786
|
+
if (this.resolveDone === void 0) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
this.emit("closed", event);
|
|
790
|
+
this.removeAllListeners();
|
|
791
|
+
const resolve = this.resolveDone;
|
|
792
|
+
this.resolveDone = void 0;
|
|
793
|
+
resolve();
|
|
244
794
|
}
|
|
245
|
-
|
|
246
|
-
|
|
795
|
+
};
|
|
796
|
+
function reconnectDelayForAttempt(attempt, policy) {
|
|
797
|
+
const exponent = Math.max(0, attempt - 1);
|
|
798
|
+
const baseDelay = policy.initialDelayMs * policy.multiplier ** exponent;
|
|
799
|
+
const clamped = Math.min(policy.maxDelayMs, baseDelay);
|
|
800
|
+
if (policy.jitterRatio <= 0) {
|
|
801
|
+
return clamped;
|
|
247
802
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return
|
|
803
|
+
const spread = clamped * policy.jitterRatio;
|
|
804
|
+
const min = Math.max(0, clamped - spread);
|
|
805
|
+
const max = clamped + spread;
|
|
806
|
+
return min + Math.random() * (max - min);
|
|
252
807
|
}
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
808
|
+
function waitForDelay(ms, signal) {
|
|
809
|
+
if (ms <= 0 || signal?.aborted) {
|
|
810
|
+
return {
|
|
811
|
+
promise: Promise.resolve(),
|
|
812
|
+
cancel: () => void 0
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
let timer;
|
|
816
|
+
let settled = false;
|
|
817
|
+
let resolvePromise;
|
|
818
|
+
const onAbort = () => {
|
|
819
|
+
finish();
|
|
820
|
+
};
|
|
821
|
+
const finish = () => {
|
|
822
|
+
if (settled) {
|
|
823
|
+
return;
|
|
258
824
|
}
|
|
259
|
-
|
|
260
|
-
|
|
825
|
+
settled = true;
|
|
826
|
+
if (timer) {
|
|
827
|
+
clearTimeout(timer);
|
|
828
|
+
timer = void 0;
|
|
261
829
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
830
|
+
signal?.removeEventListener("abort", onAbort);
|
|
831
|
+
resolvePromise?.();
|
|
832
|
+
resolvePromise = void 0;
|
|
833
|
+
};
|
|
834
|
+
const promise = new Promise((resolve) => {
|
|
835
|
+
resolvePromise = resolve;
|
|
836
|
+
timer = setTimeout(() => {
|
|
837
|
+
finish();
|
|
838
|
+
}, ms);
|
|
839
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
840
|
+
});
|
|
841
|
+
return {
|
|
842
|
+
promise,
|
|
843
|
+
cancel: finish
|
|
844
|
+
};
|
|
266
845
|
}
|
|
267
|
-
function
|
|
268
|
-
const token = apiKey.replace(BEARER_PREFIX_REGEX, "").trim();
|
|
269
|
-
const parts = token.split(".");
|
|
270
|
-
if (parts.length !== 3) {
|
|
271
|
-
return void 0;
|
|
272
|
-
}
|
|
273
|
-
const [, payloadSegment] = parts;
|
|
274
|
-
if (payloadSegment === void 0) {
|
|
275
|
-
return void 0;
|
|
276
|
-
}
|
|
277
|
-
const payload = parseJwtSegment(payloadSegment);
|
|
278
|
-
if (payload === void 0) {
|
|
279
|
-
return void 0;
|
|
280
|
-
}
|
|
846
|
+
function closeQuietly(socket, code, reason) {
|
|
281
847
|
try {
|
|
282
|
-
|
|
283
|
-
return decoded !== null && typeof decoded === "object" ? decoded : void 0;
|
|
848
|
+
socket.close(code, reason);
|
|
284
849
|
} catch {
|
|
285
|
-
return void 0;
|
|
286
850
|
}
|
|
287
851
|
}
|
|
288
|
-
function
|
|
289
|
-
|
|
290
|
-
const value = firstNonEmptyString(source[key]);
|
|
291
|
-
if (value) {
|
|
292
|
-
return value;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return void 0;
|
|
852
|
+
function toErrorMessage(error) {
|
|
853
|
+
return error instanceof Error ? error.message : String(error);
|
|
296
854
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
855
|
+
|
|
856
|
+
// src/tail/stream.ts
|
|
857
|
+
var NORMAL_CLOSE_CODE2 = 1e3;
|
|
858
|
+
var TailStream = class {
|
|
859
|
+
sessionId;
|
|
860
|
+
token;
|
|
861
|
+
websocketBaseUrl;
|
|
862
|
+
websocketFactory;
|
|
863
|
+
batchSize;
|
|
864
|
+
agent;
|
|
865
|
+
follow;
|
|
866
|
+
shouldReconnect;
|
|
867
|
+
reconnectPolicy;
|
|
868
|
+
catchUpIdleMs;
|
|
869
|
+
connectionTimeoutMs;
|
|
870
|
+
inactivityTimeoutMs;
|
|
871
|
+
maxBufferedBatches;
|
|
872
|
+
signal;
|
|
873
|
+
onLifecycleEvent;
|
|
874
|
+
cursor;
|
|
875
|
+
constructor(input) {
|
|
876
|
+
const opts = input.options;
|
|
877
|
+
const follow = opts.follow ?? true;
|
|
878
|
+
const policy = opts.reconnectPolicy;
|
|
879
|
+
const mode = policy?.mode === "fixed" ? "fixed" : "exponential";
|
|
880
|
+
const initialDelayMs = policy?.initialDelayMs ?? 500;
|
|
881
|
+
this.sessionId = input.sessionId;
|
|
882
|
+
this.token = input.token;
|
|
883
|
+
this.websocketBaseUrl = input.websocketBaseUrl;
|
|
884
|
+
this.websocketFactory = input.websocketFactory;
|
|
885
|
+
this.cursor = opts.cursor ?? 0;
|
|
886
|
+
this.batchSize = opts.batchSize;
|
|
887
|
+
this.agent = opts.agent?.trim();
|
|
888
|
+
this.follow = follow;
|
|
889
|
+
this.shouldReconnect = follow ? opts.reconnect ?? true : false;
|
|
890
|
+
this.catchUpIdleMs = opts.catchUpIdleMs ?? 1e3;
|
|
891
|
+
this.connectionTimeoutMs = opts.connectionTimeoutMs ?? 4e3;
|
|
892
|
+
this.inactivityTimeoutMs = opts.inactivityTimeoutMs;
|
|
893
|
+
this.maxBufferedBatches = opts.maxBufferedBatches ?? 1024;
|
|
894
|
+
this.signal = opts.signal;
|
|
895
|
+
this.onLifecycleEvent = opts.onLifecycleEvent;
|
|
896
|
+
this.reconnectPolicy = {
|
|
897
|
+
mode,
|
|
898
|
+
initialDelayMs,
|
|
899
|
+
maxDelayMs: policy?.maxDelayMs ?? (mode === "fixed" ? initialDelayMs : Math.max(initialDelayMs, 15e3)),
|
|
900
|
+
multiplier: mode === "fixed" ? 1 : policy?.multiplier ?? 2,
|
|
901
|
+
jitterRatio: policy?.jitterRatio ?? 0.2,
|
|
902
|
+
maxAttempts: policy?.maxAttempts ?? Number.POSITIVE_INFINITY
|
|
903
|
+
};
|
|
300
904
|
}
|
|
301
|
-
|
|
302
|
-
|
|
905
|
+
/**
|
|
906
|
+
* Pushes batches to a callback, enabling emitter-style consumers.
|
|
907
|
+
*/
|
|
908
|
+
async subscribe(onBatch) {
|
|
909
|
+
await this.subscribeWithSignal(onBatch, this.signal);
|
|
303
910
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
911
|
+
async subscribeWithSignal(onBatch, signal) {
|
|
912
|
+
let streamError;
|
|
913
|
+
let streamReason = this.follow ? "graceful" : "caught_up";
|
|
914
|
+
let queuedBatches = 0;
|
|
915
|
+
let catchUpTimer;
|
|
916
|
+
let dispatchChain = Promise.resolve();
|
|
917
|
+
const stream = new ManagedWebSocket({
|
|
918
|
+
// URL is re-read per attempt by `ManagedWebSocket` and reflects current cursor.
|
|
919
|
+
url: () => this.buildTailUrl(),
|
|
920
|
+
websocketFactory: this.websocketFactory,
|
|
921
|
+
signal,
|
|
922
|
+
shouldReconnect: this.shouldReconnect,
|
|
923
|
+
reconnectPolicy: {
|
|
924
|
+
initialDelayMs: this.reconnectPolicy.initialDelayMs,
|
|
925
|
+
maxDelayMs: this.reconnectPolicy.maxDelayMs,
|
|
926
|
+
multiplier: this.reconnectPolicy.multiplier,
|
|
927
|
+
jitterRatio: this.reconnectPolicy.jitterRatio,
|
|
928
|
+
maxAttempts: this.reconnectPolicy.maxAttempts
|
|
929
|
+
},
|
|
930
|
+
connectionTimeoutMs: this.connectionTimeoutMs,
|
|
931
|
+
inactivityTimeoutMs: this.inactivityTimeoutMs
|
|
932
|
+
});
|
|
933
|
+
const fail = (error) => {
|
|
934
|
+
if (streamError !== void 0) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
streamError = error;
|
|
938
|
+
stream.close(NORMAL_CLOSE_CODE2, "stream failed");
|
|
939
|
+
};
|
|
940
|
+
const emitLifecycle = (event) => {
|
|
941
|
+
if (!this.onLifecycleEvent) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
this.onLifecycleEvent(event);
|
|
946
|
+
} catch (error) {
|
|
947
|
+
fail(error);
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
const clearCatchUpTimer = () => {
|
|
951
|
+
if (catchUpTimer) {
|
|
952
|
+
clearTimeout(catchUpTimer);
|
|
953
|
+
catchUpTimer = void 0;
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
const scheduleCatchUpClose = () => {
|
|
957
|
+
if (this.follow) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
clearCatchUpTimer();
|
|
961
|
+
catchUpTimer = setTimeout(() => {
|
|
962
|
+
streamReason = "caught_up";
|
|
963
|
+
stream.close(NORMAL_CLOSE_CODE2, "caught up");
|
|
964
|
+
}, this.catchUpIdleMs);
|
|
965
|
+
};
|
|
966
|
+
const dispatchBatch = (batch) => {
|
|
967
|
+
if (streamError !== void 0) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (this.maxBufferedBatches > 0 && queuedBatches > this.maxBufferedBatches) {
|
|
971
|
+
fail(
|
|
972
|
+
new StarciteBackpressureError(
|
|
973
|
+
`Tail consumer for session '${this.sessionId}' fell behind after buffering ${this.maxBufferedBatches} batch(es)`,
|
|
974
|
+
{ sessionId: this.sessionId, attempts: 0 }
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
queuedBatches += 1;
|
|
980
|
+
dispatchChain = dispatchChain.then(async () => {
|
|
981
|
+
try {
|
|
982
|
+
await onBatch(batch);
|
|
983
|
+
} finally {
|
|
984
|
+
queuedBatches -= 1;
|
|
985
|
+
}
|
|
986
|
+
}).catch((error) => {
|
|
987
|
+
fail(error);
|
|
988
|
+
});
|
|
989
|
+
};
|
|
990
|
+
const onConnectAttempt = (event) => {
|
|
991
|
+
emitLifecycle({
|
|
992
|
+
type: "connect_attempt",
|
|
993
|
+
sessionId: this.sessionId,
|
|
994
|
+
attempt: event.attempt,
|
|
995
|
+
cursor: this.cursor
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
const onConnectFailed = (event) => {
|
|
999
|
+
if (!this.shouldReconnect) {
|
|
1000
|
+
fail(
|
|
1001
|
+
new StarciteTailError(
|
|
1002
|
+
`Tail connection failed for session '${this.sessionId}': ${event.rootCause}`,
|
|
1003
|
+
{
|
|
1004
|
+
sessionId: this.sessionId,
|
|
1005
|
+
stage: "connect",
|
|
1006
|
+
attempts: event.attempt - 1
|
|
1007
|
+
}
|
|
1008
|
+
)
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
const onReconnectScheduled = (event) => {
|
|
1013
|
+
emitLifecycle({
|
|
1014
|
+
type: "reconnect_scheduled",
|
|
1015
|
+
sessionId: this.sessionId,
|
|
1016
|
+
attempt: event.attempt,
|
|
1017
|
+
delayMs: event.delayMs,
|
|
1018
|
+
trigger: event.trigger,
|
|
1019
|
+
closeCode: event.closeCode,
|
|
1020
|
+
closeReason: event.closeReason
|
|
1021
|
+
});
|
|
1022
|
+
};
|
|
1023
|
+
const onDropped = (event) => {
|
|
1024
|
+
if (event.closeCode === 4001 || event.closeReason === "token_expired") {
|
|
1025
|
+
fail(
|
|
1026
|
+
new StarciteTokenExpiredError(
|
|
1027
|
+
`Tail token expired for session '${this.sessionId}'. Re-issue a session token and reconnect from the last processed cursor.`,
|
|
1028
|
+
{
|
|
1029
|
+
sessionId: this.sessionId,
|
|
1030
|
+
attempts: event.attempt,
|
|
1031
|
+
closeCode: event.closeCode,
|
|
1032
|
+
closeReason: event.closeReason
|
|
1033
|
+
}
|
|
1034
|
+
)
|
|
1035
|
+
);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
emitLifecycle({
|
|
1039
|
+
type: "stream_dropped",
|
|
1040
|
+
sessionId: this.sessionId,
|
|
1041
|
+
attempt: event.attempt,
|
|
1042
|
+
closeCode: event.closeCode,
|
|
1043
|
+
closeReason: event.closeReason
|
|
1044
|
+
});
|
|
1045
|
+
if (!this.shouldReconnect) {
|
|
1046
|
+
fail(
|
|
1047
|
+
new StarciteTailError(
|
|
1048
|
+
`Tail connection dropped for session '${this.sessionId}' (${describeClose(event.closeCode, event.closeReason)})`,
|
|
1049
|
+
{
|
|
1050
|
+
sessionId: this.sessionId,
|
|
1051
|
+
stage: "stream",
|
|
1052
|
+
attempts: event.attempt - 1,
|
|
1053
|
+
closeCode: event.closeCode,
|
|
1054
|
+
closeReason: event.closeReason
|
|
1055
|
+
}
|
|
1056
|
+
)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
const onRetryLimit = (event) => {
|
|
1061
|
+
const message = event.trigger === "connect_failed" ? `Tail connection failed for session '${this.sessionId}' after ${this.reconnectPolicy.maxAttempts} reconnect attempt(s): ${event.rootCause ?? "Unknown error"}` : `Tail connection dropped for session '${this.sessionId}' after ${this.reconnectPolicy.maxAttempts} reconnect attempt(s) (${describeClose(event.closeCode, event.closeReason)})`;
|
|
1062
|
+
fail(
|
|
1063
|
+
new StarciteRetryLimitError(message, {
|
|
1064
|
+
sessionId: this.sessionId,
|
|
1065
|
+
attempts: event.attempt,
|
|
1066
|
+
closeCode: event.closeCode,
|
|
1067
|
+
closeReason: event.closeReason
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
};
|
|
1071
|
+
const onOpen = () => {
|
|
1072
|
+
scheduleCatchUpClose();
|
|
1073
|
+
};
|
|
1074
|
+
const onMessage = (data) => {
|
|
1075
|
+
try {
|
|
1076
|
+
const parsedEvents = parseTailFrame(data);
|
|
1077
|
+
const matchingEvents = [];
|
|
1078
|
+
for (const parsedEvent of parsedEvents) {
|
|
1079
|
+
this.cursor = Math.max(this.cursor, parsedEvent.seq);
|
|
1080
|
+
if (this.agent && agentFromActor(parsedEvent.actor) !== this.agent) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
matchingEvents.push(parsedEvent);
|
|
1084
|
+
}
|
|
1085
|
+
if (matchingEvents.length > 0) {
|
|
1086
|
+
stream.resetReconnectAttempts();
|
|
1087
|
+
dispatchBatch(matchingEvents);
|
|
1088
|
+
}
|
|
1089
|
+
scheduleCatchUpClose();
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
fail(error);
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
const onFatal = (error) => {
|
|
1095
|
+
fail(error);
|
|
1096
|
+
};
|
|
1097
|
+
const onClosed = (event) => {
|
|
1098
|
+
clearCatchUpTimer();
|
|
1099
|
+
if (event.aborted) {
|
|
1100
|
+
streamReason = "aborted";
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (!this.follow) {
|
|
1104
|
+
streamReason = "caught_up";
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (event.graceful) {
|
|
1108
|
+
streamReason = "graceful";
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
stream.on("connect_attempt", onConnectAttempt);
|
|
1112
|
+
stream.on("connect_failed", onConnectFailed);
|
|
1113
|
+
stream.on("reconnect_scheduled", onReconnectScheduled);
|
|
1114
|
+
stream.on("dropped", onDropped);
|
|
1115
|
+
stream.on("retry_limit", onRetryLimit);
|
|
1116
|
+
stream.on("open", onOpen);
|
|
1117
|
+
stream.on("message", onMessage);
|
|
1118
|
+
stream.on("fatal", onFatal);
|
|
1119
|
+
stream.on("closed", onClosed);
|
|
1120
|
+
try {
|
|
1121
|
+
await stream.waitForClose();
|
|
1122
|
+
clearCatchUpTimer();
|
|
1123
|
+
await dispatchChain;
|
|
1124
|
+
if (streamError !== void 0) {
|
|
1125
|
+
throw streamError;
|
|
1126
|
+
}
|
|
1127
|
+
emitLifecycle({
|
|
1128
|
+
type: "stream_ended",
|
|
1129
|
+
sessionId: this.sessionId,
|
|
1130
|
+
reason: streamReason
|
|
1131
|
+
});
|
|
1132
|
+
if (streamError !== void 0) {
|
|
1133
|
+
throw streamError;
|
|
1134
|
+
}
|
|
1135
|
+
} finally {
|
|
1136
|
+
clearCatchUpTimer();
|
|
1137
|
+
stream.off("connect_attempt", onConnectAttempt);
|
|
1138
|
+
stream.off("connect_failed", onConnectFailed);
|
|
1139
|
+
stream.off("reconnect_scheduled", onReconnectScheduled);
|
|
1140
|
+
stream.off("dropped", onDropped);
|
|
1141
|
+
stream.off("retry_limit", onRetryLimit);
|
|
1142
|
+
stream.off("open", onOpen);
|
|
1143
|
+
stream.off("message", onMessage);
|
|
1144
|
+
stream.off("fatal", onFatal);
|
|
1145
|
+
stream.off("closed", onClosed);
|
|
1146
|
+
stream.close(NORMAL_CLOSE_CODE2, "finished");
|
|
1147
|
+
}
|
|
310
1148
|
}
|
|
311
|
-
|
|
312
|
-
|
|
1149
|
+
buildTailUrl() {
|
|
1150
|
+
const query = new URLSearchParams({ cursor: `${this.cursor}` });
|
|
1151
|
+
if (this.batchSize !== void 0) {
|
|
1152
|
+
query.set("batch_size", `${this.batchSize}`);
|
|
1153
|
+
}
|
|
1154
|
+
if (this.token) {
|
|
1155
|
+
query.set("access_token", this.token);
|
|
1156
|
+
}
|
|
1157
|
+
return `${this.websocketBaseUrl}/sessions/${encodeURIComponent(
|
|
1158
|
+
this.sessionId
|
|
1159
|
+
)}/tail?${query.toString()}`;
|
|
313
1160
|
}
|
|
314
|
-
|
|
1161
|
+
};
|
|
1162
|
+
function describeClose(code, reason) {
|
|
1163
|
+
const codeText = `code ${code ?? "unknown"}`;
|
|
1164
|
+
return reason ? `${codeText}, reason '${reason}'` : codeText;
|
|
315
1165
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"principalType",
|
|
324
|
-
"type"
|
|
325
|
-
]);
|
|
326
|
-
const tenantId = parseClaimStrings(mergedClaims, ["tenant_id", "tenantId"]);
|
|
327
|
-
const rawPrincipalId = parseClaimStrings(mergedClaims, [
|
|
328
|
-
"principal_id",
|
|
329
|
-
"principalId",
|
|
330
|
-
"id",
|
|
331
|
-
"sub"
|
|
332
|
-
]);
|
|
333
|
-
const actorFromRawId = rawPrincipalId ? parseActorIdentityFromSubject(rawPrincipalId) : void 0;
|
|
334
|
-
const principal = {
|
|
335
|
-
tenant_id: tenantId ?? (subject ? parseTenantIdFromSubject(subject) : ""),
|
|
336
|
-
id: rawPrincipalId ?? actorFromSubject?.id ?? "",
|
|
337
|
-
type: principalTypeFromClaims === "agent" || principalTypeFromClaims === "user" ? principalTypeFromClaims : actorFromSubject?.type ?? actorFromRawId?.type ?? "user"
|
|
338
|
-
};
|
|
339
|
-
if (principal.tenant_id.length === 0 || principal.id.length === 0 || principal.type.length === 0) {
|
|
340
|
-
return void 0;
|
|
1166
|
+
|
|
1167
|
+
// src/transport.ts
|
|
1168
|
+
var TRAILING_SLASHES_REGEX = /\/+$/;
|
|
1169
|
+
function normalizeAbsoluteHttpUrl(value, context) {
|
|
1170
|
+
const parsed = new URL(value);
|
|
1171
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1172
|
+
throw new StarciteError(`${context} must use http:// or https://`);
|
|
341
1173
|
}
|
|
342
|
-
|
|
343
|
-
return result.success ? result.data : void 0;
|
|
1174
|
+
return parsed.toString().replace(TRAILING_SLASHES_REGEX, "");
|
|
344
1175
|
}
|
|
345
|
-
function
|
|
346
|
-
const
|
|
347
|
-
return
|
|
1176
|
+
function toApiBaseUrl(baseUrl) {
|
|
1177
|
+
const normalized = normalizeAbsoluteHttpUrl(baseUrl, "baseUrl");
|
|
1178
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
348
1179
|
}
|
|
349
|
-
function
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
throw new StarciteConnectionError(reason);
|
|
354
|
-
}
|
|
355
|
-
return result.data;
|
|
1180
|
+
function toWebSocketBaseUrl(apiBaseUrl) {
|
|
1181
|
+
const url = new URL(apiBaseUrl);
|
|
1182
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1183
|
+
return url.toString().replace(TRAILING_SLASHES_REGEX, "");
|
|
356
1184
|
}
|
|
357
|
-
function
|
|
358
|
-
if (
|
|
359
|
-
|
|
1185
|
+
function defaultWebSocketFactory(url) {
|
|
1186
|
+
if (typeof WebSocket === "undefined") {
|
|
1187
|
+
throw new StarciteError(
|
|
1188
|
+
"WebSocket is not available in this runtime. Provide websocketFactory in StarciteOptions."
|
|
1189
|
+
);
|
|
360
1190
|
}
|
|
361
|
-
return
|
|
1191
|
+
return new WebSocket(url);
|
|
362
1192
|
}
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
const code = event.code;
|
|
366
|
-
return typeof code === "number" ? code : void 0;
|
|
367
|
-
}
|
|
368
|
-
return void 0;
|
|
1193
|
+
function request(transport, path, init, schema) {
|
|
1194
|
+
return requestWithBaseUrl(transport, transport.baseUrl, path, init, schema);
|
|
369
1195
|
}
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
1196
|
+
async function requestWithBaseUrl(transport, baseUrl, path, init, schema) {
|
|
1197
|
+
const headers = new Headers(transport.headers);
|
|
1198
|
+
if (transport.authorization) {
|
|
1199
|
+
headers.set("authorization", transport.authorization);
|
|
374
1200
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (ms <= 0) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
await new Promise((resolve) => {
|
|
386
|
-
let settled = false;
|
|
387
|
-
const timeout = setTimeout(() => {
|
|
388
|
-
if (settled) {
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
settled = true;
|
|
392
|
-
if (signal) {
|
|
393
|
-
signal.removeEventListener("abort", onAbort);
|
|
394
|
-
}
|
|
395
|
-
resolve();
|
|
396
|
-
}, ms);
|
|
397
|
-
const onAbort = () => {
|
|
398
|
-
if (settled) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
settled = true;
|
|
402
|
-
clearTimeout(timeout);
|
|
403
|
-
signal?.removeEventListener("abort", onAbort);
|
|
404
|
-
resolve();
|
|
405
|
-
};
|
|
406
|
-
if (signal) {
|
|
407
|
-
if (signal.aborted) {
|
|
408
|
-
onAbort();
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1201
|
+
if (init.body !== void 0 && !headers.has("content-type")) {
|
|
1202
|
+
headers.set("content-type", "application/json");
|
|
1203
|
+
}
|
|
1204
|
+
if (init.headers) {
|
|
1205
|
+
const perRequestHeaders = new Headers(init.headers);
|
|
1206
|
+
for (const [key, value] of perRequestHeaders.entries()) {
|
|
1207
|
+
headers.set(key, value);
|
|
412
1208
|
}
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
function agentFromActor(actor) {
|
|
416
|
-
if (actor.startsWith("agent:")) {
|
|
417
|
-
return actor.slice("agent:".length);
|
|
418
1209
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
1210
|
+
let response;
|
|
1211
|
+
try {
|
|
1212
|
+
response = await transport.fetchFn(`${baseUrl}${path}`, {
|
|
1213
|
+
...init,
|
|
1214
|
+
headers
|
|
1215
|
+
});
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
throw new StarciteConnectionError(
|
|
1218
|
+
`Failed to connect to Starcite at ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
if (!response.ok) {
|
|
1222
|
+
let payload = null;
|
|
1223
|
+
try {
|
|
1224
|
+
payload = await response.json();
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
const code = typeof payload?.error === "string" ? payload.error : `http_${response.status}`;
|
|
1228
|
+
const message = typeof payload?.message === "string" ? payload.message : response.statusText;
|
|
1229
|
+
throw new StarciteApiError(message, response.status, code, payload);
|
|
1230
|
+
}
|
|
1231
|
+
if (response.status === 204) {
|
|
1232
|
+
return schema.parse(void 0);
|
|
1233
|
+
}
|
|
1234
|
+
let body;
|
|
1235
|
+
try {
|
|
1236
|
+
body = await response.json();
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
throw new StarciteConnectionError(
|
|
1239
|
+
`Received invalid JSON from Starcite: ${error instanceof Error ? error.message : String(error)}`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
return schema.parse(body);
|
|
429
1243
|
}
|
|
1244
|
+
|
|
1245
|
+
// src/session.ts
|
|
430
1246
|
var StarciteSession = class {
|
|
431
1247
|
/** Session identifier. */
|
|
432
1248
|
id;
|
|
1249
|
+
/** The session JWT used for auth. Extract this for frontend handoff. */
|
|
1250
|
+
token;
|
|
1251
|
+
/** Identity bound to this session. */
|
|
1252
|
+
identity;
|
|
433
1253
|
/** Optional session record captured at creation time. */
|
|
434
1254
|
record;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
1255
|
+
transport;
|
|
1256
|
+
producerId;
|
|
1257
|
+
producerSeq = 0;
|
|
1258
|
+
log;
|
|
1259
|
+
store;
|
|
1260
|
+
lifecycle = new EventEmitter3();
|
|
1261
|
+
eventSubscriptions = /* @__PURE__ */ new Map();
|
|
1262
|
+
liveSyncController;
|
|
1263
|
+
liveSyncTask;
|
|
1264
|
+
constructor(options) {
|
|
1265
|
+
this.id = options.id;
|
|
1266
|
+
this.token = options.token;
|
|
1267
|
+
this.identity = options.identity;
|
|
1268
|
+
this.transport = options.transport;
|
|
1269
|
+
this.record = options.record;
|
|
1270
|
+
this.producerId = crypto.randomUUID();
|
|
1271
|
+
this.store = options.store;
|
|
1272
|
+
this.log = new SessionLog(options.logOptions);
|
|
1273
|
+
const storedState = this.store.load(this.id);
|
|
1274
|
+
if (storedState) {
|
|
1275
|
+
this.log.hydrate(storedState);
|
|
1276
|
+
}
|
|
440
1277
|
}
|
|
441
1278
|
/**
|
|
442
|
-
* Appends
|
|
1279
|
+
* Appends an event to this session.
|
|
443
1280
|
*
|
|
444
|
-
*
|
|
1281
|
+
* The SDK manages `actor`, `producer_id`, and `producer_seq` automatically.
|
|
445
1282
|
*/
|
|
446
|
-
append(input) {
|
|
1283
|
+
async append(input, options) {
|
|
447
1284
|
const parsed = SessionAppendInputSchema.parse(input);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
1285
|
+
this.producerSeq += 1;
|
|
1286
|
+
const result = await this.appendRaw(
|
|
1287
|
+
{
|
|
1288
|
+
type: parsed.type ?? "content",
|
|
1289
|
+
payload: parsed.payload ?? { text: parsed.text },
|
|
1290
|
+
actor: parsed.actor ?? this.identity.toActor(),
|
|
1291
|
+
producer_id: this.producerId,
|
|
1292
|
+
producer_seq: this.producerSeq,
|
|
1293
|
+
source: parsed.source ?? "agent",
|
|
1294
|
+
metadata: parsed.metadata,
|
|
1295
|
+
refs: parsed.refs,
|
|
1296
|
+
idempotency_key: parsed.idempotencyKey,
|
|
1297
|
+
expected_seq: parsed.expectedSeq
|
|
1298
|
+
},
|
|
1299
|
+
options
|
|
1300
|
+
);
|
|
1301
|
+
return {
|
|
1302
|
+
seq: result.seq,
|
|
1303
|
+
deduped: result.deduped
|
|
1304
|
+
};
|
|
461
1305
|
}
|
|
462
1306
|
/**
|
|
463
|
-
* Appends a raw event payload as-is.
|
|
1307
|
+
* Appends a raw event payload as-is. Caller manages all fields.
|
|
464
1308
|
*/
|
|
465
|
-
appendRaw(input) {
|
|
466
|
-
return
|
|
1309
|
+
appendRaw(input, options) {
|
|
1310
|
+
return request(
|
|
1311
|
+
this.transport,
|
|
1312
|
+
`/sessions/${encodeURIComponent(this.id)}/append`,
|
|
1313
|
+
{
|
|
1314
|
+
method: "POST",
|
|
1315
|
+
body: JSON.stringify(input),
|
|
1316
|
+
signal: options?.signal
|
|
1317
|
+
},
|
|
1318
|
+
AppendEventResponseSchema
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
on(eventName, listener) {
|
|
1322
|
+
if (eventName === "event") {
|
|
1323
|
+
const eventListener = listener;
|
|
1324
|
+
if (!this.eventSubscriptions.has(eventListener)) {
|
|
1325
|
+
const unsubscribe = this.log.subscribe(eventListener, { replay: true });
|
|
1326
|
+
this.eventSubscriptions.set(eventListener, unsubscribe);
|
|
1327
|
+
}
|
|
1328
|
+
this.ensureLiveSync();
|
|
1329
|
+
return () => {
|
|
1330
|
+
this.off("event", eventListener);
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
if (eventName === "error") {
|
|
1334
|
+
const errorListener = listener;
|
|
1335
|
+
this.lifecycle.on("error", errorListener);
|
|
1336
|
+
return () => {
|
|
1337
|
+
this.off("error", errorListener);
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
throw new StarciteError(`Unsupported event name '${eventName}'`);
|
|
1341
|
+
}
|
|
1342
|
+
off(eventName, listener) {
|
|
1343
|
+
if (eventName === "event") {
|
|
1344
|
+
const eventListener = listener;
|
|
1345
|
+
const unsubscribe = this.eventSubscriptions.get(eventListener);
|
|
1346
|
+
if (!unsubscribe) {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
this.eventSubscriptions.delete(eventListener);
|
|
1350
|
+
unsubscribe();
|
|
1351
|
+
if (this.eventSubscriptions.size === 0) {
|
|
1352
|
+
this.liveSyncController?.abort();
|
|
1353
|
+
}
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (eventName === "error") {
|
|
1357
|
+
this.lifecycle.off("error", listener);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
throw new StarciteError(`Unsupported event name '${eventName}'`);
|
|
467
1361
|
}
|
|
468
1362
|
/**
|
|
469
|
-
*
|
|
1363
|
+
* Stops live syncing and removes listeners registered via `on()`.
|
|
470
1364
|
*/
|
|
471
|
-
|
|
472
|
-
|
|
1365
|
+
disconnect() {
|
|
1366
|
+
this.liveSyncController?.abort();
|
|
1367
|
+
for (const unsubscribe of this.eventSubscriptions.values()) {
|
|
1368
|
+
unsubscribe();
|
|
1369
|
+
}
|
|
1370
|
+
this.eventSubscriptions.clear();
|
|
1371
|
+
this.lifecycle.removeAllListeners();
|
|
473
1372
|
}
|
|
474
1373
|
/**
|
|
475
|
-
*
|
|
1374
|
+
* Backwards-compatible alias for `disconnect()`.
|
|
476
1375
|
*/
|
|
477
|
-
|
|
478
|
-
|
|
1376
|
+
close() {
|
|
1377
|
+
this.disconnect();
|
|
479
1378
|
}
|
|
480
|
-
};
|
|
481
|
-
var StarciteClient = class {
|
|
482
|
-
/** Normalized API base URL ending with `/v1`. */
|
|
483
|
-
baseUrl;
|
|
484
|
-
inferredCreatorPrincipal;
|
|
485
|
-
websocketBaseUrl;
|
|
486
|
-
fetchFn;
|
|
487
|
-
headers;
|
|
488
|
-
websocketFactory;
|
|
489
1379
|
/**
|
|
490
|
-
*
|
|
1380
|
+
* Updates in-memory session log retention.
|
|
491
1381
|
*/
|
|
492
|
-
|
|
493
|
-
this.
|
|
494
|
-
this.
|
|
495
|
-
this.fetchFn = options.fetch ?? defaultFetch;
|
|
496
|
-
this.headers = new Headers(options.headers);
|
|
497
|
-
if (options.apiKey !== void 0) {
|
|
498
|
-
const authorization = formatAuthorizationHeader(options.apiKey);
|
|
499
|
-
this.headers.set("authorization", authorization);
|
|
500
|
-
this.inferredCreatorPrincipal = parseCreatorPrincipalFromClaimsSafe(authorization);
|
|
501
|
-
}
|
|
502
|
-
this.websocketFactory = options.websocketFactory ?? defaultWebSocketFactory;
|
|
1382
|
+
setLogOptions(options) {
|
|
1383
|
+
this.log.setMaxEvents(options.maxEvents);
|
|
1384
|
+
this.persistLogState();
|
|
503
1385
|
}
|
|
504
1386
|
/**
|
|
505
|
-
* Returns a
|
|
1387
|
+
* Returns a stable snapshot of the current canonical in-memory log.
|
|
506
1388
|
*/
|
|
507
|
-
|
|
508
|
-
return
|
|
1389
|
+
getSnapshot() {
|
|
1390
|
+
return this.log.getSnapshot(this.liveSyncTask !== void 0);
|
|
509
1391
|
}
|
|
510
1392
|
/**
|
|
511
|
-
*
|
|
1393
|
+
* Streams tail events one at a time via callback.
|
|
512
1394
|
*/
|
|
513
|
-
async
|
|
514
|
-
|
|
515
|
-
|
|
1395
|
+
async tail(onEvent, options = {}) {
|
|
1396
|
+
await this.tailBatches(async (batch) => {
|
|
1397
|
+
for (const event of batch) {
|
|
1398
|
+
await onEvent(event);
|
|
1399
|
+
}
|
|
1400
|
+
}, options);
|
|
516
1401
|
}
|
|
517
1402
|
/**
|
|
518
|
-
*
|
|
1403
|
+
* Streams tail event batches grouped by incoming frame via callback.
|
|
519
1404
|
*/
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
method: "POST",
|
|
529
|
-
body: JSON.stringify(payload)
|
|
530
|
-
},
|
|
531
|
-
SessionRecordSchema
|
|
532
|
-
);
|
|
1405
|
+
async tailBatches(onBatch, options = {}) {
|
|
1406
|
+
await new TailStream({
|
|
1407
|
+
sessionId: this.id,
|
|
1408
|
+
token: this.token,
|
|
1409
|
+
websocketBaseUrl: this.transport.websocketBaseUrl,
|
|
1410
|
+
websocketFactory: this.transport.websocketFactory,
|
|
1411
|
+
options
|
|
1412
|
+
}).subscribe(onBatch);
|
|
533
1413
|
}
|
|
534
1414
|
/**
|
|
535
|
-
*
|
|
1415
|
+
* Durably consumes events and checkpoints `event.seq` after each successful handler invocation.
|
|
536
1416
|
*/
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
1417
|
+
async consume(options) {
|
|
1418
|
+
const {
|
|
1419
|
+
cursorStore,
|
|
1420
|
+
handler,
|
|
1421
|
+
cursor: requestedCursor,
|
|
1422
|
+
...tailOptions
|
|
1423
|
+
} = options;
|
|
1424
|
+
let cursor;
|
|
1425
|
+
if (requestedCursor !== void 0) {
|
|
1426
|
+
cursor = requestedCursor;
|
|
1427
|
+
} else {
|
|
1428
|
+
try {
|
|
1429
|
+
cursor = await cursorStore.load(this.id) ?? 0;
|
|
1430
|
+
} catch (error) {
|
|
541
1431
|
throw new StarciteError(
|
|
542
|
-
|
|
1432
|
+
`consume() failed to load cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
|
|
543
1433
|
);
|
|
544
1434
|
}
|
|
545
|
-
query.set("limit", `${options.limit}`);
|
|
546
1435
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1436
|
+
const stream = new TailStream({
|
|
1437
|
+
sessionId: this.id,
|
|
1438
|
+
token: this.token,
|
|
1439
|
+
websocketBaseUrl: this.transport.websocketBaseUrl,
|
|
1440
|
+
websocketFactory: this.transport.websocketFactory,
|
|
1441
|
+
options: {
|
|
1442
|
+
...tailOptions,
|
|
1443
|
+
cursor
|
|
550
1444
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1445
|
+
});
|
|
1446
|
+
await stream.subscribe(async (batch) => {
|
|
1447
|
+
for (const event of batch) {
|
|
1448
|
+
await handler(event);
|
|
1449
|
+
try {
|
|
1450
|
+
await cursorStore.save(this.id, event.seq);
|
|
1451
|
+
} catch (error) {
|
|
556
1452
|
throw new StarciteError(
|
|
557
|
-
|
|
1453
|
+
`consume() failed to save cursor for session '${this.id}': ${error instanceof Error ? error.message : String(error)}`
|
|
558
1454
|
);
|
|
559
1455
|
}
|
|
560
|
-
query.set(`metadata.${key}`, value);
|
|
561
1456
|
}
|
|
562
|
-
}
|
|
563
|
-
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
564
|
-
return this.request(
|
|
565
|
-
`/sessions${suffix}`,
|
|
566
|
-
{
|
|
567
|
-
method: "GET"
|
|
568
|
-
},
|
|
569
|
-
SessionListPageSchema
|
|
570
|
-
);
|
|
1457
|
+
});
|
|
571
1458
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
body: JSON.stringify(payload)
|
|
582
|
-
},
|
|
583
|
-
AppendEventResponseSchema
|
|
584
|
-
);
|
|
1459
|
+
emitStreamError(error) {
|
|
1460
|
+
const streamError = error instanceof Error ? error : new StarciteError(`Session stream failed: ${String(error)}`);
|
|
1461
|
+
if (this.lifecycle.listenerCount("error") > 0) {
|
|
1462
|
+
this.lifecycle.emit("error", streamError);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
queueMicrotask(() => {
|
|
1466
|
+
throw streamError;
|
|
1467
|
+
});
|
|
585
1468
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: single-loop reconnect state machine is intentionally explicit for stream correctness.
|
|
590
|
-
async *tailRawEvents(sessionId, options = {}) {
|
|
591
|
-
const initialCursor = options.cursor ?? 0;
|
|
592
|
-
const follow = options.follow ?? true;
|
|
593
|
-
const reconnectEnabled = follow ? options.reconnect ?? true : false;
|
|
594
|
-
const reconnectDelayMs = options.reconnectDelayMs ?? DEFAULT_TAIL_RECONNECT_DELAY_MS;
|
|
595
|
-
if (!Number.isInteger(initialCursor) || initialCursor < 0) {
|
|
596
|
-
throw new StarciteError("tail() cursor must be a non-negative integer");
|
|
597
|
-
}
|
|
598
|
-
if (!Number.isFinite(reconnectDelayMs) || reconnectDelayMs < 0) {
|
|
599
|
-
throw new StarciteError(
|
|
600
|
-
"tail() reconnectDelayMs must be a non-negative number"
|
|
601
|
-
);
|
|
1469
|
+
ensureLiveSync() {
|
|
1470
|
+
if (this.liveSyncTask || this.eventSubscriptions.size === 0) {
|
|
1471
|
+
return;
|
|
602
1472
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const wsUrl = `${this.websocketBaseUrl}/sessions/${encodeURIComponent(
|
|
609
|
-
sessionId
|
|
610
|
-
)}/tail?cursor=${cursor}`;
|
|
611
|
-
const websocketHeaders = new Headers();
|
|
612
|
-
const authorization = this.headers.get("authorization");
|
|
613
|
-
if (authorization) {
|
|
614
|
-
websocketHeaders.set("authorization", authorization);
|
|
1473
|
+
const controller = new AbortController();
|
|
1474
|
+
this.liveSyncController = controller;
|
|
1475
|
+
this.liveSyncTask = this.runLiveSync(controller.signal).catch((error) => {
|
|
1476
|
+
if (!controller.signal.aborted) {
|
|
1477
|
+
this.emitStreamError(error);
|
|
615
1478
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
await waitForDelay(reconnectDelayMs, options.signal);
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
634
|
-
const queue = new AsyncQueue();
|
|
635
|
-
let sawTransportError = false;
|
|
636
|
-
let closeCode;
|
|
637
|
-
let closeReason;
|
|
638
|
-
let abortRequested = false;
|
|
639
|
-
let catchUpTimer = null;
|
|
640
|
-
const resetCatchUpTimer = () => {
|
|
641
|
-
if (!follow) {
|
|
642
|
-
if (catchUpTimer) {
|
|
643
|
-
clearTimeout(catchUpTimer);
|
|
644
|
-
}
|
|
645
|
-
catchUpTimer = setTimeout(() => {
|
|
646
|
-
queue.close();
|
|
647
|
-
}, CATCH_UP_IDLE_MS);
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
const onMessage = (event) => {
|
|
651
|
-
try {
|
|
652
|
-
const parsed = parseEventFrame(getEventData(event));
|
|
653
|
-
cursor = Math.max(cursor, parsed.seq);
|
|
654
|
-
if (options.agent && agentFromActor(parsed.actor) !== options.agent) {
|
|
655
|
-
resetCatchUpTimer();
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
queue.push(parsed);
|
|
659
|
-
resetCatchUpTimer();
|
|
660
|
-
} catch (error) {
|
|
661
|
-
queue.fail(error);
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
const onError = () => {
|
|
665
|
-
sawTransportError = true;
|
|
666
|
-
if (catchUpTimer) {
|
|
667
|
-
clearTimeout(catchUpTimer);
|
|
668
|
-
}
|
|
669
|
-
queue.close();
|
|
670
|
-
};
|
|
671
|
-
const onClose = (event) => {
|
|
672
|
-
closeCode = getCloseCode(event);
|
|
673
|
-
closeReason = getCloseReason(event);
|
|
674
|
-
if (catchUpTimer) {
|
|
675
|
-
clearTimeout(catchUpTimer);
|
|
676
|
-
}
|
|
677
|
-
queue.close();
|
|
678
|
-
};
|
|
679
|
-
const onAbort = () => {
|
|
680
|
-
abortRequested = true;
|
|
681
|
-
if (catchUpTimer) {
|
|
682
|
-
clearTimeout(catchUpTimer);
|
|
683
|
-
}
|
|
684
|
-
queue.close();
|
|
685
|
-
socket.close(NORMAL_WEBSOCKET_CLOSE_CODE, "aborted");
|
|
686
|
-
};
|
|
687
|
-
const onOpen = () => {
|
|
688
|
-
resetCatchUpTimer();
|
|
689
|
-
};
|
|
690
|
-
socket.addEventListener("open", onOpen);
|
|
691
|
-
socket.addEventListener("message", onMessage);
|
|
692
|
-
socket.addEventListener("error", onError);
|
|
693
|
-
socket.addEventListener("close", onClose);
|
|
694
|
-
if (options.signal) {
|
|
695
|
-
if (options.signal.aborted) {
|
|
696
|
-
onAbort();
|
|
697
|
-
} else {
|
|
698
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1479
|
+
}).finally(() => {
|
|
1480
|
+
this.liveSyncTask = void 0;
|
|
1481
|
+
this.liveSyncController = void 0;
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
async runLiveSync(signal) {
|
|
1485
|
+
while (!signal.aborted && this.eventSubscriptions.size > 0) {
|
|
1486
|
+
const stream = new TailStream({
|
|
1487
|
+
sessionId: this.id,
|
|
1488
|
+
token: this.token,
|
|
1489
|
+
websocketBaseUrl: this.transport.websocketBaseUrl,
|
|
1490
|
+
websocketFactory: this.transport.websocketFactory,
|
|
1491
|
+
options: {
|
|
1492
|
+
cursor: this.log.lastSeq,
|
|
1493
|
+
signal
|
|
699
1494
|
}
|
|
700
|
-
}
|
|
701
|
-
let iterationError = null;
|
|
1495
|
+
});
|
|
702
1496
|
try {
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
if (
|
|
706
|
-
|
|
1497
|
+
await stream.subscribe((batch) => {
|
|
1498
|
+
const appliedEvents = this.log.applyBatch(batch);
|
|
1499
|
+
if (appliedEvents.length > 0) {
|
|
1500
|
+
this.persistLogState();
|
|
707
1501
|
}
|
|
708
|
-
|
|
709
|
-
}
|
|
1502
|
+
});
|
|
710
1503
|
} catch (error) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (catchUpTimer) {
|
|
714
|
-
clearTimeout(catchUpTimer);
|
|
1504
|
+
if (signal.aborted) {
|
|
1505
|
+
return;
|
|
715
1506
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
socket.removeEventListener("error", onError);
|
|
719
|
-
socket.removeEventListener("close", onClose);
|
|
720
|
-
if (options.signal) {
|
|
721
|
-
options.signal.removeEventListener("abort", onAbort);
|
|
1507
|
+
if (error instanceof SessionLogGapError) {
|
|
1508
|
+
continue;
|
|
722
1509
|
}
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
if (iterationError) {
|
|
726
|
-
throw iterationError;
|
|
1510
|
+
throw error;
|
|
727
1511
|
}
|
|
728
|
-
if (abortRequested || options.signal?.aborted || !follow) {
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
const gracefullyClosed = !sawTransportError && closeCode === NORMAL_WEBSOCKET_CLOSE_CODE;
|
|
732
|
-
if (gracefullyClosed) {
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
if (!reconnectEnabled) {
|
|
736
|
-
throw new StarciteConnectionError(
|
|
737
|
-
`Tail connection dropped for session '${sessionId}' (${describeClose(
|
|
738
|
-
closeCode,
|
|
739
|
-
closeReason
|
|
740
|
-
)})`
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
await waitForDelay(reconnectDelayMs, options.signal);
|
|
744
1512
|
}
|
|
745
1513
|
}
|
|
1514
|
+
persistLogState() {
|
|
1515
|
+
this.store.save(this.id, {
|
|
1516
|
+
cursor: this.log.cursor,
|
|
1517
|
+
events: [...this.log.events]
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
// src/session-store.ts
|
|
1523
|
+
function cloneEvents(events) {
|
|
1524
|
+
return events.map((event) => structuredClone(event));
|
|
1525
|
+
}
|
|
1526
|
+
function cloneState(state) {
|
|
1527
|
+
return {
|
|
1528
|
+
cursor: state.cursor,
|
|
1529
|
+
events: cloneEvents(state.events)
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
var MemoryStore = class {
|
|
1533
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1534
|
+
load(sessionId) {
|
|
1535
|
+
const stored = this.sessions.get(sessionId);
|
|
1536
|
+
return stored ? cloneState(stored) : void 0;
|
|
1537
|
+
}
|
|
1538
|
+
save(sessionId, state) {
|
|
1539
|
+
this.sessions.set(sessionId, cloneState(state));
|
|
1540
|
+
}
|
|
1541
|
+
clear(sessionId) {
|
|
1542
|
+
this.sessions.delete(sessionId);
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
// src/client.ts
|
|
1547
|
+
var DEFAULT_BASE_URL = globalThis.process?.env?.STARCITE_BASE_URL ?? "http://localhost:4000";
|
|
1548
|
+
var DEFAULT_AUTH_URL = globalThis.process?.env?.STARCITE_AUTH_URL;
|
|
1549
|
+
function resolveAuthBaseUrl(explicitAuthUrl, apiKey) {
|
|
1550
|
+
if (explicitAuthUrl) {
|
|
1551
|
+
return normalizeAbsoluteHttpUrl(explicitAuthUrl, "authUrl");
|
|
1552
|
+
}
|
|
1553
|
+
if (DEFAULT_AUTH_URL) {
|
|
1554
|
+
return normalizeAbsoluteHttpUrl(DEFAULT_AUTH_URL, "STARCITE_AUTH_URL");
|
|
1555
|
+
}
|
|
1556
|
+
if (apiKey) {
|
|
1557
|
+
return inferIssuerAuthorityFromApiKey(apiKey);
|
|
1558
|
+
}
|
|
1559
|
+
return void 0;
|
|
1560
|
+
}
|
|
1561
|
+
var Starcite = class {
|
|
1562
|
+
/** Normalized API base URL ending with `/v1`. */
|
|
1563
|
+
baseUrl;
|
|
1564
|
+
transport;
|
|
1565
|
+
authBaseUrl;
|
|
1566
|
+
inferredIdentity;
|
|
1567
|
+
store;
|
|
1568
|
+
constructor(options = {}) {
|
|
1569
|
+
const baseUrl = toApiBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
1570
|
+
this.baseUrl = baseUrl;
|
|
1571
|
+
const userFetch = options.fetch;
|
|
1572
|
+
const fetchFn = userFetch ? (input, init) => userFetch(input, init) : (input, init) => fetch(input, init);
|
|
1573
|
+
const headers = new Headers(options.headers);
|
|
1574
|
+
const apiKey = options.apiKey?.trim();
|
|
1575
|
+
let authorization;
|
|
1576
|
+
if (apiKey) {
|
|
1577
|
+
authorization = `Bearer ${apiKey}`;
|
|
1578
|
+
this.inferredIdentity = inferIdentityFromApiKey(apiKey);
|
|
1579
|
+
}
|
|
1580
|
+
this.authBaseUrl = resolveAuthBaseUrl(options.authUrl, apiKey);
|
|
1581
|
+
const websocketFactory = options.websocketFactory ?? defaultWebSocketFactory;
|
|
1582
|
+
this.store = options.store ?? new MemoryStore();
|
|
1583
|
+
this.transport = {
|
|
1584
|
+
baseUrl,
|
|
1585
|
+
websocketBaseUrl: toWebSocketBaseUrl(baseUrl),
|
|
1586
|
+
authorization: authorization ?? null,
|
|
1587
|
+
fetchFn,
|
|
1588
|
+
headers,
|
|
1589
|
+
websocketFactory
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Creates a user identity bound to this client's tenant.
|
|
1594
|
+
*/
|
|
1595
|
+
user(options) {
|
|
1596
|
+
return new StarciteIdentity({
|
|
1597
|
+
tenantId: this.requireTenantId("user()"),
|
|
1598
|
+
id: options.id,
|
|
1599
|
+
type: "user"
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
746
1602
|
/**
|
|
747
|
-
*
|
|
1603
|
+
* Creates an agent identity bound to this client's tenant.
|
|
748
1604
|
*/
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1605
|
+
agent(options) {
|
|
1606
|
+
return new StarciteIdentity({
|
|
1607
|
+
tenantId: this.requireTenantId("agent()"),
|
|
1608
|
+
id: options.id,
|
|
1609
|
+
type: "agent"
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
session(input) {
|
|
1613
|
+
if ("token" in input) {
|
|
1614
|
+
return this.sessionFromToken(input.token, input.logOptions);
|
|
752
1615
|
}
|
|
1616
|
+
return this.sessionFromIdentity(input);
|
|
753
1617
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1618
|
+
/**
|
|
1619
|
+
* Lists sessions from the archive-backed catalog.
|
|
1620
|
+
*/
|
|
1621
|
+
listSessions(options = {}, requestOptions) {
|
|
1622
|
+
const parsed = SessionListOptionsSchema.parse(options);
|
|
1623
|
+
const query = new URLSearchParams();
|
|
1624
|
+
if (parsed.limit !== void 0) {
|
|
1625
|
+
query.set("limit", `${parsed.limit}`);
|
|
758
1626
|
}
|
|
759
|
-
if (
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1627
|
+
if (parsed.cursor !== void 0) {
|
|
1628
|
+
query.set("cursor", parsed.cursor);
|
|
1629
|
+
}
|
|
1630
|
+
if (parsed.metadata !== void 0) {
|
|
1631
|
+
for (const [key, value] of Object.entries(parsed.metadata)) {
|
|
1632
|
+
query.set(`metadata.${key}`, value);
|
|
763
1633
|
}
|
|
764
1634
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1635
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
1636
|
+
return request(
|
|
1637
|
+
this.transport,
|
|
1638
|
+
`/sessions${suffix}`,
|
|
1639
|
+
{
|
|
1640
|
+
method: "GET",
|
|
1641
|
+
signal: requestOptions?.signal
|
|
1642
|
+
},
|
|
1643
|
+
SessionListPageSchema
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
async sessionFromIdentity(input) {
|
|
1647
|
+
let sessionId = input.id;
|
|
1648
|
+
let record;
|
|
1649
|
+
if (!sessionId) {
|
|
1650
|
+
record = await this.createSession({
|
|
1651
|
+
creator_principal: input.identity.toCreatorPrincipal(),
|
|
1652
|
+
title: input.title,
|
|
1653
|
+
metadata: input.metadata
|
|
770
1654
|
});
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1655
|
+
sessionId = record.id;
|
|
1656
|
+
}
|
|
1657
|
+
const tokenResponse = await this.issueSessionToken({
|
|
1658
|
+
session_id: sessionId,
|
|
1659
|
+
principal: input.identity.toTokenPrincipal(),
|
|
1660
|
+
scopes: ["session:read", "session:append"]
|
|
1661
|
+
});
|
|
1662
|
+
return new StarciteSession({
|
|
1663
|
+
id: sessionId,
|
|
1664
|
+
token: tokenResponse.token,
|
|
1665
|
+
identity: input.identity,
|
|
1666
|
+
transport: this.buildSessionTransport(tokenResponse.token),
|
|
1667
|
+
store: this.store,
|
|
1668
|
+
record,
|
|
1669
|
+
logOptions: input.logOptions
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
sessionFromToken(token, logOptions) {
|
|
1673
|
+
const decoded = decodeSessionToken(token);
|
|
1674
|
+
const sessionId = decoded.sessionId;
|
|
1675
|
+
if (!sessionId) {
|
|
1676
|
+
throw new StarciteError(
|
|
1677
|
+
"session({ token }) requires a token with a session_id claim."
|
|
775
1678
|
);
|
|
776
1679
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1680
|
+
return new StarciteSession({
|
|
1681
|
+
id: sessionId,
|
|
1682
|
+
token,
|
|
1683
|
+
identity: decoded.identity,
|
|
1684
|
+
transport: this.buildSessionTransport(token),
|
|
1685
|
+
store: this.store,
|
|
1686
|
+
logOptions
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
buildSessionTransport(token) {
|
|
1690
|
+
return {
|
|
1691
|
+
...this.transport,
|
|
1692
|
+
authorization: `Bearer ${token}`
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
createSession(input) {
|
|
1696
|
+
return request(
|
|
1697
|
+
this.transport,
|
|
1698
|
+
"/sessions",
|
|
1699
|
+
{
|
|
1700
|
+
method: "POST",
|
|
1701
|
+
body: JSON.stringify(input)
|
|
1702
|
+
},
|
|
1703
|
+
SessionRecordSchema
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
issueSessionToken(input) {
|
|
1707
|
+
if (!this.transport.authorization) {
|
|
1708
|
+
throw new StarciteError(
|
|
1709
|
+
"session() with identity requires apiKey. Set StarciteOptions.apiKey."
|
|
1710
|
+
);
|
|
782
1711
|
}
|
|
783
|
-
if (
|
|
784
|
-
|
|
1712
|
+
if (!this.authBaseUrl) {
|
|
1713
|
+
throw new StarciteError(
|
|
1714
|
+
"session() could not resolve auth issuer URL. Set StarciteOptions.authUrl, STARCITE_AUTH_URL, or use an API key JWT with an 'iss' claim."
|
|
1715
|
+
);
|
|
785
1716
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1717
|
+
return requestWithBaseUrl(
|
|
1718
|
+
this.transport,
|
|
1719
|
+
this.authBaseUrl,
|
|
1720
|
+
"/api/v1/session-tokens",
|
|
1721
|
+
{
|
|
1722
|
+
method: "POST",
|
|
1723
|
+
headers: {
|
|
1724
|
+
"cache-control": "no-store"
|
|
1725
|
+
},
|
|
1726
|
+
body: JSON.stringify(input)
|
|
1727
|
+
},
|
|
1728
|
+
IssueSessionTokenResponseSchema
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
requireTenantId(method) {
|
|
1732
|
+
const tenantId = this.inferredIdentity?.tenantId;
|
|
1733
|
+
if (!tenantId) {
|
|
1734
|
+
throw new StarciteError(`${method} requires apiKey to determine tenant.`);
|
|
789
1735
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1736
|
+
return tenantId;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// src/cursor-store.ts
|
|
1741
|
+
var DEFAULT_KEY_PREFIX = "starcite";
|
|
1742
|
+
var InMemoryCursorStore = class {
|
|
1743
|
+
cursors;
|
|
1744
|
+
constructor(initial = {}) {
|
|
1745
|
+
this.cursors = new Map(Object.entries(initial));
|
|
1746
|
+
}
|
|
1747
|
+
load(sessionId) {
|
|
1748
|
+
return this.cursors.get(sessionId);
|
|
1749
|
+
}
|
|
1750
|
+
save(sessionId, cursor) {
|
|
1751
|
+
this.cursors.set(sessionId, cursor);
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
var WebStorageCursorStore = class {
|
|
1755
|
+
storage;
|
|
1756
|
+
keyForSession;
|
|
1757
|
+
constructor(storage, options = {}) {
|
|
1758
|
+
this.storage = storage;
|
|
1759
|
+
const prefix = options.keyPrefix?.trim() || DEFAULT_KEY_PREFIX;
|
|
1760
|
+
this.keyForSession = options.keyForSession ?? ((sessionId) => `${prefix}:${sessionId}:lastSeq`);
|
|
1761
|
+
}
|
|
1762
|
+
load(sessionId) {
|
|
1763
|
+
const raw = this.storage.getItem(this.keyForSession(sessionId));
|
|
1764
|
+
if (raw === null) {
|
|
1765
|
+
return void 0;
|
|
796
1766
|
}
|
|
797
|
-
|
|
1767
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1768
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
1769
|
+
}
|
|
1770
|
+
save(sessionId, cursor) {
|
|
1771
|
+
this.storage.setItem(this.keyForSession(sessionId), `${cursor}`);
|
|
798
1772
|
}
|
|
799
1773
|
};
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1774
|
+
var LocalStorageCursorStore = class extends WebStorageCursorStore {
|
|
1775
|
+
constructor(options = {}) {
|
|
1776
|
+
if (typeof localStorage === "undefined") {
|
|
1777
|
+
throw new StarciteError(
|
|
1778
|
+
"localStorage is not available in this runtime. Use WebStorageCursorStore with a custom storage adapter."
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
super(localStorage, options);
|
|
807
1782
|
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// src/index.ts
|
|
811
|
-
function createStarciteClient(options = {}) {
|
|
812
|
-
return new StarciteClient(options);
|
|
813
|
-
}
|
|
814
|
-
var starcite = createStarciteClient();
|
|
1783
|
+
};
|
|
815
1784
|
export {
|
|
1785
|
+
InMemoryCursorStore,
|
|
1786
|
+
LocalStorageCursorStore,
|
|
1787
|
+
MemoryStore,
|
|
1788
|
+
SessionLogConflictError,
|
|
1789
|
+
SessionLogGapError,
|
|
1790
|
+
Starcite,
|
|
816
1791
|
StarciteApiError,
|
|
817
|
-
|
|
1792
|
+
StarciteBackpressureError,
|
|
818
1793
|
StarciteConnectionError,
|
|
819
1794
|
StarciteError,
|
|
1795
|
+
StarciteIdentity,
|
|
1796
|
+
StarciteRetryLimitError,
|
|
820
1797
|
StarciteSession,
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1798
|
+
StarciteTailError,
|
|
1799
|
+
StarciteTokenExpiredError,
|
|
1800
|
+
WebStorageCursorStore
|
|
824
1801
|
};
|
|
825
1802
|
//# sourceMappingURL=index.js.map
|