puls-dev 0.2.1 → 0.2.2

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.
Files changed (36) hide show
  1. package/dist/core/config.d.ts +5 -0
  2. package/dist/providers/firebase/appcheck.d.ts +15 -0
  3. package/dist/providers/firebase/appcheck.js +109 -0
  4. package/dist/providers/firebase/appcheck.test.d.ts +1 -0
  5. package/dist/providers/firebase/appcheck.test.js +141 -0
  6. package/dist/providers/firebase/index.d.ts +2 -0
  7. package/dist/providers/firebase/index.js +2 -0
  8. package/dist/providers/gcp/api.d.ts +10 -0
  9. package/dist/providers/gcp/api.js +111 -0
  10. package/dist/providers/gcp/clouddns.d.ts +37 -0
  11. package/dist/providers/gcp/clouddns.js +284 -0
  12. package/dist/providers/gcp/clouddns.test.d.ts +1 -0
  13. package/dist/providers/gcp/clouddns.test.js +259 -0
  14. package/dist/providers/gcp/cloudrun.d.ts +31 -0
  15. package/dist/providers/gcp/cloudrun.js +240 -0
  16. package/dist/providers/gcp/cloudrun.test.d.ts +1 -0
  17. package/dist/providers/gcp/cloudrun.test.js +281 -0
  18. package/dist/providers/gcp/cloudsql.d.ts +37 -0
  19. package/dist/providers/gcp/cloudsql.js +262 -0
  20. package/dist/providers/gcp/cloudsql.test.d.ts +1 -0
  21. package/dist/providers/gcp/cloudsql.test.js +295 -0
  22. package/dist/providers/gcp/iam.d.ts +38 -0
  23. package/dist/providers/gcp/iam.js +309 -0
  24. package/dist/providers/gcp/iam.test.d.ts +1 -0
  25. package/dist/providers/gcp/iam.test.js +305 -0
  26. package/dist/providers/gcp/index.d.ts +19 -0
  27. package/dist/providers/gcp/index.js +19 -0
  28. package/dist/providers/gcp/pubsub.d.ts +31 -0
  29. package/dist/providers/gcp/pubsub.js +227 -0
  30. package/dist/providers/gcp/pubsub.test.d.ts +1 -0
  31. package/dist/providers/gcp/pubsub.test.js +244 -0
  32. package/dist/providers/gcp/secrets.d.ts +21 -0
  33. package/dist/providers/gcp/secrets.js +187 -0
  34. package/dist/providers/gcp/secrets.test.d.ts +1 -0
  35. package/dist/providers/gcp/secrets.test.js +264 -0
  36. package/package.json +5 -1
@@ -23,6 +23,11 @@ export interface GlobalConfig {
23
23
  projectId: string;
24
24
  serviceAccountPath: string;
25
25
  };
26
+ gcp?: {
27
+ projectId?: string;
28
+ serviceAccountPath?: string;
29
+ region?: string;
30
+ };
26
31
  };
27
32
  }
