@ours.network/fleet 1.2.0-nightly.70 → 1.2.0-nightly.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account-origin.d.ts +4 -1
- package/dist/account-origin.js +27 -2
- package/dist/application/conversation-history.d.ts +7 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/resource-observer.d.ts +40 -0
- package/dist/application/resource-observer.js +278 -0
- package/dist/application/role-removal-service.js +1 -1
- package/dist/application/role-repository.d.ts +1 -1
- package/dist/application/role-repository.js +11 -5
- package/dist/application/task-layouts.js +2 -0
- package/dist/application/task-room-service.d.ts +2 -0
- package/dist/application/task-room-service.js +2 -2
- package/dist/build-info.json +4 -4
- package/dist/daemon-recovery.d.ts +13 -1
- package/dist/daemon-recovery.js +117 -3
- package/dist/monitor.d.ts +1 -0
- package/dist/monitor.js +1 -1
- package/dist/notifications/fleet-producer.d.ts +4 -1
- package/dist/notifications/fleet-producer.js +39 -10
- package/dist/notifications/outbox.d.ts +14 -1
- package/dist/notifications/outbox.js +51 -10
- package/dist/notifications/target-cleanup.d.ts +10 -4
- package/dist/notifications/target-cleanup.js +35 -23
- package/dist/notifications/task-producer.d.ts +3 -1
- package/dist/notifications/task-producer.js +38 -5
- package/dist/ops.js +1 -1
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +3 -0
- package/dist/rooms-tasks/add-member.js +2 -1
- package/dist/rooms-tasks/layout.js +2 -1
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +27 -11
- package/dist/rooms-tasks/types.d.ts +4 -1
- package/dist/runner.js +22 -7
- package/dist/subscriptions/login.d.ts +4 -3
- package/dist/subscriptions/login.js +14 -6
- package/dist/subscriptions/service.d.ts +1 -1
- package/dist/subscriptions/service.js +4 -4
- package/dist/subscriptions/store.js +3 -0
- package/dist/tunnel-setup-input.js +7 -1
- package/dist/watchdog/query.d.ts +3 -1
- package/dist/watchdog/query.js +2 -1
- package/dist/web/auth.d.ts +2 -2
- package/dist/web/auth.js +4 -4
- package/dist/web/events.d.ts +2 -0
- package/dist/web/events.js +9 -0
- package/dist/web/prefix-gateway.js +20 -10
- package/dist/web/resource-views.d.ts +124 -0
- package/dist/web/resource-views.js +424 -0
- package/dist/web/runtime.js +6 -3
- package/dist/web/server.d.ts +3 -0
- package/dist/web/server.js +171 -20
- package/dist/workspace-enrollment.js +53 -28
- package/dist/workspace-replacement.js +28 -10
- package/docs/conversation-stream.md +6 -0
- package/package.json +1 -1
package/dist/paths.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare function deriveXdgRuntimeDir(env?: NodeJS.ProcessEnv, uid?: numbe
|
|
|
13
13
|
export declare const stateRoot: () => string;
|
|
14
14
|
/** The Owner invitation file that tunnel setup writes itself after enrollment. */
|
|
15
15
|
export declare const workspaceOwnerInvite: () => string;
|
|
16
|
+
/** Serializes writers of the workspace binding and of its unfinished replacement record. */
|
|
17
|
+
export declare const workspaceBindingLock: () => string;
|
|
18
|
+
export declare const workspaceReplacementFile: () => string;
|
|
16
19
|
export declare const agentsRoot: () => string;
|
|
17
20
|
export declare const tmpRoot: () => string;
|
|
18
21
|
export declare const logsRoot: () => string;
|
package/dist/paths.js
CHANGED
|
@@ -23,6 +23,9 @@ export function deriveXdgRuntimeDir(env = process.env, uid = process.getuid?.(),
|
|
|
23
23
|
export const stateRoot = () => join(home(), '.ours-fleet');
|
|
24
24
|
/** The Owner invitation file that tunnel setup writes itself after enrollment. */
|
|
25
25
|
export const workspaceOwnerInvite = () => join(stateRoot(), 'workspace', 'owner.invite');
|
|
26
|
+
/** Serializes writers of the workspace binding and of its unfinished replacement record. */
|
|
27
|
+
export const workspaceBindingLock = () => join(stateRoot(), 'workspace', '.binding.lock');
|
|
28
|
+
export const workspaceReplacementFile = () => join(stateRoot(), 'workspace', 'replacement.json');
|
|
26
29
|
export const agentsRoot = () => join(stateRoot(), 'agents');
|
|
27
30
|
export const tmpRoot = () => join(stateRoot(), 'tmp');
|
|
28
31
|
export const logsRoot = () => join(stateRoot(), 'logs');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publishFleetResourceCommit } from '../application/resource-observer.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -62,7 +63,7 @@ export async function requestMemberAddition(input) {
|
|
|
62
63
|
})();
|
|
63
64
|
const template = sealTemplateSnapshot(plan.snapshot, input.cfg.agentTemplates ?? {}, plan.launchDefinitions);
|
|
64
65
|
const receipt = { taskId, requestId: r.requestId, roleId, hash, state: 'running' };
|
|
65
|
-
const save = () => replaceFileAtomically(path, JSON.stringify(receipt) + '\n', 0o600);
|
|
66
|
+
const save = () => { replaceFileAtomically(path, JSON.stringify(receipt) + '\n', 0o600); publishFleetResourceCommit(path); };
|
|
66
67
|
save();
|
|
67
68
|
running.add(path);
|
|
68
69
|
void (input.provision ?? provisionMembers)({ cfg: input.cfg, cowork: input.cowork, configPath: input.configPath, binPath: input.binPath,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publishFleetResourceCommit } from '../application/resource-observer.js';
|
|
1
2
|
/** Durable local room-layout controller.
|
|
2
3
|
* Uses native Cowork operations and a supervising Fleet's exact-instance port.
|
|
3
4
|
* Unknown mutation outcomes stop for reconciliation; they are never replayed.
|
|
@@ -45,7 +46,7 @@ export class RoomLayout {
|
|
|
45
46
|
}
|
|
46
47
|
get cowork() { return this.coworkAdapter ??= this.coworkFactory(); }
|
|
47
48
|
snapshot() { return JSON.parse(readFileSync(this.file, 'utf8')); }
|
|
48
|
-
save(s) { replaceFileAtomically(this.file, JSON.stringify(s, null, 2)); }
|
|
49
|
+
save(s) { replaceFileAtomically(this.file, JSON.stringify(s, null, 2)); publishFleetResourceCommit(this.file); }
|
|
49
50
|
async verify(i) {
|
|
50
51
|
need(i.supervisor === this.supervisor.id || i.remote, 'remote binding unsupported');
|
|
51
52
|
need(i.launch && i.cid && i.session, 'incomplete instance reference');
|
|
@@ -39,6 +39,7 @@ export declare function listTasks(filter?: {
|
|
|
39
39
|
state?: TaskState | TaskState[];
|
|
40
40
|
listId?: string;
|
|
41
41
|
includeDeleting?: boolean;
|
|
42
|
+
strict?: boolean;
|
|
42
43
|
}): TaskRecord[];
|
|
43
44
|
export declare function moveTaskToList(id: string, listId: string): TaskRecord;
|
|
44
45
|
export declare function findByIdempotencyKey(key: string): TaskRecord | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publishFleetResourceCommit } from '../application/resource-observer.js';
|
|
1
2
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
@@ -100,15 +101,27 @@ function writeTask(record) {
|
|
|
100
101
|
pending.push({ eventId: `task:${record.task_id}:${randomUUID()}`, taskId: record.task_id,
|
|
101
102
|
title: `Task ${!previous ? 'created' : 'updated'}: ${record.title}`.slice(0, 160),
|
|
102
103
|
body: `Task ${record.task_id}: ${before ? `${before} → ` : 'created in '}${next}.`,
|
|
103
|
-
|
|
104
|
+
target: { kind: 'task', taskId: record.task_id } });
|
|
104
105
|
}
|
|
105
106
|
if (pending.length)
|
|
106
107
|
persisted.notification_events = pending;
|
|
107
108
|
else
|
|
108
109
|
delete persisted.notification_events;
|
|
109
110
|
replaceFileAtomically(taskPath(record.task_id), JSON.stringify(persisted, null, 2) + '\n');
|
|
111
|
+
publishFleetResourceCommit(taskPath(record.task_id));
|
|
110
112
|
}
|
|
111
113
|
const retiredNotificationsDir = () => join(tasksDir(), '.notification-retired');
|
|
114
|
+
function typedTaskNotification(value, id) {
|
|
115
|
+
if (!value || typeof value !== 'object' || Object.keys(value).some(key => !['eventId', 'title', 'body', 'target', 'taskId'].includes(key)))
|
|
116
|
+
return false;
|
|
117
|
+
const target = value.target;
|
|
118
|
+
return typeof value.eventId === 'string' && value.eventId.startsWith(`task:${id}:`) && value.eventId.length <= 512
|
|
119
|
+
&& value.taskId === id && !!target && typeof target === 'object' && !Array.isArray(target)
|
|
120
|
+
&& Object.keys(target).every(key => ['kind', 'taskId'].includes(key)) && target.taskId === id
|
|
121
|
+
&& (target.kind === 'task' || (target.kind === 'task-deleted' && value.eventId === `task:${id}:deleted`))
|
|
122
|
+
&& typeof value.title === 'string' && !!value.title.trim() && value.title.length <= 160
|
|
123
|
+
&& typeof value.body === 'string' && !!value.body.trim() && value.body.length <= 512;
|
|
124
|
+
}
|
|
112
125
|
/** Atomic snapshots contain the events, including intermediate states between polls. */
|
|
113
126
|
export function pendingTaskNotifications(warn = () => { }) {
|
|
114
127
|
const result = [];
|
|
@@ -127,13 +140,13 @@ export function pendingTaskNotifications(warn = () => { }) {
|
|
|
127
140
|
continue;
|
|
128
141
|
if (value.notification_events === undefined)
|
|
129
142
|
continue;
|
|
130
|
-
if (!Array.isArray(value.notification_events)
|
|
131
|
-
|| typeof e.eventId !== 'string' || !e.eventId.startsWith(`task:${id}:`) || e.eventId.length > 512
|
|
132
|
-
|| e.taskId !== id || (e.url !== `/fleet/tasks/${id}` && !(e.eventId === `task:${id}:deleted` && e.url === `/fleet/tasks?deletedTask=${id}`)) || typeof e.title !== 'string' || !e.title.trim() || e.title.length > 160
|
|
133
|
-
|| typeof e.body !== 'string' || !e.body.trim() || e.body.length > 512))
|
|
143
|
+
if (!Array.isArray(value.notification_events))
|
|
134
144
|
throw new Error('invalid notification events');
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
// Only typed task events are delivered. Unsupported entries are never sent or converted,
|
|
146
|
+
// and they do not block later lifecycle events.
|
|
147
|
+
const events = value.notification_events.filter(event => typedTaskNotification(event, id));
|
|
148
|
+
if (events.length)
|
|
149
|
+
result.push({ taskId: id, events });
|
|
137
150
|
}
|
|
138
151
|
catch (error) {
|
|
139
152
|
if (!isNotFoundError(error))
|
|
@@ -151,9 +164,9 @@ export function acknowledgeTaskNotification(id, eventId) {
|
|
|
151
164
|
if (!existsSync(path))
|
|
152
165
|
continue;
|
|
153
166
|
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
154
|
-
if (!value.notification_events?.some(event => event.eventId === eventId))
|
|
167
|
+
if (!value.notification_events?.some(event => typedTaskNotification(event, id) && event.eventId === eventId))
|
|
155
168
|
continue;
|
|
156
|
-
value.notification_events = value.notification_events.filter(event => event.eventId !== eventId);
|
|
169
|
+
value.notification_events = value.notification_events.filter(event => !typedTaskNotification(event, id) || event.eventId !== eventId);
|
|
157
170
|
replaceFileAtomically(path, JSON.stringify(value, null, 2) + '\n');
|
|
158
171
|
if (path.startsWith(retiredNotificationsDir() + '/') && !value.notification_events.length)
|
|
159
172
|
unlinkSync(path);
|
|
@@ -280,7 +293,10 @@ export function listTasks(filter) {
|
|
|
280
293
|
if ((!states || states.includes(t.state)) && (!filter?.listId || t.list_id === filter.listId))
|
|
281
294
|
tasks.push(t);
|
|
282
295
|
}
|
|
283
|
-
catch {
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (filter?.strict)
|
|
298
|
+
throw error; /* legacy CLI inventory tolerates corrupt files */
|
|
299
|
+
}
|
|
284
300
|
}
|
|
285
301
|
const order = new Map(readTaskLists().map((item, index) => [item.list_id, index]));
|
|
286
302
|
return tasks.sort((a, b) => (order.get(a.list_id) - order.get(b.list_id))
|
|
@@ -885,7 +901,7 @@ export function unlinkDeletedTask(id) {
|
|
|
885
901
|
// before unlink may replay two copies; both carry identical service dedupe IDs.
|
|
886
902
|
const removed = { eventId: `task:${id}:deleted`, taskId: id,
|
|
887
903
|
title: `Task deleted: ${stored.title}`.slice(0, 160), body: `Task ${id}: deleting → deleted.`,
|
|
888
|
-
|
|
904
|
+
target: { kind: 'task-deleted', taskId: id } };
|
|
889
905
|
replaceFileAtomically(join(retiredNotificationsDir(), `${id}.json`), JSON.stringify({ notification_events: [...(stored.notification_events ?? []), removed] }) + '\n');
|
|
890
906
|
try {
|
|
891
907
|
unlinkSync(taskPath(id));
|
|
@@ -110,7 +110,10 @@ export interface TaskNotificationEvent {
|
|
|
110
110
|
eventId: string;
|
|
111
111
|
title: string;
|
|
112
112
|
body: string;
|
|
113
|
-
|
|
113
|
+
target: {
|
|
114
|
+
kind: 'task' | 'task-deleted';
|
|
115
|
+
taskId: string;
|
|
116
|
+
};
|
|
114
117
|
taskId: string;
|
|
115
118
|
}
|
|
116
119
|
export interface TaskRecord {
|
package/dist/runner.js
CHANGED
|
@@ -17,7 +17,7 @@ import { agentDir, stateRoot } from './paths.js';
|
|
|
17
17
|
import { loadConfig, findRole, isolationContextFor, resolveSnapshotMonitor, resolvePermissions, } from './config.js';
|
|
18
18
|
import { getAdapter } from './harness/registry.js';
|
|
19
19
|
import { createMonitor, probeIdentityPresence, } from './monitor.js';
|
|
20
|
-
import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
|
|
20
|
+
import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, acquireSelectedDaemonHealth, } from './daemon-recovery.js';
|
|
21
21
|
import { realExec } from './exec.js';
|
|
22
22
|
import { resolveIsolation } from './isolation/policy.js';
|
|
23
23
|
import { pinSubscriptionForLaunch } from './subscriptions/launch.js';
|
|
@@ -48,6 +48,7 @@ export class SupervisorRecycleRequiredError extends Error {
|
|
|
48
48
|
this.name = 'SupervisorRecycleRequiredError';
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
const defaultProbeGeneration = (env) => probeDaemonGeneration((url, init) => globalThis.fetch(url, init), env);
|
|
51
52
|
const defaultDeps = () => ({
|
|
52
53
|
prepareAgentOurs: prepareManagedAgent,
|
|
53
54
|
releaseAgentOurs: releaseManagedAgent,
|
|
@@ -64,7 +65,7 @@ const defaultDeps = () => ({
|
|
|
64
65
|
now: () => Date.now(),
|
|
65
66
|
log: line => process.stderr.write(line + '\n'),
|
|
66
67
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
67
|
-
probeGeneration:
|
|
68
|
+
probeGeneration: defaultProbeGeneration,
|
|
68
69
|
createMonitor: opts => createMonitor(opts),
|
|
69
70
|
createOwnerChannel: opts => new OwnerChannel(opts),
|
|
70
71
|
startAgentSession: (adapter, options) => adapter.start(options),
|
|
@@ -622,6 +623,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
622
623
|
// Self-reminders live and die with this attempt's runtime binding: every exit
|
|
623
624
|
// path below stops the dispatcher before that binding is released.
|
|
624
625
|
let reminderManager;
|
|
626
|
+
let selectedHealth;
|
|
625
627
|
try {
|
|
626
628
|
const managedHarness = prepareManagedHarness(role, dir, runCwd, managedService.descriptor, { ...harnessChildEnv(role, launch.env, dir), ...subscription?.env });
|
|
627
629
|
// Start-stagger: space this launch at least start_stagger_ms after the previous
|
|
@@ -669,8 +671,15 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
669
671
|
name, identity: role.identity, agentDir: dir, cfg: role.monitor,
|
|
670
672
|
deps: resolvedMonitorDeps,
|
|
671
673
|
}) : null;
|
|
674
|
+
// Injected legacy probes keep their test/embedding contract. Production uses
|
|
675
|
+
// one retained owner for generation and identity health in this process.
|
|
676
|
+
const retainHealth = deps.probeGeneration === defaultProbeGeneration
|
|
677
|
+
&& !deps.identityProbeDeps ? acquireSelectedDaemonHealth : undefined;
|
|
678
|
+
selectedHealth = retainHealth?.(resolvedMonitorDeps.env);
|
|
672
679
|
const daemonObserver = new DaemonGenerationObserver();
|
|
673
|
-
const initialDaemon = daemonObserver.observe(
|
|
680
|
+
const initialDaemon = daemonObserver.observe(selectedHealth
|
|
681
|
+
? (await selectedHealth.sample()).generation
|
|
682
|
+
: await deps.probeGeneration(resolvedMonitorDeps.env));
|
|
674
683
|
let sessionStartedWithoutDaemonBaseline = initialDaemon.kind !== 'baseline';
|
|
675
684
|
if (monitor)
|
|
676
685
|
await monitor.prime({ resetCursor: resetMonitorCursor });
|
|
@@ -1113,7 +1122,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
1113
1122
|
const now = deps.now();
|
|
1114
1123
|
if (now >= nextDaemonProbeAt) {
|
|
1115
1124
|
nextDaemonProbeAt = now + 2_000;
|
|
1116
|
-
const observation = daemonObserver.observe(
|
|
1125
|
+
const observation = daemonObserver.observe(selectedHealth
|
|
1126
|
+
? (await selectedHealth.sample()).generation
|
|
1127
|
+
: await deps.probeGeneration(resolvedMonitorDeps.env));
|
|
1117
1128
|
if (observation.kind === 'lost')
|
|
1118
1129
|
recoveryController.noteLoss(observation.reason);
|
|
1119
1130
|
if (observation.kind === 'changed' || observation.kind === 'available'
|
|
@@ -1147,19 +1158,22 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
1147
1158
|
}
|
|
1148
1159
|
if (temp && now >= nextIdentityPollAt) {
|
|
1149
1160
|
nextIdentityPollAt = now + TEMP_IDENTITY_POLL_MS;
|
|
1150
|
-
const presence =
|
|
1161
|
+
const presence = selectedHealth
|
|
1162
|
+
? selectedHealth.presence(role.identity, await selectedHealth.sample())
|
|
1163
|
+
: await probeIdentityPresence(role.identity, deps.fetch, resolvedMonitorDeps.env, deps.identityProbeDeps);
|
|
1164
|
+
const identityObservedAt = deps.now();
|
|
1151
1165
|
if (presence.state === 'present') {
|
|
1152
1166
|
identityObserved = true;
|
|
1153
1167
|
identityAbsentSince = undefined;
|
|
1154
1168
|
}
|
|
1155
1169
|
else if (presence.state === 'absent' && identityObserved) {
|
|
1156
|
-
identityAbsentSince ??=
|
|
1170
|
+
identityAbsentSince ??= identityObservedAt;
|
|
1157
1171
|
// Require a continuous, time-bounded run of authoritative absence.
|
|
1158
1172
|
// The first positive observation is the readiness gate: cold harness
|
|
1159
1173
|
// starts may spend minutes loading the harness and briefing before the
|
|
1160
1174
|
// agent creates/binds its identity, and absence before then is not a
|
|
1161
1175
|
// close event. After readiness, debounce a real disappearance.
|
|
1162
|
-
if (
|
|
1176
|
+
if (identityObservedAt - identityAbsentSince >= TEMP_IDENTITY_CLOSE_DEBOUNCE_MS) {
|
|
1163
1177
|
retirementReason = 'identity-closed';
|
|
1164
1178
|
deps.log(`[${name}] temporary identity '${role.identity}' closed; retiring session and supervisor`);
|
|
1165
1179
|
reminderManager?.stop();
|
|
@@ -1273,6 +1287,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
1273
1287
|
};
|
|
1274
1288
|
}
|
|
1275
1289
|
finally {
|
|
1290
|
+
selectedHealth?.release();
|
|
1276
1291
|
reminderManager?.stop();
|
|
1277
1292
|
managedService.setReminders?.(undefined);
|
|
1278
1293
|
await managedService.close(layoutRetiring);
|
|
@@ -33,6 +33,7 @@ declare abstract class BaseLogin implements LoginHandle {
|
|
|
33
33
|
readonly provider: SubscriptionProvider;
|
|
34
34
|
readonly owner: string;
|
|
35
35
|
readonly kind: LoginView['kind'];
|
|
36
|
+
protected readonly changed: () => void;
|
|
36
37
|
readonly loginId: string;
|
|
37
38
|
readonly expiresAt: number;
|
|
38
39
|
state: LoginState;
|
|
@@ -42,7 +43,7 @@ declare abstract class BaseLogin implements LoginHandle {
|
|
|
42
43
|
done: Promise<boolean>;
|
|
43
44
|
protected finish: (ok: boolean) => void;
|
|
44
45
|
private readonly timer;
|
|
45
|
-
constructor(provider: SubscriptionProvider, owner: string, kind: LoginView['kind'], now?: number);
|
|
46
|
+
constructor(provider: SubscriptionProvider, owner: string, kind: LoginView['kind'], now?: number, changed?: () => void);
|
|
46
47
|
view(): LoginView;
|
|
47
48
|
abstract submitCode(code: string): void;
|
|
48
49
|
protected abstract stop(): void;
|
|
@@ -59,7 +60,7 @@ export declare class ClaudeLogin extends BaseLogin {
|
|
|
59
60
|
private continued;
|
|
60
61
|
/** Resolves with the CLI exit status once it exits. */
|
|
61
62
|
exited: Promise<number | null>;
|
|
62
|
-
constructor(owner: string, bins: SubscriptionBinaries, home: string, profileId: string);
|
|
63
|
+
constructor(owner: string, bins: SubscriptionBinaries, home: string, profileId: string, changed?: () => void);
|
|
63
64
|
submitCode(code: string): void;
|
|
64
65
|
protected stop(): void;
|
|
65
66
|
}
|
|
@@ -69,7 +70,7 @@ export declare class CodexDeviceLogin extends BaseLogin {
|
|
|
69
70
|
private codexLoginId?;
|
|
70
71
|
/** Resolves true when the app-server reports a successful login. */
|
|
71
72
|
completed: Promise<boolean>;
|
|
72
|
-
constructor(owner: string, bins: SubscriptionBinaries, home: string, profileId: string);
|
|
73
|
+
constructor(owner: string, bins: SubscriptionBinaries, home: string, profileId: string, changed?: () => void);
|
|
73
74
|
submitCode(): void;
|
|
74
75
|
protected stop(): void;
|
|
75
76
|
}
|
|
@@ -26,6 +26,7 @@ class BaseLogin {
|
|
|
26
26
|
provider;
|
|
27
27
|
owner;
|
|
28
28
|
kind;
|
|
29
|
+
changed;
|
|
29
30
|
loginId = randomBytes(16).toString('hex');
|
|
30
31
|
expiresAt;
|
|
31
32
|
state = 'starting';
|
|
@@ -35,10 +36,11 @@ class BaseLogin {
|
|
|
35
36
|
done;
|
|
36
37
|
finish;
|
|
37
38
|
timer;
|
|
38
|
-
constructor(provider, owner, kind, now = Date.now()) {
|
|
39
|
+
constructor(provider, owner, kind, now = Date.now(), changed = () => { }) {
|
|
39
40
|
this.provider = provider;
|
|
40
41
|
this.owner = owner;
|
|
41
42
|
this.kind = kind;
|
|
43
|
+
this.changed = changed;
|
|
42
44
|
this.expiresAt = now + LOGIN_TTL_MS;
|
|
43
45
|
this.done = new Promise(resolve => { this.finish = resolve; });
|
|
44
46
|
this.timer = setTimeout(() => this.end('expired', 'login timed out'), LOGIN_TTL_MS);
|
|
@@ -64,6 +66,7 @@ class BaseLogin {
|
|
|
64
66
|
clearTimeout(this.timer);
|
|
65
67
|
this.stop();
|
|
66
68
|
this.finish(state === 'succeeded');
|
|
69
|
+
this.changed();
|
|
67
70
|
}
|
|
68
71
|
/** Called by the service after verifying the new home. */
|
|
69
72
|
complete(ok, error) { this.end(ok ? 'succeeded' : 'failed', error); }
|
|
@@ -76,8 +79,8 @@ export class ClaudeLogin extends BaseLogin {
|
|
|
76
79
|
continued = false;
|
|
77
80
|
/** Resolves with the CLI exit status once it exits. */
|
|
78
81
|
exited;
|
|
79
|
-
constructor(owner, bins, home, profileId) {
|
|
80
|
-
super('claude', owner, 'paste_code');
|
|
82
|
+
constructor(owner, bins, home, profileId, changed) {
|
|
83
|
+
super('claude', owner, 'paste_code', Date.now(), changed);
|
|
81
84
|
const darwin = process.platform === 'darwin';
|
|
82
85
|
const args = darwin
|
|
83
86
|
? ['-q', '/dev/null', bins.claude, 'auth', 'login']
|
|
@@ -109,6 +112,7 @@ export class ClaudeLogin extends BaseLogin {
|
|
|
109
112
|
}
|
|
110
113
|
this.url = url;
|
|
111
114
|
this.state = 'awaiting_user';
|
|
115
|
+
this.changed();
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
// Claude asks for Enter after success; the answer carries no secret.
|
|
@@ -131,6 +135,7 @@ export class ClaudeLogin extends BaseLogin {
|
|
|
131
135
|
throw new FleetError('invalid_request', 'that does not look like a sign-in code');
|
|
132
136
|
this.submitted = true;
|
|
133
137
|
this.state = 'verifying';
|
|
138
|
+
this.changed();
|
|
134
139
|
this.output = '';
|
|
135
140
|
this.child?.stdin.write(`${code}\r`);
|
|
136
141
|
}
|
|
@@ -151,8 +156,8 @@ export class CodexDeviceLogin extends BaseLogin {
|
|
|
151
156
|
codexLoginId;
|
|
152
157
|
/** Resolves true when the app-server reports a successful login. */
|
|
153
158
|
completed;
|
|
154
|
-
constructor(owner, bins, home, profileId) {
|
|
155
|
-
super('codex', owner, 'device_code');
|
|
159
|
+
constructor(owner, bins, home, profileId, changed) {
|
|
160
|
+
super('codex', owner, 'device_code', Date.now(), changed);
|
|
156
161
|
this.completed = (async () => {
|
|
157
162
|
try {
|
|
158
163
|
this.server = await CodexAppServer.start(bins.codex, profileEnv('CODEX_HOME', profileId, home));
|
|
@@ -184,9 +189,12 @@ export class CodexDeviceLogin extends BaseLogin {
|
|
|
184
189
|
this.url = url;
|
|
185
190
|
this.userCode = String(started.userCode).slice(0, 32);
|
|
186
191
|
this.state = 'awaiting_user';
|
|
192
|
+
this.changed();
|
|
187
193
|
const ok = await Promise.race([completion, this.done.then(() => false)]);
|
|
188
|
-
if (this.state === 'awaiting_user')
|
|
194
|
+
if (this.state === 'awaiting_user') {
|
|
189
195
|
this.state = 'verifying';
|
|
196
|
+
this.changed();
|
|
197
|
+
}
|
|
190
198
|
return ok;
|
|
191
199
|
}
|
|
192
200
|
catch {
|
|
@@ -71,7 +71,7 @@ export declare class SubscriptionService {
|
|
|
71
71
|
private readonly logins;
|
|
72
72
|
private readonly probes;
|
|
73
73
|
constructor(options: SubscriptionServiceOptions);
|
|
74
|
-
list(): Promise<ProviderView[]>;
|
|
74
|
+
list(observeOnly?: boolean): Promise<ProviderView[]>;
|
|
75
75
|
/** Health + account + (Codex) usage for one profile. Concurrent calls share one run. */
|
|
76
76
|
check(provider: SubscriptionProvider, profileId: string): Promise<CheckResult>;
|
|
77
77
|
private checkClaude;
|
|
@@ -23,7 +23,7 @@ export class SubscriptionService {
|
|
|
23
23
|
this.bins = options.binaries ?? defaultBinaries();
|
|
24
24
|
this.now = options.now ?? Date.now;
|
|
25
25
|
}
|
|
26
|
-
async list() {
|
|
26
|
+
async list(observeOnly = false) {
|
|
27
27
|
const state = readSubscriptionState();
|
|
28
28
|
const pins = await this.pins();
|
|
29
29
|
return SUBSCRIPTION_PROVIDERS.map(provider => {
|
|
@@ -31,7 +31,7 @@ export class SubscriptionService {
|
|
|
31
31
|
const profiles = p.profiles.map(profile => {
|
|
32
32
|
const key = `${provider}/${profile.id}`;
|
|
33
33
|
const cached = this.checks.get(key);
|
|
34
|
-
if (!cached || this.now() - Date.parse(cached.health.checkedAt ?? '0') > STALE_CHECK_MS)
|
|
34
|
+
if (!observeOnly && (!cached || this.now() - Date.parse(cached.health.checkedAt ?? '0') > STALE_CHECK_MS))
|
|
35
35
|
void this.check(provider, profile.id).catch(() => { });
|
|
36
36
|
const observed = provider === 'claude' ? this.observedUsage(pins, profile.id) : undefined;
|
|
37
37
|
const probed = provider === 'claude' ? this.probedUsage(profile.id) : undefined;
|
|
@@ -135,8 +135,8 @@ export class SubscriptionService {
|
|
|
135
135
|
}
|
|
136
136
|
const { id: profileId, home } = createProfileHome(provider);
|
|
137
137
|
const login = provider === 'claude'
|
|
138
|
-
? new ClaudeLogin(owner, this.bins, home, profileId)
|
|
139
|
-
: new CodexDeviceLogin(owner, this.bins, home, profileId);
|
|
138
|
+
? new ClaudeLogin(owner, this.bins, home, profileId, this.options.onChange)
|
|
139
|
+
: new CodexDeviceLogin(owner, this.bins, home, profileId, this.options.onChange);
|
|
140
140
|
const loginId = login.view().loginId;
|
|
141
141
|
this.logins.set(loginId, login);
|
|
142
142
|
void this.completeLogin(provider, profileId, login);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publishFleetResourceCommit } from '../application/resource-observer.js';
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
|
|
2
3
|
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -94,6 +95,7 @@ export async function updateSubscriptionState(fn) {
|
|
|
94
95
|
const state = readSubscriptionState();
|
|
95
96
|
const result = fn(state);
|
|
96
97
|
replaceFileAtomically(stateFile(), `${JSON.stringify(state, null, 2)}\n`, 0o600);
|
|
98
|
+
publishFleetResourceCommit(stateFile());
|
|
97
99
|
return result;
|
|
98
100
|
});
|
|
99
101
|
}
|
|
@@ -137,4 +139,5 @@ export function readPin(stateDir, provider) {
|
|
|
137
139
|
}
|
|
138
140
|
export function writePin(stateDir, pin) {
|
|
139
141
|
replaceFileAtomically(join(stateDir, PIN_FILE), `${JSON.stringify({ [pin.provider]: pin })}\n`, 0o600);
|
|
142
|
+
publishFleetResourceCommit(join(stateDir, PIN_FILE));
|
|
140
143
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateConfiguredAccountOrigin } from './account-origin.js';
|
|
1
2
|
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { decodeWorkspacePayload } from './workspace-enrollment.js';
|
|
3
4
|
/** Read the same private-file transport as enrollment, without consuming the grant. */
|
|
@@ -31,8 +32,13 @@ function decodeTunnelSetupGrant(input) {
|
|
|
31
32
|
catch {
|
|
32
33
|
throw Error('Invalid tunnel setup payload');
|
|
33
34
|
}
|
|
35
|
+
try {
|
|
36
|
+
validateConfiguredAccountOrigin(grant?.appOrigin);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw Error('Tunnel setup grant is invalid or expired; get a fresh command from the App');
|
|
40
|
+
}
|
|
34
41
|
if (grant?.version !== 2 || Object.keys(grant).sort().join(',') !== 'appOrigin,code,expiresAt,version'
|
|
35
|
-
|| !['https://app.ours.network', 'https://app.ours-tunnel.com'].includes(grant.appOrigin)
|
|
36
42
|
|| !/^[A-Za-z0-9_-]{43}$/.test(grant.code) || !Number.isSafeInteger(grant.expiresAt)
|
|
37
43
|
|| grant.expiresAt <= Date.now() || grant.expiresAt > Date.now() + 16 * 60000)
|
|
38
44
|
throw Error('Tunnel setup grant is invalid or expired; get a fresh command from the App');
|
package/dist/watchdog/query.d.ts
CHANGED
|
@@ -39,7 +39,9 @@ export declare function buildWatchdogFindings(cfg: {
|
|
|
39
39
|
* polling. `now` is injectable (defaults to Date.now) so the TTL boundary is
|
|
40
40
|
* unit-testable without real timers.
|
|
41
41
|
*/
|
|
42
|
-
export declare function cachedWatchdogFindingsProvider(build: () => Map<string, WatchdogRoleFinding>, ttlMs: number, now?: () => number): () => Map<string, WatchdogRoleFinding
|
|
42
|
+
export declare function cachedWatchdogFindingsProvider(build: () => Map<string, WatchdogRoleFinding>, ttlMs: number, now?: () => number): (() => Map<string, WatchdogRoleFinding>) & {
|
|
43
|
+
invalidate: () => void;
|
|
44
|
+
};
|
|
43
45
|
export interface WatchdogSummary {
|
|
44
46
|
name: string;
|
|
45
47
|
enabled: boolean;
|
package/dist/watchdog/query.js
CHANGED
|
@@ -59,12 +59,13 @@ export function buildWatchdogFindings(cfg, latestReport) {
|
|
|
59
59
|
*/
|
|
60
60
|
export function cachedWatchdogFindingsProvider(build, ttlMs, now = Date.now) {
|
|
61
61
|
let cached;
|
|
62
|
-
|
|
62
|
+
const read = () => {
|
|
63
63
|
const at = now();
|
|
64
64
|
if (!cached || at - cached.at >= ttlMs)
|
|
65
65
|
cached = { at, value: build() };
|
|
66
66
|
return cached.value;
|
|
67
67
|
};
|
|
68
|
+
return Object.assign(read, { invalidate: () => { cached = undefined; } });
|
|
68
69
|
}
|
|
69
70
|
const DEFAULT_REPORTS_LIMIT = 50;
|
|
70
71
|
const MAX_REPORTS_LIMIT = 500;
|
package/dist/web/auth.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare class WebAuth {
|
|
|
32
32
|
private readonly devices;
|
|
33
33
|
private readonly access;
|
|
34
34
|
private readonly workspaceDevices?;
|
|
35
|
-
readonly appOrigin
|
|
35
|
+
readonly appOrigin?: string | undefined;
|
|
36
36
|
private _bootstrapSecret;
|
|
37
37
|
private bootstrapExpiresAt;
|
|
38
38
|
private bootstrapUsed;
|
|
@@ -43,7 +43,7 @@ export declare class WebAuth {
|
|
|
43
43
|
private readonly transports;
|
|
44
44
|
private readonly renewals;
|
|
45
45
|
private readonly sockets;
|
|
46
|
-
constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig, workspaceDevices?: WorkspaceDeviceStore | undefined, appOrigin?: string);
|
|
46
|
+
constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig, workspaceDevices?: WorkspaceDeviceStore | undefined, appOrigin?: string | undefined);
|
|
47
47
|
get bootstrapSecret(): string;
|
|
48
48
|
get origin(): string;
|
|
49
49
|
get host(): string;
|
package/dist/web/auth.js
CHANGED
|
@@ -27,7 +27,7 @@ export class WebAuth {
|
|
|
27
27
|
transports = new Map();
|
|
28
28
|
renewals = new Map();
|
|
29
29
|
sockets = new Map();
|
|
30
|
-
constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }, workspaceDevices, appOrigin
|
|
30
|
+
constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }, workspaceDevices, appOrigin) {
|
|
31
31
|
this._origin = _origin;
|
|
32
32
|
this._host = _host;
|
|
33
33
|
this.now = now;
|
|
@@ -58,7 +58,7 @@ export class WebAuth {
|
|
|
58
58
|
}
|
|
59
59
|
/** The configured account origin, when this request comes from it and workspace devices are enabled. */
|
|
60
60
|
accountOrigin(request) {
|
|
61
|
-
return this.workspaceDevices && request.headers.origin === this.appOrigin ? this.appOrigin : undefined;
|
|
61
|
+
return this.workspaceDevices && this.appOrigin && request.headers.origin === this.appOrigin ? this.appOrigin : undefined;
|
|
62
62
|
}
|
|
63
63
|
validateBoundary(request, requireOrigin) {
|
|
64
64
|
const host = request.headers.host;
|
|
@@ -68,7 +68,7 @@ export class WebAuth {
|
|
|
68
68
|
throw new FleetError('forbidden', `This address is not configured for the fleet console. Open ${this.origin} or set --public-origin.`);
|
|
69
69
|
if (this.workspaceDevices && String(request.headers.authorization ?? '').startsWith('Bearer ') && request.headers.origin && request.headers.origin !== this.appOrigin && !origins.has(request.headers.origin))
|
|
70
70
|
throw new FleetError('forbidden', 'workspace request Origin does not match configured account origin');
|
|
71
|
-
const workspaceRequest = Boolean(this.workspaceDevices) && request.headers.origin === this.appOrigin
|
|
71
|
+
const workspaceRequest = Boolean(this.workspaceDevices) && Boolean(this.appOrigin) && request.headers.origin === this.appOrigin
|
|
72
72
|
&& (String(request.headers.authorization ?? '').startsWith('Bearer ') || (request.url ?? '').split('?')[0] === '/api/v1/devices/enroll' || request.method === 'OPTIONS');
|
|
73
73
|
if (requireOrigin && !workspaceRequest && (!request.headers.origin || !origins.has(request.headers.origin)))
|
|
74
74
|
throw new FleetError('forbidden', 'request Origin does not match the configured control-panel origin');
|
|
@@ -131,7 +131,7 @@ export class WebAuth {
|
|
|
131
131
|
return session;
|
|
132
132
|
}
|
|
133
133
|
this.validateBoundary(request, mutation);
|
|
134
|
-
if (request.headers.origin === this.appOrigin && !this.allowedOrigins.has(this.appOrigin))
|
|
134
|
+
if (this.appOrigin && request.headers.origin === this.appOrigin && !this.allowedOrigins.has(this.appOrigin))
|
|
135
135
|
throw new FleetError('unauthorized', 'workspace bearer credential required');
|
|
136
136
|
const id = parseCookies(request.headers.cookie ?? '').ofs_session;
|
|
137
137
|
const session = id ? this.sessions.get(id) : undefined;
|
package/dist/web/events.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface FleetEvent {
|
|
|
8
8
|
}
|
|
9
9
|
export declare class FleetEventBus {
|
|
10
10
|
private readonly events;
|
|
11
|
+
private readonly resourceListeners;
|
|
12
|
+
subscribeResources(listener: () => void): () => void;
|
|
11
13
|
private readonly clients;
|
|
12
14
|
publish(kind: string, payload: unknown, roleId?: string): FleetEvent;
|
|
13
15
|
attach(socket: WebSocket, lastEventId?: string): () => void;
|
package/dist/web/events.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
export class FleetEventBus {
|
|
3
3
|
events = [];
|
|
4
|
+
resourceListeners = new Set();
|
|
5
|
+
subscribeResources(listener) { this.resourceListeners.add(listener); return () => { this.resourceListeners.delete(listener); }; }
|
|
4
6
|
clients = new Set();
|
|
5
7
|
publish(kind, payload, roleId) {
|
|
6
8
|
const event = { eventId: randomUUID(), at: new Date().toISOString(), roleId, kind, payload };
|
|
@@ -12,6 +14,12 @@ export class FleetEventBus {
|
|
|
12
14
|
if (socket.readyState === socket.OPEN && socket.bufferedAmount < 1024 * 1024)
|
|
13
15
|
socket.send(frame);
|
|
14
16
|
}
|
|
17
|
+
for (const listener of this.resourceListeners) {
|
|
18
|
+
try {
|
|
19
|
+
listener();
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
}
|
|
15
23
|
return event;
|
|
16
24
|
}
|
|
17
25
|
attach(socket, lastEventId) {
|
|
@@ -30,5 +38,6 @@ export class FleetEventBus {
|
|
|
30
38
|
for (const socket of this.clients)
|
|
31
39
|
socket.close(1001, 'server shutdown');
|
|
32
40
|
this.clients.clear();
|
|
41
|
+
this.resourceListeners.clear();
|
|
33
42
|
}
|
|
34
43
|
}
|
|
@@ -103,17 +103,24 @@ export function createPrefixGateway(options) {
|
|
|
103
103
|
delete headers['sec-websocket-protocol'];
|
|
104
104
|
return { ...route, headers, session, authHeaders, deviceSocket: socketCredential !== undefined };
|
|
105
105
|
};
|
|
106
|
-
const track = (authHeaders, session, close) => {
|
|
106
|
+
const track = (authHeaders, session, close, neutral = false) => {
|
|
107
107
|
if (!session)
|
|
108
108
|
return () => { };
|
|
109
109
|
// The session authenticated for this request, with its own deadline, not whatever is current later.
|
|
110
110
|
const unbind = auth.bindTransport(session, close);
|
|
111
|
-
const timer = setInterval(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
const timer = setInterval(() => {
|
|
112
|
+
if (neutral) {
|
|
113
|
+
if (!auth.sessionValid(session))
|
|
114
|
+
close();
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
try {
|
|
118
|
+
auth.authenticate({ headers: authHeaders });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
close();
|
|
122
|
+
}
|
|
123
|
+
}, neutral ? 1000 : 30_000);
|
|
117
124
|
timer.unref();
|
|
118
125
|
return () => { clearInterval(timer); unbind(); };
|
|
119
126
|
};
|
|
@@ -123,7 +130,7 @@ export function createPrefixGateway(options) {
|
|
|
123
130
|
const accountCors = (req) => {
|
|
124
131
|
const origin = auth?.accountOrigin(req);
|
|
125
132
|
return origin ? { 'access-control-allow-origin': origin, vary: 'Origin',
|
|
126
|
-
'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag, X-Ours-Workspace-Auth, X-Ours-Request-Id' } : {};
|
|
133
|
+
'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag, X-Ours-Workspace-Auth, X-Ours-Request-Id, X-Resource-Producer, X-Resource-Revision, X-Resource-Source-Generation, X-Resource-Source-Position, X-Resource-Connection, X-Resource-View, X-Resource-Epoch, X-Resource-Baseline' } : {};
|
|
127
134
|
};
|
|
128
135
|
const server = createServer((req, res) => {
|
|
129
136
|
const cors = accountCors(req);
|
|
@@ -134,7 +141,7 @@ export function createPrefixGateway(options) {
|
|
|
134
141
|
auth.validateBoundary({ headers: req.headers, method: req.method, url: req.url }, false);
|
|
135
142
|
res.writeHead(204, { ...cors, 'cache-control': 'no-store', 'access-control-max-age': '600',
|
|
136
143
|
'access-control-allow-methods': 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
137
|
-
'access-control-allow-headers': 'Authorization, Content-Type, Idempotency-Key, If-None-Match, Range, X-CSRF-Token, X-Ours-Messenger-CSRF, X-Voice-Duration' });
|
|
144
|
+
'access-control-allow-headers': 'Authorization, Content-Type, Idempotency-Key, If-None-Match, Range, X-CSRF-Token, X-Ours-Messenger-CSRF, X-Voice-Duration, X-Resource-Connection, X-Resource-View, X-Resource-Epoch, X-Resource-Baseline' });
|
|
138
145
|
res.end();
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
@@ -186,7 +193,10 @@ export function createPrefixGateway(options) {
|
|
|
186
193
|
res.writeHead(502, { ...(route.service ? cors : {}), 'content-type': 'application/json' });
|
|
187
194
|
res.end(JSON.stringify({ error: { code: 'upstream_unavailable', message: 'Service is unavailable' } }));
|
|
188
195
|
});
|
|
189
|
-
const
|
|
196
|
+
const neutral = req.method === 'GET' && route.service?.prefix === '/notifications'
|
|
197
|
+
&& new URL(req.url ?? '/', 'http://gateway.invalid').searchParams.get('resources') === '1'
|
|
198
|
+
&& new URL(req.url ?? '/', 'http://gateway.invalid').pathname === '/notifications/api/v1/events';
|
|
199
|
+
const untrack = track(route.authHeaders, route.session, () => { upstream.destroy(); res.destroy(); }, neutral);
|
|
190
200
|
res.on('close', untrack);
|
|
191
201
|
req.on('aborted', () => upstream.destroy());
|
|
192
202
|
res.on('close', () => upstream.destroy());
|