@ninedeploy/sdk 0.18.25 → 0.18.27

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 (40) hide show
  1. package/dist/databaseImport.d.ts +219 -6
  2. package/dist/databaseImport.d.ts.map +1 -1
  3. package/dist/databaseImport.js +43 -13
  4. package/dist/databaseImport.js.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/resources/auth.d.ts +3 -2
  10. package/dist/resources/auth.d.ts.map +1 -1
  11. package/dist/resources/auth.js.map +1 -1
  12. package/dist/resources/databases.d.ts +27 -1
  13. package/dist/resources/databases.d.ts.map +1 -1
  14. package/dist/resources/databases.js +21 -0
  15. package/dist/resources/databases.js.map +1 -1
  16. package/dist/resources/services.d.ts +3 -1
  17. package/dist/resources/services.d.ts.map +1 -1
  18. package/dist/resources/services.js +1 -0
  19. package/dist/resources/services.js.map +1 -1
  20. package/dist/resources/settings.d.ts +9 -1
  21. package/dist/resources/settings.d.ts.map +1 -1
  22. package/dist/resources/settings.js +4 -0
  23. package/dist/resources/settings.js.map +1 -1
  24. package/dist/resources/system.d.ts +11 -1
  25. package/dist/resources/system.d.ts.map +1 -1
  26. package/dist/resources/system.js +4 -0
  27. package/dist/resources/system.js.map +1 -1
  28. package/dist/resources/volumes.d.ts +19 -1
  29. package/dist/resources/volumes.d.ts.map +1 -1
  30. package/dist/resources/volumes.js +10 -0
  31. package/dist/resources/volumes.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/databaseImport.ts +87 -19
  34. package/src/index.ts +2 -0
  35. package/src/resources/auth.ts +4 -3
  36. package/src/resources/databases.ts +39 -0
  37. package/src/resources/services.ts +4 -0
  38. package/src/resources/settings.ts +14 -0
  39. package/src/resources/system.ts +10 -0
  40. package/src/resources/volumes.ts +28 -0
@@ -5,6 +5,8 @@ import type {
5
5
  DatabaseImport,
6
6
  DatabaseImportCreate,
7
7
  DatabaseImportOptions,
8
+ VolumeImport,
9
+ VolumeImportCreate,
8
10
  } from '@ninedeploy/schemas';
9
11
  import { NineDeployError } from './errors.js';
10
12
  import type { Transport } from './transport.js';
@@ -18,8 +20,11 @@ export const DATABASE_IMPORT_FINISHED_STATUSES: ReadonlyArray<DatabaseImport['st
18
20
  'expired',
19
21
  ];
20
22
 
