openship 0.6.1 → 0.6.6
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-IWS2QHJ3.js → bare-OXJVW3AK.js} +11 -9
- package/dist/{chunk-VZ2ZHPS2.js → chunk-2YM3NI2E.js} +3 -9
- package/dist/chunk-4I2BM2DT.js +68 -0
- package/dist/{chunk-2NZSUXOB.js → chunk-5DMWSTOM.js} +310 -1459
- package/dist/{chunk-3FYIXVX5.js → chunk-77QDF6UD.js} +581 -122
- package/dist/{chunk-IOT5ZX23.js → chunk-A4GXYE4L.js} +449 -383
- package/dist/chunk-CBLD2HS5.js +428 -0
- package/dist/chunk-ELGRMZOW.js +25 -0
- package/dist/{chunk-TWJCLD4W.js → chunk-FE7ZZQSE.js} +5 -5
- package/dist/{chunk-3NZICQHF.js → chunk-HKRQXPTI.js} +31 -8
- package/dist/{chunk-IXM5245F.js → chunk-LFE5KQ7Y.js} +553 -538
- package/dist/{chunk-VDDMADOF.js → chunk-LH2PLRQO.js} +16 -2
- package/dist/{chunk-EMXI4UCU.js → chunk-LUKYPVJ4.js} +29 -10
- package/dist/{chunk-UC7YIEHT.js → chunk-MLYSEZQB.js} +352 -47
- package/dist/chunk-NK5N5LXN.js +1681 -0
- package/dist/{chunk-RRJJGVKE.js → chunk-OBPKYXQL.js} +75 -17
- package/dist/{chunk-6ODUBKUB.js → chunk-PCPVTH6M.js} +78 -88
- package/dist/chunk-Q7WGTQQ5.js +88 -0
- package/dist/{chunk-EW6KB2J7.js → chunk-QVW76WDL.js} +440 -74
- package/dist/{chunk-YIPMDGG6.js → chunk-RSR7PFIQ.js} +79 -23
- package/dist/{chunk-3LDWWY7U.js → chunk-SM26HDS2.js} +41 -7
- package/dist/{chunk-OTLSJYAC.js → chunk-UJ6JZM65.js} +5185 -341
- package/dist/chunk-UVNMQM7W.js +444 -0
- package/dist/chunk-VTGPEC6B.js +308 -0
- package/dist/chunk-WVAML4H6.js +2145 -0
- package/dist/{chunk-JIMUYEKN.js → chunk-YIMOCZBZ.js} +1 -1
- package/dist/chunk-YMMQRE5J.js +292 -0
- package/dist/cloud-75VIT4KV.js +37 -0
- package/dist/{detect-ALF4GXRX.js → detect-OIE5V3VZ.js} +3 -3
- package/dist/{docker-QECQJ5P4.js → docker-VTR2GK5Y.js} +16 -8
- package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-22IBXNYJ.js} +5 -5
- package/dist/{edge-import-SAYTRJLO.js → edge-import-WJUB4M7F.js} +27 -23
- package/dist/edge-preflight-GBNCPNVU.js +42 -0
- package/dist/{ensure-container-edge-CFSCPGZK.js → ensure-container-edge-KPI2CVXJ.js} +9 -8
- package/dist/{executor-PT6UEJ5M.js → executor-7LXXZPBR.js} +15 -5
- package/dist/host-channel-preflight-KE6OONYK.js +50 -0
- package/dist/index.js +3783 -4221
- package/dist/{nginx-YOEEFBTE.js → nginx-KUFMLMAC.js} +13 -5
- package/dist/node-bootstrap.js +241 -0
- package/dist/node-entry.js +52 -1
- package/dist/{openresty-lua-WGBO2M3Z.js → openresty-lua-ATTH35BE.js} +8 -2
- package/dist/privilege-I4W3ZNZK.js +19 -0
- package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
- package/dist/server/engine/samples/postfix/master.cf +14 -0
- package/dist/server/index.js +77659 -61909
- 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/0105_mail_inbound_rules.sql +80 -0
- package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
- package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
- package/dist/server/migrations/0108_credential.sql +68 -0
- package/dist/server/migrations/meta/_journal.json +77 -0
- package/dist/{setup-M2ZDGH3O.js → setup-XGLGUUFW.js} +10 -9
- package/dist/ssh-client-JBMA7SN4.js +28 -0
- package/dist/up-K4BV7466.js +57 -0
- package/package.json +4 -2
- package/dist/chunk-IW2UKBVF.js +0 -144
- package/dist/chunk-ZCFVHA25.js +0 -89
- package/dist/cloud-M2YLWEXO.js +0 -31
- /package/dist/{dist-es-JGBPABJI.js → dist-es-AXFRS7PS.js} +0 -0
|
@@ -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;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
-- Inbound-mail notification rules — "mail arrives at a watched address, tell me in a channel".
|
|
2
|
+
--
|
|
3
|
+
-- Capture is Postfix's own `recipient_bcc_maps`, which the shipped main.cf already points
|
|
4
|
+
-- at two SQL tables in the engine's `vmail` DB (main.cf.pgsql:36-38). Arming a rule is
|
|
5
|
+
-- therefore ONE row in `vmail.recipient_bcc_domain` — no postmap, no reload, no container
|
|
6
|
+
-- recreate, and identical on the container and legacy host flavors, so it reaches every
|
|
7
|
+
-- mail box already provisioned.
|
|
8
|
+
--
|
|
9
|
+
-- ONE TABLE, ON PURPOSE. The obvious second table — a record of each armed collector —
|
|
10
|
+
-- would only cache state the mail engine already holds authoritatively: the token is
|
|
11
|
+
-- literally inside `recipient_bcc_domain.bcc_address`
|
|
12
|
+
-- (`openship-hook+<token>@<domain>`), the maildir is in `vmail.mailbox`, and "a foreign
|
|
13
|
+
-- BCC took the slot" is derivable by comparing that address against our own prefix. A
|
|
14
|
+
-- cache of the engine's own state is a thing that goes stale; the collector is READ from
|
|
15
|
+
-- vmail instead.
|
|
16
|
+
--
|
|
17
|
+
-- What genuinely has to persist here is the RULES: they reference
|
|
18
|
+
-- `notification_channel` ids, which exist only in this database, and they are created and
|
|
19
|
+
-- edited from a dashboard tab. Keeping them on the mail box would make every list and
|
|
20
|
+
-- toggle an SSH round-trip, and would make the rules unreadable exactly when the mail box
|
|
21
|
+
-- is unreachable.
|
|
22
|
+
--
|
|
23
|
+
-- No cursor table either. The BCC copy is a full message in the collector's maildir, so
|
|
24
|
+
-- the read job dispatches and then DELETES the file: deletion is the cursor, and it
|
|
25
|
+
-- doubles as the prune that keeps a second full copy of every watched message from
|
|
26
|
+
-- growing forever on a bind mount with no quota of its own.
|
|
27
|
+
CREATE TABLE IF NOT EXISTS "mail_inbound_rule" (
|
|
28
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
29
|
+
"server_id" text NOT NULL REFERENCES "mail_servers"("server_id") ON DELETE CASCADE,
|
|
30
|
+
-- Carried so dispatch can fan out to the right org's channels without joining back
|
|
31
|
+
-- through `servers`. This does NOT make the table organization-scope for dump purposes
|
|
32
|
+
-- (see dump.ts): its FK targets are instance-scope.
|
|
33
|
+
"organization_id" text NOT NULL REFERENCES "organization"("id") ON DELETE CASCADE,
|
|
34
|
+
-- Operator-facing label, e.g. "Support inbox → #support".
|
|
35
|
+
"name" text NOT NULL,
|
|
36
|
+
-- mailbox | domain | all. Text, not an enum: a new scope should be a registry entry and
|
|
37
|
+
-- a UI option, not a migration.
|
|
38
|
+
"scope" text NOT NULL,
|
|
39
|
+
-- Full address for `mailbox`, the domain for `domain`, NULL for `all`.
|
|
40
|
+
--
|
|
41
|
+
-- `all` is a genuine fan-out: there is no global `always_bcc` anywhere in the shipped
|
|
42
|
+
-- Postfix config, and the domain lookup keys on exact equality with no wildcard
|
|
43
|
+
-- convention — so it arms one row per domain and needs a reconcile when a domain is
|
|
44
|
+
-- added. There is deliberately no CHECK tying scope to target: this schema has no CHECK
|
|
45
|
+
-- constraints anywhere, and the SQL and the drizzle schema are hand-maintained in
|
|
46
|
+
-- parallel. The invariant is enforced fail-closed at the write boundary and in the
|
|
47
|
+
-- filter, where a mailbox rule with no target matches NOTHING rather than everything.
|
|
48
|
+
"target" text,
|
|
49
|
+
-- Optional extra matching, applied in the control plane at filter time so editing one
|
|
50
|
+
-- never touches the mail box. NULL = match everything.
|
|
51
|
+
"from_pattern" text,
|
|
52
|
+
"subject_pattern" text,
|
|
53
|
+
-- Spam gate, and not optional in practice. The shipped amavis policy sets
|
|
54
|
+
-- spam_lover='Y' AND bad_header_lover='Y' on the catch-all '@.' policy with empty
|
|
55
|
+
-- quarantine targets, so the global $final_spam_destiny = D_DISCARD is dead for every
|
|
56
|
+
-- recipient: spam and bad-header mail ARE delivered and therefore ARE captured.
|
|
57
|
+
-- Nothing upstream filters on our behalf.
|
|
58
|
+
"max_spam_score" real,
|
|
59
|
+
-- Channel ids as a jsonb array, deliberately NOT a join table with an FK to
|
|
60
|
+
-- notification_channel: the dump's FK_PARENT maps a bare `channelId` column to that
|
|
61
|
+
-- table unconditionally for ANY table, which would make assertDumpSelfContained reject
|
|
62
|
+
-- any dump carrying a real join row on a remap path.
|
|
63
|
+
"channel_ids" jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
64
|
+
"enabled" boolean NOT NULL DEFAULT true,
|
|
65
|
+
-- Burst control state. The notification subsystem has no rate limiting or windowed
|
|
66
|
+
-- dedup of its own — that is explicitly the producer's job — so a mailing list hitting
|
|
67
|
+
-- a watched address must not become one alert per message.
|
|
68
|
+
"paused_reason" text,
|
|
69
|
+
"last_matched_at" timestamp,
|
|
70
|
+
"created_at" timestamp NOT NULL DEFAULT now(),
|
|
71
|
+
"updated_at" timestamp NOT NULL DEFAULT now()
|
|
72
|
+
);
|
|
73
|
+
--> statement-breakpoint
|
|
74
|
+
-- The dispatch path's read on every tick: "enabled rules for this server".
|
|
75
|
+
CREATE INDEX IF NOT EXISTS "idx_mail_inbound_rule_server_enabled"
|
|
76
|
+
ON "mail_inbound_rule" ("server_id", "enabled");
|
|
77
|
+
--> statement-breakpoint
|
|
78
|
+
-- The rules tab lists per org.
|
|
79
|
+
CREATE INDEX IF NOT EXISTS "idx_mail_inbound_rule_org"
|
|
80
|
+
ON "mail_inbound_rule" ("organization_id");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- MCP observability: attribute an audit row to the CLIENT that made it, and
|
|
2
|
+
-- count how much a credential is used.
|
|
3
|
+
--
|
|
4
|
+
-- `source` already said "an AI assistant did this" (0088), unforgeably — but not
|
|
5
|
+
-- WHICH assistant. With two clients connected under one user (Claude Desktop and
|
|
6
|
+
-- Cursor, say) every row read the same, so "what did this agent do" could not be
|
|
7
|
+
-- answered and disconnecting the wrong one was a coin flip. The column holds the
|
|
8
|
+
-- canonical principal id the auth layer already mints (`oauth:<clientId>` /
|
|
9
|
+
-- `pat:<tokenId>`), so it resolves to a name through existing tables.
|
|
10
|
+
--
|
|
11
|
+
-- The index is PARTIAL: only MCP rows ever carry a client id, and mcp.tool_called
|
|
12
|
+
-- makes audit_event's highest-volume writer, so indexing the NULLs would be paid
|
|
13
|
+
-- for on every insert in the table for no read.
|
|
14
|
+
ALTER TABLE "audit_event" ADD COLUMN IF NOT EXISTS "source_client_id" text;--> statement-breakpoint
|
|
15
|
+
CREATE INDEX IF NOT EXISTS "audit_event_org_client_idx"
|
|
16
|
+
ON "audit_event" ("organization_id", "source_client_id", "created_at" DESC)
|
|
17
|
+
WHERE "source_client_id" IS NOT NULL;--> statement-breakpoint
|
|
18
|
+
|
|
19
|
+
-- Call counter for every bearer credential (manual PATs and the OAuth-binding
|
|
20
|
+
-- rows behind MCP connections alike). NOT NULL DEFAULT 0 is safe here: existing
|
|
21
|
+
-- rows genuinely have no counted history, and 0 alongside a non-null
|
|
22
|
+
-- `last_used_at` reads correctly as "used before counting existed".
|
|
23
|
+
ALTER TABLE "personal_access_token" ADD COLUMN IF NOT EXISTS "use_count" integer DEFAULT 0 NOT NULL;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Indexes for the build-minute allowance query.
|
|
2
|
+
--
|
|
3
|
+
-- `sumBuildMillisForOrg` joins build_session → deployment, filters on
|
|
4
|
+
-- deployment.organization_id and windows on build_session.started_at. It used to
|
|
5
|
+
-- run only on the billing page (once, on demand); it now runs on the plan gate in
|
|
6
|
+
-- `createQueuedDeployment`, i.e. on every deploy. Neither side of that join had a
|
|
7
|
+
-- supporting index — build_session carried only its primary key and
|
|
8
|
+
-- deployment.organization_id had none — so the query was a double sequential scan
|
|
9
|
+
-- whose cost grows with TOTAL deployment history across all orgs, not with the
|
|
10
|
+
-- org being checked.
|
|
11
|
+
--
|
|
12
|
+
-- (domain.project_id, which the free-subdomain count joins on, is already covered
|
|
13
|
+
-- by idx_domain_project.)
|
|
14
|
+
CREATE INDEX IF NOT EXISTS "idx_build_session_started_at"
|
|
15
|
+
ON "build_session" ("started_at");--> statement-breakpoint
|
|
16
|
+
|
|
17
|
+
-- Org scoping for the same join. Also serves the org-wide deployment listings,
|
|
18
|
+
-- which had been scanning for the same reason.
|
|
19
|
+
CREATE INDEX IF NOT EXISTS "idx_deployment_org"
|
|
20
|
+
ON "deployment" ("organization_id");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- Third-party credentials, in one place.
|
|
2
|
+
--
|
|
3
|
+
-- Openship needs other people's secrets for unrelated jobs — write a DNS record, pull a
|
|
4
|
+
-- private image, later push a backup — and each job grew its own storage: a single token
|
|
5
|
+
-- column on `dns_credential`, FIVE encrypted columns inlined on a backup destination, a
|
|
6
|
+
-- channel's own on notifications. Same `enc1:` envelope in each, three shapes, three
|
|
7
|
+
-- forms in the UI, and no way to reuse one credential for two features.
|
|
8
|
+
--
|
|
9
|
+
-- The unit here is the CREDENTIAL, deliberately not the thing that uses it. A backup
|
|
10
|
+
-- destination is a place, a DNS zone is a zone, a notification channel is a route; each
|
|
11
|
+
-- keeps its own row and points at a credential. That is what makes "one Cloudflare token
|
|
12
|
+
-- for every zone" and "one ghcr.io login for every project" possible — the reuse is the
|
|
13
|
+
-- point, fewer screens is the side effect.
|
|
14
|
+
--
|
|
15
|
+
-- Shape notes:
|
|
16
|
+
-- • `provider` is TEXT, matching dns_credential's reasoning: adding a provider should
|
|
17
|
+
-- be a registry entry in @repo/core, not a migration.
|
|
18
|
+
-- • `selector` is what the credential APPLIES TO when one per provider is not enough —
|
|
19
|
+
-- a registry host for `docker-registry`, NULL for an org-wide `cloudflare` token. It
|
|
20
|
+
-- is stored normalized (see normalizeCredentialSelector) so the value a pull derives
|
|
21
|
+
-- from an image reference matches the value the operator typed.
|
|
22
|
+
-- • `secrets_enc` is ONE `enc1:` AES-256-GCM envelope over a JSON object, so a provider
|
|
23
|
+
-- with several secret fields (an S3 key pair, an SFTP key + passphrase) needs no new
|
|
24
|
+
-- columns. The readable half lives in `public_fields`, which is what the API can
|
|
25
|
+
-- return without decrypting anything.
|
|
26
|
+
-- • `status` + `last_error` exist so a credential that was revoked upstream becomes
|
|
27
|
+
-- VISIBLE instead of silently failing every deploy or backup forever. `last_error` is
|
|
28
|
+
-- the provider's redacted reason, never its response body.
|
|
29
|
+
CREATE TABLE IF NOT EXISTS "credential" (
|
|
30
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
31
|
+
"organization_id" text NOT NULL REFERENCES "organization"("id") ON DELETE CASCADE,
|
|
32
|
+
-- Provider id from CREDENTIAL_PROVIDERS ("docker-registry", "cloudflare").
|
|
33
|
+
"provider" text NOT NULL,
|
|
34
|
+
-- Operator-facing label, e.g. "GHCR read-only".
|
|
35
|
+
"name" text NOT NULL,
|
|
36
|
+
-- The resource this credential is for, normalized; NULL when the provider is org-wide.
|
|
37
|
+
"selector" text,
|
|
38
|
+
-- Non-secret fields (username, region, endpoint) — returned by the API as-is.
|
|
39
|
+
"public_fields" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
40
|
+
-- Every secret field for this provider, as one enc1: envelope over a JSON object.
|
|
41
|
+
"secrets_enc" text NOT NULL,
|
|
42
|
+
-- active | invalid. Flipped to 'invalid' the moment a provider answers 401/403, which
|
|
43
|
+
-- is the only time we learn a stored credential stopped working.
|
|
44
|
+
"status" text NOT NULL DEFAULT 'active',
|
|
45
|
+
"last_verified_at" timestamp,
|
|
46
|
+
-- Redacted reason the last verify or use failed. Never provider response bodies.
|
|
47
|
+
"last_error" text,
|
|
48
|
+
"created_at" timestamp NOT NULL DEFAULT now(),
|
|
49
|
+
"updated_at" timestamp NOT NULL DEFAULT now()
|
|
50
|
+
);
|
|
51
|
+
--> statement-breakpoint
|
|
52
|
+
-- The resolve path asks exactly one question — "this org's credential for this provider
|
|
53
|
+
-- and this resource" — on every deploy that pulls and every domain that provisions.
|
|
54
|
+
-- Leading org column means the plain org-wide listing and the per-provider listing both
|
|
55
|
+
-- ride this index as prefixes, so it is the only lookup index the table needs. `status`
|
|
56
|
+
-- is filtered in code: a handful of rows per org makes an extra column here dead weight.
|
|
57
|
+
CREATE INDEX IF NOT EXISTS "idx_credential_org_provider_selector"
|
|
58
|
+
ON "credential" ("organization_id", "provider", "selector");
|
|
59
|
+
--> statement-breakpoint
|
|
60
|
+
-- One label per provider per resource per org. A second "GHCR read-only" for the same
|
|
61
|
+
-- host is a double-submit, and two credentials matching one registry makes "which one
|
|
62
|
+
-- authenticated this pull" unanswerable when one of them starts failing.
|
|
63
|
+
--
|
|
64
|
+
-- COALESCE, not a bare column: Postgres treats NULLs as DISTINCT in a unique index, so
|
|
65
|
+
-- an org-wide provider (selector IS NULL — every Cloudflare token) would accept
|
|
66
|
+
-- unlimited duplicates of the same name and this constraint would quietly do nothing.
|
|
67
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "uq_credential_org_provider_selector_name"
|
|
68
|
+
ON "credential" ("organization_id", "provider", COALESCE("selector", ''), "name");
|
|
@@ -687,6 +687,83 @@
|
|
|
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
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
"idx": 105,
|
|
742
|
+
"version": "7",
|
|
743
|
+
"when": 1788042107325,
|
|
744
|
+
"tag": "0105_mail_inbound_rules",
|
|
745
|
+
"breakpoints": true
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
"idx": 106,
|
|
749
|
+
"version": "7",
|
|
750
|
+
"when": 1788128507325,
|
|
751
|
+
"tag": "0106_mcp_call_tracking",
|
|
752
|
+
"breakpoints": true
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
"idx": 107,
|
|
756
|
+
"version": "7",
|
|
757
|
+
"when": 1788214907325,
|
|
758
|
+
"tag": "0107_build_minute_metering_indexes",
|
|
759
|
+
"breakpoints": true
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"idx": 108,
|
|
763
|
+
"version": "7",
|
|
764
|
+
"when": 1788301307325,
|
|
765
|
+
"tag": "0108_credential",
|
|
766
|
+
"breakpoints": true
|
|
690
767
|
}
|
|
691
768
|
]
|
|
692
769
|
}
|
|
@@ -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-
|
|
11
|
-
import "./chunk-EW6KB2J7.js";
|
|
7
|
+
} from "./chunk-HKRQXPTI.js";
|
|
8
|
+
import "./chunk-A4GXYE4L.js";
|
|
9
|
+
import "./chunk-QVW76WDL.js";
|
|
10
|
+
import "./chunk-RSR7PFIQ.js";
|
|
12
11
|
import "./chunk-I3DPMTSF.js";
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-4I2BM2DT.js";
|
|
13
|
+
import "./chunk-YMMQRE5J.js";
|
|
14
|
+
import "./chunk-UVNMQM7W.js";
|
|
16
15
|
import "./chunk-ZFJXPCQP.js";
|
|
17
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-2YM3NI2E.js";
|
|
17
|
+
import "./chunk-SM26HDS2.js";
|
|
18
|
+
import "./chunk-UJ6JZM65.js";
|
|
18
19
|
import "./chunk-PXKCZB2I.js";
|
|
19
20
|
export {
|
|
20
21
|
SystemManager
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
DOCKER_DIAL_STDIO_COMMAND,
|
|
7
|
+
attachDialStdioDiagnostics,
|
|
8
|
+
connectSshClient,
|
|
9
|
+
dialStdioDiagnostics,
|
|
10
|
+
execSshCommand,
|
|
11
|
+
openDockerDialStdioChannel,
|
|
12
|
+
openSftp,
|
|
13
|
+
openSshExecChannel,
|
|
14
|
+
openSshUnixSocket
|
|
15
|
+
} from "./chunk-PCPVTH6M.js";
|
|
16
|
+
import "./chunk-UJ6JZM65.js";
|
|
17
|
+
import "./chunk-PXKCZB2I.js";
|
|
18
|
+
export {
|
|
19
|
+
DOCKER_DIAL_STDIO_COMMAND,
|
|
20
|
+
attachDialStdioDiagnostics,
|
|
21
|
+
connectSshClient,
|
|
22
|
+
dialStdioDiagnostics,
|
|
23
|
+
execSshCommand,
|
|
24
|
+
openDockerDialStdioChannel,
|
|
25
|
+
openSftp,
|
|
26
|
+
openSshExecChannel,
|
|
27
|
+
openSshUnixSocket
|
|
28
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
normalizeUrl,
|
|
7
|
+
runHeadlessProvision,
|
|
8
|
+
startService,
|
|
9
|
+
upCommand
|
|
10
|
+
} from "./chunk-NK5N5LXN.js";
|
|
11
|
+
import "./chunk-WVAML4H6.js";
|
|
12
|
+
import "./chunk-VTGPEC6B.js";
|
|
13
|
+
import "./chunk-5DMWSTOM.js";
|
|
14
|
+
import "./chunk-QWHBRY2K.js";
|
|
15
|
+
import "./chunk-CBLD2HS5.js";
|
|
16
|
+
import "./chunk-ELGRMZOW.js";
|
|
17
|
+
import "./chunk-HKRQXPTI.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-FE7ZZQSE.js";
|
|
27
|
+
import "./chunk-OBPKYXQL.js";
|
|
28
|
+
import "./chunk-A4GXYE4L.js";
|
|
29
|
+
import "./chunk-QVW76WDL.js";
|
|
30
|
+
import "./chunk-RSR7PFIQ.js";
|
|
31
|
+
import "./chunk-I3DPMTSF.js";
|
|
32
|
+
import "./chunk-WNWQBB7F.js";
|
|
33
|
+
import "./chunk-2GWYYHOR.js";
|
|
34
|
+
import "./chunk-RXQBD4ZY.js";
|
|
35
|
+
import "./chunk-77QDF6UD.js";
|
|
36
|
+
import "./chunk-LUKYPVJ4.js";
|
|
37
|
+
import "./chunk-LFE5KQ7Y.js";
|
|
38
|
+
import "./chunk-YIMOCZBZ.js";
|
|
39
|
+
import "./chunk-LH2PLRQO.js";
|
|
40
|
+
import "./chunk-MLYSEZQB.js";
|
|
41
|
+
import "./chunk-Q7WGTQQ5.js";
|
|
42
|
+
import "./chunk-PCPVTH6M.js";
|
|
43
|
+
import "./chunk-6FH4AXVY.js";
|
|
44
|
+
import "./chunk-4I2BM2DT.js";
|
|
45
|
+
import "./chunk-YMMQRE5J.js";
|
|
46
|
+
import "./chunk-UVNMQM7W.js";
|
|
47
|
+
import "./chunk-ZFJXPCQP.js";
|
|
48
|
+
import "./chunk-2YM3NI2E.js";
|
|
49
|
+
import "./chunk-SM26HDS2.js";
|
|
50
|
+
import "./chunk-UJ6JZM65.js";
|
|
51
|
+
import "./chunk-PXKCZB2I.js";
|
|
52
|
+
export {
|
|
53
|
+
normalizeUrl,
|
|
54
|
+
runHeadlessProvision,
|
|
55
|
+
startService,
|
|
56
|
+
upCommand
|
|
57
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openship",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
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
|
},
|