puls-dev 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.
Files changed (93) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +148 -0
  3. package/dist/core/checker.d.ts +5 -0
  4. package/dist/core/checker.js +148 -0
  5. package/dist/core/config.d.ts +35 -0
  6. package/dist/core/config.js +15 -0
  7. package/dist/core/decorators.d.ts +26 -0
  8. package/dist/core/decorators.js +86 -0
  9. package/dist/core/output.d.ts +8 -0
  10. package/dist/core/output.js +19 -0
  11. package/dist/core/resource.d.ts +20 -0
  12. package/dist/core/resource.js +77 -0
  13. package/dist/core/stack.d.ts +20 -0
  14. package/dist/core/stack.js +120 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +12 -0
  17. package/dist/providers/aws/acm.d.ts +22 -0
  18. package/dist/providers/aws/acm.js +109 -0
  19. package/dist/providers/aws/api.d.ts +28 -0
  20. package/dist/providers/aws/api.js +36 -0
  21. package/dist/providers/aws/apigateway.d.ts +24 -0
  22. package/dist/providers/aws/apigateway.js +157 -0
  23. package/dist/providers/aws/cloudfront.d.ts +31 -0
  24. package/dist/providers/aws/cloudfront.js +205 -0
  25. package/dist/providers/aws/fargate.d.ts +43 -0
  26. package/dist/providers/aws/fargate.js +277 -0
  27. package/dist/providers/aws/index.d.ts +23 -0
  28. package/dist/providers/aws/index.js +29 -0
  29. package/dist/providers/aws/lambda.d.ts +30 -0
  30. package/dist/providers/aws/lambda.js +159 -0
  31. package/dist/providers/aws/list.d.ts +2 -0
  32. package/dist/providers/aws/list.js +44 -0
  33. package/dist/providers/aws/rds.d.ts +46 -0
  34. package/dist/providers/aws/rds.js +227 -0
  35. package/dist/providers/aws/route53.d.ts +38 -0
  36. package/dist/providers/aws/route53.js +218 -0
  37. package/dist/providers/aws/s3.d.ts +20 -0
  38. package/dist/providers/aws/s3.js +165 -0
  39. package/dist/providers/aws/secrets.d.ts +25 -0
  40. package/dist/providers/aws/secrets.js +151 -0
  41. package/dist/providers/aws/sqs.d.ts +33 -0
  42. package/dist/providers/aws/sqs.js +178 -0
  43. package/dist/providers/do/api.d.ts +11 -0
  44. package/dist/providers/do/api.js +52 -0
  45. package/dist/providers/do/certificate.d.ts +7 -0
  46. package/dist/providers/do/certificate.js +36 -0
  47. package/dist/providers/do/domain.d.ts +21 -0
  48. package/dist/providers/do/domain.js +81 -0
  49. package/dist/providers/do/droplet.d.ts +35 -0
  50. package/dist/providers/do/droplet.js +180 -0
  51. package/dist/providers/do/firewall.d.ts +23 -0
  52. package/dist/providers/do/firewall.js +94 -0
  53. package/dist/providers/do/index.d.ts +15 -0
  54. package/dist/providers/do/index.js +21 -0
  55. package/dist/providers/do/list.d.ts +2 -0
  56. package/dist/providers/do/list.js +59 -0
  57. package/dist/providers/do/load_balancer.d.ts +12 -0
  58. package/dist/providers/do/load_balancer.js +62 -0
  59. package/dist/providers/firebase/api.d.ts +4 -0
  60. package/dist/providers/firebase/api.js +62 -0
  61. package/dist/providers/firebase/auth.d.ts +35 -0
  62. package/dist/providers/firebase/auth.js +147 -0
  63. package/dist/providers/firebase/firestore.d.ts +28 -0
  64. package/dist/providers/firebase/firestore.js +120 -0
  65. package/dist/providers/firebase/functions.d.ts +50 -0
  66. package/dist/providers/firebase/functions.js +163 -0
  67. package/dist/providers/firebase/hosting.d.ts +14 -0
  68. package/dist/providers/firebase/hosting.js +144 -0
  69. package/dist/providers/firebase/index.d.ts +15 -0
  70. package/dist/providers/firebase/index.js +15 -0
  71. package/dist/providers/firebase/remoteconfig.d.ts +22 -0
  72. package/dist/providers/firebase/remoteconfig.js +135 -0
  73. package/dist/providers/firebase/storage.d.ts +34 -0
  74. package/dist/providers/firebase/storage.js +117 -0
  75. package/dist/providers/proxmox/api.d.ts +12 -0
  76. package/dist/providers/proxmox/api.js +50 -0
  77. package/dist/providers/proxmox/index.d.ts +15 -0
  78. package/dist/providers/proxmox/index.js +10 -0
  79. package/dist/providers/proxmox/list.d.ts +2 -0
  80. package/dist/providers/proxmox/list.js +15 -0
  81. package/dist/providers/proxmox/vm.d.ts +61 -0
  82. package/dist/providers/proxmox/vm.js +482 -0
  83. package/dist/types/aws.d.ts +55 -0
  84. package/dist/types/aws.js +48 -0
  85. package/dist/types/do.d.ts +19 -0
  86. package/dist/types/do.js +19 -0
  87. package/dist/types/gcp.d.ts +9 -0
  88. package/dist/types/gcp.js +9 -0
  89. package/dist/types/inventory.d.ts +87 -0
  90. package/dist/types/inventory.js +2 -0
  91. package/dist/types/proxmox.d.ts +11 -0
  92. package/dist/types/proxmox.js +28 -0
  93. package/package.json +56 -0
