@jeffusion/bungee-types 4.3.0 → 4.4.0
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/config-v2.d.ts +68 -0
- package/dist/config-v2.js +1 -0
- package/dist/daemon-control.d.ts +59 -0
- package/dist/daemon-control.js +369 -0
- package/dist/daemon-file.d.ts +84 -0
- package/dist/daemon-file.js +944 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -0
- package/dist/rate-limit.d.ts +3 -0
- package/dist/rate-limit.js +3 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +2 -0
- package/package.json +15 -5
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AppConfig, Endpoint, PluginConfigOptions, RouteConfig, Service } from './types.js';
|
|
2
|
+
export type ConfigurationId = string;
|
|
3
|
+
export type Sha256Digest = `sha256:${string}`;
|
|
4
|
+
export interface PluginActivationV2 {
|
|
5
|
+
readonly plugin_name: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PluginBindingV2 {
|
|
8
|
+
readonly id: ConfigurationId;
|
|
9
|
+
readonly position: number;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly options?: PluginConfigOptions;
|
|
12
|
+
readonly enabled: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface UpstreamManagedByV2 {
|
|
15
|
+
readonly plugin: string;
|
|
16
|
+
readonly contributionId: string;
|
|
17
|
+
readonly bindingId: ConfigurationId;
|
|
18
|
+
}
|
|
19
|
+
type UpstreamPolicyV2 = Omit<Endpoint, 'id' | 'plugins' | 'weight' | 'priority' | 'is_disabled'>;
|
|
20
|
+
export interface UpstreamV2 extends UpstreamPolicyV2 {
|
|
21
|
+
readonly id: ConfigurationId;
|
|
22
|
+
readonly position: number;
|
|
23
|
+
readonly weight: number;
|
|
24
|
+
readonly priority: number;
|
|
25
|
+
readonly is_disabled: boolean;
|
|
26
|
+
readonly managedBy?: UpstreamManagedByV2;
|
|
27
|
+
readonly plugins: readonly PluginBindingV2[];
|
|
28
|
+
}
|
|
29
|
+
type ServicePolicyV2 = Omit<Service, 'name' | 'endpoints' | 'plugins'>;
|
|
30
|
+
export interface ServiceV2 extends ServicePolicyV2 {
|
|
31
|
+
readonly id: ConfigurationId;
|
|
32
|
+
readonly position: number;
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly endpoints: readonly UpstreamV2[];
|
|
35
|
+
readonly plugins: readonly PluginBindingV2[];
|
|
36
|
+
}
|
|
37
|
+
type RoutePolicyV2 = Omit<RouteConfig, 'path' | 'service' | 'endpoints' | 'plugins'>;
|
|
38
|
+
interface RouteV2Base extends RoutePolicyV2 {
|
|
39
|
+
readonly id: ConfigurationId;
|
|
40
|
+
readonly position: number;
|
|
41
|
+
readonly path: string;
|
|
42
|
+
readonly plugins: readonly PluginBindingV2[];
|
|
43
|
+
}
|
|
44
|
+
export interface ServiceRouteV2 extends RouteV2Base {
|
|
45
|
+
readonly service_id: ConfigurationId;
|
|
46
|
+
readonly endpoints?: never;
|
|
47
|
+
}
|
|
48
|
+
export interface DirectRouteV2 extends RouteV2Base {
|
|
49
|
+
readonly service_id?: never;
|
|
50
|
+
readonly endpoints: readonly UpstreamV2[];
|
|
51
|
+
}
|
|
52
|
+
export type RouteV2 = ServiceRouteV2 | DirectRouteV2;
|
|
53
|
+
type GlobalPolicyV2 = Omit<AppConfig, 'config_version' | 'plugins' | 'services' | 'routes'>;
|
|
54
|
+
export interface LogicalConfigurationV2 extends GlobalPolicyV2 {
|
|
55
|
+
readonly services: readonly ServiceV2[];
|
|
56
|
+
readonly routes: readonly RouteV2[];
|
|
57
|
+
readonly plugins: readonly PluginBindingV2[];
|
|
58
|
+
}
|
|
59
|
+
export interface ConfigurationAggregateV2 {
|
|
60
|
+
readonly logical_configuration: LogicalConfigurationV2;
|
|
61
|
+
readonly plugin_activations: readonly PluginActivationV2[];
|
|
62
|
+
}
|
|
63
|
+
export interface CommittedConfigurationSnapshotV2 {
|
|
64
|
+
readonly revision: number;
|
|
65
|
+
readonly content_hash: Sha256Digest;
|
|
66
|
+
readonly aggregate: ConfigurationAggregateV2;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export declare const DAEMON_METADATA_SCHEMA: "bungee-daemon-metadata-v1";
|
|
2
|
+
export declare const DAEMON_METADATA_MAX_BYTES: number;
|
|
3
|
+
export declare const DAEMON_SHUTDOWN_PATH: "/__bungee/internal/daemon/v1/shutdown";
|
|
4
|
+
export declare const DAEMON_CONTROL_HTTP_PREFIX: "/__bungee/internal/daemon/";
|
|
5
|
+
export declare const DAEMON_AUTHORIZATION_HEADER: "authorization";
|
|
6
|
+
export declare const DAEMON_BOOT_HEADER: "x-bungee-daemon-boot";
|
|
7
|
+
export declare const DAEMON_INSTANCE_HEADER: "x-bungee-daemon-instance";
|
|
8
|
+
export declare const DAEMON_PID_HEADER: "x-bungee-daemon-pid";
|
|
9
|
+
export declare const DAEMON_PROCESS_IDENTITY_MARKER_PREFIX: "--bungee-process-identity=";
|
|
10
|
+
type DaemonMetadataBase = {
|
|
11
|
+
readonly schema: typeof DAEMON_METADATA_SCHEMA;
|
|
12
|
+
readonly launcher_pid: number;
|
|
13
|
+
readonly boot_nonce: string;
|
|
14
|
+
readonly executable: string;
|
|
15
|
+
readonly shutdown_secret: string;
|
|
16
|
+
readonly entrypoint: string | null;
|
|
17
|
+
};
|
|
18
|
+
export type DaemonMetadataLaunching = DaemonMetadataBase & {
|
|
19
|
+
readonly state: 'launching';
|
|
20
|
+
readonly pid: null;
|
|
21
|
+
readonly instance_id: null;
|
|
22
|
+
readonly management_host: null;
|
|
23
|
+
readonly management_port: null;
|
|
24
|
+
};
|
|
25
|
+
export type DaemonMetadataStarting = DaemonMetadataBase & {
|
|
26
|
+
readonly state: 'starting';
|
|
27
|
+
readonly pid: number;
|
|
28
|
+
readonly instance_id: null;
|
|
29
|
+
readonly management_host: null;
|
|
30
|
+
readonly management_port: null;
|
|
31
|
+
};
|
|
32
|
+
type DaemonMetadataServing = DaemonMetadataBase & {
|
|
33
|
+
readonly pid: number;
|
|
34
|
+
readonly instance_id: string;
|
|
35
|
+
readonly management_host: '127.0.0.1';
|
|
36
|
+
readonly management_port: number;
|
|
37
|
+
};
|
|
38
|
+
export type DaemonMetadataArmed = DaemonMetadataServing & {
|
|
39
|
+
readonly state: 'armed';
|
|
40
|
+
};
|
|
41
|
+
export type DaemonMetadataStopping = DaemonMetadataServing & {
|
|
42
|
+
readonly state: 'stopping';
|
|
43
|
+
};
|
|
44
|
+
export type DaemonMetadataV1 = DaemonMetadataLaunching | DaemonMetadataStarting | DaemonMetadataArmed | DaemonMetadataStopping;
|
|
45
|
+
export type DaemonMetadataState = DaemonMetadataV1['state'];
|
|
46
|
+
export type DaemonMetadataCodecErrorCode = 'invalid_metadata' | 'duplicate_key' | 'unknown_field' | 'missing_field' | 'invalid_utf8' | 'message_too_large';
|
|
47
|
+
export declare class DaemonMetadataCodecError extends Error {
|
|
48
|
+
readonly code: DaemonMetadataCodecErrorCode;
|
|
49
|
+
readonly name = "DaemonMetadataCodecError";
|
|
50
|
+
constructor(code: DaemonMetadataCodecErrorCode, message?: string);
|
|
51
|
+
}
|
|
52
|
+
export declare function decodeDaemonShutdownSecret(value: string): Uint8Array;
|
|
53
|
+
export declare function encodeDaemonShutdownSecret(value: Uint8Array): string;
|
|
54
|
+
export declare function validateDaemonMetadataV1(value: unknown): DaemonMetadataV1;
|
|
55
|
+
export declare function decodeDaemonMetadataV1(value: string | Uint8Array): DaemonMetadataV1;
|
|
56
|
+
export declare function encodeDaemonMetadataV1(value: DaemonMetadataV1): string;
|
|
57
|
+
export declare const parseDaemonMetadataV1: typeof decodeDaemonMetadataV1;
|
|
58
|
+
export declare const serializeDaemonMetadataV1: typeof encodeDaemonMetadataV1;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
export const DAEMON_METADATA_SCHEMA = 'bungee-daemon-metadata-v1';
|
|
2
|
+
export const DAEMON_METADATA_MAX_BYTES = 4 * 1024;
|
|
3
|
+
export const DAEMON_SHUTDOWN_PATH = '/__bungee/internal/daemon/v1/shutdown';
|
|
4
|
+
export const DAEMON_CONTROL_HTTP_PREFIX = '/__bungee/internal/daemon/';
|
|
5
|
+
export const DAEMON_AUTHORIZATION_HEADER = 'authorization';
|
|
6
|
+
export const DAEMON_BOOT_HEADER = 'x-bungee-daemon-boot';
|
|
7
|
+
export const DAEMON_INSTANCE_HEADER = 'x-bungee-daemon-instance';
|
|
8
|
+
export const DAEMON_PID_HEADER = 'x-bungee-daemon-pid';
|
|
9
|
+
export const DAEMON_PROCESS_IDENTITY_MARKER_PREFIX = '--bungee-process-identity=';
|
|
10
|
+
export class DaemonMetadataCodecError extends Error {
|
|
11
|
+
constructor(code, message = code) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = 'DaemonMetadataCodecError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
18
|
+
const BASE64URL = /^[A-Za-z0-9_-]{43}$/;
|
|
19
|
+
const METADATA_FIELDS = [
|
|
20
|
+
'schema', 'state', 'launcher_pid', 'boot_nonce', 'shutdown_secret', 'executable', 'entrypoint',
|
|
21
|
+
'pid', 'instance_id', 'management_host', 'management_port',
|
|
22
|
+
];
|
|
23
|
+
function fail(code, message) {
|
|
24
|
+
throw new DaemonMetadataCodecError(code, message);
|
|
25
|
+
}
|
|
26
|
+
function utf8ByteLength(value) {
|
|
27
|
+
let length = 0;
|
|
28
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
29
|
+
const code = value.charCodeAt(index);
|
|
30
|
+
if (code < 0x80)
|
|
31
|
+
length += 1;
|
|
32
|
+
else if (code < 0x800)
|
|
33
|
+
length += 2;
|
|
34
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length
|
|
35
|
+
&& value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) {
|
|
36
|
+
length += 4;
|
|
37
|
+
index += 1;
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
length += 3;
|
|
41
|
+
}
|
|
42
|
+
return length;
|
|
43
|
+
}
|
|
44
|
+
function decodeUtf8(bytes) {
|
|
45
|
+
let result = '';
|
|
46
|
+
for (let index = 0; index < bytes.length;) {
|
|
47
|
+
const first = bytes[index++];
|
|
48
|
+
if (first < 0x80) {
|
|
49
|
+
result += String.fromCharCode(first);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const count = first >= 0xf0 ? 3 : first >= 0xe0 ? 2 : 1;
|
|
53
|
+
if (index + count > bytes.length)
|
|
54
|
+
fail('invalid_utf8', 'metadata is not valid UTF-8');
|
|
55
|
+
let code = first & (count === 3 ? 0x07 : count === 2 ? 0x0f : 0x1f);
|
|
56
|
+
for (let part = 0; part < count; part += 1) {
|
|
57
|
+
const next = bytes[index++];
|
|
58
|
+
if ((next & 0xc0) !== 0x80)
|
|
59
|
+
fail('invalid_utf8', 'metadata is not valid UTF-8');
|
|
60
|
+
code = (code << 6) | (next & 0x3f);
|
|
61
|
+
}
|
|
62
|
+
if ((count === 1 && code < 0x80) || (count === 2 && code < 0x800)
|
|
63
|
+
|| (count === 3 && code < 0x10000) || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
64
|
+
fail('invalid_utf8', 'metadata is not valid UTF-8');
|
|
65
|
+
}
|
|
66
|
+
if (code <= 0xffff)
|
|
67
|
+
result += String.fromCharCode(code);
|
|
68
|
+
else {
|
|
69
|
+
const scalar = code - 0x10000;
|
|
70
|
+
result += String.fromCharCode(0xd800 + (scalar >> 10), 0xdc00 + (scalar & 0x3ff));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function stringEnd(value, start) {
|
|
76
|
+
for (let index = start + 1; index < value.length; index += 1) {
|
|
77
|
+
const char = value.charCodeAt(index);
|
|
78
|
+
if (char === 0x22)
|
|
79
|
+
return index + 1;
|
|
80
|
+
if (char < 0x20)
|
|
81
|
+
fail('invalid_metadata', 'metadata contains an invalid string');
|
|
82
|
+
if (char === 0x5c) {
|
|
83
|
+
index += 1;
|
|
84
|
+
if (index >= value.length)
|
|
85
|
+
fail('invalid_metadata', 'metadata contains an invalid escape');
|
|
86
|
+
if (value[index] === 'u')
|
|
87
|
+
index += 4;
|
|
88
|
+
if (index >= value.length)
|
|
89
|
+
fail('invalid_metadata', 'metadata contains an invalid escape');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
fail('invalid_metadata', 'metadata contains an unterminated string');
|
|
93
|
+
}
|
|
94
|
+
function skipWhitespace(value, start) {
|
|
95
|
+
let index = start;
|
|
96
|
+
while (index < value.length && /\s/.test(value[index]))
|
|
97
|
+
index += 1;
|
|
98
|
+
return index;
|
|
99
|
+
}
|
|
100
|
+
function scanValue(value, start) {
|
|
101
|
+
const index = skipWhitespace(value, start);
|
|
102
|
+
if (value[index] === '"')
|
|
103
|
+
return stringEnd(value, index);
|
|
104
|
+
if (value[index] === '{') {
|
|
105
|
+
let cursor = skipWhitespace(value, index + 1);
|
|
106
|
+
const keys = new Set();
|
|
107
|
+
if (value[cursor] === '}')
|
|
108
|
+
return cursor + 1;
|
|
109
|
+
while (true) {
|
|
110
|
+
if (value[cursor] !== '"')
|
|
111
|
+
fail('invalid_metadata', 'object key must be a string');
|
|
112
|
+
const end = stringEnd(value, cursor);
|
|
113
|
+
let key;
|
|
114
|
+
try {
|
|
115
|
+
key = JSON.parse(value.slice(cursor, end));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
fail('invalid_metadata', 'object key is not valid JSON');
|
|
119
|
+
}
|
|
120
|
+
if (keys.has(key))
|
|
121
|
+
fail('duplicate_key', `duplicate object key: ${key}`);
|
|
122
|
+
keys.add(key);
|
|
123
|
+
cursor = skipWhitespace(value, end);
|
|
124
|
+
if (value[cursor] !== ':')
|
|
125
|
+
fail('invalid_metadata', 'object key is missing a colon');
|
|
126
|
+
cursor = scanValue(value, cursor + 1);
|
|
127
|
+
cursor = skipWhitespace(value, cursor);
|
|
128
|
+
if (value[cursor] === '}')
|
|
129
|
+
return cursor + 1;
|
|
130
|
+
if (value[cursor] !== ',')
|
|
131
|
+
fail('invalid_metadata', 'object is missing a comma');
|
|
132
|
+
cursor = skipWhitespace(value, cursor + 1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (value[index] === '[') {
|
|
136
|
+
let cursor = skipWhitespace(value, index + 1);
|
|
137
|
+
if (value[cursor] === ']')
|
|
138
|
+
return cursor + 1;
|
|
139
|
+
while (true) {
|
|
140
|
+
cursor = scanValue(value, cursor);
|
|
141
|
+
cursor = skipWhitespace(value, cursor);
|
|
142
|
+
if (value[cursor] === ']')
|
|
143
|
+
return cursor + 1;
|
|
144
|
+
if (value[cursor] !== ',')
|
|
145
|
+
fail('invalid_metadata', 'array is missing a comma');
|
|
146
|
+
cursor = skipWhitespace(value, cursor + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
let cursor = index;
|
|
150
|
+
while (cursor < value.length && !',]}'.includes(value[cursor]) && !/\s/.test(value[cursor]))
|
|
151
|
+
cursor += 1;
|
|
152
|
+
if (cursor === index)
|
|
153
|
+
fail('invalid_metadata', 'metadata contains an invalid value');
|
|
154
|
+
return cursor;
|
|
155
|
+
}
|
|
156
|
+
function assertNoDuplicateKeys(value) {
|
|
157
|
+
const end = scanValue(value, 0);
|
|
158
|
+
if (skipWhitespace(value, end) !== value.length)
|
|
159
|
+
fail('invalid_metadata', 'metadata contains trailing data');
|
|
160
|
+
}
|
|
161
|
+
function base64Value(character) {
|
|
162
|
+
if (character >= 'A' && character <= 'Z')
|
|
163
|
+
return character.charCodeAt(0) - 65;
|
|
164
|
+
if (character >= 'a' && character <= 'z')
|
|
165
|
+
return character.charCodeAt(0) - 71;
|
|
166
|
+
if (character >= '0' && character <= '9')
|
|
167
|
+
return character.charCodeAt(0) + 4;
|
|
168
|
+
if (character === '-')
|
|
169
|
+
return 62;
|
|
170
|
+
if (character === '_')
|
|
171
|
+
return 63;
|
|
172
|
+
return -1;
|
|
173
|
+
}
|
|
174
|
+
function encodeSecret(value) {
|
|
175
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
176
|
+
let result = '';
|
|
177
|
+
for (let index = 0; index < value.length; index += 3) {
|
|
178
|
+
const a = value[index];
|
|
179
|
+
const b = index + 1 < value.length ? value[index + 1] : 0;
|
|
180
|
+
const c = index + 2 < value.length ? value[index + 2] : 0;
|
|
181
|
+
result += alphabet[a >> 2] + alphabet[((a & 3) << 4) | (b >> 4)];
|
|
182
|
+
if (index + 1 < value.length)
|
|
183
|
+
result += alphabet[((b & 15) << 2) | (c >> 6)];
|
|
184
|
+
if (index + 2 < value.length)
|
|
185
|
+
result += alphabet[c & 63];
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
function decodeSecret(value) {
|
|
190
|
+
if (!BASE64URL.test(value))
|
|
191
|
+
fail('invalid_metadata', 'shutdown_secret must be canonical base64url');
|
|
192
|
+
const result = new Uint8Array(32);
|
|
193
|
+
let output = 0;
|
|
194
|
+
for (let index = 0; index < value.length; index += 4) {
|
|
195
|
+
const a = base64Value(value[index]);
|
|
196
|
+
const b = base64Value(value[index + 1]);
|
|
197
|
+
const c = index + 2 < value.length ? base64Value(value[index + 2]) : 0;
|
|
198
|
+
const d = index + 3 < value.length ? base64Value(value[index + 3]) : 0;
|
|
199
|
+
if (a < 0 || b < 0 || c < 0 || d < 0)
|
|
200
|
+
fail('invalid_metadata', 'shutdown_secret must be canonical base64url');
|
|
201
|
+
if (output < 32)
|
|
202
|
+
result[output++] = (a << 2) | (b >> 4);
|
|
203
|
+
if (output < 32 && index + 2 < value.length)
|
|
204
|
+
result[output++] = ((b & 15) << 4) | (c >> 2);
|
|
205
|
+
if (output < 32 && index + 3 < value.length)
|
|
206
|
+
result[output++] = ((c & 3) << 6) | d;
|
|
207
|
+
}
|
|
208
|
+
if (encodeSecret(result) !== value)
|
|
209
|
+
fail('invalid_metadata', 'shutdown_secret is not canonical base64url');
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
export function decodeDaemonShutdownSecret(value) {
|
|
213
|
+
return decodeSecret(value);
|
|
214
|
+
}
|
|
215
|
+
export function encodeDaemonShutdownSecret(value) {
|
|
216
|
+
if (value.byteLength !== 32)
|
|
217
|
+
fail('invalid_metadata', 'shutdown_secret must decode to 32 bytes');
|
|
218
|
+
return encodeSecret(value);
|
|
219
|
+
}
|
|
220
|
+
function plain(value) {
|
|
221
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)
|
|
222
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
223
|
+
fail('invalid_metadata', 'metadata must be a plain object');
|
|
224
|
+
}
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
function exactFields(value) {
|
|
228
|
+
const actual = Object.keys(value).sort();
|
|
229
|
+
const expected = [...METADATA_FIELDS].sort();
|
|
230
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
231
|
+
const unknown = actual.find((key) => !expected.includes(key));
|
|
232
|
+
if (unknown !== undefined)
|
|
233
|
+
fail('unknown_field', 'metadata fields are not exact');
|
|
234
|
+
fail(actual.length < expected.length ? 'missing_field' : 'invalid_metadata', 'metadata fields are not exact');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function stringField(value, field) {
|
|
238
|
+
if (typeof value[field] !== 'string')
|
|
239
|
+
fail('invalid_metadata', `${field} must be a string`);
|
|
240
|
+
return value[field];
|
|
241
|
+
}
|
|
242
|
+
function numberField(value, field) {
|
|
243
|
+
if (typeof value[field] !== 'number' || !Number.isSafeInteger(value[field])) {
|
|
244
|
+
fail('invalid_metadata', `${field} must be a safe integer`);
|
|
245
|
+
}
|
|
246
|
+
return value[field];
|
|
247
|
+
}
|
|
248
|
+
function uuidField(value, field) {
|
|
249
|
+
const result = stringField(value, field);
|
|
250
|
+
if (!UUID.test(result))
|
|
251
|
+
fail('invalid_metadata', `${field} must be a canonical lowercase UUID`);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
function absolutePath(value) {
|
|
255
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\\\');
|
|
256
|
+
}
|
|
257
|
+
function entrypointField(value) {
|
|
258
|
+
const entrypoint = value.entrypoint;
|
|
259
|
+
if (entrypoint === null)
|
|
260
|
+
return null;
|
|
261
|
+
if (typeof entrypoint !== 'string' || entrypoint.length === 0 || entrypoint.trim() !== entrypoint
|
|
262
|
+
|| entrypoint.includes('\0') || !absolutePath(entrypoint)) {
|
|
263
|
+
fail('invalid_metadata', 'entrypoint must be null or an absolute non-empty path');
|
|
264
|
+
}
|
|
265
|
+
return entrypoint;
|
|
266
|
+
}
|
|
267
|
+
function nullField(value, field) {
|
|
268
|
+
if (value[field] !== null)
|
|
269
|
+
fail('invalid_metadata', `${field} must be null for this state`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function positivePid(value, field) {
|
|
273
|
+
const result = numberField(value, field);
|
|
274
|
+
if (result <= 0)
|
|
275
|
+
fail('invalid_metadata', `${field} must be positive`);
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
function managementPort(value) {
|
|
279
|
+
const result = numberField(value, 'management_port');
|
|
280
|
+
if (result < 1 || result > 65535)
|
|
281
|
+
fail('invalid_metadata', 'management_port must be a valid TCP port');
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
function managementHost(value) {
|
|
285
|
+
const result = stringField(value, 'management_host');
|
|
286
|
+
if (result !== '127.0.0.1')
|
|
287
|
+
fail('invalid_metadata', 'management_host must be 127.0.0.1');
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
function validate(value) {
|
|
291
|
+
const input = plain(value);
|
|
292
|
+
exactFields(input);
|
|
293
|
+
const schema = stringField(input, 'schema');
|
|
294
|
+
const state = stringField(input, 'state');
|
|
295
|
+
const launcherPid = positivePid(input, 'launcher_pid');
|
|
296
|
+
const bootNonce = uuidField(input, 'boot_nonce');
|
|
297
|
+
const executable = stringField(input, 'executable');
|
|
298
|
+
const secret = stringField(input, 'shutdown_secret');
|
|
299
|
+
const entrypoint = entrypointField(input);
|
|
300
|
+
if (schema !== DAEMON_METADATA_SCHEMA)
|
|
301
|
+
fail('invalid_metadata', 'schema is unsupported');
|
|
302
|
+
if (executable.length === 0 || executable.trim() !== executable || executable.includes('\0')) {
|
|
303
|
+
fail('invalid_metadata', 'executable must be a non-empty unpadded string');
|
|
304
|
+
}
|
|
305
|
+
decodeSecret(secret);
|
|
306
|
+
const base = {
|
|
307
|
+
schema: DAEMON_METADATA_SCHEMA,
|
|
308
|
+
launcher_pid: launcherPid,
|
|
309
|
+
boot_nonce: bootNonce,
|
|
310
|
+
executable,
|
|
311
|
+
shutdown_secret: secret,
|
|
312
|
+
entrypoint,
|
|
313
|
+
};
|
|
314
|
+
let result;
|
|
315
|
+
if (state === 'launching') {
|
|
316
|
+
result = Object.freeze({
|
|
317
|
+
...base, state, pid: nullField(input, 'pid'), instance_id: nullField(input, 'instance_id'),
|
|
318
|
+
management_host: nullField(input, 'management_host'), management_port: nullField(input, 'management_port'),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
else if (state === 'starting') {
|
|
322
|
+
result = Object.freeze({
|
|
323
|
+
...base, state, pid: positivePid(input, 'pid'), instance_id: nullField(input, 'instance_id'),
|
|
324
|
+
management_host: nullField(input, 'management_host'), management_port: nullField(input, 'management_port'),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
else if (state === 'armed' || state === 'stopping') {
|
|
328
|
+
result = Object.freeze({
|
|
329
|
+
...base, state, pid: positivePid(input, 'pid'), instance_id: uuidField(input, 'instance_id'),
|
|
330
|
+
management_host: managementHost(input), management_port: managementPort(input),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
fail('invalid_metadata', 'state is invalid');
|
|
335
|
+
}
|
|
336
|
+
if (utf8ByteLength(JSON.stringify(result)) > DAEMON_METADATA_MAX_BYTES) {
|
|
337
|
+
fail('message_too_large', 'daemon metadata exceeds 4 KiB');
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
export function validateDaemonMetadataV1(value) {
|
|
342
|
+
return validate(value);
|
|
343
|
+
}
|
|
344
|
+
export function decodeDaemonMetadataV1(value) {
|
|
345
|
+
if (value instanceof Uint8Array && value.byteLength > DAEMON_METADATA_MAX_BYTES) {
|
|
346
|
+
fail('message_too_large', 'daemon metadata exceeds 4 KiB');
|
|
347
|
+
}
|
|
348
|
+
const text = typeof value === 'string' ? value : decodeUtf8(value);
|
|
349
|
+
if (utf8ByteLength(text) > DAEMON_METADATA_MAX_BYTES)
|
|
350
|
+
fail('message_too_large', 'daemon metadata exceeds 4 KiB');
|
|
351
|
+
try {
|
|
352
|
+
assertNoDuplicateKeys(text);
|
|
353
|
+
return validate(JSON.parse(text));
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (error instanceof DaemonMetadataCodecError)
|
|
357
|
+
throw error;
|
|
358
|
+
fail('invalid_metadata', 'metadata is not valid JSON');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
export function encodeDaemonMetadataV1(value) {
|
|
362
|
+
const metadata = validate(value);
|
|
363
|
+
const result = JSON.stringify(metadata);
|
|
364
|
+
if (utf8ByteLength(result) > DAEMON_METADATA_MAX_BYTES)
|
|
365
|
+
fail('message_too_large', 'daemon metadata exceeds 4 KiB');
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
export const parseDaemonMetadataV1 = decodeDaemonMetadataV1;
|
|
369
|
+
export const serializeDaemonMetadataV1 = encodeDaemonMetadataV1;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { DaemonMetadataState, DaemonMetadataV1 } from './daemon-control.js';
|
|
2
|
+
export type DaemonFileErrorCode = 'race' | 'path' | 'symlink' | 'containment' | 'directory' | 'owner' | 'permissions' | 'file' | 'limit' | 'invalid' | 'acl' | 'state' | 'secret' | 'transition';
|
|
3
|
+
export declare class DaemonFileError extends Error {
|
|
4
|
+
readonly code: DaemonFileErrorCode;
|
|
5
|
+
readonly name = "DaemonFileError";
|
|
6
|
+
constructor(code: DaemonFileErrorCode, message?: string, cause?: unknown);
|
|
7
|
+
}
|
|
8
|
+
export type WindowsAclEntry = {
|
|
9
|
+
readonly sid: string;
|
|
10
|
+
readonly access: 'allow' | 'deny';
|
|
11
|
+
readonly rights: number;
|
|
12
|
+
readonly inheritance: number;
|
|
13
|
+
readonly propagation: number;
|
|
14
|
+
readonly inherited: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type WindowsAclSnapshot = {
|
|
17
|
+
readonly currentSid: string;
|
|
18
|
+
readonly entries: readonly WindowsAclEntry[];
|
|
19
|
+
};
|
|
20
|
+
export type WindowsAclAdapter = {
|
|
21
|
+
readonly read: (path: string) => Promise<WindowsAclSnapshot>;
|
|
22
|
+
readonly set: (path: string, currentSid: string, kind?: 'directory' | 'file') => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
/** Test-only evidence points for the create-launching primitive. */
|
|
25
|
+
export type DaemonFileTestStage = 'runtime_components' | 'runtime_lstat' | 'runtime_mkdir' | 'runtime_created_lstat' | 'runtime_realpath' | 'profile_realpath' | 'directory_acl' | 'target_components' | 'target_lstat' | 'target_realpath' | 'create_open' | 'descriptor_stat' | 'verify_lstat' | 'file_acl' | 'file_write' | 'file_sync' | 'file_close';
|
|
26
|
+
export type DaemonFileOptions = {
|
|
27
|
+
/** The trusted, canonical runtime root. The only accepted target is root/daemon.json. */
|
|
28
|
+
readonly runtimeDirectory: string;
|
|
29
|
+
/** Test-only platform injection; production leaves this unset. */
|
|
30
|
+
readonly platform?: NodeJS.Platform;
|
|
31
|
+
readonly windowsAcl?: WindowsAclAdapter;
|
|
32
|
+
/** Deterministic race injection for the metadata primitive tests. */
|
|
33
|
+
readonly testHooks?: {
|
|
34
|
+
readonly onStage?: (stage: DaemonFileTestStage) => void;
|
|
35
|
+
readonly afterOpen?: (target: string) => void | Promise<void>;
|
|
36
|
+
readonly afterInitialStat?: (target: string) => void | Promise<void>;
|
|
37
|
+
readonly rename?: (from: string, to: string) => Promise<void>;
|
|
38
|
+
readonly sleep?: (milliseconds: number) => Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/** Formats only the bounded, allowlisted fields safe for exposing a Windows ACL failure. */
|
|
42
|
+
export declare function formatDaemonFileAclError(error: unknown): string | null;
|
|
43
|
+
/** @internal source-test probe; not re-exported from the package root. */
|
|
44
|
+
export declare function __testSelectWindowsAclExecutable(environment: NodeJS.ProcessEnv): string;
|
|
45
|
+
/** @internal source-test probe; not re-exported from the package root. */
|
|
46
|
+
export declare function __testReadWindowsAcl(path: string, deadlineMs?: number): Promise<WindowsAclSnapshot>;
|
|
47
|
+
/** @internal source-test probe; not re-exported from the package root. */
|
|
48
|
+
export declare function __testEnsureWindowsAcl(path: string, kind: 'directory' | 'file', deadlineMs?: number): Promise<void>;
|
|
49
|
+
/** @internal source-test probe; not re-exported from the package root. */
|
|
50
|
+
export declare function __testWindowsAclIsSecure(snapshot: WindowsAclSnapshot, kind: 'directory' | 'file'): boolean;
|
|
51
|
+
/** @internal source-test factory; not re-exported from the package root. */
|
|
52
|
+
export declare function __testCreateDaemonFileAclError(overrides?: Readonly<Record<string, unknown>>): DaemonFileError;
|
|
53
|
+
export declare function readDaemonMetadataFile(path: string, options: DaemonFileOptions): Promise<DaemonMetadataV1>;
|
|
54
|
+
export type DaemonMetadataTransition = {
|
|
55
|
+
readonly expectedBootNonce: string;
|
|
56
|
+
readonly expectedState: DaemonMetadataState;
|
|
57
|
+
readonly expectedShutdownSecret: string;
|
|
58
|
+
readonly next: DaemonMetadataV1;
|
|
59
|
+
};
|
|
60
|
+
export declare function transitionDaemonMetadataFile(path: string, transition: DaemonMetadataTransition, options: DaemonFileOptions): Promise<DaemonMetadataV1>;
|
|
61
|
+
export type DaemonMetadataDeleteExpectation = {
|
|
62
|
+
readonly bootNonce: string;
|
|
63
|
+
readonly state: DaemonMetadataState;
|
|
64
|
+
readonly shutdownSecret: string;
|
|
65
|
+
};
|
|
66
|
+
export type DaemonMetadataMasterDeleteExpectation = {
|
|
67
|
+
readonly bootNonce: string;
|
|
68
|
+
readonly shutdownSecret: string;
|
|
69
|
+
readonly pid: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Deletes metadata only after the caller has independently proven the owner is dead.
|
|
73
|
+
* This is a guarded delete, not a cross-process CAS primitive.
|
|
74
|
+
*/
|
|
75
|
+
export declare function deleteDaemonMetadataAfterOwnerExit(path: string, expected: DaemonMetadataDeleteExpectation, options: DaemonFileOptions): Promise<boolean>;
|
|
76
|
+
export type DaemonMetadataLauncherDeleteExpectation = {
|
|
77
|
+
readonly bootNonce: string;
|
|
78
|
+
readonly shutdownSecret: string;
|
|
79
|
+
};
|
|
80
|
+
/** Deletes a launch record owned by the current CLI process. */
|
|
81
|
+
export declare function deleteDaemonMetadataForLauncher(path: string, expected: DaemonMetadataLauncherDeleteExpectation, options: DaemonFileOptions): Promise<boolean>;
|
|
82
|
+
/** Deletes the current master's stopping record after runtime cleanup succeeds. */
|
|
83
|
+
export declare function deleteDaemonMetadataForMaster(path: string, expected: DaemonMetadataMasterDeleteExpectation, options: DaemonFileOptions): Promise<boolean>;
|
|
84
|
+
export declare function createLaunchingDaemonMetadataFile(path: string, metadata: DaemonMetadataV1, options: DaemonFileOptions): Promise<void>;
|