@ninedeploy/sdk 0.10.5 → 0.16.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.
Files changed (129) hide show
  1. package/LICENSE +21 -21
  2. package/dist/apiPath.d.ts +5 -0
  3. package/dist/apiPath.d.ts.map +1 -0
  4. package/dist/apiPath.js +18 -0
  5. package/dist/apiPath.js.map +1 -0
  6. package/dist/client.d.ts +29 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +51 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/databaseImport.d.ts +138 -0
  11. package/dist/databaseImport.d.ts.map +1 -0
  12. package/dist/databaseImport.js +126 -0
  13. package/dist/databaseImport.js.map +1 -0
  14. package/dist/errors.js +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/index.d.ts +12 -1973
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +9 -641
  19. package/dist/index.js.map +1 -1
  20. package/dist/manifest.d.ts.map +1 -1
  21. package/dist/manifest.js +25 -21
  22. package/dist/manifest.js.map +1 -1
  23. package/dist/resources/auth.d.ts +282 -0
  24. package/dist/resources/auth.d.ts.map +1 -0
  25. package/dist/resources/auth.js +111 -0
  26. package/dist/resources/auth.js.map +1 -0
  27. package/dist/resources/databases.d.ts +178 -0
  28. package/dist/resources/databases.d.ts.map +1 -0
  29. package/dist/resources/databases.js +82 -0
  30. package/dist/resources/databases.js.map +1 -0
  31. package/dist/resources/deploys.d.ts +125 -0
  32. package/dist/resources/deploys.d.ts.map +1 -0
  33. package/dist/resources/deploys.js +46 -0
  34. package/dist/resources/deploys.js.map +1 -0
  35. package/dist/resources/domains.d.ts +101 -0
  36. package/dist/resources/domains.d.ts.map +1 -0
  37. package/dist/resources/domains.js +28 -0
  38. package/dist/resources/domains.js.map +1 -0
  39. package/dist/resources/env.d.ts +55 -0
  40. package/dist/resources/env.d.ts.map +1 -0
  41. package/dist/resources/env.js +33 -0
  42. package/dist/resources/env.js.map +1 -0
  43. package/dist/resources/logs.d.ts +27 -0
  44. package/dist/resources/logs.d.ts.map +1 -0
  45. package/dist/resources/logs.js +18 -0
  46. package/dist/resources/logs.js.map +1 -0
  47. package/dist/resources/network.d.ts +134 -0
  48. package/dist/resources/network.d.ts.map +1 -0
  49. package/dist/resources/network.js +33 -0
  50. package/dist/resources/network.js.map +1 -0
  51. package/dist/resources/plugins.d.ts +42 -0
  52. package/dist/resources/plugins.d.ts.map +1 -0
  53. package/dist/resources/plugins.js +22 -0
  54. package/dist/resources/plugins.js.map +1 -0
  55. package/dist/resources/servers.d.ts +130 -0
  56. package/dist/resources/servers.d.ts.map +1 -0
  57. package/dist/resources/servers.js +37 -0
  58. package/dist/resources/servers.js.map +1 -0
  59. package/dist/resources/services.d.ts +185 -0
  60. package/dist/resources/services.d.ts.map +1 -0
  61. package/dist/resources/services.js +83 -0
  62. package/dist/resources/services.js.map +1 -0
  63. package/dist/resources/settings.d.ts +312 -0
  64. package/dist/resources/settings.d.ts.map +1 -0
  65. package/dist/resources/settings.js +85 -0
  66. package/dist/resources/settings.js.map +1 -0
  67. package/dist/resources/sources.d.ts +87 -0
  68. package/dist/resources/sources.d.ts.map +1 -0
  69. package/dist/resources/sources.js +39 -0
  70. package/dist/resources/sources.js.map +1 -0
  71. package/dist/resources/system.d.ts +240 -0
  72. package/dist/resources/system.d.ts.map +1 -0
  73. package/dist/resources/system.js +86 -0
  74. package/dist/resources/system.js.map +1 -0
  75. package/dist/resources/templates.d.ts +36 -0
  76. package/dist/resources/templates.d.ts.map +1 -0
  77. package/dist/resources/templates.js +17 -0
  78. package/dist/resources/templates.js.map +1 -0
  79. package/dist/resources/terminals.d.ts +60 -0
  80. package/dist/resources/terminals.d.ts.map +1 -0
  81. package/dist/resources/terminals.js +35 -0
  82. package/dist/resources/terminals.js.map +1 -0
  83. package/dist/resources/traefik.d.ts +84 -0
  84. package/dist/resources/traefik.d.ts.map +1 -0
  85. package/dist/resources/traefik.js +35 -0
  86. package/dist/resources/traefik.js.map +1 -0
  87. package/dist/resources/volumes.d.ts +114 -0
  88. package/dist/resources/volumes.d.ts.map +1 -0
  89. package/dist/resources/volumes.js +47 -0
  90. package/dist/resources/volumes.js.map +1 -0
  91. package/dist/terminal.d.ts +98 -0
  92. package/dist/terminal.d.ts.map +1 -0
  93. package/dist/terminal.js +180 -0
  94. package/dist/terminal.js.map +1 -0
  95. package/dist/transport.d.ts +61 -0
  96. package/dist/transport.d.ts.map +1 -0
  97. package/dist/transport.js +90 -0
  98. package/dist/transport.js.map +1 -0
  99. package/dist/types.d.ts +539 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +5 -0
  102. package/dist/types.js.map +1 -0
  103. package/package.json +5 -4
  104. package/src/apiPath.ts +23 -0
  105. package/src/client.ts +70 -0
  106. package/src/databaseImport.ts +174 -0
  107. package/src/errors.ts +27 -0
  108. package/src/index.ts +118 -0
  109. package/src/manifest.ts +414 -0
  110. package/src/resources/auth.ts +357 -0
  111. package/src/resources/databases.ts +227 -0
  112. package/src/resources/deploys.ts +161 -0
  113. package/src/resources/domains.ts +142 -0
  114. package/src/resources/env.ts +84 -0
  115. package/src/resources/logs.ts +48 -0
  116. package/src/resources/network.ts +126 -0
  117. package/src/resources/plugins.ts +52 -0
  118. package/src/resources/servers.ts +141 -0
  119. package/src/resources/services.ts +250 -0
  120. package/src/resources/settings.ts +275 -0
  121. package/src/resources/sources.ts +131 -0
  122. package/src/resources/system.ts +281 -0
  123. package/src/resources/templates.ts +55 -0
  124. package/src/resources/terminals.ts +112 -0
  125. package/src/resources/traefik.ts +116 -0
  126. package/src/resources/volumes.ts +138 -0
  127. package/src/terminal.ts +254 -0
  128. package/src/transport.ts +154 -0
  129. package/src/types.ts +588 -0
