@amerged/ohmyhost-mcp 0.1.28 → 0.1.30
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/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +1419 -0
- package/dist/{chunk-GIP4M4CK.js → chunk-MVT76LJ5.js} +92 -28
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +24153 -2354
- package/package.json +1 -1
- package/src/apps/mcp/package.json +1 -1
- package/src/apps/mcp/src/local-client.ts +12 -2
- package/src/apps/mcp/src/local-server.ts +333 -4
- package/src/apps/product-cli/package.json +4 -2
- package/src/apps/product-cli/src/cli.ts +312 -2
- package/src/apps/product-cli/src/command.ts +261 -0
- package/src/apps/product-cli/src/composition.ts +74 -4
- package/src/apps/product-cli/src/index.ts +6 -0
- package/src/apps/product-cli/src/product-api.ts +5 -0
- package/src/apps/product-cli/src/project-link-store.ts +92 -1
- package/src/apps/product-cli/src/repository-init.ts +2 -2
- package/src/apps/product-cli/src/runtime-contract.ts +34 -1
- package/src/apps/product-cli/src/source-managed-client.ts +189 -0
- package/src/apps/product-cli/src/source-publication-store.ts +563 -0
- package/src/apps/product-cli/src/source-publisher.ts +195 -0
- package/src/apps/product-cli/src/source-workspace.ts +552 -0
- package/src/packages/agent-skills/src/generated-skill-resources.ts +43 -15
- package/src/packages/contracts/package.json +10 -0
- package/src/packages/contracts/src/database-migration-admission.ts +65 -125
- package/src/packages/contracts/src/database-migration-errors.ts +14 -0
- package/src/packages/contracts/src/database-migration-lexical.ts +180 -0
- package/src/packages/contracts/src/database-migration-policies.ts +150 -0
- package/src/packages/contracts/src/framework-admission.ts +1 -1
- package/src/packages/contracts/src/framework-build-script.mjs +1 -2
- package/src/packages/contracts/src/managed-sources.ts +582 -0
- package/src/packages/contracts/src/mcp-source-codec.ts +150 -0
- package/src/packages/contracts/src/operation-failure.ts +22 -6
- package/src/packages/contracts/src/problem-policy.ts +61 -14
- package/src/packages/sdk-ts/package.json +1 -1
- package/src/packages/sdk-ts/src/generated/index.ts +92 -0
- package/src/packages/sdk-ts/src/generated/sdk.gen.ts +541 -0
- package/src/packages/sdk-ts/src/generated/types.gen.ts +911 -4
- package/src/packages/workos-auth-contracts/src/tokens.ts +61 -4
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants, promises as fs } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
parseManagedSourceRequest,
|
|
7
|
+
assertManagedSourceResponse,
|
|
8
|
+
type ManagedProjectSource,
|
|
9
|
+
type ManagedSourceUploadRequest,
|
|
10
|
+
} from "@ohmyhost/contracts/managed-sources";
|
|
11
|
+
import type { Operation } from "@ohmyhost/sdk-ts";
|
|
12
|
+
|
|
13
|
+
import { OwnerOnlyProjectLinkStore, readProjectLinks } from "./project-link-store.js";
|
|
14
|
+
import {
|
|
15
|
+
parseCurrentIdentity,
|
|
16
|
+
parseOperation,
|
|
17
|
+
parseSafeProblem,
|
|
18
|
+
ResponseContractError,
|
|
19
|
+
} from "./runtime-contract.js";
|
|
20
|
+
import { isSourcePublicationIdempotencyKey, type ManagedSourceApi } from "./source-publisher.js";
|
|
21
|
+
|
|
22
|
+
const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
|
|
23
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
24
|
+
type LocalSourceCode =
|
|
25
|
+
| "source_binding_required"
|
|
26
|
+
| "source_publication_pending"
|
|
27
|
+
| "source_publication_scope_mismatch"
|
|
28
|
+
| "source_publication_missing"
|
|
29
|
+
| "source_publication_failed"
|
|
30
|
+
| "source_publication_input_invalid"
|
|
31
|
+
| "source_local_state_invalid";
|
|
32
|
+
const MESSAGES: Record<LocalSourceCode, string> = {
|
|
33
|
+
source_binding_required:
|
|
34
|
+
"The directory has no matching confirmed source base. Read and merge the current source, then pass its expected commit explicitly; never select a fresh remote head for unchanged local files.",
|
|
35
|
+
source_publication_pending:
|
|
36
|
+
"Complete the directory's previous source publication before starting another attempt. Use source publish complete with the same project and login.",
|
|
37
|
+
source_publication_scope_mismatch:
|
|
38
|
+
"The pending source publication belongs to another directory, account, project or source binding. Resume its exact project and login.",
|
|
39
|
+
source_publication_missing:
|
|
40
|
+
"No pending source publication exists in this directory for this project and login.",
|
|
41
|
+
source_publication_failed:
|
|
42
|
+
"The source publication did not succeed. Inspect its operation before fixing the cause and starting a new attempt.",
|
|
43
|
+
source_publication_input_invalid:
|
|
44
|
+
"Use a valid source snapshot and an idempotency key of 1 to 128 characters.",
|
|
45
|
+
source_local_state_invalid:
|
|
46
|
+
"The owner-only local source metadata could not be read or stored safely.",
|
|
47
|
+
};
|
|
48
|
+
export class LocalSourcePublicationError extends Error {
|
|
49
|
+
public constructor(
|
|
50
|
+
readonly code: LocalSourceCode,
|
|
51
|
+
readonly operation?: Operation,
|
|
52
|
+
) {
|
|
53
|
+
super(MESSAGES[code]);
|
|
54
|
+
this.name = "LocalSourcePublicationError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface PendingPublication {
|
|
59
|
+
readonly version: 1;
|
|
60
|
+
readonly directory: string;
|
|
61
|
+
readonly metadata_directory: string;
|
|
62
|
+
readonly api_origin: string;
|
|
63
|
+
readonly organization_id: string;
|
|
64
|
+
readonly actor_id: string;
|
|
65
|
+
readonly project_id: string;
|
|
66
|
+
readonly operation_id: string | null;
|
|
67
|
+
readonly idempotency_key: string;
|
|
68
|
+
readonly snapshot_sha256: string;
|
|
69
|
+
readonly request: ManagedSourceUploadRequest;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PreparedSourcePublication {
|
|
73
|
+
readonly request: ManagedSourceUploadRequest;
|
|
74
|
+
readonly accepted: (operation: Operation) => Promise<void>;
|
|
75
|
+
readonly rejected: (error: unknown) => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type LocalSourceMetadataDirectory = ".ohmyhost" | ".ohmyhost/production";
|
|
79
|
+
|
|
80
|
+
/** A directory's last confirmed base is an optimistic client cache, not the repository head. */
|
|
81
|
+
export async function prepareLocalSourcePublication(input: {
|
|
82
|
+
directory: string;
|
|
83
|
+
metadataDirectory?: LocalSourceMetadataDirectory;
|
|
84
|
+
apiOrigin: string;
|
|
85
|
+
identity: unknown;
|
|
86
|
+
projectId: string;
|
|
87
|
+
request: ManagedSourceUploadRequest;
|
|
88
|
+
explicitExpectedCommit: boolean;
|
|
89
|
+
snapshotSha256: string;
|
|
90
|
+
idempotencyKey: string;
|
|
91
|
+
}): Promise<PreparedSourcePublication> {
|
|
92
|
+
if (
|
|
93
|
+
!isSourcePublicationIdempotencyKey(input.idempotencyKey) ||
|
|
94
|
+
typeof input.snapshotSha256 !== "string" ||
|
|
95
|
+
!DIGEST.test(input.snapshotSha256)
|
|
96
|
+
)
|
|
97
|
+
throw new LocalSourcePublicationError("source_publication_input_invalid");
|
|
98
|
+
try {
|
|
99
|
+
parseManagedSourceRequest("upload", input.request);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new LocalSourcePublicationError("source_publication_input_invalid");
|
|
102
|
+
}
|
|
103
|
+
const scope = await publicationScope(
|
|
104
|
+
input.directory,
|
|
105
|
+
input.apiOrigin,
|
|
106
|
+
input.identity,
|
|
107
|
+
input.projectId,
|
|
108
|
+
input.metadataDirectory,
|
|
109
|
+
);
|
|
110
|
+
let request = input.request;
|
|
111
|
+
const previous = await readPending(scope);
|
|
112
|
+
const completed = previous === null ? await readRecord(completedPath(scope)) : null;
|
|
113
|
+
const replay = completed?.idempotency_key === input.idempotencyKey;
|
|
114
|
+
const original = previous ?? (replay ? await readPending(scope, true) : null);
|
|
115
|
+
if (original !== null && request.mode === "commit" && !input.explicitExpectedCommit)
|
|
116
|
+
request = { ...request, expected_commit_sha: original.request.expected_commit_sha };
|
|
117
|
+
else if (original === null && request.mode === "commit" && !input.explicitExpectedCommit) {
|
|
118
|
+
const link = (await readProjectLinks(scope.metadata_directory, scope.api_origin)).links.find(
|
|
119
|
+
(link) =>
|
|
120
|
+
link.organization_id === scope.organization_id && link.project_id === scope.project_id,
|
|
121
|
+
);
|
|
122
|
+
if (
|
|
123
|
+
link?.version !== 3 ||
|
|
124
|
+
link.source.provider !== "managed" ||
|
|
125
|
+
link.source.source_connection_id !== request.expected_source_connection_id ||
|
|
126
|
+
link.source.source_generation !== request.expected_source_generation
|
|
127
|
+
)
|
|
128
|
+
throw new LocalSourcePublicationError("source_binding_required");
|
|
129
|
+
request = { ...request, expected_commit_sha: link.source.last_confirmed_commit_sha };
|
|
130
|
+
}
|
|
131
|
+
parseManagedSourceRequest("upload", request);
|
|
132
|
+
const record: PendingPublication = {
|
|
133
|
+
version: 1,
|
|
134
|
+
...scope,
|
|
135
|
+
operation_id: null,
|
|
136
|
+
idempotency_key: input.idempotencyKey,
|
|
137
|
+
snapshot_sha256: input.snapshotSha256,
|
|
138
|
+
request,
|
|
139
|
+
};
|
|
140
|
+
if (original !== null) {
|
|
141
|
+
assertSameAttempt(original, record);
|
|
142
|
+
} else await writePending(record, true);
|
|
143
|
+
return {
|
|
144
|
+
request,
|
|
145
|
+
accepted: async (operation) => {
|
|
146
|
+
const current = await readPending(scope, replay);
|
|
147
|
+
if (current === null) throw new LocalSourcePublicationError("source_publication_missing");
|
|
148
|
+
assertSameAttempt(current, record);
|
|
149
|
+
if (current.operation_id !== null && current.operation_id !== operation.id)
|
|
150
|
+
throw new ResponseContractError();
|
|
151
|
+
if (!replay) await writePending({ ...current, operation_id: operation.id }, false);
|
|
152
|
+
},
|
|
153
|
+
rejected: async (error) => {
|
|
154
|
+
let code: string;
|
|
155
|
+
try {
|
|
156
|
+
code = parseSafeProblem(error).code;
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
code !== "source_generation_conflict" &&
|
|
162
|
+
code !== "source_commit_conflict" &&
|
|
163
|
+
code !== "idempotency_key_reused"
|
|
164
|
+
)
|
|
165
|
+
return;
|
|
166
|
+
try {
|
|
167
|
+
const current = await readPending(scope);
|
|
168
|
+
if (current === null || current.operation_id !== null) return;
|
|
169
|
+
assertSameAttempt(current, record);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (
|
|
172
|
+
error instanceof LocalSourcePublicationError &&
|
|
173
|
+
(error.code === "source_publication_scope_mismatch" ||
|
|
174
|
+
error.code === "source_publication_pending")
|
|
175
|
+
)
|
|
176
|
+
return;
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
await fs.unlink(pendingPath(scope));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!missing(error)) throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Polling completes under the original identity and stores the operation's own confirmed commit. */
|
|
189
|
+
export async function completeLocalSourcePublication(input: {
|
|
190
|
+
directory: string;
|
|
191
|
+
metadataDirectory?: LocalSourceMetadataDirectory;
|
|
192
|
+
apiOrigin: string;
|
|
193
|
+
identity: unknown;
|
|
194
|
+
projectId: string;
|
|
195
|
+
operationId?: string;
|
|
196
|
+
api: ManagedSourceApi;
|
|
197
|
+
getOperation: (id: string) => Promise<Operation>;
|
|
198
|
+
}): Promise<{ status: "pending" | "completed"; operation: Operation; receipt: unknown }> {
|
|
199
|
+
const scope = await publicationScope(
|
|
200
|
+
input.directory,
|
|
201
|
+
input.apiOrigin,
|
|
202
|
+
input.identity,
|
|
203
|
+
input.projectId,
|
|
204
|
+
input.metadataDirectory,
|
|
205
|
+
);
|
|
206
|
+
let pending = await readPending(scope);
|
|
207
|
+
const replay = pending === null;
|
|
208
|
+
if (pending === null) pending = await readPending(scope, true);
|
|
209
|
+
if (pending === null) throw new LocalSourcePublicationError("source_publication_missing");
|
|
210
|
+
if (replay && pending.operation_id === null)
|
|
211
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
212
|
+
if (pending.operation_id === null) {
|
|
213
|
+
const accepted = await input.api.createUpload({
|
|
214
|
+
projectId: scope.project_id,
|
|
215
|
+
idempotencyKey: pending.idempotency_key,
|
|
216
|
+
body: pending.request,
|
|
217
|
+
});
|
|
218
|
+
parseOperation(accepted);
|
|
219
|
+
pending = { ...pending, operation_id: accepted.id };
|
|
220
|
+
await writePending(pending, false);
|
|
221
|
+
}
|
|
222
|
+
const operationId = pending.operation_id;
|
|
223
|
+
if (
|
|
224
|
+
operationId === null ||
|
|
225
|
+
(input.operationId !== undefined && input.operationId !== operationId)
|
|
226
|
+
)
|
|
227
|
+
throw new LocalSourcePublicationError("source_publication_scope_mismatch");
|
|
228
|
+
const operation = parseOperation(await input.getOperation(operationId));
|
|
229
|
+
const receipt = await input.api.getUpload({ projectId: scope.project_id, operationId });
|
|
230
|
+
assertManagedSourceResponse("upload", receipt);
|
|
231
|
+
if (
|
|
232
|
+
receipt.operation_id !== operationId ||
|
|
233
|
+
operation.id !== operationId ||
|
|
234
|
+
receipt.expected_source_generation !== pending.request.expected_source_generation ||
|
|
235
|
+
receipt.expected_commit_sha !== pending.request.expected_commit_sha
|
|
236
|
+
)
|
|
237
|
+
throw new ResponseContractError();
|
|
238
|
+
if (
|
|
239
|
+
operation.state === "failed" ||
|
|
240
|
+
operation.state === "cancelled" ||
|
|
241
|
+
receipt.state === "failed"
|
|
242
|
+
) {
|
|
243
|
+
if (!replay) await fs.unlink(pendingPath(scope));
|
|
244
|
+
throw new LocalSourcePublicationError("source_publication_failed", operation);
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
operation.state !== "succeeded" ||
|
|
248
|
+
receipt.state !== "committed" ||
|
|
249
|
+
receipt.commit_sha === null
|
|
250
|
+
)
|
|
251
|
+
return { status: "pending", operation, receipt };
|
|
252
|
+
const source = await input.api.getSource(scope.project_id);
|
|
253
|
+
assertManagedSourceResponse("source", source);
|
|
254
|
+
const managed = source as ManagedProjectSource;
|
|
255
|
+
const generation =
|
|
256
|
+
pending.request.expected_source_generation + (pending.request.mode === "commit" ? 0 : 1);
|
|
257
|
+
if (
|
|
258
|
+
managed.status !== "ready" ||
|
|
259
|
+
managed.source_generation !== generation ||
|
|
260
|
+
(pending.request.mode === "commit" &&
|
|
261
|
+
managed.source_connection_id !== pending.request.expected_source_connection_id)
|
|
262
|
+
)
|
|
263
|
+
throw new LocalSourcePublicationError("source_publication_scope_mismatch");
|
|
264
|
+
const tree = await input.api.files({
|
|
265
|
+
projectId: scope.project_id,
|
|
266
|
+
commitSha: receipt.commit_sha,
|
|
267
|
+
});
|
|
268
|
+
assertManagedSourceResponse("files", tree);
|
|
269
|
+
if (
|
|
270
|
+
typeof tree !== "object" ||
|
|
271
|
+
tree === null ||
|
|
272
|
+
!("files" in tree) ||
|
|
273
|
+
!Array.isArray(tree.files) ||
|
|
274
|
+
!("commit_sha" in tree) ||
|
|
275
|
+
tree.commit_sha !== receipt.commit_sha
|
|
276
|
+
)
|
|
277
|
+
throw new ResponseContractError();
|
|
278
|
+
const manifest = tree.files as { path: string; sha256: string; executable: boolean }[];
|
|
279
|
+
const snapshot = `sha256:${createHash("sha256")
|
|
280
|
+
.update(
|
|
281
|
+
JSON.stringify(
|
|
282
|
+
[...manifest]
|
|
283
|
+
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
284
|
+
.map(({ path, sha256, executable }) => [path, sha256, executable]),
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
.digest("hex")}`;
|
|
288
|
+
if (snapshot !== pending.snapshot_sha256)
|
|
289
|
+
throw new LocalSourcePublicationError("source_publication_scope_mismatch");
|
|
290
|
+
const binding = (await readProjectLinks(scope.metadata_directory, scope.api_origin)).links.find(
|
|
291
|
+
(link) => link.organization_id === scope.organization_id,
|
|
292
|
+
);
|
|
293
|
+
if (binding !== undefined && binding.project_id !== scope.project_id)
|
|
294
|
+
throw new LocalSourcePublicationError("source_publication_scope_mismatch");
|
|
295
|
+
if (replay) {
|
|
296
|
+
if (
|
|
297
|
+
binding?.version !== 3 ||
|
|
298
|
+
binding.source.provider !== "managed" ||
|
|
299
|
+
binding.source.source_connection_id !== managed.source_connection_id ||
|
|
300
|
+
binding.source.source_generation !== managed.source_generation ||
|
|
301
|
+
binding.source.last_confirmed_commit_sha !== receipt.commit_sha
|
|
302
|
+
)
|
|
303
|
+
throw new LocalSourcePublicationError("source_publication_scope_mismatch");
|
|
304
|
+
return { status: "completed", operation, receipt };
|
|
305
|
+
}
|
|
306
|
+
const current = await readPending(scope);
|
|
307
|
+
if (current === null) throw new LocalSourcePublicationError("source_publication_missing");
|
|
308
|
+
assertSameAttempt(current, pending);
|
|
309
|
+
await new OwnerOnlyProjectLinkStore(scope.metadata_directory, scope.directory).save({
|
|
310
|
+
version: 3,
|
|
311
|
+
api_origin: scope.api_origin,
|
|
312
|
+
organization_id: scope.organization_id,
|
|
313
|
+
project_id: scope.project_id,
|
|
314
|
+
source: {
|
|
315
|
+
provider: "managed",
|
|
316
|
+
source_connection_id: managed.source_connection_id,
|
|
317
|
+
source_generation: managed.source_generation,
|
|
318
|
+
namespace: managed.namespace,
|
|
319
|
+
repository_name: managed.repository_name,
|
|
320
|
+
branch: "main",
|
|
321
|
+
last_confirmed_commit_sha: receipt.commit_sha,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
await writePending(pending, false, true);
|
|
325
|
+
await fs.unlink(pendingPath(scope));
|
|
326
|
+
return { status: "completed", operation, receipt };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Discovery reveals resumable project/operation identities, never stored keys or commit messages. */
|
|
330
|
+
export async function inspectLocalSourcePublications(
|
|
331
|
+
directory: string,
|
|
332
|
+
): Promise<readonly Record<string, unknown>[]> {
|
|
333
|
+
const root = await fs.realpath(resolve(directory));
|
|
334
|
+
const result: Record<string, unknown>[] = [];
|
|
335
|
+
for (const metadataDirectory of [".ohmyhost", ".ohmyhost/production"] as const) {
|
|
336
|
+
const folder = join(root, metadataDirectory, "publications");
|
|
337
|
+
let names: string[];
|
|
338
|
+
try {
|
|
339
|
+
await checkedDirectory(folder);
|
|
340
|
+
names = await fs.readdir(folder);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (missing(error)) continue;
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
if (names.length > 1000) throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
346
|
+
for (const name of names.sort()) {
|
|
347
|
+
if (!/^[0-7][0-9A-HJKMNP-TV-Z]{25}-[0-7][0-9A-HJKMNP-TV-Z]{25}\.json$/u.test(name)) continue;
|
|
348
|
+
const pending = await readRecord(join(folder, name));
|
|
349
|
+
if (pending !== null)
|
|
350
|
+
result.push({
|
|
351
|
+
api_origin: pending.api_origin,
|
|
352
|
+
organization_id: pending.organization_id,
|
|
353
|
+
project_id: pending.project_id,
|
|
354
|
+
operation_id: pending.operation_id,
|
|
355
|
+
source_generation: pending.request.expected_source_generation,
|
|
356
|
+
mode: pending.request.mode,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function publicationScope(
|
|
364
|
+
directory: string,
|
|
365
|
+
apiOrigin: string,
|
|
366
|
+
identity: unknown,
|
|
367
|
+
projectId: string,
|
|
368
|
+
metadataDirectory: LocalSourceMetadataDirectory = ".ohmyhost",
|
|
369
|
+
) {
|
|
370
|
+
const user = parseCurrentIdentity(identity);
|
|
371
|
+
const [organizationId, ...others] = user.organization_ids;
|
|
372
|
+
if (organizationId === undefined || others.length !== 0 || !ULID.test(projectId))
|
|
373
|
+
throw new LocalSourcePublicationError("source_publication_scope_mismatch");
|
|
374
|
+
const root = await fs.realpath(resolve(directory));
|
|
375
|
+
await checkedDirectory(root);
|
|
376
|
+
return {
|
|
377
|
+
directory: root,
|
|
378
|
+
metadata_directory: join(root, metadataDirectory),
|
|
379
|
+
api_origin: apiOrigin,
|
|
380
|
+
organization_id: organizationId,
|
|
381
|
+
actor_id: user.actor_id,
|
|
382
|
+
project_id: projectId,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function pendingPath(
|
|
387
|
+
scope: Pick<PendingPublication, "metadata_directory" | "organization_id" | "project_id">,
|
|
388
|
+
): string {
|
|
389
|
+
return join(
|
|
390
|
+
scope.metadata_directory,
|
|
391
|
+
"publications",
|
|
392
|
+
`${scope.organization_id}-${scope.project_id}.json`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function completedPath(
|
|
397
|
+
scope: Pick<PendingPublication, "metadata_directory" | "organization_id" | "project_id">,
|
|
398
|
+
): string {
|
|
399
|
+
return join(
|
|
400
|
+
scope.metadata_directory,
|
|
401
|
+
"publications",
|
|
402
|
+
`completed-${scope.organization_id}-${scope.project_id}.json`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function readPending(
|
|
407
|
+
scope: Pick<
|
|
408
|
+
PendingPublication,
|
|
409
|
+
| "directory"
|
|
410
|
+
| "metadata_directory"
|
|
411
|
+
| "api_origin"
|
|
412
|
+
| "organization_id"
|
|
413
|
+
| "actor_id"
|
|
414
|
+
| "project_id"
|
|
415
|
+
>,
|
|
416
|
+
completed = false,
|
|
417
|
+
): Promise<PendingPublication | null> {
|
|
418
|
+
const value = await readRecord(completed ? completedPath(scope) : pendingPath(scope));
|
|
419
|
+
if (
|
|
420
|
+
value !== null &&
|
|
421
|
+
Object.entries(scope).some(([key, field]) => value[key as keyof PendingPublication] !== field)
|
|
422
|
+
)
|
|
423
|
+
throw new LocalSourcePublicationError("source_publication_scope_mismatch");
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function readRecord(path: string): Promise<PendingPublication | null> {
|
|
428
|
+
let handle;
|
|
429
|
+
try {
|
|
430
|
+
const status = await fs.lstat(path);
|
|
431
|
+
if (
|
|
432
|
+
!status.isFile() ||
|
|
433
|
+
status.isSymbolicLink() ||
|
|
434
|
+
status.size > 64 * 1024 ||
|
|
435
|
+
(await fs.realpath(path)) !== path
|
|
436
|
+
)
|
|
437
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
438
|
+
handle = await fs.open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
439
|
+
const content = Buffer.alloc(status.size + 1);
|
|
440
|
+
const read = await handle.read({ buffer: content, length: content.byteLength, position: 0 });
|
|
441
|
+
if (read.bytesRead !== status.size || (await handle.stat()).size !== status.size)
|
|
442
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
443
|
+
const raw = JSON.parse(content.subarray(0, read.bytesRead).toString("utf8")) as Record<
|
|
444
|
+
string,
|
|
445
|
+
unknown
|
|
446
|
+
>;
|
|
447
|
+
if (
|
|
448
|
+
Object.keys(raw).sort().join(",") !==
|
|
449
|
+
[
|
|
450
|
+
"version",
|
|
451
|
+
"directory",
|
|
452
|
+
"metadata_directory",
|
|
453
|
+
"api_origin",
|
|
454
|
+
"organization_id",
|
|
455
|
+
"actor_id",
|
|
456
|
+
"project_id",
|
|
457
|
+
"operation_id",
|
|
458
|
+
"idempotency_key",
|
|
459
|
+
"snapshot_sha256",
|
|
460
|
+
"request",
|
|
461
|
+
]
|
|
462
|
+
.sort()
|
|
463
|
+
.join(",") ||
|
|
464
|
+
raw["version"] !== 1 ||
|
|
465
|
+
typeof raw["directory"] !== "string" ||
|
|
466
|
+
typeof raw["metadata_directory"] !== "string" ||
|
|
467
|
+
![
|
|
468
|
+
join(raw["directory"], ".ohmyhost"),
|
|
469
|
+
join(raw["directory"], ".ohmyhost/production"),
|
|
470
|
+
].includes(raw["metadata_directory"]) ||
|
|
471
|
+
typeof raw["api_origin"] !== "string" ||
|
|
472
|
+
typeof raw["actor_id"] !== "string" ||
|
|
473
|
+
typeof raw["organization_id"] !== "string" ||
|
|
474
|
+
!ULID.test(raw["organization_id"]) ||
|
|
475
|
+
typeof raw["project_id"] !== "string" ||
|
|
476
|
+
!ULID.test(raw["project_id"]) ||
|
|
477
|
+
(raw["operation_id"] !== null &&
|
|
478
|
+
(typeof raw["operation_id"] !== "string" || !ULID.test(raw["operation_id"]))) ||
|
|
479
|
+
!isSourcePublicationIdempotencyKey(raw["idempotency_key"]) ||
|
|
480
|
+
typeof raw["snapshot_sha256"] !== "string" ||
|
|
481
|
+
!DIGEST.test(raw["snapshot_sha256"])
|
|
482
|
+
)
|
|
483
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
484
|
+
parseManagedSourceRequest("upload", raw["request"]);
|
|
485
|
+
const origin = new URL(raw["api_origin"]);
|
|
486
|
+
if (
|
|
487
|
+
origin.origin !== raw["api_origin"] ||
|
|
488
|
+
origin.username !== "" ||
|
|
489
|
+
origin.password !== "" ||
|
|
490
|
+
!["https:", "http:"].includes(origin.protocol)
|
|
491
|
+
)
|
|
492
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
493
|
+
return raw as unknown as PendingPublication;
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (missing(error)) return null;
|
|
496
|
+
if (error instanceof LocalSourcePublicationError) throw error;
|
|
497
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
498
|
+
} finally {
|
|
499
|
+
await handle?.close();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function writePending(
|
|
504
|
+
record: PendingPublication,
|
|
505
|
+
create: boolean,
|
|
506
|
+
completed = false,
|
|
507
|
+
): Promise<void> {
|
|
508
|
+
const parent = join(record.directory, ".ohmyhost");
|
|
509
|
+
for (const directory of [
|
|
510
|
+
parent,
|
|
511
|
+
record.metadata_directory,
|
|
512
|
+
join(record.metadata_directory, "publications"),
|
|
513
|
+
]) {
|
|
514
|
+
try {
|
|
515
|
+
await fs.mkdir(directory, { mode: 0o700 });
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (!existing(error)) throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
518
|
+
}
|
|
519
|
+
await checkedDirectory(directory);
|
|
520
|
+
await fs.chmod(directory, 0o700);
|
|
521
|
+
}
|
|
522
|
+
const target = completed ? completedPath(record) : pendingPath(record);
|
|
523
|
+
const temporary = create ? target : `${target}.${randomUUID()}.tmp`;
|
|
524
|
+
try {
|
|
525
|
+
await fs.writeFile(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: "wx" });
|
|
526
|
+
if (!create) await fs.rename(temporary, target);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (existing(error) && create)
|
|
529
|
+
throw new LocalSourcePublicationError("source_publication_pending");
|
|
530
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
531
|
+
} finally {
|
|
532
|
+
if (!create) await fs.unlink(temporary).catch(() => undefined);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function assertSameAttempt(left: PendingPublication, right: PendingPublication): void {
|
|
537
|
+
if (
|
|
538
|
+
left.idempotency_key !== right.idempotency_key ||
|
|
539
|
+
left.snapshot_sha256 !== right.snapshot_sha256 ||
|
|
540
|
+
canonicalRequest(left.request) !== canonicalRequest(right.request)
|
|
541
|
+
)
|
|
542
|
+
throw new LocalSourcePublicationError("source_publication_pending");
|
|
543
|
+
}
|
|
544
|
+
function canonicalRequest(request: ManagedSourceUploadRequest): string {
|
|
545
|
+
return JSON.stringify([
|
|
546
|
+
request.mode,
|
|
547
|
+
request.expected_source_generation,
|
|
548
|
+
request.expected_source_connection_id,
|
|
549
|
+
request.expected_commit_sha,
|
|
550
|
+
request.message,
|
|
551
|
+
]);
|
|
552
|
+
}
|
|
553
|
+
async function checkedDirectory(path: string): Promise<void> {
|
|
554
|
+
const status = await fs.lstat(path);
|
|
555
|
+
if (!status.isDirectory() || status.isSymbolicLink() || (await fs.realpath(path)) !== path)
|
|
556
|
+
throw new LocalSourcePublicationError("source_local_state_invalid");
|
|
557
|
+
}
|
|
558
|
+
function missing(error: unknown): boolean {
|
|
559
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
560
|
+
}
|
|
561
|
+
function existing(error: unknown): boolean {
|
|
562
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
563
|
+
}
|