@samyx/preview-stacks-client 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/index.d.ts +78 -14
- package/dist/index.js +17 -3
- package/dist/types.d.ts +125 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @samyx/preview-stacks-client
|
|
2
2
|
|
|
3
3
|
Typed API client for a [pstack](https://github.com/samishal1998/preview-stacks) control plane —
|
|
4
|
-
deployments, jobs, readiness, containers, logs and
|
|
5
|
-
receiving end.
|
|
4
|
+
deployments, jobs, readiness, containers, logs, notifiers and accounts, plus webhook verification for
|
|
5
|
+
the receiving end.
|
|
6
6
|
|
|
7
7
|
Zero dependencies. Works in Node, Bun, Deno and the browser (it uses `fetch` and WebCrypto, nothing
|
|
8
8
|
else).
|
|
@@ -110,6 +110,35 @@ const script = await pstack.swarm.join({ format: 'script' }); // a SECRET: tre
|
|
|
110
110
|
// formats: 'token' | 'command' | 'script' | 'cloud-config' (+ distro for cloud-config)
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
### Accounts and roles
|
|
114
|
+
|
|
115
|
+
Every account holds one of four ordered roles. Each includes everything below it:
|
|
116
|
+
|
|
117
|
+
| Role | What it adds |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `viewer` | every read — deployments, jobs, logs, specs, routing, notifiers, the swarm, the user roster |
|
|
120
|
+
| `developer` | stacks: `up` / `down` / `verify` / `sleep` / `wake`, container actions, specs, the terminal |
|
|
121
|
+
| `maintainer` | host configuration: host vars, registries, routing files, notifiers, the swarm join material |
|
|
122
|
+
| `admin` | people, and anything that can mint them: accounts, roles, SSO configuration, sealed config |
|
|
123
|
+
|
|
124
|
+
`PSTACK_TOKEN` is above all four (`me().root === true`) and holds no account. A share-link token is
|
|
125
|
+
not a role at all — it reaches the views its link names, on its one deployment.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const me = await pstack.me();
|
|
129
|
+
if (!me.root && me.user?.role !== 'admin') throw new Error('this script mints accounts');
|
|
130
|
+
|
|
131
|
+
const ci = await pstack.users.create({ username: 'ci-bot', password, role: 'developer' });
|
|
132
|
+
await pstack.users.setRole(ci.id, 'maintainer'); // takes effect on that account's next request
|
|
133
|
+
await pstack.users.remove(ci.id);
|
|
134
|
+
const roster = await pstack.users.list(); // a viewer may read this
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**An omitted `role` means `viewer`.** `users.create` used to produce an *administrator* every time; a
|
|
138
|
+
script that relied on that must now say `role: 'admin'` and mean it. `user.role` is typed `string`,
|
|
139
|
+
not `Role`, on the way back: the column is one an operator can repair by hand, and the server ranks a
|
|
140
|
+
value it does not recognise *below* `viewer` — read an unfamiliar role as less, never as more.
|
|
141
|
+
|
|
113
142
|
### Single sign-on
|
|
114
143
|
|
|
115
144
|
The sign-in itself is two browser redirects (`/api/auth/sso/start` → the provider →
|
|
@@ -153,6 +182,8 @@ Bun.serve({
|
|
|
153
182
|
(`/api/deployments/:id/logs/stream`) and is left to `EventSource` or your own reader.
|
|
154
183
|
- **No terminal.** That is a WebSocket carrying a session cookie, which belongs to a browser.
|
|
155
184
|
- **No spec authoring helpers.** A spec is YAML you own; this client submits it verbatim.
|
|
185
|
+
- **No password changes.** `PUT /api/users/:id/password` signs that account out of everything it has;
|
|
186
|
+
it is a person's job at a keyboard, not a script's.
|
|
156
187
|
|
|
157
188
|
Anything not wrapped is still reachable with the same auth and error handling:
|
|
158
189
|
|
package/dist/index.d.ts
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
*
|
|
23
23
|
* `waitForJob`, `waitForReady` and `verifyWebhook` (below). They are the three things every user
|
|
24
24
|
* would otherwise re-write, and the two waiters are the ones that are easy to get subtly wrong —
|
|
25
|
-
* polling a job without a terminal-state list
|
|
26
|
-
* error rather than "ask again".
|
|
25
|
+
* polling a job without a terminal-state list (`queued` is not running and not finished either), or
|
|
26
|
+
* treating `readiness.state === 'watching'` as an error rather than "ask again".
|
|
27
27
|
*/
|
|
28
|
-
import type { DeliveryRow, DeploymentRow, Health, HostVar, Job, Kind, Logs, NotifierRow, Readiness, Runtime, ShareLink, ShareView, SpecMeta, SsoConfig, SsoConfigResponse, SwarmInfo } from './types.ts';
|
|
28
|
+
import type { CancelStack, DeliveryRow, DeploymentRow, Health, HostVar, Job, Kind, Logs, Me, NotifierRow, Readiness, Role, Runtime, ShareLink, ShareView, SpecMeta, SsoConfig, SsoConfigResponse, SwarmInfo, User } from './types.ts';
|
|
29
29
|
export * from './types.ts';
|
|
30
30
|
/** A non-2xx answer. `body` is the parsed JSON, which is where the server's own message lives. */
|
|
31
31
|
export declare class PstackError extends Error {
|
|
@@ -46,8 +46,14 @@ export type ClientOptions = {
|
|
|
46
46
|
* else, every route needs it.
|
|
47
47
|
*/
|
|
48
48
|
token?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Swap in for tests, a proxy, or an agent with custom TLS. Defaults to global `fetch`.
|
|
51
|
+
*
|
|
52
|
+
* The SIGNATURE, not `typeof fetch`: the global carries a `preconnect` property in both Bun's and
|
|
53
|
+
* Node's lib types, so `typeof fetch` rejects every plain arrow function — which is the only
|
|
54
|
+
* shape anyone actually passes here. This is what the client calls, and the global still fits it.
|
|
55
|
+
*/
|
|
56
|
+
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
51
57
|
/** Per request, in ms. Deploys are started, never awaited, so this bounds the HTTP call only. */
|
|
52
58
|
timeoutMs?: number;
|
|
53
59
|
};
|
|
@@ -57,6 +63,11 @@ export declare function createClient(opts: ClientOptions): {
|
|
|
57
63
|
/** Escape hatch: any route this client does not wrap yet, with auth and error handling applied. */
|
|
58
64
|
request: <T>(method: string, path: string, body?: unknown) => Promise<T>;
|
|
59
65
|
health: () => Promise<Health>;
|
|
66
|
+
/**
|
|
67
|
+
* Who this token is. `{ root: true }` for `PSTACK_TOKEN` — which is above every role and has no
|
|
68
|
+
* account — otherwise `user.role`, which is what a script branching on its own privileges reads.
|
|
69
|
+
*/
|
|
70
|
+
me: () => Promise<Me>;
|
|
60
71
|
deployments: {
|
|
61
72
|
list: (vars?: Vars) => Promise<DeploymentRow[]>;
|
|
62
73
|
get: (id: string, vars?: Vars) => Promise<DeploymentRow & {
|
|
@@ -100,6 +111,12 @@ export declare function createClient(opts: ClientOptions): {
|
|
|
100
111
|
sleep: (id: string, vars?: Vars) => Promise<Job>;
|
|
101
112
|
/** `up`, recorded as a wake. */
|
|
102
113
|
wake: (id: string, vars?: Vars) => Promise<Job>;
|
|
114
|
+
/**
|
|
115
|
+
* Stop everything outstanding on this deployment's stack — the running job and the one queued
|
|
116
|
+
* behind it — in one call. Destroys nothing and undoes nothing; a `down` also clears the stack
|
|
117
|
+
* (it preempts) but then tears the stack down, which this does not.
|
|
118
|
+
*/
|
|
119
|
+
cancel: (id: string, vars?: Vars) => Promise<CancelStack>;
|
|
103
120
|
/**
|
|
104
121
|
* Mint a read-only link to this deployment: `views` default to both `details` and `logs`,
|
|
105
122
|
* `ttl` to 7 days (30 at most). The token is returned once and stored nowhere; rotating
|
|
@@ -167,27 +184,67 @@ export declare function createClient(opts: ClientOptions): {
|
|
|
167
184
|
distro?: string;
|
|
168
185
|
}) => Promise<string>;
|
|
169
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* Accounts and the role each holds.
|
|
189
|
+
*
|
|
190
|
+
* Reading the roster is ordinary team information (viewer); creating, deleting and re-roling
|
|
191
|
+
* people is ADMIN, because anything that can set a role can set its own to `admin`.
|
|
192
|
+
*/
|
|
193
|
+
users: {
|
|
194
|
+
list: () => Promise<User[]>;
|
|
195
|
+
/**
|
|
196
|
+
* An ABSENT `role` means VIEWER — the least privilege, not the most. This route used to create
|
|
197
|
+
* an administrator every time; a script that relied on that must now say `role: 'admin'` and
|
|
198
|
+
* mean it. A role outside the four is a 400 rather than a silently powerless account.
|
|
199
|
+
*/
|
|
200
|
+
create: (body: {
|
|
201
|
+
username: string;
|
|
202
|
+
password: string;
|
|
203
|
+
email?: string;
|
|
204
|
+
role?: Role;
|
|
205
|
+
}) => Promise<User>;
|
|
206
|
+
/**
|
|
207
|
+
* Promote or demote. Takes effect on that account's NEXT request — the role is read fresh per
|
|
208
|
+
* request, so there is no session to revoke. Demoting the last admin is refused (400).
|
|
209
|
+
*/
|
|
210
|
+
setRole: (id: number, role: Role) => Promise<{
|
|
211
|
+
updated: number;
|
|
212
|
+
role: Role;
|
|
213
|
+
}>;
|
|
214
|
+
/** Deleting the last admin is refused: a host nobody can administer cannot be repaired over HTTP. */
|
|
215
|
+
remove: (id: number) => Promise<{
|
|
216
|
+
deleted: number;
|
|
217
|
+
}>;
|
|
218
|
+
};
|
|
170
219
|
/**
|
|
171
220
|
* The identity provider people sign in with. The sign-in flow itself is two browser redirects
|
|
172
221
|
* (`/api/auth/sso/start` → the provider → `/api/auth/sso/callback`) and has nothing for an SDK
|
|
173
222
|
* to call — this is the configuration around it.
|
|
174
223
|
*/
|
|
175
224
|
sso: {
|
|
176
|
-
/**
|
|
225
|
+
/** Every stored provider (secrets never come back — `secretSet` is all a read learns). */
|
|
177
226
|
config: () => Promise<SsoConfigResponse>;
|
|
178
227
|
/**
|
|
179
|
-
* Save
|
|
180
|
-
*
|
|
228
|
+
* Save one provider under `key`, a lowercase slug. `key` may be omitted on a host with at
|
|
229
|
+
* most one provider — an empty host derives it from the config, a one-provider host replaces
|
|
230
|
+
* that one — which is what PUT meant before keys existed; with several it is a 400. Omit
|
|
231
|
+
* `clientSecret` to keep that key's stored one. For `mode: 'oidc'` the issuer is fetched
|
|
232
|
+
* here, so a bad one is a 400 now rather than a failed login later.
|
|
181
233
|
*/
|
|
182
234
|
save: (config: Partial<SsoConfig> & {
|
|
235
|
+
key?: string;
|
|
183
236
|
clientSecret?: string;
|
|
184
237
|
}) => Promise<{
|
|
185
238
|
ok: true;
|
|
239
|
+
key: string;
|
|
186
240
|
config: SsoConfig;
|
|
187
241
|
callbackUrl: string;
|
|
188
242
|
}>;
|
|
189
|
-
/**
|
|
190
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Forget one provider — or the only one, when `key` is omitted (a 400 names the keys when
|
|
245
|
+
* there are several). Nobody is deleted — accounts it created keep working.
|
|
246
|
+
*/
|
|
247
|
+
remove: (key?: string) => Promise<{
|
|
191
248
|
ok: true;
|
|
192
249
|
}>;
|
|
193
250
|
};
|
|
@@ -274,11 +331,18 @@ export declare function createClient(opts: ClientOptions): {
|
|
|
274
331
|
}>;
|
|
275
332
|
};
|
|
276
333
|
/**
|
|
277
|
-
* Poll a job until it
|
|
334
|
+
* Poll a job until it reaches a terminal state.
|
|
335
|
+
*
|
|
336
|
+
* Returns the finished job rather than throwing on a failed one: `failed`, `leaked`,
|
|
337
|
+
* `cancelled` and `superseded` are ANSWERS, and a CI step usually wants to branch on which. It
|
|
338
|
+
* throws only when the wait itself fails — a timeout, or the API being unreachable.
|
|
278
339
|
*
|
|
279
|
-
*
|
|
280
|
-
* `
|
|
281
|
-
*
|
|
340
|
+
* WAITING IS NOT ONLY `running`. A job accepted for a busy stack, or over the host's
|
|
341
|
+
* concurrency cap, is `queued` — it has an id and a 202 and has not started. Returning on
|
|
342
|
+
* "not running" reported SUCCESS for a job that never ran, which is the worst possible answer
|
|
343
|
+
* to give a CI pipeline: it would assert against a deployment the deploy had not touched yet.
|
|
344
|
+
* The list below is the TERMINAL one, so a state this build has never heard of keeps waiting
|
|
345
|
+
* rather than being mistaken for an answer.
|
|
282
346
|
*/
|
|
283
347
|
waitForJob(jobId: string, o?: {
|
|
284
348
|
intervalMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var TERMINAL_JOB_STATES = ["ok", "failed", "leaked", "cancelled", "superseded"];
|
|
3
|
+
|
|
1
4
|
// src/index.ts
|
|
2
5
|
class PstackError extends Error {
|
|
3
6
|
status;
|
|
@@ -61,6 +64,7 @@ function createClient(opts) {
|
|
|
61
64
|
const client = {
|
|
62
65
|
request,
|
|
63
66
|
health: () => get("/api/health"),
|
|
67
|
+
me: () => get("/api/auth/me"),
|
|
64
68
|
deployments: {
|
|
65
69
|
list: (vars) => get(`/api/deployments${qs(vars)}`).then((r) => r.deployments),
|
|
66
70
|
get: (id, vars) => get(`/api/deployments/${enc(id)}${qs(vars)}`),
|
|
@@ -71,6 +75,7 @@ function createClient(opts) {
|
|
|
71
75
|
down: (id, body = {}, vars) => post(`/api/deployments/${enc(id)}/down${qs(vars)}`, body).then((r) => r.job),
|
|
72
76
|
sleep: (id, vars) => post(`/api/deployments/${enc(id)}/sleep${qs(vars)}`).then((r) => r.job),
|
|
73
77
|
wake: (id, vars) => post(`/api/deployments/${enc(id)}/wake${qs(vars)}`).then((r) => r.job),
|
|
78
|
+
cancel: (id, vars) => post(`/api/deployments/${enc(id)}/cancel${qs(vars)}`),
|
|
74
79
|
share: (id, body = {}) => post(`/api/deployments/${enc(id)}/share`, body),
|
|
75
80
|
runtime: (id, vars) => get(`/api/deployments/${enc(id)}/runtime${qs(vars)}`),
|
|
76
81
|
readiness: (id, o = {}) => get(`/api/deployments/${enc(id)}/readiness${qs(o.vars, { wait: o.wait, refresh: o.refresh ? 1 : undefined, timeout: o.timeout })}`),
|
|
@@ -90,10 +95,16 @@ function createClient(opts) {
|
|
|
90
95
|
info: () => get("/api/swarm"),
|
|
91
96
|
join: (o = { format: "command" }) => get(`/api/swarm/join${qs(undefined, { format: o.format, distro: o.distro })}`)
|
|
92
97
|
},
|
|
98
|
+
users: {
|
|
99
|
+
list: () => get("/api/users").then((r) => r.users),
|
|
100
|
+
create: (body) => post("/api/users", body).then((r) => r.user),
|
|
101
|
+
setRole: (id, role) => request("PATCH", `/api/users/${id}`, { role }),
|
|
102
|
+
remove: (id) => del(`/api/users/${id}`)
|
|
103
|
+
},
|
|
93
104
|
sso: {
|
|
94
105
|
config: () => get("/api/sso/config"),
|
|
95
106
|
save: (config) => put("/api/sso/config", config),
|
|
96
|
-
remove: () => del("/api/sso/config")
|
|
107
|
+
remove: (key) => del(key ? `/api/sso/config/${enc(key)}` : "/api/sso/config")
|
|
97
108
|
},
|
|
98
109
|
jobs: {
|
|
99
110
|
list: () => get("/api/jobs").then((r) => r.jobs),
|
|
@@ -126,10 +137,12 @@ function createClient(opts) {
|
|
|
126
137
|
const deadline = Date.now() + (o.timeoutMs ?? 30 * 60000);
|
|
127
138
|
for (;; ) {
|
|
128
139
|
const job = await client.jobs.get(jobId);
|
|
129
|
-
if (job.state
|
|
140
|
+
if (TERMINAL_JOB_STATES.includes(job.state))
|
|
130
141
|
return job;
|
|
131
142
|
if (Date.now() > deadline) {
|
|
132
|
-
throw new PstackError(0, jobId, {
|
|
143
|
+
throw new PstackError(0, jobId, {
|
|
144
|
+
error: `job ${jobId} still ${job.state} after the wait timeout`
|
|
145
|
+
});
|
|
133
146
|
}
|
|
134
147
|
await new Promise((r) => setTimeout(r, interval));
|
|
135
148
|
}
|
|
@@ -181,5 +194,6 @@ async function verifyWebhook(args) {
|
|
|
181
194
|
export {
|
|
182
195
|
verifyWebhook,
|
|
183
196
|
createClient,
|
|
197
|
+
TERMINAL_JOB_STATES,
|
|
184
198
|
PstackError
|
|
185
199
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -26,18 +26,45 @@ export type SleepRecord = {
|
|
|
26
26
|
/** `HostRegexp` patterns (wildcard subdomains), Go syntax. */
|
|
27
27
|
rules: string[];
|
|
28
28
|
};
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* A job's state.
|
|
31
|
+
*
|
|
32
|
+
* Two of these mean it has NOT run yet, and both are easy to mistake for an answer:
|
|
33
|
+
*
|
|
34
|
+
* - `queued` — accepted, with an id, waiting its turn. A stack runs one job at a time, and the
|
|
35
|
+
* host runs at most `PSTACK_MAX_JOBS` across every stack; over either limit a job waits rather
|
|
36
|
+
* than being refused. It has no `startedAt`.
|
|
37
|
+
* - `superseded` — it was queued and a newer job for the same stack replaced it. The queue is one
|
|
38
|
+
* deep, so five rapid pushes run the first deploy and then exactly one more carrying the newest
|
|
39
|
+
* spec; the three in between end here. Nothing ran, so unlike `cancelled` there is no partial
|
|
40
|
+
* state to clean up.
|
|
41
|
+
*
|
|
42
|
+
* `cancelled` = a person stopped it. If it had started, whatever it had already done was NOT undone.
|
|
43
|
+
*/
|
|
44
|
+
export type JobState = 'queued' | 'running' | 'ok' | 'failed' | 'leaked' | 'cancelled' | 'superseded';
|
|
45
|
+
/**
|
|
46
|
+
* The states a job never leaves. Exported because "is this over?" is the question every caller of
|
|
47
|
+
* this SDK asks, and `state !== 'running'` — the obvious way to ask it — answers YES for a queued
|
|
48
|
+
* job that has not started. `waitForJob` uses this list.
|
|
49
|
+
*/
|
|
50
|
+
export declare const TERMINAL_JOB_STATES: readonly JobState[];
|
|
31
51
|
export type ReadinessState = 'watching' | 'ready' | 'failed' | 'timedout';
|
|
32
52
|
export type Health = {
|
|
33
53
|
ok: boolean;
|
|
34
54
|
/** False only in loopback dev mode, where the server refuses to bind off-localhost. */
|
|
35
55
|
authEnforced: boolean;
|
|
36
56
|
hasUsers?: boolean;
|
|
37
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The ENABLED sign-in providers, or null when there are none. Readable BEFORE authenticating —
|
|
59
|
+
* a login page needs one button per entry. `key` is what `/api/auth/sso/start?provider=` takes;
|
|
60
|
+
* `preset` is the preset the config came from (`''` for a bare OIDC issuer), for an icon.
|
|
61
|
+
*/
|
|
38
62
|
sso?: {
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
providers: Array<{
|
|
64
|
+
key: string;
|
|
65
|
+
label: string;
|
|
66
|
+
preset: string;
|
|
67
|
+
}>;
|
|
41
68
|
} | null;
|
|
42
69
|
dataDir: string;
|
|
43
70
|
version: string;
|
|
@@ -121,7 +148,8 @@ export type Job = {
|
|
|
121
148
|
stack: string;
|
|
122
149
|
action: JobAction;
|
|
123
150
|
state: JobState;
|
|
124
|
-
|
|
151
|
+
/** `null` while the job is `queued`, and on a `superseded` one — it never started. */
|
|
152
|
+
startedAt: number | null;
|
|
125
153
|
endedAt?: number;
|
|
126
154
|
outcome?: {
|
|
127
155
|
ok: boolean;
|
|
@@ -137,6 +165,27 @@ export type Job = {
|
|
|
137
165
|
}>;
|
|
138
166
|
cancelledBy?: string;
|
|
139
167
|
};
|
|
168
|
+
/** The four fields a 202 carries — `state` is `running` when it dispatched, `queued` when it waits. */
|
|
169
|
+
export type JobStub = {
|
|
170
|
+
id: string;
|
|
171
|
+
stack: string;
|
|
172
|
+
action: JobAction;
|
|
173
|
+
state: JobState;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* `POST /api/deployments/:id/cancel` — stop everything this deployment's stack has outstanding: the
|
|
177
|
+
* running job AND the one queued behind it, in one call.
|
|
178
|
+
*
|
|
179
|
+
* NOT a teardown. It stops work and destroys nothing; it also undoes nothing a running job had
|
|
180
|
+
* already done. `cancelled` is `[]`, never null, when there was nothing outstanding, and `warning`
|
|
181
|
+
* differs by whether anything had actually started — a queued job leaves no partial state behind.
|
|
182
|
+
*/
|
|
183
|
+
export type CancelStack = {
|
|
184
|
+
stack: string;
|
|
185
|
+
cancelled: JobStub[];
|
|
186
|
+
by: string;
|
|
187
|
+
warning: string;
|
|
188
|
+
};
|
|
140
189
|
export type ContainerReadiness = {
|
|
141
190
|
name: string;
|
|
142
191
|
service: string | null;
|
|
@@ -183,6 +232,8 @@ export type RuntimeContainer = {
|
|
|
183
232
|
node?: string | null;
|
|
184
233
|
/** A swarm task on ANOTHER node: listed, but out of reach of exec/stop from the manager. */
|
|
185
234
|
remote?: boolean;
|
|
235
|
+
/** A one-shot swarm service's synthetic row (0.31.0+): container verbs have nothing to act on. */
|
|
236
|
+
job?: boolean;
|
|
186
237
|
};
|
|
187
238
|
export type RuntimeRoute = {
|
|
188
239
|
router: string;
|
|
@@ -270,6 +321,45 @@ export type PstackEvent = {
|
|
|
270
321
|
at: number;
|
|
271
322
|
data: Record<string, unknown>;
|
|
272
323
|
};
|
|
324
|
+
/**
|
|
325
|
+
* The four ordered roles, weakest first — each includes everything below it:
|
|
326
|
+
*
|
|
327
|
+
* viewer every read
|
|
328
|
+
* developer + stacks and deployments (up/down/verify/sleep/wake, containers, specs, terminal)
|
|
329
|
+
* maintainer + host configuration (host vars, registries, routing, notifiers, swarm join)
|
|
330
|
+
* admin + people, and anything that can mint them (users, SSO config, sealed config)
|
|
331
|
+
*
|
|
332
|
+
* `root` — the `PSTACK_TOKEN` bearer — sits ABOVE all four and is not one of them. A share link is
|
|
333
|
+
* not a role at any rank: it reaches the reads its own deployment allows and nothing else.
|
|
334
|
+
*/
|
|
335
|
+
export type Role = 'viewer' | 'developer' | 'maintainer' | 'admin';
|
|
336
|
+
/** An account, as every response carries it. */
|
|
337
|
+
export type User = {
|
|
338
|
+
id: number;
|
|
339
|
+
username: string;
|
|
340
|
+
/**
|
|
341
|
+
* `string`, not `Role`, on the way OUT: `users.role` is a plain TEXT column an operator is
|
|
342
|
+
* expected to be able to repair over SSH, so a value outside the four is possible. The server
|
|
343
|
+
* ranks such a role BELOW viewer — read an unfamiliar one as *less* than viewer, never as more.
|
|
344
|
+
* What you SEND is a `Role`; anything else is a 400.
|
|
345
|
+
*/
|
|
346
|
+
role: string;
|
|
347
|
+
/** From an SSO provider's claims; null for every locally-created account. */
|
|
348
|
+
email: string | null;
|
|
349
|
+
createdAt: number;
|
|
350
|
+
};
|
|
351
|
+
/** Who a token is. `user` is present only for an account, `share` only for a share link. */
|
|
352
|
+
export type Me = {
|
|
353
|
+
/** The `PSTACK_TOKEN` bearer — above every role, and it carries no account. */
|
|
354
|
+
root: boolean;
|
|
355
|
+
user?: User;
|
|
356
|
+
/** Epoch ms, or absent when the link could not be re-verified. */
|
|
357
|
+
share?: {
|
|
358
|
+
deployment: string;
|
|
359
|
+
views: ShareView[];
|
|
360
|
+
expiresAt?: number | null;
|
|
361
|
+
};
|
|
362
|
+
};
|
|
273
363
|
export type SsoClaimMap = {
|
|
274
364
|
subject: string;
|
|
275
365
|
username: string;
|
|
@@ -315,24 +405,46 @@ export type SsoConfig = {
|
|
|
315
405
|
* rather than letting every later login fail.
|
|
316
406
|
*/
|
|
317
407
|
requiredGroups: string[];
|
|
408
|
+
/**
|
|
409
|
+
* The role an account this provider MINTS is created at. Deliberately not narrowed to `Role`: the
|
|
410
|
+
* server stores this string without validating it, and one it does not recognise ranks below
|
|
411
|
+
* viewer. Whatever it says is the floor every person who signs in through this provider gets.
|
|
412
|
+
*/
|
|
318
413
|
defaultRole: string;
|
|
319
414
|
};
|
|
320
415
|
export type SsoPreset = {
|
|
321
416
|
key: string;
|
|
322
417
|
label: string;
|
|
418
|
+
/** How the provider is talked to; the config's mode defaults from it. */
|
|
419
|
+
mode: 'oidc' | 'oauth2';
|
|
420
|
+
/** Login-page button text ("Continue with GitHub"). */
|
|
421
|
+
buttonLabel: string;
|
|
422
|
+
/** Where the operator registers the OAuth app, and the walkthrough to show beside the form. */
|
|
423
|
+
setupUrl: string;
|
|
424
|
+
setupHint: string;
|
|
425
|
+
/**
|
|
426
|
+
* The issuer, for an oidc preset (`''` otherwise). One containing `<` is a TEMPLATE — render it
|
|
427
|
+
* as a field for the operator to fill in; the server refuses it verbatim.
|
|
428
|
+
*/
|
|
429
|
+
discoveryUrl: string;
|
|
323
430
|
authorizeUrl: string;
|
|
324
431
|
tokenUrl: string;
|
|
325
|
-
userInfoUrl: string
|
|
432
|
+
userInfoUrl: string;
|
|
326
433
|
scopes: string;
|
|
327
434
|
claimMap: SsoClaimMap;
|
|
328
435
|
};
|
|
436
|
+
/** One stored provider, under its operator-chosen slug. */
|
|
437
|
+
export type SsoProviderEntry = {
|
|
438
|
+
key: string;
|
|
439
|
+
config: SsoConfig;
|
|
440
|
+
/** All a read learns about the client secret — the value has no read path. */
|
|
441
|
+
secretSet: boolean;
|
|
442
|
+
updatedAt: number;
|
|
443
|
+
};
|
|
329
444
|
export type SsoConfigResponse = {
|
|
330
|
-
|
|
331
|
-
|
|
445
|
+
/** Every stored provider, in key order — disabled ones included. */
|
|
446
|
+
providers: SsoProviderEntry[];
|
|
447
|
+
/** Register THIS with every provider, byte for byte. Built server-side; never guess it. */
|
|
332
448
|
callbackUrl: string;
|
|
333
449
|
presets: SsoPreset[];
|
|
334
|
-
config: SsoConfig | null;
|
|
335
|
-
/** A mask when one is stored. Submit it back unchanged to keep the stored secret. */
|
|
336
|
-
clientSecret: string;
|
|
337
|
-
updatedAt: number | null;
|
|
338
450
|
};
|
package/package.json
CHANGED