@apicircle/core 1.0.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/LICENSE +110 -0
- package/README.md +35 -0
- package/dist/index.cjs +6815 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1801 -0
- package/dist/index.d.ts +1801 -0
- package/dist/index.js +6678 -0
- package/dist/index.js.map +1 -0
- package/dist/patches-N7mvDpXn.d.cts +85 -0
- package/dist/patches-N7mvDpXn.d.ts +85 -0
- package/dist/test/mock-idp.cjs +232 -0
- package/dist/test/mock-idp.cjs.map +1 -0
- package/dist/test/mock-idp.d.cts +32 -0
- package/dist/test/mock-idp.d.ts +32 -0
- package/dist/test/mock-idp.js +207 -0
- package/dist/test/mock-idp.js.map +1 -0
- package/dist/workspace/file-backed.cjs +165 -0
- package/dist/workspace/file-backed.cjs.map +1 -0
- package/dist/workspace/file-backed.d.cts +37 -0
- package/dist/workspace/file-backed.d.ts +37 -0
- package/dist/workspace/file-backed.js +128 -0
- package/dist/workspace/file-backed.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/auth/oauth2/__fixtures__/mockIdp.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
async function startMockIdp() {
|
|
4
|
+
let nextAuthorizeError = null;
|
|
5
|
+
const deviceCodes = /* @__PURE__ */ new Map();
|
|
6
|
+
const server = createServer((req, res) => {
|
|
7
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
8
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
9
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
10
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept");
|
|
11
|
+
if (req.method === "OPTIONS") {
|
|
12
|
+
res.statusCode = 204;
|
|
13
|
+
res.end();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (url.pathname === "/authorize") {
|
|
17
|
+
const redirectUri = url.searchParams.get("redirect_uri") ?? "";
|
|
18
|
+
const state = url.searchParams.get("state") ?? "";
|
|
19
|
+
const responseType = url.searchParams.get("response_type") ?? "code";
|
|
20
|
+
if (!redirectUri) {
|
|
21
|
+
res.statusCode = 400;
|
|
22
|
+
res.end("redirect_uri required");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (nextAuthorizeError) {
|
|
26
|
+
const params = new URLSearchParams({
|
|
27
|
+
error: nextAuthorizeError.error,
|
|
28
|
+
state
|
|
29
|
+
});
|
|
30
|
+
if (nextAuthorizeError.description) {
|
|
31
|
+
params.set("error_description", nextAuthorizeError.description);
|
|
32
|
+
}
|
|
33
|
+
res.statusCode = 302;
|
|
34
|
+
res.setHeader("Location", `${redirectUri}?${params.toString()}`);
|
|
35
|
+
res.end();
|
|
36
|
+
nextAuthorizeError = null;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (responseType === "token") {
|
|
40
|
+
res.statusCode = 302;
|
|
41
|
+
res.setHeader(
|
|
42
|
+
"Location",
|
|
43
|
+
`${redirectUri}#access_token=tk-implicit&token_type=Bearer&expires_in=3600&state=${state}`
|
|
44
|
+
);
|
|
45
|
+
res.end();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
res.statusCode = 302;
|
|
49
|
+
res.setHeader("Location", `${redirectUri}?code=test-code&state=${state}`);
|
|
50
|
+
res.end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (url.pathname === "/token" && req.method === "POST") {
|
|
54
|
+
collectBody(req, (body) => {
|
|
55
|
+
const params = new URLSearchParams(body);
|
|
56
|
+
const grant = params.get("grant_type");
|
|
57
|
+
const clientId = params.get("client_id");
|
|
58
|
+
if (!clientId) {
|
|
59
|
+
jsonError(res, 400, "invalid_client", "client_id missing");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (grant === "client_credentials") {
|
|
63
|
+
jsonOk(res, {
|
|
64
|
+
access_token: `tk-cc-${clientId}`,
|
|
65
|
+
token_type: "Bearer",
|
|
66
|
+
expires_in: 3600,
|
|
67
|
+
scope: params.get("scope") ?? ""
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (grant === "password") {
|
|
72
|
+
if (params.get("password") !== "hunter2") {
|
|
73
|
+
jsonError(res, 400, "invalid_grant", "wrong password");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
jsonOk(res, {
|
|
77
|
+
access_token: `tk-ropc-${params.get("username") ?? ""}`,
|
|
78
|
+
token_type: "Bearer",
|
|
79
|
+
expires_in: 3600,
|
|
80
|
+
refresh_token: "rt-ropc"
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (grant === "authorization_code") {
|
|
85
|
+
if (params.get("code") !== "test-code") {
|
|
86
|
+
jsonError(res, 400, "invalid_grant", "unknown code");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
jsonOk(res, {
|
|
90
|
+
access_token: "tk-authcode",
|
|
91
|
+
token_type: "Bearer",
|
|
92
|
+
expires_in: 3600,
|
|
93
|
+
refresh_token: "rt-authcode",
|
|
94
|
+
scope: params.get("scope") ?? ""
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (grant === "refresh_token") {
|
|
99
|
+
if (params.get("refresh_token") === "rt-rotated-once") {
|
|
100
|
+
jsonError(res, 400, "invalid_grant", "refresh already used");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
jsonOk(res, {
|
|
104
|
+
access_token: "tk-refreshed",
|
|
105
|
+
token_type: "Bearer",
|
|
106
|
+
expires_in: 3600,
|
|
107
|
+
// Rotate: hand back a fresh refresh_token.
|
|
108
|
+
refresh_token: "rt-rotated-once"
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (grant === "urn:ietf:params:oauth:grant-type:device_code") {
|
|
113
|
+
const code = params.get("device_code") ?? "";
|
|
114
|
+
const state = deviceCodes.get(code);
|
|
115
|
+
if (!state) {
|
|
116
|
+
jsonError(res, 400, "invalid_grant", "unknown device_code");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
state.pollCount++;
|
|
120
|
+
if (state.pollCount < state.approvedAfter) {
|
|
121
|
+
jsonError(res, 400, "authorization_pending");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
jsonOk(res, {
|
|
125
|
+
access_token: "tk-device",
|
|
126
|
+
token_type: "Bearer",
|
|
127
|
+
expires_in: 3600
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
jsonError(res, 400, "unsupported_grant_type");
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (url.pathname === "/device_authorize" && req.method === "POST") {
|
|
136
|
+
const code = `dc-${Date.now()}`;
|
|
137
|
+
deviceCodes.set(code, { approvedAfter: 2, pollCount: 0 });
|
|
138
|
+
jsonOk(res, {
|
|
139
|
+
device_code: code,
|
|
140
|
+
user_code: "ABCD-EFGH",
|
|
141
|
+
verification_uri: `http://127.0.0.1:${server.address().port}/device`,
|
|
142
|
+
interval: 1,
|
|
143
|
+
expires_in: 600
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (url.pathname === "/protected") {
|
|
148
|
+
const auth = req.headers["authorization"] ?? "";
|
|
149
|
+
if (!auth.toLowerCase().startsWith("bearer tk-")) {
|
|
150
|
+
res.statusCode = 401;
|
|
151
|
+
res.setHeader("Content-Type", "application/json");
|
|
152
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
jsonOk(res, { ok: true, sawAuth: auth });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (url.pathname === "/www-auth-bearer") {
|
|
159
|
+
res.statusCode = 401;
|
|
160
|
+
res.setHeader(
|
|
161
|
+
"WWW-Authenticate",
|
|
162
|
+
'Bearer error="invalid_token", error_description="The access token expired"'
|
|
163
|
+
);
|
|
164
|
+
res.end();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
res.statusCode = 404;
|
|
168
|
+
res.end("Not Found");
|
|
169
|
+
});
|
|
170
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
171
|
+
const port = server.address().port;
|
|
172
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
173
|
+
return {
|
|
174
|
+
port,
|
|
175
|
+
url: (path) => `${baseUrl}${path}`,
|
|
176
|
+
setNextAuthorizeError: (err) => {
|
|
177
|
+
nextAuthorizeError = err;
|
|
178
|
+
},
|
|
179
|
+
approveDevice: () => {
|
|
180
|
+
for (const state of deviceCodes.values()) state.approvedAfter = state.pollCount + 1;
|
|
181
|
+
},
|
|
182
|
+
close: () => new Promise((resolve, reject) => {
|
|
183
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
184
|
+
})
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function jsonOk(res, body) {
|
|
188
|
+
res.statusCode = 200;
|
|
189
|
+
res.setHeader("Content-Type", "application/json");
|
|
190
|
+
res.end(JSON.stringify(body));
|
|
191
|
+
}
|
|
192
|
+
function jsonError(res, status, error, description) {
|
|
193
|
+
res.statusCode = status;
|
|
194
|
+
res.setHeader("Content-Type", "application/json");
|
|
195
|
+
const body = { error };
|
|
196
|
+
if (description) body.error_description = description;
|
|
197
|
+
res.end(JSON.stringify(body));
|
|
198
|
+
}
|
|
199
|
+
function collectBody(req, cb) {
|
|
200
|
+
const chunks = [];
|
|
201
|
+
req.on("data", (c) => chunks.push(c));
|
|
202
|
+
req.on("end", () => cb(Buffer.concat(chunks).toString("utf8")));
|
|
203
|
+
}
|
|
204
|
+
export {
|
|
205
|
+
startMockIdp
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=mock-idp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/oauth2/__fixtures__/mockIdp.ts"],"sourcesContent":["/**\n * Programmatic OAuth2 IdP for E2E tests. Implements every grant the\n * studio supports with the simplest possible logic — no real user\n * accounts, no real key material. Tokens are deterministic strings\n * keyed by `client_id` so assertions can match exactly.\n *\n * POST /token — token endpoint (every grant_type)\n * GET /authorize — auth-code / implicit redirect\n * POST /device_authorize — device flow user-code endpoint\n * GET /protected — resource server, requires Bearer header\n * POST /protected — same, lets tests assert on POST too\n * GET /www-auth-bearer — emits WWW-Authenticate Bearer error (no JSON)\n * POST /digest-protected — Digest 401-retry endpoint\n *\n * Spin up via `await startMockIdp()` and tear down with the returned\n * `close()`. Port is dynamic to avoid collisions in CI.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { type AddressInfo } from 'node:net';\n\ninterface DeviceState {\n approvedAfter: number; // poll count threshold\n pollCount: number;\n}\n\nexport interface MockIdp {\n port: number;\n url: (path: string) => string;\n /** Force the next /authorize redirect to include this error param. */\n setNextAuthorizeError: (err: { error: string; description?: string } | null) => void;\n /** Approve the current device flow (subsequent /token polls succeed). */\n approveDevice: () => void;\n close: () => Promise<void>;\n}\n\nexport async function startMockIdp(): Promise<MockIdp> {\n let nextAuthorizeError: { error: string; description?: string } | null = null;\n const deviceCodes = new Map<string, DeviceState>();\n\n const server: Server = createServer((req, res) => {\n const url = new URL(req.url ?? '/', `http://127.0.0.1`);\n\n // Permissive CORS so the web app's fetch can reach the IdP across\n // origins (e.g. http://localhost:5174 → http://127.0.0.1:<idp>).\n // Real IdPs aren't this permissive — but only the test harness\n // ever talks to this server.\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');\n if (req.method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n\n if (url.pathname === '/authorize') {\n const redirectUri = url.searchParams.get('redirect_uri') ?? '';\n const state = url.searchParams.get('state') ?? '';\n const responseType = url.searchParams.get('response_type') ?? 'code';\n if (!redirectUri) {\n res.statusCode = 400;\n res.end('redirect_uri required');\n return;\n }\n if (nextAuthorizeError) {\n const params = new URLSearchParams({\n error: nextAuthorizeError.error,\n state,\n });\n if (nextAuthorizeError.description) {\n params.set('error_description', nextAuthorizeError.description);\n }\n res.statusCode = 302;\n res.setHeader('Location', `${redirectUri}?${params.toString()}`);\n res.end();\n nextAuthorizeError = null;\n return;\n }\n if (responseType === 'token') {\n // Implicit: redirect with fragment.\n res.statusCode = 302;\n res.setHeader(\n 'Location',\n `${redirectUri}#access_token=tk-implicit&token_type=Bearer&expires_in=3600&state=${state}`,\n );\n res.end();\n return;\n }\n // Default: auth-code redirect.\n res.statusCode = 302;\n res.setHeader('Location', `${redirectUri}?code=test-code&state=${state}`);\n res.end();\n return;\n }\n\n if (url.pathname === '/token' && req.method === 'POST') {\n collectBody(req, (body) => {\n const params = new URLSearchParams(body);\n const grant = params.get('grant_type');\n const clientId = params.get('client_id');\n if (!clientId) {\n jsonError(res, 400, 'invalid_client', 'client_id missing');\n return;\n }\n if (grant === 'client_credentials') {\n jsonOk(res, {\n access_token: `tk-cc-${clientId}`,\n token_type: 'Bearer',\n expires_in: 3600,\n scope: params.get('scope') ?? '',\n });\n return;\n }\n if (grant === 'password') {\n if (params.get('password') !== 'hunter2') {\n jsonError(res, 400, 'invalid_grant', 'wrong password');\n return;\n }\n jsonOk(res, {\n access_token: `tk-ropc-${params.get('username') ?? ''}`,\n token_type: 'Bearer',\n expires_in: 3600,\n refresh_token: 'rt-ropc',\n });\n return;\n }\n if (grant === 'authorization_code') {\n if (params.get('code') !== 'test-code') {\n jsonError(res, 400, 'invalid_grant', 'unknown code');\n return;\n }\n // PKCE clients carry code_verifier — accept any non-empty value.\n jsonOk(res, {\n access_token: 'tk-authcode',\n token_type: 'Bearer',\n expires_in: 3600,\n refresh_token: 'rt-authcode',\n scope: params.get('scope') ?? '',\n });\n return;\n }\n if (grant === 'refresh_token') {\n if (params.get('refresh_token') === 'rt-rotated-once') {\n jsonError(res, 400, 'invalid_grant', 'refresh already used');\n return;\n }\n jsonOk(res, {\n access_token: 'tk-refreshed',\n token_type: 'Bearer',\n expires_in: 3600,\n // Rotate: hand back a fresh refresh_token.\n refresh_token: 'rt-rotated-once',\n });\n return;\n }\n if (grant === 'urn:ietf:params:oauth:grant-type:device_code') {\n const code = params.get('device_code') ?? '';\n const state = deviceCodes.get(code);\n if (!state) {\n jsonError(res, 400, 'invalid_grant', 'unknown device_code');\n return;\n }\n state.pollCount++;\n if (state.pollCount < state.approvedAfter) {\n jsonError(res, 400, 'authorization_pending');\n return;\n }\n jsonOk(res, {\n access_token: 'tk-device',\n token_type: 'Bearer',\n expires_in: 3600,\n });\n return;\n }\n jsonError(res, 400, 'unsupported_grant_type');\n });\n return;\n }\n\n if (url.pathname === '/device_authorize' && req.method === 'POST') {\n const code = `dc-${Date.now()}`;\n deviceCodes.set(code, { approvedAfter: 2, pollCount: 0 });\n jsonOk(res, {\n device_code: code,\n user_code: 'ABCD-EFGH',\n verification_uri: `http://127.0.0.1:${(server.address() as AddressInfo).port}/device`,\n interval: 1,\n expires_in: 600,\n });\n return;\n }\n\n if (url.pathname === '/protected') {\n const auth = req.headers['authorization'] ?? '';\n if (!auth.toLowerCase().startsWith('bearer tk-')) {\n res.statusCode = 401;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: 'unauthorized' }));\n return;\n }\n jsonOk(res, { ok: true, sawAuth: auth });\n return;\n }\n\n if (url.pathname === '/www-auth-bearer') {\n res.statusCode = 401;\n res.setHeader(\n 'WWW-Authenticate',\n 'Bearer error=\"invalid_token\", error_description=\"The access token expired\"',\n );\n res.end();\n return;\n }\n\n res.statusCode = 404;\n res.end('Not Found');\n });\n\n await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));\n const port = (server.address() as AddressInfo).port;\n const baseUrl = `http://127.0.0.1:${port}`;\n\n return {\n port,\n url: (path) => `${baseUrl}${path}`,\n setNextAuthorizeError: (err) => {\n nextAuthorizeError = err;\n },\n approveDevice: () => {\n for (const state of deviceCodes.values()) state.approvedAfter = state.pollCount + 1;\n },\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n }),\n };\n}\n\nfunction jsonOk(res: ServerResponse, body: unknown): void {\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify(body));\n}\n\nfunction jsonError(res: ServerResponse, status: number, error: string, description?: string): void {\n res.statusCode = status;\n res.setHeader('Content-Type', 'application/json');\n const body: Record<string, string> = { error };\n if (description) body.error_description = description;\n res.end(JSON.stringify(body));\n}\n\nfunction collectBody(req: IncomingMessage, cb: (body: string) => void): void {\n const chunks: Buffer[] = [];\n req.on('data', (c: Buffer) => chunks.push(c));\n req.on('end', () => cb(Buffer.concat(chunks).toString('utf8')));\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/workspace/fileBackedWorkspace.ts
|
|
31
|
+
var fileBackedWorkspace_exports = {};
|
|
32
|
+
__export(fileBackedWorkspace_exports, {
|
|
33
|
+
loadFromFile: () => loadFromFile,
|
|
34
|
+
saveToFile: () => saveToFile,
|
|
35
|
+
withWorkspace: () => withWorkspace
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(fileBackedWorkspace_exports);
|
|
38
|
+
var import_node_fs = require("fs");
|
|
39
|
+
var path = __toESM(require("path"), 1);
|
|
40
|
+
var import_shared = require("@apicircle/shared");
|
|
41
|
+
var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
|
|
42
|
+
var SYNCED_FILE = "workspace.synced.json";
|
|
43
|
+
var LOCAL_FILE = "workspace.local.json";
|
|
44
|
+
async function loadFromFile(dir, options = {}) {
|
|
45
|
+
const syncedPath = path.join(dir, SYNCED_FILE);
|
|
46
|
+
const localPath = path.join(dir, LOCAL_FILE);
|
|
47
|
+
let syncedRaw;
|
|
48
|
+
try {
|
|
49
|
+
syncedRaw = await import_node_fs.promises.readFile(syncedPath, "utf-8");
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (options.allowMissing && isENOENT(err)) return null;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
const synced = JSON.parse(syncedRaw);
|
|
55
|
+
let local;
|
|
56
|
+
try {
|
|
57
|
+
local = JSON.parse(await import_node_fs.promises.readFile(localPath, "utf-8"));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (!isENOENT(err)) throw err;
|
|
60
|
+
local = createEmptyLocalForSynced(synced);
|
|
61
|
+
}
|
|
62
|
+
return { synced, local };
|
|
63
|
+
}
|
|
64
|
+
async function saveToFile(dir, state, options = {}) {
|
|
65
|
+
await import_node_fs.promises.mkdir(dir, { recursive: true });
|
|
66
|
+
const syncedPath = path.join(dir, SYNCED_FILE);
|
|
67
|
+
const localPath = path.join(dir, LOCAL_FILE);
|
|
68
|
+
await ensureFile(syncedPath);
|
|
69
|
+
const release = await import_proper_lockfile.default.lock(syncedPath, {
|
|
70
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },
|
|
71
|
+
stale: options.lockTimeoutMs ?? 3e4
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
await writeJsonAtomic(syncedPath, state.synced);
|
|
75
|
+
await writeJsonAtomic(localPath, state.local);
|
|
76
|
+
} finally {
|
|
77
|
+
await release();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function withWorkspace(dir, fn, options = {}) {
|
|
81
|
+
await import_node_fs.promises.mkdir(dir, { recursive: true });
|
|
82
|
+
const syncedPath = path.join(dir, SYNCED_FILE);
|
|
83
|
+
const localPath = path.join(dir, LOCAL_FILE);
|
|
84
|
+
await ensureFile(syncedPath);
|
|
85
|
+
const release = await import_proper_lockfile.default.lock(syncedPath, {
|
|
86
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },
|
|
87
|
+
stale: options.lockTimeoutMs ?? 3e4
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
const syncedRaw = await import_node_fs.promises.readFile(syncedPath, "utf-8");
|
|
91
|
+
const synced = JSON.parse(syncedRaw);
|
|
92
|
+
let local;
|
|
93
|
+
try {
|
|
94
|
+
local = JSON.parse(await import_node_fs.promises.readFile(localPath, "utf-8"));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (!isENOENT(err)) throw err;
|
|
97
|
+
local = createEmptyLocalForSynced(synced);
|
|
98
|
+
}
|
|
99
|
+
const out = await fn({ synced, local });
|
|
100
|
+
await writeJsonAtomic(syncedPath, out.next.synced);
|
|
101
|
+
await writeJsonAtomic(localPath, out.next.local);
|
|
102
|
+
return out.result;
|
|
103
|
+
} finally {
|
|
104
|
+
await release();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
var WORKSPACE_FILE_MODE = 384;
|
|
108
|
+
async function ensureFile(filePath) {
|
|
109
|
+
try {
|
|
110
|
+
await import_node_fs.promises.access(filePath);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (!isENOENT(err)) throw err;
|
|
113
|
+
await import_node_fs.promises.writeFile(filePath, "{}", { encoding: "utf-8", mode: WORKSPACE_FILE_MODE });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function writeJsonAtomic(filePath, value) {
|
|
117
|
+
const tmp = `${filePath}.tmp`;
|
|
118
|
+
await import_node_fs.promises.writeFile(tmp, JSON.stringify(value, null, 2) + "\n", {
|
|
119
|
+
encoding: "utf-8",
|
|
120
|
+
mode: WORKSPACE_FILE_MODE
|
|
121
|
+
});
|
|
122
|
+
await import_node_fs.promises.rename(tmp, filePath);
|
|
123
|
+
}
|
|
124
|
+
function isENOENT(err) {
|
|
125
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
126
|
+
}
|
|
127
|
+
function createEmptyLocalForSynced(synced) {
|
|
128
|
+
return {
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
workspaceId: synced.workspaceId,
|
|
131
|
+
executionPlans: {},
|
|
132
|
+
history: { requestRuns: [], planRuns: [] },
|
|
133
|
+
secretIndex: { entries: {} },
|
|
134
|
+
sessions: { github: { workspace: null, links: {} } },
|
|
135
|
+
connectedRepo: null,
|
|
136
|
+
workingBranch: null,
|
|
137
|
+
seededWorkspaceSha: null,
|
|
138
|
+
retiredBranch: null,
|
|
139
|
+
sync: {
|
|
140
|
+
lastPulledSnapshot: null,
|
|
141
|
+
lastPulledSha: null,
|
|
142
|
+
lastPulledAt: null,
|
|
143
|
+
dirtyKeys: []
|
|
144
|
+
},
|
|
145
|
+
linkedCollections: {},
|
|
146
|
+
globalContext: {},
|
|
147
|
+
mockRuntime: { active: {} },
|
|
148
|
+
ui: {
|
|
149
|
+
activeRequestId: null,
|
|
150
|
+
sidebarExpandedSections: [],
|
|
151
|
+
themeId: "studio-dark",
|
|
152
|
+
fontId: "system-mono",
|
|
153
|
+
fontSizePercent: import_shared.FONT_SIZE_PERCENT_DEFAULT
|
|
154
|
+
},
|
|
155
|
+
settings: { validateOnSend: true, monacoConsumesWheel: false },
|
|
156
|
+
snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
160
|
+
0 && (module.exports = {
|
|
161
|
+
loadFromFile,
|
|
162
|
+
saveToFile,
|
|
163
|
+
withWorkspace
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=file-backed.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/workspace/fileBackedWorkspace.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport { FONT_SIZE_PERCENT_DEFAULT } from '@apicircle/shared';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// fileBackedWorkspace — load/save a `{ synced, local }` pair as two JSON\n// files on disk, with a `proper-lockfile` advisory lock so concurrent CLI /\n// MCP writers can't corrupt the document.\n//\n// Layout (relative to the directory passed in):\n// workspace.synced.json ← matches WorkspaceSynced exactly, push-to-git target\n// workspace.local.json ← WorkspaceLocal, host-private (CLI/MCP doesn't push)\n//\n// The lock is held on `workspace.synced.json` because that's the file the\n// editor races against. Stale locks are released after 30s.\n// =============================================================================\n\nconst SYNCED_FILE = 'workspace.synced.json';\nconst LOCAL_FILE = 'workspace.local.json';\n\nexport interface LoadFromFileOptions {\n /** When true, return `null` instead of throwing if the synced file is missing. */\n allowMissing?: boolean;\n}\n\nexport interface SaveToFileOptions {\n /** Lock timeout (ms). Defaults to 30000. */\n lockTimeoutMs?: number;\n}\n\n/**\n * Load both workspace documents from `dir`. The synced file is required;\n * the local file is optional and falls back to a minimal empty shape so a\n * CLI on a fresh machine can still operate (it just won't have history /\n * overrides until the desktop app runs once).\n */\nexport async function loadFromFile(\n dir: string,\n options: LoadFromFileOptions = {},\n): Promise<WorkspaceState | null> {\n const syncedPath = path.join(dir, SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n let syncedRaw: string;\n try {\n syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n } catch (err) {\n if (options.allowMissing && isENOENT(err)) return null;\n throw err;\n }\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n\n return { synced, local };\n}\n\n/**\n * Atomically write both documents back to disk. Acquires an advisory lock\n * on the synced file for the duration of the write so a parallel CLI /\n * MCP / desktop save can't interleave.\n *\n * Both files are written via `<file>.tmp` + rename so a crash mid-write\n * never leaves a partial JSON document on disk.\n */\nexport async function saveToFile(\n dir: string,\n state: WorkspaceState,\n options: SaveToFileOptions = {},\n): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n // proper-lockfile requires the target file to exist. Touch it on first save.\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n await writeJsonAtomic(syncedPath, state.synced);\n await writeJsonAtomic(localPath, state.local);\n } finally {\n await release();\n }\n}\n\n/**\n * Run a load → mutate → save cycle under one lock so a single mutation\n * can't be clobbered by a racing reader-then-writer.\n */\nexport async function withWorkspace<T>(\n dir: string,\n fn: (state: WorkspaceState) => Promise<{ next: WorkspaceState; result?: T }>,\n options: SaveToFileOptions = {},\n): Promise<T | undefined> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n const syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n const out = await fn({ synced, local });\n await writeJsonAtomic(syncedPath, out.next.synced);\n await writeJsonAtomic(localPath, out.next.local);\n return out.result;\n } finally {\n await release();\n }\n}\n\n// ---------------------------------------------------------------------------\n// internals\n// ---------------------------------------------------------------------------\n\n// File mode for workspace JSON: owner read/write only. Default `fs.writeFile`\n// uses 0o666 minus umask (typically 0o644 — world-readable). The workspace\n// docs carry the synced state (which after redaction is mostly safe to read\n// but still includes per-workspace metadata) and the local state (which\n// holds the encrypted Secret Vault payload table, session metadata, and the\n// vault entries themselves). On multi-user POSIX hosts (CI runners,\n// classroom VMs, shared dev servers) the default would leak both. 0o600\n// keeps the file owner-only. Windows ignores POSIX modes — the inherited\n// per-user ACL under %USERPROFILE% is what protects it there.\nconst WORKSPACE_FILE_MODE = 0o600;\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: WORKSPACE_FILE_MODE });\n }\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fs.writeFile(tmp, JSON.stringify(value, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: WORKSPACE_FILE_MODE,\n });\n await fs.rename(tmp, filePath);\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\nfunction createEmptyLocalForSynced(synced: WorkspaceSynced): WorkspaceLocal {\n return {\n schemaVersion: 1,\n workspaceId: synced.workspaceId,\n executionPlans: {},\n history: { requestRuns: [], planRuns: [] },\n secretIndex: { entries: {} },\n sessions: { github: { workspace: null, links: {} } },\n connectedRepo: null,\n workingBranch: null,\n seededWorkspaceSha: null,\n retiredBranch: null,\n sync: {\n lastPulledSnapshot: null,\n lastPulledSha: null,\n lastPulledAt: null,\n dirtyKeys: [],\n },\n linkedCollections: {},\n globalContext: {},\n mockRuntime: { active: {} },\n ui: {\n activeRequestId: null,\n sidebarExpandedSections: [],\n themeId: 'studio-dark',\n fontId: 'system-mono',\n fontSizePercent: FONT_SIZE_PERCENT_DEFAULT,\n },\n settings: { validateOnSend: true, monacoConsumesWheel: false },\n snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 },\n };\n}\n"],"mappings":"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","names":["fs","lockfile"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { W as WorkspaceState } from '../patches-N7mvDpXn.cjs';
|
|
2
|
+
import '@apicircle/shared';
|
|
3
|
+
|
|
4
|
+
interface LoadFromFileOptions {
|
|
5
|
+
/** When true, return `null` instead of throwing if the synced file is missing. */
|
|
6
|
+
allowMissing?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface SaveToFileOptions {
|
|
9
|
+
/** Lock timeout (ms). Defaults to 30000. */
|
|
10
|
+
lockTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Load both workspace documents from `dir`. The synced file is required;
|
|
14
|
+
* the local file is optional and falls back to a minimal empty shape so a
|
|
15
|
+
* CLI on a fresh machine can still operate (it just won't have history /
|
|
16
|
+
* overrides until the desktop app runs once).
|
|
17
|
+
*/
|
|
18
|
+
declare function loadFromFile(dir: string, options?: LoadFromFileOptions): Promise<WorkspaceState | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Atomically write both documents back to disk. Acquires an advisory lock
|
|
21
|
+
* on the synced file for the duration of the write so a parallel CLI /
|
|
22
|
+
* MCP / desktop save can't interleave.
|
|
23
|
+
*
|
|
24
|
+
* Both files are written via `<file>.tmp` + rename so a crash mid-write
|
|
25
|
+
* never leaves a partial JSON document on disk.
|
|
26
|
+
*/
|
|
27
|
+
declare function saveToFile(dir: string, state: WorkspaceState, options?: SaveToFileOptions): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Run a load → mutate → save cycle under one lock so a single mutation
|
|
30
|
+
* can't be clobbered by a racing reader-then-writer.
|
|
31
|
+
*/
|
|
32
|
+
declare function withWorkspace<T>(dir: string, fn: (state: WorkspaceState) => Promise<{
|
|
33
|
+
next: WorkspaceState;
|
|
34
|
+
result?: T;
|
|
35
|
+
}>, options?: SaveToFileOptions): Promise<T | undefined>;
|
|
36
|
+
|
|
37
|
+
export { type LoadFromFileOptions, type SaveToFileOptions, loadFromFile, saveToFile, withWorkspace };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { W as WorkspaceState } from '../patches-N7mvDpXn.js';
|
|
2
|
+
import '@apicircle/shared';
|
|
3
|
+
|
|
4
|
+
interface LoadFromFileOptions {
|
|
5
|
+
/** When true, return `null` instead of throwing if the synced file is missing. */
|
|
6
|
+
allowMissing?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface SaveToFileOptions {
|
|
9
|
+
/** Lock timeout (ms). Defaults to 30000. */
|
|
10
|
+
lockTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Load both workspace documents from `dir`. The synced file is required;
|
|
14
|
+
* the local file is optional and falls back to a minimal empty shape so a
|
|
15
|
+
* CLI on a fresh machine can still operate (it just won't have history /
|
|
16
|
+
* overrides until the desktop app runs once).
|
|
17
|
+
*/
|
|
18
|
+
declare function loadFromFile(dir: string, options?: LoadFromFileOptions): Promise<WorkspaceState | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Atomically write both documents back to disk. Acquires an advisory lock
|
|
21
|
+
* on the synced file for the duration of the write so a parallel CLI /
|
|
22
|
+
* MCP / desktop save can't interleave.
|
|
23
|
+
*
|
|
24
|
+
* Both files are written via `<file>.tmp` + rename so a crash mid-write
|
|
25
|
+
* never leaves a partial JSON document on disk.
|
|
26
|
+
*/
|
|
27
|
+
declare function saveToFile(dir: string, state: WorkspaceState, options?: SaveToFileOptions): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Run a load → mutate → save cycle under one lock so a single mutation
|
|
30
|
+
* can't be clobbered by a racing reader-then-writer.
|
|
31
|
+
*/
|
|
32
|
+
declare function withWorkspace<T>(dir: string, fn: (state: WorkspaceState) => Promise<{
|
|
33
|
+
next: WorkspaceState;
|
|
34
|
+
result?: T;
|
|
35
|
+
}>, options?: SaveToFileOptions): Promise<T | undefined>;
|
|
36
|
+
|
|
37
|
+
export { type LoadFromFileOptions, type SaveToFileOptions, loadFromFile, saveToFile, withWorkspace };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/workspace/fileBackedWorkspace.ts
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { FONT_SIZE_PERCENT_DEFAULT } from "@apicircle/shared";
|
|
5
|
+
import lockfile from "proper-lockfile";
|
|
6
|
+
var SYNCED_FILE = "workspace.synced.json";
|
|
7
|
+
var LOCAL_FILE = "workspace.local.json";
|
|
8
|
+
async function loadFromFile(dir, options = {}) {
|
|
9
|
+
const syncedPath = path.join(dir, SYNCED_FILE);
|
|
10
|
+
const localPath = path.join(dir, LOCAL_FILE);
|
|
11
|
+
let syncedRaw;
|
|
12
|
+
try {
|
|
13
|
+
syncedRaw = await fs.readFile(syncedPath, "utf-8");
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (options.allowMissing && isENOENT(err)) return null;
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
const synced = JSON.parse(syncedRaw);
|
|
19
|
+
let local;
|
|
20
|
+
try {
|
|
21
|
+
local = JSON.parse(await fs.readFile(localPath, "utf-8"));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (!isENOENT(err)) throw err;
|
|
24
|
+
local = createEmptyLocalForSynced(synced);
|
|
25
|
+
}
|
|
26
|
+
return { synced, local };
|
|
27
|
+
}
|
|
28
|
+
async function saveToFile(dir, state, options = {}) {
|
|
29
|
+
await fs.mkdir(dir, { recursive: true });
|
|
30
|
+
const syncedPath = path.join(dir, SYNCED_FILE);
|
|
31
|
+
const localPath = path.join(dir, LOCAL_FILE);
|
|
32
|
+
await ensureFile(syncedPath);
|
|
33
|
+
const release = await lockfile.lock(syncedPath, {
|
|
34
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },
|
|
35
|
+
stale: options.lockTimeoutMs ?? 3e4
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await writeJsonAtomic(syncedPath, state.synced);
|
|
39
|
+
await writeJsonAtomic(localPath, state.local);
|
|
40
|
+
} finally {
|
|
41
|
+
await release();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function withWorkspace(dir, fn, options = {}) {
|
|
45
|
+
await fs.mkdir(dir, { recursive: true });
|
|
46
|
+
const syncedPath = path.join(dir, SYNCED_FILE);
|
|
47
|
+
const localPath = path.join(dir, LOCAL_FILE);
|
|
48
|
+
await ensureFile(syncedPath);
|
|
49
|
+
const release = await lockfile.lock(syncedPath, {
|
|
50
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },
|
|
51
|
+
stale: options.lockTimeoutMs ?? 3e4
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const syncedRaw = await fs.readFile(syncedPath, "utf-8");
|
|
55
|
+
const synced = JSON.parse(syncedRaw);
|
|
56
|
+
let local;
|
|
57
|
+
try {
|
|
58
|
+
local = JSON.parse(await fs.readFile(localPath, "utf-8"));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (!isENOENT(err)) throw err;
|
|
61
|
+
local = createEmptyLocalForSynced(synced);
|
|
62
|
+
}
|
|
63
|
+
const out = await fn({ synced, local });
|
|
64
|
+
await writeJsonAtomic(syncedPath, out.next.synced);
|
|
65
|
+
await writeJsonAtomic(localPath, out.next.local);
|
|
66
|
+
return out.result;
|
|
67
|
+
} finally {
|
|
68
|
+
await release();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var WORKSPACE_FILE_MODE = 384;
|
|
72
|
+
async function ensureFile(filePath) {
|
|
73
|
+
try {
|
|
74
|
+
await fs.access(filePath);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (!isENOENT(err)) throw err;
|
|
77
|
+
await fs.writeFile(filePath, "{}", { encoding: "utf-8", mode: WORKSPACE_FILE_MODE });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function writeJsonAtomic(filePath, value) {
|
|
81
|
+
const tmp = `${filePath}.tmp`;
|
|
82
|
+
await fs.writeFile(tmp, JSON.stringify(value, null, 2) + "\n", {
|
|
83
|
+
encoding: "utf-8",
|
|
84
|
+
mode: WORKSPACE_FILE_MODE
|
|
85
|
+
});
|
|
86
|
+
await fs.rename(tmp, filePath);
|
|
87
|
+
}
|
|
88
|
+
function isENOENT(err) {
|
|
89
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
90
|
+
}
|
|
91
|
+
function createEmptyLocalForSynced(synced) {
|
|
92
|
+
return {
|
|
93
|
+
schemaVersion: 1,
|
|
94
|
+
workspaceId: synced.workspaceId,
|
|
95
|
+
executionPlans: {},
|
|
96
|
+
history: { requestRuns: [], planRuns: [] },
|
|
97
|
+
secretIndex: { entries: {} },
|
|
98
|
+
sessions: { github: { workspace: null, links: {} } },
|
|
99
|
+
connectedRepo: null,
|
|
100
|
+
workingBranch: null,
|
|
101
|
+
seededWorkspaceSha: null,
|
|
102
|
+
retiredBranch: null,
|
|
103
|
+
sync: {
|
|
104
|
+
lastPulledSnapshot: null,
|
|
105
|
+
lastPulledSha: null,
|
|
106
|
+
lastPulledAt: null,
|
|
107
|
+
dirtyKeys: []
|
|
108
|
+
},
|
|
109
|
+
linkedCollections: {},
|
|
110
|
+
globalContext: {},
|
|
111
|
+
mockRuntime: { active: {} },
|
|
112
|
+
ui: {
|
|
113
|
+
activeRequestId: null,
|
|
114
|
+
sidebarExpandedSections: [],
|
|
115
|
+
themeId: "studio-dark",
|
|
116
|
+
fontId: "system-mono",
|
|
117
|
+
fontSizePercent: FONT_SIZE_PERCENT_DEFAULT
|
|
118
|
+
},
|
|
119
|
+
settings: { validateOnSend: true, monacoConsumesWheel: false },
|
|
120
|
+
snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
loadFromFile,
|
|
125
|
+
saveToFile,
|
|
126
|
+
withWorkspace
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=file-backed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/workspace/fileBackedWorkspace.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport { FONT_SIZE_PERCENT_DEFAULT } from '@apicircle/shared';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// fileBackedWorkspace — load/save a `{ synced, local }` pair as two JSON\n// files on disk, with a `proper-lockfile` advisory lock so concurrent CLI /\n// MCP writers can't corrupt the document.\n//\n// Layout (relative to the directory passed in):\n// workspace.synced.json ← matches WorkspaceSynced exactly, push-to-git target\n// workspace.local.json ← WorkspaceLocal, host-private (CLI/MCP doesn't push)\n//\n// The lock is held on `workspace.synced.json` because that's the file the\n// editor races against. Stale locks are released after 30s.\n// =============================================================================\n\nconst SYNCED_FILE = 'workspace.synced.json';\nconst LOCAL_FILE = 'workspace.local.json';\n\nexport interface LoadFromFileOptions {\n /** When true, return `null` instead of throwing if the synced file is missing. */\n allowMissing?: boolean;\n}\n\nexport interface SaveToFileOptions {\n /** Lock timeout (ms). Defaults to 30000. */\n lockTimeoutMs?: number;\n}\n\n/**\n * Load both workspace documents from `dir`. The synced file is required;\n * the local file is optional and falls back to a minimal empty shape so a\n * CLI on a fresh machine can still operate (it just won't have history /\n * overrides until the desktop app runs once).\n */\nexport async function loadFromFile(\n dir: string,\n options: LoadFromFileOptions = {},\n): Promise<WorkspaceState | null> {\n const syncedPath = path.join(dir, SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n let syncedRaw: string;\n try {\n syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n } catch (err) {\n if (options.allowMissing && isENOENT(err)) return null;\n throw err;\n }\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n\n return { synced, local };\n}\n\n/**\n * Atomically write both documents back to disk. Acquires an advisory lock\n * on the synced file for the duration of the write so a parallel CLI /\n * MCP / desktop save can't interleave.\n *\n * Both files are written via `<file>.tmp` + rename so a crash mid-write\n * never leaves a partial JSON document on disk.\n */\nexport async function saveToFile(\n dir: string,\n state: WorkspaceState,\n options: SaveToFileOptions = {},\n): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n // proper-lockfile requires the target file to exist. Touch it on first save.\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n await writeJsonAtomic(syncedPath, state.synced);\n await writeJsonAtomic(localPath, state.local);\n } finally {\n await release();\n }\n}\n\n/**\n * Run a load → mutate → save cycle under one lock so a single mutation\n * can't be clobbered by a racing reader-then-writer.\n */\nexport async function withWorkspace<T>(\n dir: string,\n fn: (state: WorkspaceState) => Promise<{ next: WorkspaceState; result?: T }>,\n options: SaveToFileOptions = {},\n): Promise<T | undefined> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n const syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n const out = await fn({ synced, local });\n await writeJsonAtomic(syncedPath, out.next.synced);\n await writeJsonAtomic(localPath, out.next.local);\n return out.result;\n } finally {\n await release();\n }\n}\n\n// ---------------------------------------------------------------------------\n// internals\n// ---------------------------------------------------------------------------\n\n// File mode for workspace JSON: owner read/write only. Default `fs.writeFile`\n// uses 0o666 minus umask (typically 0o644 — world-readable). The workspace\n// docs carry the synced state (which after redaction is mostly safe to read\n// but still includes per-workspace metadata) and the local state (which\n// holds the encrypted Secret Vault payload table, session metadata, and the\n// vault entries themselves). On multi-user POSIX hosts (CI runners,\n// classroom VMs, shared dev servers) the default would leak both. 0o600\n// keeps the file owner-only. Windows ignores POSIX modes — the inherited\n// per-user ACL under %USERPROFILE% is what protects it there.\nconst WORKSPACE_FILE_MODE = 0o600;\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: WORKSPACE_FILE_MODE });\n }\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fs.writeFile(tmp, JSON.stringify(value, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: WORKSPACE_FILE_MODE,\n });\n await fs.rename(tmp, filePath);\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\nfunction createEmptyLocalForSynced(synced: WorkspaceSynced): WorkspaceLocal {\n return {\n schemaVersion: 1,\n workspaceId: synced.workspaceId,\n executionPlans: {},\n history: { requestRuns: [], planRuns: [] },\n secretIndex: { entries: {} },\n sessions: { github: { workspace: null, links: {} } },\n connectedRepo: null,\n workingBranch: null,\n seededWorkspaceSha: null,\n retiredBranch: null,\n sync: {\n lastPulledSnapshot: null,\n lastPulledSha: null,\n lastPulledAt: null,\n dirtyKeys: [],\n },\n linkedCollections: {},\n globalContext: {},\n mockRuntime: { active: {} },\n ui: {\n activeRequestId: null,\n sidebarExpandedSections: [],\n themeId: 'studio-dark',\n fontId: 'system-mono',\n fontSizePercent: FONT_SIZE_PERCENT_DEFAULT,\n },\n settings: { validateOnSend: true, monacoConsumesWheel: false },\n snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 },\n };\n}\n"],"mappings":"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","names":[]}
|