@stacksjs/ts-cloud 0.12.7 → 0.12.9
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/dist/aws/index.d.ts +1 -0
- package/dist/aws/index.js +1 -1
- package/dist/aws/sms-voice.d.ts +41 -0
- package/dist/bin/{chunk-1jetwd47.js → chunk-04dkwaqp.js} +1 -1
- package/dist/bin/{chunk-h5wbsxce.js → chunk-0thy7q96.js} +48 -48
- package/dist/bin/{chunk-rpn3mzkh.js → chunk-aw8bhggq.js} +1 -1
- package/dist/bin/{chunk-vjzkfd63.js → chunk-azymqbcy.js} +1 -1
- package/dist/bin/{chunk-js12scjn.js → chunk-sg1g92cf.js} +1 -1
- package/dist/bin/{chunk-39pe9tc3.js → chunk-vj2dp0nh.js} +9 -9
- package/dist/bin/cli.js +113 -113
- package/dist/bin/dashboard-server.js +1 -1
- package/dist/{chunk-13qjvbqh.js → chunk-0hw5wjjg.js} +1 -1
- package/dist/chunk-1bj94sc2.js +1 -0
- package/dist/{chunk-3mgj3hrh.js → chunk-2k0mt6j5.js} +2 -2
- package/dist/chunk-2qm3rw8b.js +4 -0
- package/dist/{chunk-vtvfg2f2.js → chunk-3swnmwfc.js} +74 -74
- package/dist/{chunk-aj025v2s.js → chunk-557qj7q9.js} +2 -2
- package/dist/{chunk-86ss83s2.js → chunk-5hkq5081.js} +2 -2
- package/dist/chunk-5pbtmy2k.js +1 -0
- package/dist/{chunk-152he7m3.js → chunk-70v77sqv.js} +2 -2
- package/dist/{chunk-by2bwzk6.js → chunk-7j100g7w.js} +2 -2
- package/dist/chunk-7sc8t7v1.js +1 -0
- package/dist/{chunk-m8fs0233.js → chunk-82gezmen.js} +2 -2
- package/dist/{chunk-y4xkj2f1.js → chunk-982rye5b.js} +2 -2
- package/dist/{chunk-mz0dk78z.js → chunk-a368cs92.js} +1 -1
- package/dist/{chunk-t0xgmhd6.js → chunk-a5213256.js} +8 -8
- package/dist/{chunk-hn40z11p.js → chunk-bcffjsnj.js} +1 -1
- package/dist/chunk-c1hr34a5.js +2 -0
- package/dist/{chunk-wavjkdb8.js → chunk-c8cadhgc.js} +2 -2
- package/dist/{chunk-xcj8zfmg.js → chunk-fbwpm0zm.js} +2 -2
- package/dist/{chunk-s5q2k7a0.js → chunk-fgfm7cfm.js} +120 -114
- package/dist/{chunk-jm5mg890.js → chunk-fsssdd5k.js} +2 -2
- package/dist/chunk-gny5s4kp.js +1 -0
- package/dist/{chunk-73ztr55z.js → chunk-jphewtbx.js} +2 -2
- package/dist/{chunk-h42zck7h.js → chunk-jxrs1219.js} +3 -3
- package/dist/{chunk-x3hay61c.js → chunk-pb47hr0h.js} +1 -1
- package/dist/chunk-qpjcd415.js +22 -0
- package/dist/{chunk-6kpbveb8.js → chunk-qrte4gy2.js} +1 -1
- package/dist/{chunk-tz9myq9a.js → chunk-rg7d67xk.js} +2 -2
- package/dist/{chunk-1qdne0bf.js → chunk-w2ddj42w.js} +1 -1
- package/dist/chunk-xkw7h4pw.js +1 -0
- package/dist/deploy/index.d.ts +2 -0
- package/dist/deploy/index.js +1 -1
- package/dist/deploy/site-config-editor.d.ts +26 -0
- package/dist/deploy/site-ports.d.ts +20 -2
- package/dist/dns/index.js +1 -1
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/shared/mail-provision.d.ts +21 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -8
- package/dist/operations/index.d.ts +24 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/inventory.d.ts +168 -0
- package/dist/operations/inventory.test.d.ts +1 -0
- package/dist/operations/site-attach.d.ts +95 -0
- package/dist/operations/site-attach.test.d.ts +1 -0
- package/dist/protection/index.js +1 -1
- package/dist/push/index.js +1 -1
- package/dist/spend/index.js +1 -1
- package/dist/ui/404.html +1 -1
- package/dist/ui/accept-invitation.html +1 -1
- package/dist/ui/access-denied.html +1 -1
- package/dist/ui/account/automation.html +6 -6
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +5 -5
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +6 -6
- package/dist/ui/data/services.html +6 -6
- package/dist/ui/data/volumes.html +6 -6
- package/dist/ui/index.html +6 -6
- package/dist/ui/infrastructure/topology.html +6 -6
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +6 -6
- package/dist/ui/operations/configuration.html +6 -6
- package/dist/ui/operations/jobs.html +6 -6
- package/dist/ui/operations/maintenance.html +6 -6
- package/dist/ui/operations/observability.html +6 -6
- package/dist/ui/operations/previews.html +6 -6
- package/dist/ui/operations/queue.html +6 -6
- package/dist/ui/operations/regions.html +6 -6
- package/dist/ui/operations/releases.html +6 -6
- package/dist/ui/operations/spend.html +6 -6
- package/dist/ui/operations/workloads.html +6 -6
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +6 -6
- package/dist/ui/server/activity.html +1 -1
- package/dist/ui/server/capacity.html +5 -5
- package/dist/ui/server/database.html +5 -5
- package/dist/ui/server/deployments.html +5 -5
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +6 -6
- package/dist/ui/server/fleet.html +6 -6
- package/dist/ui/server/logs.html +5 -5
- package/dist/ui/server/metrics.html +5 -5
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/services.html +2 -2
- package/dist/ui/server/sites.html +6 -6
- package/dist/ui/server/ssh-keys.html +5 -5
- package/dist/ui/server/team.html +6 -6
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +6 -6
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +5 -5
- package/dist/ui/serverless/deployments.html +2 -2
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +6 -6
- package/dist/ui/serverless/logs.html +6 -6
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +6 -6
- package/dist/ui/serverless/secrets.html +5 -5
- package/dist/ui/serverless/traces.html +6 -6
- package/dist/ui/serverless.html +6 -6
- package/package.json +7 -3
- package/dist/chunk-a84gmpyh.js +0 -1
- package/dist/chunk-nms5q44e.js +0 -1
- package/dist/chunk-qm32ytbr.js +0 -1
- package/dist/chunk-t3t9ymec.js +0 -1
- package/dist/chunk-y2v22hkv.js +0 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet operations: plan-then-apply changes to live topology.
|
|
3
|
+
*
|
|
4
|
+
* Consolidating servers - moving an app to another box, attaching one as a
|
|
5
|
+
* site, renaming a box, tearing a drained one down - is a routine cleanup that
|
|
6
|
+
* is otherwise an afternoon of SSH. Every operation here is expressed as an
|
|
7
|
+
* {@link OperationPlan}: it says what it would change before touching anything,
|
|
8
|
+
* each step asks reality whether it is already satisfied so a run that died
|
|
9
|
+
* halfway resumes by being run again, irreversible steps are gated on typed
|
|
10
|
+
* confirmation, and nothing reads stdin so the whole sequence drives from CI.
|
|
11
|
+
*
|
|
12
|
+
* These modules existed for several releases without being exported, so they
|
|
13
|
+
* type-checked on import and threw at runtime. This barrel is what makes them
|
|
14
|
+
* reachable.
|
|
15
|
+
*
|
|
16
|
+
* @see https://github.com/stacksjs/ts-cloud/issues/167
|
|
17
|
+
* @see https://github.com/stacksjs/ts-cloud/issues/191
|
|
18
|
+
*/
|
|
19
|
+
export * from './drained-sites';
|
|
20
|
+
export * from './inventory';
|
|
21
|
+
export * from './plan';
|
|
22
|
+
export * from './server-rename';
|
|
23
|
+
export * from './site-attach';
|
|
24
|
+
export * from './site-move';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$i as C,Ai as b,Bi as c,Ci as d,Di as e,Ei as f,Fi as g,Gi as h,Hi as i,Ii as j,Ji as k,Ki as l,Li as m,Mi as n,Ni as o,Oi as p,Pi as q,Qi as r,Ri as s,Si as t,Ti as u,Ui as v,Vi as w,Wi as x,Xi as y,Yi as z,Zi as A,_i as B,aj as D,bj as E,cj as F,dj as G,ej as H,fj as I,gj as J,hj as K,ij as L,jj as M,kj as N,lj as O,mj as P,nj as Q,oj as R,pj as S,qj as T,zi as a}from"../chunk-qpjcd415.js";import"../chunk-2qm3rw8b.js";import"../chunk-c1hr34a5.js";import"../chunk-fsssdd5k.js";import"../chunk-0hw5wjjg.js";export{e as ENVIRONMENT_LABEL,d as PROJECT_LABEL,f as ROLE_LABEL,r as applyPlan,x as attachConflicts,y as attachIsViable,w as attachPreconditions,R as buildCertificatePackScript,O as buildCertificateStateScript,S as buildCertificateUnpackScript,K as buildDrainSourceScript,a as buildDrainedSiteScanScript,J as buildHealthGateScript,C as buildPauseWorkersScript,G as buildRestoreScript,u as buildSetHostnameScript,F as buildSnapshotScript,L as buildSourceStateScript,H as buildTargetStateScript,D as buildWorkersStateScript,Q as certificatesMatch,z as formatAttachPlan,c as formatDrainedSiteRefusal,m as formatInventory,q as formatPlan,P as parseCertificateState,b as parseDrainedSites,M as parseSourceDrained,I as parseTargetReady,E as parseWorkersPaused,o as pendingSteps,p as planIsDestructive,t as planServerRename,T as planSiteMove,j as probeHostRoutes,k as reconcile,v as resolveAttachTarget,n as resolvePlan,h as routesFromFragments,A as siteMoveArchivePath,N as siteMoveCertArchivePath,B as siteUnitGlob,i as tenantsOf,g as toInventoryServer,l as unaccountedSites,s as validateServerName};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is hosted on which box, across every project sharing it.
|
|
3
|
+
*
|
|
4
|
+
* Consolidating servers starts with a question nothing could answer: what is
|
|
5
|
+
* actually running on each one? A project's own config cannot answer it. It
|
|
6
|
+
* describes ONE project's sites, and the boxes are multi-tenant - other
|
|
7
|
+
* projects deploy onto them with `cloud.attachTo`, from their own repositories,
|
|
8
|
+
* and appear in no file the first project owns. Reading config and calling it
|
|
9
|
+
* an inventory reports a shared box as if one project were alone on it, which
|
|
10
|
+
* is precisely the wrong answer to consolidate against.
|
|
11
|
+
*
|
|
12
|
+
* So the answer comes from the box. Each project's deploy writes an rpx
|
|
13
|
+
* registry fragment into `HOST_SITES_DIR`, and those files together are the
|
|
14
|
+
* only complete record of what the host serves and for whom. `site-ports`
|
|
15
|
+
* already reads them to allocate ports around co-tenants; this module reads
|
|
16
|
+
* the same files for the routes rather than the ports, so the two can never
|
|
17
|
+
* disagree about what is on a box.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is pure except {@link probeHostRoutes}, which takes its exec
|
|
20
|
+
* as an argument - the same shape `site-move` uses, so an inventory can be
|
|
21
|
+
* tested without a box, a provider, or a credential.
|
|
22
|
+
*
|
|
23
|
+
* @see https://github.com/stacksjs/ts-cloud/issues/167
|
|
24
|
+
* @see https://github.com/stacksjs/stacks/issues/2342
|
|
25
|
+
*/
|
|
26
|
+
import type { HostSiteFragment } from '../deploy/site-ports';
|
|
27
|
+
/** The ts-cloud labels every provisioned box carries. */
|
|
28
|
+
export declare const PROJECT_LABEL = "ts-cloud/project";
|
|
29
|
+
export declare const ENVIRONMENT_LABEL = "ts-cloud/environment";
|
|
30
|
+
export declare const ROLE_LABEL = "ts-cloud/role";
|
|
31
|
+
/**
|
|
32
|
+
* A server in the inventory, reduced to what deciding a consolidation needs.
|
|
33
|
+
*
|
|
34
|
+
* Structural and provider-agnostic on purpose: a Hetzner server satisfies it
|
|
35
|
+
* after {@link toInventoryServer}, and another driver can satisfy it without
|
|
36
|
+
* importing anything from here.
|
|
37
|
+
*/
|
|
38
|
+
export interface InventoryServer {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
status: string;
|
|
42
|
+
ipv4?: string;
|
|
43
|
+
ipv6?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
location?: string;
|
|
46
|
+
labels: Record<string, string>;
|
|
47
|
+
/** `ts-cloud/project`: the project that provisioned and owns this box. */
|
|
48
|
+
project?: string;
|
|
49
|
+
/** `ts-cloud/environment`: production, staging, ... */
|
|
50
|
+
environment?: string;
|
|
51
|
+
/** `ts-cloud/role`: app, services, lb. */
|
|
52
|
+
role?: string;
|
|
53
|
+
}
|
|
54
|
+
/** One route a box serves, and the project whose fragment declared it. */
|
|
55
|
+
export interface HostedRoute {
|
|
56
|
+
slug: string;
|
|
57
|
+
host: string;
|
|
58
|
+
path: string;
|
|
59
|
+
/** Where the route goes: an upstream, a served directory, or a redirect target. */
|
|
60
|
+
target: string;
|
|
61
|
+
kind: 'app' | 'static' | 'redirect' | 'unknown';
|
|
62
|
+
}
|
|
63
|
+
/** What a box answered when asked what it serves. */
|
|
64
|
+
export interface HostProbe {
|
|
65
|
+
server: string;
|
|
66
|
+
ip?: string;
|
|
67
|
+
routes: HostedRoute[];
|
|
68
|
+
/** Why the probe produced nothing, when it produced nothing. */
|
|
69
|
+
unavailable?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Shape one provider server record into {@link InventoryServer}.
|
|
73
|
+
*
|
|
74
|
+
* Written against the Hetzner payload, but touches only fields any provider
|
|
75
|
+
* listing carries, so a driver with a different shape maps onto the same type
|
|
76
|
+
* rather than forcing a second one.
|
|
77
|
+
*
|
|
78
|
+
* An unlabelled box is kept rather than dropped. A server provisioned by hand,
|
|
79
|
+
* or by a ts-cloud old enough not to have labelled it, is exactly the kind a
|
|
80
|
+
* consolidation needs to see; requiring the labels would hide it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function toInventoryServer(raw: any): InventoryServer;
|
|
83
|
+
/**
|
|
84
|
+
* Flatten registry fragments into one route list, sorted so two runs against an
|
|
85
|
+
* unchanged box produce an identical listing.
|
|
86
|
+
*/
|
|
87
|
+
export declare function routesFromFragments(fragments: readonly HostSiteFragment[]): HostedRoute[];
|
|
88
|
+
/** Group routes by the project that owns them, biggest tenant first. */
|
|
89
|
+
export declare function tenantsOf(routes: readonly HostedRoute[]): Array<{
|
|
90
|
+
slug: string;
|
|
91
|
+
routes: HostedRoute[];
|
|
92
|
+
}>;
|
|
93
|
+
/** Run a script on a host, resolving with its exit code and output. */
|
|
94
|
+
export type InventoryExec = (host: string, command: string) => Promise<{
|
|
95
|
+
code: number;
|
|
96
|
+
stdout: string;
|
|
97
|
+
stderr: string;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Ask one box what it serves.
|
|
101
|
+
*
|
|
102
|
+
* Never throws. A box that is off, unreachable, or refuses the key comes back
|
|
103
|
+
* with an `unavailable` reason instead, so one bad server does not cost the
|
|
104
|
+
* listing of every other one - and so a caller can tell "serves nothing" apart
|
|
105
|
+
* from "was never asked", which are the two answers most worth not confusing.
|
|
106
|
+
*/
|
|
107
|
+
export declare function probeHostRoutes(server: InventoryServer, exec: InventoryExec, sitesDir?: string): Promise<HostProbe>;
|
|
108
|
+
/** One site a project declares, in the terms the box reports. */
|
|
109
|
+
export interface DeclaredSite {
|
|
110
|
+
name: string;
|
|
111
|
+
kind?: string;
|
|
112
|
+
domain?: string;
|
|
113
|
+
path: string;
|
|
114
|
+
port?: number;
|
|
115
|
+
/** `siteInstallBase(slug, name)`, when the caller resolved it. */
|
|
116
|
+
installBase?: string;
|
|
117
|
+
/**
|
|
118
|
+
* No `domain`, so the gateway never routes it and it cannot appear in a
|
|
119
|
+
* registry fragment. That is a deliberate configuration - a loopback-only
|
|
120
|
+
* service reached through another site's proxy - not a missing deploy.
|
|
121
|
+
*/
|
|
122
|
+
loopbackOnly: boolean;
|
|
123
|
+
}
|
|
124
|
+
/** Declared sites lined up against what one box actually serves. */
|
|
125
|
+
export interface Reconciliation {
|
|
126
|
+
/** Declared and present on the box. */
|
|
127
|
+
present: DeclaredSite[];
|
|
128
|
+
/** Declared, routable, and absent from the box's registry. */
|
|
129
|
+
absent: DeclaredSite[];
|
|
130
|
+
/** Declared with no domain: no gateway route by design. */
|
|
131
|
+
loopback: DeclaredSite[];
|
|
132
|
+
/** Routes on the box owned by some other project. */
|
|
133
|
+
foreign: HostedRoute[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Line a project's declared sites up against what a box serves.
|
|
137
|
+
*
|
|
138
|
+
* Matched on host and path rather than on the site key, because the site key is
|
|
139
|
+
* local to a repository and the box has no idea what it is. Two projects both
|
|
140
|
+
* calling a site `main` is ordinary; two projects serving the same host and
|
|
141
|
+
* path is the collision worth seeing.
|
|
142
|
+
*/
|
|
143
|
+
export declare function reconcile(declared: readonly DeclaredSite[], routes: readonly HostedRoute[], slug: string): Reconciliation;
|
|
144
|
+
/**
|
|
145
|
+
* Sites no probed box accounts for.
|
|
146
|
+
*
|
|
147
|
+
* Callers must pass only the probes that ANSWERED. A box that could not be read
|
|
148
|
+
* proves nothing about what it holds, and counting it would report every site
|
|
149
|
+
* on it as missing - a true statement about the listing and a false one about
|
|
150
|
+
* the deployment.
|
|
151
|
+
*/
|
|
152
|
+
export declare function unaccountedSites(declared: readonly DeclaredSite[], answered: readonly HostProbe[], slug: string): DeclaredSite[];
|
|
153
|
+
export interface Inventory {
|
|
154
|
+
slug: string;
|
|
155
|
+
servers: InventoryServer[];
|
|
156
|
+
probes: HostProbe[];
|
|
157
|
+
declared: DeclaredSite[];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The inventory as lines, ready to print.
|
|
161
|
+
*
|
|
162
|
+
* Lines rather than printed output, matching {@link import('./plan').formatPlan}:
|
|
163
|
+
* the caller owns the stream, and the format stays testable. Every count is
|
|
164
|
+
* stated so a partial answer reads as partial - a box that could not be read
|
|
165
|
+
* says so on its own line, and the summary separates "not deployed" from "not
|
|
166
|
+
* visible from here".
|
|
167
|
+
*/
|
|
168
|
+
export declare function formatInventory(inventory: Inventory): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a project may safely attach to a server another project owns.
|
|
3
|
+
*
|
|
4
|
+
* `cloud.attachTo` already works: the attaching project's deploy puts its sites
|
|
5
|
+
* on the owner's box instead of provisioning one. What has never existed is the
|
|
6
|
+
* check BEFORE that deploy.
|
|
7
|
+
*
|
|
8
|
+
* Every way an attach goes wrong is currently discovered while it is going
|
|
9
|
+
* wrong. `validateDeploymentConfig` only ever sees one project's `sites`, so a
|
|
10
|
+
* second attach passes it; the port guard that would catch the clash runs from
|
|
11
|
+
* inside the deploy, after the operator has committed a config change and
|
|
12
|
+
* started shipping. And the worst case does not error at all: ts-cloud's units
|
|
13
|
+
* do not set exclusive binding, so two services on one port both bind and the
|
|
14
|
+
* kernel load-balances between them. Both look healthy, nothing is logged, and
|
|
15
|
+
* each domain serves the other project's site about half the time - which is
|
|
16
|
+
* what happened to predicthq.org for a day and a half.
|
|
17
|
+
*
|
|
18
|
+
* The checks here are the same ones the deploy makes, moved to before anything
|
|
19
|
+
* is written or shipped, and answered from the box's own registry rather than
|
|
20
|
+
* from any project's config.
|
|
21
|
+
*
|
|
22
|
+
* @see https://github.com/stacksjs/ts-cloud/issues/167
|
|
23
|
+
* @see https://github.com/stacksjs/ts-cloud/issues/168
|
|
24
|
+
* @see https://github.com/stacksjs/stacks/issues/2342
|
|
25
|
+
*/
|
|
26
|
+
import type { DeclaredSite, HostedRoute, InventoryServer } from './inventory';
|
|
27
|
+
/** The server an attach would target, or why one could not be picked. */
|
|
28
|
+
export type AttachTarget = {
|
|
29
|
+
server: InventoryServer;
|
|
30
|
+
} | {
|
|
31
|
+
problem: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Pick the box named by the caller, by provider name or by owning project.
|
|
35
|
+
*
|
|
36
|
+
* Both spellings are accepted because both are what an operator has in front of
|
|
37
|
+
* them: the provider console shows `stacks-production-app`, while `attachTo`
|
|
38
|
+
* takes the owner's slug (`stacks`). Making them translate between the two is
|
|
39
|
+
* how the wrong box gets named. An ambiguous match refuses rather than picking.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveAttachTarget(servers: readonly InventoryServer[], wanted: string, environment?: string): AttachTarget;
|
|
42
|
+
/**
|
|
43
|
+
* Reasons an attach must not proceed at all, independent of what is on the box.
|
|
44
|
+
*
|
|
45
|
+
* Separate from conflicts because these are about identity rather than
|
|
46
|
+
* occupancy: no amount of moving ports would make any of them safe.
|
|
47
|
+
*/
|
|
48
|
+
export declare function attachPreconditions(slug: string, server: InventoryServer): string[];
|
|
49
|
+
export interface AttachConflict {
|
|
50
|
+
kind: 'port' | 'route';
|
|
51
|
+
site: string;
|
|
52
|
+
detail: string;
|
|
53
|
+
/** The slug already holding it. */
|
|
54
|
+
heldBy: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Where this project's sites would land on top of another project's.
|
|
58
|
+
*
|
|
59
|
+
* Two independent collisions, and the port one is the dangerous half: a route
|
|
60
|
+
* clash produces a visibly wrong page, while a port clash produces a working
|
|
61
|
+
* box that serves the wrong site to about half its visitors with nothing
|
|
62
|
+
* logged as an error.
|
|
63
|
+
*
|
|
64
|
+
* Ports come from {@link occupiedHostPorts} rather than from the routes
|
|
65
|
+
* directly, so the collision check and the port allocator can never disagree
|
|
66
|
+
* about which ports are taken. Our own slug is ignored on both axes: a
|
|
67
|
+
* re-attach finds its own fragment on the box from the last deploy, and
|
|
68
|
+
* counting it would make every repeat run conflict with itself.
|
|
69
|
+
*/
|
|
70
|
+
export declare function attachConflicts(slug: string, declared: readonly DeclaredSite[], routes: readonly HostedRoute[]): AttachConflict[];
|
|
71
|
+
export interface AttachPlan {
|
|
72
|
+
/** The attaching project. */
|
|
73
|
+
slug: string;
|
|
74
|
+
/** The project that owns the box. */
|
|
75
|
+
owner: string;
|
|
76
|
+
server: InventoryServer;
|
|
77
|
+
declared: readonly DeclaredSite[];
|
|
78
|
+
conflicts: readonly AttachConflict[];
|
|
79
|
+
/** Was the box's registry actually read? A check that saw nothing proves nothing. */
|
|
80
|
+
registryRead: boolean;
|
|
81
|
+
/** Why the registry could not be read, when it could not. */
|
|
82
|
+
registryProblem?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Is this attach safe to carry out? */
|
|
85
|
+
export declare function attachIsViable(plan: AttachPlan): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The attach as lines, ready to print.
|
|
88
|
+
*
|
|
89
|
+
* Deliberately does not describe the config edits an attach needs. Those live
|
|
90
|
+
* in two different repositories - `attachTo` here, the tenant's slug in the
|
|
91
|
+
* owner's `tenants` - so which of them a given caller can make is the caller's
|
|
92
|
+
* business, and printing "these edits make it real" underneath a refusal reads
|
|
93
|
+
* as though the operation is going ahead.
|
|
94
|
+
*/
|
|
95
|
+
export declare function formatAttachPlan(plan: AttachPlan): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/protection/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{a as y,b as z,c as A,d as B,e as C,f as D,g as E,h as F,i as G,j as H,k as I,l as J,m as K,n as L,o as M}from"../chunk-w2ddj42w.js";import{ki as s,li as t,mi as u,ni as v,oi as w,pi as x}from"../chunk-bcffjsnj.js";import{Fm as k,Gm as l,Hm as m,Im as n,Jm as o,Km as p,Lm as q,Mm as r}from"../chunk-pb47hr0h.js";import{$t as c,Zt as a,_t as b,au as d,bu as e,cu as f,du as g,eu as h,fu as i,gu as j}from"../chunk-fsssdd5k.js";import"../chunk-0hw5wjjg.js";export{b as CHAIN_HEADER,H as DEFAULT_ATTACK_MODE_HOURS,k as DEFAULT_DDOS_THRESHOLDS,e as DEFAULT_RECURSION_LIMITS,a as DEPTH_HEADER,G as MAX_CONTROL_HOURS,F as PROTECTION_SETTING,I as ProtectionControlStore,y as RATE_LIMIT_KEY_SEPARATOR,C as RateLimiter,h as RecursionGuard,c as TRACE_HEADER,L as applyControlsToDdos,K as applyControlsToRateLimits,r as applyMitigationFactor,E as defaultRateLimitRules,J as describePosture,s as escapeSecLang,d as functionFingerprint,z as globMatches,u as globToRegex,g as inspectInvocation,t as isSafePattern,l as isValidCidr,M as mergeControlsIntoConfig,f as parseChain,q as planMitigation,i as propagationHeaders,D as rateLimitHeaders,B as rateLimitKey,j as recursionBlockedResponse,p as renderDdosInstallScript,o as renderNftablesRuleset,n as renderSysctlFile,v as renderWafConfig,w as renderWafInstallScript,A as ruleMatches,m as sysctlHardening,x as wafEventTelemetry};
|
package/dist/push/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../chunk-
|
|
1
|
+
import"../chunk-0hw5wjjg.js";import*as h from"node:http2";import{createSign as b}from"node:crypto";var f="api.push.apple.com",k="api.sandbox.push.apple.com",S=2700000;class R{config;token=null;tokenGeneratedAt=0;client=null;host;constructor(e){this.config=e,this.host=e.production?f:k}generateToken(){let e=Math.floor(Date.now()/1000);if(this.token&&Date.now()-this.tokenGeneratedAt<S)return this.token;let t={alg:"ES256",kid:this.config.keyId},s={iss:this.config.teamId,iat:e},r=Buffer.from(JSON.stringify(t)).toString("base64url"),n=Buffer.from(JSON.stringify(s)).toString("base64url"),i=`${r}.${n}`,o=b("SHA256");o.update(i);let a=o.sign(this.config.privateKey),l=this.derToRaw(a).toString("base64url");return this.token=`${i}.${l}`,this.tokenGeneratedAt=Date.now(),this.token}derToRaw(e){let t=2;if(e[t]!==2)throw Error("Invalid DER signature: expected 0x02 for R");t++;let s=e[t];t++;let r=e.subarray(t,t+s);if(t+=s,e[t]!==2)throw Error("Invalid DER signature: expected 0x02 for S");t++;let n=e[t];t++;let i=e.subarray(t,t+n);if(r[0]===0&&r.length===33)r=r.subarray(1);if(i[0]===0&&i.length===33)i=i.subarray(1);let o=Buffer.alloc(64);return r.copy(o,32-r.length),i.copy(o,64-i.length),o}async getClient(){if(this.client&&!this.client.destroyed)return this.client;return new Promise((e,t)=>{this.client=h.connect(`https://${this.host}`),this.client.on("error",(s)=>{t(s)}),this.client.on("connect",()=>{e(this.client)}),this.client.on("close",()=>{this.client=null})})}buildPayload(e){let t={};if(e.title||e.body||e.subtitle){if(t.alert={},e.title)t.alert.title=e.title;if(e.subtitle)t.alert.subtitle=e.subtitle;if(e.body)t.alert.body=e.body}if(e.badge!==void 0)t.badge=e.badge;if(e.sound!==void 0)t.sound=e.sound;if(e.category)t.category=e.category;if(e.threadId)t["thread-id"]=e.threadId;if(e.contentAvailable)t["content-available"]=1;if(e.mutableContent)t["mutable-content"]=1;if(e.targetContentId)t["target-content-id"]=e.targetContentId;let s={aps:t};if(e.data)Object.assign(s,e.data);return s}async send(e){try{let t=await this.getClient(),s=this.generateToken(),r=JSON.stringify(this.buildPayload(e)),n={":method":"POST",":path":`/3/device/${e.deviceToken}`,authorization:`bearer ${s}`,"apns-topic":this.config.bundleId,"apns-push-type":e.pushType||"alert","apns-priority":String(e.priority||10)};if(e.expiration!==void 0)n["apns-expiration"]=String(e.expiration);if(e.collapseId)n["apns-collapse-id"]=e.collapseId;return new Promise((i)=>{let o=t.request(n),a="",d=0,l;o.on("response",(c)=>{d=c[":status"],l=c["apns-id"]}),o.on("data",(c)=>{a+=c.toString()}),o.on("end",()=>{if(d===200)i({success:!0,deviceToken:e.deviceToken,apnsId:l,statusCode:d});else{let c="Unknown error",g,u;if(a)try{let p=JSON.parse(a);g=p.reason,u=p.timestamp,c=g||c}catch{c=a}i({success:!1,deviceToken:e.deviceToken,apnsId:l,statusCode:d,error:c,reason:g,timestamp:u})}}),o.on("error",(c)=>{i({success:!1,deviceToken:e.deviceToken,error:c.message})}),o.write(r),o.end()})}catch(t){return{success:!1,deviceToken:e.deviceToken,error:t.message}}}async sendBatch(e,t){let s=t?.concurrency||10,r=[];for(let n=0;n<e.length;n+=s){let i=e.slice(n,n+s),o=await Promise.all(i.map((a)=>this.send(a)));r.push(...o)}return{sent:r.filter((n)=>n.success).length,failed:r.filter((n)=>!n.success).length,results:r}}async sendSimple(e,t,s,r){return this.send({deviceToken:e,title:t,body:s,data:r})}async sendSilent(e,t){return this.send({deviceToken:e,contentAvailable:!0,pushType:"background",priority:5,data:t})}close(){if(this.client)this.client.close(),this.client=null;this.token=null,this.tokenGeneratedAt=0}}import{createSign as T}from"node:crypto";var C="https://fcm.googleapis.com/v1/projects",y="https://oauth2.googleapis.com/token",P=3300000;class m{config;accessToken=null;tokenExpiresAt=0;constructor(e){this.config=e}static fromServiceAccount(e){return new m({projectId:e.project_id,clientEmail:e.client_email,privateKey:e.private_key})}generateJWT(){let e=Math.floor(Date.now()/1000),t=e+3600,s={alg:"RS256",typ:"JWT"},r={iss:this.config.clientEmail,sub:this.config.clientEmail,aud:y,iat:e,exp:t,scope:"https://www.googleapis.com/auth/firebase.messaging"},n=Buffer.from(JSON.stringify(s)).toString("base64url"),i=Buffer.from(JSON.stringify(r)).toString("base64url"),o=`${n}.${i}`,a=T("SHA256");a.update(o);let d=a.sign(this.config.privateKey,"base64url");return`${o}.${d}`}async getAccessToken(){if(this.accessToken&&Date.now()<this.tokenExpiresAt)return this.accessToken;let e=this.generateJWT(),t=await fetch(y,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",assertion:e}).toString()});if(!t.ok){let r=await t.text();throw Error(`Failed to get access token: ${r}`)}let s=await t.json();return this.accessToken=s.access_token,this.tokenExpiresAt=Date.now()+P,this.accessToken}buildMessage(e){let t={};if(e.token)t.token=e.token;else if(e.topic)t.topic=e.topic;else if(e.condition)t.condition=e.condition;if(e.title||e.body||e.imageUrl){if(t.notification={},e.title)t.notification.title=e.title;if(e.body)t.notification.body=e.body;if(e.imageUrl)t.notification.image=e.imageUrl}if(e.data&&Object.keys(e.data).length>0)t.data=e.data;if(e.android){if(t.android={priority:e.android.priority||"high"},e.android.ttl)t.android.ttl=`${e.android.ttl}s`;if(e.android.collapseKey)t.android.collapse_key=e.android.collapseKey;if(e.android.directBootOk)t.android.direct_boot_ok=e.android.directBootOk;let s={};if(e.android.channelId)s.channel_id=e.android.channelId;if(e.android.icon)s.icon=e.android.icon;if(e.android.color)s.color=e.android.color;if(e.android.sound)s.sound=e.android.sound;if(e.android.clickAction)s.click_action=e.android.clickAction;if(e.android.tag)s.tag=e.android.tag;if(e.android.visibility)s.visibility=e.android.visibility;if(e.android.notificationCount!==void 0)s.notification_count=e.android.notificationCount;if(Object.keys(s).length>0)t.android.notification=s}if(e.webpush)t.webpush=e.webpush;if(e.apns)t.apns=e.apns;if(e.fcmOptions)t.fcm_options=e.fcmOptions;return{message:t}}async send(e){try{let t=await this.getAccessToken(),s=this.buildMessage(e),r=await fetch(`${C}/${this.config.projectId}/messages:send`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify(s)}),n=await r.json();if(r.ok)return{success:!0,messageId:n.name};else return{success:!1,error:n.error?.message||"Unknown error",errorCode:n.error?.status}}catch(t){return{success:!1,error:t.message}}}async sendBatch(e,t,s){let r=s?.concurrency||10,n=[];for(let i=0;i<e.length;i+=r){let a=e.slice(i,i+r).map(async(l)=>({...await this.send({...t,token:l}),token:l})),d=await Promise.all(a);n.push(...d)}return{sent:n.filter((i)=>i.success).length,failed:n.filter((i)=>!i.success).length,results:n}}async sendToTopic(e,t){return this.send({...t,topic:e})}async sendToCondition(e,t){return this.send({...t,condition:e})}async sendSimple(e,t,s,r){return this.send({token:e,title:t,body:s,data:r})}async sendSilent(e,t){return this.send({token:e,data:t,android:{priority:"high"}})}async subscribeToTopic(e,t){try{let s=await this.getAccessToken(),r=await fetch("https://iid.googleapis.com/iid/v1:batchAdd",{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${t}`,registration_tokens:e})});if(r.ok)return{success:!0};else return{success:!1,error:(await r.json()).error?.message||"Failed to subscribe"}}catch(s){return{success:!1,error:s.message}}}async unsubscribeFromTopic(e,t){try{let s=await this.getAccessToken(),r=await fetch("https://iid.googleapis.com/iid/v1:batchRemove",{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${t}`,registration_tokens:e})});if(r.ok)return{success:!0};else return{success:!1,error:(await r.json()).error?.message||"Failed to unsubscribe"}}catch(s){return{success:!1,error:s.message}}}}export{R as APNsClient,m as FCMClient};
|
package/dist/spend/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$
|
|
1
|
+
import{$k as t,Al as U,Bl as V,Cl as W,Dl as X,Dm as Ca,El as Y,Em as Da,Fl as Z,Gl as _,Hl as aa,Ik as a,Il as ba,Jk as b,Jl as ca,Kk as c,Kl as da,Lk as d,Ll as ea,Mk as e,Ml as fa,Nk as f,Nl as ga,Ok as g,Ol as ha,Pk as h,Pl as ia,Qk as i,Ql as ja,Rk as j,Rl as ka,Sk as k,Sl as la,Tk as l,Tl as ma,Uk as m,Ul as na,Vk as n,Vl as oa,Wk as o,Wl as pa,Xk as p,Yk as q,Zk as r,_k as s,al as u,bl as v,cl as w,dl as x,el as y,fl as z,gl as A,hl as B,il as C,jl as D,kl as E,ll as F,ml as G,nl as H,ol as I,om as qa,pl as J,pm as ra,ql as K,qm as sa,rl as L,rm as ta,sl as M,sm as ua,tl as N,tm as va,ul as O,um as wa,vl as P,vm as xa,wl as Q,wm as ya,xl as R,xm as za,yl as S,ym as Aa,zl as T,zm as Ba}from"../chunk-5hkq5081.js";import"../chunk-pb47hr0h.js";import"../chunk-fsssdd5k.js";import"../chunk-0hw5wjjg.js";export{Z as AnomalyConfigStore,za as AwsSpendTransport,Aa as ComputeSpendTransport,x as CounterTracker,u as DEFAULT_METER_MAPPINGS,l as DEFAULT_PRICE_BOOK,B as DEFAULT_THRESHOLDS,_ as DETECTABLE_SIGNALS,c as ENFORCEMENT_ACTIONS,K as ENFORCEMENT_DESCRIPTIONS,d as ENFORCEMENT_SEVERITY,ea as FOCUS_VERSION,b as METERS,a as METER_KEYS,e as MICRO_CENTS,G as MIN_PROJECTION_CONFIDENCE,pa as RecordingSpendTransport,X as SENSITIVITY_THRESHOLDS,xa as SPEND_CYCLE_SECONDS,ka as SPEND_GATE_SETTING,ua as SPEND_LEASE_SETTING,ba as SignalSource,ma as SpendCapError,la as SpendGate,va as SpendLoopLease,ta as SpendNotificationRouter,wa as SpendRunner,ia as SpendService,C as SpendStore,qa as URGENT_SPEND_EVENTS,L as USER_VISIBLE_ACTIONS,z as aggregateDeltas,W as anomalyOptionsForSignal,na as assertSpendAllows,q as budgetWindow,Ba as compositeSpendTransport,oa as createEnforcementHandlers,U as detectAnomalies,V as detectLatestAnomaly,I as evaluateBudget,T as ewma,v as findMapping,g as findPriceEntry,ra as formatCents,F as formatTimeToExhaustion,H as governingLimitCents,Ca as handleSpendRequest,s as hourBucket,t as hourBucketsBetween,P as isOperationAllowed,m as isValidTimeZone,da as lookbackHoursForSignal,Q as median,R as medianAbsoluteDeviation,J as mergeDecisions,k as mergePriceBooks,y as meterTelemetry,ca as optionsForSignal,E as percentOfLimit,M as planEnforcement,r as previousBudgetWindow,h as priceUsage,i as priceUsageLines,D as projectSpend,S as robustZScore,j as roundCents,Y as routeMatches,N as runEnforcement,A as scopeKey,aa as signalDefinition,sa as spendNotificationText,Da as spendOpenApiPaths,ya as startSpendLoop,ha as streamFocusJsonl,O as strongestActiveAction,f as tieredMicroCents,ga as toFocusJsonl,fa as toFocusRecords,w as usageDeltaKey,p as utcInstantOfLocal,ja as zeroFill,o as zoneOffsetMs,n as zonedParts};
|
package/dist/ui/404.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="
|
|
1
|
+
<!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="mtekc2s63mrzwt"><meta name="title" content="stx Project"><meta name="description" content="A website built with stx templating engine"><meta property="og:title" content="stx Project"><meta property="og:description" content="A website built with stx templating engine"><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="stx Project"><meta name="twitter:description" content="A website built with stx templating engine"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ts-cloud - Accept invitation</title><link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='3' y='3' width='26' height='26' rx='8' fill='%235a8be0'/%3E%3C/svg%3E"><link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_stx/style.448ab192.css"><style data-stx-cloak>[x-cloak]{display:none !important}</style><link data-crosswind="generated" rel="stylesheet" href="/_stx/crosswind.616c25f3.css"></head><body class="auth-page"><main class="auth-card" id="main-content"><a class="brand" href="/"><span class="dot"></span><span>ts-cloud</span></a><div><span class="context-label">Organization invitation</span><h1>Join your team</h1><p>Choose the dashboard username and password you will use after this invitation is accepted.</p></div><form id="accept-invitation-form" class="auth-form"><div class="field-block"><label for="invite-name">Name</label><input id="invite-name" name="name" autocomplete="name" placeholder="Chris Breuer"></div><div class="field-block"><label for="invite-username">Username</label><input id="invite-username" name="username" autocomplete="username" minlength="2" maxlength="32" required></div><div class="field-block"><label for="invite-password">Password</label><input id="invite-password" name="password" type="password" autocomplete="new-password" minlength="12" required><small>At least 12 characters.</small></div><button class="btn" type="submit">Accept invitation</button><div id="invitation-status" class="auth-status" role="status"></div></form><p class="note">If this link expired or was already used, ask an organization owner to resend the invitation.</p></main> <script data-stx-scoped data-stx-run="always">
|
|
2
2
|
;(function() {
|
|
3
3
|
'use strict';
|
|
4
4
|
(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="
|
|
1
|
+
<!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="mtekc2s63mrzwt"><meta name="title" content="Access unavailable · ts-cloud"><meta name="description" content="A website built with stx templating engine"><meta property="og:title" content="Access unavailable · ts-cloud"><meta property="og:description" content="A website built with stx templating engine"><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="Access unavailable · ts-cloud"><meta name="twitter:description" content="A website built with stx templating engine"> <script data-stx-runtime src="/_stx/runtime.dbee66d1.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Access unavailable · ts-cloud</title><link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='3' y='3' width='26' height='26' rx='8' fill='%235a8be0'/%3E%3C/svg%3E"><link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_stx/style.448ab192.css"><style data-stx-cloak>[x-cloak]{display:none !important}</style><link data-crosswind="generated" rel="stylesheet" href="/_stx/crosswind.906ee17c.css"></head><body data-stx-auto><a data-stx-scope="stx_scope_nav_50" class="skip-link" href="#main-content">Skip to content</a><button class="nav-toggle-button" type="button" aria-label="Open navigation" aria-controls="primary-navigation" aria-expanded="false"><span></span><span></span><span></span></button><button class="nav-scrim" type="button" aria-label="Close navigation"></button><aside class="app-sidebar" id="primary-navigation" aria-label="Primary navigation"><a class="brand" href="/?env=production"><span class="dot"></span><span>ts-cloud</span></a><div class="sidebar-context"><span class="context-label">Project</span><strong>ts-cloud</strong></div><div class="environment-picker" aria-label="Environment"><span class="context-label">Environment</span><details><summary class="environment-current production"><span>PROD</span><strong>production</strong></summary></details></div><button class="command-trigger" type="button" aria-keyshortcuts="Meta+K Control+K"><span>Search resources</span><kbd>⌘ K</kbd></button><div class="nav-groups"><section class="nav-group"><h2>Overview</h2><a class="" href="/?env=production">Dashboard</a><a href="/server/activity?env=production">Activity</a></section><section class="nav-group"><h2>Applications</h2><a class="" href="/server/sites?env=production">Services</a><a class="" href="/server/deployments?env=production">Deployments</a><a class="" href="/server/logs?env=production">Logs</a><a class="" href="/integrations?env=production">Git integrations</a><a class="" href="/applications/new?env=production">Create application</a><a class="" href="/applications/compose?env=production">Compose applications</a></section><section class="nav-group"><h2>Operations</h2><a class="" href="/operations/queue?env=production">Deployment queue</a><a class="" href="/operations/previews?env=production">Preview environments</a><a class="" href="/operations/releases?env=production">Releases & promotion</a><a class="" href="/operations/regions?env=production">Regions & failover</a><a class="" href="/operations/maintenance?env=production">Maintenance & DR</a><a class="" href="/operations/workloads?env=production">Workloads</a><a class="" href="/operations/observability?env=production">Observability</a><a class="" href="/operations/alerts?env=production">Alerts & health</a><a class="" href="/operations/spend?env=production">Spend & budgets</a><a href="/server/metrics?env=production">Metrics</a><a href="/server/services?env=production">Runtime services</a><a class="" href="/operations/jobs?env=production">Workers & schedules</a><a class="" href="/operations/configuration?env=production">Environment & secrets</a><a class="" href="/data/backups?env=production">Backups & recovery</a><a class="" href="/data/volumes?env=production">Persistent volumes</a><a href="/server/actions?env=production">Run operations</a></section><section class="nav-group"><h2>Infrastructure</h2><a class="" href="/infrastructure/topology?env=production">Infrastructure map</a><a class="" href="/data/services?env=production">Data services</a><a class="" href="/data/volumes?env=production">Persistent volumes</a><a href="/server/firewall?env=production">Network & firewall</a><a href="/server/ssh-keys?env=production">SSH keys</a><a class="" href="/server/fleet?env=production">Fleet inventory</a><a class="" href="/server/capacity?env=production">Capacity & placement</a><a href="/security?env=production">Security posture</a><a href="/server/terminal?env=production">Terminal</a></section><section class="nav-group"><h2>Organization</h2><a class="" href="/server/team?env=production">People & access</a><a class="" href="/account/automation?env=production">API & automation</a></section><section class="nav-group"><h2>Account</h2><a class="" href="/account/security?env=production">Sign-in & security</a></section></div><form method="post" action="/api/logout" class="signout"><button type="submit">Sign out</button></form></aside><dialog class="command-palette" aria-labelledby="command-title"><div class="command-search"><span aria-hidden="true">⌕</span><input type="search" placeholder="Search services, domains, deployments…" aria-label="Search resources" autocomplete="off"><button type="button" class="command-close" aria-label="Close search">Esc</button></div><h2 id="command-title" class="sr-only">Search resources and navigation</h2><div class="command-status" role="status">Start typing to search this project.</div><section class="saved-views" aria-labelledby="saved-views-title"><div class="saved-views-heading"><h3 id="saved-views-title">Saved views</h3><form class="save-view-form"><label class="sr-only" for="save-view-name">Saved view name</label><input id="save-view-name" maxlength="64" placeholder="Name this view" autocomplete="off"><button type="submit">Save current filters</button></form></div><div class="saved-view-list"></div></section><form class="tag-result-form"><label for="tag-result-name">Tag selected resource</label><input id="tag-result-name" maxlength="32" placeholder="customer-facing" pattern="[A-Za-z0-9][A-Za-z0-9 -]{0,31}" autocomplete="off"><input class="tag-color" type="color" value="#5a8be0" aria-label="Tag color"><button type="submit">Add tag</button></form><div class="command-results" role="listbox" aria-label="Search results"></div><footer><span>↑ ↓ navigate</span><span>Enter open</span><span>Esc close</span></footer></dialog><div class="context-bar" aria-label="Current scope"><div class="context-path"><div class="context-breadcrumb"><span>ts-cloud</span><i>/</i><strong>production</strong></div><div class="context-filters" aria-label="Active filters"></div></div><span class="scope-safety production">Production scope</span></div> <script data-stx-scoped data-stx-run="always">
|
|
2
2
|
(function() {
|
|
3
3
|
var { batch, computed, configureFetch, defineEmits, defineExpose, definePageMeta, defineProps, defineSlots, defineStore, derived, effect, goBack, goForward, inject, invalidateRoute, isDerived, isSignal, navigate, nextTick, onBeforeMount, onBeforeUnmount, onDestroy, onMount, onMounted, onUnmounted, peek, provide, reactive, ref, refresh, registerStoresClient, setRouteParams, state, untrack, useAsync, useClickOutside, useColorMode, useCookie, useCounter, useDark, useDebounce, useDebouncedValue, useEventListener, useFetch, useFocus, useHead, useId, useInterval, useLocalStorage, useMediaQuery, useMutation, useOptimistic, usePreferredContrast, usePreferredDark, usePreferredLight, usePreferredReducedMotion, useQuery, useReactiveProp, useRef, useRoute, useRouteParam, useRouteParams, useScrollLock, useSearchParams, useSeoMeta, useSessionStorage, useSlots, useStore, useThrottle, useTimeout, useToggle, useWebSocket, watch, watchEffect, watchMultiple, withDefaults } = window.stx;
|
|
4
4
|
var __destroyHooks = [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="
|
|
2
|
-
function
|
|
1
|
+
<!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="mtekc2s63mrzwt"><meta name="title" content="stx Project"><meta name="description" content="A website built with stx templating engine"><meta property="og:title" content="stx Project"><meta property="og:description" content="A website built with stx templating engine"><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="stx Project"><meta name="twitter:description" content="A website built with stx templating engine"> <script data-stx-runtime src="/_stx/runtime.dbee66d1.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ts-cloud - API & automation</title><link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='3' y='3' width='26' height='26' rx='8' fill='%235a8be0'/%3E%3C/svg%3E"><link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"><link rel="stylesheet" href="/_stx/style.448ab192.css"> <script data-stx-scoped>
|
|
2
|
+
function __stx_setup_1788018980030_5() {
|
|
3
3
|
const { batch, computed, configureFetch, defineEmits, defineExpose, definePageMeta, defineProps, defineSlots, defineStore, derived, effect, goBack, goForward, inject, invalidateRoute, isDerived, isSignal, navigate, nextTick, onBeforeMount, onBeforeUnmount, onDestroy, onMount, onMounted, onUnmounted, peek, provide, reactive, ref, refresh, registerStoresClient, setRouteParams, state, untrack, useAsync, useClickOutside, useColorMode, useCookie, useCounter, useDark, useDebounce, useDebouncedValue, useEventListener, useFetch, useFocus, useHead, useId, useInterval, useLocalStorage, useMediaQuery, useMutation, useOptimistic, usePreferredContrast, usePreferredDark, usePreferredLight, usePreferredReducedMotion, useQuery, useReactiveProp, useRef, useRoute, useRouteParam, useRouteParams, useScrollLock, useSearchParams, useSeoMeta, useSessionStorage, useSlots, useStore, useThrottle, useTimeout, useToggle, useWebSocket, watch, watchEffect, watchMultiple, withDefaults } = window.stx;
|
|
4
4
|
|
|
5
5
|
const accounts = state([]);
|
|
@@ -189,9 +189,9 @@ onMount(() => {
|
|
|
189
189
|
|
|
190
190
|
return { accounts, project, environments, resources, message, messageKind, reveal, accountSlug, accountName, accountRole, accountScopeType, accountScopeId, selectedAccount, tokenName, tokenCapabilities, tokenScopeType, tokenScopeId, tokenExpiry, password, mfaCode, capabilities, setMessage, setScope, scopeLabel, tokenState, toggleCapability, load, selectAccount, stepUp, createAccount, createToken, tokenAction, disableAccount, curlExample, cliExample, copy };
|
|
191
191
|
}
|
|
192
|
-
window.__stx_latestSetup=
|
|
193
|
-
if(window.stx)window.stx._latestSetup=
|
|
194
|
-
</script> <style data-stx-cloak>[x-cloak]{display:none !important}</style><link data-crosswind="generated" rel="stylesheet" href="/_stx/crosswind.906ee17c.css"></head><body data-stx="
|
|
192
|
+
window.__stx_latestSetup=__stx_setup_1788018980030_5;
|
|
193
|
+
if(window.stx)window.stx._latestSetup=__stx_setup_1788018980030_5;
|
|
194
|
+
</script> <style data-stx-cloak>[x-cloak]{display:none !important}</style><link data-crosswind="generated" rel="stylesheet" href="/_stx/crosswind.906ee17c.css"></head><body data-stx="__stx_setup_1788018980030_5"><a data-stx-scope="stx_scope_nav_8" class="skip-link" href="#main-content">Skip to content</a><button class="nav-toggle-button" type="button" aria-label="Open navigation" aria-controls="primary-navigation" aria-expanded="false"><span></span><span></span><span></span></button><button class="nav-scrim" type="button" aria-label="Close navigation"></button><aside class="app-sidebar" id="primary-navigation" aria-label="Primary navigation"><a class="brand" href="/?env=production"><span class="dot"></span><span>ts-cloud</span></a><div class="sidebar-context"><span class="context-label">Project</span><strong>ts-cloud</strong></div><div class="environment-picker" aria-label="Environment"><span class="context-label">Environment</span><details><summary class="environment-current production"><span>PROD</span><strong>production</strong></summary></details></div><button class="command-trigger" type="button" aria-keyshortcuts="Meta+K Control+K"><span>Search resources</span><kbd>⌘ K</kbd></button><div class="nav-groups"><section class="nav-group"><h2>Overview</h2><a class="" href="/?env=production">Dashboard</a><a href="/server/activity?env=production">Activity</a></section><section class="nav-group"><h2>Applications</h2><a class="" href="/server/sites?env=production">Services</a><a class="" href="/server/deployments?env=production">Deployments</a><a class="" href="/server/logs?env=production">Logs</a><a class="" href="/integrations?env=production">Git integrations</a><a class="" href="/applications/new?env=production">Create application</a><a class="" href="/applications/compose?env=production">Compose applications</a></section><section class="nav-group"><h2>Operations</h2><a class="" href="/operations/queue?env=production">Deployment queue</a><a class="" href="/operations/previews?env=production">Preview environments</a><a class="" href="/operations/releases?env=production">Releases & promotion</a><a class="" href="/operations/regions?env=production">Regions & failover</a><a class="" href="/operations/maintenance?env=production">Maintenance & DR</a><a class="" href="/operations/workloads?env=production">Workloads</a><a class="" href="/operations/observability?env=production">Observability</a><a class="" href="/operations/alerts?env=production">Alerts & health</a><a class="" href="/operations/spend?env=production">Spend & budgets</a><a href="/server/metrics?env=production">Metrics</a><a href="/server/services?env=production">Runtime services</a><a class="" href="/operations/jobs?env=production">Workers & schedules</a><a class="" href="/operations/configuration?env=production">Environment & secrets</a><a class="" href="/data/backups?env=production">Backups & recovery</a><a class="" href="/data/volumes?env=production">Persistent volumes</a><a href="/server/actions?env=production">Run operations</a></section><section class="nav-group"><h2>Infrastructure</h2><a class="" href="/infrastructure/topology?env=production">Infrastructure map</a><a class="" href="/data/services?env=production">Data services</a><a class="" href="/data/volumes?env=production">Persistent volumes</a><a href="/server/firewall?env=production">Network & firewall</a><a href="/server/ssh-keys?env=production">SSH keys</a><a class="" href="/server/fleet?env=production">Fleet inventory</a><a class="" href="/server/capacity?env=production">Capacity & placement</a><a href="/security?env=production">Security posture</a><a href="/server/terminal?env=production">Terminal</a></section><section class="nav-group"><h2>Organization</h2><a class="" href="/server/team?env=production">People & access</a><a class="active" href="/account/automation?env=production">API & automation</a></section><section class="nav-group"><h2>Account</h2><a class="" href="/account/security?env=production">Sign-in & security</a></section></div><form method="post" action="/api/logout" class="signout"><button type="submit">Sign out</button></form></aside><dialog class="command-palette" aria-labelledby="command-title"><div class="command-search"><span aria-hidden="true">⌕</span><input type="search" placeholder="Search services, domains, deployments…" aria-label="Search resources" autocomplete="off"><button type="button" class="command-close" aria-label="Close search">Esc</button></div><h2 id="command-title" class="sr-only">Search resources and navigation</h2><div class="command-status" role="status">Start typing to search this project.</div><section class="saved-views" aria-labelledby="saved-views-title"><div class="saved-views-heading"><h3 id="saved-views-title">Saved views</h3><form class="save-view-form"><label class="sr-only" for="save-view-name">Saved view name</label><input id="save-view-name" maxlength="64" placeholder="Name this view" autocomplete="off"><button type="submit">Save current filters</button></form></div><div class="saved-view-list"></div></section><form class="tag-result-form"><label for="tag-result-name">Tag selected resource</label><input id="tag-result-name" maxlength="32" placeholder="customer-facing" pattern="[A-Za-z0-9][A-Za-z0-9 -]{0,31}" autocomplete="off"><input class="tag-color" type="color" value="#5a8be0" aria-label="Tag color"><button type="submit">Add tag</button></form><div class="command-results" role="listbox" aria-label="Search results"></div><footer><span>↑ ↓ navigate</span><span>Enter open</span><span>Esc close</span></footer></dialog><div class="context-bar" aria-label="Current scope"><div class="context-path"><div class="context-breadcrumb"><span>ts-cloud</span><i>/</i><strong>production</strong></div><div class="context-filters" aria-label="Active filters"></div></div><span class="scope-safety production">Production scope</span></div> <script data-stx-scoped data-stx-run="always">
|
|
195
195
|
(function() {
|
|
196
196
|
var { batch, computed, configureFetch, defineEmits, defineExpose, definePageMeta, defineProps, defineSlots, defineStore, derived, effect, goBack, goForward, inject, invalidateRoute, isDerived, isSignal, navigate, nextTick, onBeforeMount, onBeforeUnmount, onDestroy, onMount, onMounted, onUnmounted, peek, provide, reactive, ref, refresh, registerStoresClient, setRouteParams, state, untrack, useAsync, useClickOutside, useColorMode, useCookie, useCounter, useDark, useDebounce, useDebouncedValue, useEventListener, useFetch, useFocus, useHead, useId, useInterval, useLocalStorage, useMediaQuery, useMutation, useOptimistic, usePreferredContrast, usePreferredDark, usePreferredLight, usePreferredReducedMotion, useQuery, useReactiveProp, useRef, useRoute, useRouteParam, useRouteParams, useScrollLock, useSearchParams, useSeoMeta, useSessionStorage, useSlots, useStore, useThrottle, useTimeout, useToggle, useWebSocket, watch, watchEffect, watchMultiple, withDefaults } = window.stx;
|
|
197
197
|
var __destroyHooks = [];
|
|
@@ -542,6 +542,6 @@ if(window.stx)window.stx._latestSetup=__stx_setup_1787788196566_5;
|
|
|
542
542
|
|
|
543
543
|
|
|
544
544
|
try { if (typeof __stxScopeRenames !== 'undefined' && __stxScopeRenames) { for (var __stxRK in __stxScopeRenames) __scopeRegistration[__stxRK] = __stxScopeRenames[__stxRK]; } } catch (e) { /* no renames */ }
|
|
545
|
-
window.stx._scopes['
|
|
545
|
+
window.stx._scopes['stx_scope_nav_8'] = __scopeRegistration;
|
|
546
546
|
})();
|
|
547
547
|
</script> <div class="wrap"><div class="crumbs"><a href="/">Dashboard</a><span class="sep">/</span> Organization <span class="sep">/</span> API & automation</div><header class="page"><div><h1>API & automation</h1><p>Create least-privilege machine identities for CI. Tokens never create browser sessions and cannot access outside their membership and token scope.</p></div><a class="btn ghost" href="/api/v1/openapi.json">OpenAPI 3.1</a></header><div class="automation-message msg" :class="messageKind()" role="status" aria-live="polite" x-cloak>{{ message() }}</div><div class="panel reveal" @show="!!reveal()" x-cloak><div><strong>Copy this token now</strong><span>The raw secret is shown once. Rotation keeps the old token active until you explicitly revoke it.</span></div><code x-cloak>{{ reveal()?.secret }}</code><button class="btn sm" type="button" @click="copy(reveal()?.secret)">Copy token</button><label>curl</label><code x-cloak>{{ curlExample() }}</code><button class="btn ghost sm" type="button" @click="copy(curlExample())">Copy curl</button><label>CLI</label><code x-cloak>{{ cliExample() }}</code><button class="btn ghost sm" type="button" @click="copy(cliExample())">Copy CLI</button><button class="btn danger sm" type="button" @click="reveal.set(null)">I saved it; hide secret</button></div><div class="section"><h2>Service accounts</h2><div class="accounts" x-cloak><template :for="item in accounts()"><article class="account panel" :class="selectedAccount() === item.account.id ? 'selected' : ''" x-cloak><button class="account-main" type="button" @click="selectAccount(item)" x-cloak><span x-cloak><strong x-cloak>{{ item.account.name }}</strong><small x-cloak>{{ item.account.slug }} · {{ item.membership?.roleTemplate }} · {{ scopeLabel(item.membership?.scope) }}</small><small x-cloak>Created {{ new Date(item.account.createdAt).toLocaleDateString() }} by {{ item.createdBy?.displayName || 'offline administrator' }}</small></span><span class="tag" x-cloak>{{ item.account.state }}</span></button><div class="token-list" x-cloak><template :for="token in item.tokens"><div class="token" x-cloak><div x-cloak><strong x-cloak>{{ token.name }}</strong><code x-cloak>{{ token.prefix }}</code><small x-cloak>{{ tokenState(token) }} · created {{ new Date(token.createdAt).toLocaleDateString() }} by {{ token.createdBy?.displayName || 'offline administrator' }} · expires {{ new Date(token.expiresAt).toLocaleDateString() }} · last used {{ token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleString() : 'never' }}</small><small x-cloak>{{ token.capabilities.join(', ') }} · {{ scopeLabel(token.scope) }}</small></div><div class="table-actions"><button class="btn ghost sm" type="button" @show="token.state === 'active'" @click="tokenAction(token, 'rotate')" x-cloak>Rotate</button><button class="btn danger sm" type="button" @show="token.state === 'active'" @click="tokenAction(token, 'revoke')" x-cloak>Revoke</button></div></div></template><div class="compact empty" @show="item.tokens.length === 0" x-cloak><span>No tokens yet.</span></div></div><button class="btn danger sm" type="button" @show="item.account.state === 'active'" @click="disableAccount(item)" x-cloak>Disable account and tokens</button></article></template><div class="compact empty panel" @show="accounts().length === 0" x-cloak><strong>No service accounts</strong><span>Create one for a CI system or external automation.</span></div></div></div><div class="section"><h2>Create service account</h2><div class="panel"><form class="form-grid" @submit.prevent="createAccount()"><div class="field-block"><label for="account-name">Name</label><input id="account-name" :value="accountName()" @input="accountName.set($event.target.value)" placeholder="Production CI" required></div><div class="field-block"><label for="account-slug">Slug</label><input id="account-slug" :value="accountSlug()" @input="accountSlug.set($event.target.value)" pattern="[a-z0-9][a-z0-9-]{1,47}" placeholder="production-ci" required></div><div class="field-block"><label for="account-role">Maximum role</label><select id="account-role" :value="accountRole()" @change="accountRole.set($event.target.value)"><option value="viewer">Viewer</option><option value="auditor">Auditor</option><option value="deployer">Deployer</option><option value="operator">Operator</option><option value="admin">Admin</option></select><small>Service accounts can never be owners.</small></div><div class="field-block"><label for="account-scope">Maximum scope</label><select id="account-scope" :value="accountScopeType()" @change="accountScopeType.set($event.target.value); setScope($event.target.value, accountScopeId)"><option value="organization">Organization</option><option value="project">Current project</option><option value="environment">One environment</option><option value="resource">One service</option></select></div><div class="field-block" @show="accountScopeType() === 'environment'" x-cloak><label for="account-environment">Environment</label><select id="account-environment" :value="accountScopeId()" @change="accountScopeId.set($event.target.value)"><template :for="environment in environments()"><option :value="environment.id">{{ environment.name }}</option></template></select></div><div class="field-block" @show="accountScopeType() === 'resource'" x-cloak><label for="account-resource">Service</label><select id="account-resource" :value="accountScopeId()" @change="accountScopeId.set($event.target.value)"><template :for="resource in resources()"><option :value="resource.id">{{ resource.name }}</option></template></select></div><div class="form-actions"><button class="btn" type="submit">Create service account</button></div></form></div></div><div class="section"><h2>Create API token</h2><div class="panel"><form class="form-grid" @submit.prevent="createToken()"><div class="field-block"><label for="token-account">Service account</label><select id="token-account" :value="selectedAccount()" @change="selectAccount(accounts().find(item => item.account.id === $event.target.value))" required><template :for="item in accounts()"><option :value="item.account.id" :disabled="item.account.state !== 'active'">{{ item.account.name }}</option></template></select></div><div class="field-block"><label for="token-name">Token name</label><input id="token-name" :value="tokenName()" @input="tokenName.set($event.target.value)" required></div><div class="field-block"><label for="token-expiry">Expiry</label><input id="token-expiry" type="date" :value="tokenExpiry()" @input="tokenExpiry.set($event.target.value)"><small>Defaults to 90 days; maximum one year.</small></div><div class="field-block"><label for="token-scope">Token scope</label><select id="token-scope" :value="tokenScopeType()" @change="tokenScopeType.set($event.target.value); setScope($event.target.value, tokenScopeId)"><option value="organization">Organization</option><option value="project">Current project</option><option value="environment">One environment</option><option value="resource">One service</option></select><small>Must be equal to or narrower than the service-account scope.</small></div><div class="field-block" @show="tokenScopeType() === 'environment'" x-cloak><label>Environment</label><select :value="tokenScopeId()" @change="tokenScopeId.set($event.target.value)"><template :for="environment in environments()"><option :value="environment.id">{{ environment.name }}</option></template></select></div><div class="field-block" @show="tokenScopeType() === 'resource'" x-cloak><label>Service</label><select :value="tokenScopeId()" @change="tokenScopeId.set($event.target.value)"><template :for="resource in resources()"><option :value="resource.id">{{ resource.name }}</option></template></select></div><fieldset class="capabilities"><legend>Explicit capabilities</legend><template :for="capability in capabilities"><label class="capability" x-cloak><input type="checkbox" :checked="tokenCapabilities().includes(capability[0])" @change="toggleCapability(capability[0])"><span x-cloak><b x-cloak>{{ capability[0] }}</b><small x-cloak>{{ capability[1] }}</small></span></label></template></fieldset><div class="form-actions"><button class="btn" type="submit" :disabled="!selectedAccount() || tokenCapabilities().length === 0">Create and reveal token</button></div></form></div></div><div class="section"><h2>Confirm sensitive changes</h2><div class="auth-grid panel"><div class="field-block"><label for="automation-password">Your password</label><input id="automation-password" type="password" autocomplete="current-password" :value="password()" @input="password.set($event.target.value)" required></div><div class="field-block"><label for="automation-code">Authenticator or recovery code</label><input id="automation-code" autocomplete="one-time-code" :value="mfaCode()" @input="mfaCode.set($event.target.value)"><small>Required when MFA is enabled. Used only for recent-auth confirmation.</small></div></div></div></div><style> .automation-message{min-height:20px;margin-top:12px}.accounts{display:grid;gap:12px}.account{display:grid;gap:14px}.account.selected{border-color:var(--accent)} .account-main{display:flex;justify-content:space-between;gap:12px;width:100%;text-align:left;background:none;border:0;color:inherit;cursor:pointer}.account-main span:first-child{display:grid;gap:4px}.account-main small,.token small{color:var(--txt3);font-size:12px;line-height:1.5} .token-list{display:grid;gap:10px}.token{display:flex;justify-content:space-between;gap:14px;padding:12px;border:1px solid var(--panel-br);border-radius:10px}.token>div:first-child{display:grid;gap:4px;min-width:0}.token code{color:var(--txt2)} .reveal{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;margin-top:18px;border-color:rgba(53,209,155,.35)}.reveal>div,.reveal>code,.reveal>label{grid-column:1/-1}.reveal>div{display:grid;gap:4px}.reveal span{color:var(--txt2);font-size:12px}.reveal code{padding:10px;background:rgba(0,0,0,.25);border-radius:8px;overflow-wrap:anywhere;user-select:all} .capabilities{grid-column:1/-1;border:1px solid var(--panel-br);border-radius:10px;padding:12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.capabilities legend{padding:0 6px;color:var(--txt3);font-size:11px;font-weight:800;text-transform:uppercase}.capability{display:flex;gap:9px;align-items:flex-start;padding:9px}.capability input{width:auto;margin-top:3px}.capability span{display:grid;gap:2px}.capability small{color:var(--txt3);font-size:11px}.auth-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px} @media(max-width:700px){.token{display:grid}.reveal,.capabilities,.auth-grid{grid-template-columns:1fr}.reveal>*{grid-column:1}} </style></body></html>
|