@ours.network/fleet 1.2.0-nightly.49 → 1.2.0-nightly.50
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/build-info.json +4 -4
- package/dist/web/auth.js +2 -1
- package/dist/web/prefix-gateway.d.ts +3 -0
- package/dist/web/prefix-gateway.js +30 -2
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +6 -0
- package/dist/web/workspace-devices.d.ts +5 -0
- package/dist/web/workspace-devices.js +5 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.50",
|
|
3
|
+
"buildId": "f9c280d92583",
|
|
4
|
+
"commit": "e8706ae9eecd3a26992da89c9c7c38651a8ea5fd",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-06T10:15:17.427Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"managed-cli.codex-reasoning-effort-v1",
|
|
9
9
|
"managed-cli.setup-v1",
|
package/dist/web/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WorkspaceDeviceAuthError } from './workspace-devices.js';
|
|
1
2
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { FleetError } from '../application/errors.js';
|
|
3
4
|
import { TrustedDeviceStore } from './device-store.js';
|
|
@@ -117,7 +118,7 @@ export class WebAuth {
|
|
|
117
118
|
if (authorization.startsWith('Bearer ')) {
|
|
118
119
|
this.validateBoundary(request, mutation);
|
|
119
120
|
if (!this.workspaceDevices)
|
|
120
|
-
throw new
|
|
121
|
+
throw new WorkspaceDeviceAuthError('workspace bearer credential is invalid');
|
|
121
122
|
const device = this.requireWorkspaceDevices().authenticate(authorization.slice(7));
|
|
122
123
|
const id = `workspace:${device.id}`;
|
|
123
124
|
const session = { id, csrf: '', createdAt: device.createdAt, lastSeenAt: this.now(), absoluteExpiresAt: device.expiresAt };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuditSink } from './audit.js';
|
|
1
2
|
import { type IncomingMessage } from 'node:http';
|
|
2
3
|
import type { WebAuth } from './auth.js';
|
|
3
4
|
/** Each entry exposes one dedicated backend listener. Endpoint dispatch stays upstream. */
|
|
@@ -21,6 +22,8 @@ export interface GatewayOptions {
|
|
|
21
22
|
auth: WebAuth | 'backend';
|
|
22
23
|
fleetOrigin: string;
|
|
23
24
|
services: ServiceTarget[];
|
|
25
|
+
/** Bounded unauthorized-response diagnostics: fixed service/action enums only. */
|
|
26
|
+
audit?: Pick<AuditSink, 'record'>;
|
|
24
27
|
}
|
|
25
28
|
/** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
|
|
26
29
|
export declare function createPrefixGateway(options: GatewayOptions): {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { WorkspaceDeviceAuthError } from './workspace-devices.js';
|
|
1
3
|
import { createServer, request as httpRequest } from 'node:http';
|
|
2
4
|
import { request as httpsRequest } from 'node:https';
|
|
3
5
|
/** The App offers exactly these two subprotocols; only the marker is ever answered or forwarded. */
|
|
@@ -22,6 +24,16 @@ function headersFor(message) {
|
|
|
22
24
|
/** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
|
|
23
25
|
export function createPrefixGateway(options) {
|
|
24
26
|
const auth = options.auth === 'backend' ? undefined : options.auth;
|
|
27
|
+
let auditWindow = Date.now(), auditCount = 0;
|
|
28
|
+
const rejected = (requestId, prefix, result) => {
|
|
29
|
+
if (Date.now() - auditWindow >= 60_000) {
|
|
30
|
+
auditWindow = Date.now();
|
|
31
|
+
auditCount = 0;
|
|
32
|
+
}
|
|
33
|
+
if (auditCount++ >= 60)
|
|
34
|
+
return;
|
|
35
|
+
void options.audit?.record({ requestId, action: 'gateway' + prefix.replaceAll('/', '.'), result, errorCode: 'unauthorized' }).catch(() => { });
|
|
36
|
+
};
|
|
25
37
|
const services = options.services.map(target => {
|
|
26
38
|
if (!/^\/[a-z][a-z0-9-]*$/.test(target.prefix) || target.prefix === '/fleet')
|
|
27
39
|
throw new Error('invalid service prefix');
|
|
@@ -67,6 +79,8 @@ export function createPrefixGateway(options) {
|
|
|
67
79
|
for (const key of Object.keys(headers))
|
|
68
80
|
if (key.startsWith('x-forwarded-') || key === 'forwarded')
|
|
69
81
|
delete headers[key];
|
|
82
|
+
delete headers['x-ours-workspace-auth'];
|
|
83
|
+
delete headers['x-ours-request-id'];
|
|
70
84
|
if (route.service) {
|
|
71
85
|
if (auth)
|
|
72
86
|
for (const name of ['cookie', 'authorization', 'x-csrf-token', 'x-ours-api-token'])
|
|
@@ -108,10 +122,11 @@ export function createPrefixGateway(options) {
|
|
|
108
122
|
const accountCors = (req) => {
|
|
109
123
|
const origin = auth?.accountOrigin(req);
|
|
110
124
|
return origin ? { 'access-control-allow-origin': origin, vary: 'Origin',
|
|
111
|
-
'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag' } : {};
|
|
125
|
+
'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag, X-Ours-Workspace-Auth, X-Ours-Request-Id' } : {};
|
|
112
126
|
};
|
|
113
127
|
const server = createServer((req, res) => {
|
|
114
128
|
const cors = accountCors(req);
|
|
129
|
+
const requestId = randomUUID();
|
|
115
130
|
let route;
|
|
116
131
|
try {
|
|
117
132
|
if (req.method === 'OPTIONS' && cors.vary && req.headers['access-control-request-method'] && select(req).service) {
|
|
@@ -126,7 +141,12 @@ export function createPrefixGateway(options) {
|
|
|
126
141
|
}
|
|
127
142
|
catch (error) {
|
|
128
143
|
const code = error.code;
|
|
129
|
-
|
|
144
|
+
const deviceRejected = error instanceof WorkspaceDeviceAuthError;
|
|
145
|
+
if (code === 'unauthorized')
|
|
146
|
+
rejected(requestId, '/auth', deviceRejected ? 'device_rejected' : 'gateway_rejected');
|
|
147
|
+
res.writeHead(code === 'unauthorized' ? 401 : 403, { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store',
|
|
148
|
+
...(code === 'unauthorized' ? { 'x-ours-request-id': requestId } : {}),
|
|
149
|
+
...(deviceRejected ? { 'x-ours-workspace-auth': 'rejected' } : {}) });
|
|
130
150
|
res.end(JSON.stringify({ error: { code: code ?? 'forbidden', message: error.message } }));
|
|
131
151
|
return;
|
|
132
152
|
}
|
|
@@ -136,6 +156,14 @@ export function createPrefixGateway(options) {
|
|
|
136
156
|
const headers = headersFor(response);
|
|
137
157
|
if (route.service) {
|
|
138
158
|
delete headers['set-cookie'];
|
|
159
|
+
// Only Fleet may assert device rejection; never trust a service's marker or correlation id.
|
|
160
|
+
delete headers['x-ours-workspace-auth'];
|
|
161
|
+
delete headers['x-ours-request-id'];
|
|
162
|
+
if (response.statusCode === 401) {
|
|
163
|
+
headers['x-ours-request-id'] = requestId;
|
|
164
|
+
headers['x-ours-workspace-auth'] = 'service';
|
|
165
|
+
rejected(requestId, route.service.prefix, 'upstream_401');
|
|
166
|
+
}
|
|
139
167
|
for (const key of Object.keys(headers))
|
|
140
168
|
if (key.startsWith('access-control-'))
|
|
141
169
|
delete headers[key];
|
package/dist/web/runtime.js
CHANGED
|
@@ -249,7 +249,7 @@ export async function startWebConsole(options) {
|
|
|
249
249
|
const services = ['daemon', 'cowork', 'messenger', 'notifications'].map(name => ({ prefix: '/' + name, origin: provider.origin, upstreamPrefix: basePath + '/' + name, stripBrowserContext: true, headers: { 'X-Ours-Api-Token': credential, ...(name === 'messenger' ? { Origin: provider.origin, 'X-Ours-Messenger-CSRF': '1' } : name === 'cowork' ? { Origin: provider.origin } : {}) },
|
|
250
250
|
// The App reports a visible page to the notification service, so an open app is not sent a push.
|
|
251
251
|
...(name === 'notifications' ? { deviceSocketPaths: ['/api/v1/presence'] } : {}) }));
|
|
252
|
-
gateway = createPrefixGateway({ auth: server.auth, fleetOrigin: address, services });
|
|
252
|
+
gateway = createPrefixGateway({ auth: server.auth, fleetOrigin: address, services, audit });
|
|
253
253
|
const listen = (port) => new Promise((resolve, reject) => { gateway.server.once('error', reject); gateway.server.listen(port, bind, () => { gateway.server.off('error', reject); resolve(); }); });
|
|
254
254
|
try {
|
|
255
255
|
await listen(requestedPort);
|
package/dist/web/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WorkspaceDeviceAuthError } from './workspace-devices.js';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { packagedPresetRoot } from '../preset-bootstrap.js';
|
|
3
4
|
import { validateCatalog } from '../init-wizard.js';
|
|
@@ -48,6 +49,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
48
49
|
if (request.headers.origin === auth.appOrigin && request.url.startsWith('/api/')) {
|
|
49
50
|
reply.header('Access-Control-Allow-Origin', auth.appOrigin).header('Vary', 'Origin');
|
|
50
51
|
reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
52
|
+
reply.header('Access-Control-Expose-Headers', 'X-Ours-Workspace-Auth, X-Ours-Request-Id');
|
|
51
53
|
reply.header('Access-Control-Allow-Headers', 'Authorization,Content-Type,X-CSRF-Token,Idempotency-Key');
|
|
52
54
|
if (request.method === 'OPTIONS') {
|
|
53
55
|
auth.validateBoundary(request, false);
|
|
@@ -78,6 +80,10 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
78
80
|
requestId: request.id, action: `${request.method} ${request.routeOptions.url ?? request.url}`,
|
|
79
81
|
result: 'rejected', errorCode: fleetError.code,
|
|
80
82
|
});
|
|
83
|
+
if (error instanceof WorkspaceDeviceAuthError)
|
|
84
|
+
reply.header('X-Ours-Workspace-Auth', 'rejected');
|
|
85
|
+
if (fleetError.code === 'unauthorized')
|
|
86
|
+
reply.header('X-Ours-Request-Id', request.id);
|
|
81
87
|
reply.code(statusFor(fleetError.code)).send({ error: fleetError.toJSON() });
|
|
82
88
|
});
|
|
83
89
|
app.post('/api/v1/auth/exchange', async (request, reply) => {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { FleetError } from '../application/errors.js';
|
|
2
|
+
/** Only failure of the enrolled device check may invalidate the App's saved credential. */
|
|
3
|
+
export declare class WorkspaceDeviceAuthError extends FleetError {
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
1
6
|
export interface WorkspaceDevice {
|
|
2
7
|
id: string;
|
|
3
8
|
label: string;
|
|
@@ -6,6 +6,10 @@ import { stateRoot } from '../paths.js';
|
|
|
6
6
|
import { FleetError } from '../application/errors.js';
|
|
7
7
|
const opaque = () => randomBytes(32).toString('base64url');
|
|
8
8
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
9
|
+
/** Only failure of the enrolled device check may invalidate the App's saved credential. */
|
|
10
|
+
export class WorkspaceDeviceAuthError extends FleetError {
|
|
11
|
+
constructor(message = 'workspace device is missing, expired, or revoked') { super('unauthorized', message); }
|
|
12
|
+
}
|
|
9
13
|
export class WorkspaceDeviceStore {
|
|
10
14
|
now;
|
|
11
15
|
db;
|
|
@@ -73,7 +77,7 @@ export class WorkspaceDeviceStore {
|
|
|
73
77
|
const row = this.db.prepare('SELECT * FROM devices WHERE id=? AND revokedAt IS NULL AND expiresAt>?').get(id, now);
|
|
74
78
|
const supplied = Buffer.from(hash(token || '')), expected = Buffer.from(String(row?.hash || '0'.repeat(64)));
|
|
75
79
|
if (!row || !timingSafeEqual(supplied, expected))
|
|
76
|
-
throw new
|
|
80
|
+
throw new WorkspaceDeviceAuthError();
|
|
77
81
|
this.db.prepare('UPDATE devices SET lastUsedAt=? WHERE id=?').run(now, id);
|
|
78
82
|
return { id, label: String(row.label), createdAt: Number(row.createdAt), lastUsedAt: now, expiresAt: Number(row.expiresAt), revokedAt: null };
|
|
79
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.50",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|