@malloy-publisher/server 0.0.194 → 0.0.195
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/dist/server.mjs
CHANGED
|
@@ -220381,6 +220381,7 @@ import fs from "fs/promises";
|
|
|
220381
220381
|
import path2 from "path";
|
|
220382
220382
|
|
|
220383
220383
|
// src/service/connection_config.ts
|
|
220384
|
+
import { createPrivateKey } from "crypto";
|
|
220384
220385
|
import path from "path";
|
|
220385
220386
|
var PUBLISHER_DUCKDB_API_FIELDS = new Set(["attachedDatabases"]);
|
|
220386
220387
|
function normalizeSnowflakePrivateKey(privateKey) {
|
|
@@ -220399,6 +220400,12 @@ function normalizeSnowflakePrivateKey(privateKey) {
|
|
|
220399
220400
|
endRegex: /-----END\s+PRIVATE\s+KEY-----/i,
|
|
220400
220401
|
beginMarker: "-----BEGIN PRIVATE KEY-----",
|
|
220401
220402
|
endMarker: "-----END PRIVATE KEY-----"
|
|
220403
|
+
},
|
|
220404
|
+
{
|
|
220405
|
+
beginRegex: /-----BEGIN\s+RSA\s+PRIVATE\s+KEY-----/i,
|
|
220406
|
+
endRegex: /-----END\s+RSA\s+PRIVATE\s+KEY-----/i,
|
|
220407
|
+
beginMarker: "-----BEGIN RSA PRIVATE KEY-----",
|
|
220408
|
+
endMarker: "-----END RSA PRIVATE KEY-----"
|
|
220402
220409
|
}
|
|
220403
220410
|
];
|
|
220404
220411
|
for (const pattern of keyPatterns) {
|
|
@@ -220425,6 +220432,21 @@ ${pattern.endMarker}
|
|
|
220425
220432
|
privateKeyContent += `
|
|
220426
220433
|
`;
|
|
220427
220434
|
}
|
|
220435
|
+
if (/-----BEGIN\s+RSA\s+PRIVATE\s+KEY-----/i.test(privateKeyContent)) {
|
|
220436
|
+
try {
|
|
220437
|
+
privateKeyContent = createPrivateKey({
|
|
220438
|
+
key: privateKeyContent,
|
|
220439
|
+
format: "pem"
|
|
220440
|
+
}).export({ type: "pkcs8", format: "pem" }).toString();
|
|
220441
|
+
} catch (err) {
|
|
220442
|
+
throw new Error(`Failed to convert Snowflake RSA private key (PKCS#1) to PKCS#8: ${err instanceof Error ? err.message : String(err)}`);
|
|
220443
|
+
}
|
|
220444
|
+
if (!privateKeyContent.endsWith(`
|
|
220445
|
+
`)) {
|
|
220446
|
+
privateKeyContent += `
|
|
220447
|
+
`;
|
|
220448
|
+
}
|
|
220449
|
+
}
|
|
220428
220450
|
return privateKeyContent;
|
|
220429
220451
|
}
|
|
220430
220452
|
function validateDuckdbApiSurface(connection) {
|
|
@@ -231556,6 +231578,9 @@ class Project {
|
|
|
231556
231578
|
this.metadata.readme = payload.readme;
|
|
231557
231579
|
await this.writeProjectReadme(payload.readme);
|
|
231558
231580
|
}
|
|
231581
|
+
if (payload.materializationStorage !== undefined) {
|
|
231582
|
+
this.metadata.materializationStorage = payload.materializationStorage;
|
|
231583
|
+
}
|
|
231559
231584
|
if (payload.connections) {
|
|
231560
231585
|
const payloadConnections = payload.connections;
|
|
231561
231586
|
await this.runConnectionUpdateExclusive(async () => {
|
|
@@ -232407,6 +232432,9 @@ class ProjectStore {
|
|
|
232407
232432
|
if (!newProject.metadata)
|
|
232408
232433
|
newProject.metadata = {};
|
|
232409
232434
|
newProject.metadata.location = absoluteProjectPath;
|
|
232435
|
+
if (project.materializationStorage !== undefined) {
|
|
232436
|
+
newProject.metadata.materializationStorage = project.materializationStorage;
|
|
232437
|
+
}
|
|
232410
232438
|
this.projects.set(projectName, newProject);
|
|
232411
232439
|
project?.packages?.forEach((_package) => {
|
|
232412
232440
|
if (_package.name) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "crypto";
|
|
1
2
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
assembleProjectConnections,
|
|
5
|
+
normalizeSnowflakePrivateKey,
|
|
6
|
+
} from "./connection_config";
|
|
3
7
|
import { components } from "../api";
|
|
4
8
|
|
|
5
9
|
type ApiConnection = components["schemas"]["Connection"];
|
|
@@ -121,3 +125,44 @@ describe("assembleProjectConnections — databricks", () => {
|
|
|
121
125
|
);
|
|
122
126
|
});
|
|
123
127
|
});
|
|
128
|
+
|
|
129
|
+
describe("normalizeSnowflakePrivateKey", () => {
|
|
130
|
+
const { privateKey: pkcs8Pem } = generateKeyPairSync("rsa", {
|
|
131
|
+
modulusLength: 2048,
|
|
132
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
133
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
134
|
+
});
|
|
135
|
+
const { privateKey: pkcs1Pem } = generateKeyPairSync("rsa", {
|
|
136
|
+
modulusLength: 2048,
|
|
137
|
+
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
138
|
+
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("passes a multi-line PKCS#8 key through and adds a trailing newline", () => {
|
|
142
|
+
const trimmed = (pkcs8Pem as string).trimEnd();
|
|
143
|
+
const result = normalizeSnowflakePrivateKey(trimmed);
|
|
144
|
+
expect(result).toContain("-----BEGIN PRIVATE KEY-----");
|
|
145
|
+
expect(result.endsWith("\n")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("converts a multi-line PKCS#1 RSA key to PKCS#8", () => {
|
|
149
|
+
const result = normalizeSnowflakePrivateKey(pkcs1Pem as string);
|
|
150
|
+
expect(result).toContain("-----BEGIN PRIVATE KEY-----");
|
|
151
|
+
expect(result).not.toContain("BEGIN RSA PRIVATE KEY");
|
|
152
|
+
expect(result.endsWith("\n")).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("converts a single-line PKCS#1 RSA key to PKCS#8", () => {
|
|
156
|
+
const singleLine = (pkcs1Pem as string).replace(/\n/g, "");
|
|
157
|
+
const result = normalizeSnowflakePrivateKey(singleLine);
|
|
158
|
+
expect(result).toContain("-----BEGIN PRIVATE KEY-----");
|
|
159
|
+
expect(result).not.toContain("BEGIN RSA PRIVATE KEY");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("reconstructs a single-line PKCS#8 key without conversion", () => {
|
|
163
|
+
const singleLine = (pkcs8Pem as string).replace(/\n/g, "");
|
|
164
|
+
const result = normalizeSnowflakePrivateKey(singleLine);
|
|
165
|
+
expect(result.startsWith("-----BEGIN PRIVATE KEY-----\n")).toBe(true);
|
|
166
|
+
expect(result.endsWith("-----END PRIVATE KEY-----\n")).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPrivateKey } from "crypto";
|
|
1
2
|
import path from "path";
|
|
2
3
|
import { components } from "../api";
|
|
3
4
|
|
|
@@ -48,6 +49,12 @@ export function normalizeSnowflakePrivateKey(privateKey: string): string {
|
|
|
48
49
|
beginMarker: "-----BEGIN PRIVATE KEY-----",
|
|
49
50
|
endMarker: "-----END PRIVATE KEY-----",
|
|
50
51
|
},
|
|
52
|
+
{
|
|
53
|
+
beginRegex: /-----BEGIN\s+RSA\s+PRIVATE\s+KEY-----/i,
|
|
54
|
+
endRegex: /-----END\s+RSA\s+PRIVATE\s+KEY-----/i,
|
|
55
|
+
beginMarker: "-----BEGIN RSA PRIVATE KEY-----",
|
|
56
|
+
endMarker: "-----END RSA PRIVATE KEY-----",
|
|
57
|
+
},
|
|
51
58
|
];
|
|
52
59
|
|
|
53
60
|
for (const pattern of keyPatterns) {
|
|
@@ -73,6 +80,28 @@ export function normalizeSnowflakePrivateKey(privateKey: string): string {
|
|
|
73
80
|
privateKeyContent += "\n";
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
// Snowflake's Node SDK requires PKCS#8 ("BEGIN PRIVATE KEY"). Convert
|
|
84
|
+
// PKCS#1 ("BEGIN RSA PRIVATE KEY") so users can paste either format.
|
|
85
|
+
if (/-----BEGIN\s+RSA\s+PRIVATE\s+KEY-----/i.test(privateKeyContent)) {
|
|
86
|
+
try {
|
|
87
|
+
privateKeyContent = createPrivateKey({
|
|
88
|
+
key: privateKeyContent,
|
|
89
|
+
format: "pem",
|
|
90
|
+
})
|
|
91
|
+
.export({ type: "pkcs8", format: "pem" })
|
|
92
|
+
.toString();
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Failed to convert Snowflake RSA private key (PKCS#1) to PKCS#8: ${
|
|
96
|
+
err instanceof Error ? err.message : String(err)
|
|
97
|
+
}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (!privateKeyContent.endsWith("\n")) {
|
|
101
|
+
privateKeyContent += "\n";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
return privateKeyContent;
|
|
77
106
|
}
|
|
78
107
|
|
package/src/service/project.ts
CHANGED
|
@@ -99,6 +99,10 @@ export class Project {
|
|
|
99
99
|
await this.writeProjectReadme(payload.readme);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
if (payload.materializationStorage !== undefined) {
|
|
103
|
+
this.metadata.materializationStorage = payload.materializationStorage;
|
|
104
|
+
}
|
|
105
|
+
|
|
102
106
|
// Handle connections update
|
|
103
107
|
// TODO: Update project connections should have its own API endpoint
|
|
104
108
|
if (payload.connections) {
|
|
@@ -10,6 +10,11 @@ import { ProjectStore } from "./project_store";
|
|
|
10
10
|
|
|
11
11
|
type MockData = Record<string, unknown>;
|
|
12
12
|
|
|
13
|
+
const initializeDuckLakeCalls: Array<{
|
|
14
|
+
projectId: string;
|
|
15
|
+
config: { catalogUrl: string; dataPath: string };
|
|
16
|
+
}> = [];
|
|
17
|
+
|
|
13
18
|
mock.module("../storage/StorageManager", () => {
|
|
14
19
|
return {
|
|
15
20
|
StorageManager: class MockStorageManager {
|
|
@@ -17,6 +22,13 @@ mock.module("../storage/StorageManager", () => {
|
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
async initializeDuckLakeForProject(
|
|
26
|
+
projectId: string,
|
|
27
|
+
config: { catalogUrl: string; dataPath: string },
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
initializeDuckLakeCalls.push({ projectId, config });
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
getRepository() {
|
|
21
33
|
return {
|
|
22
34
|
// ===== PROJECT METHODS =====
|
|
@@ -467,6 +479,69 @@ describe("ProjectStore Service", () => {
|
|
|
467
479
|
expect(readmeContent).toBe("Updated README content");
|
|
468
480
|
});
|
|
469
481
|
|
|
482
|
+
it("should propagate materializationStorage on addProject for new project", async () => {
|
|
483
|
+
writeFileSync(
|
|
484
|
+
path.join(serverRootPath, "publisher.config.json"),
|
|
485
|
+
JSON.stringify({ frozenConfig: false, projects: [] }),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
await projectStore.finishedInitialization;
|
|
489
|
+
|
|
490
|
+
const materializationStorage = {
|
|
491
|
+
catalogUrl:
|
|
492
|
+
"postgres:host=localhost port=5432 dbname=ducklake user=u password=p",
|
|
493
|
+
dataPath: "gs://test-bucket",
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
initializeDuckLakeCalls.length = 0;
|
|
497
|
+
const project = await projectStore.addProject({
|
|
498
|
+
name: projectName,
|
|
499
|
+
materializationStorage,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(project.metadata.materializationStorage).toEqual(
|
|
503
|
+
materializationStorage,
|
|
504
|
+
);
|
|
505
|
+
expect(initializeDuckLakeCalls).toHaveLength(1);
|
|
506
|
+
expect(initializeDuckLakeCalls[0].config).toEqual(materializationStorage);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("should propagate materializationStorage on update", async () => {
|
|
510
|
+
const projectPath = path.join(serverRootPath, projectName);
|
|
511
|
+
mkdirSync(projectPath, { recursive: true });
|
|
512
|
+
writeFileSync(
|
|
513
|
+
path.join(projectPath, "publisher.json"),
|
|
514
|
+
JSON.stringify({ name: projectName, description: "Test package" }),
|
|
515
|
+
);
|
|
516
|
+
writeFileSync(
|
|
517
|
+
path.join(serverRootPath, "publisher.config.json"),
|
|
518
|
+
JSON.stringify({
|
|
519
|
+
frozenConfig: false,
|
|
520
|
+
projects: [
|
|
521
|
+
{
|
|
522
|
+
name: projectName,
|
|
523
|
+
packages: [{ name: projectName, location: projectPath }],
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
await projectStore.finishedInitialization;
|
|
530
|
+
const project = await projectStore.getProject(projectName);
|
|
531
|
+
|
|
532
|
+
const materializationStorage = {
|
|
533
|
+
catalogUrl:
|
|
534
|
+
"postgres:host=localhost port=5432 dbname=ducklake user=u password=p",
|
|
535
|
+
dataPath: "gs://test-bucket",
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
await project.update({ name: projectName, materializationStorage });
|
|
539
|
+
|
|
540
|
+
expect(project.metadata.materializationStorage).toEqual(
|
|
541
|
+
materializationStorage,
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
|
|
470
545
|
it(
|
|
471
546
|
"should handle project reload",
|
|
472
547
|
async () => {
|
|
@@ -807,6 +807,10 @@ export class ProjectStore {
|
|
|
807
807
|
|
|
808
808
|
if (!newProject.metadata) newProject.metadata = {};
|
|
809
809
|
newProject.metadata.location = absoluteProjectPath;
|
|
810
|
+
if (project.materializationStorage !== undefined) {
|
|
811
|
+
newProject.metadata.materializationStorage =
|
|
812
|
+
project.materializationStorage;
|
|
813
|
+
}
|
|
810
814
|
|
|
811
815
|
this.projects.set(projectName, newProject);
|
|
812
816
|
|