@hachej/boring-bash 0.1.64
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/LICENSE +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -0
- package/dist/server/index.d.ts +228 -0
- package/dist/server/index.js +697 -0
- package/dist/shared/index.d.ts +46 -0
- package/dist/shared/index.js +0 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 boringdata
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BoundFilesystemContext, FilesystemAccess, FilesystemBinding, FilesystemBindingProvider, FilesystemBindingResolver, FilesystemId, FilesystemProjection, PreparedFilesystemBinding, RuntimeBindingPlan } from './shared/index.js';
|
package/dist/index.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { PreparedFilesystemBinding, FilesystemId, FilesystemProjection, BoundFilesystemContext, FilesystemBindingProvider, FilesystemBinding, FilesystemAccess, FilesystemBindingResolver, RuntimeBindingPlan } from '../shared/index.js';
|
|
2
|
+
|
|
3
|
+
declare const COMPANY_CONTEXT_FILESYSTEM_ID = "company_context";
|
|
4
|
+
declare const COMPANY_CONTEXT_SENTINEL = "FORBIDDEN_FINANCE_SECRET_123";
|
|
5
|
+
interface CompanyContextFixtureFile {
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
declare const DEFAULT_COMPANY_CONTEXT_FIXTURE_FILES: readonly CompanyContextFixtureFile[];
|
|
10
|
+
interface CompanyContextFixtureProjectionPolicy {
|
|
11
|
+
readonly allowedPathPrefixes: readonly string[];
|
|
12
|
+
readonly grantManagementBinding?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface CompanyContextFixtureProviderOptions {
|
|
15
|
+
readonly sourceRoot: string;
|
|
16
|
+
readonly projectionRootParent?: string;
|
|
17
|
+
readonly resolvePolicy: (ctx: BoundFilesystemContext) => CompanyContextFixtureProjectionPolicy | Promise<CompanyContextFixtureProjectionPolicy>;
|
|
18
|
+
}
|
|
19
|
+
interface CompanyContextFixturePreparedLifecycle {
|
|
20
|
+
active: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface CompanyContextFixturePreparedHandle {
|
|
23
|
+
readonly kind: "company-context-fixture-projection";
|
|
24
|
+
/** Logical filesystem identity for this prepared binding; paths are scoped by this id. */
|
|
25
|
+
readonly filesystem: FilesystemId;
|
|
26
|
+
readonly sourceRoot: string;
|
|
27
|
+
readonly projectionRoot: string;
|
|
28
|
+
readonly visiblePaths: readonly string[];
|
|
29
|
+
readonly access?: "readonly" | "readwrite";
|
|
30
|
+
readonly projection?: FilesystemProjection;
|
|
31
|
+
readonly lifecycle?: CompanyContextFixturePreparedLifecycle;
|
|
32
|
+
}
|
|
33
|
+
type CompanyContextFixturePreparedBinding = PreparedFilesystemBinding & {
|
|
34
|
+
handle: CompanyContextFixturePreparedHandle;
|
|
35
|
+
};
|
|
36
|
+
declare function seedCompanyContextFixture(root: string): Promise<void>;
|
|
37
|
+
declare class FixtureCompanyContextBindingProvider implements FilesystemBindingProvider {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(options: CompanyContextFixtureProviderOptions);
|
|
40
|
+
invalidateBinding(_ctx: BoundFilesystemContext, _filesystem: FilesystemId): Promise<void>;
|
|
41
|
+
disposeBinding(prepared: PreparedFilesystemBinding): Promise<void>;
|
|
42
|
+
prepareBinding(ctx: BoundFilesystemContext, binding: FilesystemBinding): Promise<CompanyContextFixturePreparedBinding>;
|
|
43
|
+
}
|
|
44
|
+
declare function readFixtureProjectionFile(handle: CompanyContextFixturePreparedHandle, companyPath: string): Promise<string>;
|
|
45
|
+
declare function listFixtureProjectionFiles(handle: CompanyContextFixturePreparedHandle): Promise<string[]>;
|
|
46
|
+
|
|
47
|
+
declare const READONLY_PROJECTION_MUTATION_CODE = "READONLY_PROJECTION_READONLY";
|
|
48
|
+
declare const READONLY_PROJECTION_INVALID_PATH_CODE = "READONLY_PROJECTION_INVALID_PATH";
|
|
49
|
+
declare const READONLY_PROJECTION_BINDING_NOT_FOUND_CODE = "READONLY_PROJECTION_BINDING_NOT_FOUND";
|
|
50
|
+
interface FilesystemPathDescriptor {
|
|
51
|
+
readonly filesystem: FilesystemId;
|
|
52
|
+
readonly path: string;
|
|
53
|
+
}
|
|
54
|
+
interface ReadonlyProjectionOperationMetadata {
|
|
55
|
+
readonly filesystem: FilesystemId;
|
|
56
|
+
readonly path: string;
|
|
57
|
+
readonly operation: string;
|
|
58
|
+
}
|
|
59
|
+
declare class ReadonlyProjectionOperationError extends Error {
|
|
60
|
+
readonly code: string;
|
|
61
|
+
readonly metadata: ReadonlyProjectionOperationMetadata;
|
|
62
|
+
constructor(code: string, message: string, metadata: ReadonlyProjectionOperationMetadata);
|
|
63
|
+
}
|
|
64
|
+
interface ReadonlyProjectionSearchOptions {
|
|
65
|
+
readonly offset?: number;
|
|
66
|
+
readonly limit?: number;
|
|
67
|
+
}
|
|
68
|
+
interface ReadonlyProjectionOperations {
|
|
69
|
+
read(descriptor: FilesystemPathDescriptor): Promise<{
|
|
70
|
+
content: string;
|
|
71
|
+
metadata: ReadonlyProjectionOperationMetadata;
|
|
72
|
+
}>;
|
|
73
|
+
list(descriptor: FilesystemPathDescriptor): Promise<{
|
|
74
|
+
entries: string[];
|
|
75
|
+
metadata: ReadonlyProjectionOperationMetadata;
|
|
76
|
+
}>;
|
|
77
|
+
find(descriptor: FilesystemPathDescriptor, pattern: string, options?: ReadonlyProjectionSearchOptions): Promise<{
|
|
78
|
+
paths: string[];
|
|
79
|
+
metadata: ReadonlyProjectionOperationMetadata;
|
|
80
|
+
}>;
|
|
81
|
+
grep(descriptor: FilesystemPathDescriptor, pattern: string, options?: ReadonlyProjectionSearchOptions): Promise<{
|
|
82
|
+
matches: Array<{
|
|
83
|
+
path: string;
|
|
84
|
+
line: number;
|
|
85
|
+
text: string;
|
|
86
|
+
}>;
|
|
87
|
+
metadata: ReadonlyProjectionOperationMetadata;
|
|
88
|
+
}>;
|
|
89
|
+
stat(descriptor: FilesystemPathDescriptor): Promise<{
|
|
90
|
+
isDirectory: boolean;
|
|
91
|
+
metadata: ReadonlyProjectionOperationMetadata;
|
|
92
|
+
}>;
|
|
93
|
+
rejectMutation(operation: string, descriptor: FilesystemPathDescriptor): never;
|
|
94
|
+
}
|
|
95
|
+
interface ReadonlyProjectionHandle {
|
|
96
|
+
readonly filesystem: FilesystemId;
|
|
97
|
+
readonly projectionRoot: string;
|
|
98
|
+
}
|
|
99
|
+
declare function createReadonlyProjectionOperations(handle: ReadonlyProjectionHandle): ReadonlyProjectionOperations;
|
|
100
|
+
|
|
101
|
+
declare const MANAGEMENT_PROJECTION_BINDING_REQUIRED_CODE = "MANAGEMENT_PROJECTION_BINDING_REQUIRED";
|
|
102
|
+
declare const MANAGEMENT_PROJECTION_INVALID_PATH_CODE = "MANAGEMENT_PROJECTION_INVALID_PATH";
|
|
103
|
+
interface ManagementProjectionOperationMetadata {
|
|
104
|
+
readonly filesystem: FilesystemId;
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly operation: string;
|
|
107
|
+
}
|
|
108
|
+
declare class ManagementProjectionOperationError extends Error {
|
|
109
|
+
readonly code: string;
|
|
110
|
+
readonly metadata: ManagementProjectionOperationMetadata;
|
|
111
|
+
constructor(code: string, message: string, metadata: ManagementProjectionOperationMetadata);
|
|
112
|
+
}
|
|
113
|
+
interface ManagementProjectionHandle {
|
|
114
|
+
readonly filesystem: FilesystemId;
|
|
115
|
+
readonly projectionRoot: string;
|
|
116
|
+
readonly access?: "readonly" | "readwrite";
|
|
117
|
+
readonly projection?: "policy-filtered" | "management";
|
|
118
|
+
readonly lifecycle?: {
|
|
119
|
+
readonly active: boolean;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
interface ManagementProjectionOperations {
|
|
123
|
+
read(descriptor: FilesystemPathDescriptor): Promise<{
|
|
124
|
+
content: string;
|
|
125
|
+
metadata: ManagementProjectionOperationMetadata;
|
|
126
|
+
}>;
|
|
127
|
+
list(descriptor: FilesystemPathDescriptor): Promise<{
|
|
128
|
+
entries: string[];
|
|
129
|
+
metadata: ManagementProjectionOperationMetadata;
|
|
130
|
+
}>;
|
|
131
|
+
write(descriptor: FilesystemPathDescriptor, content: string): Promise<{
|
|
132
|
+
metadata: ManagementProjectionOperationMetadata;
|
|
133
|
+
}>;
|
|
134
|
+
edit(descriptor: FilesystemPathDescriptor, oldText: string, newText: string): Promise<{
|
|
135
|
+
metadata: ManagementProjectionOperationMetadata;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
declare function createManagementProjectionOperations(handle: ManagementProjectionHandle): ManagementProjectionOperations;
|
|
139
|
+
|
|
140
|
+
interface FilesystemRuntimeLifecycleEvent {
|
|
141
|
+
readonly type: "prepare" | "prepare-error" | "dispose" | "invalidate";
|
|
142
|
+
readonly context: BoundFilesystemContext;
|
|
143
|
+
readonly bindings: readonly string[];
|
|
144
|
+
readonly preparedLabels: readonly string[];
|
|
145
|
+
}
|
|
146
|
+
interface ScopedFilesystemRuntimeBindingManagerOptions {
|
|
147
|
+
readonly resolver: FilesystemBindingResolver;
|
|
148
|
+
readonly providers: Readonly<Record<string, FilesystemBindingProvider>>;
|
|
149
|
+
readonly onLifecycleEvent?: (event: FilesystemRuntimeLifecycleEvent) => void;
|
|
150
|
+
}
|
|
151
|
+
interface PreparedBindingSelector {
|
|
152
|
+
readonly filesystem: FilesystemId;
|
|
153
|
+
readonly access?: FilesystemAccess;
|
|
154
|
+
readonly projection?: FilesystemProjection;
|
|
155
|
+
}
|
|
156
|
+
interface ScopedPreparedFilesystemBinding extends PreparedFilesystemBinding {
|
|
157
|
+
readonly scopeKey: string;
|
|
158
|
+
readonly context: BoundFilesystemContext;
|
|
159
|
+
readonly preparedLabel: string;
|
|
160
|
+
}
|
|
161
|
+
interface ScopedRuntimeBindingPlan extends RuntimeBindingPlan {
|
|
162
|
+
readonly scopeKey: string;
|
|
163
|
+
readonly context: BoundFilesystemContext;
|
|
164
|
+
readonly bindings: ScopedPreparedFilesystemBinding[];
|
|
165
|
+
}
|
|
166
|
+
declare function filesystemRuntimeScopeKey(ctx: BoundFilesystemContext): string;
|
|
167
|
+
declare class ScopedFilesystemRuntimeBindingManager {
|
|
168
|
+
#private;
|
|
169
|
+
constructor(options: ScopedFilesystemRuntimeBindingManagerOptions);
|
|
170
|
+
prepareRuntime(ctx: BoundFilesystemContext): Promise<ScopedRuntimeBindingPlan>;
|
|
171
|
+
getPreparedBinding(ctx: BoundFilesystemContext, selector: PreparedBindingSelector): ScopedPreparedFilesystemBinding | undefined;
|
|
172
|
+
disposeRuntime(ctx: BoundFilesystemContext): Promise<void>;
|
|
173
|
+
invalidate(ctx: BoundFilesystemContext, filesystem: FilesystemId): Promise<void>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface ReadonlyProjectionConformanceSubject {
|
|
177
|
+
readonly filesystem: string;
|
|
178
|
+
readonly rootPath: string;
|
|
179
|
+
readonly operations: ReadonlyProjectionConformanceOperations;
|
|
180
|
+
readonly allowedReadPath: string;
|
|
181
|
+
readonly deniedReadPath: string;
|
|
182
|
+
readonly deniedDirectoryName: string;
|
|
183
|
+
readonly deniedSentinel: string;
|
|
184
|
+
readonly allowedFindPattern: string;
|
|
185
|
+
readonly expectedAllowedFindCount: number;
|
|
186
|
+
readonly expectedVisiblePaths: readonly string[];
|
|
187
|
+
readonly projection: ReadonlyProjectionProbe;
|
|
188
|
+
}
|
|
189
|
+
interface ReadonlyProjectionConformanceOperations {
|
|
190
|
+
read(descriptor: {
|
|
191
|
+
filesystem: string;
|
|
192
|
+
path: string;
|
|
193
|
+
}): Promise<{
|
|
194
|
+
content: string;
|
|
195
|
+
}>;
|
|
196
|
+
list(descriptor: {
|
|
197
|
+
filesystem: string;
|
|
198
|
+
path: string;
|
|
199
|
+
}): Promise<{
|
|
200
|
+
entries: readonly unknown[];
|
|
201
|
+
}>;
|
|
202
|
+
find(descriptor: {
|
|
203
|
+
filesystem: string;
|
|
204
|
+
path: string;
|
|
205
|
+
}, pattern: string): Promise<{
|
|
206
|
+
matches?: readonly unknown[];
|
|
207
|
+
paths?: readonly unknown[];
|
|
208
|
+
}>;
|
|
209
|
+
grep(descriptor: {
|
|
210
|
+
filesystem: string;
|
|
211
|
+
path: string;
|
|
212
|
+
}, pattern: string): Promise<{
|
|
213
|
+
matches: readonly unknown[];
|
|
214
|
+
}>;
|
|
215
|
+
}
|
|
216
|
+
interface ReadonlyProjectionProbe {
|
|
217
|
+
listVisiblePaths(): Promise<readonly string[]>;
|
|
218
|
+
writeExistingAllowedPath(): Promise<unknown>;
|
|
219
|
+
writeNewAllowedPath(): Promise<unknown>;
|
|
220
|
+
followSymlinkEscape(): Promise<unknown>;
|
|
221
|
+
}
|
|
222
|
+
interface ReadonlyProjectionConformanceResult {
|
|
223
|
+
readonly passed: boolean;
|
|
224
|
+
readonly failures: readonly string[];
|
|
225
|
+
}
|
|
226
|
+
declare function checkReadonlyProjectionConformance(subject: ReadonlyProjectionConformanceSubject): Promise<ReadonlyProjectionConformanceResult>;
|
|
227
|
+
|
|
228
|
+
export { BoundFilesystemContext, COMPANY_CONTEXT_FILESYSTEM_ID, COMPANY_CONTEXT_SENTINEL, type CompanyContextFixtureFile, type CompanyContextFixturePreparedBinding, type CompanyContextFixturePreparedHandle, type CompanyContextFixturePreparedLifecycle, type CompanyContextFixtureProjectionPolicy, type CompanyContextFixtureProviderOptions, DEFAULT_COMPANY_CONTEXT_FIXTURE_FILES, FilesystemBinding, FilesystemBindingProvider, FilesystemBindingResolver, FilesystemId, type FilesystemPathDescriptor, type FilesystemRuntimeLifecycleEvent, FixtureCompanyContextBindingProvider, MANAGEMENT_PROJECTION_BINDING_REQUIRED_CODE, MANAGEMENT_PROJECTION_INVALID_PATH_CODE, ManagementProjectionOperationError, type ManagementProjectionOperationMetadata, type ManagementProjectionOperations, type PreparedBindingSelector, PreparedFilesystemBinding, READONLY_PROJECTION_BINDING_NOT_FOUND_CODE, READONLY_PROJECTION_INVALID_PATH_CODE, READONLY_PROJECTION_MUTATION_CODE, type ReadonlyProjectionConformanceOperations, type ReadonlyProjectionConformanceResult, type ReadonlyProjectionConformanceSubject, ReadonlyProjectionOperationError, type ReadonlyProjectionOperationMetadata, type ReadonlyProjectionOperations, type ReadonlyProjectionProbe, RuntimeBindingPlan, ScopedFilesystemRuntimeBindingManager, type ScopedFilesystemRuntimeBindingManagerOptions, type ScopedPreparedFilesystemBinding, type ScopedRuntimeBindingPlan, checkReadonlyProjectionConformance, createManagementProjectionOperations, createReadonlyProjectionOperations, filesystemRuntimeScopeKey, listFixtureProjectionFiles, readFixtureProjectionFile, seedCompanyContextFixture };
|
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
// src/server/testing/companyContextFixtureProvider.ts
|
|
2
|
+
import { constants } from "fs";
|
|
3
|
+
import { access, chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath } from "fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
var COMPANY_CONTEXT_FILESYSTEM_ID = "company_context";
|
|
7
|
+
var COMPANY_CONTEXT_SENTINEL = "FORBIDDEN_FINANCE_SECRET_123";
|
|
8
|
+
var DEFAULT_COMPANY_CONTEXT_FIXTURE_FILES = [
|
|
9
|
+
{ path: "/company/hr/policy.md", content: "# HR policy\nVacation and onboarding policies.\n" },
|
|
10
|
+
{ path: "/company/hr/onboarding.md", content: "# Onboarding\nWelcome to the company.\n" },
|
|
11
|
+
{ path: "/company/finance/budget.md", content: `# Finance budget
|
|
12
|
+
${COMPANY_CONTEXT_SENTINEL}
|
|
13
|
+
` },
|
|
14
|
+
{ path: "/company/legal/contract.md", content: "# Legal contract\nStandard terms.\n" }
|
|
15
|
+
];
|
|
16
|
+
function normalizeCompanyPath(path) {
|
|
17
|
+
const normalized = path.replace(/\\/g, "/");
|
|
18
|
+
if (!normalized.startsWith("/")) return `/${normalized}`;
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
function assertSafeCompanyPath(path) {
|
|
22
|
+
const normalized = normalizeCompanyPath(path);
|
|
23
|
+
if (normalized.includes("\0")) throw new Error(`invalid company path: ${path}`);
|
|
24
|
+
for (const part of normalized.split("/")) {
|
|
25
|
+
if (part === "..") throw new Error(`invalid company path traversal: ${path}`);
|
|
26
|
+
}
|
|
27
|
+
return normalized.replace(/\/+/g, "/");
|
|
28
|
+
}
|
|
29
|
+
function isAllowed(path, prefixes) {
|
|
30
|
+
const normalized = assertSafeCompanyPath(path);
|
|
31
|
+
return prefixes.some((prefix) => {
|
|
32
|
+
const normalizedPrefix = assertSafeCompanyPath(prefix).replace(/\/+$/, "");
|
|
33
|
+
return normalized === normalizedPrefix || normalized.startsWith(`${normalizedPrefix}/`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function assertInsideRoot(root, candidate) {
|
|
37
|
+
const realRoot = await realpath(root);
|
|
38
|
+
const existing = await realpath(candidate);
|
|
39
|
+
const rel = relative(realRoot, existing);
|
|
40
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
41
|
+
throw new Error(`path escapes company context root: ${candidate}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function walkFiles(root, current = root) {
|
|
45
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const absolutePath = join(current, entry.name);
|
|
49
|
+
const entryStat = await lstat(absolutePath);
|
|
50
|
+
if (entryStat.isSymbolicLink()) {
|
|
51
|
+
await assertInsideRoot(root, absolutePath);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (entry.isDirectory()) out.push(...await walkFiles(root, absolutePath));
|
|
55
|
+
else if (entry.isFile()) out.push(absolutePath);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
async function makeProjectionReadonly(root, current = root) {
|
|
60
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const absolutePath = join(current, entry.name);
|
|
63
|
+
if (entry.isDirectory()) await makeProjectionReadonly(root, absolutePath);
|
|
64
|
+
else if (entry.isFile()) await chmod(absolutePath, 292);
|
|
65
|
+
}
|
|
66
|
+
await chmod(current, 365);
|
|
67
|
+
}
|
|
68
|
+
async function seedCompanyContextFixture(root) {
|
|
69
|
+
for (const file of DEFAULT_COMPANY_CONTEXT_FIXTURE_FILES) {
|
|
70
|
+
const safePath = assertSafeCompanyPath(file.path);
|
|
71
|
+
const target = join(root, ...safePath.slice(1).split("/"));
|
|
72
|
+
await mkdir(dirname(target), { recursive: true });
|
|
73
|
+
await access(dirname(target), constants.W_OK);
|
|
74
|
+
await import("fs/promises").then(({ writeFile: writeFile2 }) => writeFile2(target, file.content));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
var FixtureCompanyContextBindingProvider = class {
|
|
78
|
+
#sourceRoot;
|
|
79
|
+
#projectionRootParent;
|
|
80
|
+
#resolvePolicy;
|
|
81
|
+
constructor(options) {
|
|
82
|
+
this.#sourceRoot = resolve(options.sourceRoot);
|
|
83
|
+
this.#projectionRootParent = resolve(options.projectionRootParent ?? tmpdir());
|
|
84
|
+
this.#resolvePolicy = options.resolvePolicy;
|
|
85
|
+
}
|
|
86
|
+
async invalidateBinding(_ctx, _filesystem) {
|
|
87
|
+
}
|
|
88
|
+
async disposeBinding(prepared) {
|
|
89
|
+
const handle = prepared.handle;
|
|
90
|
+
if (handle.lifecycle) handle.lifecycle.active = false;
|
|
91
|
+
}
|
|
92
|
+
async prepareBinding(ctx, binding) {
|
|
93
|
+
if (binding.filesystem !== COMPANY_CONTEXT_FILESYSTEM_ID) {
|
|
94
|
+
throw new Error(`fixture company provider cannot prepare filesystem ${binding.filesystem}`);
|
|
95
|
+
}
|
|
96
|
+
if (binding.access === "readwrite" && binding.projection === "management") {
|
|
97
|
+
const policy2 = await this.#resolvePolicy(ctx);
|
|
98
|
+
if (!policy2.grantManagementBinding) {
|
|
99
|
+
throw new Error("fixture company provider policy did not grant readwrite management binding");
|
|
100
|
+
}
|
|
101
|
+
const lifecycle2 = { active: true };
|
|
102
|
+
return {
|
|
103
|
+
binding,
|
|
104
|
+
handle: {
|
|
105
|
+
kind: "company-context-fixture-projection",
|
|
106
|
+
filesystem: binding.filesystem,
|
|
107
|
+
sourceRoot: this.#sourceRoot,
|
|
108
|
+
projectionRoot: this.#sourceRoot,
|
|
109
|
+
visiblePaths: await listFixtureProjectionFiles({
|
|
110
|
+
kind: "company-context-fixture-projection",
|
|
111
|
+
filesystem: binding.filesystem,
|
|
112
|
+
sourceRoot: this.#sourceRoot,
|
|
113
|
+
projectionRoot: this.#sourceRoot,
|
|
114
|
+
visiblePaths: [],
|
|
115
|
+
access: "readwrite",
|
|
116
|
+
projection: "management"
|
|
117
|
+
}),
|
|
118
|
+
access: "readwrite",
|
|
119
|
+
projection: "management",
|
|
120
|
+
lifecycle: lifecycle2
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (binding.access !== "readonly" || binding.projection !== "policy-filtered") {
|
|
125
|
+
throw new Error("fixture company provider only prepares readonly policy-filtered or readwrite management bindings");
|
|
126
|
+
}
|
|
127
|
+
const policy = await this.#resolvePolicy(ctx);
|
|
128
|
+
const projectionRoot = await mkdtemp(join(this.#projectionRootParent, "boring-company-context-"));
|
|
129
|
+
const visiblePaths = [];
|
|
130
|
+
const files = await walkFiles(this.#sourceRoot);
|
|
131
|
+
for (const sourceFile of files) {
|
|
132
|
+
await assertInsideRoot(this.#sourceRoot, sourceFile);
|
|
133
|
+
const rel = relative(this.#sourceRoot, sourceFile).split(sep).join("/");
|
|
134
|
+
const companyPath = assertSafeCompanyPath(`/${rel}`);
|
|
135
|
+
if (!isAllowed(companyPath, policy.allowedPathPrefixes)) continue;
|
|
136
|
+
const destination = join(projectionRoot, ...companyPath.slice(1).split("/"));
|
|
137
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
138
|
+
await copyFile(sourceFile, destination);
|
|
139
|
+
visiblePaths.push(companyPath);
|
|
140
|
+
}
|
|
141
|
+
visiblePaths.sort();
|
|
142
|
+
await makeProjectionReadonly(projectionRoot);
|
|
143
|
+
const lifecycle = { active: true };
|
|
144
|
+
return {
|
|
145
|
+
binding,
|
|
146
|
+
handle: {
|
|
147
|
+
kind: "company-context-fixture-projection",
|
|
148
|
+
filesystem: binding.filesystem,
|
|
149
|
+
sourceRoot: this.#sourceRoot,
|
|
150
|
+
projectionRoot,
|
|
151
|
+
visiblePaths,
|
|
152
|
+
access: "readonly",
|
|
153
|
+
projection: "policy-filtered",
|
|
154
|
+
lifecycle
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
async function readFixtureProjectionFile(handle, companyPath) {
|
|
160
|
+
const safePath = assertSafeCompanyPath(companyPath);
|
|
161
|
+
const target = join(handle.projectionRoot, ...safePath.slice(1).split("/"));
|
|
162
|
+
await assertInsideRoot(handle.projectionRoot, target);
|
|
163
|
+
return await readFile(target, "utf8");
|
|
164
|
+
}
|
|
165
|
+
async function listFixtureProjectionFiles(handle) {
|
|
166
|
+
const files = await walkFiles(handle.projectionRoot);
|
|
167
|
+
return files.map((file) => `/${relative(handle.projectionRoot, file).split(sep).join("/")}`).sort();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/server/managementProjectionOperations.ts
|
|
171
|
+
import { lstat as lstat2, mkdir as mkdir2, readFile as readFile2, readdir as readdir2, realpath as realpath2, stat, writeFile } from "fs/promises";
|
|
172
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, sep as sep2 } from "path";
|
|
173
|
+
var MANAGEMENT_PROJECTION_BINDING_REQUIRED_CODE = "MANAGEMENT_PROJECTION_BINDING_REQUIRED";
|
|
174
|
+
var MANAGEMENT_PROJECTION_INVALID_PATH_CODE = "MANAGEMENT_PROJECTION_INVALID_PATH";
|
|
175
|
+
var ManagementProjectionOperationError = class extends Error {
|
|
176
|
+
code;
|
|
177
|
+
metadata;
|
|
178
|
+
constructor(code, message, metadata) {
|
|
179
|
+
super(message);
|
|
180
|
+
this.name = "ManagementProjectionOperationError";
|
|
181
|
+
this.code = code;
|
|
182
|
+
this.metadata = metadata;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
function normalizeProjectionPath(path) {
|
|
186
|
+
const normalized = path.replace(/\\/g, "/");
|
|
187
|
+
if (normalized.includes("\0")) throw new Error("null byte in projection path");
|
|
188
|
+
if (normalized.includes(":/")) throw new Error("filesystem prefixes are not valid path strings");
|
|
189
|
+
if (normalized === "") return "/";
|
|
190
|
+
const withRoot = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
191
|
+
const parts = withRoot.split("/").filter(Boolean);
|
|
192
|
+
if (parts.some((part) => part === ".." || part === ".")) throw new Error("path traversal is not allowed");
|
|
193
|
+
return `/${parts.join("/")}`;
|
|
194
|
+
}
|
|
195
|
+
function assertManagementHandle(handle) {
|
|
196
|
+
if (handle.access !== "readwrite" || handle.projection !== "management") {
|
|
197
|
+
throw new ManagementProjectionOperationError(
|
|
198
|
+
MANAGEMENT_PROJECTION_BINDING_REQUIRED_CODE,
|
|
199
|
+
"management operation requires a readwrite management binding",
|
|
200
|
+
{ filesystem: handle.filesystem, path: "management_binding_required", operation: "bind" }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (handle.lifecycle?.active === false) {
|
|
204
|
+
throw new ManagementProjectionOperationError(
|
|
205
|
+
MANAGEMENT_PROJECTION_BINDING_REQUIRED_CODE,
|
|
206
|
+
"management binding is no longer active",
|
|
207
|
+
{ filesystem: handle.filesystem, path: "management_binding_inactive", operation: "bind" }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function assertProjectionDescriptor(descriptor, operation, expectedFilesystem) {
|
|
212
|
+
if (descriptor.filesystem !== expectedFilesystem) {
|
|
213
|
+
throw new ManagementProjectionOperationError(
|
|
214
|
+
MANAGEMENT_PROJECTION_BINDING_REQUIRED_CODE,
|
|
215
|
+
`No management binding for filesystem ${descriptor.filesystem}`,
|
|
216
|
+
{ filesystem: descriptor.filesystem, path: descriptor.path, operation }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
return { filesystem: descriptor.filesystem, path: normalizeProjectionPath(descriptor.path), operation };
|
|
221
|
+
} catch (err) {
|
|
222
|
+
throw new ManagementProjectionOperationError(
|
|
223
|
+
MANAGEMENT_PROJECTION_INVALID_PATH_CODE,
|
|
224
|
+
err.message,
|
|
225
|
+
{ filesystem: descriptor.filesystem, path: "invalid_path", operation }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function managementPath(handle, path) {
|
|
230
|
+
const normalized = normalizeProjectionPath(path);
|
|
231
|
+
return join2(handle.projectionRoot, ...normalized.slice(1).split("/"));
|
|
232
|
+
}
|
|
233
|
+
function assertRelativeInsideRoot(root, candidate) {
|
|
234
|
+
const rel = relative2(root, candidate);
|
|
235
|
+
if (rel.startsWith("..") || isAbsolute2(rel)) throw new Error("path escapes management projection root");
|
|
236
|
+
}
|
|
237
|
+
async function assertExistingPathInsideManagementRoot(handle, candidate) {
|
|
238
|
+
const root = await realpath2(handle.projectionRoot);
|
|
239
|
+
assertRelativeInsideRoot(root, await realpath2(candidate));
|
|
240
|
+
}
|
|
241
|
+
async function assertWritablePathInsideManagementRoot(handle, candidate) {
|
|
242
|
+
try {
|
|
243
|
+
await lstat2(candidate);
|
|
244
|
+
await assertExistingPathInsideManagementRoot(handle, candidate);
|
|
245
|
+
return;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error.code !== "ENOENT") throw error;
|
|
248
|
+
}
|
|
249
|
+
const root = await realpath2(handle.projectionRoot);
|
|
250
|
+
assertRelativeInsideRoot(root, await realpath2(dirname2(candidate)));
|
|
251
|
+
}
|
|
252
|
+
async function walkFiles2(root, current = root) {
|
|
253
|
+
const entries = await readdir2(current, { withFileTypes: true });
|
|
254
|
+
const out = [];
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
const absolutePath = join2(current, entry.name);
|
|
257
|
+
if (entry.isDirectory()) out.push(...await walkFiles2(root, absolutePath));
|
|
258
|
+
else if (entry.isFile()) out.push(absolutePath);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function createManagementProjectionOperations(handle) {
|
|
263
|
+
assertManagementHandle(handle);
|
|
264
|
+
return {
|
|
265
|
+
async read(descriptor) {
|
|
266
|
+
assertManagementHandle(handle);
|
|
267
|
+
const metadata = assertProjectionDescriptor(descriptor, "read", handle.filesystem);
|
|
268
|
+
const target = managementPath(handle, metadata.path);
|
|
269
|
+
await assertExistingPathInsideManagementRoot(handle, target);
|
|
270
|
+
return { content: await readFile2(target, "utf8"), metadata };
|
|
271
|
+
},
|
|
272
|
+
async list(descriptor) {
|
|
273
|
+
assertManagementHandle(handle);
|
|
274
|
+
const metadata = assertProjectionDescriptor(descriptor, "list", handle.filesystem);
|
|
275
|
+
const target = managementPath(handle, metadata.path);
|
|
276
|
+
await assertExistingPathInsideManagementRoot(handle, target);
|
|
277
|
+
const rootStat = await stat(target);
|
|
278
|
+
const files = rootStat.isDirectory() ? await walkFiles2(handle.projectionRoot, target) : [target];
|
|
279
|
+
return { entries: files.map((file) => `/${relative2(handle.projectionRoot, file).split(sep2).join("/")}`).sort(), metadata };
|
|
280
|
+
},
|
|
281
|
+
async write(descriptor, content) {
|
|
282
|
+
assertManagementHandle(handle);
|
|
283
|
+
const metadata = assertProjectionDescriptor(descriptor, "write", handle.filesystem);
|
|
284
|
+
const target = managementPath(handle, metadata.path);
|
|
285
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
286
|
+
await assertWritablePathInsideManagementRoot(handle, target);
|
|
287
|
+
await writeFile(target, content, "utf8");
|
|
288
|
+
return { metadata };
|
|
289
|
+
},
|
|
290
|
+
async edit(descriptor, oldText, newText) {
|
|
291
|
+
assertManagementHandle(handle);
|
|
292
|
+
const metadata = assertProjectionDescriptor(descriptor, "edit", handle.filesystem);
|
|
293
|
+
const target = managementPath(handle, metadata.path);
|
|
294
|
+
await assertExistingPathInsideManagementRoot(handle, target);
|
|
295
|
+
const content = await readFile2(target, "utf8");
|
|
296
|
+
if (!content.includes(oldText)) throw new Error("oldText not found in management projection file");
|
|
297
|
+
await writeFile(target, content.replace(oldText, newText), "utf8");
|
|
298
|
+
return { metadata };
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/server/readonlyProjectionOperations.ts
|
|
304
|
+
import { lstat as lstat3, readdir as readdir3, readFile as readFile3, realpath as realpath3 } from "fs/promises";
|
|
305
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
|
|
306
|
+
var READONLY_PROJECTION_MUTATION_CODE = "READONLY_PROJECTION_READONLY";
|
|
307
|
+
var READONLY_PROJECTION_INVALID_PATH_CODE = "READONLY_PROJECTION_INVALID_PATH";
|
|
308
|
+
var READONLY_PROJECTION_BINDING_NOT_FOUND_CODE = "READONLY_PROJECTION_BINDING_NOT_FOUND";
|
|
309
|
+
var ReadonlyProjectionOperationError = class extends Error {
|
|
310
|
+
code;
|
|
311
|
+
metadata;
|
|
312
|
+
constructor(code, message, metadata) {
|
|
313
|
+
super(message);
|
|
314
|
+
this.name = "ReadonlyProjectionOperationError";
|
|
315
|
+
this.code = code;
|
|
316
|
+
this.metadata = metadata;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function normalizeProjectionPath2(path) {
|
|
320
|
+
const normalized = path.replace(/\\/g, "/");
|
|
321
|
+
if (normalized.includes("\0")) throw new Error("null byte in projection path");
|
|
322
|
+
if (normalized.includes(":/")) throw new Error("filesystem prefixes are not valid path strings");
|
|
323
|
+
if (normalized === "") return "/";
|
|
324
|
+
const withRoot = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
325
|
+
const parts = withRoot.split("/").filter(Boolean);
|
|
326
|
+
if (parts.some((part) => part === ".." || part === ".")) {
|
|
327
|
+
throw new Error("path traversal is not allowed");
|
|
328
|
+
}
|
|
329
|
+
return `/${parts.join("/")}`;
|
|
330
|
+
}
|
|
331
|
+
function unreadableProjectionPathError(metadata) {
|
|
332
|
+
return new ReadonlyProjectionOperationError(
|
|
333
|
+
READONLY_PROJECTION_INVALID_PATH_CODE,
|
|
334
|
+
"projection path is not readable",
|
|
335
|
+
{ ...metadata, path: "not_found_or_denied" }
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
function assertProjectionDescriptor2(descriptor, operation, expectedFilesystem) {
|
|
339
|
+
if (descriptor.filesystem !== expectedFilesystem) {
|
|
340
|
+
throw new ReadonlyProjectionOperationError(
|
|
341
|
+
READONLY_PROJECTION_BINDING_NOT_FOUND_CODE,
|
|
342
|
+
`No readonly binding for filesystem ${descriptor.filesystem}`,
|
|
343
|
+
{ filesystem: descriptor.filesystem, path: descriptor.path, operation }
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return { filesystem: descriptor.filesystem, path: normalizeProjectionPath2(descriptor.path), operation };
|
|
348
|
+
} catch (err) {
|
|
349
|
+
throw new ReadonlyProjectionOperationError(
|
|
350
|
+
READONLY_PROJECTION_INVALID_PATH_CODE,
|
|
351
|
+
err.message,
|
|
352
|
+
{ filesystem: descriptor.filesystem, path: "invalid_path", operation }
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function projectionPath(handle, path) {
|
|
357
|
+
const normalized = normalizeProjectionPath2(path);
|
|
358
|
+
return join3(handle.projectionRoot, ...normalized.slice(1).split("/"));
|
|
359
|
+
}
|
|
360
|
+
function isNotFoundError(error) {
|
|
361
|
+
return error?.code === "ENOENT";
|
|
362
|
+
}
|
|
363
|
+
async function assertNoSymlinkInProjectionPath(root, candidate) {
|
|
364
|
+
const rootStat = await lstat3(root);
|
|
365
|
+
if (rootStat.isSymbolicLink()) throw new Error("readonly projection root must not be a symlink");
|
|
366
|
+
const rel = relative3(root, candidate);
|
|
367
|
+
if (!rel) return;
|
|
368
|
+
let current = root;
|
|
369
|
+
for (const part of rel.split(sep3).filter(Boolean)) {
|
|
370
|
+
current = join3(current, part);
|
|
371
|
+
const currentStat = await lstat3(current).catch((error) => {
|
|
372
|
+
if (isNotFoundError(error)) return null;
|
|
373
|
+
throw error;
|
|
374
|
+
});
|
|
375
|
+
if (!currentStat) return;
|
|
376
|
+
if (currentStat.isSymbolicLink()) throw new Error("symlinks are not readable in readonly projections");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async function assertInsideProjection(handle, candidate) {
|
|
380
|
+
const root = resolve3(handle.projectionRoot);
|
|
381
|
+
const resolvedCandidate = resolve3(candidate);
|
|
382
|
+
const rel = relative3(root, resolvedCandidate);
|
|
383
|
+
if (rel.startsWith("..") || isAbsolute3(rel)) throw new Error("path escapes readonly projection");
|
|
384
|
+
await assertNoSymlinkInProjectionPath(root, resolvedCandidate);
|
|
385
|
+
const realRoot = await realpath3(root);
|
|
386
|
+
const targetExists = await lstat3(resolvedCandidate).then(() => true).catch((error) => {
|
|
387
|
+
if (isNotFoundError(error)) return false;
|
|
388
|
+
throw error;
|
|
389
|
+
});
|
|
390
|
+
const anchor = targetExists ? resolvedCandidate : resolve3(dirname3(resolvedCandidate));
|
|
391
|
+
const realAnchor = await realpath3(anchor).catch((error) => {
|
|
392
|
+
if (isNotFoundError(error)) return null;
|
|
393
|
+
throw error;
|
|
394
|
+
});
|
|
395
|
+
if (!realAnchor) return;
|
|
396
|
+
const realRel = relative3(realRoot, realAnchor);
|
|
397
|
+
if (realRel.startsWith("..") || isAbsolute3(realRel)) throw new Error("path escapes readonly projection");
|
|
398
|
+
}
|
|
399
|
+
async function walkFiles3(root, current = root) {
|
|
400
|
+
const entries = await readdir3(current, { withFileTypes: true });
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
const absolutePath = join3(current, entry.name);
|
|
404
|
+
const entryStat = await lstat3(absolutePath);
|
|
405
|
+
if (entryStat.isSymbolicLink()) continue;
|
|
406
|
+
if (entryStat.isDirectory()) out.push(...await walkFiles3(root, absolutePath));
|
|
407
|
+
else if (entryStat.isFile()) out.push(absolutePath);
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
function pageVisibleResults(items, options) {
|
|
412
|
+
const offset = Math.max(0, Math.trunc(options?.offset ?? 0));
|
|
413
|
+
const limit = options?.limit == null ? void 0 : Math.max(0, Math.trunc(options.limit));
|
|
414
|
+
return limit == null ? items.slice(offset) : items.slice(offset, offset + limit);
|
|
415
|
+
}
|
|
416
|
+
function globToRegex(pattern) {
|
|
417
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
418
|
+
return new RegExp(`^${escaped}$`);
|
|
419
|
+
}
|
|
420
|
+
function createReadonlyProjectionOperations(handle) {
|
|
421
|
+
const expectedFilesystem = handle.filesystem;
|
|
422
|
+
async function filesUnder(path) {
|
|
423
|
+
const root = projectionPath(handle, path);
|
|
424
|
+
await assertInsideProjection(handle, root);
|
|
425
|
+
const rootStat = await lstat3(root);
|
|
426
|
+
if (rootStat.isSymbolicLink()) throw new Error("symlinks are not readable in readonly projections");
|
|
427
|
+
const files = rootStat.isDirectory() ? await walkFiles3(handle.projectionRoot, root) : [root];
|
|
428
|
+
return files.map((file) => `/${relative3(handle.projectionRoot, file).split(sep3).join("/")}`).sort();
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
async read(descriptor) {
|
|
432
|
+
const metadata = assertProjectionDescriptor2(descriptor, "read", expectedFilesystem);
|
|
433
|
+
const target = projectionPath(handle, metadata.path);
|
|
434
|
+
try {
|
|
435
|
+
await assertInsideProjection(handle, target);
|
|
436
|
+
return { content: await readFile3(target, "utf8"), metadata };
|
|
437
|
+
} catch {
|
|
438
|
+
throw unreadableProjectionPathError(metadata);
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
async list(descriptor) {
|
|
442
|
+
const metadata = assertProjectionDescriptor2(descriptor, "list", expectedFilesystem);
|
|
443
|
+
const target = projectionPath(handle, metadata.path);
|
|
444
|
+
try {
|
|
445
|
+
await assertInsideProjection(handle, target);
|
|
446
|
+
const entries = await readdir3(target, { withFileTypes: true });
|
|
447
|
+
if (entries.some((entry) => entry.isSymbolicLink())) throw new Error("symlinks are not readable in readonly projections");
|
|
448
|
+
return { entries: entries.map((entry) => entry.name).sort(), metadata };
|
|
449
|
+
} catch {
|
|
450
|
+
throw unreadableProjectionPathError(metadata);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
async find(descriptor, pattern, options) {
|
|
454
|
+
const metadata = assertProjectionDescriptor2(descriptor, "find", expectedFilesystem);
|
|
455
|
+
try {
|
|
456
|
+
const matcher = globToRegex(pattern);
|
|
457
|
+
const paths = (await filesUnder(metadata.path)).filter((path) => matcher.test(path.slice(1)) || matcher.test(path.split("/").at(-1) ?? path));
|
|
458
|
+
return { paths: pageVisibleResults(paths, options), metadata };
|
|
459
|
+
} catch {
|
|
460
|
+
throw unreadableProjectionPathError(metadata);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
async grep(descriptor, pattern, options) {
|
|
464
|
+
const metadata = assertProjectionDescriptor2(descriptor, "grep", expectedFilesystem);
|
|
465
|
+
try {
|
|
466
|
+
const matches = [];
|
|
467
|
+
for (const path of await filesUnder(metadata.path)) {
|
|
468
|
+
const content = await readFile3(projectionPath(handle, path), "utf8");
|
|
469
|
+
content.split("\n").forEach((text, index) => {
|
|
470
|
+
if (text.includes(pattern)) matches.push({ path, line: index + 1, text });
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return { matches: pageVisibleResults(matches, options), metadata };
|
|
474
|
+
} catch {
|
|
475
|
+
throw unreadableProjectionPathError(metadata);
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
async stat(descriptor) {
|
|
479
|
+
const metadata = assertProjectionDescriptor2(descriptor, "stat", expectedFilesystem);
|
|
480
|
+
const target = projectionPath(handle, metadata.path);
|
|
481
|
+
try {
|
|
482
|
+
await assertInsideProjection(handle, target);
|
|
483
|
+
return { isDirectory: (await lstat3(target)).isDirectory(), metadata };
|
|
484
|
+
} catch {
|
|
485
|
+
throw unreadableProjectionPathError(metadata);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
rejectMutation(operation, descriptor) {
|
|
489
|
+
const metadata = assertProjectionDescriptor2(descriptor, operation, expectedFilesystem);
|
|
490
|
+
throw new ReadonlyProjectionOperationError(
|
|
491
|
+
READONLY_PROJECTION_MUTATION_CODE,
|
|
492
|
+
`filesystem is readonly for ${operation}`,
|
|
493
|
+
{ ...metadata, path: "readonly" }
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/server/runtimeBindingManager.ts
|
|
500
|
+
function filesystemRuntimeScopeKey(ctx) {
|
|
501
|
+
return [
|
|
502
|
+
ctx.humanUserId,
|
|
503
|
+
ctx.agentId,
|
|
504
|
+
ctx.sessionId,
|
|
505
|
+
ctx.workspaceId,
|
|
506
|
+
ctx.requestId
|
|
507
|
+
].join("\0");
|
|
508
|
+
}
|
|
509
|
+
function sameBinding(binding, selector) {
|
|
510
|
+
return binding.filesystem === selector.filesystem && (selector.access === void 0 || binding.access === selector.access) && (selector.projection === void 0 || binding.projection === selector.projection);
|
|
511
|
+
}
|
|
512
|
+
var ScopedFilesystemRuntimeBindingManager = class {
|
|
513
|
+
#resolver;
|
|
514
|
+
#providers;
|
|
515
|
+
#onLifecycleEvent;
|
|
516
|
+
#plans = /* @__PURE__ */ new Map();
|
|
517
|
+
#nextPreparedLabel = 1;
|
|
518
|
+
constructor(options) {
|
|
519
|
+
this.#resolver = options.resolver;
|
|
520
|
+
this.#providers = options.providers;
|
|
521
|
+
this.#onLifecycleEvent = options.onLifecycleEvent;
|
|
522
|
+
}
|
|
523
|
+
async prepareRuntime(ctx) {
|
|
524
|
+
const scopeKey = filesystemRuntimeScopeKey(ctx);
|
|
525
|
+
await this.disposeRuntime(ctx);
|
|
526
|
+
const bindings = await this.#resolver.resolveBindings(ctx);
|
|
527
|
+
const prepared = [];
|
|
528
|
+
try {
|
|
529
|
+
for (const binding of bindings) {
|
|
530
|
+
const provider = this.#providers[String(binding.filesystem)];
|
|
531
|
+
if (!provider) throw new Error(`no filesystem binding provider for ${binding.filesystem}`);
|
|
532
|
+
const preparedBinding = await provider.prepareBinding(ctx, binding);
|
|
533
|
+
prepared.push({
|
|
534
|
+
...preparedBinding,
|
|
535
|
+
scopeKey,
|
|
536
|
+
context: { ...ctx },
|
|
537
|
+
preparedLabel: `prepared-${this.#nextPreparedLabel++}`
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
} catch (error) {
|
|
541
|
+
this.#emit("prepare-error", ctx, bindings, prepared);
|
|
542
|
+
await Promise.allSettled(prepared.map((binding) => this.#disposePrepared(binding)));
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
const plan = { scopeKey, context: { ...ctx }, bindings: prepared };
|
|
546
|
+
this.#plans.set(scopeKey, plan);
|
|
547
|
+
this.#emit("prepare", ctx, bindings, prepared);
|
|
548
|
+
return plan;
|
|
549
|
+
}
|
|
550
|
+
getPreparedBinding(ctx, selector) {
|
|
551
|
+
const plan = this.#plans.get(filesystemRuntimeScopeKey(ctx));
|
|
552
|
+
return plan?.bindings.find((binding) => sameBinding(binding.binding, selector));
|
|
553
|
+
}
|
|
554
|
+
async disposeRuntime(ctx) {
|
|
555
|
+
const scopeKey = filesystemRuntimeScopeKey(ctx);
|
|
556
|
+
const plan = this.#plans.get(scopeKey);
|
|
557
|
+
if (!plan) return;
|
|
558
|
+
this.#plans.delete(scopeKey);
|
|
559
|
+
await Promise.all(plan.bindings.map((binding) => this.#disposePrepared(binding)));
|
|
560
|
+
this.#emit("dispose", ctx, plan.bindings.map((binding) => binding.binding), plan.bindings);
|
|
561
|
+
}
|
|
562
|
+
async invalidate(ctx, filesystem) {
|
|
563
|
+
const scopeKey = filesystemRuntimeScopeKey(ctx);
|
|
564
|
+
const plan = this.#plans.get(scopeKey);
|
|
565
|
+
if (!plan) return;
|
|
566
|
+
const remaining = [];
|
|
567
|
+
for (const binding of plan.bindings) {
|
|
568
|
+
if (binding.binding.filesystem === filesystem) {
|
|
569
|
+
await this.#disposePrepared(binding);
|
|
570
|
+
} else {
|
|
571
|
+
remaining.push(binding);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const provider = this.#providers[String(filesystem)];
|
|
575
|
+
await provider?.invalidateBinding?.(ctx, filesystem);
|
|
576
|
+
const invalidated = plan.bindings.filter((binding) => binding.binding.filesystem === filesystem);
|
|
577
|
+
if (remaining.length === 0) {
|
|
578
|
+
this.#plans.delete(scopeKey);
|
|
579
|
+
} else {
|
|
580
|
+
this.#plans.set(scopeKey, { ...plan, bindings: remaining });
|
|
581
|
+
}
|
|
582
|
+
this.#emit("invalidate", ctx, invalidated.map((binding) => binding.binding), invalidated);
|
|
583
|
+
}
|
|
584
|
+
#emit(type, ctx, bindings, prepared) {
|
|
585
|
+
this.#onLifecycleEvent?.({
|
|
586
|
+
type,
|
|
587
|
+
context: { ...ctx },
|
|
588
|
+
bindings: bindings.map((binding) => `${binding.filesystem}:${binding.access}:${binding.projection}`),
|
|
589
|
+
preparedLabels: prepared.map((binding) => binding.preparedLabel)
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
async #disposePrepared(binding) {
|
|
593
|
+
const provider = this.#providers[String(binding.binding.filesystem)];
|
|
594
|
+
await provider?.disposeBinding?.(binding);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/server/testing/readonlyProjectionConformance.ts
|
|
599
|
+
async function expectRejects(label, failures, fn) {
|
|
600
|
+
try {
|
|
601
|
+
await fn();
|
|
602
|
+
failures.push(label);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function assertSerializedDoesNotLeak(label, value, deniedDirectoryName, deniedSentinel, failures) {
|
|
607
|
+
const serialized = JSON.stringify(value);
|
|
608
|
+
if (serialized.includes(deniedDirectoryName)) {
|
|
609
|
+
failures.push(`${label} leaked denied directory name`);
|
|
610
|
+
}
|
|
611
|
+
if (serialized.includes(deniedSentinel)) {
|
|
612
|
+
failures.push(`${label} leaked denied sentinel`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function checkReadonlyProjectionConformance(subject) {
|
|
616
|
+
const failures = [];
|
|
617
|
+
const {
|
|
618
|
+
filesystem,
|
|
619
|
+
rootPath,
|
|
620
|
+
operations,
|
|
621
|
+
allowedReadPath,
|
|
622
|
+
deniedReadPath,
|
|
623
|
+
deniedDirectoryName,
|
|
624
|
+
deniedSentinel,
|
|
625
|
+
allowedFindPattern,
|
|
626
|
+
expectedAllowedFindCount,
|
|
627
|
+
expectedVisiblePaths,
|
|
628
|
+
projection
|
|
629
|
+
} = subject;
|
|
630
|
+
const visiblePaths = [...await projection.listVisiblePaths()].sort();
|
|
631
|
+
const expectedPaths = [...expectedVisiblePaths].sort();
|
|
632
|
+
if (JSON.stringify(visiblePaths) !== JSON.stringify(expectedPaths)) {
|
|
633
|
+
failures.push("visible path set/count does not match the expected policy-filtered projection");
|
|
634
|
+
}
|
|
635
|
+
if (visiblePaths.some((path) => path === deniedReadPath || path.startsWith(`${deniedReadPath}/`))) {
|
|
636
|
+
failures.push("denied path is present in visiblePaths");
|
|
637
|
+
}
|
|
638
|
+
assertSerializedDoesNotLeak("visiblePaths", visiblePaths, deniedDirectoryName, deniedSentinel, failures);
|
|
639
|
+
const allowed = await operations.read({ filesystem, path: allowedReadPath });
|
|
640
|
+
assertSerializedDoesNotLeak("allowed read", allowed, deniedDirectoryName, deniedSentinel, failures);
|
|
641
|
+
await expectRejects("denied read unexpectedly succeeded", failures, async () => {
|
|
642
|
+
await operations.read({ filesystem, path: deniedReadPath });
|
|
643
|
+
});
|
|
644
|
+
const list = await operations.list({ filesystem, path: rootPath });
|
|
645
|
+
assertSerializedDoesNotLeak("list", list, deniedDirectoryName, deniedSentinel, failures);
|
|
646
|
+
const deniedFileName = deniedReadPath.split("/").at(-1) ?? deniedDirectoryName;
|
|
647
|
+
const findDeniedName = await operations.find({ filesystem, path: rootPath }, deniedFileName);
|
|
648
|
+
const deniedFindItems = findDeniedName.matches ?? findDeniedName.paths ?? [];
|
|
649
|
+
if (deniedFindItems.length > 0) {
|
|
650
|
+
failures.push("find returned denied resource matches");
|
|
651
|
+
}
|
|
652
|
+
assertSerializedDoesNotLeak("find denied resource", findDeniedName, deniedDirectoryName, deniedSentinel, failures);
|
|
653
|
+
const findAllowed = await operations.find({ filesystem, path: rootPath }, allowedFindPattern);
|
|
654
|
+
const allowedFindItems = findAllowed.matches ?? findAllowed.paths ?? [];
|
|
655
|
+
if (allowedFindItems.length !== expectedAllowedFindCount) {
|
|
656
|
+
failures.push("find visible count does not match expected policy-filtered results");
|
|
657
|
+
}
|
|
658
|
+
assertSerializedDoesNotLeak("find allowed", findAllowed, deniedDirectoryName, deniedSentinel, failures);
|
|
659
|
+
const grep = await operations.grep({ filesystem, path: rootPath }, deniedSentinel);
|
|
660
|
+
if (grep.matches.length > 0) {
|
|
661
|
+
failures.push("grep returned denied sentinel matches");
|
|
662
|
+
}
|
|
663
|
+
assertSerializedDoesNotLeak("grep", grep, deniedDirectoryName, deniedSentinel, failures);
|
|
664
|
+
await expectRejects(
|
|
665
|
+
"write to existing projection file unexpectedly succeeded",
|
|
666
|
+
failures,
|
|
667
|
+
() => projection.writeExistingAllowedPath()
|
|
668
|
+
);
|
|
669
|
+
await expectRejects(
|
|
670
|
+
"write to new projection file unexpectedly succeeded",
|
|
671
|
+
failures,
|
|
672
|
+
() => projection.writeNewAllowedPath()
|
|
673
|
+
);
|
|
674
|
+
await expectRejects("symlink escape unexpectedly succeeded", failures, () => projection.followSymlinkEscape());
|
|
675
|
+
return { passed: failures.length === 0, failures };
|
|
676
|
+
}
|
|
677
|
+
export {
|
|
678
|
+
COMPANY_CONTEXT_FILESYSTEM_ID,
|
|
679
|
+
COMPANY_CONTEXT_SENTINEL,
|
|
680
|
+
DEFAULT_COMPANY_CONTEXT_FIXTURE_FILES,
|
|
681
|
+
FixtureCompanyContextBindingProvider,
|
|
682
|
+
MANAGEMENT_PROJECTION_BINDING_REQUIRED_CODE,
|
|
683
|
+
MANAGEMENT_PROJECTION_INVALID_PATH_CODE,
|
|
684
|
+
ManagementProjectionOperationError,
|
|
685
|
+
READONLY_PROJECTION_BINDING_NOT_FOUND_CODE,
|
|
686
|
+
READONLY_PROJECTION_INVALID_PATH_CODE,
|
|
687
|
+
READONLY_PROJECTION_MUTATION_CODE,
|
|
688
|
+
ReadonlyProjectionOperationError,
|
|
689
|
+
ScopedFilesystemRuntimeBindingManager,
|
|
690
|
+
checkReadonlyProjectionConformance,
|
|
691
|
+
createManagementProjectionOperations,
|
|
692
|
+
createReadonlyProjectionOperations,
|
|
693
|
+
filesystemRuntimeScopeKey,
|
|
694
|
+
listFixtureProjectionFiles,
|
|
695
|
+
readFixtureProjectionFile,
|
|
696
|
+
seedCompanyContextFixture
|
|
697
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Logical filesystem identity used by tools, UI, and runtime bindings. */
|
|
2
|
+
type FilesystemId = "user" | "company_context" | (string & {});
|
|
3
|
+
/** Access granted for a prepared runtime filesystem binding. */
|
|
4
|
+
type FilesystemAccess = "readonly" | "readwrite";
|
|
5
|
+
/** Provider-declared projection mode for a bound filesystem. */
|
|
6
|
+
type FilesystemProjection =
|
|
7
|
+
/** Contains only resources allowed for this actor/session. */
|
|
8
|
+
"policy-filtered"
|
|
9
|
+
/** Broader management view; still policy-granted by the host app/provider. */
|
|
10
|
+
| "management";
|
|
11
|
+
/** Policy-resolved filesystem binding requested for one runtime/session. */
|
|
12
|
+
interface FilesystemBinding {
|
|
13
|
+
filesystem: FilesystemId;
|
|
14
|
+
access: FilesystemAccess;
|
|
15
|
+
mountPath: string;
|
|
16
|
+
projection: FilesystemProjection;
|
|
17
|
+
}
|
|
18
|
+
/** Actor/session identity supplied when resolving or invalidating bindings. */
|
|
19
|
+
interface BoundFilesystemContext {
|
|
20
|
+
humanUserId: string;
|
|
21
|
+
agentId: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
workspaceId: string;
|
|
24
|
+
requestId: string;
|
|
25
|
+
}
|
|
26
|
+
/** Resolves policy-granted filesystem bindings for one actor/session. */
|
|
27
|
+
interface FilesystemBindingResolver {
|
|
28
|
+
resolveBindings(ctx: BoundFilesystemContext): Promise<FilesystemBinding[]>;
|
|
29
|
+
}
|
|
30
|
+
/** Provider/runtime-prepared binding with an opaque mount/projection handle. */
|
|
31
|
+
interface PreparedFilesystemBinding {
|
|
32
|
+
binding: FilesystemBinding;
|
|
33
|
+
handle: unknown;
|
|
34
|
+
}
|
|
35
|
+
/** Provider lifecycle seam for preparing, disposing, and invalidating bindings. */
|
|
36
|
+
interface FilesystemBindingProvider {
|
|
37
|
+
prepareBinding(ctx: BoundFilesystemContext, binding: FilesystemBinding): Promise<PreparedFilesystemBinding>;
|
|
38
|
+
disposeBinding?(prepared: PreparedFilesystemBinding): Promise<void>;
|
|
39
|
+
invalidateBinding?(ctx: BoundFilesystemContext, filesystem: FilesystemId): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Bindings prepared for this one runtime/session. */
|
|
42
|
+
interface RuntimeBindingPlan {
|
|
43
|
+
bindings: PreparedFilesystemBinding[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type { BoundFilesystemContext, FilesystemAccess, FilesystemBinding, FilesystemBindingProvider, FilesystemBindingResolver, FilesystemId, FilesystemProjection, PreparedFilesystemBinding, RuntimeBindingPlan };
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hachej/boring-bash",
|
|
3
|
+
"version": "0.1.64",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Filesystem and shell binding contracts for Boring runtimes.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/hachej/boring-ui"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/hachej/boring-ui",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./shared": {
|
|
21
|
+
"types": "./dist/shared/index.d.ts",
|
|
22
|
+
"import": "./dist/shared/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./server": {
|
|
25
|
+
"types": "./dist/server/index.d.ts",
|
|
26
|
+
"import": "./dist/server/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsup": "^8.4.0",
|
|
31
|
+
"typescript": "^5.8.3",
|
|
32
|
+
"vitest": "^4.1.9"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"check:invariants": "node ./scripts/check-invariants.mjs",
|
|
38
|
+
"lint": "pnpm run typecheck && pnpm run check:invariants",
|
|
39
|
+
"test": "vitest run --passWithNoTests",
|
|
40
|
+
"clean": "rm -rf dist .tsbuildinfo"
|
|
41
|
+
}
|
|
42
|
+
}
|