openship 0.6.0 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/{bare-FSUAM5V5.js → bare-XQIPDO4R.js} +10 -9
- package/dist/{chunk-PSZSKEPA.js → chunk-3ZAGC5NT.js} +64 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
- package/dist/chunk-6HOA5JT7.js +124 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
- package/dist/{chunk-GUQ3QCJ7.js → chunk-7334NSJL.js} +399 -369
- package/dist/chunk-7S4USQYF.js +1753 -0
- package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/chunk-EZPPTWJQ.js +292 -0
- package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
- package/dist/chunk-HPXP6LXZ.js +428 -0
- package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
- package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
- package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
- package/dist/{chunk-OGY63DGD.js → chunk-PZRRL5PC.js} +71 -28
- package/dist/{chunk-VE2YOK4I.js → chunk-QLX7NBEA.js} +1218 -588
- package/dist/chunk-RFWJE7ZN.js +68 -0
- package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
- package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
- package/dist/{chunk-B3CE2OMR.js → chunk-VHMOQL5Y.js} +276 -50
- package/dist/{chunk-QEH4QUBX.js → chunk-VLQ3UIVO.js} +393 -53
- package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
- package/dist/chunk-WFRXYWK2.js +444 -0
- package/dist/chunk-XRNRT37W.js +316 -0
- package/dist/{chunk-EP2TNALB.js → chunk-XVOBL7BC.js} +31 -8
- package/dist/cloud-3DODOWTR.js +36 -0
- package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
- package/dist/{edge-import-CQWHN2Q3.js → edge-import-MCJNVTZ2.js} +26 -23
- package/dist/edge-preflight-ES6FH4JK.js +42 -0
- package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-YPITNH45.js} +9 -8
- package/dist/{executor-JELARVKC.js → executor-DZGX55K2.js} +12 -5
- package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
- package/dist/index.js +3668 -4063
- package/dist/{nginx-AXGNWRKZ.js → nginx-NSAHWGJV.js} +11 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-DR4D3EZ6.js} +6 -2
- package/dist/privilege-YTVI4SKF.js +19 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/index.js +21501 -13137
- package/dist/server/lua/geo_country.lua +4 -3
- package/dist/server/lua/pipe_stream.lua +52 -39
- package/dist/server/lua/site_logger.lua +22 -24
- package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
- package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
- package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
- package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
- package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
- package/dist/server/migrations/0103_dns_credential.sql +41 -0
- package/dist/server/migrations/0104_instance_host_control.sql +15 -0
- package/dist/server/migrations/meta/_journal.json +49 -0
- package/dist/{setup-FA4PW4NU.js → setup-WUUZURHS.js} +10 -9
- package/dist/up-XEO3SR6Z.js +58 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-E5M6VMCG.js +0 -31
- package/dist/server/lua/pipe_log.lua +0 -77
- /package/dist/{dist-es-JGBPABJI.js → dist-es-ZP5QDSJ3.js} +0 -0
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
-- image from apps/api/assets/geoip, so no runtime download)
|
|
15
15
|
--
|
|
16
16
|
-- Falls back gracefully - if the library or database is missing,
|
|
17
|
-
-- get_country_code() returns nil.
|
|
18
|
-
--
|
|
19
|
-
-- the
|
|
17
|
+
-- get_country_code() returns nil. site_logger.lua wraps this module in
|
|
18
|
+
-- pcall(require, ...) so a missing DB never crashes the request pipeline
|
|
19
|
+
-- (the country it resolves flows into the ring buffer, and from there to
|
|
20
|
+
-- both /logs/recent and the live SSE stream). mgmt_api `GET /status` reports whether geo actually
|
|
20
21
|
-- resolved, so a silently-dark edge is visible instead of just yielding an
|
|
21
22
|
-- empty country map.
|
|
22
23
|
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
-- content_by_lua: SSE endpoint for real-time request log streaming.
|
|
3
3
|
-- GET /logs/stream?domain=example.com
|
|
4
4
|
-- Internal only - 127.0.0.1:9145
|
|
5
|
+
--
|
|
6
|
+
-- Reads the SAME ring buffer site_logger.lua writes (rlog:{domain}:seq +
|
|
7
|
+
-- rlog:{domain}:{slot}), by a PER-CONNECTION cursor. Nothing is consumed: N
|
|
8
|
+
-- concurrent watchers each walk the ring independently and none steals another's
|
|
9
|
+
-- frames. This is what replaced the old single-subscriber log_pipe:q queue —
|
|
10
|
+
-- a containerized edge tails this over `docker exec curl`, and a curl that
|
|
11
|
+
-- outlives its client (the daemon buffers its stdout, so no EPIPE) can no longer
|
|
12
|
+
-- drain the queue out from under the next reader.
|
|
5
13
|
|
|
6
14
|
local sh = ngx.shared.request_data
|
|
7
15
|
if not sh then
|
|
@@ -20,12 +28,10 @@ end
|
|
|
20
28
|
domain = domain:lower()
|
|
21
29
|
if domain:sub(1, 4) == "www." then domain = domain:sub(5) end
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
local
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
sh:delete(QUEUE_KEY)
|
|
28
|
-
sh:set(SUB_KEY, true, 30)
|
|
31
|
+
-- MUST match site_logger.lua's RING — same slot arithmetic on both ends.
|
|
32
|
+
local RING = 1000
|
|
33
|
+
local SEQ_KEY = "rlog:" .. domain .. ":seq"
|
|
34
|
+
local slot_key = function(seq) return "rlog:" .. domain .. ":" .. (seq % RING) end
|
|
29
35
|
|
|
30
36
|
ngx.header["Content-Type"] = "text/event-stream"
|
|
31
37
|
ngx.header["Cache-Control"] = "no-cache, no-store"
|
|
@@ -36,55 +42,62 @@ ngx.header["X-Accel-Buffering"] = "no"
|
|
|
36
42
|
-- Without this, ngx.flush on an empty buffer sends chunked-EOF (0\r\n\r\n)
|
|
37
43
|
-- and terminates the response immediately.
|
|
38
44
|
ngx.print(": connected\n\n")
|
|
39
|
-
if not ngx.flush(true) then
|
|
40
|
-
sh:delete(SUB_KEY)
|
|
41
|
-
return
|
|
42
|
-
end
|
|
45
|
+
if not ngx.flush(true) then return end
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
-- Start at the current head: stream only requests that arrive AFTER connect.
|
|
48
|
+
-- History up to this point is served separately by /logs/recent, and the client
|
|
49
|
+
-- dedups the two by the deterministic `{host}:{seq}` id, so a row seen by both
|
|
50
|
+
-- collapses to one.
|
|
51
|
+
local cursor = tonumber(sh:get(SEQ_KEY)) or 0
|
|
52
|
+
local started = ngx.now()
|
|
53
|
+
local last_hb = started
|
|
47
54
|
|
|
48
55
|
while true do
|
|
49
|
-
-- Max 1 hour per connection
|
|
50
|
-
if ngx.now() - started > 3600 then
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
-- Max 1 hour per connection.
|
|
57
|
+
if ngx.now() - started > 3600 then return end
|
|
58
|
+
|
|
59
|
+
local head = tonumber(sh:get(SEQ_KEY)) or 0
|
|
60
|
+
|
|
61
|
+
-- If we've fallen more than a full ring behind, the oldest unread slots have
|
|
62
|
+
-- already been overwritten. Jump to the oldest slot still holding its own
|
|
63
|
+
-- entry (head-RING maps to head's slot, so head-RING+1 is the oldest live one)
|
|
64
|
+
-- and skip the lost range — same bound the old 2000-entry queue had.
|
|
65
|
+
if cursor < head - RING then cursor = head - RING end
|
|
54
66
|
|
|
55
|
-
-- Drain up to 100 queued entries per cycle
|
|
56
67
|
local sent = 0
|
|
57
|
-
while sent < 100 do
|
|
58
|
-
local
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
while cursor < head and sent < 100 do
|
|
69
|
+
local next_seq = cursor + 1
|
|
70
|
+
local entry = sh:get(slot_key(next_seq))
|
|
71
|
+
if entry then
|
|
72
|
+
ngx.print("event: request\ndata: ", entry, "\n\n")
|
|
73
|
+
sent = sent + 1
|
|
74
|
+
cursor = next_seq
|
|
75
|
+
elseif next_seq < head then
|
|
76
|
+
-- A later seq already exists, so this can't be the incr(seq)→set(slot)
|
|
77
|
+
-- write window: the slot is genuinely lost (expired or wrapped). Skip it.
|
|
78
|
+
cursor = next_seq
|
|
79
|
+
else
|
|
80
|
+
-- next_seq == head: the newest entry's slot may not be written yet
|
|
81
|
+
-- (the microsecond window between incr(seq) and set(slot)). Retry it
|
|
82
|
+
-- next cycle without advancing.
|
|
83
|
+
break
|
|
84
|
+
end
|
|
62
85
|
end
|
|
63
86
|
|
|
64
87
|
if sent > 0 then
|
|
65
|
-
if not ngx.flush(true) then
|
|
66
|
-
sh:delete(SUB_KEY)
|
|
67
|
-
return
|
|
68
|
-
end
|
|
88
|
+
if not ngx.flush(true) then return end
|
|
69
89
|
end
|
|
70
90
|
|
|
71
91
|
local now = ngx.now()
|
|
72
92
|
|
|
73
|
-
-- Heartbeat every 15s
|
|
93
|
+
-- Heartbeat every 15s. Its flush is also how a dead client is detected: once
|
|
94
|
+
-- the reader (a `docker exec curl`) goes away and its socket closes, this
|
|
95
|
+
-- flush fails and the connection tears down here.
|
|
74
96
|
if now - last_hb > 15 then
|
|
75
97
|
ngx.print(": ping\n\n")
|
|
76
|
-
if not ngx.flush(true) then
|
|
77
|
-
sh:delete(SUB_KEY)
|
|
78
|
-
return
|
|
79
|
-
end
|
|
98
|
+
if not ngx.flush(true) then return end
|
|
80
99
|
last_hb = now
|
|
81
100
|
end
|
|
82
101
|
|
|
83
|
-
-- Refresh subscriber TTL every 10s
|
|
84
|
-
if now - last_ref > 10 then
|
|
85
|
-
sh:set(SUB_KEY, true, 30)
|
|
86
|
-
last_ref = now
|
|
87
|
-
end
|
|
88
|
-
|
|
89
102
|
ngx.sleep(0.05)
|
|
90
103
|
end
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
-- Shared dict key schema (request_data zone):
|
|
28
28
|
-- rlog:{domain}:seq monotonic write pointer
|
|
29
29
|
-- rlog:{domain}:{slot} JSON entry (ring buf) (TTL 1h)
|
|
30
|
-
--
|
|
31
|
-
--
|
|
30
|
+
--
|
|
31
|
+
-- The ring is ALSO the live source: pipe_stream.lua's SSE endpoint reads these
|
|
32
|
+
-- slots by a per-connection cursor, so there is no separate live queue or
|
|
33
|
+
-- subscriber flag — every watcher sees the full stream and steals from none.
|
|
32
34
|
--
|
|
33
35
|
-- NOTE on `:u` — it counts non-static REQUESTS, not people. It was surfaced as
|
|
34
36
|
-- "unique IPs" all the way to the dashboard, which it never was. Real distinct
|
|
@@ -43,10 +45,6 @@ if not geo_ok then
|
|
|
43
45
|
ngx.log(ngx.WARN, "[site_logger] geo_country module not available. GeoIP disabled.")
|
|
44
46
|
end
|
|
45
47
|
|
|
46
|
-
-- SAFE LOAD: Pipe module (per-worker, cached by require)
|
|
47
|
-
local pipe_ok, pipe_log = pcall(require, "openship.pipe_log")
|
|
48
|
-
if not pipe_ok then pipe_log = nil end
|
|
49
|
-
|
|
50
48
|
local analytics = ngx.shared.analytics
|
|
51
49
|
local request_data = ngx.shared.request_data
|
|
52
50
|
-- Per-host edge config, pushed by the API (see mgmt_api's /analytics/config).
|
|
@@ -194,11 +192,12 @@ local status = ngx.var.status or "0"
|
|
|
194
192
|
|
|
195
193
|
-- Client country, resolved ONCE per request.
|
|
196
194
|
--
|
|
197
|
-
-- Hoisted out of the geo block because
|
|
198
|
-
--
|
|
199
|
-
--
|
|
200
|
-
--
|
|
201
|
-
--
|
|
195
|
+
-- Hoisted out of the geo block because both the daily/per-minute rollups and the raw
|
|
196
|
+
-- ring buffer need it. The ring is the single source for the request log — /logs/recent
|
|
197
|
+
-- reads it directly and the live SSE stream (pipe_stream.lua) reads the same slots by
|
|
198
|
+
-- cursor — so resolving the country here means every row carries its flag no matter
|
|
199
|
+
-- which path delivered it. An earlier split (looked up in the rollup, again in a
|
|
200
|
+
-- separate live pipe) left the ring with no country at all.
|
|
202
201
|
--
|
|
203
202
|
-- pcall'd because a corrupt or partially-written mmdb makes the lookup raise, and an
|
|
204
203
|
-- error here would abort every counter below it (see the section header).
|
|
@@ -356,10 +355,20 @@ pcall(function()
|
|
|
356
355
|
bump_indexed(gpfx .. "s:" .. status, D48H, gpfx .. "sn", gpfx .. "si:", status)
|
|
357
356
|
end)
|
|
358
357
|
|
|
359
|
-
-- ── 4. Raw request ring buffer
|
|
358
|
+
-- ── 4. Raw request ring buffer (also the live SSE source) ────────────────────
|
|
359
|
+
--
|
|
360
|
+
-- `seq` is claimed BEFORE encoding so the stored JSON can carry a deterministic
|
|
361
|
+
-- id (`{host}:{seq}`). That id is the dedup key everywhere downstream: /logs/recent
|
|
362
|
+
-- returns these entries verbatim, and pipe_stream.lua streams the same slots live,
|
|
363
|
+
-- so the same request reaches the client with one stable id instead of a fresh
|
|
364
|
+
-- random one per source. `incr` with init 0 never returns nil here; guard anyway
|
|
365
|
+
-- so a shdict hiccup can't index a nil into the slot key.
|
|
360
366
|
|
|
361
367
|
pcall(function()
|
|
368
|
+
local seq = request_data:incr("rlog:" .. host .. ":seq", 1, 0)
|
|
369
|
+
if not seq then return end
|
|
362
370
|
local ok_j, j = pcall(cjson.encode, {
|
|
371
|
+
id = host .. ":" .. seq,
|
|
363
372
|
ip = ip,
|
|
364
373
|
-- nil is simply omitted by cjson, so a box with no GeoIP writes the same shape
|
|
365
374
|
-- minus this key rather than a null the reader has to special-case.
|
|
@@ -374,17 +383,6 @@ pcall(function()
|
|
|
374
383
|
rt = rt,
|
|
375
384
|
})
|
|
376
385
|
if ok_j and j then
|
|
377
|
-
|
|
378
|
-
local slot = seq % RING
|
|
379
|
-
request_data:set("rlog:" .. host .. ":" .. slot, j, D1H)
|
|
386
|
+
request_data:set("rlog:" .. host .. ":" .. (seq % RING), j, D1H)
|
|
380
387
|
end
|
|
381
388
|
end)
|
|
382
|
-
|
|
383
|
-
-- ── 5. Live-log pipe (only when a subscriber is watching) ───────────────────
|
|
384
|
-
-- Fire via timer to avoid blocking the log phase
|
|
385
|
-
|
|
386
|
-
if pipe_log and pipe_log.pipe_request_log
|
|
387
|
-
and request_data:get("log_pipe:sub:" .. host) then
|
|
388
|
-
ngx.timer.at(0, pipe_log.pipe_request_log,
|
|
389
|
-
host, ip, ts, ua, uri, req_len, bytes, rt, method, status, cc)
|
|
390
|
-
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Openship Mail: which product this instance presents itself as.
|
|
2
|
+
--
|
|
3
|
+
-- Nullable with NO default, deliberately. The instance_settings row exists on
|
|
4
|
+
-- every instance, so a NOT NULL DEFAULT 'platform' would write a concrete value
|
|
5
|
+
-- everywhere and permanently shadow the OPENSHIP_PRODUCT env var — the resolver
|
|
6
|
+
-- reads `settings.product_mode ?? env.OPENSHIP_PRODUCT`, so "unset" has to stay
|
|
7
|
+
-- representable. NULL means "no instance override".
|
|
8
|
+
--
|
|
9
|
+
-- Presentation scope only: this hides nav entries and rebrands the shell. It is
|
|
10
|
+
-- not an authorization boundary and no route reads it — webmail deploys through
|
|
11
|
+
-- the ordinary project pipeline, so the platform endpoints stay live in mail mode.
|
|
12
|
+
ALTER TABLE "instance_settings" ADD COLUMN IF NOT EXISTS "product_mode" text;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- The webmail project serving a mail server, as a stored FK.
|
|
2
|
+
--
|
|
3
|
+
-- Webmail is now an ordinary catalog app (packages/core/src/apps/catalog/webmail.json)
|
|
4
|
+
-- installed through the SAME generic installer as every other app, so its slug is
|
|
5
|
+
-- derived from the project NAME the operator typed. The link therefore cannot be a
|
|
6
|
+
-- naming convention any more: the old lookup parsed `webmail-<serverId>` back out of
|
|
7
|
+
-- the slug, which the generic installer never produces — and which, while it existed,
|
|
8
|
+
-- mislinked any project a user happened to name "Webmail Prod" (slug `webmail-prod`,
|
|
9
|
+
-- read back as mail server "prod").
|
|
10
|
+
--
|
|
11
|
+
-- Nullable: most servers have no webmail, and an operator may run one outside
|
|
12
|
+
-- openship. ON DELETE SET NULL so deleting the webmail project clears the link and
|
|
13
|
+
-- /emails falls back to the install CTA rather than pointing at a row that is gone.
|
|
14
|
+
--
|
|
15
|
+
-- No backfill. A pre-existing webmail project still carries the retired
|
|
16
|
+
-- `mail-webmail` template id and the `webmail-<serverId>` slug; the mail status
|
|
17
|
+
-- resolver keeps that legacy slug lookup as a fallback for exactly those rows, and
|
|
18
|
+
-- stamps this column the first time it resolves one.
|
|
19
|
+
ALTER TABLE "mail_servers" ADD COLUMN IF NOT EXISTS "webmail_project_id" text;--> statement-breakpoint
|
|
20
|
+
|
|
21
|
+
DO $$ BEGIN
|
|
22
|
+
ALTER TABLE "mail_servers" ADD CONSTRAINT "mail_servers_webmail_project_id_project_id_fk"
|
|
23
|
+
FOREIGN KEY ("webmail_project_id") REFERENCES "public"."project"("id") ON DELETE set null ON UPDATE no action;
|
|
24
|
+
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Pasted / uploaded SSH private-key material, stored encrypted at rest (enc1:).
|
|
2
|
+
--
|
|
3
|
+
-- Nullable with no default. The existing `ssh_key_path` points at a key file on
|
|
4
|
+
-- the API host — useless on a remote / VPS instance where the operator's key
|
|
5
|
+
-- lives on their own machine. This column holds the key bytes themselves so the
|
|
6
|
+
-- dashboard can paste or upload a key from any browser; buildSshConfig() prefers
|
|
7
|
+
-- it (decrypted) over reading `ssh_key_path` off disk.
|
|
8
|
+
--
|
|
9
|
+
-- Write-only: never serialized back to the client, and listed in
|
|
10
|
+
-- ENCRYPTED_COLUMNS so it is stripped from any cross-host dump/restore.
|
|
11
|
+
ALTER TABLE "servers" ADD COLUMN IF NOT EXISTS "ssh_private_key" text;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- The setup step an incomplete mail install is paused at, mirrored from the
|
|
2
|
+
-- on-host state file's `resumeStep` whenever the wizard halts (a failed step,
|
|
3
|
+
-- or a DNS/PTR hold). NULL once the install completes or before it ever halts.
|
|
4
|
+
--
|
|
5
|
+
-- Lets the /emails server list surface WHERE an incomplete install stopped
|
|
6
|
+
-- ("Stopped · step 6: Retrieve DKIM Keys") without the per-server SSH probe
|
|
7
|
+
-- this table exists to avoid — only the step id is stored; the human label is
|
|
8
|
+
-- derived from it against MAIL_SETUP_STEPS.
|
|
9
|
+
--
|
|
10
|
+
-- Nullable, no default, no backfill: a pre-existing incomplete install gets its
|
|
11
|
+
-- step stamped the next time the wizard halts (or cleared on completion).
|
|
12
|
+
ALTER TABLE "mail_servers" ADD COLUMN IF NOT EXISTS "resume_step" integer;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Deployment-class axes — deconflate the legacy `has_server` / `has_build`
|
|
2
|
+
-- booleans, which each mixed two independent concerns (issue #538). See
|
|
3
|
+
-- @repo/core deployment-class.ts.
|
|
4
|
+
--
|
|
5
|
+
-- All three are EXPLICIT OVERRIDES: NULL means "derive from the legacy flags +
|
|
6
|
+
-- framework + source", so every pre-existing project keeps classifying exactly
|
|
7
|
+
-- as before and no backfill is needed. `has_server` / `has_build` are kept and
|
|
8
|
+
-- written in sync, never dropped — rollback snapshots and every legacy reader
|
|
9
|
+
-- still depend on them.
|
|
10
|
+
--
|
|
11
|
+
-- source_kind text "git" | "image" | "upload" (git needs a clone)
|
|
12
|
+
-- build_kind text "dockerfile" | "buildpack" | "static" | "prebuilt"
|
|
13
|
+
-- workload_type text "web" | "worker" | "static" (worker = portless
|
|
14
|
+
-- long-running container: no port, no route)
|
|
15
|
+
ALTER TABLE "project" ADD COLUMN IF NOT EXISTS "source_kind" text;
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
ALTER TABLE "project" ADD COLUMN IF NOT EXISTS "build_kind" text;
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
ALTER TABLE "project" ADD COLUMN IF NOT EXISTS "workload_type" text;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- DNS provider credentials — the token that lets Openship write a domain's records.
|
|
2
|
+
--
|
|
3
|
+
-- Adding a custom domain has always ended the same way: Openship computes the exact
|
|
4
|
+
-- A/CNAME + `_openship-challenge` TXT records and then asks the operator to go paste
|
|
5
|
+
-- them somewhere else. Every minute between "added" and "pasted" is a domain that
|
|
6
|
+
-- doesn't resolve, can't pass ACME, and looks broken. With a scoped provider token we
|
|
7
|
+
-- write those records ourselves and the domain verifies on its own.
|
|
8
|
+
--
|
|
9
|
+
-- The token is the sensitive part: Zone:Read + DNS:Edit can rewrite every record in
|
|
10
|
+
-- every zone it can see, so it is stored ONLY as an `enc1:` AES-256-GCM envelope
|
|
11
|
+
-- (`encryptSecretField`) and is never returned by the API — not in full, not as a
|
|
12
|
+
-- prefix. `status` exists so a token that gets revoked upstream becomes visible in the
|
|
13
|
+
-- UI instead of silently turning auto-provisioning off forever.
|
|
14
|
+
CREATE TABLE IF NOT EXISTS "dns_credential" (
|
|
15
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
16
|
+
"organization_id" text NOT NULL REFERENCES "organization"("id") ON DELETE CASCADE,
|
|
17
|
+
-- Provider name ("cloudflare"). Text, not an enum: adding a provider should be a
|
|
18
|
+
-- registry entry, not a migration.
|
|
19
|
+
"provider" text NOT NULL,
|
|
20
|
+
-- Operator-facing label, e.g. "Cloudflare production".
|
|
21
|
+
"name" text NOT NULL,
|
|
22
|
+
"api_token_enc" text NOT NULL,
|
|
23
|
+
-- active | invalid. Flipped to 'invalid' by the provisioning path when the provider
|
|
24
|
+
-- answers 401/403, which is the only moment we learn a stored token stopped working.
|
|
25
|
+
"status" text NOT NULL DEFAULT 'active',
|
|
26
|
+
"last_verified_at" timestamp,
|
|
27
|
+
"created_at" timestamp NOT NULL DEFAULT now(),
|
|
28
|
+
"updated_at" timestamp NOT NULL DEFAULT now()
|
|
29
|
+
);
|
|
30
|
+
--> statement-breakpoint
|
|
31
|
+
-- Zone resolution runs on EVERY domain add and remove and asks one question: "this
|
|
32
|
+
-- org's active credentials". Leading org column means the plain org-wide listing rides
|
|
33
|
+
-- the same index as a prefix, so this is the only one the table needs.
|
|
34
|
+
CREATE INDEX IF NOT EXISTS "idx_dns_credential_org_status"
|
|
35
|
+
ON "dns_credential" ("organization_id", "status");
|
|
36
|
+
--> statement-breakpoint
|
|
37
|
+
-- One label per provider per org. A second "Cloudflare production" is a double-submit,
|
|
38
|
+
-- and two credentials that resolve the same zone makes "which token wrote this record"
|
|
39
|
+
-- unanswerable at cleanup time.
|
|
40
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "uq_dns_credential_org_provider_name"
|
|
41
|
+
ON "dns_credential" ("organization_id", "provider", "name");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Runtime host-control toggle (#527): may OpenShip deploy to the machine it runs on?
|
|
2
|
+
--
|
|
3
|
+
-- Nullable with NO default, deliberately — the same contract as product_mode
|
|
4
|
+
-- (0098). The instance_settings row exists on every instance, so a NOT NULL
|
|
5
|
+
-- DEFAULT would write a concrete value everywhere and permanently shadow the
|
|
6
|
+
-- OPENSHIP_HOST_CONTROL env var (written by `openship up --no-host-control`).
|
|
7
|
+
-- The resolver reads `settings.host_control_enabled ?? env`, so "unset" has to
|
|
8
|
+
-- stay representable. NULL means "no instance override, use env".
|
|
9
|
+
--
|
|
10
|
+
-- Unlike product_mode this DOES gate privileged behavior (host-root deploys via
|
|
11
|
+
-- the container->host SSH channel + mounted docker socket), so the WRITE path is
|
|
12
|
+
-- gated to the box-owning org's owner. The precedence still lets the DB override
|
|
13
|
+
-- env: the operator's need is to re-enable from Settings what --no-host-control
|
|
14
|
+
-- turned off.
|
|
15
|
+
ALTER TABLE "instance_settings" ADD COLUMN IF NOT EXISTS "host_control_enabled" boolean;
|
|
@@ -687,6 +687,55 @@
|
|
|
687
687
|
"when": 1787350907325,
|
|
688
688
|
"tag": "0097_update_status_upstream_only",
|
|
689
689
|
"breakpoints": true
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
"idx": 98,
|
|
693
|
+
"version": "7",
|
|
694
|
+
"when": 1787437307325,
|
|
695
|
+
"tag": "0098_instance_product_mode",
|
|
696
|
+
"breakpoints": true
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
"idx": 99,
|
|
700
|
+
"version": "7",
|
|
701
|
+
"when": 1787523707325,
|
|
702
|
+
"tag": "0099_mail_webmail_project",
|
|
703
|
+
"breakpoints": true
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"idx": 100,
|
|
707
|
+
"version": "7",
|
|
708
|
+
"when": 1787610107325,
|
|
709
|
+
"tag": "0100_server_ssh_private_key",
|
|
710
|
+
"breakpoints": true
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
"idx": 101,
|
|
714
|
+
"version": "7",
|
|
715
|
+
"when": 1787696507325,
|
|
716
|
+
"tag": "0101_mail_resume_step",
|
|
717
|
+
"breakpoints": true
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
"idx": 102,
|
|
721
|
+
"version": "7",
|
|
722
|
+
"when": 1787782907325,
|
|
723
|
+
"tag": "0102_project_workload_axes",
|
|
724
|
+
"breakpoints": true
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
"idx": 103,
|
|
728
|
+
"version": "7",
|
|
729
|
+
"when": 1787869307325,
|
|
730
|
+
"tag": "0103_dns_credential",
|
|
731
|
+
"breakpoints": true
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
"idx": 104,
|
|
735
|
+
"version": "7",
|
|
736
|
+
"when": 1787955707325,
|
|
737
|
+
"tag": "0104_instance_host_control",
|
|
738
|
+
"breakpoints": true
|
|
690
739
|
}
|
|
691
740
|
]
|
|
692
741
|
}
|
|
@@ -4,17 +4,18 @@ import { createRequire as __ospCreateRequire } from "node:module";
|
|
|
4
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
5
5
|
import {
|
|
6
6
|
SystemManager
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-ZCFVHA25.js";
|
|
11
|
-
import "./chunk-QEH4QUBX.js";
|
|
7
|
+
} from "./chunk-XVOBL7BC.js";
|
|
8
|
+
import "./chunk-7334NSJL.js";
|
|
9
|
+
import "./chunk-VLQ3UIVO.js";
|
|
12
10
|
import "./chunk-I3DPMTSF.js";
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-RFWJE7ZN.js";
|
|
12
|
+
import "./chunk-WE7BXW6L.js";
|
|
13
|
+
import "./chunk-A7EWM2KA.js";
|
|
14
|
+
import "./chunk-PZRRL5PC.js";
|
|
15
|
+
import "./chunk-EZPPTWJQ.js";
|
|
16
|
+
import "./chunk-WFRXYWK2.js";
|
|
16
17
|
import "./chunk-ZFJXPCQP.js";
|
|
17
|
-
import "./chunk-
|
|
18
|
+
import "./chunk-LEYSL46B.js";
|
|
18
19
|
import "./chunk-PXKCZB2I.js";
|
|
19
20
|
export {
|
|
20
21
|
SystemManager
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
|
+
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
|
+
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
+
import {
|
|
6
|
+
ensureInternalToken,
|
|
7
|
+
normalizeUrl,
|
|
8
|
+
runHeadlessProvision,
|
|
9
|
+
startService,
|
|
10
|
+
upCommand
|
|
11
|
+
} from "./chunk-7S4USQYF.js";
|
|
12
|
+
import "./chunk-6HOA5JT7.js";
|
|
13
|
+
import "./chunk-HPXP6LXZ.js";
|
|
14
|
+
import "./chunk-XRNRT37W.js";
|
|
15
|
+
import "./chunk-QLX7NBEA.js";
|
|
16
|
+
import "./chunk-QWHBRY2K.js";
|
|
17
|
+
import "./chunk-ELGRMZOW.js";
|
|
18
|
+
import "./chunk-ONKEYSMY.js";
|
|
19
|
+
import "./chunk-XZYFZ6A5.js";
|
|
20
|
+
import "./chunk-PDW4L3NV.js";
|
|
21
|
+
import "./chunk-DOD2BXHQ.js";
|
|
22
|
+
import "./chunk-42WQABXM.js";
|
|
23
|
+
import "./chunk-DRK3MEDS.js";
|
|
24
|
+
import "./chunk-3NQQXMDR.js";
|
|
25
|
+
import "./chunk-ZWLDLTJP.js";
|
|
26
|
+
import "./chunk-6JELW75A.js";
|
|
27
|
+
import "./chunk-3ZAGC5NT.js";
|
|
28
|
+
import "./chunk-WNWQBB7F.js";
|
|
29
|
+
import "./chunk-2GWYYHOR.js";
|
|
30
|
+
import "./chunk-RXQBD4ZY.js";
|
|
31
|
+
import "./chunk-XVOBL7BC.js";
|
|
32
|
+
import "./chunk-7334NSJL.js";
|
|
33
|
+
import "./chunk-VLQ3UIVO.js";
|
|
34
|
+
import "./chunk-I3DPMTSF.js";
|
|
35
|
+
import "./chunk-VDESJSG6.js";
|
|
36
|
+
import "./chunk-LFCSC4IU.js";
|
|
37
|
+
import "./chunk-GRTXMUHA.js";
|
|
38
|
+
import "./chunk-MSZUCNPV.js";
|
|
39
|
+
import "./chunk-TM2IQPYU.js";
|
|
40
|
+
import "./chunk-VHMOQL5Y.js";
|
|
41
|
+
import "./chunk-5OUWIPOB.js";
|
|
42
|
+
import "./chunk-6FH4AXVY.js";
|
|
43
|
+
import "./chunk-RFWJE7ZN.js";
|
|
44
|
+
import "./chunk-WE7BXW6L.js";
|
|
45
|
+
import "./chunk-A7EWM2KA.js";
|
|
46
|
+
import "./chunk-PZRRL5PC.js";
|
|
47
|
+
import "./chunk-EZPPTWJQ.js";
|
|
48
|
+
import "./chunk-WFRXYWK2.js";
|
|
49
|
+
import "./chunk-ZFJXPCQP.js";
|
|
50
|
+
import "./chunk-LEYSL46B.js";
|
|
51
|
+
import "./chunk-PXKCZB2I.js";
|
|
52
|
+
export {
|
|
53
|
+
ensureInternalToken,
|
|
54
|
+
normalizeUrl,
|
|
55
|
+
runHeadlessProvision,
|
|
56
|
+
startService,
|
|
57
|
+
upCommand
|
|
58
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openship",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Openship CLI - deploy, manage, and open your Openship platform from the terminal",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"bin": {
|
|
21
21
|
"openship": "./dist/node-entry.js"
|
|
22
22
|
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
23
26
|
"files": [
|
|
24
27
|
"dist",
|
|
25
28
|
"README.md"
|
|
@@ -42,7 +45,6 @@
|
|
|
42
45
|
"commander": "^14.0.3",
|
|
43
46
|
"dockerode": "^4.0.9",
|
|
44
47
|
"open": "^10.2.0",
|
|
45
|
-
"openship": "^0.5.0",
|
|
46
48
|
"ora": "^9.3.0",
|
|
47
49
|
"ssh2": "^1.17.0"
|
|
48
50
|
},
|