showpane 0.4.9 → 0.4.11
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/bundle/meta/scaffold-manifest.json +17 -24
- package/bundle/scaffold/VERSION +1 -1
- package/bundle/scaffold/__dot__env.example +3 -5
- package/bundle/scaffold/package.json +2 -1
- package/bundle/scaffold/prisma/schema.local.prisma +1 -1
- package/bundle/scaffold/prisma/seed.ts +6 -1
- package/bundle/scaffold/prisma.config.ts +5 -0
- package/bundle/scaffold/scripts/prisma-schema.mjs +1 -53
- package/bundle/scaffold/src/__tests__/client-portals.test.ts +4 -37
- package/bundle/scaffold/src/__tests__/deploy-bundle.test.ts +48 -0
- package/bundle/scaffold/src/app/api/client-auth/route.ts +1 -1
- package/bundle/scaffold/src/lib/client-portals.ts +8 -13
- package/bundle/scaffold/src/lib/deploy-bundle.ts +106 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +33 -0
- package/bundle/scaffold/src/lib/runtime-state.ts +2 -32
- package/bundle/scaffold/src/types/adm-zip.d.ts +15 -0
- package/bundle/toolchain/VERSION +1 -1
- package/bundle/toolchain/bin/create-deploy-bundle.ts +3 -72
- package/bundle/toolchain/bin/ensure-cloud-project-link.ts +73 -0
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-create/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +3 -3
- package/bundle/toolchain/skills/portal-delete/SKILL.md +4 -4
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +32 -264
- package/bundle/toolchain/skills/portal-dev/SKILL.md +15 -13
- package/bundle/toolchain/skills/portal-list/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-preview/SKILL.md +9 -23
- package/bundle/toolchain/skills/portal-setup/SKILL.md +19 -28
- package/bundle/toolchain/skills/portal-share/SKILL.md +3 -4
- package/bundle/toolchain/skills/portal-status/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-update/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-verify/SKILL.md +21 -33
- package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +1 -5
- package/bundle/toolchain/skills/shared/preamble.md +2 -2
- package/dist/index.js +56 -10
- package/package.json +1 -1
- package/bundle/scaffold/docker/Caddyfile +0 -3
- package/bundle/scaffold/docker/Dockerfile +0 -30
- package/bundle/scaffold/docker-compose.yml +0 -53
- package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +0 -143
- package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +0 -6
- package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +0 -2
- package/bundle/scaffold/prisma/migrations/migration_lock.toml +0 -3
- package/bundle/scaffold/prisma/schema.prisma +0 -128
- package/bundle/scaffold/scripts/backup.sh +0 -19
- package/bundle/scaffold/scripts/e2e-verify.sh +0 -487
- package/bundle/scaffold/scripts/restore.sh +0 -31
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
4
|
-
"scaffoldVersion": "0.2.
|
|
3
|
+
"generatedAt": "2026-04-09T22:32:17.805Z",
|
|
4
|
+
"scaffoldVersion": "0.2.4",
|
|
5
5
|
"files": {
|
|
6
|
-
".env.example": "
|
|
6
|
+
".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
|
|
7
7
|
".gitignore": "998e5f43865ea56ac79a05acfd5d4b0d696f310bd5325a1ed458c3d40154d437",
|
|
8
|
-
"VERSION": "
|
|
9
|
-
"docker-compose.yml": "420fd123da019c22f03662933537e24779b4c2c91f90c23abfec5965cd0f35ce",
|
|
10
|
-
"docker/Caddyfile": "d9c58086986795f5b3e42ff9b5942e60b8df946a1a0c40351381616c0b4d2bed",
|
|
11
|
-
"docker/Dockerfile": "340470e3735ea53b2c03003a13a91361652291add33c40a2bf13e6af2a8cb73a",
|
|
8
|
+
"VERSION": "1725bfa6524c5265e7c171cf06568417d39b947fff49c242f03859479c82334b",
|
|
12
9
|
"next.config.ts": "cf27999cc274cce79bc4c8df11789807719abf40752b60e4b4967a3d2f0ed013",
|
|
13
10
|
"package-lock.json": "d8e30eb86f08e70787d4459a084b4ab2a9f119696bbd3146ec4ba5675fffd3c2",
|
|
14
|
-
"package.json": "
|
|
11
|
+
"package.json": "b095e17e7fc181c630e87fe9f473c5a4ef969afcd4b110f9f9c6d6a6d93f1c0b",
|
|
15
12
|
"postcss.config.js": "fa650b380adfabb151a0b352f7135e107e6352345f899060f1c5c231228f94bf",
|
|
16
|
-
"prisma
|
|
17
|
-
"prisma/
|
|
18
|
-
"prisma/
|
|
19
|
-
"prisma/migrations/migration_lock.toml": "99836963713b4f5b269ad49af0ed3d7b0b2e336115c2f92dc9ac683d139d0900",
|
|
20
|
-
"prisma/schema.local.prisma": "4b36b52ef0ccfd5980f05ab97e821001a0f4bb658f13d693a6e3422233399eb8",
|
|
21
|
-
"prisma/schema.prisma": "904e517e8956e1b61534d18db6a27c104008ec1d57d474e22e67946c9a1f60ca",
|
|
22
|
-
"prisma/seed.ts": "1dea3de4b36263e6172817508e113ba416fbcd8caff80bc190f35cd3b4b3e2c5",
|
|
13
|
+
"prisma.config.ts": "36f56fd74eae70632e484443e38d08665158d72d5c978dc456651d8d5e1a636e",
|
|
14
|
+
"prisma/schema.local.prisma": "f5d6f3cb17d6d229f46ef82eef7c0ff4261596924f0173fef075ac394f423073",
|
|
15
|
+
"prisma/seed.ts": "398b645c31ea0d5b0291f27c32aded22bdb64021e581a547e85b3cccca65c551",
|
|
23
16
|
"public/example-avatar.svg": "0edeb0d3fbefa89cc27ffe6564d20e3ee0fd073cb6d9f2a025248ef3b3f277fd",
|
|
24
17
|
"public/example-logo.svg": "bc5cd933aff2a17698dee66a7b4ea940ad12238e9d813474d643b459b1e8d6da",
|
|
25
18
|
"public/robots.txt": "331ea9090db0c9f6f597bd9840fd5b171830f6e0b3ba1cb24dfa91f0c95aedc1",
|
|
26
|
-
"scripts/backup.sh": "7d94758c5f1b0d3a722ec8a1a0b819f1a5f480b762790cf4eeccdd6fda46d11b",
|
|
27
|
-
"scripts/e2e-verify.sh": "48c5983b8f2facc228bb4ea1ac0998064268f0fdb0088208dcb12bb1e48c3244",
|
|
28
19
|
"scripts/prisma-db-push.mjs": "76ac85fe65b5dc3d9cc7432e44618fcc84b7443574c8d88198d01f13ac23c040",
|
|
29
20
|
"scripts/prisma-generate.mjs": "d371e63388fa39f963b7c3c7cb8f87e0d9cd43cbf69d254b999108e29b8738c8",
|
|
30
|
-
"scripts/prisma-schema.mjs": "
|
|
31
|
-
"
|
|
32
|
-
"src/__tests__/
|
|
21
|
+
"scripts/prisma-schema.mjs": "0a86cc1b5f84120948aed8f97a84f2d5b173f91a43ea34ad6767441894121d83",
|
|
22
|
+
"src/__tests__/client-portals.test.ts": "fe8e491e62fb2a84de52cdc1154d1451083f93bbccf1c5e65b42810d007eecc2",
|
|
23
|
+
"src/__tests__/deploy-bundle.test.ts": "abd3216170f306c09df6abb0d2afad966a5741e8859f25a310a0a09693d37609",
|
|
33
24
|
"src/__tests__/portal-contracts.test.ts": "80066377d3281786c2bb9ecc857514124e094a2e66dca2fb08ded994c25fa2bc",
|
|
34
25
|
"src/app/(portal)/client/[slug]/page.tsx": "4f2f9253b2ad5d37a0f13759db52c786ae9c401f50fae9431da1417e9736e000",
|
|
35
26
|
"src/app/(portal)/client/[slug]/s/[token]/route.ts": "a445e54b9139e40dfe0bc039e34e6af224a27f75a614741ab224d317ad4d3ec9",
|
|
@@ -37,7 +28,7 @@
|
|
|
37
28
|
"src/app/(portal)/client/example/page.tsx": "f330864f63c9feea76c8a62c3eba3ce57578627e0d4abd929fd7fefdfc7af058",
|
|
38
29
|
"src/app/(portal)/client/layout.tsx": "4f43871510408a81da229d48ae316ec1d1c1beda93121922246300a2c8fd0999",
|
|
39
30
|
"src/app/(portal)/client/page.tsx": "af36f1a6f359d6a7bd4a6ac550058c9d9c107e9885bb238b4c06ec26700e23e3",
|
|
40
|
-
"src/app/api/client-auth/route.ts": "
|
|
31
|
+
"src/app/api/client-auth/route.ts": "ce1858559b1e944d5b1dc719d1f03bebf66286671700b1b5397382109f0f1e0d",
|
|
41
32
|
"src/app/api/client-auth/share/route.ts": "ed82414212dcd26af8c6c0f2bd44d9d79a727ed35cfedbac8c4077a6220aad14",
|
|
42
33
|
"src/app/api/client-events/route.ts": "13d545537b7e8ce421e6169d25c105adf2a2de3d978ae0a2c6751ff5f7d2eb33",
|
|
43
34
|
"src/app/api/client-files/[...path]/route.ts": "d6279a8eab48aa82d07ac8d467276968898c0d426eeec0a7d1fba1fa754ef405",
|
|
@@ -54,18 +45,20 @@
|
|
|
54
45
|
"src/lib/abuse-controls.ts": "d79d58d93267aca48ad0b7b9b91f753c9a3c27263e4e98daf768a950c44a6fc6",
|
|
55
46
|
"src/lib/branding.ts": "cc55f40e02bc3e486b227988f95739ca1cda8012c97b591295995eb4465efd57",
|
|
56
47
|
"src/lib/client-auth.ts": "b9bdfe77dbe5d6ec6c6a930627fc43d3253f0d76fd8fc4093af5a75742bebe42",
|
|
57
|
-
"src/lib/client-portals.ts": "
|
|
48
|
+
"src/lib/client-portals.ts": "9b531f9a9ea459b4ab85257b9dd282874fa1422838fe89d511940e417114216a",
|
|
58
49
|
"src/lib/control-plane.ts": "e0cf39f28ec7de715fd5cfbb5f4240773fcd3d775cd1677588dd749fff740a0e",
|
|
59
50
|
"src/lib/db.ts": "65fb87fde5a05fa033234f3a976f6730d99fab7982a83821c792d09fc659e439",
|
|
51
|
+
"src/lib/deploy-bundle.ts": "e9675cccb2c802e408639481986c6b629737541853e1c93f322c08a5b9dfc5f9",
|
|
60
52
|
"src/lib/files.ts": "24fd8d1d53c180d62441019395fb140ba3baa28311918ac488284adcdda8eb9a",
|
|
61
53
|
"src/lib/load-app-env.ts": "78b80e17d896885f0d72315ee9a6cf7a0a8c6c08171f26e3d599bb9b2e8afeee",
|
|
62
|
-
"src/lib/portal-contracts.ts": "
|
|
54
|
+
"src/lib/portal-contracts.ts": "519a97afe3ae618077c95aba5a9764383e7edf6c9effd62a5638a50a0b2a676a",
|
|
63
55
|
"src/lib/prisma-client.ts": "28cd100129a0178a6c8fdfe49e6997b19983fcc427b9fa7caee3ac26226e5eb3",
|
|
64
|
-
"src/lib/runtime-state.ts": "
|
|
56
|
+
"src/lib/runtime-state.ts": "3d30de7dfeaaa48d8b6fd5d29976ecd001408172100c95b063d5d804fdce0a2e",
|
|
65
57
|
"src/lib/storage.ts": "ae3b85fc6cccd39d4174a391dcbe6e91fb9460eb407ec9dbfedd63594a441d08",
|
|
66
58
|
"src/lib/token.ts": "518898ca3cbba069f507f736ed80f5afa0c5af07b0b02fdd2d682e466598c803",
|
|
67
59
|
"src/lib/utils.ts": "d1f1e0d62cb8d8d1e04c26e14de842d8a151f75812d81b046c65b5d1fe8e4b27",
|
|
68
60
|
"src/middleware.ts": "5623ef170d9327e47373943eae33a8b07a6dda970525d0833e0865e4429df094",
|
|
61
|
+
"src/types/adm-zip.d.ts": "a9a32ea84d6d6cd89626ba5cd6f5519158a652362abbe5647474114c30ecc3c4",
|
|
69
62
|
"tailwind.config.ts": "dc0dec22249b290b857190495ce7140327a6f4d94fedd5dffcdb298fa3928071",
|
|
70
63
|
"tests/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
71
64
|
"tsconfig.json": "1b12f56ab16430b64622ff8512cb05b06cf633060dbec6ebad6f9e792a6dc2a0",
|
package/bundle/scaffold/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.4
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# Database
|
|
2
2
|
# Local development (SQLite, zero config):
|
|
3
|
+
# Prisma commands auto-select the matching schema from this value.
|
|
3
4
|
DATABASE_URL="file:./dev.db"
|
|
4
|
-
# Production (PostgreSQL):
|
|
5
|
-
# DATABASE_URL="postgresql://portal:change-me-in-.env@db:5432/portal"
|
|
6
|
-
# POSTGRES_USER="portal"
|
|
7
|
-
# POSTGRES_PASSWORD="change-me-in-.env"
|
|
8
|
-
# POSTGRES_DB="portal"
|
|
9
5
|
|
|
10
6
|
# Auth — generate with: openssl rand -base64 32
|
|
11
7
|
AUTH_SECRET=""
|
|
@@ -22,3 +18,5 @@ STORAGE_PROVIDER="local"
|
|
|
22
18
|
|
|
23
19
|
# Public URL (used for share links)
|
|
24
20
|
# NEXT_PUBLIC_APP_URL="https://portal.example.com"
|
|
21
|
+
|
|
22
|
+
# Showpane Cloud publish builds a prebuilt artifact locally during /portal deploy.
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
"name": "showpane-portal",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
|
+
"packageManager": "npm@10.9.2",
|
|
5
6
|
"scripts": {
|
|
6
7
|
"dev": "next dev",
|
|
7
8
|
"prisma:generate": "node scripts/prisma-generate.mjs",
|
|
8
9
|
"prisma:db-push": "node scripts/prisma-db-push.mjs",
|
|
10
|
+
"cloud:build": "npm run prisma:generate && npx --yes vercel build --prod",
|
|
9
11
|
"build": "npm run prisma:generate && next build",
|
|
10
12
|
"start": "next start",
|
|
11
13
|
"postinstall": "npm run prisma:generate",
|
|
12
|
-
"db:migrate": "prisma migrate deploy",
|
|
13
14
|
"db:push": "npm run prisma:db-push",
|
|
14
15
|
"db:seed": "npx tsx prisma/seed.ts",
|
|
15
16
|
"test": "vitest"
|
|
@@ -27,7 +27,12 @@ async function main() {
|
|
|
27
27
|
slug: "example",
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
|
-
update: {
|
|
30
|
+
update: {
|
|
31
|
+
companyName: "Acme Health",
|
|
32
|
+
username: "example",
|
|
33
|
+
passwordHash,
|
|
34
|
+
lastUpdated: "2 April 2026",
|
|
35
|
+
},
|
|
31
36
|
create: {
|
|
32
37
|
organizationId: org.id,
|
|
33
38
|
slug: "example",
|
|
@@ -1,64 +1,12 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
|
|
6
5
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
6
|
const appRoot = path.resolve(scriptDir, "..");
|
|
8
7
|
|
|
9
|
-
function parseEnvValue(rawValue) {
|
|
10
|
-
const value = rawValue.trim();
|
|
11
|
-
if (
|
|
12
|
-
(value.startsWith("\"") && value.endsWith("\"")) ||
|
|
13
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
14
|
-
) {
|
|
15
|
-
return value.slice(1, -1);
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseEnvFile(filePath) {
|
|
21
|
-
if (!fs.existsSync(filePath)) return {};
|
|
22
|
-
|
|
23
|
-
const env = {};
|
|
24
|
-
for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
25
|
-
const trimmed = line.trim();
|
|
26
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
27
|
-
|
|
28
|
-
const separator = trimmed.indexOf("=");
|
|
29
|
-
if (separator === -1) continue;
|
|
30
|
-
|
|
31
|
-
const key = trimmed.slice(0, separator).trim();
|
|
32
|
-
const value = trimmed.slice(separator + 1);
|
|
33
|
-
env[key] = parseEnvValue(value);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return env;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getDatabaseUrl() {
|
|
40
|
-
if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
|
|
41
|
-
|
|
42
|
-
const envPaths = [
|
|
43
|
-
path.join(appRoot, ".env"),
|
|
44
|
-
path.join(appRoot, ".env.local"),
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const merged = {};
|
|
48
|
-
for (const envPath of envPaths) {
|
|
49
|
-
Object.assign(merged, parseEnvFile(envPath));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return merged.DATABASE_URL ?? null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
8
|
export function getSchemaPath() {
|
|
56
|
-
|
|
57
|
-
const schemaName = databaseUrl?.startsWith("file:")
|
|
58
|
-
? "schema.local.prisma"
|
|
59
|
-
: "schema.prisma";
|
|
60
|
-
|
|
61
|
-
return path.join(appRoot, "prisma", schemaName);
|
|
9
|
+
return path.join(appRoot, "prisma", "schema.local.prisma");
|
|
62
10
|
}
|
|
63
11
|
|
|
64
12
|
export function runPrismaCommand(args) {
|
|
@@ -21,56 +21,23 @@ const mockedPrisma = vi.mocked(prisma);
|
|
|
21
21
|
describe("resolveDefaultOrganizationId", () => {
|
|
22
22
|
beforeEach(() => {
|
|
23
23
|
vi.resetAllMocks();
|
|
24
|
-
delete process.env.ORG_ID;
|
|
25
24
|
});
|
|
26
25
|
|
|
27
|
-
it("
|
|
28
|
-
process.env.ORG_ID = "cloud-org-123";
|
|
29
|
-
mockedPrisma.organization.findUnique.mockResolvedValue({
|
|
30
|
-
id: "cloud-org-123",
|
|
31
|
-
} as never);
|
|
32
|
-
|
|
33
|
-
const result = await resolveDefaultOrganizationId();
|
|
34
|
-
|
|
35
|
-
expect(result).toBe("cloud-org-123");
|
|
36
|
-
expect(mockedPrisma.organization.findUnique).toHaveBeenCalledWith({
|
|
37
|
-
where: { id: "cloud-org-123" },
|
|
38
|
-
select: { id: true },
|
|
39
|
-
});
|
|
40
|
-
expect(mockedPrisma.organization.findFirst).not.toHaveBeenCalled();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns null when ORG_ID is set but org not in DB", async () => {
|
|
44
|
-
process.env.ORG_ID = "nonexistent-org";
|
|
45
|
-
mockedPrisma.organization.findUnique.mockResolvedValue(null);
|
|
46
|
-
|
|
47
|
-
const result = await resolveDefaultOrganizationId();
|
|
48
|
-
|
|
49
|
-
expect(result).toBeNull();
|
|
50
|
-
expect(mockedPrisma.organization.findUnique).toHaveBeenCalledWith({
|
|
51
|
-
where: { id: "nonexistent-org" },
|
|
52
|
-
select: { id: true },
|
|
53
|
-
});
|
|
54
|
-
expect(mockedPrisma.organization.findFirst).not.toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("falls back to first org in DB when ORG_ID not set", async () => {
|
|
58
|
-
// ORG_ID is not set (deleted in beforeEach)
|
|
26
|
+
it("falls back to first org in DB", async () => {
|
|
59
27
|
mockedPrisma.organization.findFirst.mockResolvedValue({
|
|
60
|
-
id: "
|
|
28
|
+
id: "local-org-1",
|
|
61
29
|
} as never);
|
|
62
30
|
|
|
63
31
|
const result = await resolveDefaultOrganizationId();
|
|
64
32
|
|
|
65
|
-
expect(result).toBe("
|
|
33
|
+
expect(result).toBe("local-org-1");
|
|
66
34
|
expect(mockedPrisma.organization.findFirst).toHaveBeenCalledWith({
|
|
67
35
|
select: { id: true },
|
|
68
36
|
orderBy: { createdAt: "asc" },
|
|
69
37
|
});
|
|
70
|
-
expect(mockedPrisma.organization.findUnique).not.toHaveBeenCalled();
|
|
71
38
|
});
|
|
72
39
|
|
|
73
|
-
it("returns null when
|
|
40
|
+
it("returns null when no orgs exist in DB", async () => {
|
|
74
41
|
mockedPrisma.organization.findFirst.mockResolvedValue(null);
|
|
75
42
|
|
|
76
43
|
const result = await resolveDefaultOrganizationId();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import AdmZip from "adm-zip";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { createDeployBundle } from "@/lib/deploy-bundle";
|
|
13
|
+
|
|
14
|
+
const tempDirs: string[] = [];
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const dir of tempDirs.splice(0)) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("createDeployBundle", () => {
|
|
23
|
+
it("materializes symlinked function aliases into the artifact", () => {
|
|
24
|
+
const appRoot = mkdtempSync(path.join(os.tmpdir(), "showpane-bundle-"));
|
|
25
|
+
tempDirs.push(appRoot);
|
|
26
|
+
|
|
27
|
+
const outputRoot = path.join(appRoot, ".vercel", "output");
|
|
28
|
+
const realFuncDir = path.join(outputRoot, "functions", "real.func");
|
|
29
|
+
mkdirSync(realFuncDir, { recursive: true });
|
|
30
|
+
writeFileSync(path.join(outputRoot, "config.json"), JSON.stringify({ version: 3 }));
|
|
31
|
+
writeFileSync(path.join(realFuncDir, ".vc-config.json"), JSON.stringify({ handler: "index.js" }));
|
|
32
|
+
writeFileSync(path.join(realFuncDir, "index.js"), "module.exports = {};\n");
|
|
33
|
+
|
|
34
|
+
const aliasDir = path.join(outputRoot, "functions", "alias.func");
|
|
35
|
+
symlinkSync("real.func", aliasDir);
|
|
36
|
+
|
|
37
|
+
const artifactPath = path.join(appRoot, "artifact.zip");
|
|
38
|
+
const result = createDeployBundle(appRoot, artifactPath);
|
|
39
|
+
expect(result.fileCount).toBe(5);
|
|
40
|
+
|
|
41
|
+
const names = new Set(new AdmZip(artifactPath).getEntries().map((entry) => entry.entryName));
|
|
42
|
+
expect(names.has(".vercel/output/config.json")).toBe(true);
|
|
43
|
+
expect(names.has(".vercel/output/functions/real.func/.vc-config.json")).toBe(true);
|
|
44
|
+
expect(names.has(".vercel/output/functions/real.func/index.js")).toBe(true);
|
|
45
|
+
expect(names.has(".vercel/output/functions/alias.func/.vc-config.json")).toBe(true);
|
|
46
|
+
expect(names.has(".vercel/output/functions/alias.func/index.js")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -60,7 +60,7 @@ export async function POST(req: NextRequest) {
|
|
|
60
60
|
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// Resolve organization —
|
|
63
|
+
// Resolve organization — local uses the first org, hosted runtime uses runtime-state
|
|
64
64
|
const orgId = await resolveDefaultOrganizationId();
|
|
65
65
|
if (!orgId) {
|
|
66
66
|
return NextResponse.json({ error: "No organization configured" }, { status: 503 });
|
|
@@ -77,6 +77,10 @@ export async function getClientPortalId(
|
|
|
77
77
|
organizationId: string,
|
|
78
78
|
slug: string
|
|
79
79
|
): Promise<string | null> {
|
|
80
|
+
if (isRuntimeSnapshotMode()) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
const portal = await prisma.clientPortal.findFirst({
|
|
81
85
|
where: { organizationId, slug, isActive: true },
|
|
82
86
|
select: { id: true },
|
|
@@ -108,24 +112,15 @@ export async function getCredentialVersion(
|
|
|
108
112
|
|
|
109
113
|
/**
|
|
110
114
|
* Resolve the organizationId for the current request context.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
115
|
+
* Hosted runtime reads it from the runtime snapshot.
|
|
116
|
+
* Local workspace returns the first org from the SQLite database.
|
|
113
117
|
*/
|
|
114
118
|
export async function resolveDefaultOrganizationId(): Promise<string | null> {
|
|
115
119
|
if (isRuntimeSnapshotMode()) {
|
|
116
120
|
const state = await getRuntimeState();
|
|
117
|
-
return state?.organization.id ??
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Cloud: each Vercel project has ORG_ID set during provisioning
|
|
121
|
-
if (process.env.ORG_ID) {
|
|
122
|
-
const org = await prisma.organization.findUnique({
|
|
123
|
-
where: { id: process.env.ORG_ID },
|
|
124
|
-
select: { id: true },
|
|
125
|
-
});
|
|
126
|
-
return org?.id ?? null;
|
|
121
|
+
return state?.organization.id ?? null;
|
|
127
122
|
}
|
|
128
|
-
//
|
|
123
|
+
// Local workspace: use the first org in the DB
|
|
129
124
|
const org = await prisma.organization.findFirst({
|
|
130
125
|
select: { id: true },
|
|
131
126
|
orderBy: { createdAt: "asc" },
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import AdmZip from "adm-zip";
|
|
2
|
+
import {
|
|
3
|
+
lstatSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
statSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
function walkFiles(dir: string, root: string, out: Set<string>) {
|
|
12
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
13
|
+
const fullPath = path.join(dir, entry.name);
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
walkFiles(fullPath, root, out);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
out.add(path.relative(root, fullPath));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function collectTracedFiles(appPath: string): Set<string> {
|
|
23
|
+
const files = new Set<string>();
|
|
24
|
+
const outputRoot = path.join(appPath, ".vercel", "output");
|
|
25
|
+
const functionsRoot = path.join(outputRoot, "functions");
|
|
26
|
+
|
|
27
|
+
walkFiles(outputRoot, appPath, files);
|
|
28
|
+
|
|
29
|
+
const queue = [functionsRoot];
|
|
30
|
+
while (queue.length > 0) {
|
|
31
|
+
const current = queue.pop();
|
|
32
|
+
if (!current) continue;
|
|
33
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
34
|
+
const fullPath = path.join(current, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
queue.push(fullPath);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (entry.name !== ".vc-config.json") continue;
|
|
40
|
+
|
|
41
|
+
const config = JSON.parse(readFileSync(fullPath, "utf8")) as {
|
|
42
|
+
filePathMap?: Record<string, string>;
|
|
43
|
+
};
|
|
44
|
+
for (const relativePath of Object.values(config.filePathMap ?? {})) {
|
|
45
|
+
files.add(relativePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addPathToZip(zip: AdmZip, sourcePath: string, zipPath: string): number {
|
|
54
|
+
const zipPathPosix = zipPath.replace(/\\/g, "/");
|
|
55
|
+
|
|
56
|
+
if (zipPathPosix === ".env" || zipPathPosix.startsWith(".env.")) {
|
|
57
|
+
zip.addFile(zipPathPosix, Buffer.from("NODE_ENV=production\n"));
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const entry = lstatSync(sourcePath, { throwIfNoEntry: false });
|
|
62
|
+
if (!entry) {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (entry.isSymbolicLink()) {
|
|
67
|
+
return addPathToZip(zip, realpathSync(sourcePath), zipPathPosix);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
let count = 0;
|
|
72
|
+
for (const child of readdirSync(sourcePath, { withFileTypes: true })) {
|
|
73
|
+
count += addPathToZip(
|
|
74
|
+
zip,
|
|
75
|
+
path.join(sourcePath, child.name),
|
|
76
|
+
path.posix.join(zipPathPosix, child.name),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return count;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!entry.isFile() && !statSync(sourcePath, { throwIfNoEntry: false })?.isFile()) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
zip.addLocalFile(sourcePath, path.posix.dirname(zipPathPosix), path.posix.basename(zipPathPosix));
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createDeployBundle(appPath: string, outputPath: string): { fileCount: number } {
|
|
91
|
+
const outputRoot = path.join(appPath, ".vercel", "output");
|
|
92
|
+
if (!statSync(outputRoot, { throwIfNoEntry: false })?.isDirectory()) {
|
|
93
|
+
throw new Error("Missing .vercel/output. Run `npm run cloud:build` first.");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const zip = new AdmZip();
|
|
97
|
+
const tracedFiles = collectTracedFiles(appPath);
|
|
98
|
+
let fileCount = 0;
|
|
99
|
+
|
|
100
|
+
for (const relativePath of tracedFiles) {
|
|
101
|
+
fileCount += addPathToZip(zip, path.join(appPath, relativePath), relativePath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
zip.writeZip(outputPath);
|
|
105
|
+
return { fileCount };
|
|
106
|
+
}
|
|
@@ -49,6 +49,39 @@ export interface PortalFileSyncManifestPayload {
|
|
|
49
49
|
files: PortalFileSyncManifestEntry[];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface RuntimePortalSnapshot {
|
|
53
|
+
slug: string;
|
|
54
|
+
companyName: string;
|
|
55
|
+
logoUrl?: string | null;
|
|
56
|
+
username: string;
|
|
57
|
+
passwordHash: string;
|
|
58
|
+
credentialVersion: string;
|
|
59
|
+
isActive: boolean;
|
|
60
|
+
lastUpdated?: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RuntimeOrganizationSnapshot {
|
|
64
|
+
id: string;
|
|
65
|
+
slug: string;
|
|
66
|
+
name: string;
|
|
67
|
+
logoUrl?: string | null;
|
|
68
|
+
primaryColor?: string;
|
|
69
|
+
portalLabel?: string;
|
|
70
|
+
websiteUrl?: string | null;
|
|
71
|
+
contactName?: string | null;
|
|
72
|
+
contactTitle?: string | null;
|
|
73
|
+
contactEmail?: string | null;
|
|
74
|
+
contactPhone?: string | null;
|
|
75
|
+
contactAvatar?: string | null;
|
|
76
|
+
supportEmail?: string | null;
|
|
77
|
+
customDomain?: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RuntimeStatePayload {
|
|
81
|
+
organization: RuntimeOrganizationSnapshot;
|
|
82
|
+
portals: RuntimePortalSnapshot[];
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
const PORTAL_EVENT_TYPE_SET = new Set<string>(PORTAL_EVENT_TYPES);
|
|
53
86
|
|
|
54
87
|
export function isPortalEventType(value: unknown): value is PortalEventType {
|
|
@@ -1,38 +1,8 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import type { RuntimeStatePayload } from "@/lib/portal-contracts";
|
|
3
4
|
|
|
4
|
-
export type
|
|
5
|
-
slug: string;
|
|
6
|
-
companyName: string;
|
|
7
|
-
logoUrl?: string | null;
|
|
8
|
-
username: string;
|
|
9
|
-
passwordHash: string;
|
|
10
|
-
credentialVersion: string;
|
|
11
|
-
isActive: boolean;
|
|
12
|
-
lastUpdated?: string | null;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type RuntimeOrganizationSnapshot = {
|
|
16
|
-
id: string;
|
|
17
|
-
slug: string;
|
|
18
|
-
name: string;
|
|
19
|
-
logoUrl?: string | null;
|
|
20
|
-
primaryColor?: string;
|
|
21
|
-
portalLabel?: string;
|
|
22
|
-
websiteUrl?: string | null;
|
|
23
|
-
contactName?: string | null;
|
|
24
|
-
contactTitle?: string | null;
|
|
25
|
-
contactEmail?: string | null;
|
|
26
|
-
contactPhone?: string | null;
|
|
27
|
-
contactAvatar?: string | null;
|
|
28
|
-
supportEmail?: string | null;
|
|
29
|
-
customDomain?: string | null;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type RuntimeState = {
|
|
33
|
-
organization: RuntimeOrganizationSnapshot;
|
|
34
|
-
portals: RuntimePortalSnapshot[];
|
|
35
|
-
};
|
|
5
|
+
export type RuntimeState = RuntimeStatePayload;
|
|
36
6
|
|
|
37
7
|
let cachedState: RuntimeState | null | undefined;
|
|
38
8
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare module "adm-zip" {
|
|
2
|
+
export type AdmZipEntry = {
|
|
3
|
+
entryName: string;
|
|
4
|
+
isDirectory: boolean;
|
|
5
|
+
getData(): Buffer;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default class AdmZip {
|
|
9
|
+
constructor(path?: string);
|
|
10
|
+
addFile(entryName: string, content: Buffer): void;
|
|
11
|
+
addLocalFile(filePath: string, zipPath?: string, zipName?: string): void;
|
|
12
|
+
getEntries(): AdmZipEntry[];
|
|
13
|
+
writeZip(targetFileName: string): void;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/bundle/toolchain/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.4 (requires app >= 0.2.4)
|