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