@geekmidas/cli 1.10.41 → 1.12.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/CHANGELOG.md +149 -0
- package/README.md +14 -0
- package/dist/{CachedStateProvider-37pG1HBx.cjs → CachedStateProvider-BdX7klRc.cjs} +1 -1
- package/dist/{CachedStateProvider-C8A-cCm5.mjs → CachedStateProvider-CzyFct42.mjs} +1 -1
- package/dist/{CachedStateProvider-C8A-cCm5.mjs.map → CachedStateProvider-CzyFct42.mjs.map} +1 -1
- package/dist/CachedStateProvider-GcGziXh1.mjs +3 -0
- package/dist/{CachedStateProvider-CxsXOKkg.cjs → CachedStateProvider-klLWZXWF.cjs} +1 -1
- package/dist/{CachedStateProvider-CxsXOKkg.cjs.map → CachedStateProvider-klLWZXWF.cjs.map} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs → HostingerProvider-B2ZYz7Yc.mjs} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs.map → HostingerProvider-B2ZYz7Yc.mjs.map} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs → HostingerProvider-CrHfsgz9.cjs} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs.map → HostingerProvider-CrHfsgz9.cjs.map} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs → LocalStateProvider-CP49rjzt.mjs} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs.map → LocalStateProvider-CP49rjzt.mjs.map} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs → LocalStateProvider-CalzNfNA.cjs} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs.map → LocalStateProvider-CalzNfNA.cjs.map} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs → Route53Provider-DwgXiLxt.cjs} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs.map → Route53Provider-DwgXiLxt.cjs.map} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs → Route53Provider-tlgBzrRQ.mjs} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs.map → Route53Provider-tlgBzrRQ.mjs.map} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs → SSMStateProvider-BlrTiV5m.mjs} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs.map → SSMStateProvider-BlrTiV5m.mjs.map} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs → SSMStateProvider-Bt6r8CTL.cjs} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs.map → SSMStateProvider-Bt6r8CTL.cjs.map} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs → backup-provisioner-B3HBN-2k.cjs} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs.map → backup-provisioner-B3HBN-2k.cjs.map} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs → backup-provisioner-Xunq-p_8.mjs} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs.map → backup-provisioner-Xunq-p_8.mjs.map} +1 -1
- package/dist/{bundler-c6QvGUBM.mjs → bundler-DkA4qzUD.mjs} +2 -2
- package/dist/{bundler-c6QvGUBM.mjs.map → bundler-DkA4qzUD.mjs.map} +1 -1
- package/dist/{bundler-BG6P249p.cjs → bundler-Dufq3uvI.cjs} +2 -2
- package/dist/{bundler-BG6P249p.cjs.map → bundler-Dufq3uvI.cjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/dokploy-api-BPYkG-ZZ.cjs +3 -0
- package/dist/dokploy-api-BiSzWM4v.mjs +3 -0
- package/dist/{dokploy-api-Dvyq-LsM.cjs → dokploy-api-CqOIKHme.cjs} +1 -1
- package/dist/{dokploy-api-Dvyq-LsM.cjs.map → dokploy-api-CqOIKHme.cjs.map} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs → dokploy-api-sfZFlWuE.mjs} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs.map → dokploy-api-sfZFlWuE.mjs.map} +1 -1
- package/dist/{encryption-CipsS0KW.cjs → encryption-BVS7Vmzs.cjs} +1 -1
- package/dist/encryption-Bd_m4q3g.mjs +3 -0
- package/dist/{encryption-DSkIlEZ6.mjs → encryption-CO5Q4Bhf.mjs} +1 -1
- package/dist/{encryption-DSkIlEZ6.mjs.map → encryption-CO5Q4Bhf.mjs.map} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs → encryption-MfXAiFTm.cjs} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs.map → encryption-MfXAiFTm.cjs.map} +1 -1
- package/dist/{index-BBvGMG_A.d.mts → index-Bw548Bta.d.mts} +2 -2
- package/dist/{index-Dz2a7xQU.d.cts.map → index-Bw548Bta.d.mts.map} +1 -1
- package/dist/{index-Dz2a7xQU.d.cts → index-DTpkeFb8.d.cts} +2 -2
- package/dist/{index-BBvGMG_A.d.mts.map → index-DTpkeFb8.d.cts.map} +1 -1
- package/dist/index.cjs +863 -1330
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +990 -1457
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-DHp49UJV.cjs → openapi-BzNOC7_i.cjs} +14 -2
- package/dist/openapi-BzNOC7_i.cjs.map +1 -0
- package/dist/{openapi-DF9GyhyZ.mjs → openapi-Cfwi0fqF.mjs} +14 -2
- package/dist/openapi-Cfwi0fqF.mjs.map +1 -0
- package/dist/{openapi-react-query-C4UdILaI.mjs → openapi-react-query-C4iIvpPf.mjs} +1 -1
- package/dist/{openapi-react-query-C4UdILaI.mjs.map → openapi-react-query-C4iIvpPf.mjs.map} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs → openapi-react-query-CYWik72n.cjs} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs.map → openapi-react-query-CYWik72n.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{reconcile-CYGmqXvq.mjs → reconcile-B2FoJE0a.mjs} +1 -1
- package/dist/{reconcile-CYGmqXvq.mjs.map → reconcile-B2FoJE0a.mjs.map} +1 -1
- package/dist/{reconcile-D93YG7G5.cjs → reconcile-BzqKJ2Rt.cjs} +1 -1
- package/dist/{reconcile-D93YG7G5.cjs.map → reconcile-BzqKJ2Rt.cjs.map} +1 -1
- package/dist/setup-DDXUXzzl.mjs +765 -0
- package/dist/setup-DDXUXzzl.mjs.map +1 -0
- package/dist/setup-DLwhfyt-.cjs +8 -0
- package/dist/setup-Dh-B7YTX.mjs +8 -0
- package/dist/setup-WZ-xqAQO.cjs +849 -0
- package/dist/setup-WZ-xqAQO.cjs.map +1 -0
- package/dist/{sync-COnAugP-.mjs → sync-B5UHXwRM.mjs} +1 -1
- package/dist/sync-BLA94rgC.cjs +4 -0
- package/dist/{sync-D_NowTkZ.mjs → sync-CSooqRQ8.mjs} +1 -1
- package/dist/{sync-D_NowTkZ.mjs.map → sync-CSooqRQ8.mjs.map} +1 -1
- package/dist/{sync-DGXXSk2v.cjs → sync-o_CtEbvR.cjs} +1 -1
- package/dist/{sync-DGXXSk2v.cjs.map → sync-o_CtEbvR.cjs.map} +1 -1
- package/dist/{types-DdHfUbxk.d.cts → types-B3GQUg3k.d.mts} +3 -1
- package/dist/{types-DdHfUbxk.d.cts.map → types-B3GQUg3k.d.mts.map} +1 -1
- package/dist/{types-OszPdw9m.d.mts → types-BWQDiooe.d.cts} +3 -1
- package/dist/{types-OszPdw9m.d.mts.map → types-BWQDiooe.d.cts.map} +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/package.json +6 -5
- package/src/build/__tests__/index-new.spec.ts +3 -1
- package/src/build/__tests__/manifests.spec.ts +2 -2
- package/src/build/index.ts +61 -21
- package/src/build/manifests.ts +62 -5
- package/src/dev/index.ts +25 -11
- package/src/generators/EndpointGenerator.ts +14 -2
- package/src/generators/QueueGenerator.ts +245 -0
- package/src/generators/SubscriberGenerator.ts +5 -0
- package/src/generators/TopicGenerator.ts +45 -0
- package/src/generators/__tests__/QueueGenerator.spec.ts +141 -0
- package/src/generators/index.ts +2 -0
- package/src/index.ts +4 -0
- package/src/init/versions.ts +6 -6
- package/src/setup/__tests__/auto-setup.spec.ts +147 -0
- package/src/setup/index.ts +46 -6
- package/src/test/index.ts +17 -0
- package/src/types.ts +14 -51
- package/dist/CachedStateProvider-CC0DyO09.mjs +0 -3
- package/dist/dokploy-api-BOx71j6F.cjs +0 -3
- package/dist/dokploy-api-BZGk-lkD.mjs +0 -3
- package/dist/encryption-Db2zvGqM.mjs +0 -3
- package/dist/openapi-DF9GyhyZ.mjs.map +0 -1
- package/dist/openapi-DHp49UJV.cjs.map +0 -1
- package/dist/sync-D1Pa30oV.cjs +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,154 @@
|
|
|
1
1
|
# @geekmidas/cli
|
|
2
2
|
|
|
3
|
+
## 1.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#8](https://github.com/geekmidas/toolbox/pull/8) [`b004fd8`](https://github.com/geekmidas/toolbox/commit/b004fd8ee74b5f20a047260b16669d16d8fc03b4) Thanks [@geekmidas](https://github.com/geekmidas)! - feat: queue workers (`q`) — producer, runtime adaptors, and `gkm` discovery
|
|
8
|
+
|
|
9
|
+
Adds end-to-end support for point-to-point queues, alongside subscribers (`s`):
|
|
10
|
+
|
|
11
|
+
**`@geekmidas/constructs/queue`** — the `q` builder:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { q } from '@geekmidas/constructs/queue';
|
|
15
|
+
|
|
16
|
+
export const orders = q
|
|
17
|
+
.queue('orders')
|
|
18
|
+
.services([db]) // array; sniffed for required env vars
|
|
19
|
+
.message(z.object({ orderId: z.string() }))
|
|
20
|
+
.handle(async ({ messages, services }) => { … }); // the single consumer
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Unlike `s` (topic fan-out, filtered by `subscribedEvents`), a queue drains
|
|
24
|
+
_every_ message of its one typed `message`.
|
|
25
|
+
- **Producer side** — `orders.publisher`, a ready-to-inject `Service` typed to
|
|
26
|
+
the queue's message. Drop it into any `.services([...])` and call
|
|
27
|
+
`services.ordersPublisher.publish([{ type: 'orders', payload }])`. It reads
|
|
28
|
+
`<NAME>_PUBLISHER_CONNECTION_STRING` and picks its transport from the URL
|
|
29
|
+
protocol — `pgboss://` locally, `sqs://` deployed — so the same code targets
|
|
30
|
+
Postgres in dev and SQS in prod. The env requirement is sniffed into the
|
|
31
|
+
manifest, so infra links exactly that queue with least privilege.
|
|
32
|
+
- **Runtime adaptors** — `AWSLambdaQueue` (`@geekmidas/constructs/aws`, SQS
|
|
33
|
+
event-source with partial-batch failures) and `TestQueueAdaptor`
|
|
34
|
+
(`@geekmidas/constructs/testing`).
|
|
35
|
+
|
|
36
|
+
**`@geekmidas/cli`** — `gkm build`/`gkm dev` discover `q` definitions:
|
|
37
|
+
- ✨ New `queues: './src/queues/**/*.ts'` config glob.
|
|
38
|
+
- Server / `gkm dev`: an in-process pg-boss poller (`setupQueues()`) runs
|
|
39
|
+
alongside the Hono server — each queue subscribes by its name on the shared
|
|
40
|
+
`EVENT_SUBSCRIBER_CONNECTION_STRING`. Queues are background workers, not HTTP
|
|
41
|
+
routes.
|
|
42
|
+
- AWS: one `AWSLambdaQueue` handler per queue.
|
|
43
|
+
- Queues are recorded in the manifest's `queues` field (`QueueInfo`).
|
|
44
|
+
|
|
45
|
+
- ✨ [#8](https://github.com/geekmidas/toolbox/pull/8) [`0dad77e`](https://github.com/geekmidas/toolbox/commit/0dad77e574000e4018033b956ed4bb95935911a5) Thanks [@geekmidas](https://github.com/geekmidas)! - feat(topic): add the `t` topic construct + derived publisher (closes the topic/queue asymmetry)
|
|
46
|
+
|
|
47
|
+
Topics now have the same app-driven story queues already had — declare the topic
|
|
48
|
+
in the app, get a typed publisher for free, and let `gkm build` capture it. This
|
|
49
|
+
removes the need to hand-write a publisher `Service` (e.g. `EventsService`) to
|
|
50
|
+
fan events out.
|
|
51
|
+
|
|
52
|
+
**`@geekmidas/constructs/topic`** — the `t` builder:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { t } from "@geekmidas/constructs/topic";
|
|
56
|
+
|
|
57
|
+
export const userTopic = t.topic("users").events({
|
|
58
|
+
"user.created": z.object({ userId: z.string(), email: z.string() }),
|
|
59
|
+
"user.updated": z.object({
|
|
60
|
+
userId: z.string(),
|
|
61
|
+
changes: z.array(z.string()),
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- A `Topic` is a _resource_ construct (`ConstructType.Topic`) — fan-out, owned by
|
|
67
|
+
no single handler. It declares the event contract and derives a publisher.
|
|
68
|
+
- **`userTopic.publisher`** — a derived `Service` typed to the union of the topic's
|
|
69
|
+
events, reading `<NAME>_PUBLISHER_CONNECTION_STRING` (transport by protocol:
|
|
70
|
+
`sns://` deployed, `pgboss://` local). Replaces hand-written publisher services.
|
|
71
|
+
Inject via `.publisher(userTopic.publisher)` (declarative `.event(...)`) or
|
|
72
|
+
`.services([userTopic.publisher])`.
|
|
73
|
+
- **`s.topic(userTopic)`** — binds a subscriber to a topic: supplies the
|
|
74
|
+
subscribable event types/payloads _and_ records the binding for the manifest.
|
|
75
|
+
A consumer doesn't publish, so this requires **no** publisher connection string
|
|
76
|
+
(least privilege) — unlike typing via `.publisher(...)`.
|
|
77
|
+
|
|
78
|
+
**`@geekmidas/manifest`** — new `TopicInfo` + `manifest.topics`; `SubscriberInfo`
|
|
79
|
+
gains `topic` (the bound topic name).
|
|
80
|
+
|
|
81
|
+
**`@geekmidas/cli`** — `TopicGenerator` discovers `t` topics into `manifest.topics`
|
|
82
|
+
(a topic has no handler to generate); new `topics` config glob; wired through
|
|
83
|
+
`gkm build`/`gkm dev` and both manifest writers.
|
|
84
|
+
|
|
85
|
+
Hand-written publisher services still work; `t` is the encouraged path.
|
|
86
|
+
|
|
87
|
+
### Patch Changes
|
|
88
|
+
|
|
89
|
+
- ✨ [#8](https://github.com/geekmidas/toolbox/pull/8) [`b42e96b`](https://github.com/geekmidas/toolbox/commit/b42e96b9dd28d8926a1253a97aa553bd0e08bf56) Thanks [@geekmidas](https://github.com/geekmidas)! - feat(cloud): add `fromManifest` integrators backed by a shared `@geekmidas/manifest` package
|
|
90
|
+
|
|
91
|
+
Introduces `@geekmidas/manifest` — a dependency-free package holding the
|
|
92
|
+
deployment manifest types (`RouteInfo`/`FunctionInfo`/`CronInfo`/`SubscriberInfo`
|
|
93
|
+
and their `*Manifest` containers) that `gkm build` emits. `@geekmidas/cli`
|
|
94
|
+
re-exports these from the shared package (no behaviour change), so producer and
|
|
95
|
+
consumers share one contract.
|
|
96
|
+
|
|
97
|
+
`@geekmidas/cloud/sst` constructs gain static `fromManifest` factories that map
|
|
98
|
+
a manifest straight into infrastructure:
|
|
99
|
+
- `Api.fromManifest(stack, id, routesManifest, props)` — one route per
|
|
100
|
+
`RouteInfo` (env vars, authorizer, timeout/memory mapped); supply
|
|
101
|
+
`authorizers`/`links`/native args via `props`.
|
|
102
|
+
- `Function.fromManifest(stack, functionsManifest, props)` — one `Function` per
|
|
103
|
+
entry.
|
|
104
|
+
- `Cron.fromManifest(stack, cronsManifest, { links, ... })` — one `Cron` per
|
|
105
|
+
entry; each handler becomes a validated `Function` the cron triggers.
|
|
106
|
+
|
|
107
|
+
`Api` routes also gain per-route `timeout`/`memory` passthrough.
|
|
108
|
+
|
|
109
|
+
- ✨ [#8](https://github.com/geekmidas/toolbox/pull/8) [`03b08fe`](https://github.com/geekmidas/toolbox/commit/03b08feba2e735539c43f95b77792c18a627b07d) Thanks [@geekmidas](https://github.com/geekmidas)! - refactor(manifest): model the unified `gkm build` manifest and add `QueueInfo`
|
|
110
|
+
|
|
111
|
+
`gkm build` emits a single TypeScript module per provider
|
|
112
|
+
(`export const manifest = { routes, functions, crons, subscribers } as const`),
|
|
113
|
+
not separate JSON files. `@geekmidas/manifest` now models that:
|
|
114
|
+
- a unified `Manifest` type plus `ManifestField<T>` (a field is a flat
|
|
115
|
+
`readonly T[]` or a partitioned `Record<string, readonly T[]>`) and a
|
|
116
|
+
`flattenManifestField` helper;
|
|
117
|
+
- the item types (`RouteInfo`/`FunctionInfo`/`CronInfo`/`SubscriberInfo`) gain a
|
|
118
|
+
new `QueueInfo`, `SubscriberInfo.transport`, and readonly array fields so the
|
|
119
|
+
`as const` manifest assigns cleanly;
|
|
120
|
+
- 🔥 the per-unit `*Manifest` wrapper types are removed.
|
|
121
|
+
|
|
122
|
+
`@geekmidas/cloud/sst`'s `Api`/`Function`/`Cron` `fromManifest` now take the
|
|
123
|
+
manifest **field** (`Api.fromManifest(stack, id, manifest.routes, …)`) and
|
|
124
|
+
flatten the flat-or-partitioned shape. `@geekmidas/cli` re-exports the updated
|
|
125
|
+
types.
|
|
126
|
+
|
|
127
|
+
Also fixes `@geekmidas/events` to externalise `pg-boss` (it was the one
|
|
128
|
+
transport dep being bundled).
|
|
129
|
+
|
|
130
|
+
- Updated dependencies [[`b42e96b`](https://github.com/geekmidas/toolbox/commit/b42e96b9dd28d8926a1253a97aa553bd0e08bf56), [`b004fd8`](https://github.com/geekmidas/toolbox/commit/b004fd8ee74b5f20a047260b16669d16d8fc03b4), [`7323f34`](https://github.com/geekmidas/toolbox/commit/7323f34176d63170dd53450889ac0b5959420c3c), [`79e2929`](https://github.com/geekmidas/toolbox/commit/79e292978d3dbc8927e25814bdb051d1c380600a), [`03b08fe`](https://github.com/geekmidas/toolbox/commit/03b08feba2e735539c43f95b77792c18a627b07d), [`0dad77e`](https://github.com/geekmidas/toolbox/commit/0dad77e574000e4018033b956ed4bb95935911a5)]:
|
|
131
|
+
- @geekmidas/manifest@0.1.0
|
|
132
|
+
- @geekmidas/constructs@5.0.0
|
|
133
|
+
- @geekmidas/envkit@1.1.0
|
|
134
|
+
|
|
135
|
+
## 1.11.0
|
|
136
|
+
|
|
137
|
+
### Minor Changes
|
|
138
|
+
|
|
139
|
+
- ✨ [#6](https://github.com/geekmidas/toolbox/pull/6) [`86a7967`](https://github.com/geekmidas/toolbox/commit/86a7967332a437c73177d06f6a2ed709e42c7060) Thanks [@geekmidas](https://github.com/geekmidas)! - feat(cli): add `gkm test --auto-setup` to self-provision a stage in CI
|
|
140
|
+
|
|
141
|
+
`gkm test` previously required a local secrets file and the matching `~/.gkm`
|
|
142
|
+
encryption key, so it could not run on a fresh CI checkout (where `.env` and
|
|
143
|
+
`.gkm/` are gitignored).
|
|
144
|
+
|
|
145
|
+
With `--auto-setup` (or the `GKM_AUTO_SETUP` env var), `gkm test` now
|
|
146
|
+
regenerates a fresh stage from the committed `gkm.config.ts` when no secrets
|
|
147
|
+
exist — minting service credentials and a local key, then starting Docker with
|
|
148
|
+
those values. For tests this is safe because the credentials are ephemeral local
|
|
149
|
+
service passwords used to bring up the matching containers. The behavior is a
|
|
150
|
+
no-op when secrets already exist and is scoped to `gkm test` only.
|
|
151
|
+
|
|
3
152
|
## 1.10.41
|
|
4
153
|
|
|
5
154
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -619,6 +619,7 @@ gkm test [options]
|
|
|
619
619
|
- `--watch`: Enable watch mode
|
|
620
620
|
- `--coverage`: Generate coverage report
|
|
621
621
|
- `--ui`: Open Vitest UI
|
|
622
|
+
- `--auto-setup`: Generate a fresh stage (secrets + key) from `gkm.config.ts` when none exists — for CI (also via `GKM_AUTO_SETUP`)
|
|
622
623
|
- `--pattern <pattern>`: Pattern to filter tests
|
|
623
624
|
|
|
624
625
|
**What it does:**
|
|
@@ -646,6 +647,19 @@ gkm test --coverage
|
|
|
646
647
|
>
|
|
647
648
|
> `gkm dev` and `gkm test` decrypt your secrets and pass them as environment variables to `docker compose up`, ensuring credentials and ports are consistent. Running `docker compose up` directly skips this — variables like `${POSTGRES_USER:-postgres}` fall back to defaults that won't match your project credentials.
|
|
648
649
|
|
|
650
|
+
> **Running in CI:**
|
|
651
|
+
>
|
|
652
|
+
> A fresh CI checkout has no `.gkm/secrets/{stage}.json` and no encryption key (`.env` and `.gkm/` are typically gitignored), so there's nothing to decrypt. Pass `--auto-setup` (or set `GKM_AUTO_SETUP=1`) to regenerate the stage from the committed `gkm.config.ts`:
|
|
653
|
+
>
|
|
654
|
+
> ```yaml
|
|
655
|
+
> env:
|
|
656
|
+
> GKM_AUTO_SETUP: '1'
|
|
657
|
+
> steps:
|
|
658
|
+
> - run: gkm test --run
|
|
659
|
+
> ```
|
|
660
|
+
>
|
|
661
|
+
> When no secrets exist for the stage, `gkm test` generates fresh service credentials and a local key, then starts Docker with those values. This is safe for tests — the credentials are ephemeral local service passwords used to bring up the matching containers, so nothing real is committed. It's a no-op when secrets already exist and is scoped to `gkm test` only.
|
|
662
|
+
|
|
649
663
|
### `gkm secrets:init`
|
|
650
664
|
|
|
651
665
|
Initialize secrets for a deployment stage. Generates secure random passwords for configured Docker Compose services.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CachedStateProvider-
|
|
1
|
+
{"version":3,"file":"CachedStateProvider-CzyFct42.mjs","names":["remote: StateProvider","local: StateProvider","stage: string","state: DokployStageState"],"sources":["../src/deploy/CachedStateProvider.ts"],"sourcesContent":["/**\n * Cached State Provider\n *\n * Wraps a remote state provider (SSM) with local filesystem cache.\n * - Read: Local first, then remote if missing\n * - Write: Remote first, then update local cache\n *\n * This enables fast local development while keeping SSM as source of truth.\n */\n\nimport type { StateProvider } from './StateProvider';\nimport type { DokployStageState } from './state';\n\n/**\n * Cached state provider that wraps a remote provider with local cache.\n */\nexport class CachedStateProvider implements StateProvider {\n\tconstructor(\n\t\tprivate readonly remote: StateProvider,\n\t\tprivate readonly local: StateProvider,\n\t) {}\n\n\tasync read(stage: string): Promise<DokployStageState | null> {\n\t\t// Try local cache first\n\t\tconst localState = await this.local.read(stage);\n\t\tif (localState) {\n\t\t\treturn localState;\n\t\t}\n\n\t\t// Fall back to remote\n\t\tconst remoteState = await this.remote.read(stage);\n\t\tif (remoteState) {\n\t\t\t// Update local cache\n\t\t\tawait this.local.write(stage, remoteState);\n\t\t}\n\n\t\treturn remoteState;\n\t}\n\n\tasync write(stage: string, state: DokployStageState): Promise<void> {\n\t\t// Write to remote first (source of truth)\n\t\tawait this.remote.write(stage, state);\n\n\t\t// Update local cache\n\t\tawait this.local.write(stage, state);\n\t}\n\n\t/**\n\t * Force pull from remote to local.\n\t * Used by `gkm state pull` command.\n\t */\n\tasync pull(stage: string): Promise<DokployStageState | null> {\n\t\tconst remoteState = await this.remote.read(stage);\n\t\tif (remoteState) {\n\t\t\tawait this.local.write(stage, remoteState);\n\t\t}\n\t\treturn remoteState;\n\t}\n\n\t/**\n\t * Force push from local to remote.\n\t * Used by `gkm state push` command.\n\t */\n\tasync push(stage: string): Promise<DokployStageState | null> {\n\t\tconst localState = await this.local.read(stage);\n\t\tif (localState) {\n\t\t\tawait this.remote.write(stage, localState);\n\t\t}\n\t\treturn localState;\n\t}\n\n\t/**\n\t * Get both local and remote state for comparison.\n\t * Used by `gkm state diff` command.\n\t */\n\tasync diff(stage: string): Promise<{\n\t\tlocal: DokployStageState | null;\n\t\tremote: DokployStageState | null;\n\t}> {\n\t\tconst [local, remote] = await Promise.all([\n\t\t\tthis.local.read(stage),\n\t\t\tthis.remote.read(stage),\n\t\t]);\n\t\treturn { local, remote };\n\t}\n}\n"],"mappings":";;;;AAgBA,IAAa,sBAAb,MAA0D;CACzD,YACkBA,QACAC,OAChB;EAFgB;EACA;CACd;CAEJ,MAAM,KAAKC,OAAkD;EAE5D,MAAM,aAAa,MAAM,KAAK,MAAM,KAAK,MAAM;AAC/C,MAAI,WACH,QAAO;EAIR,MAAM,cAAc,MAAM,KAAK,OAAO,KAAK,MAAM;AACjD,MAAI,YAEH,OAAM,KAAK,MAAM,MAAM,OAAO,YAAY;AAG3C,SAAO;CACP;CAED,MAAM,MAAMA,OAAeC,OAAyC;AAEnE,QAAM,KAAK,OAAO,MAAM,OAAO,MAAM;AAGrC,QAAM,KAAK,MAAM,MAAM,OAAO,MAAM;CACpC;;;;;CAMD,MAAM,KAAKD,OAAkD;EAC5D,MAAM,cAAc,MAAM,KAAK,OAAO,KAAK,MAAM;AACjD,MAAI,YACH,OAAM,KAAK,MAAM,MAAM,OAAO,YAAY;AAE3C,SAAO;CACP;;;;;CAMD,MAAM,KAAKA,OAAkD;EAC5D,MAAM,aAAa,MAAM,KAAK,MAAM,KAAK,MAAM;AAC/C,MAAI,WACH,OAAM,KAAK,OAAO,MAAM,OAAO,WAAW;AAE3C,SAAO;CACP;;;;;CAMD,MAAM,KAAKA,OAGR;EACF,MAAM,CAAC,OAAO,OAAO,GAAG,MAAM,QAAQ,IAAI,CACzC,KAAK,MAAM,KAAK,MAAM,EACtB,KAAK,OAAO,KAAK,MAAM,AACvB,EAAC;AACF,SAAO;GAAE;GAAO;EAAQ;CACxB;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CachedStateProvider-
|
|
1
|
+
{"version":3,"file":"CachedStateProvider-klLWZXWF.cjs","names":["remote: StateProvider","local: StateProvider","stage: string","state: DokployStageState"],"sources":["../src/deploy/CachedStateProvider.ts"],"sourcesContent":["/**\n * Cached State Provider\n *\n * Wraps a remote state provider (SSM) with local filesystem cache.\n * - Read: Local first, then remote if missing\n * - Write: Remote first, then update local cache\n *\n * This enables fast local development while keeping SSM as source of truth.\n */\n\nimport type { StateProvider } from './StateProvider';\nimport type { DokployStageState } from './state';\n\n/**\n * Cached state provider that wraps a remote provider with local cache.\n */\nexport class CachedStateProvider implements StateProvider {\n\tconstructor(\n\t\tprivate readonly remote: StateProvider,\n\t\tprivate readonly local: StateProvider,\n\t) {}\n\n\tasync read(stage: string): Promise<DokployStageState | null> {\n\t\t// Try local cache first\n\t\tconst localState = await this.local.read(stage);\n\t\tif (localState) {\n\t\t\treturn localState;\n\t\t}\n\n\t\t// Fall back to remote\n\t\tconst remoteState = await this.remote.read(stage);\n\t\tif (remoteState) {\n\t\t\t// Update local cache\n\t\t\tawait this.local.write(stage, remoteState);\n\t\t}\n\n\t\treturn remoteState;\n\t}\n\n\tasync write(stage: string, state: DokployStageState): Promise<void> {\n\t\t// Write to remote first (source of truth)\n\t\tawait this.remote.write(stage, state);\n\n\t\t// Update local cache\n\t\tawait this.local.write(stage, state);\n\t}\n\n\t/**\n\t * Force pull from remote to local.\n\t * Used by `gkm state pull` command.\n\t */\n\tasync pull(stage: string): Promise<DokployStageState | null> {\n\t\tconst remoteState = await this.remote.read(stage);\n\t\tif (remoteState) {\n\t\t\tawait this.local.write(stage, remoteState);\n\t\t}\n\t\treturn remoteState;\n\t}\n\n\t/**\n\t * Force push from local to remote.\n\t * Used by `gkm state push` command.\n\t */\n\tasync push(stage: string): Promise<DokployStageState | null> {\n\t\tconst localState = await this.local.read(stage);\n\t\tif (localState) {\n\t\t\tawait this.remote.write(stage, localState);\n\t\t}\n\t\treturn localState;\n\t}\n\n\t/**\n\t * Get both local and remote state for comparison.\n\t * Used by `gkm state diff` command.\n\t */\n\tasync diff(stage: string): Promise<{\n\t\tlocal: DokployStageState | null;\n\t\tremote: DokployStageState | null;\n\t}> {\n\t\tconst [local, remote] = await Promise.all([\n\t\t\tthis.local.read(stage),\n\t\t\tthis.remote.read(stage),\n\t\t]);\n\t\treturn { local, remote };\n\t}\n}\n"],"mappings":";;;;;AAgBA,IAAa,sBAAb,MAA0D;CACzD,YACkBA,QACAC,OAChB;EAFgB;EACA;CACd;CAEJ,MAAM,KAAKC,OAAkD;EAE5D,MAAM,aAAa,MAAM,KAAK,MAAM,KAAK,MAAM;AAC/C,MAAI,WACH,QAAO;EAIR,MAAM,cAAc,MAAM,KAAK,OAAO,KAAK,MAAM;AACjD,MAAI,YAEH,OAAM,KAAK,MAAM,MAAM,OAAO,YAAY;AAG3C,SAAO;CACP;CAED,MAAM,MAAMA,OAAeC,OAAyC;AAEnE,QAAM,KAAK,OAAO,MAAM,OAAO,MAAM;AAGrC,QAAM,KAAK,MAAM,MAAM,OAAO,MAAM;CACpC;;;;;CAMD,MAAM,KAAKD,OAAkD;EAC5D,MAAM,cAAc,MAAM,KAAK,OAAO,KAAK,MAAM;AACjD,MAAI,YACH,OAAM,KAAK,MAAM,MAAM,OAAO,YAAY;AAE3C,SAAO;CACP;;;;;CAMD,MAAM,KAAKA,OAAkD;EAC5D,MAAM,aAAa,MAAM,KAAK,MAAM,KAAK,MAAM;AAC/C,MAAI,WACH,OAAM,KAAK,OAAO,MAAM,OAAO,WAAW;AAE3C,SAAO;CACP;;;;;CAMD,MAAM,KAAKA,OAGR;EACF,MAAM,CAAC,OAAO,OAAO,GAAG,MAAM,QAAQ,IAAI,CACzC,KAAK,MAAM,KAAK,MAAM,EACtB,KAAK,OAAO,KAAK,MAAM,AACvB,EAAC;AACF,SAAO;GAAE;GAAO;EAAQ;CACxB;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HostingerProvider-ANWchdiK.mjs","names":["message: string","status: number","statusText: string","errors?: Record<string, string[]>","token: string","method: 'GET' | 'POST' | 'PUT' | 'DELETE'","endpoint: string","body?: unknown","errors: Record<string, string[]> | undefined","domain: string","records: DnsRecord[]","filters: DnsRecordFilter[]","name: string","type: DnsRecordType","subdomain: string","ip: string","domain: string","records: UpsertDnsRecord[]","results: UpsertResult[]","records: DeleteDnsRecord[]","results: DeleteResult[]"],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tinterface RecordResponse {\n\t\t\tdata: Array<{\n\t\t\t\tname: string;\n\t\t\t\ttype: DnsRecordType;\n\t\t\t\tttl: number;\n\t\t\t\trecords: Array<{ content: string }>;\n\t\t\t}>;\n\t\t}\n\n\t\tconst response = await this.request<RecordResponse>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\treturn response.data || [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to check what exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\t// Filter to only records that exist\n\t\tconst recordsToDelete = records.filter((record) =>\n\t\t\texistingRecords.some(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t),\n\t\t);\n\n\t\t// Delete existing records\n\t\tif (recordsToDelete.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait api.deleteRecords(\n\t\t\t\t\tdomain,\n\t\t\t\t\trecordsToDelete.map((r) => ({\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\ttype: r.type as\n\t\t\t\t\t\t\t| 'A'\n\t\t\t\t\t\t\t| 'AAAA'\n\t\t\t\t\t\t\t| 'CNAME'\n\t\t\t\t\t\t\t| 'MX'\n\t\t\t\t\t\t\t| 'TXT'\n\t\t\t\t\t\t\t| 'SRV'\n\t\t\t\t\t\t\t| 'CAA',\n\t\t\t\t\t})),\n\t\t\t\t);\n\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: true,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// If batch delete fails, report error for all records\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t\terror: String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Mark non-existent records as not found\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tif (!existing) {\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: false,\n\t\t\t\t\tnotFound: true,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":";;;;;;;;;AAOA,MAAM,qBAAqB;;;;AAgD3B,IAAa,oBAAb,cAAuC,MAAM;CAC5C,YACCA,SACOC,QACAC,YACAC,QACN;AACD,QAAM,QAAQ;EAJP;EACA;EACA;AAGP,OAAK,OAAO;CACZ;AACD;;;;;;;;;;;;;;;;;AAkBD,IAAa,eAAb,MAA0B;CACzB,AAAQ;CAER,YAAYC,OAAe;AAC1B,OAAK,QAAQ;CACb;;;;CAKD,MAAc,QACbC,QACAC,UACAC,MACa;EACb,MAAM,OAAO,EAAE,mBAAmB,EAAE,SAAS;EAE7C,MAAM,WAAW,MAAM,MAAM,KAAK;GACjC;GACA,SAAS;IACR,gBAAgB;IAChB,gBAAgB,SAAS,KAAK,MAAM;GACpC;GACD,MAAM,OAAO,KAAK,UAAU,KAAK;EACjC,EAAC;AAEF,OAAK,SAAS,IAAI;GACjB,IAAI,gBAAgB,uBAAuB,SAAS,OAAO,GAAG,SAAS,WAAW;GAClF,IAAIC;AAEJ,OAAI;IACH,MAAM,YAAa,MAAM,SAAS,MAAM;AACxC,QAAI,UAAU,QACb,iBAAgB,uBAAuB,UAAU,QAAQ;AAE1D,aAAS,UAAU;GACnB,QAAO,CAEP;AAED,SAAM,IAAI,kBACT,cACA,SAAS,QACT,SAAS,YACT;EAED;EAGD,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,OAAK,QAAQ,KAAK,MAAM,KAAK,GAC5B;AAED,SAAO,KAAK,MAAM,KAAK;CACvB;;;;;;CAOD,MAAM,WAAWC,QAAsC;EAUtD,MAAM,WAAW,MAAM,KAAK,QAC3B,QACC,oBAAoB,OAAO,EAC5B;AAED,SAAO,SAAS,QAAQ,CAAE;CAC1B;;;;;;;;CASD,MAAM,cACLA,QACAC,SACA,YAAY,OACI;AAChB,QAAM,KAAK,QAAQ,QAAQ,oBAAoB,OAAO,GAAG;GACxD;GACA,MAAM;EACN,EAAC;CACF;;;;;;;;CASD,MAAM,gBACLD,QACAC,SACmB;AACnB,QAAM,KAAK,QAAQ,SAAS,oBAAoB,OAAO,YAAY;GAClE,WAAW;GACX,MAAM;EACN,EAAC;AACF,SAAO;CACP;;;;;;;CAQD,MAAM,cACLD,QACAE,SACgB;AAChB,QAAM,KAAK,QAAQ,WAAW,oBAAoB,OAAO,GAAG,EAC3D,QACA,EAAC;CACF;;;;;;;;CASD,MAAM,aACLF,QACAG,MACAC,OAAsB,KACH;EACnB,MAAM,UAAU,MAAM,KAAK,WAAW,OAAO;AAC7C,SAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,SAAS,KAAK;CAC9D;;;;;;;;;;CAWD,MAAM,yBACLJ,QACAK,WACAC,IACA,MAAM,KACa;EACnB,MAAM,SAAS,MAAM,KAAK,aAAa,QAAQ,WAAW,IAAI;AAC9D,MAAI,OACH,QAAO;AAGR,QAAM,KAAK,cAAc,QAAQ,CAChC;GACC,MAAM;GACN,MAAM;GACN;GACA,SAAS,CAAC,EAAE,SAAS,GAAI,CAAC;EAC1B,CACD,EAAC;AAEF,SAAO;CACP;AACD;;;;;;;AChPD,IAAa,oBAAb,MAAsD;CACrD,AAAS,OAAO;CAChB,AAAQ,MAA2B;;;;CAKnC,MAAc,SAAgC;AAC7C,MAAI,KAAK,IACR,QAAO,KAAK;EAGb,MAAM,QAAQ,MAAM,mBAAmB;AACvC,OAAK,MACJ,OAAM,IAAI,MACT;AAIF,OAAK,MAAM,IAAI,aAAa;AAC5B,SAAO,KAAK;CACZ;CAED,MAAM,WAAWC,QAAsC;EACtD,MAAM,MAAM,MAAM,KAAK,QAAQ;EAC/B,MAAM,UAAU,MAAM,IAAI,WAAW,OAAO;AAE5C,SAAO,QAAQ,IAAI,CAAC,OAAO;GAC1B,MAAM,EAAE;GACR,MAAM,EAAE;GACR,KAAK,EAAE;GACP,QAAQ,EAAE,QAAQ,IAAI,CAAC,QAAQ,IAAI,QAAQ;EAC3C,GAAE;CACH;CAED,MAAM,cACLA,QACAC,SAC0B;EAC1B,MAAM,MAAM,MAAM,KAAK,QAAQ;EAC/B,MAAMC,UAA0B,CAAE;EAGlC,MAAM,kBAAkB,MAAM,IAAI,WAAW,OAAO;AAEpD,OAAK,MAAM,UAAU,SAAS;GAC7B,MAAM,WAAW,gBAAgB,KAChC,CAAC,MAAM,EAAE,SAAS,OAAO,QAAQ,EAAE,SAAS,OAAO,KACnD;GAED,MAAM,gBAAgB,UAAU,UAAU,IAAI;AAE9C,OAAI,YAAY,kBAAkB,OAAO,OAAO;AAE/C,YAAQ,KAAK;KACZ;KACA,SAAS;KACT,WAAW;IACX,EAAC;AACF;GACA;AAGD,SAAM,IAAI,cAAc,QAAQ,CAC/B;IACC,MAAM,OAAO;IACb,MAAM,OAAO;IACb,KAAK,OAAO;IACZ,SAAS,CAAC,EAAE,SAAS,OAAO,MAAO,CAAC;GACpC,CACD,EAAC;AAEF,WAAQ,KAAK;IACZ;IACA,UAAU;IACV,WAAW;GACX,EAAC;EACF;AAED,SAAO;CACP;CAED,MAAM,cACLF,QACAG,SAC0B;EAC1B,MAAM,MAAM,MAAM,KAAK,QAAQ;EAC/B,MAAMC,UAA0B,CAAE;EAGlC,MAAM,kBAAkB,MAAM,IAAI,WAAW,OAAO;EAGpD,MAAM,kBAAkB,QAAQ,OAAO,CAAC,WACvC,gBAAgB,KACf,CAAC,MAAM,EAAE,SAAS,OAAO,QAAQ,EAAE,SAAS,OAAO,KACnD,CACD;AAGD,MAAI,gBAAgB,SAAS,EAC5B,KAAI;AACH,SAAM,IAAI,cACT,QACA,gBAAgB,IAAI,CAAC,OAAO;IAC3B,MAAM,EAAE;IACR,MAAM,EAAE;GAQR,GAAE,CACH;AAED,QAAK,MAAM,UAAU,gBACpB,SAAQ,KAAK;IACZ;IACA,SAAS;IACT,UAAU;GACV,EAAC;EAEH,SAAQ,OAAO;AAEf,QAAK,MAAM,UAAU,gBACpB,SAAQ,KAAK;IACZ;IACA,SAAS;IACT,UAAU;IACV,OAAO,OAAO,MAAM;GACpB,EAAC;EAEH;AAIF,OAAK,MAAM,UAAU,SAAS;GAC7B,MAAM,WAAW,gBAAgB,KAChC,CAAC,MAAM,EAAE,SAAS,OAAO,QAAQ,EAAE,SAAS,OAAO,KACnD;AACD,QAAK,SACJ,SAAQ,KAAK;IACZ;IACA,SAAS;IACT,UAAU;GACV,EAAC;EAEH;AAED,SAAO;CACP;AACD"}
|
|
1
|
+
{"version":3,"file":"HostingerProvider-B2ZYz7Yc.mjs","names":["message: string","status: number","statusText: string","errors?: Record<string, string[]>","token: string","method: 'GET' | 'POST' | 'PUT' | 'DELETE'","endpoint: string","body?: unknown","errors: Record<string, string[]> | undefined","domain: string","records: DnsRecord[]","filters: DnsRecordFilter[]","name: string","type: DnsRecordType","subdomain: string","ip: string","domain: string","records: UpsertDnsRecord[]","results: UpsertResult[]","records: DeleteDnsRecord[]","results: DeleteResult[]"],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tinterface RecordResponse {\n\t\t\tdata: Array<{\n\t\t\t\tname: string;\n\t\t\t\ttype: DnsRecordType;\n\t\t\t\tttl: number;\n\t\t\t\trecords: Array<{ content: string }>;\n\t\t\t}>;\n\t\t}\n\n\t\tconst response = await this.request<RecordResponse>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\treturn response.data || [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to check what exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\t// Filter to only records that exist\n\t\tconst recordsToDelete = records.filter((record) =>\n\t\t\texistingRecords.some(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t),\n\t\t);\n\n\t\t// Delete existing records\n\t\tif (recordsToDelete.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait api.deleteRecords(\n\t\t\t\t\tdomain,\n\t\t\t\t\trecordsToDelete.map((r) => ({\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\ttype: r.type as\n\t\t\t\t\t\t\t| 'A'\n\t\t\t\t\t\t\t| 'AAAA'\n\t\t\t\t\t\t\t| 'CNAME'\n\t\t\t\t\t\t\t| 'MX'\n\t\t\t\t\t\t\t| 'TXT'\n\t\t\t\t\t\t\t| 'SRV'\n\t\t\t\t\t\t\t| 'CAA',\n\t\t\t\t\t})),\n\t\t\t\t);\n\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: true,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// If batch delete fails, report error for all records\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t\terror: String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Mark non-existent records as not found\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tif (!existing) {\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: false,\n\t\t\t\t\tnotFound: true,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HostingerProvider-5KYmwoK2.cjs","names":["message: string","status: number","statusText: string","errors?: Record<string, string[]>","token: string","method: 'GET' | 'POST' | 'PUT' | 'DELETE'","endpoint: string","body?: unknown","errors: Record<string, string[]> | undefined","domain: string","records: DnsRecord[]","filters: DnsRecordFilter[]","name: string","type: DnsRecordType","subdomain: string","ip: string","domain: string","records: UpsertDnsRecord[]","results: UpsertResult[]","records: DeleteDnsRecord[]","results: DeleteResult[]"],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tinterface RecordResponse {\n\t\t\tdata: Array<{\n\t\t\t\tname: string;\n\t\t\t\ttype: DnsRecordType;\n\t\t\t\tttl: number;\n\t\t\t\trecords: Array<{ content: string }>;\n\t\t\t}>;\n\t\t}\n\n\t\tconst response = await this.request<RecordResponse>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\treturn response.data || [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to check what exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\t// Filter to only records that exist\n\t\tconst recordsToDelete = records.filter((record) =>\n\t\t\texistingRecords.some(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t),\n\t\t);\n\n\t\t// Delete existing records\n\t\tif (recordsToDelete.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait api.deleteRecords(\n\t\t\t\t\tdomain,\n\t\t\t\t\trecordsToDelete.map((r) => ({\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\ttype: r.type as\n\t\t\t\t\t\t\t| 'A'\n\t\t\t\t\t\t\t| 'AAAA'\n\t\t\t\t\t\t\t| 'CNAME'\n\t\t\t\t\t\t\t| 'MX'\n\t\t\t\t\t\t\t| 'TXT'\n\t\t\t\t\t\t\t| 'SRV'\n\t\t\t\t\t\t\t| 'CAA',\n\t\t\t\t\t})),\n\t\t\t\t);\n\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: true,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// If batch delete fails, report error for all records\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t\terror: String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Mark non-existent records as not found\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tif (!existing) {\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: false,\n\t\t\t\t\tnotFound: true,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HostingerProvider-CrHfsgz9.cjs","names":["message: string","status: number","statusText: string","errors?: Record<string, string[]>","token: string","method: 'GET' | 'POST' | 'PUT' | 'DELETE'","endpoint: string","body?: unknown","errors: Record<string, string[]> | undefined","domain: string","records: DnsRecord[]","filters: DnsRecordFilter[]","name: string","type: DnsRecordType","subdomain: string","ip: string","domain: string","records: UpsertDnsRecord[]","results: UpsertResult[]","records: DeleteDnsRecord[]","results: DeleteResult[]"],"sources":["../src/deploy/dns/hostinger-api.ts","../src/deploy/dns/HostingerProvider.ts"],"sourcesContent":["/**\n * Hostinger DNS API client\n *\n * API Documentation: https://developers.hostinger.com/\n * Authentication: Bearer token from hpanel.hostinger.com/profile/api\n */\n\nconst HOSTINGER_API_BASE = 'https://developers.hostinger.com';\n\n/**\n * DNS record types supported by Hostinger\n */\nexport type DnsRecordType =\n\t| 'A'\n\t| 'AAAA'\n\t| 'CNAME'\n\t| 'MX'\n\t| 'TXT'\n\t| 'NS'\n\t| 'SRV'\n\t| 'CAA';\n\n/**\n * A single DNS record\n */\nexport interface DnsRecord {\n\t/** Subdomain name (e.g., 'api.joemoer' for api.joemoer.traflabs.io) */\n\tname: string;\n\t/** Record type */\n\ttype: DnsRecordType;\n\t/** TTL in seconds */\n\tttl: number;\n\t/** Record values */\n\trecords: Array<{ content: string }>;\n}\n\n/**\n * Filter for deleting specific records\n */\nexport interface DnsRecordFilter {\n\tname: string;\n\ttype: DnsRecordType;\n}\n\n/**\n * API error response\n */\nexport interface HostingerErrorResponse {\n\tmessage?: string;\n\terrors?: Record<string, string[]>;\n}\n\n/**\n * Hostinger API error\n */\nexport class HostingerApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic status: number,\n\t\tpublic statusText: string,\n\t\tpublic errors?: Record<string, string[]>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'HostingerApiError';\n\t}\n}\n\n/**\n * Hostinger DNS API client\n *\n * @example\n * ```ts\n * const api = new HostingerApi(token);\n *\n * // Get all records for a domain\n * const records = await api.getRecords('traflabs.io');\n *\n * // Create/update records\n * await api.upsertRecords('traflabs.io', [\n * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }\n * ]);\n * ```\n */\nexport class HostingerApi {\n\tprivate token: string;\n\n\tconstructor(token: string) {\n\t\tthis.token = token;\n\t}\n\n\t/**\n\t * Make a request to the Hostinger API\n\t */\n\tprivate async request<T>(\n\t\tmethod: 'GET' | 'POST' | 'PUT' | 'DELETE',\n\t\tendpoint: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst url = `${HOSTINGER_API_BASE}${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\tAuthorization: `Bearer ${this.token}`,\n\t\t\t},\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;\n\t\t\tlet errors: Record<string, string[]> | undefined;\n\n\t\t\ttry {\n\t\t\t\tconst errorBody = (await response.json()) as HostingerErrorResponse;\n\t\t\t\tif (errorBody.message) {\n\t\t\t\t\terrorMessage = `Hostinger API error: ${errorBody.message}`;\n\t\t\t\t}\n\t\t\t\terrors = errorBody.errors;\n\t\t\t} catch {\n\t\t\t\t// Ignore JSON parse errors\n\t\t\t}\n\n\t\t\tthrow new HostingerApiError(\n\t\t\t\terrorMessage,\n\t\t\t\tresponse.status,\n\t\t\t\tresponse.statusText,\n\t\t\t\terrors,\n\t\t\t);\n\t\t}\n\n\t\t// Handle empty responses\n\t\tconst text = await response.text();\n\t\tif (!text || text.trim() === '') {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\t/**\n\t * Get all DNS records for a domain\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t */\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tinterface RecordResponse {\n\t\t\tdata: Array<{\n\t\t\t\tname: string;\n\t\t\t\ttype: DnsRecordType;\n\t\t\t\tttl: number;\n\t\t\t\trecords: Array<{ content: string }>;\n\t\t\t}>;\n\t\t}\n\n\t\tconst response = await this.request<RecordResponse>(\n\t\t\t'GET',\n\t\t\t`/api/dns/v1/zones/${domain}`,\n\t\t);\n\n\t\treturn response.data || [];\n\t}\n\n\t/**\n\t * Create or update DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to create/update\n\t * @param overwrite - If true, replaces all existing records. If false, merges with existing.\n\t */\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t\toverwrite = false,\n\t): Promise<void> {\n\t\tawait this.request('PUT', `/api/dns/v1/zones/${domain}`, {\n\t\t\toverwrite,\n\t\t\tzone: records,\n\t\t});\n\t}\n\n\t/**\n\t * Validate DNS records before applying\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param records - Records to validate\n\t * @returns true if valid, throws if invalid\n\t */\n\tasync validateRecords(\n\t\tdomain: string,\n\t\trecords: DnsRecord[],\n\t): Promise<boolean> {\n\t\tawait this.request('POST', `/api/dns/v1/zones/${domain}/validate`, {\n\t\t\toverwrite: false,\n\t\t\tzone: records,\n\t\t});\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete specific DNS records\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param filters - Filters to match records for deletion\n\t */\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\tfilters: DnsRecordFilter[],\n\t): Promise<void> {\n\t\tawait this.request('DELETE', `/api/dns/v1/zones/${domain}`, {\n\t\t\tfilters,\n\t\t});\n\t}\n\n\t/**\n\t * Check if a specific record exists\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param name - Subdomain name (e.g., 'api.joemoer')\n\t * @param type - Record type (e.g., 'A')\n\t */\n\tasync recordExists(\n\t\tdomain: string,\n\t\tname: string,\n\t\ttype: DnsRecordType = 'A',\n\t): Promise<boolean> {\n\t\tconst records = await this.getRecords(domain);\n\t\treturn records.some((r) => r.name === name && r.type === type);\n\t}\n\n\t/**\n\t * Create a single A record if it doesn't exist\n\t *\n\t * @param domain - Root domain (e.g., 'traflabs.io')\n\t * @param subdomain - Subdomain name (e.g., 'api.joemoer')\n\t * @param ip - IP address to point to\n\t * @param ttl - TTL in seconds (default: 300)\n\t * @returns true if created, false if already exists\n\t */\n\tasync createARecordIfNotExists(\n\t\tdomain: string,\n\t\tsubdomain: string,\n\t\tip: string,\n\t\tttl = 300,\n\t): Promise<boolean> {\n\t\tconst exists = await this.recordExists(domain, subdomain, 'A');\n\t\tif (exists) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait this.upsertRecords(domain, [\n\t\t\t{\n\t\t\t\tname: subdomain,\n\t\t\t\ttype: 'A',\n\t\t\t\tttl,\n\t\t\t\trecords: [{ content: ip }],\n\t\t\t},\n\t\t]);\n\n\t\treturn true;\n\t}\n}\n","/**\n * Hostinger DNS Provider\n *\n * Implements DnsProvider interface using the Hostinger DNS API.\n */\n\nimport { getHostingerToken } from '../../auth/credentials';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\nimport { HostingerApi } from './hostinger-api';\n\n/**\n * Hostinger DNS provider implementation.\n */\nexport class HostingerProvider implements DnsProvider {\n\treadonly name = 'hostinger';\n\tprivate api: HostingerApi | null = null;\n\n\t/**\n\t * Get or create the Hostinger API client.\n\t */\n\tprivate async getApi(): Promise<HostingerApi> {\n\t\tif (this.api) {\n\t\t\treturn this.api;\n\t\t}\n\n\t\tconst token = await getHostingerToken();\n\t\tif (!token) {\n\t\t\tthrow new Error(\n\t\t\t\t'Hostinger API token not configured. Run `gkm login --service=hostinger` or get your token from https://hpanel.hostinger.com/profile/api',\n\t\t\t);\n\t\t}\n\n\t\tthis.api = new HostingerApi(token);\n\t\treturn this.api;\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst api = await this.getApi();\n\t\tconst records = await api.getRecords(domain);\n\n\t\treturn records.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttype: r.type,\n\t\t\tttl: r.ttl,\n\t\t\tvalues: r.records.map((rec) => rec.content),\n\t\t}));\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to check what already exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\n\t\t\tconst existingValue = existing?.records?.[0]?.content;\n\n\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: false,\n\t\t\t\t\tunchanged: true,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create or update the record\n\t\t\tawait api.upsertRecords(domain, [\n\t\t\t\t{\n\t\t\t\t\tname: record.name,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\tttl: record.ttl,\n\t\t\t\t\trecords: [{ content: record.value }],\n\t\t\t\t},\n\t\t\t]);\n\n\t\t\tresults.push({\n\t\t\t\trecord,\n\t\t\t\tcreated: !existing,\n\t\t\t\tunchanged: false,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst api = await this.getApi();\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to check what exists\n\t\tconst existingRecords = await api.getRecords(domain);\n\n\t\t// Filter to only records that exist\n\t\tconst recordsToDelete = records.filter((record) =>\n\t\t\texistingRecords.some(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t),\n\t\t);\n\n\t\t// Delete existing records\n\t\tif (recordsToDelete.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait api.deleteRecords(\n\t\t\t\t\tdomain,\n\t\t\t\t\trecordsToDelete.map((r) => ({\n\t\t\t\t\t\tname: r.name,\n\t\t\t\t\t\ttype: r.type as\n\t\t\t\t\t\t\t| 'A'\n\t\t\t\t\t\t\t| 'AAAA'\n\t\t\t\t\t\t\t| 'CNAME'\n\t\t\t\t\t\t\t| 'MX'\n\t\t\t\t\t\t\t| 'TXT'\n\t\t\t\t\t\t\t| 'SRV'\n\t\t\t\t\t\t\t| 'CAA',\n\t\t\t\t\t})),\n\t\t\t\t);\n\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: true,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// If batch delete fails, report error for all records\n\t\t\t\tfor (const record of recordsToDelete) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: false,\n\t\t\t\t\t\terror: String(error),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Mark non-existent records as not found\n\t\tfor (const record of records) {\n\t\t\tconst existing = existingRecords.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tif (!existing) {\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: false,\n\t\t\t\t\tnotFound: true,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalStateProvider-
|
|
1
|
+
{"version":3,"file":"LocalStateProvider-CP49rjzt.mjs","names":["workspaceRoot: string","stage: string","state: DokployStageState"],"sources":["../src/deploy/LocalStateProvider.ts"],"sourcesContent":["/**\n * Local Filesystem State Provider\n *\n * Stores deployment state in .gkm/deploy-{stage}.json files.\n * This is the default provider for local development.\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { StateProvider } from './StateProvider';\nimport type { DokployStageState } from './state';\n\n/**\n * Get the state file path for a stage.\n */\nfunction getStateFilePath(workspaceRoot: string, stage: string): string {\n\treturn join(workspaceRoot, '.gkm', `deploy-${stage}.json`);\n}\n\n/**\n * Local filesystem state provider.\n *\n * Stores state in .gkm/deploy-{stage}.json files in the workspace root.\n */\nexport class LocalStateProvider implements StateProvider {\n\tconstructor(private readonly workspaceRoot: string) {}\n\n\tasync read(stage: string): Promise<DokployStageState | null> {\n\t\tconst filePath = getStateFilePath(this.workspaceRoot, stage);\n\n\t\ttry {\n\t\t\tconst content = await readFile(filePath, 'utf-8');\n\t\t\treturn JSON.parse(content) as DokployStageState;\n\t\t} catch (error) {\n\t\t\t// File doesn't exist - return null\n\t\t\tif ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t// Log other errors but don't fail\n\t\t\tconsole.warn(`Warning: Could not read deploy state: ${error}`);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync write(stage: string, state: DokployStageState): Promise<void> {\n\t\tconst filePath = getStateFilePath(this.workspaceRoot, stage);\n\t\tconst dir = join(this.workspaceRoot, '.gkm');\n\n\t\t// Ensure .gkm directory exists\n\t\tawait mkdir(dir, { recursive: true });\n\n\t\t// Update last deployed timestamp\n\t\tstate.lastDeployedAt = new Date().toISOString();\n\n\t\tawait writeFile(filePath, JSON.stringify(state, null, 2));\n\t}\n}\n"],"mappings":";;;;;;;AAeA,SAAS,iBAAiBA,eAAuBC,OAAuB;AACvE,QAAO,KAAK,eAAe,SAAS,SAAS,MAAM,OAAO;AAC1D;;;;;;AAOD,IAAa,qBAAb,MAAyD;CACxD,YAA6BD,eAAuB;EAAvB;CAAyB;CAEtD,MAAM,KAAKC,OAAkD;EAC5D,MAAM,WAAW,iBAAiB,KAAK,eAAe,MAAM;AAE5D,MAAI;GACH,MAAM,UAAU,MAAM,SAAS,UAAU,QAAQ;AACjD,UAAO,KAAK,MAAM,QAAQ;EAC1B,SAAQ,OAAO;AAEf,OAAK,MAAgC,SAAS,SAC7C,QAAO;AAGR,WAAQ,MAAM,wCAAwC,MAAM,EAAE;AAC9D,UAAO;EACP;CACD;CAED,MAAM,MAAMA,OAAeC,OAAyC;EACnE,MAAM,WAAW,iBAAiB,KAAK,eAAe,MAAM;EAC5D,MAAM,MAAM,KAAK,KAAK,eAAe,OAAO;AAG5C,QAAM,MAAM,KAAK,EAAE,WAAW,KAAM,EAAC;AAGrC,QAAM,iBAAiB,qBAAI,QAAO,aAAa;AAE/C,QAAM,UAAU,UAAU,KAAK,UAAU,OAAO,MAAM,EAAE,CAAC;CACzD;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalStateProvider-
|
|
1
|
+
{"version":3,"file":"LocalStateProvider-CalzNfNA.cjs","names":["workspaceRoot: string","stage: string","state: DokployStageState"],"sources":["../src/deploy/LocalStateProvider.ts"],"sourcesContent":["/**\n * Local Filesystem State Provider\n *\n * Stores deployment state in .gkm/deploy-{stage}.json files.\n * This is the default provider for local development.\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { StateProvider } from './StateProvider';\nimport type { DokployStageState } from './state';\n\n/**\n * Get the state file path for a stage.\n */\nfunction getStateFilePath(workspaceRoot: string, stage: string): string {\n\treturn join(workspaceRoot, '.gkm', `deploy-${stage}.json`);\n}\n\n/**\n * Local filesystem state provider.\n *\n * Stores state in .gkm/deploy-{stage}.json files in the workspace root.\n */\nexport class LocalStateProvider implements StateProvider {\n\tconstructor(private readonly workspaceRoot: string) {}\n\n\tasync read(stage: string): Promise<DokployStageState | null> {\n\t\tconst filePath = getStateFilePath(this.workspaceRoot, stage);\n\n\t\ttry {\n\t\t\tconst content = await readFile(filePath, 'utf-8');\n\t\t\treturn JSON.parse(content) as DokployStageState;\n\t\t} catch (error) {\n\t\t\t// File doesn't exist - return null\n\t\t\tif ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t// Log other errors but don't fail\n\t\t\tconsole.warn(`Warning: Could not read deploy state: ${error}`);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync write(stage: string, state: DokployStageState): Promise<void> {\n\t\tconst filePath = getStateFilePath(this.workspaceRoot, stage);\n\t\tconst dir = join(this.workspaceRoot, '.gkm');\n\n\t\t// Ensure .gkm directory exists\n\t\tawait mkdir(dir, { recursive: true });\n\n\t\t// Update last deployed timestamp\n\t\tstate.lastDeployedAt = new Date().toISOString();\n\n\t\tawait writeFile(filePath, JSON.stringify(state, null, 2));\n\t}\n}\n"],"mappings":";;;;;;;;AAeA,SAAS,iBAAiBA,eAAuBC,OAAuB;AACvE,QAAO,oBAAK,eAAe,SAAS,SAAS,MAAM,OAAO;AAC1D;;;;;;AAOD,IAAa,qBAAb,MAAyD;CACxD,YAA6BD,eAAuB;EAAvB;CAAyB;CAEtD,MAAM,KAAKC,OAAkD;EAC5D,MAAM,WAAW,iBAAiB,KAAK,eAAe,MAAM;AAE5D,MAAI;GACH,MAAM,UAAU,MAAM,+BAAS,UAAU,QAAQ;AACjD,UAAO,KAAK,MAAM,QAAQ;EAC1B,SAAQ,OAAO;AAEf,OAAK,MAAgC,SAAS,SAC7C,QAAO;AAGR,WAAQ,MAAM,wCAAwC,MAAM,EAAE;AAC9D,UAAO;EACP;CACD;CAED,MAAM,MAAMA,OAAeC,OAAyC;EACnE,MAAM,WAAW,iBAAiB,KAAK,eAAe,MAAM;EAC5D,MAAM,MAAM,oBAAK,KAAK,eAAe,OAAO;AAG5C,QAAM,4BAAM,KAAK,EAAE,WAAW,KAAM,EAAC;AAGrC,QAAM,iBAAiB,qBAAI,QAAO,aAAa;AAE/C,QAAM,gCAAU,UAAU,KAAK,UAAU,OAAO,MAAM,EAAE,CAAC;CACzD;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Route53Provider-owQQ4pn6.cjs","names":["options: Route53ProviderOptions","Route53Client","domain: string","ListHostedZonesByNameCommand","type: string","validTypes: DnsRecordType[]","recordName: string","records: DnsRecord[]","nextRecordName: string | undefined","nextRecordType: RRType | undefined","ListResourceRecordSetsCommand","records: UpsertDnsRecord[]","results: UpsertResult[]","ChangeResourceRecordSetsCommand","records: DeleteDnsRecord[]","results: DeleteResult[]"],"sources":["../src/deploy/dns/Route53Provider.ts"],"sourcesContent":["/**\n * Route53 DNS Provider\n *\n * Implements DnsProvider interface using AWS Route53.\n */\n\nimport {\n\tChangeResourceRecordSetsCommand,\n\tListHostedZonesByNameCommand,\n\tListResourceRecordSetsCommand,\n\tRoute53Client,\n\ttype RRType,\n} from '@aws-sdk/client-route-53';\nimport { fromIni } from '@aws-sdk/credential-providers';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tDnsRecordType,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\n\nexport interface Route53ProviderOptions {\n\t/** AWS region (optional - uses AWS_REGION env var if not provided) */\n\tregion?: string;\n\t/** AWS profile name (optional - uses default credential chain if not provided) */\n\tprofile?: string;\n\t/** Hosted zone ID (optional - auto-detected from domain if not provided) */\n\thostedZoneId?: string;\n\t/** Custom endpoint for testing with localstack */\n\tendpoint?: string;\n}\n\n/**\n * Route53 DNS provider implementation.\n *\n * Uses AWS default credential chain for authentication.\n * Region can be specified or will use AWS_REGION/AWS_DEFAULT_REGION env vars.\n * Profile can be specified to use a named profile from ~/.aws/credentials.\n */\nexport class Route53Provider implements DnsProvider {\n\treadonly name = 'route53';\n\tprivate client: Route53Client;\n\tprivate hostedZoneId?: string;\n\tprivate hostedZoneCache: Map<string, string> = new Map();\n\n\tconstructor(options: Route53ProviderOptions = {}) {\n\t\t// Route53 is a global service, default to us-east-1 if no region specified\n\t\tconst region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';\n\n\t\tthis.client = new Route53Client({\n\t\t\tregion,\n\t\t\t...(options.endpoint && { endpoint: options.endpoint }),\n\t\t\t...(options.profile && {\n\t\t\t\tcredentials: fromIni({ profile: options.profile }),\n\t\t\t}),\n\t\t});\n\t\tthis.hostedZoneId = options.hostedZoneId;\n\t}\n\n\t/**\n\t * Get the hosted zone ID for a domain.\n\t * Uses cache to avoid repeated API calls.\n\t */\n\tprivate async getHostedZoneId(domain: string): Promise<string> {\n\t\t// Use configured zone ID if provided\n\t\tif (this.hostedZoneId) {\n\t\t\treturn this.hostedZoneId;\n\t\t}\n\n\t\t// Check cache\n\t\tif (this.hostedZoneCache.has(domain)) {\n\t\t\treturn this.hostedZoneCache.get(domain)!;\n\t\t}\n\n\t\t// Auto-detect from domain\n\t\tconst command = new ListHostedZonesByNameCommand({\n\t\t\tDNSName: domain,\n\t\t\tMaxItems: 1,\n\t\t});\n\n\t\tconst response = await this.client.send(command);\n\t\tconst zones = response.HostedZones ?? [];\n\n\t\t// Find exact match (domain with trailing dot)\n\t\tconst normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;\n\t\tconst zone = zones.find((z) => z.Name === normalizedDomain);\n\n\t\tif (!zone?.Id) {\n\t\t\tthrow new Error(\n\t\t\t\t`No hosted zone found for domain: ${domain}. Create one in Route53 or provide hostedZoneId in config.`,\n\t\t\t);\n\t\t}\n\n\t\t// Zone ID comes as \"/hostedzone/Z1234567890\" - extract just the ID\n\t\tconst zoneId = zone.Id.replace('/hostedzone/', '');\n\t\tthis.hostedZoneCache.set(domain, zoneId);\n\t\treturn zoneId;\n\t}\n\n\t/**\n\t * Convert Route53 record type to our DnsRecordType.\n\t * Excludes NS and SOA which are auto-managed by Route53 for the zone.\n\t */\n\tprivate toRecordType(type: string): DnsRecordType | null {\n\t\t// Exclude NS and SOA which are auto-managed zone records\n\t\tconst managedTypes = ['NS', 'SOA'];\n\t\tif (managedTypes.includes(type)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst validTypes: DnsRecordType[] = [\n\t\t\t'A',\n\t\t\t'AAAA',\n\t\t\t'CNAME',\n\t\t\t'MX',\n\t\t\t'TXT',\n\t\t\t'SRV',\n\t\t\t'CAA',\n\t\t];\n\t\treturn validTypes.includes(type as DnsRecordType)\n\t\t\t? (type as DnsRecordType)\n\t\t\t: null;\n\t}\n\n\t/**\n\t * Extract subdomain from full record name relative to domain.\n\t */\n\tprivate extractSubdomain(recordName: string, domain: string): string {\n\t\tconst normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;\n\t\tconst normalizedName = recordName.endsWith('.')\n\t\t\t? recordName\n\t\t\t: `${recordName}.`;\n\n\t\tif (normalizedName === normalizedDomain) {\n\t\t\treturn '@';\n\t\t}\n\n\t\t// Remove the domain suffix\n\t\tconst subdomain = normalizedName.replace(`.${normalizedDomain}`, '');\n\t\treturn subdomain.replace(/\\.$/, ''); // Remove trailing dot if any\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst records: DnsRecord[] = [];\n\n\t\tlet nextRecordName: string | undefined;\n\t\tlet nextRecordType: RRType | undefined;\n\n\t\t// Paginate through all records\n\t\tdo {\n\t\t\tconst command = new ListResourceRecordSetsCommand({\n\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\tStartRecordName: nextRecordName,\n\t\t\t\tStartRecordType: nextRecordType,\n\t\t\t\tMaxItems: 100,\n\t\t\t});\n\n\t\t\tconst response = await this.client.send(command);\n\n\t\t\tfor (const recordSet of response.ResourceRecordSets ?? []) {\n\t\t\t\tconst type = this.toRecordType(recordSet.Type ?? '');\n\t\t\t\tif (!type || !recordSet.Name) continue;\n\n\t\t\t\tconst values = (recordSet.ResourceRecords ?? [])\n\t\t\t\t\t.map((r) => r.Value)\n\t\t\t\t\t.filter((v): v is string => !!v);\n\n\t\t\t\trecords.push({\n\t\t\t\t\tname: this.extractSubdomain(recordSet.Name, domain),\n\t\t\t\t\ttype,\n\t\t\t\t\tttl: recordSet.TTL ?? 300,\n\t\t\t\t\tvalues,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (response.IsTruncated) {\n\t\t\t\tnextRecordName = response.NextRecordName;\n\t\t\t\tnextRecordType = response.NextRecordType;\n\t\t\t} else {\n\t\t\t\tnextRecordName = undefined;\n\t\t\t}\n\t\t} while (nextRecordName);\n\n\t\treturn records;\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to determine if creating or updating\n\t\tconst existingRecords = await this.getRecords(domain);\n\n\t\t// Process records in batches (Route53 allows max 1000 changes per request)\n\t\tconst batchSize = 100;\n\t\tfor (let i = 0; i < records.length; i += batchSize) {\n\t\t\tconst batch = records.slice(i, i + batchSize);\n\t\t\tconst changes = [];\n\n\t\t\tfor (const record of batch) {\n\t\t\t\tconst existing = existingRecords.find(\n\t\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t\t);\n\n\t\t\t\tconst existingValue = existing?.values?.[0];\n\n\t\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tcreated: false,\n\t\t\t\t\t\tunchanged: true,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Build full record name\n\t\t\t\tconst recordName =\n\t\t\t\t\trecord.name === '@' ? domain : `${record.name}.${domain}`;\n\n\t\t\t\tchanges.push({\n\t\t\t\t\tAction: 'UPSERT' as const,\n\t\t\t\t\tResourceRecordSet: {\n\t\t\t\t\t\tName: recordName,\n\t\t\t\t\t\tType: record.type,\n\t\t\t\t\t\tTTL: record.ttl,\n\t\t\t\t\t\tResourceRecords: [{ Value: record.value }],\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: !existing,\n\t\t\t\t\tunchanged: false,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Execute batch if there are changes\n\t\t\tif (changes.length > 0) {\n\t\t\t\tconst command = new ChangeResourceRecordSetsCommand({\n\t\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\t\tChangeBatch: {\n\t\t\t\t\t\tComment: 'Upsert by gkm deploy',\n\t\t\t\t\t\tChanges: changes,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tawait this.client.send(command);\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to find the ones to delete\n\t\tconst existingRecords = await this.getRecords(domain);\n\n\t\t// Process records in batches (Route53 allows max 1000 changes per request)\n\t\tconst batchSize = 100;\n\t\tfor (let i = 0; i < records.length; i += batchSize) {\n\t\t\tconst batch = records.slice(i, i + batchSize);\n\t\t\tconst changes = [];\n\n\t\t\tfor (const record of batch) {\n\t\t\t\tconst existing = existingRecords.find(\n\t\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t\t);\n\n\t\t\t\tif (!existing) {\n\t\t\t\t\t// Record doesn't exist - already deleted\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: true,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Build full record name\n\t\t\t\tconst recordName =\n\t\t\t\t\trecord.name === '@' ? domain : `${record.name}.${domain}`;\n\n\t\t\t\tchanges.push({\n\t\t\t\t\tAction: 'DELETE' as const,\n\t\t\t\t\tResourceRecordSet: {\n\t\t\t\t\t\tName: recordName,\n\t\t\t\t\t\tType: record.type,\n\t\t\t\t\t\tTTL: existing.ttl,\n\t\t\t\t\t\tResourceRecords: existing.values.map((v) => ({ Value: v })),\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: true,\n\t\t\t\t\tnotFound: false,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Execute batch if there are changes\n\t\t\tif (changes.length > 0) {\n\t\t\t\ttry {\n\t\t\t\t\tconst command = new ChangeResourceRecordSetsCommand({\n\t\t\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\t\t\tChangeBatch: {\n\t\t\t\t\t\t\tComment: 'Delete by gkm undeploy',\n\t\t\t\t\t\t\tChanges: changes,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tawait this.client.send(command);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Mark all records in this batch as failed\n\t\t\t\t\tfor (const result of results.slice(-changes.length)) {\n\t\t\t\t\t\tif (result.deleted) {\n\t\t\t\t\t\t\tresult.deleted = false;\n\t\t\t\t\t\t\tresult.error = String(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Route53Provider-DwgXiLxt.cjs","names":["options: Route53ProviderOptions","Route53Client","domain: string","ListHostedZonesByNameCommand","type: string","validTypes: DnsRecordType[]","recordName: string","records: DnsRecord[]","nextRecordName: string | undefined","nextRecordType: RRType | undefined","ListResourceRecordSetsCommand","records: UpsertDnsRecord[]","results: UpsertResult[]","ChangeResourceRecordSetsCommand","records: DeleteDnsRecord[]","results: DeleteResult[]"],"sources":["../src/deploy/dns/Route53Provider.ts"],"sourcesContent":["/**\n * Route53 DNS Provider\n *\n * Implements DnsProvider interface using AWS Route53.\n */\n\nimport {\n\tChangeResourceRecordSetsCommand,\n\tListHostedZonesByNameCommand,\n\tListResourceRecordSetsCommand,\n\tRoute53Client,\n\ttype RRType,\n} from '@aws-sdk/client-route-53';\nimport { fromIni } from '@aws-sdk/credential-providers';\nimport type {\n\tDeleteDnsRecord,\n\tDeleteResult,\n\tDnsProvider,\n\tDnsRecord,\n\tDnsRecordType,\n\tUpsertDnsRecord,\n\tUpsertResult,\n} from './DnsProvider';\n\nexport interface Route53ProviderOptions {\n\t/** AWS region (optional - uses AWS_REGION env var if not provided) */\n\tregion?: string;\n\t/** AWS profile name (optional - uses default credential chain if not provided) */\n\tprofile?: string;\n\t/** Hosted zone ID (optional - auto-detected from domain if not provided) */\n\thostedZoneId?: string;\n\t/** Custom endpoint for testing with localstack */\n\tendpoint?: string;\n}\n\n/**\n * Route53 DNS provider implementation.\n *\n * Uses AWS default credential chain for authentication.\n * Region can be specified or will use AWS_REGION/AWS_DEFAULT_REGION env vars.\n * Profile can be specified to use a named profile from ~/.aws/credentials.\n */\nexport class Route53Provider implements DnsProvider {\n\treadonly name = 'route53';\n\tprivate client: Route53Client;\n\tprivate hostedZoneId?: string;\n\tprivate hostedZoneCache: Map<string, string> = new Map();\n\n\tconstructor(options: Route53ProviderOptions = {}) {\n\t\t// Route53 is a global service, default to us-east-1 if no region specified\n\t\tconst region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';\n\n\t\tthis.client = new Route53Client({\n\t\t\tregion,\n\t\t\t...(options.endpoint && { endpoint: options.endpoint }),\n\t\t\t...(options.profile && {\n\t\t\t\tcredentials: fromIni({ profile: options.profile }),\n\t\t\t}),\n\t\t});\n\t\tthis.hostedZoneId = options.hostedZoneId;\n\t}\n\n\t/**\n\t * Get the hosted zone ID for a domain.\n\t * Uses cache to avoid repeated API calls.\n\t */\n\tprivate async getHostedZoneId(domain: string): Promise<string> {\n\t\t// Use configured zone ID if provided\n\t\tif (this.hostedZoneId) {\n\t\t\treturn this.hostedZoneId;\n\t\t}\n\n\t\t// Check cache\n\t\tif (this.hostedZoneCache.has(domain)) {\n\t\t\treturn this.hostedZoneCache.get(domain)!;\n\t\t}\n\n\t\t// Auto-detect from domain\n\t\tconst command = new ListHostedZonesByNameCommand({\n\t\t\tDNSName: domain,\n\t\t\tMaxItems: 1,\n\t\t});\n\n\t\tconst response = await this.client.send(command);\n\t\tconst zones = response.HostedZones ?? [];\n\n\t\t// Find exact match (domain with trailing dot)\n\t\tconst normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;\n\t\tconst zone = zones.find((z) => z.Name === normalizedDomain);\n\n\t\tif (!zone?.Id) {\n\t\t\tthrow new Error(\n\t\t\t\t`No hosted zone found for domain: ${domain}. Create one in Route53 or provide hostedZoneId in config.`,\n\t\t\t);\n\t\t}\n\n\t\t// Zone ID comes as \"/hostedzone/Z1234567890\" - extract just the ID\n\t\tconst zoneId = zone.Id.replace('/hostedzone/', '');\n\t\tthis.hostedZoneCache.set(domain, zoneId);\n\t\treturn zoneId;\n\t}\n\n\t/**\n\t * Convert Route53 record type to our DnsRecordType.\n\t * Excludes NS and SOA which are auto-managed by Route53 for the zone.\n\t */\n\tprivate toRecordType(type: string): DnsRecordType | null {\n\t\t// Exclude NS and SOA which are auto-managed zone records\n\t\tconst managedTypes = ['NS', 'SOA'];\n\t\tif (managedTypes.includes(type)) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst validTypes: DnsRecordType[] = [\n\t\t\t'A',\n\t\t\t'AAAA',\n\t\t\t'CNAME',\n\t\t\t'MX',\n\t\t\t'TXT',\n\t\t\t'SRV',\n\t\t\t'CAA',\n\t\t];\n\t\treturn validTypes.includes(type as DnsRecordType)\n\t\t\t? (type as DnsRecordType)\n\t\t\t: null;\n\t}\n\n\t/**\n\t * Extract subdomain from full record name relative to domain.\n\t */\n\tprivate extractSubdomain(recordName: string, domain: string): string {\n\t\tconst normalizedDomain = domain.endsWith('.') ? domain : `${domain}.`;\n\t\tconst normalizedName = recordName.endsWith('.')\n\t\t\t? recordName\n\t\t\t: `${recordName}.`;\n\n\t\tif (normalizedName === normalizedDomain) {\n\t\t\treturn '@';\n\t\t}\n\n\t\t// Remove the domain suffix\n\t\tconst subdomain = normalizedName.replace(`.${normalizedDomain}`, '');\n\t\treturn subdomain.replace(/\\.$/, ''); // Remove trailing dot if any\n\t}\n\n\tasync getRecords(domain: string): Promise<DnsRecord[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst records: DnsRecord[] = [];\n\n\t\tlet nextRecordName: string | undefined;\n\t\tlet nextRecordType: RRType | undefined;\n\n\t\t// Paginate through all records\n\t\tdo {\n\t\t\tconst command = new ListResourceRecordSetsCommand({\n\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\tStartRecordName: nextRecordName,\n\t\t\t\tStartRecordType: nextRecordType,\n\t\t\t\tMaxItems: 100,\n\t\t\t});\n\n\t\t\tconst response = await this.client.send(command);\n\n\t\t\tfor (const recordSet of response.ResourceRecordSets ?? []) {\n\t\t\t\tconst type = this.toRecordType(recordSet.Type ?? '');\n\t\t\t\tif (!type || !recordSet.Name) continue;\n\n\t\t\t\tconst values = (recordSet.ResourceRecords ?? [])\n\t\t\t\t\t.map((r) => r.Value)\n\t\t\t\t\t.filter((v): v is string => !!v);\n\n\t\t\t\trecords.push({\n\t\t\t\t\tname: this.extractSubdomain(recordSet.Name, domain),\n\t\t\t\t\ttype,\n\t\t\t\t\tttl: recordSet.TTL ?? 300,\n\t\t\t\t\tvalues,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (response.IsTruncated) {\n\t\t\t\tnextRecordName = response.NextRecordName;\n\t\t\t\tnextRecordType = response.NextRecordType;\n\t\t\t} else {\n\t\t\t\tnextRecordName = undefined;\n\t\t\t}\n\t\t} while (nextRecordName);\n\n\t\treturn records;\n\t}\n\n\tasync upsertRecords(\n\t\tdomain: string,\n\t\trecords: UpsertDnsRecord[],\n\t): Promise<UpsertResult[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst results: UpsertResult[] = [];\n\n\t\t// Get existing records to determine if creating or updating\n\t\tconst existingRecords = await this.getRecords(domain);\n\n\t\t// Process records in batches (Route53 allows max 1000 changes per request)\n\t\tconst batchSize = 100;\n\t\tfor (let i = 0; i < records.length; i += batchSize) {\n\t\t\tconst batch = records.slice(i, i + batchSize);\n\t\t\tconst changes = [];\n\n\t\t\tfor (const record of batch) {\n\t\t\t\tconst existing = existingRecords.find(\n\t\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t\t);\n\n\t\t\t\tconst existingValue = existing?.values?.[0];\n\n\t\t\t\tif (existing && existingValue === record.value) {\n\t\t\t\t\t// Record exists with same value - unchanged\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tcreated: false,\n\t\t\t\t\t\tunchanged: true,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Build full record name\n\t\t\t\tconst recordName =\n\t\t\t\t\trecord.name === '@' ? domain : `${record.name}.${domain}`;\n\n\t\t\t\tchanges.push({\n\t\t\t\t\tAction: 'UPSERT' as const,\n\t\t\t\t\tResourceRecordSet: {\n\t\t\t\t\t\tName: recordName,\n\t\t\t\t\t\tType: record.type,\n\t\t\t\t\t\tTTL: record.ttl,\n\t\t\t\t\t\tResourceRecords: [{ Value: record.value }],\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tcreated: !existing,\n\t\t\t\t\tunchanged: false,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Execute batch if there are changes\n\t\t\tif (changes.length > 0) {\n\t\t\t\tconst command = new ChangeResourceRecordSetsCommand({\n\t\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\t\tChangeBatch: {\n\t\t\t\t\t\tComment: 'Upsert by gkm deploy',\n\t\t\t\t\t\tChanges: changes,\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tawait this.client.send(command);\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tasync deleteRecords(\n\t\tdomain: string,\n\t\trecords: DeleteDnsRecord[],\n\t): Promise<DeleteResult[]> {\n\t\tconst zoneId = await this.getHostedZoneId(domain);\n\t\tconst results: DeleteResult[] = [];\n\n\t\t// Get existing records to find the ones to delete\n\t\tconst existingRecords = await this.getRecords(domain);\n\n\t\t// Process records in batches (Route53 allows max 1000 changes per request)\n\t\tconst batchSize = 100;\n\t\tfor (let i = 0; i < records.length; i += batchSize) {\n\t\t\tconst batch = records.slice(i, i + batchSize);\n\t\t\tconst changes = [];\n\n\t\t\tfor (const record of batch) {\n\t\t\t\tconst existing = existingRecords.find(\n\t\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t\t);\n\n\t\t\t\tif (!existing) {\n\t\t\t\t\t// Record doesn't exist - already deleted\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tdeleted: false,\n\t\t\t\t\t\tnotFound: true,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Build full record name\n\t\t\t\tconst recordName =\n\t\t\t\t\trecord.name === '@' ? domain : `${record.name}.${domain}`;\n\n\t\t\t\tchanges.push({\n\t\t\t\t\tAction: 'DELETE' as const,\n\t\t\t\t\tResourceRecordSet: {\n\t\t\t\t\t\tName: recordName,\n\t\t\t\t\t\tType: record.type,\n\t\t\t\t\t\tTTL: existing.ttl,\n\t\t\t\t\t\tResourceRecords: existing.values.map((v) => ({ Value: v })),\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\tresults.push({\n\t\t\t\t\trecord,\n\t\t\t\t\tdeleted: true,\n\t\t\t\t\tnotFound: false,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Execute batch if there are changes\n\t\t\tif (changes.length > 0) {\n\t\t\t\ttry {\n\t\t\t\t\tconst command = new ChangeResourceRecordSetsCommand({\n\t\t\t\t\t\tHostedZoneId: zoneId,\n\t\t\t\t\t\tChangeBatch: {\n\t\t\t\t\t\t\tComment: 'Delete by gkm undeploy',\n\t\t\t\t\t\t\tChanges: changes,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\n\t\t\t\t\tawait this.client.send(command);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Mark all records in this batch as failed\n\t\t\t\t\tfor (const result of results.slice(-changes.length)) {\n\t\t\t\t\t\tif (result.deleted) {\n\t\t\t\t\t\t\tresult.deleted = false;\n\t\t\t\t\t\t\tresult.error = String(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n}\n"],"mappings":"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"}
|