cinatra 0.1.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.
@@ -0,0 +1,137 @@
1
+ // `cinatra extensions submit <tarball.tgz>` — submit a built extension
2
+ // tarball to the Cinatra Marketplace for review.
3
+ //
4
+ // Flow:
5
+ // 1. Read the tarball bytes from disk.
6
+ // 2. Extract `package.json` via pacote to derive `name` + `version`.
7
+ // `name` MUST be of the form `@<namespace>/<extension-stem>`.
8
+ // 3. Compute sha256(bytes) + size; base64-encode.
9
+ // 4. Call `cinatra/extension-submit-for-review` over MCP. The marketplace
10
+ // verifies digest + size, stages the tarball into the hidden scope,
11
+ // and records a `pending` submission.
12
+ //
13
+ // Vendor surface intentionally minimal: no flags for `--namespace` etc. The
14
+ // tarball's own `package.json` is the source of truth — that matches the
15
+ // invariant we want to enforce at promotion time (reviewed bytes == installed
16
+ // bytes from the same package.json).
17
+
18
+ import { readFile } from "node:fs/promises";
19
+ import { resolve as resolvePath } from "node:path";
20
+ import { createHash } from "node:crypto";
21
+
22
+ import pacote from "pacote";
23
+
24
+ import { callMarketplaceTool } from "./marketplace-mcp.mjs";
25
+ import {
26
+ assertDependencyOrdering,
27
+ DEFAULT_REGISTRY_URL,
28
+ } from "./extensions-dependency-gate.mjs";
29
+
30
+ /**
31
+ * @param {string[]} args the args AFTER `cinatra extensions submit`
32
+ */
33
+ export async function runExtensionsSubmit(args) {
34
+ const positional = args.filter((a) => !a.startsWith("--"));
35
+ const tarballPathArg = positional[0];
36
+ if (!tarballPathArg) {
37
+ throw new Error(
38
+ "Usage: cinatra extensions submit <tarball.tgz> [--description \"<short text>\"] [--skip-dependency-check]",
39
+ );
40
+ }
41
+ const tarballPath = resolvePath(process.cwd(), tarballPathArg);
42
+
43
+ const descriptionFlagIndex = args.indexOf("--description");
44
+ const description =
45
+ descriptionFlagIndex >= 0 && descriptionFlagIndex + 1 < args.length
46
+ ? args[descriptionFlagIndex + 1]
47
+ : undefined;
48
+
49
+ // Read tarball bytes from disk (the marketplace caps at 50 MiB — let the
50
+ // server reject oversize rather than duplicating the constant here).
51
+ let tarballBytes;
52
+ try {
53
+ tarballBytes = await readFile(tarballPath);
54
+ } catch (err) {
55
+ throw new Error(`Could not read tarball at ${tarballPath}: ${err instanceof Error ? err.message : String(err)}`);
56
+ }
57
+
58
+ // pacote.manifest('file:<path>') reads the package.json out of the .tgz
59
+ // without unpacking the whole tarball. The vendor doesn't need to pass the
60
+ // name/version flags — the tarball is the source of truth.
61
+ let manifest;
62
+ try {
63
+ manifest = await pacote.manifest(`file:${tarballPath}`);
64
+ } catch (err) {
65
+ throw new Error(`Could not parse the tarball's package.json: ${err instanceof Error ? err.message : String(err)}`);
66
+ }
67
+ if (typeof manifest.name !== "string" || !manifest.name.startsWith("@")) {
68
+ throw new Error(
69
+ `Tarball package name "${manifest.name}" is not scoped — extensions submitted to the marketplace MUST be scoped (@namespace/name).`,
70
+ );
71
+ }
72
+ const [namespace, extensionName] = (() => {
73
+ const idx = manifest.name.indexOf("/");
74
+ if (idx < 0) {
75
+ throw new Error(`Tarball package name "${manifest.name}" is missing the /<extension> suffix.`);
76
+ }
77
+ return [manifest.name.slice(0, idx), manifest.name.slice(idx + 1)];
78
+ })();
79
+ const version = String(manifest.version ?? "");
80
+ if (!version) {
81
+ throw new Error("Tarball package.json is missing a `version` field.");
82
+ }
83
+
84
+ // Dependency-ordering preflight: every @cinatra-ai/* EXTENSION EDGE this package
85
+ // declares in its canonical `cinatra.dependencies` MUST already be published on
86
+ // the registry (these extension packages live ONLY there). Host-internal SDK/app
87
+ // peers (sdk-extensions, sdk-ui, mcp-client, …) are NOT edges — they are
88
+ // host-provided under model-B and intentionally never on the registry, so the
89
+ // gate skips them. Fail BEFORE submit if a real edge is missing — submitting
90
+ // would produce a public repo that can't install a sibling extension it needs.
91
+ // The marketplace re-validates at approval; this is the fast local preflight. A
92
+ // zero-dep / host-internal-only package passes trivially (no probes).
93
+ const skipDependencyCheck = args.includes("--skip-dependency-check");
94
+ if (skipDependencyCheck) {
95
+ process.stderr.write(
96
+ "⚠ --skip-dependency-check: bypassing the @cinatra-ai/* dependency-ordering gate. " +
97
+ "Only safe if you have independently confirmed the closure is published.\n",
98
+ );
99
+ } else {
100
+ const registryUrl = (process.env.CINATRA_REGISTRY_URL || DEFAULT_REGISTRY_URL).trim();
101
+ // A read-scope registry token (distinct from the marketplace submit token);
102
+ // not required once the registry's public-read flip is live.
103
+ const registryToken = process.env.CINATRA_REGISTRY_TOKEN;
104
+ const report = await assertDependencyOrdering({ manifest, registryUrl, token: registryToken });
105
+ if (report.deps.length > 0) {
106
+ process.stderr.write(
107
+ `Dependency-ordering gate OK — ${report.satisfied.length}/${report.deps.length} @cinatra-ai/* dependency(ies) present on ${registryUrl}.\n`,
108
+ );
109
+ }
110
+ }
111
+
112
+ // Digest the raw bytes (the marketplace recomputes + verifies).
113
+ const artifactDigestSha256 = createHash("sha256").update(tarballBytes).digest("hex");
114
+ const artifactSizeBytes = tarballBytes.byteLength;
115
+ const tarballBase64 = tarballBytes.toString("base64");
116
+
117
+ process.stderr.write(
118
+ `Submitting ${manifest.name}@${version} (${artifactSizeBytes.toLocaleString()} bytes) to the Cinatra Marketplace…\n`,
119
+ );
120
+
121
+ const result = await callMarketplaceTool("extension_submit_for_review", {
122
+ namespace,
123
+ extension_name: extensionName,
124
+ version,
125
+ artifact_digest_sha256: artifactDigestSha256,
126
+ artifact_size_bytes: artifactSizeBytes,
127
+ tarball_base64: tarballBase64,
128
+ ...(description ? { description } : {}),
129
+ });
130
+
131
+ process.stdout.write(
132
+ `submission_id: ${result.submission_id}\n` +
133
+ `target: ${result.target_final_identity}\n` +
134
+ `status: ${result.status}\n` +
135
+ (result.idempotent_replay ? "(idempotent replay — same digest was already pending)\n" : ""),
136
+ );
137
+ }