28
33
  declare class ConfigManager {
@@ -0,0 +1,15 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ export declare class FirebaseAppCheckBuilder extends BaseBuilder {
3
+ private _configs;
4
+ constructor();
5
+ enforce(serviceName: string): this;
6
+ unenforced(serviceName: string): this;
7
+ off(serviceName: string): this;
8
+ mode(serviceName: string, mode: "ENFORCED" | "UNENFORCED" | "OFF"): this;
9
+ deploy(): Promise<{
10
+ project: string;
11
+ }>;
12
+ destroy(): Promise<{
13
+ destroyed: string;
14
+ }>;
15
+ }
@@ -0,0 +1,109 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { cloudFetch, getProjectId } from "./api.js";
3
+ const APP_CHECK_BASE = "https://firebaseappcheck.googleapis.com";
4
+ const SERVICE_IDS = {
5
+ firestore: "firestore.googleapis.com",
6
+ storage: "firebasestorage.googleapis.com",
7
+ database: "firebasedatabase.googleapis.com",
8
+ auth: "identitytoolkit.googleapis.com",
9
+ };
10
+ function resolveServiceId(name) {
11
+ return SERVICE_IDS[name.toLowerCase()] ?? name;
12
+ }
13
+ export class FirebaseAppCheckBuilder extends BaseBuilder {
14
+ _configs = new Map();
15
+ constructor() {
16
+ super("appcheck");
17
+ this.discoveryPromise = Promise.resolve(null);
18
+ }
19
+ enforce(serviceName) {
20
+ this._configs.set(resolveServiceId(serviceName), "ENFORCED");
21
+ return this;
22
+ }
23
+ unenforced(serviceName) {
24
+ this._configs.set(resolveServiceId(serviceName), "UNENFORCED");
25
+ return this;
26
+ }
27
+ off(serviceName) {
28
+ this._configs.set(resolveServiceId(serviceName), "OFF");
29
+ return this;
30
+ }
31
+ mode(serviceName, mode) {
32
+ this._configs.set(resolveServiceId(serviceName), mode);
33
+ return this;
34
+ }
35
+ async deploy() {
36
+ const dryRun = this.isDryRunActive();
37
+ const project = getProjectId();
38
+ console.log(`\nšŸ›”ļø Finalizing Firebase App Check...`);
39
+ if (dryRun) {
40
+ console.log(` šŸ“ [PLAN] Configure App Check enforcement modes:`);
41
+ for (const [serviceId, mode] of this._configs.entries()) {
42
+ console.log(` └─ ${serviceId}: ${mode}`);
43
+ }
44
+ return { project };
45
+ }
46
+ // 1. Fetch current status of each configured service
47
+ const existingConfigs = {};
48
+ for (const serviceId of this._configs.keys()) {
49
+ try {
50
+ const data = await cloudFetch(APP_CHECK_BASE, `/v1/projects/${project}/services/${serviceId}`);
51
+ existingConfigs[serviceId] = data.enforcementMode ?? "OFF";
52
+ }
53
+ catch (err) {
54
+ // If not found or not registered yet, default to OFF
55
+ existingConfigs[serviceId] = "OFF";
56
+ }
57
+ }
58
+ // 2. Patch services whose modes have changed
59
+ for (const [serviceId, mode] of this._configs.entries()) {
60
+ const existingMode = existingConfigs[serviceId] ?? "OFF";
61
+ if (existingMode !== mode) {
62
+ console.log(` šŸ”„ Updating App Check service "${serviceId}": ${existingMode} → ${mode}`);
63
+ await cloudFetch(APP_CHECK_BASE, `/v1/projects/${project}/services/${serviceId}?updateMask=enforcementMode`, {
64
+ method: "PATCH",
65
+ body: JSON.stringify({
66
+ name: `projects/${project}/services/${serviceId}`,
67
+ enforcementMode: mode,
68
+ }),
69
+ });
70
+ console.log(` āœ… App Check service "${serviceId}" updated to ${mode}`);
71
+ }
72
+ else {
73
+ console.log(` āœ… App Check service "${serviceId}" already set to ${mode}`);
74
+ }
75
+ }
76
+ await this.deploySidecars();
77
+ return { project };
78
+ }
79
+ async destroy() {
80
+ const dryRun = this.isDryRunActive();
81
+ const project = getProjectId();
82
+ console.log(`\nšŸ—‘ļø Destroying Firebase App Check...`);
83
+ console.log(` ā„¹ļø Reverting all configured services to OFF enforcement mode`);
84
+ if (dryRun) {
85
+ for (const serviceId of this._configs.keys()) {
86
+ console.log(` šŸ“ [PLAN] Revert ${serviceId} App Check to OFF`);
87
+ }
88
+ return { destroyed: "appcheck" };
89
+ }
90
+ for (const serviceId of this._configs.keys()) {
91
+ try {
92
+ console.log(` šŸ”„ Reverting App Check service "${serviceId}" to OFF...`);
93
+ await cloudFetch(APP_CHECK_BASE, `/v1/projects/${project}/services/${serviceId}?updateMask=enforcementMode`, {
94
+ method: "PATCH",
95
+ body: JSON.stringify({
96
+ name: `projects/${project}/services/${serviceId}`,
97
+ enforcementMode: "OFF",
98
+ }),
99
+ });
100
+ console.log(` āœ… App Check service "${serviceId}" reverted to OFF`);
101
+ }
102
+ catch (err) {
103
+ // Log error and continue teardown silently
104
+ }
105
+ }
106
+ await this.destroySidecars();
107
+ return { destroyed: "appcheck" };
108
+ }
109
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,141 @@
1
+ import { test, describe, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { GoogleAuth } from "google-auth-library";
4
+ import { FirebaseAppCheckBuilder } from "./appcheck.js";
5
+ import { Config } from "../../core/config.js";
6
+ describe("FirebaseAppCheckBuilder Unit Tests", () => {
7
+ let originalFetch;
8
+ let fetchCalls = [];
9
+ let mockResponses = {};
10
+ beforeEach(() => {
11
+ Config.set({
12
+ dryRun: false,
13
+ providers: {
14
+ firebase: {
15
+ projectId: "my-project",
16
+ serviceAccountPath: "/fake/sa.json",
17
+ },
18
+ },
19
+ });
20
+ originalFetch = globalThis.fetch;
21
+ fetchCalls = [];
22
+ mockResponses = {};
23
+ globalThis.fetch = async (input, init) => {
24
+ const url = String(input);
25
+ const method = init?.method ?? "GET";
26
+ let body;
27
+ if (init?.body) {
28
+ if (typeof init.body === "string") {
29
+ try {
30
+ body = JSON.parse(init.body);
31
+ }
32
+ catch {
33
+ body = init.body;
34
+ }
35
+ }
36
+ else {
37
+ body = "[Binary/Buffer Body]";
38
+ }
39
+ }
40
+ const headers = init?.headers;
41
+ fetchCalls.push({ url, method, body, headers });
42
+ const matchKey = Object.keys(mockResponses)
43
+ .filter((key) => {
44
+ const [mMethod, mPath] = key.split(" ");
45
+ return method === mMethod && url.includes(mPath);
46
+ })
47
+ .sort((a, b) => b.split(" ")[1].length - a.split(" ")[1].length)[0];
48
+ if (matchKey) {
49
+ const resp = mockResponses[matchKey];
50
+ return {
51
+ ok: resp.status >= 200 && resp.status < 300,
52
+ status: resp.status,
53
+ json: async () => resp.body,
54
+ text: async () => JSON.stringify(resp.body),
55
+ };
56
+ }
57
+ return {
58
+ ok: false,
59
+ status: 404,
60
+ json: async () => ({ message: `Endpoint not mocked: ${method} ${url}` }),
61
+ text: async () => `Endpoint not mocked: ${method} ${url}`,
62
+ };
63
+ };
64
+ mock.method(GoogleAuth.prototype, "getClient", async () => {
65
+ return {
66
+ getAccessToken: async () => ({ token: "fake-access-token" }),
67
+ };
68
+ });
69
+ });
70
+ afterEach(() => {
71
+ globalThis.fetch = originalFetch;
72
+ mock.restoreAll();
73
+ });
74
+ test("runs in dry-run mode safely and logs plans", async () => {
75
+ Config.set({
76
+ dryRun: true,
77
+ providers: {
78
+ firebase: {
79
+ projectId: "my-project",
80
+ serviceAccountPath: "/fake/sa.json",
81
+ },
82
+ },
83
+ });
84
+ const builder = new FirebaseAppCheckBuilder()
85
+ .enforce("firestore")
86
+ .unenforced("storage")
87
+ .off("auth");
88
+ const deployResult = await builder.deploy();
89
+ assert.strictEqual(deployResult.project, "my-project");
90
+ assert.strictEqual(fetchCalls.length, 0); // zero write calls
91
+ });
92
+ test("syncs App Check services idempotently - updates changed and skips identical", async () => {
93
+ // 1. Mock GET calls returning existing statuses:
94
+ // firestore is currently OFF (needs to be ENFORCED)
95
+ // storage is currently UNENFORCED (needs to be UNENFORCED - should skip)
96
+ mockResponses["GET /services/firestore.googleapis.com"] = {
97
+ status: 200,
98
+ body: { name: "projects/my-project/services/firestore.googleapis.com", enforcementMode: "OFF" },
99
+ };
100
+ mockResponses["GET /services/firebasestorage.googleapis.com"] = {
101
+ status: 200,
102
+ body: { name: "projects/my-project/services/firebasestorage.googleapis.com", enforcementMode: "UNENFORCED" },
103
+ };
104
+ // 2. Mock PATCH calls
105
+ mockResponses["PATCH /services/firestore.googleapis.com"] = {
106
+ status: 200,
107
+ body: { name: "projects/my-project/services/firestore.googleapis.com", enforcementMode: "ENFORCED" },
108
+ };
109
+ const builder = new FirebaseAppCheckBuilder()
110
+ .enforce("firestore")
111
+ .unenforced("storage");
112
+ const deployResult = await builder.deploy();
113
+ assert.strictEqual(deployResult.project, "my-project");
114
+ // We should have exactly 2 GET calls and 1 PATCH call
115
+ const patchCalls = fetchCalls.filter((c) => c.method === "PATCH");
116
+ assert.strictEqual(patchCalls.length, 1);
117
+ assert.strictEqual(patchCalls[0].url.includes("/services/firestore.googleapis.com"), true);
118
+ assert.strictEqual(patchCalls[0].body.enforcementMode, "ENFORCED");
119
+ });
120
+ test("destroys App Check configuration by reverting all configured services to OFF", async () => {
121
+ // Mock PATCH calls returning OFF
122
+ mockResponses["PATCH /services/firestore.googleapis.com"] = {
123
+ status: 200,
124
+ body: { name: "projects/my-project/services/firestore.googleapis.com", enforcementMode: "OFF" },
125
+ };
126
+ mockResponses["PATCH /services/firebasestorage.googleapis.com"] = {
127
+ status: 200,
128
+ body: { name: "projects/my-project/services/firebasestorage.googleapis.com", enforcementMode: "OFF" },
129
+ };
130
+ const builder = new FirebaseAppCheckBuilder()
131
+ .enforce("firestore")
132
+ .unenforced("storage");
133
+ const destroyResult = await builder.destroy();
134
+ assert.deepStrictEqual(destroyResult, { destroyed: "appcheck" });
135
+ // Verify both services were patched to OFF
136
+ const patchCalls = fetchCalls.filter((c) => c.method === "PATCH");
137
+ assert.strictEqual(patchCalls.length, 2);
138
+ assert.strictEqual(patchCalls.some((c) => c.url.includes("/services/firestore.googleapis.com") && c.body.enforcementMode === "OFF"), true);
139
+ assert.strictEqual(patchCalls.some((c) => c.url.includes("/services/firebasestorage.googleapis.com") && c.body.enforcementMode === "OFF"), true);
140
+ });
141
+ });
@@ -4,6 +4,7 @@ import { FirebaseFirestoreBuilder } from './firestore.js';
4
4
  import { FirebaseStorageBuilder } from './storage.js';
5
5
  import { FirebaseAuthBuilder } from './auth.js';
6
6
  import { FirebaseRemoteConfigBuilder } from './remoteconfig.js';
7
+ import { FirebaseAppCheckBuilder } from './appcheck.js';
7
8
  export { FUNCTIONS_RUNTIME };
8
9
  export declare const Firebase: {
9
10
  Hosting: (siteId: string) => FirebaseHostingBuilder;
@@ -12,4 +13,5 @@ export declare const Firebase: {
12
13
  Storage: (bucket?: string) => FirebaseStorageBuilder;
13
14
  Auth: () => FirebaseAuthBuilder;
14
15
  RemoteConfig: () => FirebaseRemoteConfigBuilder;
16
+ AppCheck: () => FirebaseAppCheckBuilder;
15
17
  };
@@ -4,6 +4,7 @@ import { FirebaseFirestoreBuilder } from './firestore.js';
4
4
  import { FirebaseStorageBuilder } from './storage.js';
5
5
  import { FirebaseAuthBuilder } from './auth.js';
6
6
  import { FirebaseRemoteConfigBuilder } from './remoteconfig.js';
7
+ import { FirebaseAppCheckBuilder } from './appcheck.js';
7
8
  export { FUNCTIONS_RUNTIME };
8
9
  export const Firebase = {
9
10
  Hosting: (siteId) => new FirebaseHostingBuilder(siteId),
@@ -12,4 +13,5 @@ export const Firebase = {
12
13
  Storage: (bucket) => new FirebaseStorageBuilder(bucket),
13
14
  Auth: () => new FirebaseAuthBuilder(),
14
15
  RemoteConfig: () => new FirebaseRemoteConfigBuilder(),
16
+ AppCheck: () => new FirebaseAppCheckBuilder(),
15
17
  };
@@ -0,0 +1,10 @@
1
+ export interface GCPConfig {
2
+ projectId: string;
3
+ serviceAccountPath: string;
4
+ region?: string;
5
+ }
6
+ export declare function resolveGCPConfig(): GCPConfig;
7
+ export declare function getProjectId(): string;
8
+ export declare function getRegion(): string;
9
+ export declare function getGCPToken(scopes: string[]): Promise<string>;
10
+ export declare function gcpFetch(base: string, path: string, opts?: RequestInit): Promise<any>;
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import { GoogleAuth } from 'google-auth-library';
3
+ import { Config } from '../../core/config.js';
4
+ export function resolveGCPConfig() {
5
+ // 1. Check Config.providers.gcp
6
+ const gcpCfg = Config.get().providers.gcp;
7
+ if (gcpCfg?.serviceAccountPath) {
8
+ if (gcpCfg.projectId) {
9
+ return {
10
+ projectId: gcpCfg.projectId,
11
+ serviceAccountPath: gcpCfg.serviceAccountPath,
12
+ region: gcpCfg.region,
13
+ };
14
+ }
15
+ try {
16
+ const sa = JSON.parse(fs.readFileSync(gcpCfg.serviceAccountPath, 'utf8'));
17
+ return {
18
+ projectId: sa.project_id,
19
+ serviceAccountPath: gcpCfg.serviceAccountPath,
20
+ region: gcpCfg.region,
21
+ };
22
+ }
23
+ catch (e) {
24
+ // Continue to next fallback
25
+ }
26
+ }
27
+ // 2. Fallback to Config.providers.firebase
28
+ const fbCfg = Config.get().providers.firebase;
29
+ if (fbCfg?.serviceAccountPath) {
30
+ if (fbCfg.projectId) {
31
+ return {
32
+ projectId: fbCfg.projectId,
33
+ serviceAccountPath: fbCfg.serviceAccountPath,
34
+ };
35
+ }
36
+ try {
37
+ const sa = JSON.parse(fs.readFileSync(fbCfg.serviceAccountPath, 'utf8'));
38
+ return {
39
+ projectId: sa.project_id,
40
+ serviceAccountPath: fbCfg.serviceAccountPath,
41
+ };
42
+ }
43
+ catch (e) {
44
+ // Continue to next fallback
45
+ }
46
+ }
47
+ // 3. Fallback to process.env.GCP_SA
48
+ const gcpSa = process.env.GCP_SA;
49
+ if (gcpSa && fs.existsSync(gcpSa)) {
50
+ try {
51
+ const sa = JSON.parse(fs.readFileSync(gcpSa, 'utf8'));
52
+ return {
53
+ projectId: sa.project_id,
54
+ serviceAccountPath: gcpSa,
55
+ };
56
+ }
57
+ catch (e) {
58
+ // Continue to next fallback
59
+ }
60
+ }
61
+ // 4. Fallback to process.env.FIREBASE_SA
62
+ const fbSa = process.env.FIREBASE_SA;
63
+ if (fbSa && fs.existsSync(fbSa)) {
64
+ try {
65
+ const sa = JSON.parse(fs.readFileSync(fbSa, 'utf8'));
66
+ return {
67
+ projectId: sa.project_id,
68
+ serviceAccountPath: fbSa,
69
+ };
70
+ }
71
+ catch (e) {
72
+ // Continue to next fallback
73
+ }
74
+ }
75
+ throw new Error('GCP credentials not configured. Please set GCP_SA or FIREBASE_SA env var, or configure providers.gcp or providers.firebase in Config.');
76
+ }
77
+ export function getProjectId() {
78
+ return resolveGCPConfig().projectId;
79
+ }
80
+ export function getRegion() {
81
+ const gcpCfg = Config.get().providers.gcp;
82
+ return gcpCfg?.region ?? 'us-central1';
83
+ }
84
+ export async function getGCPToken(scopes) {
85
+ const { serviceAccountPath } = resolveGCPConfig();
86
+ const auth = new GoogleAuth({ keyFile: serviceAccountPath, scopes });
87
+ const client = await auth.getClient();
88
+ const token = await client.getAccessToken();
89
+ if (!token.token) {
90
+ throw new Error('Failed to retrieve GCP access token');
91
+ }
92
+ return token.token;
93
+ }
94
+ const CLOUD_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
95
+ export async function gcpFetch(base, path, opts = {}) {
96
+ const token = await getGCPToken([CLOUD_SCOPE]);
97
+ const res = await fetch(`${base}${path}`, {
98
+ ...opts,
99
+ headers: {
100
+ 'Authorization': `Bearer ${token}`,
101
+ 'Content-Type': 'application/json',
102
+ ...(opts.headers ?? {}),
103
+ },
104
+ });
105
+ if (!res.ok) {
106
+ const body = await res.text();
107
+ throw new Error(`GCP API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
108
+ }
109
+ const text = await res.text();
110
+ return text ? JSON.parse(text) : null;
111
+ }
@@ -0,0 +1,37 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ export interface GCPDNSRecord {
4
+ name: string;
5
+ type: string;
6
+ value: any;
7
+ ttl: number;
8
+ }
9
+ export declare class GCPCloudDNSZoneBuilder extends BaseBuilder {
10
+ zoneName: string;
11
+ readonly out: {
12
+ zone: Output<{
13
+ name: string;
14
+ id: string;
15
+ }>;
16
+ };
17
+ cleanZoneName: string;
18
+ zoneId: string;
19
+ private records;
20
+ constructor(zoneName: string);
21
+ private discoverZone;
22
+ record(name: string, type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "PTR" | "SRV" | "CAA" | "SPF", value: string, ttl?: number): this;
23
+ pointer(name: string, target: BaseBuilder | Output<string> | string): this;
24
+ deploy(): Promise<{
25
+ zone: string;
26
+ id: string;
27
+ records: {
28
+ name: string;
29
+ type: string;
30
+ ttl: number;
31
+ rrdatas: string[];
32
+ }[];
33
+ }>;
34
+ destroy(): Promise<{
35
+ destroyed: string;
36
+ }>;
37
+ }