@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.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.49",
3
- "buildId": "992b454dab6f",
4
- "commit": "007bb41d5f822b22ebc9c708882f6cd7e70ae537",
2
+ "version": "1.2.0-nightly.50",
3
+ "buildId": "f9c280d92583",
4
+ "commit": "e8706ae9eecd3a26992da89c9c7c38651a8ea5fd",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-06T08:03:31.440Z",
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 FleetError('unauthorized', 'workspace bearer credential is invalid');
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
- res.writeHead(code === 'unauthorized' ? 401 : 403, { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store' });
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];
@@ -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);
@@ -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 FleetError('unauthorized', 'workspace device is missing, expired, or revoked');
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.49",
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",