21
- /** 0.14: `databases.imports.wait` polling options. */
22
- export interface ImportWaitOptions {
23
+ /** A1: a volume import that no longer changes (`VolumeImport['status']`). */
24
+ export const VOLUME_IMPORT_FINISHED_STATUSES: ReadonlyArray<VolumeImport['status']> = ['completed', 'failed', 'cancelled', 'expired'];
25
+
26
+ /** 0.14: `databases.imports.wait` (and A1 `volumeImports.wait`) polling options. */
27
+ export interface ImportWaitOptions<Row = DatabaseImport> {
23
28
  /**
24
29
  * `finished` (default) waits for a final status; `uploaded` returns as soon
25
30
  * as the import leaves `uploading` (an S3 download landing in `pending`).
@@ -30,7 +35,7 @@ export interface ImportWaitOptions {
30
35
  /** Give up after this long (default: never). Throws `import_wait_timeout`. */
31
36
  timeoutMs?: number;
32
37
  /** Called with every polled row. */
33
- onStatus?: (row: DatabaseImport) => void;
38
+ onStatus?: (row: Row) => void;
34
39
  }
35
40
 
36
41
  /**
@@ -60,6 +65,22 @@ export interface ImportFileOptions {
60
65
  onProgress?: (progress: { receivedBytes: number; sizeBytes: number; row: DatabaseImport }) => void;
61
66
  }
62
67
 
68
+ /**
69
+ * A1: `volumeImports.importFile` options: a gzipped tar of a volume's
70
+ * contents, restored into a service's data volume. The sha256 is required
71
+ * (the server checks it before the volume is touched) unless resuming.
72
+ */
73
+ export interface VolumeImportFileOptions {
74
+ sizeBytes?: number;
75
+ filename?: string;
76
+ sha256?: string;
77
+ resumeImportId?: number;
78
+ start?: boolean;
79
+ poll?: boolean | Omit<ImportWaitOptions<VolumeImport>, 'until'>;
80
+ onCreated?: (row: VolumeImport) => void;
81
+ onProgress?: (progress: { receivedBytes: number; sizeBytes: number; row: VolumeImport }) => void;
82
+ }
83
+
63
84
  /** 0.14: the import bytes from `offset` on (a factory seeks; the rest skip). */
64
85
  async function* sourceFrom(data: ImportFileData, offset: number): AsyncIterable<Uint8Array> {
65
86
  if (data instanceof Uint8Array) {
@@ -102,29 +123,51 @@ async function* rechunk(source: AsyncIterable<Uint8Array>, size: number): AsyncI
102
123
  if (len > 0) yield buf.subarray(0, len);
103
124
  }
104
125
 
105
- /** Chunked dump-import helpers bound to one transport (used by `databases`). */
106
- export function createImportHelpers(t: Transport) {
126
+ interface UploadRow {
127
+ id: number;
128
+ status: string;
129
+ sizeBytes: number;
130
+ receivedBytes: number;
131
+ chunkSize: number;
132
+ }
133
+ interface UploadFileOptions<Row> {
134
+ sizeBytes?: number;
135
+ resumeImportId?: number;
136
+ start?: boolean;
137
+ poll?: boolean | Omit<ImportWaitOptions<Row>, 'until'>;
138
+ onCreated?: (row: Row) => void;
139
+ onProgress?: (progress: { receivedBytes: number; sizeBytes: number; row: Row }) => void;
140
+ }
141
+
142
+ /**
143
+ * The chunked, resumable upload shared by dump imports and volume imports:
144
+ * create (or resume) a row under `base(id)`, send ordered chunks of the
145
+ * size the server advertised, start, and optionally poll to a final status.
146
+ */
147
+ function chunkedUploads<Row extends UploadRow, Opts extends UploadFileOptions<Row>>(
148
+ t: Transport,
149
+ cfg: { base: (id: number) => string; createBody: (sizeBytes: number, opts: Opts) => unknown; finished: ReadonlyArray<string> },
150
+ ) {
107
151
  const { get, send, request } = t;
108
- // ── 0.14 dump import helpers ─────────────────────────────────────────────
109
- const importPath = (id: number, importId?: number) => `/v1/databases/${id}/imports${importId === undefined ? '' : `/${importId}`}`;
110
- const getImport = (id: number, importId: number) => get<DatabaseImport>(importPath(id, importId));
152
+ const importPath = (id: number, importId?: number) => `${cfg.base(id)}${importId === undefined ? '' : `/${importId}`}`;
153
+ const getImport = (id: number, importId: number) => get<Row>(importPath(id, importId));
111
154
  /** An 8 MiB chunk over a slow uplink outlasts the default 30 s budget. */
112
155
  const CHUNK_TIMEOUT_MS = 10 * 60_000;
113
156
  const uploadChunk = (id: number, importId: number, index: number, chunk: Uint8Array) =>
114
- request<DatabaseImport>(`${importPath(id, importId)}/chunks/${index}`, { method: 'PUT', raw: chunk }, CHUNK_TIMEOUT_MS);
115
- const startImport = (id: number, importId: number) => send<DatabaseImport>('POST', `${importPath(id, importId)}/start`);
157
+ request<Row>(`${importPath(id, importId)}/chunks/${index}`, { method: 'PUT', raw: chunk }, CHUNK_TIMEOUT_MS);
158
+ const startImport = (id: number, importId: number) => send<Row>('POST', `${importPath(id, importId)}/start`);
116
159
  const sleep = (ms: number) =>
117
160
  new Promise<void>((resolve) => {
118
161
  (globalThis as unknown as { setTimeout: (fn: () => void, ms: number) => unknown }).setTimeout(resolve, ms);
119
162
  });
120
163
 
121
- async function waitForImport(id: number, importId: number, o: ImportWaitOptions = {}): Promise<DatabaseImport> {
164
+ async function waitForImport(id: number, importId: number, o: ImportWaitOptions<Row> = {}): Promise<Row> {
122
165
  const interval = o.intervalMs ?? 2000;
123
166
  const deadline = o.timeoutMs === undefined ? Number.POSITIVE_INFINITY : Date.now() + o.timeoutMs;
124
167
  for (;;) {
125
168
  const row = await getImport(id, importId);
126
169
  o.onStatus?.(row);
127
- const done = o.until === 'uploaded' ? row.status !== 'uploading' : DATABASE_IMPORT_FINISHED_STATUSES.includes(row.status);
170
+ const done = o.until === 'uploaded' ? row.status !== 'uploading' : cfg.finished.includes(row.status);
128
171
  if (done) return row;
129
172
  if (Date.now() + interval > deadline) {
130
173
  throw new NineDeployError(0, 'import_wait_timeout', `Import ${importId} is still ${row.status}`);
@@ -133,18 +176,14 @@ export function createImportHelpers(t: Transport) {
133
176
  }
134
177
  }
135
178
 
136
- async function importFile(id: number, data: ImportFileData, opts: ImportFileOptions = {}): Promise<DatabaseImport> {
179
+ async function importFile(id: number, data: ImportFileData, opts: Opts = {} as Opts): Promise<Row> {
137
180
  const declared = data instanceof Uint8Array ? data.length : opts.sizeBytes;
138
- let row: DatabaseImport;
181
+ let row: Row;
139
182
  if (opts.resumeImportId !== undefined) {
140
183
  row = await getImport(id, opts.resumeImportId);
141
184
  } else {
142
185
  if (declared === undefined) throw new NineDeployError(0, 'import_size_required', 'sizeBytes is required for a streamed import');
143
- const input: DatabaseImportCreate = { source: 'upload', sizeBytes: declared };
144
- if (opts.sha256 !== undefined) input.sha256 = opts.sha256;
145
- if (opts.filename !== undefined) input.filename = opts.filename;
146
- if (opts.options !== undefined) input.options = opts.options;
147
- row = await send<DatabaseImport>('POST', importPath(id), input);
186
+ row = await send<Row>('POST', importPath(id), cfg.createBody(declared, opts));
148
187
  opts.onCreated?.(row);
149
188
  }
150
189
  const size = row.sizeBytes;
@@ -172,3 +211,32 @@ export function createImportHelpers(t: Transport) {
172
211
 
173
212
  return { importPath, getImport, uploadChunk, startImport, waitForImport, importFile };
174
213
  }
214
+
215
+ /** Chunked dump-import helpers bound to one transport (used by `databases`). */
216
+ export function createImportHelpers(t: Transport) {
217
+ return chunkedUploads<DatabaseImport, ImportFileOptions>(t, {
218
+ base: (id) => `/v1/databases/${id}/imports`,
219
+ createBody: (sizeBytes, opts) => {
220
+ const input: DatabaseImportCreate = { source: 'upload', sizeBytes };
221
+ if (opts.sha256 !== undefined) input.sha256 = opts.sha256;
222
+ if (opts.filename !== undefined) input.filename = opts.filename;
223
+ if (opts.options !== undefined) input.options = opts.options;
224
+ return input;
225
+ },
226
+ finished: DATABASE_IMPORT_FINISHED_STATUSES,
227
+ });
228
+ }
229
+
230
+ /** A1: chunked volume-import helpers (`/v1/services/:id/volume-imports`). */
231
+ export function createVolumeImportHelpers(t: Transport) {
232
+ return chunkedUploads<VolumeImport, VolumeImportFileOptions>(t, {
233
+ base: (id) => `/v1/services/${id}/volume-imports`,
234
+ createBody: (sizeBytes, opts) => {
235
+ if (opts.sha256 === undefined) throw new NineDeployError(0, 'import_sha256_required', "A volume import needs the archive's sha256");
236
+ const input: VolumeImportCreate = { sizeBytes, sha256: opts.sha256 };
237
+ if (opts.filename !== undefined) input.filename = opts.filename;
238
+ return input;
239
+ },
240
+ finished: VOLUME_IMPORT_FINISHED_STATUSES,
241
+ });
242
+ }
package/src/index.ts CHANGED
@@ -76,12 +76,14 @@ export type {
76
76
 
77
77
  export {
78
78
  DATABASE_IMPORT_FINISHED_STATUSES,
79
+ VOLUME_IMPORT_FINISHED_STATUSES,
79
80
  } from './databaseImport.js';
80
81
 
81
82
  export type {
82
83
  ImportWaitOptions,
83
84
  ImportFileData,
84
85
  ImportFileOptions,
86
+ VolumeImportFileOptions,
85
87
  } from './databaseImport.js';
86
88
 
87
89
  export type {
@@ -14,6 +14,7 @@ import type {
14
14
  PublicUser,
15
15
  Refresh,
16
16
  Register,
17
+ Setup,
17
18
  Session,
18
19
  UserCreate,
19
20
  WorkspaceEntry,
@@ -48,7 +49,7 @@ import type { Transport } from '../transport.js';
48
49
  export interface AuthApi {
49
50
  auth: {
50
51
  /** `setupTokenRequired` (0.16): the first-run bootstrap needs the installer's setup token. */
51
- status: () => Promise<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean }>;
52
+ status: () => Promise<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean; acmeConfigured?: boolean }>;
52
53
  /**
53
54
  * Introspect the current bearer token. Returns the
54
55
  * token's id (when opaque), name, scopes, expiry and
@@ -58,7 +59,7 @@ export interface AuthApi {
58
59
  * tools.
59
60
  */
60
61
  introspectToken: () => Promise<TokenIntrospection>;
61
- setup: (input: Register) => Promise<Session>;
62
+ setup: (input: Setup) => Promise<Session>;
62
63
  register: (input: Register) => Promise<Session>;
63
64
  login: (input: Login) => Promise<Session>;
64
65
  refresh: (input: Refresh) => Promise<Session>;
@@ -249,7 +250,7 @@ export function authApi(t: Transport): AuthApi {
249
250
  const { get, send, request } = t;
250
251
  return {
251
252
  auth: {
252
- status: () => get<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean }>('/v1/auth/status'),
253
+ status: () => get<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean; acmeConfigured?: boolean }>('/v1/auth/status'),
253
254
  introspectToken: () => get<TokenIntrospection>('/v1/auth/token'),
254
255
  setup: (input) => send<Session>('POST', '/v1/setup', input),
255
256
  register: (input) => send<Session>('POST', '/v1/auth/register', input),
@@ -15,6 +15,8 @@ import type {
15
15
  PublicAccessStatus,
16
16
  DatabaseImport,
17
17
  DatabaseImportCreate,
18
+ DatabaseUpgrade,
19
+ DatabaseUpgradeOptions,
18
20
  BackupDestinationObject,
19
21
  BackupReplica,
20
22
  BackupReplicasView,
@@ -104,6 +106,25 @@ export interface DatabasesApi {
104
106
  recoveries: (id: number) => Promise<PitrRecovery[]>;
105
107
  recovery: (id: number, recoveryId: number) => Promise<PitrRecovery>;
106
108
  };
109
+ /**
110
+ * O2: major-version upgrades (PostgreSQL on the panel host). `options`
111
+ * reads the running cluster's major and what it can move to. `start`
112
+ * checks the cluster, then answers 202 with the running upgrade: the
113
+ * database is stopped, dumped (the dump is kept in its backups), restored
114
+ * into a new volume on the target major and compared before the switch.
115
+ * The previous volume stays until `finalize`; until then `rollback`
116
+ * points the database back at it. Starting, rolling back and finalizing
117
+ * need `admin` on the database.
118
+ */
119
+ upgrades: {
120
+ list: (id: number) => Promise<DatabaseUpgrade[]>;
121
+ options: (id: number) => Promise<DatabaseUpgradeOptions>;
122
+ start: (id: number, input: { toMajor: number }) => Promise<DatabaseUpgrade>;
123
+ rollback: (id: number, upgradeId: number) => Promise<DatabaseUpgrade>;
124
+ finalize: (id: number, upgradeId: number) => Promise<DatabaseUpgrade & { removedVolume: string }>;
125
+ /** Poll until the upgrade is no longer `running` (default every 2 s, for up to 2 h). */
126
+ wait: (id: number, upgradeId: number, opts?: { intervalMs?: number; timeoutMs?: number }) => Promise<DatabaseUpgrade>;
127
+ };
107
128
  /**
108
129
  * 0.14: public access through a per-database Traefik TCP sidecar. Reading
109
130
  * needs `admin` on the database; `set` and `disable` are operator-only.
@@ -224,6 +245,24 @@ export function databasesApi(t: Transport): DatabasesApi {
224
245
  recoveries: (id) => get<PitrRecovery[]>(`/v1/databases/${id}/pitr/recoveries`),
225
246
  recovery: (id, recoveryId) => get<PitrRecovery>(`/v1/databases/${id}/pitr/recoveries/${recoveryId}`),
226
247
  },
248
+ upgrades: {
249
+ list: (id) => get<DatabaseUpgrade[]>(`/v1/databases/${id}/upgrades`),
250
+ options: (id) => get<DatabaseUpgradeOptions>(`/v1/databases/${id}/upgrades/options`),
251
+ start: (id, input) => send<DatabaseUpgrade>('POST', `/v1/databases/${id}/upgrades`, input),
252
+ rollback: (id, upgradeId) => send<DatabaseUpgrade>('POST', `/v1/databases/${id}/upgrades/${upgradeId}/rollback`, {}),
253
+ finalize: (id, upgradeId) => send<DatabaseUpgrade & { removedVolume: string }>('POST', `/v1/databases/${id}/upgrades/${upgradeId}/finalize`, {}),
254
+ wait: async (id, upgradeId, opts = {}) => {
255
+ const interval = opts.intervalMs ?? 2_000;
256
+ const deadline = Date.now() + (opts.timeoutMs ?? 2 * 60 * 60 * 1000);
257
+ for (;;) {
258
+ const row = (await get<DatabaseUpgrade[]>(`/v1/databases/${id}/upgrades`)).find((u) => u.id === upgradeId);
259
+ if (!row) throw new Error(`upgrade ${upgradeId} of database ${id} not found`);
260
+ if (row.status !== 'running') return row;
261
+ if (Date.now() >= deadline) throw new Error(`upgrade ${upgradeId} is still running (phase ${row.phase ?? 'unknown'})`);
262
+ await new Promise<void>((resolve) => (globalThis as unknown as { setTimeout: (fn: () => void, ms: number) => unknown }).setTimeout(resolve, interval));
263
+ }
264
+ },
265
+ },
227
266
  pgbouncerStatus: (id) => get<PgbouncerStatus>(`/v1/databases/${id}/pgbouncer`),
228
267
  enablePgbouncer: (id, input) =>
229
268
  send<PgbouncerStatus>('POST', `/v1/databases/${id}/pgbouncer/enable`, input ?? {}),
@@ -7,6 +7,7 @@ import type {
7
7
  SizingReport,
8
8
  RestartLimitConfig,
9
9
  RestartLimitView,
10
+ ServiceCapabilitiesView,
10
11
  PendingChangesView,
11
12
  PendingApplyInput,
12
13
  PendingApplyResult,
@@ -270,6 +271,8 @@ export interface ServicesApi {
270
271
  enable: (id: number) => Promise<RegistryHookView>;
271
272
  disable: (id: number) => Promise<RegistryHookView>;
272
273
  };
274
+ /** C1: what the service can use where it runs, and why not (viewer). */
275
+ capabilities: (id: number) => Promise<ServiceCapabilitiesView>;
273
276
  /** G1.3: stop the service when its containers keep restarting (opt-in; member to set). */
274
277
  restartLimit: {
275
278
  get: (id: number) => Promise<RestartLimitView>;
@@ -471,6 +474,7 @@ export function servicesApi(t: Transport): ServicesApi {
471
474
  enable: (id) => send<RegistryHookView>('POST', `/v1/services/${id}/registry-hook`),
472
475
  disable: (id) => request<RegistryHookView>(`/v1/services/${id}/registry-hook`, { method: 'DELETE' }),
473
476
  },
477
+ capabilities: (id) => get<ServiceCapabilitiesView>(`/v1/services/${id}/capabilities`),
474
478
  restartLimit: {
475
479
  get: (id) => get<RestartLimitView>(`/v1/services/${id}/restart-limit`),
476
480
  set: (id, limit) => send<RestartLimitView>('PUT', `/v1/services/${id}/restart-limit`, { limit }),
@@ -17,6 +17,8 @@ import type {
17
17
  ExecProviderPut,
18
18
  McpHttpPolicyPatch,
19
19
  McpHttpPolicyView,
20
+ PanelAccessUpdate,
21
+ PanelAccessView,
20
22
  } from '@ninedeploy/schemas';
21
23
  import type { VaultAllowlistView, VaultSettings, EmailTemplateName, EmailTemplateEntry, EmailTemplateRender } from '../types.js';
22
24
  import type { Transport } from '../transport.js';
@@ -84,6 +86,14 @@ export interface SettingsApi {
84
86
  setAllowRegistration: (enabled: boolean) => Promise<{ ok: boolean; allowRegistration: boolean }>;
85
87
  setAcmeEmail: (email: string) => Promise<{ ok: boolean; acmeEmail: string | null; applied: string }>;
86
88
  setTemplatesSource: (source: string) => Promise<{ ok: boolean; templatesSource: string | null }>;
89
+ /**
90
+ * O4: the panel IP allow-list (operator, interactive session). `set`
91
+ * is refused when the list would exclude the caller's own address.
92
+ */
93
+ panelAccess: {
94
+ get: () => Promise<PanelAccessView>;
95
+ set: (input: PanelAccessUpdate) => Promise<PanelAccessView>;
96
+ };
87
97
  /** Configure the ACME DNS-01 challenge (wildcard certificates). */
88
98
  setDns: (input: { provider: string; token?: string; wildcardApex: string }) => Promise<{ ok: boolean; dnsProvider: string | null; wildcardApex: string | null; applied: string }>;
89
99
  /**
@@ -266,6 +276,10 @@ export function settingsApi(t: Transport): SettingsApi {
266
276
  send<{ ok: boolean; acmeEmail: string | null; applied: string }>('PUT', '/v1/settings/acme-email', { email }),
267
277
  setTemplatesSource: (source) =>
268
278
  send<{ ok: boolean; templatesSource: string | null }>('PUT', '/v1/settings/templates-source', { source }),
279
+ panelAccess: {
280
+ get: () => get<PanelAccessView>('/v1/settings/panel-access'),
281
+ set: (input) => send<PanelAccessView>('PUT', '/v1/settings/panel-access', input),
282
+ },
269
283
  setDns: (input) =>
270
284
  send<{ ok: boolean; dnsProvider: string | null; wildcardApex: string | null; applied: string }>('PUT', '/v1/settings/dns', input),
271
285
  autoUrl: {
@@ -17,6 +17,7 @@ import type {
17
17
  StatsSnapshot,
18
18
  TopologyGraph,
19
19
  UpdateCheckResult,
20
+ UpdateChannel,
20
21
  SelfUpdateStatus,
21
22
  AutoPruneConfigUpdateInput,
22
23
  AutoPruneStatus,
@@ -55,6 +56,11 @@ export interface SystemApi {
55
56
  exportUrl: () => string;
56
57
  /** Latest-release check (admin). updateAvailable is null when offline/disabled. */
57
58
  updateCheck: (force?: boolean) => Promise<UpdateCheckResult>;
59
+ /** A6: which releases the panel offers — every release, or the stable channel (operator). */
60
+ updateChannel: {
61
+ get: () => Promise<{ channel: UpdateChannel }>;
62
+ set: (channel: UpdateChannel) => Promise<{ channel: UpdateChannel; check: UpdateCheckResult }>;
63
+ };
58
64
  /** State of a panel-initiated upgrade (idle/running/success/failed/unsupported). */
59
65
  updateStatus: () => Promise<SelfUpdateStatus>;
60
66
  /**
@@ -214,6 +220,10 @@ export function systemApi(t: Transport): SystemApi {
214
220
  resources: () => get<DockerResources>('/v1/system/resources'),
215
221
  pruneImages: () => send<{ ok: boolean }>('POST', '/v1/system/prune-images'),
216
222
  updateCheck: (force) => get<UpdateCheckResult>(`/v1/system/update-check${force ? '?force=1' : ''}`),
223
+ updateChannel: {
224
+ get: () => get<{ channel: UpdateChannel }>('/v1/system/update-channel'),
225
+ set: (channel) => send<{ channel: UpdateChannel; check: UpdateCheckResult }>('PUT', '/v1/system/update-channel', { channel }),
226
+ },
217
227
  updateStatus: () => get<SelfUpdateStatus>('/v1/system/update-status'),
218
228
  updateStart: (version, opts) => send<{ ok: boolean }>('POST', '/v1/system/update-start', { version, ...opts }),
219
229
  exportUrl: () => '/v1/system/export',
@@ -9,7 +9,9 @@ import type {
9
9
  VolumeFileWriteInput,
10
10
  VolumePathCreateInput,
11
11
  VolumeCreateInput,
12
+ VolumeImport,
12
13
  } from '@ninedeploy/schemas';
14
+ import { createVolumeImportHelpers, type ImportFileData, type ImportWaitOptions, type VolumeImportFileOptions } from '../databaseImport.js';
13
15
  import { apiGetUrl } from '../apiPath.js';
14
16
  import type { VolumeListEntry, VolumeCreateResult, ContainerInspectData, ContainerComposeData } from '../types.js';
15
17
  import type { Transport } from '../transport.js';
@@ -78,10 +80,28 @@ export interface VolumesApi {
78
80
  input: { filePath: string; attachmentId?: number; volumeName?: string },
79
81
  ) => Promise<{ ok: boolean; deploymentId: number }>;
80
82
  };
83
+ /**
84
+ * A1: restore a gzipped tar of another host's volume into a service's data
85
+ * volume (`nd-svc-<slug>-data`). Operator only; docker services on the
86
+ * panel host with a volume mount. Uploads in ordered chunks with the
87
+ * archive's sha256; `start` is refused while the service runs, and a volume
88
+ * that exists is snapshotted first (a `pre-import` volume backup). A panel
89
+ * older than this feature answers 404.
90
+ */
91
+ volumeImports: {
92
+ list: (serviceId: number) => Promise<VolumeImport[]>;
93
+ get: (serviceId: number, importId: number) => Promise<VolumeImport>;
94
+ cancel: (serviceId: number, importId: number) => Promise<VolumeImport>;
95
+ start: (serviceId: number, importId: number) => Promise<VolumeImport>;
96
+ wait: (serviceId: number, importId: number, options?: ImportWaitOptions<VolumeImport>) => Promise<VolumeImport>;
97
+ /** Create (or resume), upload, start and optionally poll, like `databases.importFile`. */
98
+ importFile: (serviceId: number, data: ImportFileData, options?: VolumeImportFileOptions) => Promise<VolumeImport>;
99
+ };
81
100
  }
82
101
 
83
102
  export function volumesApi(t: Transport): VolumesApi {
84
103
  const { get, send, request } = t;
104
+ const vi = createVolumeImportHelpers(t);
85
105
  const volumePath = (name: string, suffix: string, options?: { serverId?: number }) => {
86
106
  const id = options?.serverId;
87
107
  if (id != null && (!Number.isSafeInteger(id) || id < 1)) throw new TypeError('Invalid volume node id');
@@ -148,5 +168,13 @@ export function volumesApi(t: Transport): VolumesApi {
148
168
  repairConfig: (serviceId, input) =>
149
169
  send<{ ok: boolean; deploymentId: number }>('POST', `/v1/services/${serviceId}/volumes/config-repair`, input),
150
170
  },
171
+ volumeImports: {
172
+ list: (serviceId) => get<VolumeImport[]>(vi.importPath(serviceId)),
173
+ get: vi.getImport,
174
+ cancel: (serviceId, importId) => send<VolumeImport>('DELETE', vi.importPath(serviceId, importId)),
175
+ start: vi.startImport,
176
+ wait: vi.waitForImport,
177
+ importFile: vi.importFile,
178
+ },
151
179
  };
152
180
  }