@stacksjs/ts-cloud 0.11.7 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/dist/aws/index.js +1 -1
  2. package/dist/bin/chunk-0rtn6q5x.js +3 -0
  3. package/dist/bin/chunk-258ehp9s.js +1 -0
  4. package/dist/bin/chunk-2yctv06c.js +1 -0
  5. package/dist/bin/chunk-3276rveh.js +37 -0
  6. package/dist/bin/chunk-3m665jwp.js +70 -0
  7. package/dist/bin/chunk-3s2p9vxj.js +1 -0
  8. package/dist/bin/chunk-4esma5m9.js +1 -0
  9. package/dist/bin/chunk-4n0j3n3b.js +2 -0
  10. package/dist/bin/chunk-54bqchag.js +92 -0
  11. package/dist/bin/{chunk-4q6tad44.js → chunk-5bhx3bsa.js} +121 -121
  12. package/dist/bin/chunk-5zagqd20.js +1 -0
  13. package/dist/bin/chunk-7hw2js5h.js +512 -0
  14. package/dist/bin/chunk-8h1qr6x4.js +50 -0
  15. package/dist/bin/{chunk-w32xv8n4.js → chunk-9vdc7adf.js} +6 -6
  16. package/dist/bin/chunk-a1dd6sby.js +1 -0
  17. package/dist/bin/chunk-a4dw35gs.js +1 -0
  18. package/dist/bin/chunk-a9d55ker.js +1 -0
  19. package/dist/bin/chunk-awa1qade.js +9 -0
  20. package/dist/bin/chunk-b6by94pk.js +2 -0
  21. package/dist/bin/chunk-c8sypnbe.js +2 -0
  22. package/dist/bin/chunk-cx4e3d4w.js +1 -0
  23. package/dist/bin/chunk-d5a3vheh.js +2 -0
  24. package/dist/bin/chunk-e1fym2bd.js +1 -0
  25. package/dist/bin/chunk-ej4h0q2y.js +1 -0
  26. package/dist/bin/chunk-ett305ym.js +4 -0
  27. package/dist/bin/chunk-f11xf3n6.js +2 -0
  28. package/dist/bin/chunk-fq7ktvgy.js +3 -0
  29. package/dist/bin/chunk-fyq4th2m.js +1 -0
  30. package/dist/bin/chunk-g67qr8yf.js +1 -0
  31. package/dist/bin/chunk-gc82cx4h.js +1 -0
  32. package/dist/bin/chunk-hchtvpr9.js +1 -0
  33. package/dist/bin/chunk-hpe4jjce.js +1 -0
  34. package/dist/bin/chunk-k5vprsef.js +6 -0
  35. package/dist/bin/chunk-m22krd4z.js +3 -0
  36. package/dist/bin/chunk-mkppcwz8.js +1 -0
  37. package/dist/bin/chunk-mpjg0kc7.js +2 -0
  38. package/dist/bin/chunk-mrm4m4dk.js +2 -0
  39. package/dist/bin/chunk-nff46rxd.js +1 -0
  40. package/dist/bin/chunk-np1z3bgt.js +1 -0
  41. package/dist/bin/chunk-nskxkk6r.js +1 -0
  42. package/dist/bin/chunk-pez932b1.js +111 -0
  43. package/dist/bin/chunk-qsdf5kzk.js +2 -0
  44. package/dist/bin/chunk-r61qmhv1.js +2 -0
  45. package/dist/bin/chunk-r6gdcz5c.js +1 -0
  46. package/dist/bin/chunk-satwnr42.js +2 -0
  47. package/dist/bin/{chunk-dz1ec589.js → chunk-scqmd4qv.js} +30 -30
  48. package/dist/bin/chunk-smbycpt5.js +44 -0
  49. package/dist/bin/chunk-sn7e0sn7.js +2 -0
  50. package/dist/bin/chunk-ss574pka.js +1 -0
  51. package/dist/bin/chunk-tj0xb2d8.js +585 -0
  52. package/dist/bin/chunk-v3rzf9d0.js +141 -0
  53. package/dist/bin/chunk-wnpma6t3.js +34 -0
  54. package/dist/bin/chunk-xan0e052.js +5 -0
  55. package/dist/bin/chunk-xbk2y43r.js +60 -0
  56. package/dist/bin/chunk-xjp3v95e.js +6 -0
  57. package/dist/bin/chunk-xyt490vp.js +6 -0
  58. package/dist/bin/chunk-yn9r02gy.js +1 -0
  59. package/dist/bin/chunk-zhkhn9cc.js +1 -0
  60. package/dist/bin/cli.js +445 -430
  61. package/dist/bin/dashboard-server.js +1 -1
  62. package/dist/chunk-0zf72ngj.js +1 -0
  63. package/dist/chunk-2hpjte1d.js +704 -0
  64. package/dist/chunk-4m6gwhwr.js +5 -0
  65. package/dist/chunk-5m29dhke.js +34 -0
  66. package/dist/chunk-6fzeemcs.js +114 -0
  67. package/dist/{chunk-zz52xje8.js → chunk-6xcbfs2r.js} +111 -111
  68. package/dist/chunk-bpnhqb70.js +6 -0
  69. package/dist/chunk-ead1nkbr.js +60 -0
  70. package/dist/chunk-fea461tj.js +141 -0
  71. package/dist/chunk-fvjczc73.js +1 -0
  72. package/dist/chunk-gearw6rk.js +4 -0
  73. package/dist/chunk-j60hesb3.js +2 -0
  74. package/dist/chunk-pmgtmt3d.js +1 -0
  75. package/dist/chunk-prf2pkvq.js +5 -0
  76. package/dist/chunk-q0wffgw5.js +37 -0
  77. package/dist/chunk-qcemnt35.js +50 -0
  78. package/dist/chunk-qvbwsaay.js +1 -0
  79. package/dist/chunk-r2pbqcnz.js +2 -0
  80. package/dist/chunk-r6110zv2.js +1 -0
  81. package/dist/chunk-rr1yaj55.js +2 -0
  82. package/dist/chunk-wcrj06cg.js +44 -0
  83. package/dist/chunk-wte9mh18.js +92 -0
  84. package/dist/{chunk-aw0cgv5w.js → chunk-y1dh3znq.js} +52 -52
  85. package/dist/chunk-y6e86wjs.js +445 -0
  86. package/dist/chunk-y8b7ht4v.js +1 -0
  87. package/dist/chunk-yf487g6e.js +2 -0
  88. package/dist/chunk-zmmc9j5f.js +2 -0
  89. package/dist/deploy/index.js +1 -1
  90. package/dist/dns/index.js +1 -1
  91. package/dist/drivers/index.js +1 -1
  92. package/dist/index.d.ts +1 -0
  93. package/dist/index.js +91 -91
  94. package/dist/operations/drained-sites.d.ts +56 -0
  95. package/dist/operations/drained-sites.test.d.ts +1 -0
  96. package/dist/pages/cloudflare-pages.d.ts +240 -0
  97. package/dist/pages/cloudflare-pages.test.d.ts +1 -0
  98. package/dist/pages/index.d.ts +2 -0
  99. package/dist/protection/index.js +1 -1
  100. package/dist/push/index.js +1 -1
  101. package/dist/spend/index.js +1 -1
  102. package/dist/ui/404.html +1 -1
  103. package/dist/ui/accept-invitation.html +1 -1
  104. package/dist/ui/access-denied.html +1 -1
  105. package/dist/ui/account/automation.html +6 -6
  106. package/dist/ui/account/security.html +2 -2
  107. package/dist/ui/applications/compose.html +6 -6
  108. package/dist/ui/applications/new.html +2 -2
  109. package/dist/ui/data/backups.html +80 -80
  110. package/dist/ui/data/services.html +78 -78
  111. package/dist/ui/data/volumes.html +50 -50
  112. package/dist/ui/index.html +50 -50
  113. package/dist/ui/infrastructure/topology.html +118 -118
  114. package/dist/ui/integrations.html +2 -2
  115. package/dist/ui/operations/alerts.html +48 -48
  116. package/dist/ui/operations/configuration.html +6 -6
  117. package/dist/ui/operations/jobs.html +75 -75
  118. package/dist/ui/operations/maintenance.html +6 -6
  119. package/dist/ui/operations/observability.html +82 -82
  120. package/dist/ui/operations/previews.html +6 -6
  121. package/dist/ui/operations/queue.html +6 -6
  122. package/dist/ui/operations/regions.html +6 -6
  123. package/dist/ui/operations/releases.html +6 -6
  124. package/dist/ui/operations/spend.html +54 -54
  125. package/dist/ui/operations/workloads.html +104 -104
  126. package/dist/ui/security.html +2 -2
  127. package/dist/ui/server/actions.html +56 -56
  128. package/dist/ui/server/activity.html +2 -2
  129. package/dist/ui/server/capacity.html +6 -6
  130. package/dist/ui/server/database.html +105 -105
  131. package/dist/ui/server/deployments.html +50 -50
  132. package/dist/ui/server/diagnostics.html +2 -2
  133. package/dist/ui/server/firewall.html +76 -76
  134. package/dist/ui/server/fleet.html +34 -34
  135. package/dist/ui/server/logs.html +24 -24
  136. package/dist/ui/server/metrics.html +112 -112
  137. package/dist/ui/server/security.html +1 -1
  138. package/dist/ui/server/services.html +26 -26
  139. package/dist/ui/server/sites.html +120 -120
  140. package/dist/ui/server/ssh-keys.html +52 -52
  141. package/dist/ui/server/team.html +144 -144
  142. package/dist/ui/server/terminal.html +15 -15
  143. package/dist/ui/serverless/alarms.html +52 -52
  144. package/dist/ui/serverless/assets.html +27 -27
  145. package/dist/ui/serverless/cost.html +2 -2
  146. package/dist/ui/serverless/data.html +35 -35
  147. package/dist/ui/serverless/deployments.html +27 -27
  148. package/dist/ui/serverless/firewall.html +2 -2
  149. package/dist/ui/serverless/functions.html +40 -40
  150. package/dist/ui/serverless/logs.html +24 -24
  151. package/dist/ui/serverless/metrics.html +2 -2
  152. package/dist/ui/serverless/queues.html +48 -48
  153. package/dist/ui/serverless/secrets.html +52 -52
  154. package/dist/ui/serverless/traces.html +6 -6
  155. package/dist/ui/serverless.html +53 -53
  156. package/dist/ui-src/src/charts.ts +8721 -13558
  157. package/dist/ui-src/src/topology.ts +2 -2
  158. package/package.json +4 -3
  159. package/dist/bin/chunk-0zwknfwp.js +0 -1
  160. package/dist/bin/chunk-3aph4c1w.js +0 -2
  161. package/dist/bin/chunk-4xhj37nj.js +0 -37
  162. package/dist/bin/chunk-5nkqza92.js +0 -1
  163. package/dist/bin/chunk-5nsqz5n3.js +0 -585
  164. package/dist/bin/chunk-64vczwwt.js +0 -1
  165. package/dist/bin/chunk-6tfwm33r.js +0 -6
  166. package/dist/bin/chunk-7g2ahkse.js +0 -1
  167. package/dist/bin/chunk-7zhgmp1s.js +0 -3
  168. package/dist/bin/chunk-82mj52xr.js +0 -141
  169. package/dist/bin/chunk-8khgprf6.js +0 -44
  170. package/dist/bin/chunk-9a88fp00.js +0 -2
  171. package/dist/bin/chunk-a7w13qfm.js +0 -2
  172. package/dist/bin/chunk-b4wt9jcg.js +0 -60
  173. package/dist/bin/chunk-btsyjhk7.js +0 -92
  174. package/dist/bin/chunk-c0bdafbx.js +0 -2
  175. package/dist/bin/chunk-cbsys5v8.js +0 -111
  176. package/dist/bin/chunk-cd0ps58j.js +0 -1
  177. package/dist/bin/chunk-d7ke90zt.js +0 -2
  178. package/dist/bin/chunk-djg4zyqk.js +0 -1
  179. package/dist/bin/chunk-dkaz333b.js +0 -1
  180. package/dist/bin/chunk-dkf4qsp2.js +0 -1
  181. package/dist/bin/chunk-e2bybahr.js +0 -3
  182. package/dist/bin/chunk-e5ab5twy.js +0 -50
  183. package/dist/bin/chunk-edvvx3hg.js +0 -1
  184. package/dist/bin/chunk-ehc63fvs.js +0 -2
  185. package/dist/bin/chunk-f06c5np9.js +0 -1
  186. package/dist/bin/chunk-g0fm1xcs.js +0 -1
  187. package/dist/bin/chunk-gq8794k6.js +0 -1
  188. package/dist/bin/chunk-hq9x489e.js +0 -70
  189. package/dist/bin/chunk-j447p8t4.js +0 -512
  190. package/dist/bin/chunk-ja1vjdn1.js +0 -6
  191. package/dist/bin/chunk-k3120rx6.js +0 -1
  192. package/dist/bin/chunk-kg61p5hw.js +0 -3
  193. package/dist/bin/chunk-mcr0r4at.js +0 -2
  194. package/dist/bin/chunk-mfrf9q4b.js +0 -2
  195. package/dist/bin/chunk-my3ewh3a.js +0 -34
  196. package/dist/bin/chunk-nw3d4k52.js +0 -1
  197. package/dist/bin/chunk-p70bc9yr.js +0 -1
  198. package/dist/bin/chunk-r6b3pq31.js +0 -4
  199. package/dist/bin/chunk-s4yzes15.js +0 -2
  200. package/dist/bin/chunk-sj5csg49.js +0 -2
  201. package/dist/bin/chunk-tvz9c388.js +0 -1
  202. package/dist/bin/chunk-wkx799z2.js +0 -1
  203. package/dist/bin/chunk-x6f61g36.js +0 -9
  204. package/dist/bin/chunk-xhwqpgwq.js +0 -2
  205. package/dist/bin/chunk-xk08030d.js +0 -6
  206. package/dist/bin/chunk-y1h63vpm.js +0 -1
  207. package/dist/bin/chunk-yw6f160x.js +0 -1
  208. package/dist/bin/chunk-zdt2rjk6.js +0 -1
  209. package/dist/bin/chunk-zh2d4704.js +0 -1
  210. package/dist/bin/chunk-znnawt0k.js +0 -1
  211. package/dist/bin/chunk-zpe64t4r.js +0 -1
  212. package/dist/bin/chunk-zsf6pccz.js +0 -1
  213. package/dist/chunk-1qdne0bf.js +0 -1
  214. package/dist/chunk-5vt93kxx.js +0 -1
  215. package/dist/chunk-73ztr55z.js +0 -2
  216. package/dist/chunk-95e6aeqz.js +0 -1
  217. package/dist/chunk-aj025v2s.js +0 -4
  218. package/dist/chunk-aw0spg20.js +0 -445
  219. package/dist/chunk-by2bwzk6.js +0 -141
  220. package/dist/chunk-dr4pj8hp.js +0 -5
  221. package/dist/chunk-hn40z11p.js +0 -37
  222. package/dist/chunk-kasdm9tx.js +0 -34
  223. package/dist/chunk-mz0dk78z.js +0 -6
  224. package/dist/chunk-nk7sav4r.js +0 -92
  225. package/dist/chunk-p10xt3jb.js +0 -1
  226. package/dist/chunk-q0rc1ax9.js +0 -114
  227. package/dist/chunk-q9q6cmt0.js +0 -1
  228. package/dist/chunk-rd38qdnf.js +0 -1
  229. package/dist/chunk-tz9myq9a.js +0 -2
  230. package/dist/chunk-wavjkdb8.js +0 -2
  231. package/dist/chunk-x3hay61c.js +0 -44
  232. package/dist/chunk-xcj8zfmg.js +0 -2
  233. package/dist/chunk-y4xkj2f1.js +0 -50
  234. package/dist/chunk-ywjkyg8p.js +0 -704
  235. package/dist/chunk-z2qqwy7e.js +0 -2
  236. package/dist/chunk-zam6ecam.js +0 -60
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Detect sites whose files are still on a box that no longer serves them.
3
+ *
4
+ * `site:move` deliberately never deletes anything on the source: it stops the
5
+ * units, drops the gateway route, and leaves the whole tree in place. That
6
+ * leftover tree IS the rollback — the one thing that makes a bad cutover
7
+ * recoverable, and the reason the operation can promise reversibility "until the
8
+ * source server is destroyed".
9
+ *
10
+ * Which makes destroying that server the moment the promise expires, and nothing
11
+ * about the box says so. It has no running units for the site, so it looks idle;
12
+ * the config has moved on; the operator is tidying up. Terminating it is exactly
13
+ * the right thing to do once the move is verified, and exactly the wrong thing
14
+ * to do before — and those two look identical from outside.
15
+ *
16
+ * So: before a teardown, ask the box whether it is holding anyone's rollback.
17
+ *
18
+ * @see https://github.com/stacksjs/ts-cloud/issues/167
19
+ */
20
+ /**
21
+ * Report every one of this project's site trees on the box and whether anything
22
+ * is still running for it.
23
+ *
24
+ * Read-only by construction — it lists and queries, and that is all. A scan that
25
+ * could change the box would be a poor thing to run immediately before deciding
26
+ * whether to keep it.
27
+ *
28
+ * A directory is only counted when it holds `releases/`, so a stray folder under
29
+ * `/var/www` is not mistaken for a deployed site and does not block a teardown
30
+ * the operator genuinely wants.
31
+ */
32
+ export declare function buildDrainedSiteScanScript(slug: string, wwwRoot?: string): string[];
33
+ export interface DrainedSite {
34
+ name: string;
35
+ /** Human-readable size of the tree left behind, e.g. `1.2G`. */
36
+ size: string;
37
+ }
38
+ /**
39
+ * Sites the box still holds but no longer runs.
40
+ *
41
+ * A site with something active is NOT drained — it is simply a site on a box
42
+ * being torn down, which is ordinary and already covered by the teardown's own
43
+ * confirmation. The interesting case is files with nothing running: either a
44
+ * completed `site:move` whose rollback this is, or a site that has been stopped
45
+ * and forgotten. Both are worth a sentence before the disk goes away.
46
+ */
47
+ export declare function parseDrainedSites(output: string | undefined): DrainedSite[];
48
+ /**
49
+ * What the operator sees instead of a teardown.
50
+ *
51
+ * Names the sites, says what the files are FOR, and gives the two ways forward.
52
+ * The flag is deliberately its own thing rather than `--force`: `--force` exists
53
+ * so a teardown can run unattended, and a CI job that skips a prompt must not
54
+ * also silently discard the only copy of a rollback.
55
+ */
56
+ export declare function formatDrainedSiteRefusal(sites: readonly DrainedSite[], slug: string, flag: string): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Deploy a built static site to Cloudflare Pages.
3
+ *
4
+ * Pages is the one Cloudflare product that is not reachable through the zone
5
+ * API the rest of this codebase talks to: projects are account-scoped, and
6
+ * uploads go through a separate content-addressed flow rather than a single
7
+ * PUT. The shape of that flow drives this module:
8
+ *
9
+ * - **Files are addressed by hash, not by path.** A deployment is a manifest
10
+ * mapping `/path` to a hash; the bytes are uploaded separately, and only for
11
+ * hashes the account has never seen. A redeploy of a site whose CSS did not
12
+ * change re-uploads nothing for that file, which is why `deployDirectory`
13
+ * checks `/pages/assets/check-missing` before sending anything.
14
+ * - **The hash is Cloudflare's own, not a plain digest.** It is a blake3 of
15
+ * the base64 payload concatenated with the file extension, truncated to 32
16
+ * hex characters. Getting this wrong does not error — it uploads fine and
17
+ * then serves 404s, because the manifest points at a hash the asset store
18
+ * does not hold under that name.
19
+ * - **Upload auth is a separate, short-lived JWT**, fetched from
20
+ * `/pages/projects/{name}/upload-token`. The account API token cannot be
21
+ * used against the asset endpoints.
22
+ *
23
+ * A custom domain is attached with {@link attachCustomDomain}, which is
24
+ * idempotent, and still needs a CNAME in DNS — see {@link pagesDnsRecord}.
25
+ */
26
+ import type { DnsRecord } from '../dns/types';
27
+ /** Cloudflare's asset-upload ceiling. Larger files are rejected outright. */
28
+ export declare const PAGES_MAX_FILE_BYTES: number;
29
+ export interface CloudflarePagesProject {
30
+ id: string;
31
+ name: string;
32
+ subdomain: string;
33
+ domains: string[];
34
+ production_branch?: string;
35
+ }
36
+ export interface CloudflarePagesDeployment {
37
+ id: string;
38
+ url: string;
39
+ environment: string;
40
+ project_name: string;
41
+ }
42
+ export interface CloudflarePagesOptions {
43
+ /** Account-scoped API token with the `Cloudflare Pages: Edit` permission. */
44
+ apiToken: string;
45
+ /** Account the project lives under. */
46
+ accountId: string;
47
+ }
48
+ /** One file in a deployment, already hashed. */
49
+ export interface PagesAsset {
50
+ /** Site-absolute path, e.g. `/guide/index.html`. */
51
+ path: string;
52
+ /** Cloudflare asset hash (32 hex chars). */
53
+ hash: string;
54
+ /** Base64 of the file's bytes. */
55
+ base64: string;
56
+ /** Extension without the dot, e.g. `html`. Empty when there is none. */
57
+ extension: string;
58
+ /** Byte length of the original file, for reporting and limit checks. */
59
+ size: number;
60
+ }
61
+ export interface DeployDirectoryResult {
62
+ deployment: CloudflarePagesDeployment;
63
+ /** Every asset in the manifest. */
64
+ total: number;
65
+ /** Assets whose bytes had to be sent, the rest already being in the store. */
66
+ uploaded: number;
67
+ /** Bytes actually transferred. */
68
+ uploadedBytes: number;
69
+ }
70
+ /**
71
+ * Cloudflare Pages, scoped to one account.
72
+ *
73
+ * Mirrors {@link CloudflareProvider}: a thin authenticated `request`, and
74
+ * methods that each map onto one API concern.
75
+ */
76
+ export declare class CloudflarePagesProvider {
77
+ readonly name = "cloudflare-pages";
78
+ private readonly apiToken;
79
+ private readonly accountId;
80
+ constructor(options: CloudflarePagesOptions);
81
+ /**
82
+ * Make an authenticated request against the account-scoped Pages API.
83
+ *
84
+ * @param method - HTTP method
85
+ * @param endpoint - Path below `/accounts/{id}`, starting with a slash
86
+ * @param body - JSON body, or a `FormData` for multipart endpoints
87
+ * @returns The unwrapped `result`
88
+ * @throws {Error} When Cloudflare reports failure
89
+ */
90
+ private request;
91
+ /**
92
+ * Look up a project by name.
93
+ *
94
+ * @param name - Project name
95
+ * @returns The project, or `null` when it does not exist
96
+ */
97
+ getProject(name: string): Promise<CloudflarePagesProject | null>;
98
+ /**
99
+ * Create a direct-upload project, or return the existing one.
100
+ *
101
+ * Direct upload means ts-cloud pushes the built output itself rather than
102
+ * Cloudflare cloning and building the repository. That keeps the build in
103
+ * your CI, where the toolchain and secrets already are.
104
+ *
105
+ * @param name - Project name; also the `<name>.pages.dev` subdomain
106
+ * @param productionBranch - Branch treated as production. @default 'main'
107
+ * @returns The project, whether created here or already present
108
+ */
109
+ ensureProject(name: string, productionBranch?: string): Promise<CloudflarePagesProject>;
110
+ /**
111
+ * Short-lived JWT for the asset endpoints.
112
+ *
113
+ * The account API token is not accepted there, so this is fetched per deploy
114
+ * rather than held.
115
+ *
116
+ * @param project - Project name
117
+ */
118
+ private uploadToken;
119
+ /**
120
+ * Ask which of these hashes the asset store does not already hold.
121
+ *
122
+ * @param jwt - Upload token
123
+ * @param hashes - Every hash in the manifest
124
+ * @returns The subset whose bytes still need sending
125
+ */
126
+ private missingHashes;
127
+ /**
128
+ * Record hashes as present in the account's asset store.
129
+ *
130
+ * Uploading bytes is not by itself enough to make them findable: until the
131
+ * hashes are upserted, `check-missing` keeps reporting them absent, and every
132
+ * subsequent deploy re-uploads the entire site. Skipping this call is not
133
+ * visible in a single deploy — it only shows up as an upload that never gets
134
+ * cheaper.
135
+ *
136
+ * @param jwt - Upload token
137
+ * @param hashes - Every hash in the manifest, not only the newly uploaded ones
138
+ */
139
+ private upsertHashes;
140
+ /**
141
+ * Upload one batch of assets.
142
+ *
143
+ * @param jwt - Upload token
144
+ * @param assets - At most {@link UPLOAD_BATCH_SIZE} assets
145
+ */
146
+ private uploadBatch;
147
+ /**
148
+ * Create a deployment from an already-hashed manifest.
149
+ *
150
+ * @param project - Project name
151
+ * @param assets - Every file in the site
152
+ * @param branch - Branch to record. Matching the production branch publishes
153
+ * to the live domain; anything else becomes a preview deployment.
154
+ * @returns The created deployment
155
+ */
156
+ createDeployment(project: string, assets: PagesAsset[], branch?: string): Promise<CloudflarePagesDeployment>;
157
+ /**
158
+ * Build, upload and deploy a directory.
159
+ *
160
+ * The whole flow: hash every file, ask what is missing, upload only that,
161
+ * then create the deployment.
162
+ *
163
+ * @param options.project - Project name
164
+ * @param options.directory - Directory to publish; becomes the site root
165
+ * @param options.branch - Branch to record on the deployment
166
+ * @param options.onProgress - Called as batches complete, for CLI output
167
+ * @returns The deployment and what it cost to send
168
+ * @throws {Error} When the directory is empty or a file exceeds the size limit
169
+ */
170
+ deployDirectory(options: {
171
+ project: string;
172
+ directory: string;
173
+ branch?: string;
174
+ onProgress?: (uploaded: number, total: number) => void;
175
+ }): Promise<DeployDirectoryResult>;
176
+ /** Custom domains already attached to a project. */
177
+ listCustomDomains(project: string): Promise<string[]>;
178
+ /**
179
+ * Attach a custom domain, or leave it attached.
180
+ *
181
+ * Idempotent so a deploy pipeline can call it on every run rather than
182
+ * guarding with a lookup that races.
183
+ *
184
+ * @param project - Project name
185
+ * @param domain - Hostname to serve, e.g. `buddy.sh`
186
+ * @returns Whether this call is what attached it
187
+ */
188
+ attachCustomDomain(project: string, domain: string): Promise<boolean>;
189
+ }
190
+ /**
191
+ * The DNS record that points a custom domain at a Pages project.
192
+ *
193
+ * Pages serves from a `*.pages.dev` host, so the zone needs a CNAME. Two things
194
+ * are easy to get wrong here:
195
+ *
196
+ * - **The target is the project's `subdomain`, not `<name>.pages.dev`.**
197
+ * Cloudflare only grants the bare name when it is free; a project named
198
+ * `buddy` whose name was taken is served from `buddy-5yo.pages.dev`.
199
+ * Deriving the host from the project name instead of reading `subdomain`
200
+ * produces a CNAME to a host that does not exist.
201
+ * - **It must be proxied.** An unproxied CNAME reaches Cloudflare's edge but
202
+ * arrives without the routing metadata Pages uses to pick a project, and
203
+ * answers 404.
204
+ *
205
+ * A CNAME at the apex is legal here only because Cloudflare flattens it.
206
+ *
207
+ * @param hostname - Public hostname, e.g. `buddy.sh` or `www.buddy.sh`
208
+ * @param subdomain - The project's `subdomain`, e.g. `buddy-5yo.pages.dev`
209
+ * @returns A record ready for `CloudflareProvider.upsertRecord`
210
+ */
211
+ export declare function pagesDnsRecord(hostname: string, subdomain: string): DnsRecord;
212
+ /**
213
+ * Cloudflare's asset hash: BLAKE3 of `<base64><extension>`, first 32 hex chars.
214
+ *
215
+ * The extension is part of the input, so the same bytes saved as `.html` and
216
+ * as `.txt` hash differently — which is deliberate, since the asset store
217
+ * serves content type from the key.
218
+ *
219
+ * BLAKE3 specifically, and it has to come from a library: Bun's `CryptoHasher`
220
+ * ships blake2b but not blake3. Substituting a different digest is not a
221
+ * degradation that shows up as an error — the upload succeeds and the site
222
+ * then serves 404s, because the manifest names a key the store does not hold.
223
+ *
224
+ * @param base64 - Base64 of the file's bytes
225
+ * @param extension - Extension without the dot
226
+ */
227
+ export declare function pagesAssetHash(base64: string, extension: string): Promise<string>;
228
+ /**
229
+ * Walk a directory into a deployable manifest.
230
+ *
231
+ * Dotfiles are included: a built site legitimately contains `.well-known`, and
232
+ * bunpress writes its output under a `.bunpress` root. Cloudflare's own
233
+ * reserved paths are skipped, since uploading them as assets shadows the
234
+ * configuration they are meant to be.
235
+ *
236
+ * @param directory - Site root
237
+ * @returns One entry per file, hashed and base64-encoded
238
+ * @throws {Error} When a file exceeds {@link PAGES_MAX_FILE_BYTES}
239
+ */
240
+ export declare function collectAssets(directory: string): Promise<PagesAsset[]>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { CloudflarePagesProvider, collectAssets, PAGES_MAX_FILE_BYTES, pagesAssetHash, pagesDnsRecord, } from './cloudflare-pages';
2
+ export type { CloudflarePagesDeployment, CloudflarePagesOptions, CloudflarePagesProject, DeployDirectoryResult, PagesAsset, } from './cloudflare-pages';
@@ -1 +1 @@
1
- import{$k as z,_k as y,al as A,bl as B,cl as C,dl as D,el as E,fl as F,gl as G,hl as H,il as I,jl as J,kl as K,ll as L,ml as M}from"../chunk-1qdne0bf.js";import{nl as s,ol as t,pl as u,ql as v,rl as w,sl as x}from"../chunk-hn40z11p.js";import{Al as r,tl as k,ul as l,vl as m,wl as n,xl as o,yl as p,zl as q}from"../chunk-x3hay61c.js";import{Ns as a,Os as b,Ps as c,Qs as d,Rs as e,Ss as f,Ts as g,Us as h,Vs as i,Ws as j}from"../chunk-zz52xje8.js";import"../chunk-z2qqwy7e.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};
1
+ import{$k as z,_k as y,al as A,bl as B,cl as C,dl as D,el as E,fl as F,gl as G,hl as H,il as I,jl as J,kl as K,ll as L,ml as M}from"../chunk-fvjczc73.js";import{nl as s,ol as t,pl as u,ql as v,rl as w,sl as x}from"../chunk-q0wffgw5.js";import{Al as r,tl as k,ul as l,vl as m,wl as n,xl as o,yl as p,zl as q}from"../chunk-wcrj06cg.js";import{Ns as a,Os as b,Ps as c,Qs as d,Rs as e,Ss as f,Ts as g,Us as h,Vs as i,Ws as j}from"../chunk-6xcbfs2r.js";import"../chunk-rr1yaj55.js";export{x as wafEventTelemetry,m as sysctlHardening,A as ruleMatches,w as renderWafInstallScript,v as renderWafConfig,n as renderSysctlFile,o as renderNftablesRuleset,p as renderDdosInstallScript,j as recursionBlockedResponse,B as rateLimitKey,D as rateLimitHeaders,i as propagationHeaders,q as planMitigation,f as parseChain,M as mergeControlsIntoConfig,l as isValidCidr,t as isSafePattern,g as inspectInvocation,u as globToRegex,z as globMatches,d as functionFingerprint,s as escapeSecLang,J as describePosture,E as defaultRateLimitRules,r as applyMitigationFactor,K as applyControlsToRateLimits,L as applyControlsToDdos,c as TRACE_HEADER,h as RecursionGuard,C as RateLimiter,y as RATE_LIMIT_KEY_SEPARATOR,I as ProtectionControlStore,F as PROTECTION_SETTING,G as MAX_CONTROL_HOURS,a as DEPTH_HEADER,e as DEFAULT_RECURSION_LIMITS,k as DEFAULT_DDOS_THRESHOLDS,H as DEFAULT_ATTACK_MODE_HOURS,b as CHAIN_HEADER};
@@ -1 +1 @@
1
- import"../chunk-z2qqwy7e.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};
1
+ import"../chunk-rr1yaj55.js";import*as K from"node:http2";import{createSign as U}from"node:crypto";var q="api.push.apple.com",D="api.sandbox.push.apple.com",M=2700000;class E{config;token=null;tokenGeneratedAt=0;client=null;host;constructor(z){this.config=z,this.host=z.production?q:D}generateToken(){let z=Math.floor(Date.now()/1000);if(this.token&&Date.now()-this.tokenGeneratedAt<M)return this.token;let J={alg:"ES256",kid:this.config.keyId},Q={iss:this.config.teamId,iat:z},V=Buffer.from(JSON.stringify(J)).toString("base64url"),W=Buffer.from(JSON.stringify(Q)).toString("base64url"),Z=`${V}.${W}`,$=U("SHA256");$.update(Z);let B=$.sign(this.config.privateKey),x=this.derToRaw(B).toString("base64url");return this.token=`${Z}.${x}`,this.tokenGeneratedAt=Date.now(),this.token}derToRaw(z){let J=2;if(z[J]!==2)throw Error("Invalid DER signature: expected 0x02 for R");J++;let Q=z[J];J++;let V=z.subarray(J,J+Q);if(J+=Q,z[J]!==2)throw Error("Invalid DER signature: expected 0x02 for S");J++;let W=z[J];J++;let Z=z.subarray(J,J+W);if(V[0]===0&&V.length===33)V=V.subarray(1);if(Z[0]===0&&Z.length===33)Z=Z.subarray(1);let $=Buffer.alloc(64);return V.copy($,32-V.length),Z.copy($,64-Z.length),$}async getClient(){if(this.client&&!this.client.destroyed)return this.client;return new Promise((z,J)=>{this.client=K.connect(`https://${this.host}`),this.client.on("error",(Q)=>{J(Q)}),this.client.on("connect",()=>{z(this.client)}),this.client.on("close",()=>{this.client=null})})}buildPayload(z){let J={};if(z.title||z.body||z.subtitle){if(J.alert={},z.title)J.alert.title=z.title;if(z.subtitle)J.alert.subtitle=z.subtitle;if(z.body)J.alert.body=z.body}if(z.badge!==void 0)J.badge=z.badge;if(z.sound!==void 0)J.sound=z.sound;if(z.category)J.category=z.category;if(z.threadId)J["thread-id"]=z.threadId;if(z.contentAvailable)J["content-available"]=1;if(z.mutableContent)J["mutable-content"]=1;if(z.targetContentId)J["target-content-id"]=z.targetContentId;let Q={aps:J};if(z.data)Object.assign(Q,z.data);return Q}async send(z){try{let J=await this.getClient(),Q=this.generateToken(),V=JSON.stringify(this.buildPayload(z)),W={":method":"POST",":path":`/3/device/${z.deviceToken}`,authorization:`bearer ${Q}`,"apns-topic":this.config.bundleId,"apns-push-type":z.pushType||"alert","apns-priority":String(z.priority||10)};if(z.expiration!==void 0)W["apns-expiration"]=String(z.expiration);if(z.collapseId)W["apns-collapse-id"]=z.collapseId;return new Promise((Z)=>{let $=J.request(W),B="",j=0,x;$.on("response",(Y)=>{j=Y[":status"],x=Y["apns-id"]}),$.on("data",(Y)=>{B+=Y.toString()}),$.on("end",()=>{if(j===200)Z({success:!0,deviceToken:z.deviceToken,apnsId:x,statusCode:j});else{let Y="Unknown error",X,w;if(B)try{let G=JSON.parse(B);X=G.reason,w=G.timestamp,Y=X||Y}catch{Y=B}Z({success:!1,deviceToken:z.deviceToken,apnsId:x,statusCode:j,error:Y,reason:X,timestamp:w})}}),$.on("error",(Y)=>{Z({success:!1,deviceToken:z.deviceToken,error:Y.message})}),$.write(V),$.end()})}catch(J){return{success:!1,deviceToken:z.deviceToken,error:J.message}}}async sendBatch(z,J){let Q=J?.concurrency||10,V=[];for(let W=0;W<z.length;W+=Q){let Z=z.slice(W,W+Q),$=await Promise.all(Z.map((B)=>this.send(B)));V.push(...$)}return{sent:V.filter((W)=>W.success).length,failed:V.filter((W)=>!W.success).length,results:V}}async sendSimple(z,J,Q,V){return this.send({deviceToken:z,title:J,body:Q,data:V})}async sendSilent(z,J){return this.send({deviceToken:z,contentAvailable:!0,pushType:"background",priority:5,data:J})}close(){if(this.client)this.client.close(),this.client=null;this.token=null,this.tokenGeneratedAt=0}}import{createSign as L}from"node:crypto";var R="https://fcm.googleapis.com/v1/projects",F="https://oauth2.googleapis.com/token",b=3300000;class H{config;accessToken=null;tokenExpiresAt=0;constructor(z){this.config=z}static fromServiceAccount(z){return new H({projectId:z.project_id,clientEmail:z.client_email,privateKey:z.private_key})}generateJWT(){let z=Math.floor(Date.now()/1000),J=z+3600,Q={alg:"RS256",typ:"JWT"},V={iss:this.config.clientEmail,sub:this.config.clientEmail,aud:F,iat:z,exp:J,scope:"https://www.googleapis.com/auth/firebase.messaging"},W=Buffer.from(JSON.stringify(Q)).toString("base64url"),Z=Buffer.from(JSON.stringify(V)).toString("base64url"),$=`${W}.${Z}`,B=L("SHA256");B.update($);let j=B.sign(this.config.privateKey,"base64url");return`${$}.${j}`}async getAccessToken(){if(this.accessToken&&Date.now()<this.tokenExpiresAt)return this.accessToken;let z=this.generateJWT(),J=await fetch(F,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",assertion:z}).toString()});if(!J.ok){let V=await J.text();throw Error(`Failed to get access token: ${V}`)}let Q=await J.json();return this.accessToken=Q.access_token,this.tokenExpiresAt=Date.now()+b,this.accessToken}buildMessage(z){let J={};if(z.token)J.token=z.token;else if(z.topic)J.topic=z.topic;else if(z.condition)J.condition=z.condition;if(z.title||z.body||z.imageUrl){if(J.notification={},z.title)J.notification.title=z.title;if(z.body)J.notification.body=z.body;if(z.imageUrl)J.notification.image=z.imageUrl}if(z.data&&Object.keys(z.data).length>0)J.data=z.data;if(z.android){if(J.android={priority:z.android.priority||"high"},z.android.ttl)J.android.ttl=`${z.android.ttl}s`;if(z.android.collapseKey)J.android.collapse_key=z.android.collapseKey;if(z.android.directBootOk)J.android.direct_boot_ok=z.android.directBootOk;let Q={};if(z.android.channelId)Q.channel_id=z.android.channelId;if(z.android.icon)Q.icon=z.android.icon;if(z.android.color)Q.color=z.android.color;if(z.android.sound)Q.sound=z.android.sound;if(z.android.clickAction)Q.click_action=z.android.clickAction;if(z.android.tag)Q.tag=z.android.tag;if(z.android.visibility)Q.visibility=z.android.visibility;if(z.android.notificationCount!==void 0)Q.notification_count=z.android.notificationCount;if(Object.keys(Q).length>0)J.android.notification=Q}if(z.webpush)J.webpush=z.webpush;if(z.apns)J.apns=z.apns;if(z.fcmOptions)J.fcm_options=z.fcmOptions;return{message:J}}async send(z){try{let J=await this.getAccessToken(),Q=this.buildMessage(z),V=await fetch(`${R}/${this.config.projectId}/messages:send`,{method:"POST",headers:{Authorization:`Bearer ${J}`,"Content-Type":"application/json"},body:JSON.stringify(Q)}),W=await V.json();if(V.ok)return{success:!0,messageId:W.name};else return{success:!1,error:W.error?.message||"Unknown error",errorCode:W.error?.status}}catch(J){return{success:!1,error:J.message}}}async sendBatch(z,J,Q){let V=Q?.concurrency||10,W=[];for(let Z=0;Z<z.length;Z+=V){let B=z.slice(Z,Z+V).map(async(x)=>{return{...await this.send({...J,token:x}),token:x}}),j=await Promise.all(B);W.push(...j)}return{sent:W.filter((Z)=>Z.success).length,failed:W.filter((Z)=>!Z.success).length,results:W}}async sendToTopic(z,J){return this.send({...J,topic:z})}async sendToCondition(z,J){return this.send({...J,condition:z})}async sendSimple(z,J,Q,V){return this.send({token:z,title:J,body:Q,data:V})}async sendSilent(z,J){return this.send({token:z,data:J,android:{priority:"high"}})}async subscribeToTopic(z,J){try{let Q=await this.getAccessToken(),V=await fetch("https://iid.googleapis.com/iid/v1:batchAdd",{method:"POST",headers:{Authorization:`Bearer ${Q}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${J}`,registration_tokens:z})});if(V.ok)return{success:!0};else return{success:!1,error:(await V.json()).error?.message||"Failed to subscribe"}}catch(Q){return{success:!1,error:Q.message}}}async unsubscribeFromTopic(z,J){try{let Q=await this.getAccessToken(),V=await fetch("https://iid.googleapis.com/iid/v1:batchRemove",{method:"POST",headers:{Authorization:`Bearer ${Q}`,"Content-Type":"application/json"},body:JSON.stringify({to:`/topics/${J}`,registration_tokens:z})});if(V.ok)return{success:!0};else return{success:!1,error:(await V.json()).error?.message||"Failed to unsubscribe"}}catch(Q){return{success:!1,error:Q.message}}}}export{H as FCMClient,E as APNsClient};
@@ -1 +1 @@
1
- import{$j as _,Aj as z,Bj as A,Cj as B,Dj as C,Ej as D,Fj as E,Gj as F,Hj as G,Ij as H,Jj as I,Jk as qa,Kj as J,Kk as ra,Lj as K,Lk as sa,Mj as L,Mk as ta,Nj as M,Nk as ua,Oj as N,Ok as va,Pj as O,Pk as wa,Qj as P,Qk as xa,Rj as Q,Rk as ya,Sj as R,Sk as za,Tj as S,Tk as Aa,Uj as T,Uk as Ba,Vj as U,Wj as V,Xj as W,Yj as X,Yk as Ca,Zj as Y,Zk as Da,_j as Z,ak as aa,bj as a,bk as ba,cj as b,ck as ca,dj as c,dk as da,ej as d,ek as ea,fj as e,fk as fa,gj as f,gk as ga,hj as g,hk as ha,ij as h,ik as ia,jj as i,jk as ja,kj as j,kk as ka,lj as k,lk as la,mj as l,mk as ma,nj as m,nk as na,oj as n,ok as oa,pj as o,pk as pa,qj as p,rj as q,sj as r,tj as s,uj as t,vj as u,wj as v,xj as w,yj as x,zj as y}from"../chunk-aw0cgv5w.js";import"../chunk-x3hay61c.js";import"../chunk-zz52xje8.js";import"../chunk-z2qqwy7e.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};
1
+ import{$j as _,Aj as z,Bj as A,Cj as B,Dj as C,Ej as D,Fj as E,Gj as F,Hj as G,Ij as H,Jj as I,Jk as pa,Kj as J,Kk as qa,Lj as K,Lk as ra,Mj as L,Mk as sa,Nj as M,Nk as ta,Oj as N,Ok as ua,Pj as O,Pk as va,Qj as P,Qk as wa,Rj as Q,Rk as xa,Sj as R,Sk as ya,Tj as S,Tk as za,Uj as T,Uk as Aa,Vj as U,Wj as V,Xj as W,Yj as X,Yk as Ba,Zj as Y,Zk as Ca,_j as Z,ak as $,bj as a,bk as aa,cj as b,ck as ba,dj as c,dk as ca,ej as d,ek as da,fj as e,fk as ea,gj as f,gk as fa,hj as g,hk as ga,ij as h,ik as ha,jj as i,jk as ia,kj as j,kk as ja,lj as k,lk as ka,mj as l,mk as la,nj as m,nk as ma,oj as n,ok as na,pj as o,pk as oa,qj as p,rj as q,sj as r,tj as s,uj as t,vj as u,wj as v,xj as w,yj as x,zj as y}from"../chunk-y1dh3znq.js";import"../chunk-wcrj06cg.js";import"../chunk-6xcbfs2r.js";import"../chunk-rr1yaj55.js";export{n as zonedParts,o as zoneOffsetMs,ia as zeroFill,p as utcInstantOfLocal,w as usageDeltaKey,ea as toFocusRecords,fa as toFocusJsonl,f as tieredMicroCents,O as strongestActiveAction,ga as streamFocusJsonl,xa as startSpendLoop,Ca as spendOpenApiPaths,ra as spendNotificationText,$ as signalDefinition,A as scopeKey,N as runEnforcement,Y as routeMatches,j as roundCents,S as robustZScore,D as projectSpend,i as priceUsageLines,h as priceUsage,r as previousBudgetWindow,M as planEnforcement,E as percentOfLimit,ba as optionsForSignal,y as meterTelemetry,k as mergePriceBooks,J as mergeDecisions,R as medianAbsoluteDeviation,Q as median,ca as lookbackHoursForSignal,m as isValidTimeZone,P as isOperationAllowed,t as hourBucketsBetween,s as hourBucket,Ba as handleSpendRequest,H as governingLimitCents,F as formatTimeToExhaustion,qa as formatCents,g as findPriceEntry,v as findMapping,T as ewma,I as evaluateBudget,V as detectLatestAnomaly,U as detectAnomalies,na as createEnforcementHandlers,Aa as compositeSpendTransport,q as budgetWindow,ma as assertSpendAllows,W as anomalyOptionsForSignal,z as aggregateDeltas,L as USER_VISIBLE_ACTIONS,pa as URGENT_SPEND_EVENTS,C as SpendStore,ha as SpendService,va as SpendRunner,sa as SpendNotificationRouter,ua as SpendLoopLease,ka as SpendGate,la as SpendCapError,aa as SignalSource,ta as SPEND_LEASE_SETTING,ja as SPEND_GATE_SETTING,wa as SPEND_CYCLE_SECONDS,X as SENSITIVITY_THRESHOLDS,oa as RecordingSpendTransport,G as MIN_PROJECTION_CONFIDENCE,e as MICRO_CENTS,a as METER_KEYS,b as METERS,da as FOCUS_VERSION,d as ENFORCEMENT_SEVERITY,K as ENFORCEMENT_DESCRIPTIONS,c as ENFORCEMENT_ACTIONS,_ as DETECTABLE_SIGNALS,B as DEFAULT_THRESHOLDS,l as DEFAULT_PRICE_BOOK,u as DEFAULT_METER_MAPPINGS,x as CounterTracker,za as ComputeSpendTransport,ya as AwsSpendTransport,Z as AnomalyConfigStore};
package/dist/ui/404.html CHANGED
@@ -2,7 +2,7 @@
2
2
  <html lang="en" data-theme="dark">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <meta name="stx-build" content="mt68a7naip9gue">
5
+ <meta name="stx-build" content="mt7lhcctpiur4a">
6
6
 
7
7
  <!-- stx SEO Tags -->
8
8
  <meta name="title" content="Page not found · ts-cloud">
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="mt689snetu3vh5"><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">
1
+ <!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="mt7lgvq3gjbx1h"><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="mt689snetu3vh5"><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.c696d17a.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 &amp; secrets</a><a class="" href="/data/backups?env=production">Backups &amp; 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">
1
+ <!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="mt7lgvq3gjbx1h"><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.c696d17a.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 &amp; secrets</a><a class="" href="/data/backups?env=production">Backups &amp; 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="mt689snetu3vh5"><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.c696d17a.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ts-cloud - API &amp; 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_1787514988755_0() {
1
+ <!DOCTYPE html><html lang="en" data-theme="dark"><head><meta charset="UTF-8"><meta name="stx-build" content="mt7lgvq3gjbx1h"><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.c696d17a.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ts-cloud - API &amp; 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_1787597620495_7() {
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=__stx_setup_1787514988755_0;
193
- if(window.stx)window.stx._latestSetup=__stx_setup_1787514988755_0;
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_1787514988755_0"><a data-stx-scope="stx_scope_nav_1" 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 &amp; secrets</a><a class="" href="/data/backups?env=production">Backups &amp; 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">
192
+ window.__stx_latestSetup=__stx_setup_1787597620495_7;
193
+ if(window.stx)window.stx._latestSetup=__stx_setup_1787597620495_7;
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_1787597620495_7"><a data-stx-scope="stx_scope_nav_10" 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 &amp; secrets</a><a class="" href="/data/backups?env=production">Backups &amp; 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_1787514988755_0;
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['stx_scope_nav_1'] = __scopeRegistration;
545
+ window.stx._scopes['stx_scope_nav_10'] = __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 &amp; automation</div><header class="page"><div><h1>API &amp; 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>