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.
Files changed (63) hide show
  1. package/README.md +7 -2
  2. package/dist/{bare-FSUAM5V5.js → bare-XQIPDO4R.js} +10 -9
  3. package/dist/{chunk-PSZSKEPA.js → chunk-3ZAGC5NT.js} +64 -17
  4. package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
  5. package/dist/chunk-6HOA5JT7.js +124 -0
  6. package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
  7. package/dist/{chunk-GUQ3QCJ7.js → chunk-7334NSJL.js} +399 -369
  8. package/dist/chunk-7S4USQYF.js +1753 -0
  9. package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
  10. package/dist/chunk-ELGRMZOW.js +25 -0
  11. package/dist/chunk-EZPPTWJQ.js +292 -0
  12. package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
  13. package/dist/chunk-HPXP6LXZ.js +428 -0
  14. package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
  15. package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
  16. package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
  17. package/dist/{chunk-OGY63DGD.js → chunk-PZRRL5PC.js} +71 -28
  18. package/dist/{chunk-VE2YOK4I.js → chunk-QLX7NBEA.js} +1218 -588
  19. package/dist/chunk-RFWJE7ZN.js +68 -0
  20. package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
  21. package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
  22. package/dist/{chunk-B3CE2OMR.js → chunk-VHMOQL5Y.js} +276 -50
  23. package/dist/{chunk-QEH4QUBX.js → chunk-VLQ3UIVO.js} +393 -53
  24. package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
  25. package/dist/chunk-WFRXYWK2.js +444 -0
  26. package/dist/chunk-XRNRT37W.js +316 -0
  27. package/dist/{chunk-EP2TNALB.js → chunk-XVOBL7BC.js} +31 -8
  28. package/dist/cloud-3DODOWTR.js +36 -0
  29. package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
  30. package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
  31. package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
  32. package/dist/{edge-import-CQWHN2Q3.js → edge-import-MCJNVTZ2.js} +26 -23
  33. package/dist/edge-preflight-ES6FH4JK.js +42 -0
  34. package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-YPITNH45.js} +9 -8
  35. package/dist/{executor-JELARVKC.js → executor-DZGX55K2.js} +12 -5
  36. package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
  37. package/dist/index.js +3668 -4063
  38. package/dist/{nginx-AXGNWRKZ.js → nginx-NSAHWGJV.js} +11 -5
  39. package/dist/node-bootstrap.js +241 -0
  40. package/dist/node-entry.js +52 -1
  41. package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-DR4D3EZ6.js} +6 -2
  42. package/dist/privilege-YTVI4SKF.js +19 -0
  43. package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
  44. package/dist/server/index.js +21501 -13137
  45. package/dist/server/lua/geo_country.lua +4 -3
  46. package/dist/server/lua/pipe_stream.lua +52 -39
  47. package/dist/server/lua/site_logger.lua +22 -24
  48. package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
  49. package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
  50. package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
  51. package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
  52. package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
  53. package/dist/server/migrations/0103_dns_credential.sql +41 -0
  54. package/dist/server/migrations/0104_instance_host_control.sql +15 -0
  55. package/dist/server/migrations/meta/_journal.json +49 -0
  56. package/dist/{setup-FA4PW4NU.js → setup-WUUZURHS.js} +10 -9
  57. package/dist/up-XEO3SR6Z.js +58 -0
  58. package/package.json +4 -2
  59. package/dist/chunk-IW2UKBVF.js +0 -144
  60. package/dist/chunk-ZCFVHA25.js +0 -89
  61. package/dist/cloud-E5M6VMCG.js +0 -31
  62. package/dist/server/lua/pipe_log.lua +0 -77
  63. /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. Both site_logger.lua and pipe_log.lua
18
- -- wrap this module in pcall(require, ...) so a missing DB never crashes
19
- -- the request pipeline. mgmt_api `GET /status` reports whether geo actually
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
- local SUB_KEY = "log_pipe:sub:" .. domain
24
- local QUEUE_KEY = "log_pipe:q:" .. domain
25
-
26
- -- Clear stale queue entries, mark subscriber active
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
- local started = ngx.now()
45
- local last_hb = started
46
- local last_ref = started
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
- sh:delete(SUB_KEY)
52
- return
53
- end
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 entry = sh:rpop(QUEUE_KEY)
59
- if not entry then break end
60
- ngx.print("event: request\ndata: ", entry, "\n\n")
61
- sent = sent + 1
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
- -- log_pipe:sub:{domain} live subscriber flag (TTL 30s)
31
- -- log_pipe:q:{domain} live log queue entries
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 THREE consumers need it — the daily/per-minute
198
- -- rollups, the raw ring buffer behind /logs/recent, and the live SSE pipe. It used to be
199
- -- looked up inside the rollup and again inside pipe_log, so the ring buffer had no
200
- -- country at all: a request-log list showed flags on rows that arrived live and none on
201
- -- rows backfilled from /logs/recent, for the same traffic.
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
- local seq = request_data:incr("rlog:" .. host .. ":seq", 1, 0)
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-EP2TNALB.js";
8
- import "./chunk-GUQ3QCJ7.js";
9
- import "./chunk-IW2UKBVF.js";
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-VZ2ZHPS2.js";
14
- import "./chunk-3LDWWY7U.js";
15
- import "./chunk-OGY63DGD.js";
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-OTLSJYAC.js";
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.0",
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
  },