初回コミット: AltStore PAL向けADPホスティングSaaS
- Fastify API・Node Worker・PostgreSQL・S3互換ストレージ構成
- ADP ZIPのマルチパートアップロードとManifest検証(パストラバーサル・ZIP爆弾等を拒否)
- manifest.json/signatureは再シリアライズせず元バイト列を保持
- Cloudflare風の運用向け管理ダッシュボード(shadcn/Radix・日本語UI・4ルート)
概要/Sources/アプリ/リリース + ルートランディングページ
- 汎用OIDC SSO・Workspace単位の認可・demo mode
- 匿名配布: /sources/{slug}/source.json, /artifacts/{releaseId}/manifest.json
- Docker Compose対応
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import test from "node:test";
|
||||
|
||||
process.env.AUTH_MODE = "demo";
|
||||
process.env.STORAGE_MODE = "local";
|
||||
process.env.PUBLIC_BASE_URL = "http://localhost:4000";
|
||||
process.env.WEB_ORIGIN = "http://localhost:3000";
|
||||
process.env.LOCAL_STORAGE_DIR = ".data/test-storage";
|
||||
process.env.PROCESS_INLINE = "true";
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const { createServer } = await import("../apps/api/src/server.ts");
|
||||
const runtime = await createServer();
|
||||
const app = runtime.app;
|
||||
|
||||
test.after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test("creates a source and returns a dashboard in demo mode", async () => {
|
||||
const sourceResponse = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/sources",
|
||||
headers: { "x-demo-user": "test@example.com", "content-type": "application/json" },
|
||||
payload: { name: "Test Source", subtitle: "Test apps", visibility: "draft" },
|
||||
});
|
||||
assert.equal(sourceResponse.statusCode, 201);
|
||||
const source = sourceResponse.json().source;
|
||||
assert.equal(source.visibility, "draft");
|
||||
|
||||
const dashboardResponse = await app.inject({ method: "GET", url: "/api/v1/dashboard", headers: { "x-demo-user": "test@example.com" } });
|
||||
assert.equal(dashboardResponse.statusCode, 200);
|
||||
const dashboard = dashboardResponse.json();
|
||||
assert.ok(dashboard.sources.some((entry) => entry.source.id === source.id));
|
||||
});
|
||||
|
||||
test("does not expose draft sources publicly", async () => {
|
||||
const response = await app.inject({ method: "GET", url: "/sources/test-source/source.json" });
|
||||
assert.equal(response.statusCode, 404);
|
||||
});
|
||||
|
||||
test("validates, stores, publishes, and serves an ADP package", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "altdock-test-adp-"));
|
||||
const fixtureRoot = join(root, "fixture");
|
||||
await (await import("node:fs/promises")).mkdir(join(fixtureRoot, "variant"), { recursive: true });
|
||||
const manifest = Buffer.from(JSON.stringify({
|
||||
distributionPackageRevision: 1,
|
||||
appleItemId: "123456789",
|
||||
bundleId: "com.example.fixture",
|
||||
shortVersionString: "1.0",
|
||||
bundleVersion: "1",
|
||||
platforms: ["ios"],
|
||||
minimumSystemVersions: { ios: "17.4" },
|
||||
variants: [{ assetPath: "variant/fixture.ipa", installTargets: [] }],
|
||||
deltas: [],
|
||||
}));
|
||||
const signature = Buffer.from("signed-fixture");
|
||||
const ipa = Buffer.from("not-an-installable-ipa-fixture");
|
||||
await writeFile(join(fixtureRoot, "manifest.json"), manifest);
|
||||
await writeFile(join(fixtureRoot, "signature"), signature);
|
||||
await writeFile(join(fixtureRoot, "variant/fixture.ipa"), ipa);
|
||||
const archivePath = join(root, "fixture.zip");
|
||||
await promisify(execFile)("zip", ["-q", "-r", archivePath, "manifest.json", "signature", "variant"], { cwd: fixtureRoot });
|
||||
const archive = await readFile(archivePath);
|
||||
const sourceResponse = await app.inject({ method: "POST", url: "/api/v1/sources", headers: { "x-demo-user": "fixture@example.com", "content-type": "application/json" }, payload: { name: "Fixture Source", visibility: "public" } });
|
||||
const source = sourceResponse.json().source;
|
||||
const uploadResponse = await app.inject({ method: "POST", url: "/api/v1/uploads", headers: { "x-demo-user": "fixture@example.com", "content-type": "application/json" }, payload: { sourceId: source.id, filename: "fixture.zip", sizeBytes: archive.length } });
|
||||
assert.equal(uploadResponse.statusCode, 201);
|
||||
const upload = uploadResponse.json();
|
||||
const putResponse = await app.inject({ method: "PUT", url: new URL(upload.uploadPlan.uploadUrl).pathname, headers: { "content-type": "application/zip" }, payload: archive });
|
||||
assert.equal(putResponse.statusCode, 200, putResponse.body);
|
||||
const completeResponse = await app.inject({ method: "POST", url: `/api/v1/uploads/${upload.upload.id}/complete`, headers: { "x-demo-user": "fixture@example.com", "content-type": "application/json" }, payload: {} });
|
||||
assert.equal(completeResponse.statusCode, 202, completeResponse.body);
|
||||
const release = completeResponse.json().release;
|
||||
assert.equal(release.status, "ready");
|
||||
const publishResponse = await app.inject({ method: "POST", url: `/api/v1/releases/${release.id}/publish`, headers: { "x-demo-user": "fixture@example.com" } });
|
||||
assert.equal(publishResponse.statusCode, 200, publishResponse.body);
|
||||
const sourceJsonResponse = await app.inject({ method: "GET", url: `/sources/${source.slug}/source.json` });
|
||||
assert.equal(sourceJsonResponse.statusCode, 200, sourceJsonResponse.body);
|
||||
const sourceJson = sourceJsonResponse.json();
|
||||
assert.equal(sourceJson.apps[0].bundleIdentifier, "com.example.fixture");
|
||||
assert.match(sourceJson.apps[0].versions[0].downloadURL, new RegExp(`/artifacts/${release.id}/manifest\\.json$`));
|
||||
const artifactResponse = await app.inject({ method: "GET", url: `/artifacts/${release.id}/manifest.json` });
|
||||
assert.equal(artifactResponse.statusCode, 200, artifactResponse.body);
|
||||
assert.deepEqual(artifactResponse.rawPayload, manifest);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { buildSourceDocument } from "../packages/core/src/index.ts";
|
||||
|
||||
test("builds a PAL source with published releases only", () => {
|
||||
const source = {
|
||||
id: "source-1",
|
||||
workspaceId: "workspace-1",
|
||||
slug: "my-apps",
|
||||
name: "My Apps",
|
||||
subtitle: "Small iOS experiments",
|
||||
description: "A test source",
|
||||
tintColor: "#E9694B",
|
||||
visibility: "public",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
const app = {
|
||||
id: "app-1",
|
||||
sourceId: "source-1",
|
||||
name: "Orbit",
|
||||
bundleIdentifier: "com.example.orbit",
|
||||
marketplaceID: "123",
|
||||
developerName: "Example",
|
||||
subtitle: "",
|
||||
localizedDescription: "An orbit app",
|
||||
tintColor: "#E9694B",
|
||||
category: "utilities",
|
||||
screenshots: [],
|
||||
appPermissions: { entitlements: [], privacy: {} },
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
const published = {
|
||||
id: "release-1",
|
||||
appId: "app-1",
|
||||
uploadId: "upload-1",
|
||||
version: "1.0",
|
||||
buildVersion: "1",
|
||||
appleItemId: "123",
|
||||
date: "2026-02-01T00:00:00.000Z",
|
||||
localizedDescription: "First release",
|
||||
sizeBytes: 1024,
|
||||
status: "published",
|
||||
manifest: { bundleId: "com.example.orbit", appleItemId: "123", shortVersionString: "1.0", bundleVersion: "1", variantPaths: [], deltaPaths: [], platforms: [], minimumSystemVersions: {} },
|
||||
createdAt: "2026-02-01T00:00:00.000Z",
|
||||
};
|
||||
const draft = { ...published, id: "release-2", version: "2.0", status: "ready" };
|
||||
const document = buildSourceDocument(source, [app], [published, draft], "https://example.test");
|
||||
assert.equal(document.apps.length, 1);
|
||||
assert.equal(document.apps[0].versions.length, 1);
|
||||
assert.equal(document.apps[0].versions[0].downloadURL, "https://example.test/artifacts/release-1/manifest.json");
|
||||
assert.deepEqual(document.featuredApps, ["com.example.orbit"]);
|
||||
});
|
||||
Reference in New Issue
Block a user