puls-dev 0.1.0 → 0.1.8
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/README.md +10 -8
- package/dist/core/checker.d.ts +1 -1
- package/dist/core/checker.js +88 -56
- package/dist/core/config.test.d.ts +1 -0
- package/dist/core/config.test.js +21 -0
- package/dist/core/decorators.js +8 -2
- package/dist/core/output.test.d.ts +1 -0
- package/dist/core/output.test.js +18 -0
- package/dist/core/resource.js +2 -2
- package/dist/core/stack.d.ts +1 -1
- package/dist/core/stack.js +2 -2
- package/dist/providers/aws/acm.d.ts +1 -1
- package/dist/providers/aws/acm.js +27 -23
- package/dist/providers/aws/api.d.ts +14 -14
- package/dist/providers/aws/api.js +21 -21
- package/dist/providers/aws/apigateway.d.ts +2 -2
- package/dist/providers/aws/apigateway.js +33 -29
- package/dist/providers/aws/cloudfront.d.ts +3 -3
- package/dist/providers/aws/cloudfront.js +49 -34
- package/dist/providers/aws/fargate.d.ts +2 -2
- package/dist/providers/aws/fargate.js +99 -52
- package/dist/providers/aws/lambda.d.ts +2 -2
- package/dist/providers/aws/lambda.js +63 -32
- package/dist/providers/aws/rds.d.ts +1 -1
- package/dist/providers/aws/rds.js +77 -39
- package/dist/providers/aws/route53.d.ts +5 -5
- package/dist/providers/aws/route53.js +42 -35
- package/dist/providers/aws/s3.d.ts +2 -2
- package/dist/providers/aws/s3.js +40 -33
- package/dist/providers/aws/secrets.js +15 -7
- package/dist/providers/aws/sqs.d.ts +1 -1
- package/dist/providers/aws/sqs.js +47 -23
- package/dist/providers/do/domain.d.ts +4 -4
- package/dist/providers/do/domain.js +15 -11
- package/dist/providers/firebase/auth.d.ts +1 -1
- package/dist/providers/firebase/auth.js +65 -33
- package/dist/providers/firebase/firestore.d.ts +2 -2
- package/dist/providers/firebase/firestore.js +45 -28
- package/dist/providers/firebase/functions.d.ts +1 -1
- package/dist/providers/firebase/functions.js +75 -42
- package/dist/providers/firebase/hosting.d.ts +1 -1
- package/dist/providers/firebase/hosting.js +92 -52
- package/dist/providers/firebase/remoteconfig.d.ts +1 -1
- package/dist/providers/firebase/remoteconfig.js +42 -33
- package/dist/providers/firebase/storage.d.ts +1 -1
- package/dist/providers/firebase/storage.js +38 -24
- package/dist/providers/proxmox/vm.d.ts +1 -1
- package/dist/providers/proxmox/vm.js +43 -24
- package/dist/types/aws.js +1 -1
- package/package.json +3 -2
|
@@ -31,7 +31,7 @@ export class SecretsBuilder extends BaseBuilder {
|
|
|
31
31
|
throw e;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
// Awaits eager discovery
|
|
34
|
+
// Awaits eager discovery - used by resolveEnvVars so callers don't need discoveryPromise directly
|
|
35
35
|
async awaitValue() {
|
|
36
36
|
await this.discoveryPromise;
|
|
37
37
|
return this.resolvedValue;
|
|
@@ -70,11 +70,15 @@ export class SecretsBuilder extends BaseBuilder {
|
|
|
70
70
|
if (this._description)
|
|
71
71
|
console.log(` └─ Description: ${this._description}`);
|
|
72
72
|
}
|
|
73
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
name: this.name,
|
|
75
|
+
arn: this.resolvedArn,
|
|
76
|
+
value: this.resolvedValue,
|
|
77
|
+
};
|
|
74
78
|
}
|
|
75
79
|
if (!existing) {
|
|
76
80
|
if (!this._value) {
|
|
77
|
-
console.log(` ⚠️ Secret "${this.name}" does not exist
|
|
81
|
+
console.log(` ⚠️ Secret "${this.name}" does not exist - add .plainText() or .keyValue() to create it`);
|
|
78
82
|
return { name: this.name, arn: null, value: null };
|
|
79
83
|
}
|
|
80
84
|
const result = await client.send(new CreateSecretCommand({
|
|
@@ -100,7 +104,11 @@ export class SecretsBuilder extends BaseBuilder {
|
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
await this.deploySidecars();
|
|
103
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
name: this.name,
|
|
109
|
+
arn: this.resolvedArn,
|
|
110
|
+
value: this.resolvedValue,
|
|
111
|
+
};
|
|
104
112
|
}
|
|
105
113
|
async destroy() {
|
|
106
114
|
const dryRun = this.isDryRunActive();
|
|
@@ -110,14 +118,14 @@ export class SecretsBuilder extends BaseBuilder {
|
|
|
110
118
|
if (this._pendingDeletion)
|
|
111
119
|
console.log(` ⏳ Secret "${this.name}" is already scheduled for deletion`);
|
|
112
120
|
else
|
|
113
|
-
console.log(` ✅ Secret "${this.name}" does not exist
|
|
121
|
+
console.log(` ✅ Secret "${this.name}" does not exist - nothing to do`);
|
|
114
122
|
return { destroyed: this.name };
|
|
115
123
|
}
|
|
116
124
|
if (dryRun) {
|
|
117
125
|
const mode = this._forceDelete
|
|
118
126
|
? "immediate"
|
|
119
127
|
: "scheduled (30-day recovery window)";
|
|
120
|
-
console.log(` 📝 [PLAN] Delete secret "${this.name}"
|
|
128
|
+
console.log(` 📝 [PLAN] Delete secret "${this.name}" - ${mode}`);
|
|
121
129
|
return { destroyed: this.name };
|
|
122
130
|
}
|
|
123
131
|
await getSecretsClient().send(new DeleteSecretCommand({
|
|
@@ -140,7 +148,7 @@ export async function resolveEnvVars(env) {
|
|
|
140
148
|
if (v instanceof SecretsBuilder) {
|
|
141
149
|
await v.awaitValue();
|
|
142
150
|
if (v.resolvedValue === null)
|
|
143
|
-
throw new Error(`Secret "${v.name}" has no value
|
|
151
|
+
throw new Error(`Secret "${v.name}" has no value - create it first or call .plainText()/.keyValue() in the stack`);
|
|
144
152
|
resolved[k] = v.resolvedValue;
|
|
145
153
|
}
|
|
146
154
|
else {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { GetQueueUrlCommand, GetQueueAttributesCommand, CreateQueueCommand, SetQueueAttributesCommand, DeleteQueueCommand, QueueAttributeName, } from
|
|
2
|
-
import { BaseBuilder } from
|
|
3
|
-
import { getSQSClient } from
|
|
1
|
+
import { GetQueueUrlCommand, GetQueueAttributesCommand, CreateQueueCommand, SetQueueAttributesCommand, DeleteQueueCommand, QueueAttributeName, } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
3
|
+
import { getSQSClient } from "./api.js";
|
|
4
4
|
export class SQSBuilder extends BaseBuilder {
|
|
5
5
|
_fifo = false;
|
|
6
6
|
_deduplication = false;
|
|
@@ -17,11 +17,26 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
17
17
|
super(name);
|
|
18
18
|
this.discoveryPromise = this.discoverQueue(name);
|
|
19
19
|
}
|
|
20
|
-
fifo(enabled = true) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
fifo(enabled = true) {
|
|
21
|
+
this._fifo = enabled;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
deduplication(enabled = true) {
|
|
25
|
+
this._deduplication = enabled;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
timeout(seconds) {
|
|
29
|
+
this._visibilityTimeout = seconds;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
retention(days) {
|
|
33
|
+
this._retentionDays = days;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
delay(seconds) {
|
|
37
|
+
this._delaySeconds = seconds;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
25
40
|
dlq(name, maxReceives = 3) {
|
|
26
41
|
this._dlqName = name;
|
|
27
42
|
this._dlqMaxReceives = maxReceives;
|
|
@@ -29,13 +44,13 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
29
44
|
}
|
|
30
45
|
queueName() {
|
|
31
46
|
const base = this.name;
|
|
32
|
-
if (this._fifo && !base.endsWith(
|
|
47
|
+
if (this._fifo && !base.endsWith(".fifo"))
|
|
33
48
|
return `${base}.fifo`;
|
|
34
49
|
return base;
|
|
35
50
|
}
|
|
36
51
|
async discoverQueue(name) {
|
|
37
52
|
const sqs = getSQSClient();
|
|
38
|
-
const qName = this._fifo && !name.endsWith(
|
|
53
|
+
const qName = this._fifo && !name.endsWith(".fifo") ? `${name}.fifo` : name;
|
|
39
54
|
try {
|
|
40
55
|
const urlResult = await sqs.send(new GetQueueUrlCommand({ QueueName: qName }));
|
|
41
56
|
this.resolvedUrl = urlResult.QueueUrl;
|
|
@@ -47,9 +62,10 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
47
62
|
return { url: this.resolvedUrl, arn: this.resolvedArn };
|
|
48
63
|
}
|
|
49
64
|
catch (e) {
|
|
50
|
-
if (e.name ===
|
|
65
|
+
if (e.name === "QueueDoesNotExist" ||
|
|
66
|
+
e.name === "AWS.SimpleQueueService.NonExistentQueue")
|
|
51
67
|
return null;
|
|
52
|
-
if (e.name ===
|
|
68
|
+
if (e.name === "CredentialsProviderError")
|
|
53
69
|
return null;
|
|
54
70
|
throw e;
|
|
55
71
|
}
|
|
@@ -66,7 +82,8 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
66
82
|
return { url, arn: attrResult.Attributes?.QueueArn };
|
|
67
83
|
}
|
|
68
84
|
catch (e) {
|
|
69
|
-
if (e.name !==
|
|
85
|
+
if (e.name !== "QueueDoesNotExist" &&
|
|
86
|
+
e.name !== "AWS.SimpleQueueService.NonExistentQueue")
|
|
70
87
|
throw e;
|
|
71
88
|
}
|
|
72
89
|
const created = await sqs.send(new CreateQueueCommand({ QueueName: queueName, Attributes: attrs }));
|
|
@@ -84,9 +101,9 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
84
101
|
DelaySeconds: String(this._delaySeconds),
|
|
85
102
|
};
|
|
86
103
|
if (this._fifo) {
|
|
87
|
-
attrs.FifoQueue =
|
|
104
|
+
attrs.FifoQueue = "true";
|
|
88
105
|
if (this._deduplication)
|
|
89
|
-
attrs.ContentBasedDeduplication =
|
|
106
|
+
attrs.ContentBasedDeduplication = "true";
|
|
90
107
|
}
|
|
91
108
|
if (redrivePolicy)
|
|
92
109
|
attrs.RedrivePolicy = redrivePolicy;
|
|
@@ -98,9 +115,13 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
98
115
|
const queueName = this.queueName();
|
|
99
116
|
console.log(`\n⚡ Finalizing SQS Queue "${queueName}"...`);
|
|
100
117
|
if (dryRun) {
|
|
101
|
-
const dlqName = this._dlqName
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
const dlqName = this._dlqName
|
|
119
|
+
? this._fifo && !this._dlqName.endsWith(".fifo")
|
|
120
|
+
? `${this._dlqName}.fifo`
|
|
121
|
+
: this._dlqName
|
|
122
|
+
: undefined;
|
|
123
|
+
console.log(` 📝 [PLAN] ${existing ? "Update" : "Create"} queue "${queueName}"`);
|
|
124
|
+
console.log(` └─ Type: ${this._fifo ? "FIFO" : "Standard"}`);
|
|
104
125
|
console.log(` └─ Visibility timeout: ${this._visibilityTimeout}s | Retention: ${this._retentionDays}d`);
|
|
105
126
|
if (this._delaySeconds)
|
|
106
127
|
console.log(` └─ Delivery delay: ${this._delaySeconds}s`);
|
|
@@ -114,7 +135,7 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
114
135
|
// Create DLQ first so we have its ARN for the redrive policy
|
|
115
136
|
let redrivePolicy;
|
|
116
137
|
if (this._dlqName) {
|
|
117
|
-
const dlqQueueName = this._fifo && !this._dlqName.endsWith(
|
|
138
|
+
const dlqQueueName = this._fifo && !this._dlqName.endsWith(".fifo")
|
|
118
139
|
? `${this._dlqName}.fifo`
|
|
119
140
|
: this._dlqName;
|
|
120
141
|
const dlqAttrs = {
|
|
@@ -122,16 +143,19 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
122
143
|
MessageRetentionPeriod: String(this._retentionDays * 86400),
|
|
123
144
|
};
|
|
124
145
|
if (this._fifo)
|
|
125
|
-
dlqAttrs.FifoQueue =
|
|
146
|
+
dlqAttrs.FifoQueue = "true";
|
|
126
147
|
const dlq = await this.ensureQueue(dlqQueueName, dlqAttrs);
|
|
127
148
|
this.resolvedDlqUrl = dlq.url;
|
|
128
149
|
this.resolvedDlqArn = dlq.arn;
|
|
129
|
-
redrivePolicy = JSON.stringify({
|
|
150
|
+
redrivePolicy = JSON.stringify({
|
|
151
|
+
deadLetterTargetArn: dlq.arn,
|
|
152
|
+
maxReceiveCount: this._dlqMaxReceives,
|
|
153
|
+
});
|
|
130
154
|
console.log(` ✅ DLQ ready: ${dlqQueueName}`);
|
|
131
155
|
}
|
|
132
156
|
const attrs = this.buildAttributes(redrivePolicy);
|
|
133
157
|
if (existing) {
|
|
134
|
-
// FIFO queues reject attribute updates that change queue type
|
|
158
|
+
// FIFO queues reject attribute updates that change queue type - only update mutable attrs
|
|
135
159
|
const mutableAttrs = {
|
|
136
160
|
VisibilityTimeout: attrs.VisibilityTimeout,
|
|
137
161
|
MessageRetentionPeriod: attrs.MessageRetentionPeriod,
|
|
@@ -164,7 +188,7 @@ export class SQSBuilder extends BaseBuilder {
|
|
|
164
188
|
const existing = await this.discoveryPromise;
|
|
165
189
|
console.log(`\n🗑️ Destroying SQS Queue "${this.queueName()}"...`);
|
|
166
190
|
if (!existing) {
|
|
167
|
-
console.log(` ✅ Queue "${this.queueName()}" does not exist
|
|
191
|
+
console.log(` ✅ Queue "${this.queueName()}" does not exist - nothing to do`);
|
|
168
192
|
return { destroyed: this.name };
|
|
169
193
|
}
|
|
170
194
|
if (dryRun) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { BaseBuilder } from
|
|
2
|
-
import { Output } from
|
|
3
|
-
import { DropletBuilder } from
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
import { DropletBuilder } from "./droplet.js";
|
|
4
4
|
export interface DNSRecord {
|
|
5
|
-
type:
|
|
5
|
+
type: "A" | "CNAME" | "TXT" | "MX";
|
|
6
6
|
name: string;
|
|
7
7
|
value: string | DropletBuilder | Output<string>;
|
|
8
8
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { BaseBuilder } from
|
|
2
|
-
import { Output } from
|
|
3
|
-
import { DropletBuilder } from
|
|
4
|
-
import { CertificateBuilder } from
|
|
5
|
-
import { getDoApi } from
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
import { Output } from "../../core/output.js";
|
|
3
|
+
import { DropletBuilder } from "./droplet.js";
|
|
4
|
+
import { CertificateBuilder } from "./certificate.js";
|
|
5
|
+
import { getDoApi } from "./api.js";
|
|
6
6
|
export class DomainBuilder extends BaseBuilder {
|
|
7
7
|
domainName;
|
|
8
8
|
records = [];
|
|
@@ -14,7 +14,9 @@ export class DomainBuilder extends BaseBuilder {
|
|
|
14
14
|
async discoverDomain(name) {
|
|
15
15
|
const api = getDoApi();
|
|
16
16
|
try {
|
|
17
|
-
return await api
|
|
17
|
+
return await api
|
|
18
|
+
.get(`/domains/${name}`)
|
|
19
|
+
.then((d) => d.domain);
|
|
18
20
|
}
|
|
19
21
|
catch {
|
|
20
22
|
return null;
|
|
@@ -26,11 +28,11 @@ export class DomainBuilder extends BaseBuilder {
|
|
|
26
28
|
return this;
|
|
27
29
|
}
|
|
28
30
|
pointer(name, target) {
|
|
29
|
-
this.records.push({ type:
|
|
31
|
+
this.records.push({ type: "A", name, value: target });
|
|
30
32
|
return this;
|
|
31
33
|
}
|
|
32
34
|
cname(name, target) {
|
|
33
|
-
this.records.push({ type:
|
|
35
|
+
this.records.push({ type: "CNAME", name, value: target });
|
|
34
36
|
return this;
|
|
35
37
|
}
|
|
36
38
|
async deploy() {
|
|
@@ -43,7 +45,7 @@ export class DomainBuilder extends BaseBuilder {
|
|
|
43
45
|
console.log(` 📝 [PLAN] Create domain ${this.domainName}`);
|
|
44
46
|
}
|
|
45
47
|
else {
|
|
46
|
-
await api.post(
|
|
48
|
+
await api.post("/domains", { name: this.domainName });
|
|
47
49
|
console.log(`🚀 Created domain ${this.domainName}`);
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -53,7 +55,9 @@ export class DomainBuilder extends BaseBuilder {
|
|
|
53
55
|
data = await record.value.get();
|
|
54
56
|
}
|
|
55
57
|
else if (record.value instanceof DropletBuilder) {
|
|
56
|
-
data =
|
|
58
|
+
data =
|
|
59
|
+
(await record.value.getPublicIp()) ??
|
|
60
|
+
`[IP of ${record.value.name} - not found]`;
|
|
57
61
|
}
|
|
58
62
|
else {
|
|
59
63
|
data = record.value;
|
|
@@ -64,7 +68,7 @@ export class DomainBuilder extends BaseBuilder {
|
|
|
64
68
|
}
|
|
65
69
|
// Delete existing record with same type+name before creating
|
|
66
70
|
const existing_records = await api.get(`/domains/${this.domainName}/records?per_page=200`);
|
|
67
|
-
const dupe = existing_records.domain_records.find(r => r.type === record.type && r.name === record.name);
|
|
71
|
+
const dupe = existing_records.domain_records.find((r) => r.type === record.type && r.name === record.name);
|
|
68
72
|
if (dupe)
|
|
69
73
|
await api.delete(`/domains/${this.domainName}/records/${dupe.id}`);
|
|
70
74
|
await api.post(`/domains/${this.domainName}/records`, {
|
|
@@ -1,38 +1,67 @@
|
|
|
1
|
-
import { BaseBuilder } from
|
|
2
|
-
import { cloudFetch, getProjectId } from
|
|
3
|
-
const AUTH_BASE =
|
|
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
4
|
const IDP_ID = {
|
|
5
|
-
google:
|
|
6
|
-
github:
|
|
7
|
-
facebook:
|
|
8
|
-
twitter:
|
|
9
|
-
apple:
|
|
10
|
-
microsoft:
|
|
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
11
|
};
|
|
12
12
|
export class FirebaseAuthBuilder extends BaseBuilder {
|
|
13
13
|
_providers = {};
|
|
14
14
|
_allowedDomains;
|
|
15
15
|
_authorizedDomains;
|
|
16
16
|
constructor() {
|
|
17
|
-
super(
|
|
17
|
+
super("auth");
|
|
18
18
|
this.discoveryPromise = Promise.resolve(null);
|
|
19
19
|
}
|
|
20
20
|
emailPassword(opts = {}) {
|
|
21
21
|
this._providers.emailPassword = { enabled: true, ...opts };
|
|
22
22
|
return this;
|
|
23
23
|
}
|
|
24
|
-
anonymous() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
anonymous() {
|
|
25
|
+
this._providers.anonymous = { enabled: true };
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
phone() {
|
|
29
|
+
this._providers.phone = { enabled: true };
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
google(creds) {
|
|
33
|
+
this._providers.google = { enabled: true, ...creds };
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
github(creds) {
|
|
37
|
+
this._providers.github = { enabled: true, ...creds };
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
facebook(creds) {
|
|
41
|
+
this._providers.facebook = { enabled: true, ...creds };
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
twitter(creds) {
|
|
45
|
+
this._providers.twitter = { enabled: true, ...creds };
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
apple(creds) {
|
|
49
|
+
this._providers.apple = { enabled: true, ...creds };
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
microsoft(creds) {
|
|
53
|
+
this._providers.microsoft = { enabled: true, ...creds };
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
32
56
|
// Domains allowed to use Firebase Auth (e.g. your app domain + localhost)
|
|
33
|
-
authorizedDomains(domains) {
|
|
57
|
+
authorizedDomains(domains) {
|
|
58
|
+
this._authorizedDomains = domains;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
34
61
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
35
|
-
projectPath() {
|
|
62
|
+
projectPath() {
|
|
63
|
+
return `/projects/${getProjectId()}`;
|
|
64
|
+
}
|
|
36
65
|
async getConfig() {
|
|
37
66
|
try {
|
|
38
67
|
return await cloudFetch(AUTH_BASE, `${this.projectPath()}/config`);
|
|
@@ -42,7 +71,7 @@ export class FirebaseAuthBuilder extends BaseBuilder {
|
|
|
42
71
|
}
|
|
43
72
|
}
|
|
44
73
|
async patchConfig(body, updateMask) {
|
|
45
|
-
await cloudFetch(AUTH_BASE, `${this.projectPath()}/config?updateMask=${updateMask}`, { method:
|
|
74
|
+
await cloudFetch(AUTH_BASE, `${this.projectPath()}/config?updateMask=${updateMask}`, { method: "PATCH", body: JSON.stringify(body) });
|
|
46
75
|
}
|
|
47
76
|
async getIdp(provider) {
|
|
48
77
|
try {
|
|
@@ -62,10 +91,10 @@ export class FirebaseAuthBuilder extends BaseBuilder {
|
|
|
62
91
|
enabled: creds.enabled ?? true,
|
|
63
92
|
};
|
|
64
93
|
if (existing) {
|
|
65
|
-
await cloudFetch(AUTH_BASE, `${this.projectPath()}/defaultSupportedIdpConfigs/${idpId}?updateMask=clientId,clientSecret,enabled`, { method:
|
|
94
|
+
await cloudFetch(AUTH_BASE, `${this.projectPath()}/defaultSupportedIdpConfigs/${idpId}?updateMask=clientId,clientSecret,enabled`, { method: "PATCH", body: JSON.stringify(body) });
|
|
66
95
|
}
|
|
67
96
|
else {
|
|
68
|
-
await cloudFetch(AUTH_BASE, `${this.projectPath()}/defaultSupportedIdpConfigs?idpId=${idpId}`, { method:
|
|
97
|
+
await cloudFetch(AUTH_BASE, `${this.projectPath()}/defaultSupportedIdpConfigs?idpId=${idpId}`, { method: "POST", body: JSON.stringify(body) });
|
|
69
98
|
}
|
|
70
99
|
}
|
|
71
100
|
// ── Deploy ────────────────────────────────────────────────────────────────
|
|
@@ -89,7 +118,7 @@ export class FirebaseAuthBuilder extends BaseBuilder {
|
|
|
89
118
|
console.log(` 📝 [PLAN] Enable OAuth provider: ${IDP_ID[name]}`);
|
|
90
119
|
}
|
|
91
120
|
if (this._authorizedDomains) {
|
|
92
|
-
console.log(` 📝 [PLAN] Set authorized domains: [${this._authorizedDomains.join(
|
|
121
|
+
console.log(` 📝 [PLAN] Set authorized domains: [${this._authorizedDomains.join(", ")}]`);
|
|
93
122
|
}
|
|
94
123
|
return { project: getProjectId() };
|
|
95
124
|
}
|
|
@@ -98,25 +127,28 @@ export class FirebaseAuthBuilder extends BaseBuilder {
|
|
|
98
127
|
const signIn = {};
|
|
99
128
|
const masks = [];
|
|
100
129
|
if (emailPassword) {
|
|
101
|
-
signIn.email = {
|
|
102
|
-
|
|
130
|
+
signIn.email = {
|
|
131
|
+
enabled: true,
|
|
132
|
+
passwordRequired: emailPassword.passwordRequired ?? true,
|
|
133
|
+
};
|
|
134
|
+
masks.push("signIn.email");
|
|
103
135
|
}
|
|
104
136
|
if (anonymous) {
|
|
105
137
|
signIn.anonymous = { enabled: true };
|
|
106
|
-
masks.push(
|
|
138
|
+
masks.push("signIn.anonymous");
|
|
107
139
|
}
|
|
108
140
|
if (phone) {
|
|
109
141
|
signIn.phoneNumber = { enabled: true };
|
|
110
|
-
masks.push(
|
|
142
|
+
masks.push("signIn.phoneNumber");
|
|
111
143
|
}
|
|
112
144
|
const body = {};
|
|
113
145
|
if (masks.length)
|
|
114
146
|
body.signIn = signIn;
|
|
115
147
|
if (this._authorizedDomains) {
|
|
116
148
|
body.authorizedDomains = this._authorizedDomains;
|
|
117
|
-
masks.push(
|
|
149
|
+
masks.push("authorizedDomains");
|
|
118
150
|
}
|
|
119
|
-
await this.patchConfig(body, masks.join(
|
|
151
|
+
await this.patchConfig(body, masks.join(","));
|
|
120
152
|
if (emailPassword)
|
|
121
153
|
console.log(` ✅ Email/Password enabled`);
|
|
122
154
|
if (anonymous)
|
|
@@ -124,7 +156,7 @@ export class FirebaseAuthBuilder extends BaseBuilder {
|
|
|
124
156
|
if (phone)
|
|
125
157
|
console.log(` ✅ Phone sign-in enabled`);
|
|
126
158
|
if (this._authorizedDomains)
|
|
127
|
-
console.log(` ✅ Authorized domains set: [${this._authorizedDomains.join(
|
|
159
|
+
console.log(` ✅ Authorized domains set: [${this._authorizedDomains.join(", ")}]`);
|
|
128
160
|
}
|
|
129
161
|
// OAuth providers
|
|
130
162
|
for (const [name, creds] of oauthEntries) {
|
|
@@ -137,11 +169,11 @@ export class FirebaseAuthBuilder extends BaseBuilder {
|
|
|
137
169
|
const dryRun = this.isDryRunActive();
|
|
138
170
|
console.log(`\n🗑️ Destroying Firebase Auth config...`);
|
|
139
171
|
if (dryRun) {
|
|
140
|
-
console.log(` ℹ️ Auth providers can be disabled individually
|
|
172
|
+
console.log(` ℹ️ Auth providers can be disabled individually - destroying the Auth config is not supported via API`);
|
|
141
173
|
}
|
|
142
174
|
else {
|
|
143
175
|
console.log(` ℹ️ Disable providers individually in the Firebase console`);
|
|
144
176
|
}
|
|
145
|
-
return { destroyed:
|
|
177
|
+
return { destroyed: "auth" };
|
|
146
178
|
}
|
|
147
179
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BaseBuilder } from
|
|
2
|
-
type FieldOrder =
|
|
1
|
+
import { BaseBuilder } from "../../core/resource.js";
|
|
2
|
+
type FieldOrder = "ASCENDING" | "DESCENDING";
|
|
3
3
|
interface IndexField {
|
|
4
4
|
field: string;
|
|
5
5
|
order: FieldOrder;
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { BaseBuilder } from
|
|
3
|
-
import { cloudFetch, getProjectId } from
|
|
4
|
-
const RULES_BASE =
|
|
5
|
-
const FS_BASE =
|
|
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
6
|
export class FirebaseFirestoreBuilder extends BaseBuilder {
|
|
7
7
|
_rulesPath;
|
|
8
8
|
_indexes = [];
|
|
9
|
-
constructor(database =
|
|
9
|
+
constructor(database = "(default)") {
|
|
10
10
|
super(database);
|
|
11
11
|
this.discoveryPromise = Promise.resolve(null);
|
|
12
12
|
}
|
|
13
|
-
rules(filePath) {
|
|
14
|
-
|
|
13
|
+
rules(filePath) {
|
|
14
|
+
this._rulesPath = filePath;
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
index(collection, fields) {
|
|
18
|
+
this._indexes.push({ collection, fields });
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
15
21
|
// ── Rules ────────────────────────────────────────────────────────────────
|
|
16
22
|
rulesRelease() {
|
|
17
23
|
return `projects/${getProjectId()}/releases/cloud.firestore`;
|
|
@@ -28,23 +34,28 @@ export class FirebaseFirestoreBuilder extends BaseBuilder {
|
|
|
28
34
|
async deployRules(dryRun) {
|
|
29
35
|
if (!this._rulesPath)
|
|
30
36
|
return;
|
|
31
|
-
const source = readFileSync(this._rulesPath,
|
|
37
|
+
const source = readFileSync(this._rulesPath, "utf8");
|
|
32
38
|
const current = await this.currentRulesReleaseRuleset();
|
|
33
39
|
if (dryRun) {
|
|
34
40
|
console.log(` 📝 [PLAN] Deploy Firestore rules from "${this._rulesPath}"`);
|
|
35
41
|
if (current)
|
|
36
|
-
console.log(` └─ replaces ruleset: ${current.split(
|
|
42
|
+
console.log(` └─ replaces ruleset: ${current.split("/").pop()}`);
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
39
45
|
const ruleset = await cloudFetch(RULES_BASE, `/projects/${getProjectId()}/rulesets`, {
|
|
40
|
-
method:
|
|
41
|
-
body: JSON.stringify({
|
|
46
|
+
method: "POST",
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
source: { files: [{ name: "firestore.rules", content: source }] },
|
|
49
|
+
}),
|
|
42
50
|
});
|
|
43
51
|
await cloudFetch(RULES_BASE, `/${this.rulesRelease()}`, {
|
|
44
|
-
method:
|
|
45
|
-
body: JSON.stringify({
|
|
52
|
+
method: "PUT",
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
name: this.rulesRelease(),
|
|
55
|
+
rulesetName: ruleset.name,
|
|
56
|
+
}),
|
|
46
57
|
});
|
|
47
|
-
console.log(` ✅ Rules deployed (ruleset: ${ruleset.name.split(
|
|
58
|
+
console.log(` ✅ Rules deployed (ruleset: ${ruleset.name.split("/").pop()})`);
|
|
48
59
|
}
|
|
49
60
|
// ── Indexes ───────────────────────────────────────────────────────────────
|
|
50
61
|
dbPath() {
|
|
@@ -60,37 +71,43 @@ export class FirebaseFirestoreBuilder extends BaseBuilder {
|
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
indexKey(collection, fields) {
|
|
63
|
-
return `${collection}:${fields.map(f => `${f.field}:${f.order}`).join(
|
|
74
|
+
return `${collection}:${fields.map((f) => `${f.field}:${f.order}`).join(",")}`;
|
|
64
75
|
}
|
|
65
76
|
async deployIndexes(dryRun) {
|
|
66
77
|
if (this._indexes.length === 0)
|
|
67
78
|
return;
|
|
68
79
|
const existing = await this.listExistingIndexes();
|
|
69
80
|
const existingKeys = new Set(existing.map((idx) => {
|
|
70
|
-
const parts = idx.name.split(
|
|
71
|
-
const collection = parts[1]?.split(
|
|
81
|
+
const parts = idx.name.split("/collectionGroups/");
|
|
82
|
+
const collection = parts[1]?.split("/")[0] ?? "";
|
|
72
83
|
const fields = (idx.fields ?? [])
|
|
73
|
-
.filter((f) => f.fieldPath !==
|
|
74
|
-
.map((f) => ({
|
|
84
|
+
.filter((f) => f.fieldPath !== "__name__")
|
|
85
|
+
.map((f) => ({
|
|
86
|
+
field: f.fieldPath,
|
|
87
|
+
order: f.order,
|
|
88
|
+
}));
|
|
75
89
|
return this.indexKey(collection, fields);
|
|
76
90
|
}));
|
|
77
|
-
const toCreate = this._indexes.filter(i => !existingKeys.has(this.indexKey(i.collection, i.fields)));
|
|
91
|
+
const toCreate = this._indexes.filter((i) => !existingKeys.has(this.indexKey(i.collection, i.fields)));
|
|
78
92
|
if (dryRun) {
|
|
79
93
|
console.log(` 📝 [PLAN] ${toCreate.length} index(es) to create, ${this._indexes.length - toCreate.length} already exist`);
|
|
80
94
|
for (const idx of toCreate) {
|
|
81
|
-
console.log(` └─ ${idx.collection}: [${idx.fields.map(f => `${f.field} ${f.order}`).join(
|
|
95
|
+
console.log(` └─ ${idx.collection}: [${idx.fields.map((f) => `${f.field} ${f.order}`).join(", ")}]`);
|
|
82
96
|
}
|
|
83
97
|
return;
|
|
84
98
|
}
|
|
85
99
|
for (const idx of toCreate) {
|
|
86
100
|
await cloudFetch(FS_BASE, `/${this.dbPath()}/collectionGroups/${idx.collection}/indexes`, {
|
|
87
|
-
method:
|
|
101
|
+
method: "POST",
|
|
88
102
|
body: JSON.stringify({
|
|
89
|
-
queryScope:
|
|
90
|
-
fields: idx.fields.map(f => ({
|
|
103
|
+
queryScope: "COLLECTION",
|
|
104
|
+
fields: idx.fields.map((f) => ({
|
|
105
|
+
fieldPath: f.field,
|
|
106
|
+
order: f.order,
|
|
107
|
+
})),
|
|
91
108
|
}),
|
|
92
109
|
});
|
|
93
|
-
console.log(` ✅ Index created: ${idx.collection} [${idx.fields.map(f => `${f.field} ${f.order}`).join(
|
|
110
|
+
console.log(` ✅ Index created: ${idx.collection} [${idx.fields.map((f) => `${f.field} ${f.order}`).join(", ")}]`);
|
|
94
111
|
}
|
|
95
112
|
if (toCreate.length === 0)
|
|
96
113
|
console.log(` ✅ All indexes already exist`);
|
|
@@ -106,10 +123,10 @@ export class FirebaseFirestoreBuilder extends BaseBuilder {
|
|
|
106
123
|
async destroy() {
|
|
107
124
|
const dryRun = this.isDryRunActive();
|
|
108
125
|
console.log(`\n🗑️ Destroying Firestore config "${this.name}"...`);
|
|
109
|
-
// Firestore databases themselves cannot be deleted via API
|
|
126
|
+
// Firestore databases themselves cannot be deleted via API - only the config managed here
|
|
110
127
|
if (dryRun) {
|
|
111
128
|
if (this._rulesPath)
|
|
112
|
-
console.log(` 📝 [PLAN] Rules release cannot be rolled back via API
|
|
129
|
+
console.log(` 📝 [PLAN] Rules release cannot be rolled back via API - do this in the Firebase console`);
|
|
113
130
|
console.log(` ℹ️ Firestore databases cannot be deleted via API`);
|
|
114
131
|
}
|
|
115
132
|
else {
|