@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 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 notifiers, plus webhook verification for the
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, or treating `readiness.state === 'watching'` as an
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
- /** Swap in for tests, a proxy, or an agent with custom TLS. Defaults to global `fetch`. */
50
- fetch?: typeof fetch;
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
- /** `clientSecret` comes back as a mask. There is no route that returns the real one. */
225
+ /** Every stored provider (secrets never come back `secretSet` is all a read learns). */
177
226
  config: () => Promise<SsoConfigResponse>;
178
227
  /**
179
- * Save. Omit `clientSecret` (or send the mask back) to keep the stored one. For `mode: 'oidc'`
180
- * the issuer is fetched here, so a bad one is a 400 now rather than a failed login later.
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
- /** Forget the provider. Nobody is deleted — accounts it created keep working. */
190
- remove: () => Promise<{
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 stops running.
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
- * Returns the finished job rather than throwing on a failed one: `failed`, `leaked` and
280
- * `cancelled` are ANSWERS, and a CI step usually wants to branch on which. It throws only when
281
- * the wait itself fails a timeout, or the API being unreachable.
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 !== "running")
140
+ if (TERMINAL_JOB_STATES.includes(job.state))
130
141
  return job;
131
142
  if (Date.now() > deadline) {
132
- throw new PstackError(0, jobId, { error: `job ${jobId} still running after the wait timeout` });
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
- /** `cancelled` = a person stopped it; nothing it had done was undone. */
30
- export type JobState = 'running' | 'ok' | 'failed' | 'leaked' | 'cancelled';
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
- /** The configured identity provider, or null. Readable BEFORE authenticating — a login page needs it. */
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
- enabled: boolean;
40
- label: string;
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
- startedAt: number;
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 | null;
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
- configured: boolean;
331
- /** Register THIS with the provider, byte for byte. Built server-side; never guess it. */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samyx/preview-stacks-client",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Typed API client for a pstack control plane: deployments, jobs, readiness, containers, logs, notifiers.",
5
5
  "repository": {
6
6
  "type": "git",