@@ -0,0 +1,357 @@
1
+ // Client surface: auth, workspaces, users, scim, sso, accessGrants, access.
2
+ // Split out of the former single-file SDK entry (src/index.ts); the public
3
+ // surface is re-exported unchanged from there.
4
+ import type {
5
+ ActiveSession,
6
+ ApiToken,
7
+ PasskeyCredential,
8
+ CreateApiTokenInput,
9
+ CreatedApiToken,
10
+ Login,
11
+ PasswordChange,
12
+ PasswordChangeResult,
13
+ PasswordReset,
14
+ PublicUser,
15
+ Refresh,
16
+ Register,
17
+ Session,
18
+ UserCreate,
19
+ WorkspaceEntry,
20
+ WorkspaceDetail,
21
+ WorkspaceCreateInput,
22
+ WorkspaceUpdateInput,
23
+ WorkspaceMemberEntry,
24
+ WorkspaceMemberInviteEntry,
25
+ WorkspaceMemberAddInput,
26
+ WorkspaceMemberRoleUpdateInput,
27
+ OidcPublicProvider,
28
+ WorkspaceInvitationEntry,
29
+ WorkspaceInvitationPublic,
30
+ WorkspaceRole,
31
+ OidcProviderEntry,
32
+ OidcProviderCreateInput,
33
+ OidcProviderUpdateInput,
34
+ AccessGrant,
35
+ AccessGrantCreate,
36
+ AccessGrantListQuery,
37
+ AccessGrantUpdate,
38
+ AccessMe,
39
+ ProjectAccessEntry,
40
+ } from '@ninedeploy/schemas';
41
+ import { apiGetUrl } from '../apiPath.js';
42
+ import type { TokenIntrospection } from '../types.js';
43
+ import type { Transport } from '../transport.js';
44
+
45
+ export interface AuthApi {
46
+ auth: {
47
+ /** `setupTokenRequired` (0.16): the first-run bootstrap needs the installer's setup token. */
48
+ status: () => Promise<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean }>;
49
+ /**
50
+ * Introspect the current bearer token. Returns the
51
+ * token's id (when opaque), name, scopes, expiry and
52
+ * the operator flag. Interactive sessions (JWT) report
53
+ * `scopes: ['session']`. The MCP server uses this to
54
+ * discover the token's authority before registering
55
+ * tools.
56
+ */
57
+ introspectToken: () => Promise<TokenIntrospection>;
58
+ setup: (input: Register) => Promise<Session>;
59
+ register: (input: Register) => Promise<Session>;
60
+ login: (input: Login) => Promise<Session>;
61
+ refresh: (input: Refresh) => Promise<Session>;
62
+ logout: () => Promise<{ ok: boolean }>;
63
+ /** Self-service password change; revokes other sessions, returns a fresh token pair
64
+ * and (r603) how many passkeys still sign in to the account. */
65
+ changePassword: (input: PasswordChange) => Promise<PasswordChangeResult>;
66
+ /** Request a reset link (always 200 — no user enumeration). */
67
+ forgotPassword: (email: string) => Promise<{ ok: boolean }>;
68
+ /** Complete a reset with a single-use token; revokes all sessions. */
69
+ resetPasswordWithToken: (input: { token: string; newPassword: string }) => Promise<{ ok: boolean }>;
70
+ twoFactor: {
71
+ /**
72
+ * Generate a pending secret + otpauth URI (auth required). Since 0.10.36
73
+ * (r502) the server wants step-up proof: the current password, or a
74
+ * sign-in from the last 10 minutes (else 403 `reauth_required`).
75
+ */
76
+ setup: (input?: { password: string }) => Promise<{ secret: string; otpauthUri: string }>;
77
+ /** Turn 2FA on. `password` is the r502 step-up proof (see `setup`). */
78
+ enable: (code: string, password?: string) => Promise<{ ok: boolean; totpEnabled: boolean }>;
79
+ disable: (input: { password: string; code: string }) => Promise<{ ok: boolean; totpEnabled: boolean }>;
80
+ };
81
+ me: () => Promise<PublicUser>;
82
+ tokens: {
83
+ create: (input?: CreateApiTokenInput) => Promise<CreatedApiToken>;
84
+ list: () => Promise<ApiToken[]>;
85
+ remove: (id: number) => Promise<void>;
86
+ };
87
+ passkeys: {
88
+ /**
89
+ * Start a registration ceremony (returns JSON options for the browser
90
+ * API). Since 0.10.36 (r502) registration needs step-up proof: the
91
+ * current password, or a sign-in from the last 10 minutes (else 403
92
+ * `reauth_required`).
93
+ */
94
+ registerOptions: (input?: { password?: string }) => Promise<{ options: string }>;
95
+ /** Complete registration: verify the browser response, store the credential. */
96
+ registerVerify: (input: { name: string; response: unknown; password?: string }) => Promise<PasskeyCredential>;
97
+ list: () => Promise<PasskeyCredential[]>;
98
+ remove: (id: number) => Promise<void>;
99
+ /** Start a passwordless login ceremony (discoverable credentials). */
100
+ loginOptions: () => Promise<{ options: string }>;
101
+ /** Complete passkey login — returns a full session on success. */
102
+ loginVerify: (response: unknown) => Promise<Session>;
103
+ };
104
+ sessions: {
105
+ list: () => Promise<ActiveSession[]>;
106
+ revoke: (id: number) => Promise<{ ok: boolean }>;
107
+ };
108
+ oidc: {
109
+ publicProviders: () => Promise<OidcPublicProvider[]>;
110
+ link: (slug: string) => Promise<{ authUrl: string }>;
111
+ listProviders: () => Promise<OidcProviderEntry[]>;
112
+ list: () => Promise<OidcProviderEntry[]>;
113
+ createProvider: (input: OidcProviderCreateInput) => Promise<OidcProviderEntry>;
114
+ create: (input: OidcProviderCreateInput) => Promise<OidcProviderEntry>;
115
+ updateProvider: (id: number, input: OidcProviderUpdateInput) => Promise<OidcProviderEntry>;
116
+ update: (id: number, input: OidcProviderUpdateInput) => Promise<OidcProviderEntry>;
117
+ deleteProvider: (id: number) => Promise<{ ok: boolean }>;
118
+ delete: (id: number) => Promise<{ ok: boolean }>;
119
+ callback: (slug: string, payload: { code: string; state: string }) => Promise<Session>;
120
+ };
121
+ };
122
+ workspaces: {
123
+ list: () => Promise<WorkspaceEntry[]>;
124
+ get: (id: number) => Promise<WorkspaceDetail>;
125
+ create: (input: WorkspaceCreateInput) => Promise<WorkspaceEntry>;
126
+ update: (id: number, input: WorkspaceUpdateInput) => Promise<WorkspaceEntry>;
127
+ delete: (id: number) => Promise<{ ok: boolean }>;
128
+ /**
129
+ * Add a member by email. r604: only an instance operator gets a direct
130
+ * add (a `WorkspaceMemberEntry`) for a registered address; for everyone
131
+ * else the answer is always an invitation (`kind: 'invitation'`), so the
132
+ * response does not reveal whether the address has an account.
133
+ */
134
+ addMember: (id: number, input: WorkspaceMemberAddInput) => Promise<WorkspaceMemberEntry | WorkspaceMemberInviteEntry>;
135
+ /**
136
+ * Create a pending invitation for an email address that is not a member of
137
+ * the workspace yet (r604: registered or not).
138
+ * r334: the response is the invitation row only — the server does NOT
139
+ * return an `acceptUrl` here (the token travels in the `x-invitation-token`
140
+ * response header and in the invite email). `addMember` returns the accept
141
+ * link in its body for the same case.
142
+ */
143
+ inviteMember: (id: number, input: WorkspaceMemberAddInput) => Promise<WorkspaceInvitationEntry>;
144
+ listInvitations: (id: number) => Promise<WorkspaceInvitationEntry[]>;
145
+ revokeInvitation: (id: number, inviteId: number) => Promise<{ ok: boolean }>;
146
+ /** Look up a pending invitation by its public token (no auth required). */
147
+ previewInvitation: (token: string) => Promise<WorkspaceInvitationPublic>;
148
+ /** Accept an invitation as the currently authenticated user. */
149
+ acceptInvitation: (token: string) => Promise<{ ok: boolean; workspaceId: number; role: WorkspaceRole }>;
150
+ updateMemberRole: (id: number, memberId: number, input: WorkspaceMemberRoleUpdateInput) => Promise<WorkspaceMemberEntry>;
151
+ removeMember: (id: number, memberId: number) => Promise<{ ok: boolean }>;
152
+ };
153
+ users: {
154
+ list: () => Promise<PublicUser[]>;
155
+ /**
156
+ * Operator-only: create a user directly (no registration flow needed).
157
+ * The new account has no workspace memberships — an existing owner/admin
158
+ * must invite it into at least one workspace before it can act.
159
+ */
160
+ create: (input: UserCreate) => Promise<PublicUser>;
161
+ /**
162
+ * Operator-only: delete a user. The `users.role` column was removed when
163
+ * authorization became workspace-only; the legacy `setRole` is gone with
164
+ * it. Role changes go through `workspaces.updateMemberRole`.
165
+ * Workspaces the user owns are transferred, not deleted: to the caller,
166
+ * or to `opts.transferTo` (an existing, active user id).
167
+ */
168
+ remove: (id: number, opts?: { transferTo?: number }) => Promise<void>;
169
+ /** Operator reset of another user's password (revokes their sessions). */
170
+ resetPassword: (id: number, input: PasswordReset) => Promise<{ ok: boolean }>;
171
+ /** Mint a one-time reset link for a user (returned exactly once). */
172
+ resetLink: (id: number) => Promise<{ url: string; expiresAt: string }>;
173
+ /**
174
+ * Operator-only: grant or revoke the INSTANCE-operator flag.
175
+ *
176
+ * Not a workspace role — this is the flag that gates operator-only routes
177
+ * and the host-privilege boundary (PM2/compose deploys, lifecycle hooks,
178
+ * docker-socket templates). Creating a workspace does NOT confer it. The
179
+ * last remaining operator cannot be demoted.
180
+ */
181
+ setOperator: (id: number, isOperator: boolean) => Promise<{ ok: boolean; id: number; isOperator: boolean }>;
182
+ };
183
+ scim: {
184
+ /** SCIM 2.0 provisioning tokens (operator-managed). */
185
+ listTokens: () => Promise<Array<{ id: number; name: string; workspaceId: number; createdAt: string | null; lastUsedAt: string | null; revoked: boolean }>>;
186
+ /** The plaintext token is returned exactly once — only its sha256 persists. */
187
+ createToken: (input: { name: string; workspaceId: number }) => Promise<{ id: number; token: string }>;
188
+ revokeToken: (id: number) => Promise<{ ok: boolean }>;
189
+ };
190
+ sso: {
191
+ /** Every configured OIDC / SAML provider. G-22. */
192
+ listProviders: () => Promise<{
193
+ providers: Array<{ id: number; type: 'oidc' | 'saml'; name: string; createdAt: string }>;
194
+ }>;
195
+ /** Add a provider (OIDC issuer URL or SAML metadata URL lives in `config`). */
196
+ addProvider: (input: {
197
+ type: 'oidc' | 'saml';
198
+ name: string;
199
+ config: Record<string, unknown>;
200
+ }) => Promise<{ ok: boolean; id?: number; name?: string; type?: 'oidc' | 'saml'; error?: string }>;
201
+ /** Remove a provider by id. */
202
+ removeProvider: (id: number) => Promise<{ ok: boolean }>;
203
+ };
204
+ /** Project and environment access grants (0.15, raise-only). Workspace admin floor. */
205
+ accessGrants: {
206
+ list: (workspaceId: number, query?: Partial<AccessGrantListQuery>) => Promise<AccessGrant[]>;
207
+ create: (workspaceId: number, input: AccessGrantCreate) => Promise<AccessGrant>;
208
+ /**
209
+ * Change the role, suspend (`suspended: true`) or reinstate
210
+ * (`suspended: false`), or both. A grant the user's identity provider
211
+ * suspended cannot be reinstated here (409 `grant_suspended_by_idp`).
212
+ */
213
+ update: (workspaceId: number, grantId: number, input: AccessGrantUpdate) => Promise<AccessGrant>;
214
+ delete: (workspaceId: number, grantId: number) => Promise<{ ok: boolean }>;
215
+ };
216
+ access: {
217
+ /** The caller's own grants and the workspaces reached only through them. */
218
+ me: () => Promise<AccessMe>;
219
+ /** Who reaches a project and how (project admin). */
220
+ project: (projectId: number) => Promise<ProjectAccessEntry[]>;
221
+ };
222
+ }
223
+
224
+ export function authApi(t: Transport): AuthApi {
225
+ const { get, send, request } = t;
226
+ return {
227
+ auth: {
228
+ status: () => get<{ initialized: boolean; allowRegistration?: boolean; setupTokenRequired?: boolean }>('/v1/auth/status'),
229
+ introspectToken: () => get<TokenIntrospection>('/v1/auth/token'),
230
+ setup: (input) => send<Session>('POST', '/v1/setup', input),
231
+ register: (input) => send<Session>('POST', '/v1/auth/register', input),
232
+ login: (input) => send<Session>('POST', '/v1/auth/login', input),
233
+ refresh: (input) => send<Session>('POST', '/v1/auth/refresh', input),
234
+ /** Revoke this user's outstanding JWTs server-side (tokenVersion bump). */
235
+ logout: () => send<{ ok: boolean }>('POST', '/v1/auth/logout', {}),
236
+ changePassword: (input) => send<PasswordChangeResult>('POST', '/v1/auth/password', input),
237
+ forgotPassword: (email) => send<{ ok: boolean }>('POST', '/v1/auth/forgot-password', { email }),
238
+ resetPasswordWithToken: (input) => send<{ ok: boolean }>('POST', '/v1/auth/reset-password', input),
239
+ twoFactor: {
240
+ setup: (input?: { password: string }) => send<{ secret: string; otpauthUri: string }>('POST', '/v1/auth/2fa/setup', input ?? {}),
241
+ enable: (code, password) =>
242
+ send<{ ok: boolean; totpEnabled: boolean }>('POST', '/v1/auth/2fa/enable', password ? { code, password } : { code }),
243
+ disable: (input) => send<{ ok: boolean; totpEnabled: boolean }>('POST', '/v1/auth/2fa/disable', input),
244
+ },
245
+ me: () => get<PublicUser>('/v1/auth/me'),
246
+ tokens: {
247
+ create: (input) => send<CreatedApiToken>('POST', '/v1/auth/tokens', input ?? {}),
248
+ list: () => get<ApiToken[]>('/v1/auth/tokens'),
249
+ remove: async (id) => {
250
+ await request(`/v1/auth/tokens/${id}`, { method: 'DELETE' });
251
+ },
252
+ },
253
+ passkeys: {
254
+ registerOptions: (input) =>
255
+ send<{ options: string }>('POST', '/v1/auth/passkey/register/options', input?.password ? { password: input.password } : undefined),
256
+ registerVerify: (input) =>
257
+ send<PasskeyCredential>('POST', '/v1/auth/passkey/register/verify', {
258
+ name: input.name,
259
+ response: input.response as Record<string, unknown>,
260
+ ...(input.password ? { password: input.password } : {}),
261
+ }),
262
+ list: () => get<PasskeyCredential[]>('/v1/auth/passkey'),
263
+ remove: async (id) => {
264
+ await request(`/v1/auth/passkey/${id}`, { method: 'DELETE' });
265
+ },
266
+ loginOptions: () => send<{ options: string }>('POST', '/v1/auth/passkey/login/options'),
267
+ loginVerify: (response) =>
268
+ send<Session>('POST', '/v1/auth/passkey/login/verify', { response: response as Record<string, unknown> }),
269
+ },
270
+ sessions: {
271
+ list: () => get<ActiveSession[]>('/v1/auth/sessions'),
272
+ revoke: (id) => send<{ ok: boolean }>('DELETE', `/v1/auth/sessions/${id}`),
273
+ },
274
+ oidc: {
275
+ publicProviders: () => get<OidcPublicProvider[]>('/v1/auth/oidc/providers/public'),
276
+ link: (slug) => request<{ authUrl: string }>(`/v1/auth/oidc/${encodeURIComponent(slug)}/link`, { method: 'POST', credentials: 'include' }),
277
+ listProviders: () => get<OidcProviderEntry[]>('/v1/auth/oidc/providers'),
278
+ list: () => get<OidcProviderEntry[]>('/v1/auth/oidc/providers'),
279
+ createProvider: (input) => send<OidcProviderEntry>('POST', '/v1/auth/oidc/providers', input),
280
+ create: (input) => send<OidcProviderEntry>('POST', '/v1/auth/oidc/providers', input),
281
+ updateProvider: (id, input) => send<OidcProviderEntry>('PATCH', `/v1/auth/oidc/providers/${id}`, input),
282
+ update: (id, input) => send<OidcProviderEntry>('PATCH', `/v1/auth/oidc/providers/${id}`, input),
283
+ deleteProvider: (id) => send<{ ok: boolean }>('DELETE', `/v1/auth/oidc/providers/${id}`),
284
+ delete: (id) => send<{ ok: boolean }>('DELETE', `/v1/auth/oidc/providers/${id}`),
285
+ callback: (slug, payload) => send<Session>('POST', `/v1/auth/oidc/${slug}/callback`, payload),
286
+ },
287
+ },
288
+ workspaces: {
289
+ list: () => get<WorkspaceEntry[]>('/v1/workspaces'),
290
+ get: (id) => get<WorkspaceDetail>(`/v1/workspaces/${id}`),
291
+ create: (input) => send<WorkspaceEntry>('POST', '/v1/workspaces', input),
292
+ update: (id, input) => send<WorkspaceEntry>('PATCH', `/v1/workspaces/${id}`, input),
293
+ delete: (id) => send<{ ok: boolean }>('DELETE', `/v1/workspaces/${id}`),
294
+ addMember: (id, input) =>
295
+ send<WorkspaceMemberEntry | WorkspaceMemberInviteEntry>('POST', `/v1/workspaces/${id}/members`, input),
296
+ inviteMember: (id, input) =>
297
+ send<WorkspaceInvitationEntry>('POST', `/v1/workspaces/${id}/invitations`, input),
298
+ listInvitations: (id) => get<WorkspaceInvitationEntry[]>(`/v1/workspaces/${id}/invitations`),
299
+ revokeInvitation: (id, inviteId) =>
300
+ send<{ ok: boolean }>('DELETE', `/v1/workspaces/${id}/invitations/${inviteId}`),
301
+ previewInvitation: (token) => get<WorkspaceInvitationPublic>(`/v1/invitations/${token}`),
302
+ acceptInvitation: (token) =>
303
+ send<{ ok: boolean; workspaceId: number; role: WorkspaceRole }>(
304
+ 'POST',
305
+ `/v1/invitations/${token}/accept`,
306
+ ),
307
+ updateMemberRole: (id, memberId, input) => send<WorkspaceMemberEntry>('PATCH', `/v1/workspaces/${id}/members/${memberId}`, input),
308
+ removeMember: (id, memberId) => send<{ ok: boolean }>('DELETE', `/v1/workspaces/${id}/members/${memberId}`),
309
+ },
310
+ users: {
311
+ list: () => get<PublicUser[]>('/v1/users'),
312
+ create: (input) => send<PublicUser>('POST', '/v1/users', input),
313
+ resetPassword: (id, input) => send<{ ok: boolean }>('PATCH', `/v1/users/${id}/password`, input),
314
+ resetLink: (id) => send<{ url: string; expiresAt: string }>('POST', `/v1/users/${id}/reset-link`),
315
+ setOperator: (id, isOperator) =>
316
+ send<{ ok: boolean; id: number; isOperator: boolean }>('PATCH', `/v1/users/${id}/operator`, { isOperator }),
317
+ remove: async (id, opts) => {
318
+ // r540: owned workspaces go to the caller unless `transferTo` names another user.
319
+ const q = opts?.transferTo != null ? `?transferTo=${encodeURIComponent(String(opts.transferTo))}` : '';
320
+ await request(`/v1/users/${id}${q}`, { method: 'DELETE' });
321
+ },
322
+ },
323
+ scim: {
324
+ listTokens: () =>
325
+ get<Array<{ id: number; name: string; workspaceId: number; createdAt: string | null; lastUsedAt: string | null; revoked: boolean }>>('/v1/scim/tokens'),
326
+ createToken: (input) =>
327
+ send<{ id: number; token: string }>('POST', '/v1/scim/tokens', input),
328
+ revokeToken: (id) => send<{ ok: boolean }>('DELETE', `/v1/scim/tokens/${id}`),
329
+ },
330
+ sso: {
331
+ listProviders: () => get<{
332
+ providers: Array<{ id: number; type: 'oidc' | 'saml'; name: string; createdAt: string }>;
333
+ }>('/v1/sso/providers'),
334
+ addProvider: (input) => send<{
335
+ ok: boolean;
336
+ id?: number;
337
+ name?: string;
338
+ type?: 'oidc' | 'saml';
339
+ error?: string;
340
+ }>('POST', '/v1/sso/providers', input),
341
+ removeProvider: (id) => send<{ ok: boolean }>('DELETE', `/v1/sso/providers/${id}`),
342
+ },
343
+ accessGrants: {
344
+ list: (workspaceId, query) =>
345
+ get<AccessGrant[]>(apiGetUrl(`/v1/workspaces/${workspaceId}/access-grants`, { ...query })),
346
+ create: (workspaceId, input) => send<AccessGrant>('POST', `/v1/workspaces/${workspaceId}/access-grants`, input),
347
+ update: (workspaceId, grantId, input) =>
348
+ send<AccessGrant>('PATCH', `/v1/workspaces/${workspaceId}/access-grants/${grantId}`, input),
349
+ delete: (workspaceId, grantId) =>
350
+ send<{ ok: boolean }>('DELETE', `/v1/workspaces/${workspaceId}/access-grants/${grantId}`),
351
+ },
352
+ access: {
353
+ me: () => get<AccessMe>('/v1/access/me'),
354
+ project: (projectId) => get<ProjectAccessEntry[]>(`/v1/projects/${projectId}/access`),
355
+ },
356
+ };
357
+ }
@@ -0,0 +1,227 @@
1
+ // Client surface: databases, backups, backupDestinations, volumeBackups.
2
+ // Split out of the former single-file SDK entry (src/index.ts); the public
3
+ // surface is re-exported unchanged from there.
4
+ import type {
5
+ CreateVolumeBackupInput,
6
+ Backup,
7
+ BackupPolicy,
8
+ BackupPolicyInput,
9
+ BackupWithDb,
10
+ CreateDatabaseInput,
11
+ ManagedDatabase,
12
+ DatabaseDetail,
13
+ SetLimitsInput,
14
+ PublicAccessPut,
15
+ PublicAccessStatus,
16
+ DatabaseImport,
17
+ DatabaseImportCreate,
18
+ BackupDestinationObject,
19
+ } from '@ninedeploy/schemas';
20
+ import type { ImportWaitOptions, ImportFileData, ImportFileOptions } from '../databaseImport.js';
21
+ import type { BackupDrillResult, BackupDrillEntry, PgbouncerStatus, DatabaseCredentialsWithPublic } from '../types.js';
22
+ import type { Transport } from '../transport.js';
23
+ import { createImportHelpers } from '../databaseImport.js';
24
+
25
+ export interface DatabasesApi {
26
+ databases: {
27
+ /** `query` is appended verbatim, e.g. `?projectId=3` (project scoping). */
28
+ list: (query?: string) => Promise<ManagedDatabase[]>;
29
+ create: (input: CreateDatabaseInput) => Promise<ManagedDatabase>;
30
+ get: (id: number) => Promise<DatabaseDetail>;
31
+ remove: (id: number, options?: { force?: boolean }) => Promise<void>;
32
+ restart: (id: number) => Promise<void>;
33
+ stop: (id: number) => Promise<void>;
34
+ start: (id: number) => Promise<void>;
35
+ logs: (id: number, lines?: number) => Promise<{ logs: string[] }>;
36
+ credentials: (id: number) => Promise<DatabaseCredentialsWithPublic>;
37
+ setLimits: (id: number, input: SetLimitsInput) => Promise<{ cpuShares: number | null; memLimitMb: number | null }>;
38
+ startStudio: (id: number, port?: number) => Promise<{ ok: boolean; port: number; url: string }>;
39
+ stopStudio: (id: number) => Promise<{ ok: boolean }>;
40
+ /**
41
+ * PgBouncer sidecar (G-32). Returns the current sidecar
42
+ * state — `enabled` + `running` + the URL the services
43
+ * should connect to. Use the same call after
44
+ * `enablePgbouncer` / `disablePgbouncer` for a fresh
45
+ * read (the route returns the post-mutation status).
46
+ */
47
+ pgbouncerStatus: (id: number) => Promise<PgbouncerStatus>;
48
+ /**
49
+ * Spin up the sidecar. `port` is optional — the row's
50
+ * existing `pgbouncerPort` (default 6432) is used
51
+ * when omitted. Only `engine='postgres'` is supported;
52
+ * the server returns 422 for any other engine.
53
+ */
54
+ enablePgbouncer: (id: number, input?: { port?: number }) => Promise<PgbouncerStatus>;
55
+ /** Stop + remove the sidecar and clear the row's flags. */
56
+ disablePgbouncer: (id: number) => Promise<PgbouncerStatus>;
57
+ /**
58
+ * Run an engine-specific smoke check against a backup
59
+ * file (pg_restore --list, redis-check-rdb, mysqldump
60
+ * header parse, ...). Returns the drill row with `status`
61
+ * of `passed` or `failed`; a failed drill is a real
62
+ * signal that the backup cannot be restored cleanly, not
63
+ * a warning. `unverifiable` (r356) means the check could
64
+ * not run at all. Requires the `member` role on the database.
65
+ */
66
+ drillBackup: (id: number, input: { backupId: number }) => Promise<BackupDrillResult>;
67
+ /** List the most recent drills for a database, newest first. */
68
+ drills: (id: number) => Promise<BackupDrillEntry[]>;
69
+ /**
70
+ * 0.14: public access through a per-database Traefik TCP sidecar. Reading
71
+ * needs `admin` on the database; `set` and `disable` are operator-only.
72
+ * Public access exposes the database's ROOT credentials.
73
+ */
74
+ publicAccess: {
75
+ get: (id: number) => Promise<PublicAccessStatus>;
76
+ /** Applies synchronously; answers the GET shape. */
77
+ set: (id: number, input: PublicAccessPut) => Promise<PublicAccessStatus>;
78
+ disable: (id: number) => Promise<{ ok: boolean }>;
79
+ };
80
+ /**
81
+ * 0.14: dump imports. Uploads are chunked at the server-advertised
82
+ * `chunkSize`; an S3 source (operator-only) downloads in the background
83
+ * (`uploading` → `pending`) and is then started like an upload.
84
+ */
85
+ imports: {
86
+ create: (id: number, input: DatabaseImportCreate) => Promise<DatabaseImport>;
87
+ /** Chunk `index` must be the next one (`receivedBytes / chunkSize`). */
88
+ uploadChunk: (id: number, importId: number, index: number, chunk: Uint8Array) => Promise<DatabaseImport>;
89
+ /** 202: the job runs in the background; poll `get` or use `wait`. */
90
+ start: (id: number, importId: number) => Promise<DatabaseImport>;
91
+ list: (id: number) => Promise<DatabaseImport[]>;
92
+ get: (id: number, importId: number) => Promise<DatabaseImport>;
93
+ /** Cancels an `uploading` or `pending` import. */
94
+ cancel: (id: number, importId: number) => Promise<DatabaseImport>;
95
+ /** Poll until the import finishes (or, with `until: 'uploaded'`, leaves `uploading`). */
96
+ wait: (id: number, importId: number, opts?: ImportWaitOptions) => Promise<DatabaseImport>;
97
+ };
98
+ /**
99
+ * 0.14: upload a dump in chunks (resuming from the server's
100
+ * `receivedBytes` when `resumeImportId` is given), start it, and
101
+ * optionally poll until it finishes.
102
+ */
103
+ importFile: (id: number, data: ImportFileData, opts?: ImportFileOptions) => Promise<DatabaseImport>;
104
+ };
105
+ backups: {
106
+ storage: (databaseId: number) => Promise<{ sizeBytes: number }>;
107
+ backupNow: (databaseId: number) => Promise<Backup>;
108
+ listForDb: (databaseId: number) => Promise<Backup[]>;
109
+ restore: (databaseId: number, backupId: number) => Promise<{ ok: boolean }>;
110
+ list: () => Promise<BackupWithDb[]>;
111
+ remove: (backupId: number) => Promise<void>;
112
+ downloadUrl: (backupId: number) => string;
113
+ /** 0.12: the database's backup schedule. `configured: false` = no policy
114
+ * saved — the built-in daily run with 7 kept applies. */
115
+ getPolicy: (databaseId: number) => Promise<BackupPolicy>;
116
+ /** 0.12: save (replace) the database's backup schedule, retention and destination. */
117
+ setPolicy: (databaseId: number, input: BackupPolicyInput) => Promise<BackupPolicy>;
118
+ };
119
+ backupDestinations: {
120
+ list: () => Promise<Array<{ id: number; name: string; endpoint: string; region: string; bucket: string; prefix: string; active: boolean; createdAt: string }>>;
121
+ create: (input: { name: string; endpoint: string; region?: string; bucket: string; prefix?: string; accessKeyId: string; secretAccessKey: string }) => Promise<{ id: number }>;
122
+ update: (id: number, input: Partial<{ name: string; endpoint: string; region: string; bucket: string; prefix: string; active: boolean; accessKeyId: string; secretAccessKey: string }>) => Promise<{ ok: boolean }>;
123
+ remove: (id: number) => Promise<{ ok: boolean }>;
124
+ test: (id: number) => Promise<{ ok: boolean }>;
125
+ /** 0.14: list objects (operator-only); `prefix` defaults to the destination prefix. */
126
+ objects: (id: number, prefix?: string) => Promise<BackupDestinationObject[]>;
127
+ };
128
+ /** Per-volume backup history. The base URL is `/v1/volumes/:name/backups`;
129
+ * restore + download go through the same path. The global `/v1/backups`
130
+ * list/delete are exposed as `backups` below. */
131
+ volumeBackups: {
132
+ list: (volumeName: string) => Promise<Backup[]>;
133
+ create: (volumeName: string, input: CreateVolumeBackupInput) => Promise<Backup>;
134
+ restore: (volumeName: string, backupId: number) => Promise<{ ok: boolean }>;
135
+ downloadUrl: (volumeName: string, backupId: number) => string;
136
+ };
137
+ }
138
+
139
+ export function databasesApi(t: Transport): DatabasesApi {
140
+ const { get, send, request } = t;
141
+ const { importPath, getImport, uploadChunk, startImport, waitForImport, importFile } = createImportHelpers(t);
142
+ return {
143
+ databases: {
144
+ list: (query) => get<ManagedDatabase[]>(`/v1/databases${query ?? ''}`),
145
+ create: (input) => send<ManagedDatabase>('POST', '/v1/databases', input),
146
+ get: (id) => get<DatabaseDetail>(`/v1/databases/${id}`),
147
+ remove: async (id, opts) => {
148
+ await request(`/v1/databases/${id}${opts?.force ? '?force=true' : ''}`, { method: 'DELETE' });
149
+ },
150
+ restart: async (id) => {
151
+ await request(`/v1/databases/${id}/restart`, { method: 'POST' });
152
+ },
153
+ stop: async (id) => {
154
+ await request(`/v1/databases/${id}/stop`, { method: 'POST' });
155
+ },
156
+ start: async (id) => {
157
+ await request(`/v1/databases/${id}/start`, { method: 'POST' });
158
+ },
159
+ logs: (id, lines) => get<{ logs: string[] }>(`/v1/databases/${id}/logs${lines ? `?lines=${lines}` : ''}`),
160
+ credentials: (id) => get<DatabaseCredentialsWithPublic>(`/v1/databases/${id}/credentials`),
161
+ setLimits: (id, input) => send<{ cpuShares: number | null; cpuLimitMilli: number | null; memLimitMb: number | null }>('PATCH', `/v1/databases/${id}/limits`, input),
162
+ startStudio: (id, port) => send<{ ok: boolean; port: number; url: string }>('POST', `/v1/databases/${id}/studio`, { port }),
163
+ stopStudio: async (id) => {
164
+ await request(`/v1/databases/${id}/studio`, { method: 'DELETE' });
165
+ return { ok: true };
166
+ },
167
+ drillBackup: (id, input) =>
168
+ send<BackupDrillResult>('POST', `/v1/databases/${id}/backups/drill`, input),
169
+ drills: (id) => get<BackupDrillEntry[]>(`/v1/databases/${id}/drills`),
170
+ pgbouncerStatus: (id) => get<PgbouncerStatus>(`/v1/databases/${id}/pgbouncer`),
171
+ enablePgbouncer: (id, input) =>
172
+ send<PgbouncerStatus>('POST', `/v1/databases/${id}/pgbouncer/enable`, input ?? {}),
173
+ disablePgbouncer: (id) =>
174
+ send<PgbouncerStatus>('POST', `/v1/databases/${id}/pgbouncer/disable`, {}),
175
+ publicAccess: {
176
+ get: (id) => get<PublicAccessStatus>(`/v1/databases/${id}/public-access`),
177
+ set: (id, input) => send<PublicAccessStatus>('PUT', `/v1/databases/${id}/public-access`, input),
178
+ disable: (id) => send<{ ok: boolean }>('DELETE', `/v1/databases/${id}/public-access`),
179
+ },
180
+ imports: {
181
+ create: (id, input) => send<DatabaseImport>('POST', importPath(id), input),
182
+ uploadChunk,
183
+ start: startImport,
184
+ list: (id) => get<DatabaseImport[]>(importPath(id)),
185
+ get: getImport,
186
+ cancel: (id, importId) => send<DatabaseImport>('DELETE', importPath(id, importId)),
187
+ wait: waitForImport,
188
+ },
189
+ importFile,
190
+ },
191
+ backups: {
192
+ storage: (databaseId) => get<{ sizeBytes: number }>(`/v1/databases/${databaseId}/storage`),
193
+ backupNow: (databaseId) => send<Backup>('POST', `/v1/databases/${databaseId}/backups`),
194
+ listForDb: (databaseId) => get<Backup[]>(`/v1/databases/${databaseId}/backups`),
195
+ restore: (databaseId, backupId) => send<{ ok: boolean }>('POST', `/v1/databases/${databaseId}/backups/${backupId}/restore`),
196
+ list: () => get<BackupWithDb[]>('/v1/backups'),
197
+ remove: async (backupId) => {
198
+ await request(`/v1/backups/${backupId}`, { method: 'DELETE' });
199
+ },
200
+ downloadUrl: (backupId) => `/v1/backups/${backupId}/download`,
201
+ getPolicy: (databaseId) => get<BackupPolicy>(`/v1/databases/${databaseId}/backup-policy`),
202
+ setPolicy: (databaseId, input) => send<BackupPolicy>('PUT', `/v1/databases/${databaseId}/backup-policy`, input),
203
+ },
204
+ backupDestinations: {
205
+ list: () => get('/v1/backup-destinations'),
206
+ create: (input) => send<{ id: number }>('POST', '/v1/backup-destinations', input),
207
+ update: (id, input) => send<{ ok: boolean }>('PATCH', `/v1/backup-destinations/${id}`, input),
208
+ remove: (id) => send<{ ok: boolean }>('DELETE', `/v1/backup-destinations/${id}`),
209
+ test: (id) => send<{ ok: boolean }>('POST', `/v1/backup-destinations/${id}/test`),
210
+ objects: (id, prefix) =>
211
+ get<BackupDestinationObject[]>(
212
+ `/v1/backup-destinations/${id}/objects${prefix === undefined ? '' : `?prefix=${encodeURIComponent(prefix)}`}`,
213
+ ),
214
+ },
215
+ volumeBackups: {
216
+ // Path segments are user-visible names: encode them like the rest of
217
+ // the client does, or '?'/'#'/'%' in a volume name rewrite the URL.
218
+ list: (volumeName) => get<Backup[]>(`/v1/volumes/${encodeURIComponent(volumeName)}/backups`),
219
+ create: (volumeName, input) =>
220
+ send<Backup>('POST', `/v1/volumes/${encodeURIComponent(volumeName)}/backups`, input),
221
+ restore: (volumeName, backupId) =>
222
+ send<{ ok: boolean }>('POST', `/v1/volumes/${encodeURIComponent(volumeName)}/backups/${backupId}/restore`),
223
+ downloadUrl: (volumeName, backupId) =>
224
+ `/v1/volumes/${encodeURIComponent(volumeName)}/backups/${backupId}/download`,
225
+ },
226
+ };
227
+ }