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.
- package/LICENSE +7 -0
- package/README.md +148 -0
- package/dist/core/checker.d.ts +5 -0
- package/dist/core/checker.js +148 -0
- package/dist/core/config.d.ts +35 -0
- package/dist/core/config.js +15 -0
- package/dist/core/decorators.d.ts +26 -0
- package/dist/core/decorators.js +86 -0
- package/dist/core/output.d.ts +8 -0
- package/dist/core/output.js +19 -0
- package/dist/core/resource.d.ts +20 -0
- package/dist/core/resource.js +77 -0
- package/dist/core/stack.d.ts +20 -0
- package/dist/core/stack.js +120 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/providers/aws/acm.d.ts +22 -0
- package/dist/providers/aws/acm.js +109 -0
- package/dist/providers/aws/api.d.ts +28 -0
- package/dist/providers/aws/api.js +36 -0
- package/dist/providers/aws/apigateway.d.ts +24 -0
- package/dist/providers/aws/apigateway.js +157 -0
- package/dist/providers/aws/cloudfront.d.ts +31 -0
- package/dist/providers/aws/cloudfront.js +205 -0
- package/dist/providers/aws/fargate.d.ts +43 -0
- package/dist/providers/aws/fargate.js +277 -0
- package/dist/providers/aws/index.d.ts +23 -0
- package/dist/providers/aws/index.js +29 -0
- package/dist/providers/aws/lambda.d.ts +30 -0
- package/dist/providers/aws/lambda.js +159 -0
- package/dist/providers/aws/list.d.ts +2 -0
- package/dist/providers/aws/list.js +44 -0
- package/dist/providers/aws/rds.d.ts +46 -0
- package/dist/providers/aws/rds.js +227 -0
- package/dist/providers/aws/route53.d.ts +38 -0
- package/dist/providers/aws/route53.js +218 -0
- package/dist/providers/aws/s3.d.ts +20 -0
- package/dist/providers/aws/s3.js +165 -0
- package/dist/providers/aws/secrets.d.ts +25 -0
- package/dist/providers/aws/secrets.js +151 -0
- package/dist/providers/aws/sqs.d.ts +33 -0
- package/dist/providers/aws/sqs.js +178 -0
- package/dist/providers/do/api.d.ts +11 -0
- package/dist/providers/do/api.js +52 -0
- package/dist/providers/do/certificate.d.ts +7 -0
- package/dist/providers/do/certificate.js +36 -0
- package/dist/providers/do/domain.d.ts +21 -0
- package/dist/providers/do/domain.js +81 -0
- package/dist/providers/do/droplet.d.ts +35 -0
- package/dist/providers/do/droplet.js +180 -0
- package/dist/providers/do/firewall.d.ts +23 -0
- package/dist/providers/do/firewall.js +94 -0
- package/dist/providers/do/index.d.ts +15 -0
- package/dist/providers/do/index.js +21 -0
- package/dist/providers/do/list.d.ts +2 -0
- package/dist/providers/do/list.js +59 -0
- package/dist/providers/do/load_balancer.d.ts +12 -0
- package/dist/providers/do/load_balancer.js +62 -0
- package/dist/providers/firebase/api.d.ts +4 -0
- package/dist/providers/firebase/api.js +62 -0
- package/dist/providers/firebase/auth.d.ts +35 -0
- package/dist/providers/firebase/auth.js +147 -0
- package/dist/providers/firebase/firestore.d.ts +28 -0
- package/dist/providers/firebase/firestore.js +120 -0
- package/dist/providers/firebase/functions.d.ts +50 -0
- package/dist/providers/firebase/functions.js +163 -0
- package/dist/providers/firebase/hosting.d.ts +14 -0
- package/dist/providers/firebase/hosting.js +144 -0
- package/dist/providers/firebase/index.d.ts +15 -0
- package/dist/providers/firebase/index.js +15 -0
- package/dist/providers/firebase/remoteconfig.d.ts +22 -0
- package/dist/providers/firebase/remoteconfig.js +135 -0
- package/dist/providers/firebase/storage.d.ts +34 -0
- package/dist/providers/firebase/storage.js +117 -0
- package/dist/providers/proxmox/api.d.ts +12 -0
- package/dist/providers/proxmox/api.js +50 -0
- package/dist/providers/proxmox/index.d.ts +15 -0
- package/dist/providers/proxmox/index.js +10 -0
- package/dist/providers/proxmox/list.d.ts +2 -0
- package/dist/providers/proxmox/list.js +15 -0
- package/dist/providers/proxmox/vm.d.ts +61 -0
- package/dist/providers/proxmox/vm.js +482 -0
- package/dist/types/aws.d.ts +55 -0
- package/dist/types/aws.js +48 -0
- package/dist/types/do.d.ts +19 -0
- package/dist/types/do.js +19 -0
- package/dist/types/gcp.d.ts +9 -0
- package/dist/types/gcp.js +9 -0
- package/dist/types/inventory.d.ts +87 -0
- package/dist/types/inventory.js +2 -0
- package/dist/types/proxmox.d.ts +11 -0
- package/dist/types/proxmox.js +28 -0
- package/package.json +56 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BaseBuilder } from '../../core/resource.js';
|
|
2
|
+
import { getDoApi } from './api.js';
|
|
3
|
+
export class FirewallBuilder extends BaseBuilder {
|
|
4
|
+
rules = [];
|
|
5
|
+
dropletNames = [];
|
|
6
|
+
constructor(name) {
|
|
7
|
+
super(name);
|
|
8
|
+
this.discoveryPromise = this.discoverFirewall(name);
|
|
9
|
+
}
|
|
10
|
+
async discoverFirewall(name) {
|
|
11
|
+
const api = getDoApi();
|
|
12
|
+
const data = await api.get('/firewalls?per_page=200');
|
|
13
|
+
return data.firewalls.find(f => f.name === name) ?? null;
|
|
14
|
+
}
|
|
15
|
+
ingress(protocol, port, sources) {
|
|
16
|
+
this.rules.push({ type: 'ingress', protocol, port, sources });
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
egress(protocol, port, destinations) {
|
|
20
|
+
this.rules.push({ type: 'egress', protocol, port, destinations });
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
attachTo(dropletName) {
|
|
24
|
+
this.dropletNames.push(dropletName);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
async resolveDropletIds(api) {
|
|
28
|
+
const ids = [];
|
|
29
|
+
for (const name of this.dropletNames) {
|
|
30
|
+
const data = await api.get(`/droplets?name=${encodeURIComponent(name)}&per_page=200`);
|
|
31
|
+
const match = data.droplets.find(d => d.name === name);
|
|
32
|
+
if (match)
|
|
33
|
+
ids.push(match.id);
|
|
34
|
+
}
|
|
35
|
+
return ids;
|
|
36
|
+
}
|
|
37
|
+
buildApiRules() {
|
|
38
|
+
const inbound = this.rules
|
|
39
|
+
.filter(r => r.type === 'ingress')
|
|
40
|
+
.map(r => ({
|
|
41
|
+
protocol: r.protocol,
|
|
42
|
+
ports: String(r.port),
|
|
43
|
+
sources: { addresses: r.sources ?? [] },
|
|
44
|
+
}));
|
|
45
|
+
const outbound = this.rules
|
|
46
|
+
.filter(r => r.type === 'egress')
|
|
47
|
+
.map(r => ({
|
|
48
|
+
protocol: r.protocol,
|
|
49
|
+
ports: String(r.port),
|
|
50
|
+
destinations: { addresses: r.destinations ?? [] },
|
|
51
|
+
}));
|
|
52
|
+
return { inbound, outbound };
|
|
53
|
+
}
|
|
54
|
+
async deploy() {
|
|
55
|
+
const dryRun = this.isDryRunActive();
|
|
56
|
+
const existing = await this.discoveryPromise;
|
|
57
|
+
const api = getDoApi();
|
|
58
|
+
console.log(`\n🛡️ Finalizing firewall "${this.name}"...`);
|
|
59
|
+
if (dryRun) {
|
|
60
|
+
this.rules.forEach(r => {
|
|
61
|
+
const dir = r.type === 'ingress' ? 'from' : 'to';
|
|
62
|
+
const targets = r.type === 'ingress' ? r.sources : r.destinations;
|
|
63
|
+
console.log(` 📝 [PLAN] ${r.type.toUpperCase()}: ${r.protocol.toUpperCase()} ${r.port} ${dir} [${targets?.join(', ')}]`);
|
|
64
|
+
});
|
|
65
|
+
return { name: this.name, rules: this.rules };
|
|
66
|
+
}
|
|
67
|
+
const dropletIds = await this.resolveDropletIds(api);
|
|
68
|
+
const { inbound, outbound } = this.buildApiRules();
|
|
69
|
+
if (existing) {
|
|
70
|
+
await api.put(`/firewalls/${existing.id}`, {
|
|
71
|
+
name: this.name,
|
|
72
|
+
inbound_rules: inbound,
|
|
73
|
+
outbound_rules: outbound,
|
|
74
|
+
droplet_ids: dropletIds,
|
|
75
|
+
});
|
|
76
|
+
console.log(`✨ Updated firewall ${this.name} (id=${existing.id})`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const result = await api.post('/firewalls', {
|
|
80
|
+
name: this.name,
|
|
81
|
+
inbound_rules: inbound,
|
|
82
|
+
outbound_rules: outbound,
|
|
83
|
+
droplet_ids: dropletIds,
|
|
84
|
+
});
|
|
85
|
+
console.log(`🚀 Created firewall ${this.name} (id=${result.firewall.id})`);
|
|
86
|
+
}
|
|
87
|
+
this.rules.forEach(r => {
|
|
88
|
+
const dir = r.type === 'ingress' ? 'from' : 'to';
|
|
89
|
+
const targets = r.type === 'ingress' ? r.sources : r.destinations;
|
|
90
|
+
console.log(` ✅ ${r.type.toUpperCase()}: ${r.protocol.toUpperCase()} ${r.port} ${dir} [${targets?.join(', ')}]`);
|
|
91
|
+
});
|
|
92
|
+
return { name: this.name, rules: this.rules };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DropletBuilder } from "./droplet.js";
|
|
2
|
+
import { DomainBuilder } from "./domain.js";
|
|
3
|
+
import { FirewallBuilder } from "./firewall.js";
|
|
4
|
+
import { CertificateBuilder } from "./certificate.js";
|
|
5
|
+
import { LoadBalancerBuilder } from "./load_balancer.js";
|
|
6
|
+
export declare const DO: {
|
|
7
|
+
init: (opts: {
|
|
8
|
+
token: string;
|
|
9
|
+
}) => void;
|
|
10
|
+
Droplet: (name: string) => DropletBuilder;
|
|
11
|
+
Domain: (name: string) => DomainBuilder;
|
|
12
|
+
Firewall: (name: string) => FirewallBuilder;
|
|
13
|
+
Certificate: (name: string) => CertificateBuilder;
|
|
14
|
+
LoadBalancer: (name: string) => LoadBalancerBuilder;
|
|
15
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Config } from "../../core/config.js";
|
|
2
|
+
import { DropletBuilder } from "./droplet.js";
|
|
3
|
+
import { DomainBuilder } from "./domain.js";
|
|
4
|
+
import { FirewallBuilder } from "./firewall.js";
|
|
5
|
+
import { CertificateBuilder } from "./certificate.js";
|
|
6
|
+
import { LoadBalancerBuilder } from "./load_balancer.js";
|
|
7
|
+
export const DO = {
|
|
8
|
+
init: (opts) => {
|
|
9
|
+
Config.set({
|
|
10
|
+
providers: {
|
|
11
|
+
...Config.get().providers,
|
|
12
|
+
do: opts,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
Droplet: (name) => new DropletBuilder(name),
|
|
17
|
+
Domain: (name) => new DomainBuilder(name),
|
|
18
|
+
Firewall: (name) => new FirewallBuilder(name),
|
|
19
|
+
Certificate: (name) => new CertificateBuilder(name),
|
|
20
|
+
LoadBalancer: (name) => new LoadBalancerBuilder(name),
|
|
21
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getDoApi } from './api.js';
|
|
2
|
+
const DO_PRICING = {
|
|
3
|
+
's-1vcpu-1gb': 6,
|
|
4
|
+
's-1vcpu-2gb': 12,
|
|
5
|
+
's-2vcpu-2gb': 18,
|
|
6
|
+
's-2vcpu-4gb': 24,
|
|
7
|
+
's-4vcpu-8gb': 48,
|
|
8
|
+
's-6vcpu-16gb': 96,
|
|
9
|
+
's-8vcpu-32gb': 192,
|
|
10
|
+
'c-2': 42,
|
|
11
|
+
'c-4': 84,
|
|
12
|
+
'c-8': 168,
|
|
13
|
+
'g-2vcpu-8gb': 60,
|
|
14
|
+
'g-4vcpu-16gb': 120,
|
|
15
|
+
'm-2vcpu-16gb': 90,
|
|
16
|
+
'm-4vcpu-32gb': 180,
|
|
17
|
+
};
|
|
18
|
+
function priceForSlug(slug) {
|
|
19
|
+
return DO_PRICING[slug] ?? 0;
|
|
20
|
+
}
|
|
21
|
+
export async function listDoResources() {
|
|
22
|
+
const api = getDoApi();
|
|
23
|
+
const [dropletsData, firewallsData, lbData, domainsData] = await Promise.all([
|
|
24
|
+
api.get('/droplets?per_page=200'),
|
|
25
|
+
api.get('/firewalls?per_page=200'),
|
|
26
|
+
api.get('/load_balancers?per_page=200'),
|
|
27
|
+
api.get('/domains?per_page=200'),
|
|
28
|
+
]);
|
|
29
|
+
const droplets = dropletsData.droplets.map((d) => {
|
|
30
|
+
const pub = (d.networks?.v4 ?? []).find((n) => n.type === 'public');
|
|
31
|
+
return {
|
|
32
|
+
id: d.id,
|
|
33
|
+
name: d.name,
|
|
34
|
+
status: d.status,
|
|
35
|
+
region: d.region?.slug ?? d.region ?? '',
|
|
36
|
+
size: d.size_slug,
|
|
37
|
+
ip: pub?.ip_address,
|
|
38
|
+
monthlyCost: priceForSlug(d.size_slug),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const firewalls = firewallsData.firewalls.map((f) => ({
|
|
42
|
+
id: f.id,
|
|
43
|
+
name: f.name,
|
|
44
|
+
dropletCount: (f.droplet_ids ?? []).length,
|
|
45
|
+
}));
|
|
46
|
+
const loadBalancers = lbData.load_balancers.map((lb) => ({
|
|
47
|
+
id: lb.id,
|
|
48
|
+
name: lb.name,
|
|
49
|
+
ip: lb.ip ?? '',
|
|
50
|
+
region: lb.region?.slug ?? lb.region ?? '',
|
|
51
|
+
status: lb.status,
|
|
52
|
+
}));
|
|
53
|
+
const domains = domainsData.domains.map((d) => ({
|
|
54
|
+
name: d.name,
|
|
55
|
+
ttl: d.ttl,
|
|
56
|
+
}));
|
|
57
|
+
const totalMonthlyCost = droplets.reduce((sum, d) => sum + d.monthlyCost, 0);
|
|
58
|
+
return { droplets, firewalls, loadBalancers, domains, totalMonthlyCost };
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DropletBuilder } from './droplet.js';
|
|
2
|
+
export declare class LoadBalancerBuilder {
|
|
3
|
+
private name;
|
|
4
|
+
private config;
|
|
5
|
+
private targetNames;
|
|
6
|
+
private discoveryPromise;
|
|
7
|
+
constructor(name: string);
|
|
8
|
+
private discoverLb;
|
|
9
|
+
private resolveDropletIds;
|
|
10
|
+
targets(droplets: (DropletBuilder | string)[]): this;
|
|
11
|
+
deploy(): Promise<any>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Config } from '../../core/config.js';
|
|
2
|
+
import { getDoApi } from './api.js';
|
|
3
|
+
export class LoadBalancerBuilder {
|
|
4
|
+
name;
|
|
5
|
+
config = {
|
|
6
|
+
region: Config.get().providers.do?.defaultRegion ?? 'fra1',
|
|
7
|
+
};
|
|
8
|
+
targetNames = [];
|
|
9
|
+
discoveryPromise;
|
|
10
|
+
constructor(name) {
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.discoveryPromise = this.discoverLb(name);
|
|
13
|
+
}
|
|
14
|
+
async discoverLb(name) {
|
|
15
|
+
const api = getDoApi();
|
|
16
|
+
const data = await api.get('/load_balancers?per_page=200');
|
|
17
|
+
return data.load_balancers.find(lb => lb.name === name) ?? null;
|
|
18
|
+
}
|
|
19
|
+
async resolveDropletIds() {
|
|
20
|
+
const api = getDoApi();
|
|
21
|
+
const ids = [];
|
|
22
|
+
for (const name of this.targetNames) {
|
|
23
|
+
const data = await api.get(`/droplets?name=${encodeURIComponent(name)}&per_page=200`);
|
|
24
|
+
const match = data.droplets.find(d => d.name === name);
|
|
25
|
+
if (match)
|
|
26
|
+
ids.push(match.id);
|
|
27
|
+
}
|
|
28
|
+
return ids;
|
|
29
|
+
}
|
|
30
|
+
targets(droplets) {
|
|
31
|
+
this.targetNames = droplets.map(d => (typeof d === 'string' ? d : d.name));
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
async deploy() {
|
|
35
|
+
const dryRun = Config.isGlobalDryRun();
|
|
36
|
+
const existing = await this.discoveryPromise;
|
|
37
|
+
console.log(`\n⚖️ Finalizing load balancer "${this.name}"...`);
|
|
38
|
+
if (existing) {
|
|
39
|
+
console.log(`✅ Load balancer ${this.name} already exists (ip=${existing.ip}).`);
|
|
40
|
+
return existing;
|
|
41
|
+
}
|
|
42
|
+
if (dryRun) {
|
|
43
|
+
console.log(` 📝 [PLAN] Create load balancer ${this.name} targeting [${this.targetNames.join(', ')}]`);
|
|
44
|
+
return this.config;
|
|
45
|
+
}
|
|
46
|
+
const dropletIds = await this.resolveDropletIds();
|
|
47
|
+
const api = getDoApi();
|
|
48
|
+
const result = await api.post('/load_balancers', {
|
|
49
|
+
name: this.name,
|
|
50
|
+
region: this.config.region,
|
|
51
|
+
forwarding_rules: [
|
|
52
|
+
{ entry_protocol: 'http', entry_port: 80, target_protocol: 'http', target_port: 80 },
|
|
53
|
+
{ entry_protocol: 'https', entry_port: 443, target_protocol: 'http', target_port: 80 },
|
|
54
|
+
],
|
|
55
|
+
droplet_ids: dropletIds,
|
|
56
|
+
});
|
|
57
|
+
console.log(`🚀 Created load balancer ${this.name} (id=${result.load_balancer.id})`);
|
|
58
|
+
if (this.targetNames.length)
|
|
59
|
+
console.log(` Targets: [${this.targetNames.join(', ')}]`);
|
|
60
|
+
return result.load_balancer;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function getProjectId(): string;
|
|
2
|
+
export declare function getFirebaseToken(scopes: string[]): Promise<string>;
|
|
3
|
+
export declare function hostingFetch(path: string, opts?: RequestInit): Promise<any>;
|
|
4
|
+
export declare function cloudFetch(base: string, path: string, opts?: RequestInit): Promise<any>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
3
|
+
import { Config } from '../../core/config.js';
|
|
4
|
+
function resolveFirebaseConfig() {
|
|
5
|
+
const cfg = Config.get().providers.firebase;
|
|
6
|
+
if (cfg?.serviceAccountPath)
|
|
7
|
+
return cfg;
|
|
8
|
+
// Fallback: auto-configure from FIREBASE_SA env var so the decorator option is optional
|
|
9
|
+
const saPath = process.env.FIREBASE_SA;
|
|
10
|
+
if (saPath) {
|
|
11
|
+
const sa = JSON.parse(readFileSync(saPath, 'utf8'));
|
|
12
|
+
return { projectId: sa.project_id, serviceAccountPath: saPath };
|
|
13
|
+
}
|
|
14
|
+
throw new Error('Firebase not configured. Set FIREBASE_SA=/path/to/sa.json or use @Deploy({ firebase: "..." })');
|
|
15
|
+
}
|
|
16
|
+
export function getProjectId() {
|
|
17
|
+
return resolveFirebaseConfig().projectId;
|
|
18
|
+
}
|
|
19
|
+
export async function getFirebaseToken(scopes) {
|
|
20
|
+
const { serviceAccountPath } = resolveFirebaseConfig();
|
|
21
|
+
const auth = new GoogleAuth({ keyFile: serviceAccountPath, scopes });
|
|
22
|
+
const client = await auth.getClient();
|
|
23
|
+
const token = await client.getAccessToken();
|
|
24
|
+
return token.token;
|
|
25
|
+
}
|
|
26
|
+
const HOSTING_SCOPE = 'https://www.googleapis.com/auth/firebase.hosting';
|
|
27
|
+
const CLOUD_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
|
|
28
|
+
export async function hostingFetch(path, opts = {}) {
|
|
29
|
+
const token = await getFirebaseToken([HOSTING_SCOPE]);
|
|
30
|
+
const base = 'https://firebasehosting.googleapis.com/v1beta1';
|
|
31
|
+
const res = await fetch(`${base}${path}`, {
|
|
32
|
+
...opts,
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': `Bearer ${token}`,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
...(opts.headers ?? {}),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const body = await res.text();
|
|
41
|
+
throw new Error(`Firebase Hosting API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
|
|
42
|
+
}
|
|
43
|
+
const text = await res.text();
|
|
44
|
+
return text ? JSON.parse(text) : null;
|
|
45
|
+
}
|
|
46
|
+
export async function cloudFetch(base, path, opts = {}) {
|
|
47
|
+
const token = await getFirebaseToken([CLOUD_SCOPE]);
|
|
48
|
+
const res = await fetch(`${base}${path}`, {
|
|
49
|
+
...opts,
|
|
50
|
+
headers: {
|
|
51
|
+
'Authorization': `Bearer ${token}`,
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
...(opts.headers ?? {}),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const body = await res.text();
|
|
58
|
+
throw new Error(`GCP API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
|
|
59
|
+
}
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
return text ? JSON.parse(text) : null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseBuilder } from '../../core/resource.js';
|
|
2
|
+
interface OAuthCredentials {
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class FirebaseAuthBuilder extends BaseBuilder {
|
|
7
|
+
private _providers;
|
|
8
|
+
private _allowedDomains?;
|
|
9
|
+
private _authorizedDomains?;
|
|
10
|
+
constructor();
|
|
11
|
+
emailPassword(opts?: {
|
|
12
|
+
passwordRequired?: boolean;
|
|
13
|
+
}): this;
|
|
14
|
+
anonymous(): this;
|
|
15
|
+
phone(): this;
|
|
16
|
+
google(creds: OAuthCredentials): this;
|
|
17
|
+
github(creds: OAuthCredentials): this;
|
|
18
|
+
facebook(creds: OAuthCredentials): this;
|
|
19
|
+
twitter(creds: OAuthCredentials): this;
|
|
20
|
+
apple(creds: OAuthCredentials): this;
|
|
21
|
+
microsoft(creds: OAuthCredentials): this;
|
|
22
|
+
authorizedDomains(domains: string[]): this;
|
|
23
|
+
private projectPath;
|
|
24
|
+
private getConfig;
|
|
25
|
+
private patchConfig;
|
|
26
|
+
private getIdp;
|
|
27
|
+
private upsertIdp;
|
|
28
|
+
deploy(): Promise<{
|
|
29
|
+
project: string;
|
|
30
|
+
}>;
|
|
31
|
+
destroy(): Promise<{
|
|
32
|
+
destroyed: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { BaseBuilder } from '../../core/resource.js';
|
|
2
|
+
import { cloudFetch, getProjectId } from './api.js';
|
|
3
|
+
const AUTH_BASE = 'https://identitytoolkit.googleapis.com/admin/v2';
|
|
4
|
+
const IDP_ID = {
|
|
5
|
+
google: 'google.com',
|
|
6
|
+
github: 'github.com',
|
|
7
|
+
facebook: 'facebook.com',
|
|
8
|
+
twitter: 'twitter.com',
|
|
9
|
+
apple: 'apple.com',
|
|
10
|
+
microsoft: 'microsoft.com',
|
|
11
|
+
};
|
|
12
|
+
export class FirebaseAuthBuilder extends BaseBuilder {
|
|
13
|
+
_providers = {};
|
|
14
|
+
_allowedDomains;
|
|
15
|
+
_authorizedDomains;
|
|
16
|
+
constructor() {
|
|
17
|
+
super('auth');
|
|
18
|
+
this.discoveryPromise = Promise.resolve(null);
|
|
19
|
+
}
|
|
20
|
+
emailPassword(opts = {}) {
|
|
21
|
+
this._providers.emailPassword = { enabled: true, ...opts };
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
anonymous() { this._providers.anonymous = { enabled: true }; return this; }
|
|
25
|
+
phone() { this._providers.phone = { enabled: true }; return this; }
|
|
26
|
+
google(creds) { this._providers.google = { enabled: true, ...creds }; return this; }
|
|
27
|
+
github(creds) { this._providers.github = { enabled: true, ...creds }; return this; }
|
|
28
|
+
facebook(creds) { this._providers.facebook = { enabled: true, ...creds }; return this; }
|
|
29
|
+
twitter(creds) { this._providers.twitter = { enabled: true, ...creds }; return this; }
|
|
30
|
+
apple(creds) { this._providers.apple = { enabled: true, ...creds }; return this; }
|
|
31
|
+
microsoft(creds) { this._providers.microsoft = { enabled: true, ...creds }; return this; }
|
|
32
|
+
// Domains allowed to use Firebase Auth (e.g. your app domain + localhost)
|
|
33
|
+
authorizedDomains(domains) { this._authorizedDomains = domains; return this; }
|
|
34
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
35
|
+
projectPath() { return `/projects/${getProjectId()}`; }
|
|
36
|
+
async getConfig() {
|
|
37
|
+
try {
|
|
38
|
+
return await cloudFetch(AUTH_BASE, `${this.projectPath()}/config`);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async patchConfig(body, updateMask) {
|
|
45
|
+
await cloudFetch(AUTH_BASE, `${this.projectPath()}/config?updateMask=${updateMask}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
46
|
+
}
|
|
47
|
+
async getIdp(provider) {
|
|
48
|
+
try {
|
|
49
|
+
return await cloudFetch(AUTH_BASE, `${this.projectPath()}/defaultSupportedIdpConfigs/${IDP_ID[provider]}`);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async upsertIdp(provider, creds) {
|
|
56
|
+
const idpId = IDP_ID[provider];
|
|
57
|
+
const existing = await this.getIdp(provider);
|
|
58
|
+
const body = {
|
|
59
|
+
name: `${this.projectPath().slice(1)}/defaultSupportedIdpConfigs/${idpId}`,
|
|
60
|
+
clientId: creds.clientId,
|
|
61
|
+
clientSecret: creds.clientSecret,
|
|
62
|
+
enabled: creds.enabled ?? true,
|
|
63
|
+
};
|
|
64
|
+
if (existing) {
|
|
65
|
+
await cloudFetch(AUTH_BASE, `${this.projectPath()}/defaultSupportedIdpConfigs/${idpId}?updateMask=clientId,clientSecret,enabled`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
await cloudFetch(AUTH_BASE, `${this.projectPath()}/defaultSupportedIdpConfigs?idpId=${idpId}`, { method: 'POST', body: JSON.stringify(body) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ── Deploy ────────────────────────────────────────────────────────────────
|
|
72
|
+
async deploy() {
|
|
73
|
+
const dryRun = this.isDryRunActive();
|
|
74
|
+
console.log(`\n🔑 Finalizing Firebase Auth...`);
|
|
75
|
+
const { emailPassword, anonymous, phone, ...oauthProviders } = this._providers;
|
|
76
|
+
const hasBuiltIn = emailPassword || anonymous || phone;
|
|
77
|
+
const oauthEntries = Object.entries(oauthProviders);
|
|
78
|
+
if (dryRun) {
|
|
79
|
+
if (hasBuiltIn) {
|
|
80
|
+
console.log(` 📝 [PLAN] Configure sign-in methods:`);
|
|
81
|
+
if (emailPassword)
|
|
82
|
+
console.log(` └─ Email/Password (passwordRequired: ${emailPassword.passwordRequired ?? true})`);
|
|
83
|
+
if (anonymous)
|
|
84
|
+
console.log(` └─ Anonymous`);
|
|
85
|
+
if (phone)
|
|
86
|
+
console.log(` └─ Phone`);
|
|
87
|
+
}
|
|
88
|
+
for (const [name] of oauthEntries) {
|
|
89
|
+
console.log(` 📝 [PLAN] Enable OAuth provider: ${IDP_ID[name]}`);
|
|
90
|
+
}
|
|
91
|
+
if (this._authorizedDomains) {
|
|
92
|
+
console.log(` 📝 [PLAN] Set authorized domains: [${this._authorizedDomains.join(', ')}]`);
|
|
93
|
+
}
|
|
94
|
+
return { project: getProjectId() };
|
|
95
|
+
}
|
|
96
|
+
// Built-in providers via project config
|
|
97
|
+
if (hasBuiltIn || this._authorizedDomains) {
|
|
98
|
+
const signIn = {};
|
|
99
|
+
const masks = [];
|
|
100
|
+
if (emailPassword) {
|
|
101
|
+
signIn.email = { enabled: true, passwordRequired: emailPassword.passwordRequired ?? true };
|
|
102
|
+
masks.push('signIn.email');
|
|
103
|
+
}
|
|
104
|
+
if (anonymous) {
|
|
105
|
+
signIn.anonymous = { enabled: true };
|
|
106
|
+
masks.push('signIn.anonymous');
|
|
107
|
+
}
|
|
108
|
+
if (phone) {
|
|
109
|
+
signIn.phoneNumber = { enabled: true };
|
|
110
|
+
masks.push('signIn.phoneNumber');
|
|
111
|
+
}
|
|
112
|
+
const body = {};
|
|
113
|
+
if (masks.length)
|
|
114
|
+
body.signIn = signIn;
|
|
115
|
+
if (this._authorizedDomains) {
|
|
116
|
+
body.authorizedDomains = this._authorizedDomains;
|
|
117
|
+
masks.push('authorizedDomains');
|
|
118
|
+
}
|
|
119
|
+
await this.patchConfig(body, masks.join(','));
|
|
120
|
+
if (emailPassword)
|
|
121
|
+
console.log(` ✅ Email/Password enabled`);
|
|
122
|
+
if (anonymous)
|
|
123
|
+
console.log(` ✅ Anonymous sign-in enabled`);
|
|
124
|
+
if (phone)
|
|
125
|
+
console.log(` ✅ Phone sign-in enabled`);
|
|
126
|
+
if (this._authorizedDomains)
|
|
127
|
+
console.log(` ✅ Authorized domains set: [${this._authorizedDomains.join(', ')}]`);
|
|
128
|
+
}
|
|
129
|
+
// OAuth providers
|
|
130
|
+
for (const [name, creds] of oauthEntries) {
|
|
131
|
+
await this.upsertIdp(name, creds);
|
|
132
|
+
console.log(` ✅ ${IDP_ID[name]} provider configured`);
|
|
133
|
+
}
|
|
134
|
+
return { project: getProjectId() };
|
|
135
|
+
}
|
|
136
|
+
async destroy() {
|
|
137
|
+
const dryRun = this.isDryRunActive();
|
|
138
|
+
console.log(`\n🗑️ Destroying Firebase Auth config...`);
|
|
139
|
+
if (dryRun) {
|
|
140
|
+
console.log(` ℹ️ Auth providers can be disabled individually — destroying the Auth config is not supported via API`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(` ℹ️ Disable providers individually in the Firebase console`);
|
|
144
|
+
}
|
|
145
|
+
return { destroyed: 'auth' };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseBuilder } from '../../core/resource.js';
|
|
2
|
+
type FieldOrder = 'ASCENDING' | 'DESCENDING';
|
|
3
|
+
interface IndexField {
|
|
4
|
+
field: string;
|
|
5
|
+
order: FieldOrder;
|
|
6
|
+
}
|
|
7
|
+
export declare class FirebaseFirestoreBuilder extends BaseBuilder {
|
|
8
|
+
private _rulesPath?;
|
|
9
|
+
private _indexes;
|
|
10
|
+
constructor(database?: string);
|
|
11
|
+
rules(filePath: string): this;
|
|
12
|
+
index(collection: string, fields: IndexField[]): this;
|
|
13
|
+
private rulesRelease;
|
|
14
|
+
private currentRulesReleaseRuleset;
|
|
15
|
+
private deployRules;
|
|
16
|
+
private dbPath;
|
|
17
|
+
private listExistingIndexes;
|
|
18
|
+
private indexKey;
|
|
19
|
+
private deployIndexes;
|
|
20
|
+
deploy(): Promise<{
|
|
21
|
+
database: string;
|
|
22
|
+
project: string;
|
|
23
|
+
}>;
|
|
24
|
+
destroy(): Promise<{
|
|
25
|
+
destroyed: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { BaseBuilder } from '../../core/resource.js';
|
|
3
|
+
import { cloudFetch, getProjectId } from './api.js';
|
|
4
|
+
const RULES_BASE = 'https://firebaserules.googleapis.com/v1';
|
|
5
|
+
const FS_BASE = 'https://firestore.googleapis.com/v1';
|
|
6
|
+
export class FirebaseFirestoreBuilder extends BaseBuilder {
|
|
7
|
+
_rulesPath;
|
|
8
|
+
_indexes = [];
|
|
9
|
+
constructor(database = '(default)') {
|
|
10
|
+
super(database);
|
|
11
|
+
this.discoveryPromise = Promise.resolve(null);
|
|
12
|
+
}
|
|
13
|
+
rules(filePath) { this._rulesPath = filePath; return this; }
|
|
14
|
+
index(collection, fields) { this._indexes.push({ collection, fields }); return this; }
|
|
15
|
+
// ── Rules ────────────────────────────────────────────────────────────────
|
|
16
|
+
rulesRelease() {
|
|
17
|
+
return `projects/${getProjectId()}/releases/cloud.firestore`;
|
|
18
|
+
}
|
|
19
|
+
async currentRulesReleaseRuleset() {
|
|
20
|
+
try {
|
|
21
|
+
const rel = await cloudFetch(RULES_BASE, `/${this.rulesRelease()}`);
|
|
22
|
+
return rel?.rulesetName ?? null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async deployRules(dryRun) {
|
|
29
|
+
if (!this._rulesPath)
|
|
30
|
+
return;
|
|
31
|
+
const source = readFileSync(this._rulesPath, 'utf8');
|
|
32
|
+
const current = await this.currentRulesReleaseRuleset();
|
|
33
|
+
if (dryRun) {
|
|
34
|
+
console.log(` 📝 [PLAN] Deploy Firestore rules from "${this._rulesPath}"`);
|
|
35
|
+
if (current)
|
|
36
|
+
console.log(` └─ replaces ruleset: ${current.split('/').pop()}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const ruleset = await cloudFetch(RULES_BASE, `/projects/${getProjectId()}/rulesets`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: JSON.stringify({ source: { files: [{ name: 'firestore.rules', content: source }] } }),
|
|
42
|
+
});
|
|
43
|
+
await cloudFetch(RULES_BASE, `/${this.rulesRelease()}`, {
|
|
44
|
+
method: 'PUT',
|
|
45
|
+
body: JSON.stringify({ name: this.rulesRelease(), rulesetName: ruleset.name }),
|
|
46
|
+
});
|
|
47
|
+
console.log(` ✅ Rules deployed (ruleset: ${ruleset.name.split('/').pop()})`);
|
|
48
|
+
}
|
|
49
|
+
// ── Indexes ───────────────────────────────────────────────────────────────
|
|
50
|
+
dbPath() {
|
|
51
|
+
return `projects/${getProjectId()}/databases/${this.name}`;
|
|
52
|
+
}
|
|
53
|
+
async listExistingIndexes() {
|
|
54
|
+
try {
|
|
55
|
+
const res = await cloudFetch(FS_BASE, `/${this.dbPath()}/collectionGroups/-/indexes`);
|
|
56
|
+
return res?.indexes ?? [];
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
indexKey(collection, fields) {
|
|
63
|
+
return `${collection}:${fields.map(f => `${f.field}:${f.order}`).join(',')}`;
|
|
64
|
+
}
|
|
65
|
+
async deployIndexes(dryRun) {
|
|
66
|
+
if (this._indexes.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
const existing = await this.listExistingIndexes();
|
|
69
|
+
const existingKeys = new Set(existing.map((idx) => {
|
|
70
|
+
const parts = idx.name.split('/collectionGroups/');
|
|
71
|
+
const collection = parts[1]?.split('/')[0] ?? '';
|
|
72
|
+
const fields = (idx.fields ?? [])
|
|
73
|
+
.filter((f) => f.fieldPath !== '__name__')
|
|
74
|
+
.map((f) => ({ field: f.fieldPath, order: f.order }));
|
|
75
|
+
return this.indexKey(collection, fields);
|
|
76
|
+
}));
|
|
77
|
+
const toCreate = this._indexes.filter(i => !existingKeys.has(this.indexKey(i.collection, i.fields)));
|
|
78
|
+
if (dryRun) {
|
|
79
|
+
console.log(` 📝 [PLAN] ${toCreate.length} index(es) to create, ${this._indexes.length - toCreate.length} already exist`);
|
|
80
|
+
for (const idx of toCreate) {
|
|
81
|
+
console.log(` └─ ${idx.collection}: [${idx.fields.map(f => `${f.field} ${f.order}`).join(', ')}]`);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const idx of toCreate) {
|
|
86
|
+
await cloudFetch(FS_BASE, `/${this.dbPath()}/collectionGroups/${idx.collection}/indexes`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
queryScope: 'COLLECTION',
|
|
90
|
+
fields: idx.fields.map(f => ({ fieldPath: f.field, order: f.order })),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
console.log(` ✅ Index created: ${idx.collection} [${idx.fields.map(f => `${f.field} ${f.order}`).join(', ')}]`);
|
|
94
|
+
}
|
|
95
|
+
if (toCreate.length === 0)
|
|
96
|
+
console.log(` ✅ All indexes already exist`);
|
|
97
|
+
}
|
|
98
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
99
|
+
async deploy() {
|
|
100
|
+
console.log(`\n🔥 Finalizing Firestore "${this.name}"...`);
|
|
101
|
+
const dryRun = this.isDryRunActive();
|
|
102
|
+
await this.deployRules(dryRun);
|
|
103
|
+
await this.deployIndexes(dryRun);
|
|
104
|
+
return { database: this.name, project: getProjectId() };
|
|
105
|
+
}
|
|
106
|
+
async destroy() {
|
|
107
|
+
const dryRun = this.isDryRunActive();
|
|
108
|
+
console.log(`\n🗑️ Destroying Firestore config "${this.name}"...`);
|
|
109
|
+
// Firestore databases themselves cannot be deleted via API — only the config managed here
|
|
110
|
+
if (dryRun) {
|
|
111
|
+
if (this._rulesPath)
|
|
112
|
+
console.log(` 📝 [PLAN] Rules release cannot be rolled back via API — do this in the Firebase console`);
|
|
113
|
+
console.log(` ℹ️ Firestore databases cannot be deleted via API`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(` ℹ️ Firestore databases cannot be deleted via API. Remove rules/indexes manually in the Firebase console.`);
|
|
117
|
+
}
|
|
118
|
+
return { destroyed: this.name };
|
|
119
|
+
}
|
|
120
|
+
}
|