@@ -0,0 +1,50 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ export declare const FUNCTIONS_RUNTIME: {
3
+ readonly NODEJS_22: "nodejs22";
4
+ readonly NODEJS_20: "nodejs20";
5
+ readonly NODEJS_18: "nodejs18";
6
+ readonly PYTHON_312: "python312";
7
+ readonly PYTHON_311: "python311";
8
+ readonly GO_122: "go122";
9
+ readonly JAVA_21: "java21";
10
+ };
11
+ export declare class FirebaseFunctionsBuilder extends BaseBuilder {
12
+ private _sourcePath?;
13
+ private _entryPoint;
14
+ private _runtime;
15
+ private _region;
16
+ private _memory;
17
+ private _timeout;
18
+ private _maxInstances;
19
+ private _minInstances;
20
+ private _env;
21
+ constructor(functionName: string);
22
+ source(path: string): this;
23
+ entryPoint(e: string): this;
24
+ runtime(r: string): this;
25
+ region(r: string): this;
26
+ memory(m: string): this;
27
+ timeout(seconds: number): this;
28
+ maxInstances(n: number): this;
29
+ minInstances(n: number): this;
30
+ env(vars: Record<string, string>): this;
31
+ private fnPath;
32
+ private getExisting;
33
+ private zipSource;
34
+ private uploadSource;
35
+ private pollOperation;
36
+ deploy(): Promise<{
37
+ name: string;
38
+ project: string;
39
+ region: string;
40
+ url?: undefined;
41
+ } | {
42
+ name: string;
43
+ url: any;
44
+ project: string;
45
+ region: string;
46
+ }>;
47
+ destroy(): Promise<{
48
+ destroyed: string;
49
+ }>;
50
+ }
@@ -0,0 +1,163 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { mkdtempSync, readFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { BaseBuilder } from '../../core/resource.js';
6
+ import { cloudFetch, getProjectId } from './api.js';
7
+ const CF_BASE = 'https://cloudfunctions.googleapis.com/v2';
8
+ export const FUNCTIONS_RUNTIME = {
9
+ NODEJS_22: 'nodejs22',
10
+ NODEJS_20: 'nodejs20',
11
+ NODEJS_18: 'nodejs18',
12
+ PYTHON_312: 'python312',
13
+ PYTHON_311: 'python311',
14
+ GO_122: 'go122',
15
+ JAVA_21: 'java21',
16
+ };
17
+ export class FirebaseFunctionsBuilder extends BaseBuilder {
18
+ _sourcePath;
19
+ _entryPoint = 'handler';
20
+ _runtime = FUNCTIONS_RUNTIME.NODEJS_20;
21
+ _region = 'us-central1';
22
+ _memory = '256M';
23
+ _timeout = 60;
24
+ _maxInstances = 100;
25
+ _minInstances = 0;
26
+ _env = {};
27
+ constructor(functionName) {
28
+ super(functionName);
29
+ // Discovery runs lazily in deploy() because we need _region, which may not be set at construction time
30
+ this.discoveryPromise = Promise.resolve(null);
31
+ }
32
+ source(path) { this._sourcePath = path; return this; }
33
+ entryPoint(e) { this._entryPoint = e; return this; }
34
+ runtime(r) { this._runtime = r; return this; }
35
+ region(r) { this._region = r; return this; }
36
+ memory(m) { this._memory = m; return this; }
37
+ timeout(seconds) { this._timeout = seconds; return this; }
38
+ maxInstances(n) { this._maxInstances = n; return this; }
39
+ minInstances(n) { this._minInstances = n; return this; }
40
+ env(vars) { this._env = vars; return this; }
41
+ fnPath() {
42
+ return `/projects/${getProjectId()}/locations/${this._region}/functions/${this.name}`;
43
+ }
44
+ async getExisting() {
45
+ try {
46
+ return await cloudFetch(CF_BASE, this.fnPath());
47
+ }
48
+ catch (e) {
49
+ if (e.message?.includes('404'))
50
+ return null;
51
+ throw e;
52
+ }
53
+ }
54
+ zipSource() {
55
+ if (!this._sourcePath)
56
+ throw new Error(`[Firebase.Functions:${this.name}] .source() is required`);
57
+ const tmp = mkdtempSync(join(tmpdir(), 'puls-fn-'));
58
+ const zipPath = join(tmp, 'function.zip');
59
+ execSync(`cd "${this._sourcePath}" && zip -r "${zipPath}" .`, { stdio: 'pipe' });
60
+ return readFileSync(zipPath);
61
+ }
62
+ async uploadSource() {
63
+ const { uploadUrl, storageSource } = await cloudFetch(CF_BASE, `/projects/${getProjectId()}/locations/${this._region}/functions:generateUploadUrl`, { method: 'POST', body: '{}' });
64
+ const zipBuffer = this.zipSource();
65
+ const res = await fetch(uploadUrl, {
66
+ method: 'PUT',
67
+ headers: { 'Content-Type': 'application/zip' },
68
+ body: new Uint8Array(zipBuffer),
69
+ });
70
+ if (!res.ok)
71
+ throw new Error(`Source upload failed: ${res.status}`);
72
+ return storageSource;
73
+ }
74
+ async pollOperation(operationName) {
75
+ const start = Date.now();
76
+ const timeout = 5 * 60 * 1000;
77
+ while (Date.now() - start < timeout) {
78
+ await new Promise(r => setTimeout(r, 4000));
79
+ const op = await cloudFetch(CF_BASE, `/${operationName}`);
80
+ if (op.done) {
81
+ if (op.error)
82
+ throw new Error(`Operation failed: ${JSON.stringify(op.error)}`);
83
+ return;
84
+ }
85
+ }
86
+ throw new Error(`[Firebase.Functions:${this.name}] Operation timed out after 5 minutes`);
87
+ }
88
+ async deploy() {
89
+ const dryRun = this.isDryRunActive();
90
+ const project = getProjectId();
91
+ console.log(`\n⚡ Finalizing Firebase Function "${this.name}"...`);
92
+ if (!this._sourcePath)
93
+ throw new Error(`[Firebase.Functions:${this.name}] .source() is required`);
94
+ if (dryRun) {
95
+ const existing = await this.getExisting();
96
+ if (existing) {
97
+ console.log(` ✅ Function "${this.name}" exists (${existing.state ?? 'ACTIVE'})`);
98
+ console.log(` 📝 [PLAN] Update → ${this._runtime}, ${this._memory}, ${this._timeout}s timeout`);
99
+ }
100
+ else {
101
+ console.log(` 📝 [PLAN] Create function "${this.name}"`);
102
+ console.log(` └─ runtime: ${this._runtime}`);
103
+ console.log(` └─ entry point: ${this._entryPoint}`);
104
+ console.log(` └─ region: ${this._region}`);
105
+ console.log(` └─ memory: ${this._memory}, timeout: ${this._timeout}s`);
106
+ console.log(` └─ source: ${this._sourcePath}`);
107
+ }
108
+ return { name: this.name, project, region: this._region };
109
+ }
110
+ const storageSource = await this.uploadSource();
111
+ console.log(` 📦 Source uploaded`);
112
+ const body = {
113
+ name: `projects/${project}/locations/${this._region}/functions/${this.name}`,
114
+ buildConfig: {
115
+ runtime: this._runtime,
116
+ entryPoint: this._entryPoint,
117
+ source: { storageSource },
118
+ environmentVariables: this._env,
119
+ },
120
+ serviceConfig: {
121
+ availableMemory: this._memory,
122
+ timeoutSeconds: this._timeout,
123
+ maxInstanceCount: this._maxInstances,
124
+ minInstanceCount: this._minInstances,
125
+ environmentVariables: this._env,
126
+ ingressSettings: 'ALLOW_ALL',
127
+ },
128
+ };
129
+ const existing = await this.getExisting();
130
+ let op;
131
+ if (!existing) {
132
+ op = await cloudFetch(CF_BASE, `/projects/${project}/locations/${this._region}/functions?functionId=${this.name}`, { method: 'POST', body: JSON.stringify(body) });
133
+ console.log(` 🚀 Creating function...`);
134
+ }
135
+ else {
136
+ const mask = 'buildConfig.source,buildConfig.runtime,buildConfig.entryPoint,buildConfig.environmentVariables,serviceConfig.availableMemory,serviceConfig.timeoutSeconds,serviceConfig.maxInstanceCount,serviceConfig.minInstanceCount,serviceConfig.environmentVariables';
137
+ op = await cloudFetch(CF_BASE, `${this.fnPath()}?updateMask=${mask}`, { method: 'PATCH', body: JSON.stringify(body) });
138
+ console.log(` 🔄 Updating function...`);
139
+ }
140
+ await this.pollOperation(op.name);
141
+ const fn = await this.getExisting();
142
+ const url = fn?.serviceConfig?.uri ?? `https://${this._region}-${project}.cloudfunctions.net/${this.name}`;
143
+ console.log(` ✅ Function live → ${url}`);
144
+ return { name: this.name, url, project, region: this._region };
145
+ }
146
+ async destroy() {
147
+ const dryRun = this.isDryRunActive();
148
+ console.log(`\n🗑️ Destroying Firebase Function "${this.name}"...`);
149
+ const existing = await this.getExisting();
150
+ if (!existing) {
151
+ console.log(` ✅ Function "${this.name}" does not exist — nothing to do`);
152
+ return { destroyed: this.name };
153
+ }
154
+ if (dryRun) {
155
+ console.log(` 📝 [PLAN] Delete function "${this.name}" in ${this._region}`);
156
+ return { destroyed: this.name };
157
+ }
158
+ const op = await cloudFetch(CF_BASE, this.fnPath(), { method: 'DELETE' });
159
+ await this.pollOperation(op.name);
160
+ console.log(` ✅ Function "${this.name}" deleted`);
161
+ return { destroyed: this.name };
162
+ }
163
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ export declare class FirebaseHostingBuilder extends BaseBuilder {
3
+ private _sourcePath?;
4
+ constructor(siteId: string);
5
+ source(path: string): this;
6
+ private discoverSite;
7
+ deploy(): Promise<{
8
+ siteId: string;
9
+ url: string;
10
+ }>;
11
+ destroy(): Promise<{
12
+ destroyed: string;
13
+ }>;
14
+ }
@@ -0,0 +1,144 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdirSync, statSync, readFileSync } from 'node:fs';
3
+ import { join, relative, extname } from 'node:path';
4
+ import { BaseBuilder } from '../../core/resource.js';
5
+ import { getProjectId, hostingFetch, getFirebaseToken } from './api.js';
6
+ const CONTENT_TYPES = {
7
+ '.html': 'text/html',
8
+ '.css': 'text/css',
9
+ '.js': 'application/javascript',
10
+ '.json': 'application/json',
11
+ '.png': 'image/png',
12
+ '.jpg': 'image/jpeg',
13
+ '.svg': 'image/svg+xml',
14
+ '.ico': 'image/x-icon',
15
+ '.woff2': 'font/woff2',
16
+ '.woff': 'font/woff',
17
+ '.ttf': 'font/ttf',
18
+ '.txt': 'text/plain',
19
+ '.xml': 'application/xml',
20
+ };
21
+ function walkDir(dir) {
22
+ const results = [];
23
+ for (const entry of readdirSync(dir)) {
24
+ const full = join(dir, entry);
25
+ if (statSync(full).isDirectory()) {
26
+ results.push(...walkDir(full));
27
+ }
28
+ else {
29
+ results.push(full);
30
+ }
31
+ }
32
+ return results;
33
+ }
34
+ function sha256(filePath) {
35
+ const content = readFileSync(filePath);
36
+ return createHash('sha256').update(content).digest('hex');
37
+ }
38
+ export class FirebaseHostingBuilder extends BaseBuilder {
39
+ _sourcePath;
40
+ constructor(siteId) {
41
+ super(siteId);
42
+ // Discovery: check if the site has any releases (confirms it exists and is active)
43
+ this.discoveryPromise = this.discoverSite(siteId);
44
+ }
45
+ source(path) { this._sourcePath = path; return this; }
46
+ async discoverSite(siteId) {
47
+ try {
48
+ const result = await hostingFetch(`/sites/${siteId}/releases?pageSize=1`);
49
+ return result ?? null;
50
+ }
51
+ catch (e) {
52
+ // Site doesn't exist yet or credentials not set yet — not an error at construction time
53
+ if (e.message?.includes('403') || e.message?.includes('404'))
54
+ return null;
55
+ if (e.message?.includes('Firebase not configured'))
56
+ return null;
57
+ throw e;
58
+ }
59
+ }
60
+ async deploy() {
61
+ const dryRun = this.isDryRunActive();
62
+ const projectId = getProjectId();
63
+ const siteId = this.name;
64
+ console.log(`\n⚡ Finalizing Firebase Hosting "${siteId}"...`);
65
+ if (!this._sourcePath)
66
+ throw new Error(`[Firebase.Hosting:${siteId}] .source("./dist") is required`);
67
+ const files = walkDir(this._sourcePath);
68
+ if (files.length === 0)
69
+ throw new Error(`[Firebase.Hosting:${siteId}] No files found in "${this._sourcePath}"`);
70
+ if (dryRun) {
71
+ console.log(` 📝 [PLAN] Deploy ${files.length} file(s) to https://${siteId}.web.app`);
72
+ for (const f of files.slice(0, 5)) {
73
+ console.log(` └─ /${relative(this._sourcePath, f)}`);
74
+ }
75
+ if (files.length > 5)
76
+ console.log(` └─ ... and ${files.length - 5} more`);
77
+ return { siteId, url: `https://${siteId}.web.app` };
78
+ }
79
+ // 1. Create a new version
80
+ const version = await hostingFetch(`/sites/${siteId}/versions`, {
81
+ method: 'POST',
82
+ body: JSON.stringify({ config: { headers: [{ glob: '**', headers: { 'Cache-Control': 'max-age=3600' } }] } }),
83
+ });
84
+ const versionId = version.name.split('/').pop();
85
+ console.log(` 📦 Version created: ${versionId}`);
86
+ // 2. Build file hash map — keys are URL paths, values are SHA256 hashes
87
+ const fileMap = {};
88
+ const absoluteToUrl = {};
89
+ for (const absPath of files) {
90
+ const urlPath = '/' + relative(this._sourcePath, absPath).replace(/\\/g, '/');
91
+ const hash = sha256(absPath);
92
+ fileMap[urlPath] = hash;
93
+ absoluteToUrl[hash] = absPath;
94
+ }
95
+ // 3. Populate files — API tells us which hashes need uploading
96
+ const populate = await hostingFetch(`/sites/${siteId}/versions/${versionId}/populateFiles`, {
97
+ method: 'POST',
98
+ body: JSON.stringify({ files: fileMap }),
99
+ });
100
+ const uploadUrl = populate.uploadUrl;
101
+ const required = populate.uploadRequiredHashes ?? [];
102
+ console.log(` 📤 Uploading ${required.length} file(s) (${files.length - required.length} cached)`);
103
+ // 4. Upload each required file
104
+ const token = await getFirebaseToken(['https://www.googleapis.com/auth/firebase.hosting']);
105
+ for (const hash of required) {
106
+ const absPath = absoluteToUrl[hash];
107
+ const content = readFileSync(absPath);
108
+ const contentType = CONTENT_TYPES[extname(absPath).toLowerCase()] ?? 'application/octet-stream';
109
+ const res = await fetch(`${uploadUrl}/${hash}`, {
110
+ method: 'POST',
111
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': contentType },
112
+ body: content,
113
+ });
114
+ if (!res.ok)
115
+ throw new Error(`Upload failed for ${absPath}: ${res.status}`);
116
+ }
117
+ // 5. Finalize the version
118
+ await hostingFetch(`/sites/${siteId}/versions/${versionId}`, {
119
+ method: 'PATCH',
120
+ body: JSON.stringify({ status: 'FINALIZED' }),
121
+ });
122
+ // 6. Create a release
123
+ await hostingFetch(`/sites/${siteId}/releases?versionName=sites/${siteId}/versions/${versionId}`, {
124
+ method: 'POST',
125
+ body: JSON.stringify({}),
126
+ });
127
+ const url = `https://${siteId}.web.app`;
128
+ console.log(`🚀 Deployed ${files.length} file(s) → ${url}`);
129
+ return { siteId, url };
130
+ }
131
+ async destroy() {
132
+ const dryRun = this.isDryRunActive();
133
+ console.log(`\n🗑️ Destroying Firebase Hosting site "${this.name}"...`);
134
+ if (dryRun) {
135
+ console.log(` 📝 [PLAN] Delete all releases for site "${this.name}"`);
136
+ return { destroyed: this.name };
137
+ }
138
+ // Firebase doesn't support deleting the default site — we can only roll back releases.
139
+ // For non-default sites, the Sites API supports deletion (requires Blaze plan).
140
+ console.log(` ℹ️ Firebase Hosting sites cannot be deleted via API (default site is permanent).`);
141
+ console.log(` To unpublish, go to the Firebase console and disable Hosting.`);
142
+ return { destroyed: this.name };
143
+ }
144
+ }
@@ -0,0 +1,15 @@
1
+ import { FirebaseHostingBuilder } from './hosting.js';
2
+ import { FirebaseFunctionsBuilder, FUNCTIONS_RUNTIME } from './functions.js';
3
+ import { FirebaseFirestoreBuilder } from './firestore.js';
4
+ import { FirebaseStorageBuilder } from './storage.js';
5
+ import { FirebaseAuthBuilder } from './auth.js';
6
+ import { FirebaseRemoteConfigBuilder } from './remoteconfig.js';
7
+ export { FUNCTIONS_RUNTIME };
8
+ export declare const Firebase: {
9
+ Hosting: (siteId: string) => FirebaseHostingBuilder;
10
+ Functions: (functionName: string) => FirebaseFunctionsBuilder;
11
+ Firestore: (database?: string) => FirebaseFirestoreBuilder;
12
+ Storage: (bucket?: string) => FirebaseStorageBuilder;
13
+ Auth: () => FirebaseAuthBuilder;
14
+ RemoteConfig: () => FirebaseRemoteConfigBuilder;
15
+ };
@@ -0,0 +1,15 @@
1
+ import { FirebaseHostingBuilder } from './hosting.js';
2
+ import { FirebaseFunctionsBuilder, FUNCTIONS_RUNTIME } from './functions.js';
3
+ import { FirebaseFirestoreBuilder } from './firestore.js';
4
+ import { FirebaseStorageBuilder } from './storage.js';
5
+ import { FirebaseAuthBuilder } from './auth.js';
6
+ import { FirebaseRemoteConfigBuilder } from './remoteconfig.js';
7
+ export { FUNCTIONS_RUNTIME };
8
+ export const Firebase = {
9
+ Hosting: (siteId) => new FirebaseHostingBuilder(siteId),
10
+ Functions: (functionName) => new FirebaseFunctionsBuilder(functionName),
11
+ Firestore: (database = '(default)') => new FirebaseFirestoreBuilder(database),
12
+ Storage: (bucket) => new FirebaseStorageBuilder(bucket),
13
+ Auth: () => new FirebaseAuthBuilder(),
14
+ RemoteConfig: () => new FirebaseRemoteConfigBuilder(),
15
+ };
@@ -0,0 +1,22 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ type ParamValue = string | number | boolean | object;
3
+ export declare class FirebaseRemoteConfigBuilder extends BaseBuilder {
4
+ private _params;
5
+ private _conditions;
6
+ constructor();
7
+ string(key: string, value: string, description?: string): this;
8
+ bool(key: string, value: boolean, description?: string): this;
9
+ number(key: string, value: number, description?: string): this;
10
+ json(key: string, value: object, description?: string): this;
11
+ param(key: string, value: ParamValue, description?: string): this;
12
+ condition(name: string, expression: string): this;
13
+ override(paramKey: string, conditionName: string, value: ParamValue): this;
14
+ private rcFetch;
15
+ deploy(): Promise<{
16
+ project: string;
17
+ }>;
18
+ destroy(): Promise<{
19
+ destroyed: string;
20
+ }>;
21
+ }
22
+ export {};
@@ -0,0 +1,135 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { getFirebaseToken, getProjectId } from './api.js';
3
+ const RC_BASE = 'https://firebaseremoteconfig.googleapis.com/v1';
4
+ const RC_SCOPE = 'https://www.googleapis.com/auth/firebase.remoteconfig';
5
+ function inferType(v) {
6
+ if (typeof v === 'boolean')
7
+ return 'BOOLEAN';
8
+ if (typeof v === 'number')
9
+ return 'NUMBER';
10
+ if (typeof v === 'object')
11
+ return 'JSON';
12
+ return 'STRING';
13
+ }
14
+ function serialize(v) {
15
+ return typeof v === 'object' ? JSON.stringify(v) : String(v);
16
+ }
17
+ export class FirebaseRemoteConfigBuilder extends BaseBuilder {
18
+ _params = [];
19
+ _conditions = [];
20
+ constructor() {
21
+ super('remoteconfig');
22
+ this.discoveryPromise = Promise.resolve(null);
23
+ }
24
+ string(key, value, description) {
25
+ this._params.push({ key, value, type: 'STRING', description });
26
+ return this;
27
+ }
28
+ bool(key, value, description) {
29
+ this._params.push({ key, value, type: 'BOOLEAN', description });
30
+ return this;
31
+ }
32
+ number(key, value, description) {
33
+ this._params.push({ key, value, type: 'NUMBER', description });
34
+ return this;
35
+ }
36
+ json(key, value, description) {
37
+ this._params.push({ key, value, type: 'JSON', description });
38
+ return this;
39
+ }
40
+ // Generic param — type inferred from value
41
+ param(key, value, description) {
42
+ this._params.push({ key, value, type: inferType(value), description });
43
+ return this;
44
+ }
45
+ condition(name, expression) {
46
+ this._conditions.push({ name, expression });
47
+ return this;
48
+ }
49
+ // Override a param's value for a specific condition
50
+ override(paramKey, conditionName, value) {
51
+ const p = this._params.find(p => p.key === paramKey);
52
+ if (!p)
53
+ throw new Error(`[RemoteConfig] param "${paramKey}" not defined — call .param()/.bool()/etc. first`);
54
+ p.conditionalValues ??= {};
55
+ p.conditionalValues[conditionName] = value;
56
+ return this;
57
+ }
58
+ // ── Internal fetch with ETag support ─────────────────────────────────────
59
+ async rcFetch(path, opts = {}) {
60
+ const token = await getFirebaseToken([RC_SCOPE]);
61
+ const headers = {
62
+ 'Authorization': `Bearer ${token}`,
63
+ 'Content-Type': 'application/json',
64
+ };
65
+ if (opts.etag)
66
+ headers['If-Match'] = opts.etag;
67
+ const res = await fetch(`${RC_BASE}${path}`, { ...opts, headers });
68
+ if (!res.ok) {
69
+ const body = await res.text();
70
+ throw new Error(`RemoteConfig API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
71
+ }
72
+ const text = await res.text();
73
+ return { data: text ? JSON.parse(text) : null, etag: res.headers.get('etag') };
74
+ }
75
+ // ── Deploy ────────────────────────────────────────────────────────────────
76
+ async deploy() {
77
+ const dryRun = this.isDryRunActive();
78
+ const project = getProjectId();
79
+ console.log(`\n🎛️ Finalizing Firebase RemoteConfig...`);
80
+ if (dryRun) {
81
+ console.log(` 📝 [PLAN] Set ${this._params.length} parameter(s):`);
82
+ for (const p of this._params) {
83
+ const overrides = p.conditionalValues ? ` + ${Object.keys(p.conditionalValues).length} override(s)` : '';
84
+ console.log(` └─ ${p.key} (${p.type}): ${serialize(p.value)}${overrides}`);
85
+ }
86
+ if (this._conditions.length) {
87
+ console.log(` 📝 [PLAN] Set ${this._conditions.length} condition(s):`);
88
+ for (const c of this._conditions)
89
+ console.log(` └─ "${c.name}": ${c.expression}`);
90
+ }
91
+ return { project };
92
+ }
93
+ // Fetch current template + ETag
94
+ const { data: current, etag } = await this.rcFetch(`/projects/${project}/remoteConfig`);
95
+ // Merge our params into the existing template (non-destructive — preserves params we didn't define)
96
+ const parameters = { ...(current?.parameters ?? {}) };
97
+ for (const p of this._params) {
98
+ const entry = {
99
+ defaultValue: { value: serialize(p.value) },
100
+ valueType: p.type,
101
+ };
102
+ if (p.description)
103
+ entry.description = p.description;
104
+ if (p.conditionalValues) {
105
+ entry.conditionalValues = {};
106
+ for (const [cond, val] of Object.entries(p.conditionalValues)) {
107
+ entry.conditionalValues[cond] = { value: serialize(val) };
108
+ }
109
+ }
110
+ parameters[p.key] = entry;
111
+ }
112
+ // Merge conditions (add new ones, preserve existing)
113
+ const existingConditions = current?.conditions ?? [];
114
+ const existingNames = new Set(existingConditions.map((c) => c.name));
115
+ const conditions = [
116
+ ...existingConditions,
117
+ ...this._conditions.filter(c => !existingNames.has(c.name)),
118
+ ];
119
+ const body = { parameters, conditions, parameterGroups: current?.parameterGroups ?? {} };
120
+ await this.rcFetch(`/projects/${project}/remoteConfig`, {
121
+ method: 'PUT',
122
+ body: JSON.stringify(body),
123
+ etag: etag ?? '*',
124
+ });
125
+ console.log(` ✅ ${this._params.length} parameter(s) published`);
126
+ if (this._conditions.length)
127
+ console.log(` ✅ ${this._conditions.length} condition(s) set`);
128
+ return { project };
129
+ }
130
+ async destroy() {
131
+ console.log(`\n🗑️ Destroying Firebase RemoteConfig...`);
132
+ console.log(` ℹ️ RemoteConfig templates cannot be deleted via API — clear parameters in the Firebase console`);
133
+ return { destroyed: 'remoteconfig' };
134
+ }
135
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ interface CorsRule {
3
+ origin: string[];
4
+ method: string[];
5
+ responseHeader?: string[];
6
+ maxAge?: number;
7
+ }
8
+ interface LifecycleRule {
9
+ deleteAfterDays?: number;
10
+ matchesPrefix?: string[];
11
+ }
12
+ export declare class FirebaseStorageBuilder extends BaseBuilder {
13
+ private _rulesPath?;
14
+ private _cors;
15
+ private _lifecycle?;
16
+ private _resolvedBucket?;
17
+ constructor(bucket?: string);
18
+ rules(filePath: string): this;
19
+ cors(rules: CorsRule[]): this;
20
+ lifecycle(rule: LifecycleRule): this;
21
+ private bucket;
22
+ private releaseName;
23
+ private deployRules;
24
+ private deployCors;
25
+ private deployLifecycle;
26
+ deploy(): Promise<{
27
+ bucket: string;
28
+ project: string;
29
+ }>;
30
+ destroy(): Promise<{
31
+ destroyed: string;
32
+ }>;
33
+ }
34
+ export {};