@fluid-app/fluid-cli-mist 0.1.9 → 0.1.11
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/dist/index.d.mts +49 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +282 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -129,9 +129,10 @@ interface DropletUpdateRequest {
|
|
|
129
129
|
}
|
|
130
130
|
interface DropletRecord {
|
|
131
131
|
/**
|
|
132
|
-
* Numeric id is NOT returned by `POST /api/droplets`
|
|
133
|
-
*
|
|
134
|
-
* `
|
|
132
|
+
* Numeric id is NOT returned by `POST /api/droplets` — attach by `uuid`
|
|
133
|
+
* instead (see `IntegrationPointCreateInput.integratable_uuid`). The
|
|
134
|
+
* `GET /api/droplets` list rows DO carry it, which is the only way to
|
|
135
|
+
* map a legacy integration point's `integratable_id` back to a uuid.
|
|
135
136
|
*/
|
|
136
137
|
id?: number;
|
|
137
138
|
uuid: string;
|
|
@@ -151,6 +152,50 @@ interface DropletShowResponse {
|
|
|
151
152
|
meta?: ApiMeta;
|
|
152
153
|
status?: number;
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Page-number pagination used by the company-level droplet + installation
|
|
157
|
+
* list endpoints (`?page=&per_page=`) — unlike the mist sub-resources,
|
|
158
|
+
* these do NOT use cursor pagination. Shape mirrors fluid-admin's
|
|
159
|
+
* `networking/droplets.api.ts` response schemas.
|
|
160
|
+
*/
|
|
161
|
+
interface PagePaginationMeta {
|
|
162
|
+
request_id?: string;
|
|
163
|
+
timestamp?: string;
|
|
164
|
+
pagination?: {
|
|
165
|
+
current_page?: number;
|
|
166
|
+
total_pages?: number;
|
|
167
|
+
total_count?: number;
|
|
168
|
+
per_page?: number;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** `GET /api/droplets` — the company's droplets, page-paginated. The list
|
|
172
|
+
* rows carry the numeric `id` (unlike `POST /api/droplets`), which is how
|
|
173
|
+
* a legacy integration point's `integratable_id` maps back to a uuid. */
|
|
174
|
+
interface DropletsListResponse {
|
|
175
|
+
droplets?: DropletRecord[];
|
|
176
|
+
meta?: PagePaginationMeta;
|
|
177
|
+
status?: number;
|
|
178
|
+
}
|
|
179
|
+
interface DropletInstallation {
|
|
180
|
+
/** The installation's own dri_ uuid (needed to uninstall later). */
|
|
181
|
+
uuid?: string;
|
|
182
|
+
/** The installed droplet's drp_ uuid. */
|
|
183
|
+
droplet_uuid?: string;
|
|
184
|
+
}
|
|
185
|
+
interface DropletInstallationsListResponse {
|
|
186
|
+
droplet_installations?: DropletInstallation[];
|
|
187
|
+
meta?: PagePaginationMeta;
|
|
188
|
+
status?: number;
|
|
189
|
+
}
|
|
190
|
+
interface DropletInstallationCreateRequest {
|
|
191
|
+
droplet_uuid: string;
|
|
192
|
+
scopes?: string[];
|
|
193
|
+
}
|
|
194
|
+
interface DropletInstallationCreateResponse {
|
|
195
|
+
droplet_installation?: DropletInstallation;
|
|
196
|
+
meta?: ApiMeta;
|
|
197
|
+
status?: number;
|
|
198
|
+
}
|
|
154
199
|
/** Hyphenated wire kinds the integration-points API speaks. */
|
|
155
200
|
type IntegrationPointKind = "droplet" | "drop-zone" | "mobile-embed";
|
|
156
201
|
interface IntegrationPoint {
|
|
@@ -311,5 +356,5 @@ interface MistFile {
|
|
|
311
356
|
//#region src/index.d.ts
|
|
312
357
|
declare const plugin: FluidPlugin;
|
|
313
358
|
//#endregion
|
|
314
|
-
export { ApiCollection, ApiMeta, Deployment, DeploymentShowResponse, DeploymentState, DeploymentsListResponse, DropZoneCreateInput, DropZoneCreateRequest, DropZoneCreateResponse, DropZoneRecord, Droplet, DropletCreateInput, DropletCreateRequest, DropletCreateResponse, DropletRecord, DropletShowResponse, DropletUpdateInput, DropletUpdateRequest, EnvVar, EnvVarsListResponse, GitCredential, GitCredentialResponse, HostableType, IntegrationPoint, IntegrationPointCreateInput, IntegrationPointCreateRequest, IntegrationPointKind, IntegrationPointShowResponse, IntegrationPointsListResponse, LogEntry, LogsResponse, MANAGED_DROPLET_ENV_KEYS, ManagedDropletEnvKey, Mist, MistCreateRequest, MistFile, MistKind, MistListResponse, MistShowResponse, MistState, MistUpdateRequest, MobileWidgetCreateInput, MobileWidgetCreateRequest, MobileWidgetCreateResponse, MobileWidgetRecord, PaginationCursor, plugin as default };
|
|
359
|
+
export { ApiCollection, ApiMeta, Deployment, DeploymentShowResponse, DeploymentState, DeploymentsListResponse, DropZoneCreateInput, DropZoneCreateRequest, DropZoneCreateResponse, DropZoneRecord, Droplet, DropletCreateInput, DropletCreateRequest, DropletCreateResponse, DropletInstallation, DropletInstallationCreateRequest, DropletInstallationCreateResponse, DropletInstallationsListResponse, DropletRecord, DropletShowResponse, DropletUpdateInput, DropletUpdateRequest, DropletsListResponse, EnvVar, EnvVarsListResponse, GitCredential, GitCredentialResponse, HostableType, IntegrationPoint, IntegrationPointCreateInput, IntegrationPointCreateRequest, IntegrationPointKind, IntegrationPointShowResponse, IntegrationPointsListResponse, LogEntry, LogsResponse, MANAGED_DROPLET_ENV_KEYS, ManagedDropletEnvKey, Mist, MistCreateRequest, MistFile, MistKind, MistListResponse, MistShowResponse, MistState, MistUpdateRequest, MobileWidgetCreateInput, MobileWidgetCreateRequest, MobileWidgetCreateResponse, MobileWidgetRecord, PagePaginationMeta, PaginationCursor, plugin as default };
|
|
315
360
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;;;;;AASA;;;;;KAAY,QAAA;AAAA,KAEA,SAAA;;;;AAeZ;;;;KAAY,YAAA;AAAA,UAEK,OAAA;EACf,EAAA;EACA,IAAA;EACA,IAAA;AAAA;;;;;AAWF;;;;cAAa,wBAAA;AAAA,KAMD,oBAAA,WAA+B,wBAAA;AAAA,UAE1B,IAAA;EACf,EAAA;EACA,IAAA;EAJyC;AAE3C;EAKE,IAAA;EACA,KAAA,EAAO,SAAA;EACP,IAAA,EAAM,QAAA;;;;;;EAMN,aAAA,GAAgB,YAAA;;EAEhB,WAAA;;;;;;EAMA,WAAA;EACA,UAAA;;EAEA,oBAAA;EACA,OAAA,EAAS,OAAA;EACT,cAAA;EACA,UAAA;EACA,UAAA;AAAA;AAAA,UAGe,gBAAA;EACf,MAAA;EACA,KAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,UAGe,OAAA;EACf,UAAA;EACA,SAAA;EACA,UAAA,GAAa,gBAAA;AAAA;AAAA,UAGE,aAAA;EACf,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,gBAAA;EACV,KAAA,EAAO,IAAA;EACP,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,gBAAA;EACV,IAAA,EAAM,IAAA;EACN,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;IACE,IAAA;IACA,IAAA,GAAO,QAAA;EAAA;AAAA;AAfX;;;;;AAAA,UAwBiB,iBAAA;EACf,IAAA;IACE,IAAA;IACA,IAAA,GAAO,QAAA;EAAA;AAAA;AAAA,UAQM,kBAAA;EACf,IAAA;;;EAGA,SAAA;EACA,mBAAA;EACA,qBAAA;EACA,gBAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,kBAAA;EACf,SAAA;EACA,mBAAA;EACA,qBAAA;AAAA;AAAA,UAGe,oBAAA;EACf,OAAA,EAAS,kBAAA;AAAA;AAAA,UAGM,oBAAA;EACf,OAAA,EAAS,kBAAA;AAAA;AAAA,UAGM,aAAA;EApCA
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;;;;;AASA;;;;;KAAY,QAAA;AAAA,KAEA,SAAA;;;;AAeZ;;;;KAAY,YAAA;AAAA,UAEK,OAAA;EACf,EAAA;EACA,IAAA;EACA,IAAA;AAAA;;;;;AAWF;;;;cAAa,wBAAA;AAAA,KAMD,oBAAA,WAA+B,wBAAA;AAAA,UAE1B,IAAA;EACf,EAAA;EACA,IAAA;EAJyC;AAE3C;EAKE,IAAA;EACA,KAAA,EAAO,SAAA;EACP,IAAA,EAAM,QAAA;;;;;;EAMN,aAAA,GAAgB,YAAA;;EAEhB,WAAA;;;;;;EAMA,WAAA;EACA,UAAA;;EAEA,oBAAA;EACA,OAAA,EAAS,OAAA;EACT,cAAA;EACA,UAAA;EACA,UAAA;AAAA;AAAA,UAGe,gBAAA;EACf,MAAA;EACA,KAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,UAGe,OAAA;EACf,UAAA;EACA,SAAA;EACA,UAAA,GAAa,gBAAA;AAAA;AAAA,UAGE,aAAA;EACf,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,gBAAA;EACV,KAAA,EAAO,IAAA;EACP,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,gBAAA;EACV,IAAA,EAAM,IAAA;EACN,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;IACE,IAAA;IACA,IAAA,GAAO,QAAA;EAAA;AAAA;AAfX;;;;;AAAA,UAwBiB,iBAAA;EACf,IAAA;IACE,IAAA;IACA,IAAA,GAAO,QAAA;EAAA;AAAA;AAAA,UAQM,kBAAA;EACf,IAAA;;;EAGA,SAAA;EACA,mBAAA;EACA,qBAAA;EACA,gBAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,kBAAA;EACf,SAAA;EACA,mBAAA;EACA,qBAAA;AAAA;AAAA,UAGe,oBAAA;EACf,OAAA,EAAS,kBAAA;AAAA;AAAA,UAGM,oBAAA;EACf,OAAA,EAAS,kBAAA;AAAA;AAAA,UAGM,aAAA;EApCA;;;;;;EA2Cf,EAAA;EACA,IAAA;EACA,IAAA;EACA,SAAA;AAAA;AAAA,UAGe,qBAAA;EACf,OAAA,EAAS,aAAA;;;EAGT,cAAA;EACA,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;AAAA,UAGe,mBAAA;EACf,OAAA,EAAS,aAAA;EACT,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;;;;;;;UASe,kBAAA;EACf,UAAA;EACA,SAAA;EACA,UAAA;IACE,YAAA;IACA,WAAA;IACA,WAAA;IACA,QAAA;EAAA;AAAA;;AAzCJ;;UAgDiB,oBAAA;EACf,QAAA,GAAW,aAAA;EACX,IAAA,GAAO,kBAAA;EACP,MAAA;AAAA;AAAA,UAYe,mBAAA;;EAEf,IAAA;EApDF;EAsDE,YAAA;AAAA;AAAA,UAGe,gCAAA;EACf,qBAAA,GAAwB,mBAAA;EACxB,IAAA,GAAO,kBAAA;EACP,MAAA;AAAA;AAAA,UAGe,gCAAA;EACf,YAAA;EACA,MAAA;AAAA;AAAA,UAGe,iCAAA;EACf,oBAAA,GAAuB,mBAAA;EACvB,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;;KASU,oBAAA;AAAA,UAEK,gBAAA;;EAEf,EAAA;EACA,IAAA,EAAM,oBAAA;EAhER;EAkEE,eAAA;;;;;;EAMA,iBAAA;EACA,iBAAA;EACA,OAAA;AAAA;;;AA5DF;;;;;;;UAwEiB,2BAAA;EACf,IAAA,EAAM,oBAAA;EACN,iBAAA;EACA,eAAA;AAAA;AAAA,UAGe,6BAAA;EACf,iBAAA,EAAmB,2BAAA;AAAA;AAAA,UAGJ,6BAAA;EACf,kBAAA,EAAoB,gBAAA;EACpB,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,4BAAA;EACf,iBAAA,EAAmB,gBAAA;EACnB,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAOe,mBAAA;EACf,IAAA;EACA,SAAA;EAxEe;EA0Ef,QAAA;IAAa,IAAA;IAAe,IAAA;IAAe,MAAA;EAAA;AAAA;AAAA,UAG5B,qBAAA;EACf,SAAA,EAAW,mBAAA;AAAA;AAAA,UAGI,cAAA;EACf,IAAA;EACA,IAAA;EACA,SAAA;EACA,MAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,sBAAA;EACf,SAAA,EAAW,cAAA;EACX,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;AAAA,UAOe,uBAAA;EACf,IAAA;EACA,SAAA;;EAEA,eAAA;;EAEA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,yBAAA;EACf,aAAA,EAAe,uBAAA;AAAA;AAAA,UAGA,kBAAA;EACf,EAAA;EACA,IAAA;EACA,SAAA;EACA,eAAA;EACA,MAAA;AAAA;AAAA,UAGe,0BAAA;EACf,aAAA,EAAe,kBAAA;EACf,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;AAAA,UAGe,MAAA;EACf,GAAA;EACA,KAAA;EACA,OAAA;EACA,UAAA;AAAA;AAAA,UAGe,mBAAA;EACf,QAAA,EAAU,MAAA;EACV,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,aAAA;EACf,UAAA;EACA,UAAA;AAAA;AAAA,UAGe,qBAAA;EACf,cAAA,EAAgB,aAAA;EAChB,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,eAAA;AAAA,UAOK,UAAA;EACf,EAAA;EACA,KAAA,EAAO,eAAA;EACP,GAAA;EACA,UAAA;EACA,cAAA;EACA,UAAA;EACA,MAAA;AAAA;AAAA,UAGe,uBAAA;EACf,WAAA,EAAa,UAAA;EACb,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,sBAAA;EACf,UAAA,EAAY,UAAA;EACZ,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,QAAA;EACf,SAAA;EACA,KAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGe,YAAA;EACf,aAAA;EACA,IAAA,EAAM,QAAA;EACN,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;;UAIe,QAAA;EACf,IAAA;;;;;;EAMA,WAAA;EACA,UAAA;AAAA;;;cC1ZI,MAAA,EAAQ,WAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -131,28 +131,28 @@ async function resolveMistId(client, slugOrId) {
|
|
|
131
131
|
}
|
|
132
132
|
//#endregion
|
|
133
133
|
//#region src/api/mists.ts
|
|
134
|
-
const BASE$
|
|
134
|
+
const BASE$4 = "/api/v202604/mists";
|
|
135
135
|
function listMists(client, params = {}) {
|
|
136
|
-
return client.get(BASE$
|
|
136
|
+
return client.get(BASE$4, params);
|
|
137
137
|
}
|
|
138
138
|
async function showMist(client, slug) {
|
|
139
139
|
const id = await resolveMistId(client, slug);
|
|
140
|
-
return client.get(`${BASE$
|
|
140
|
+
return client.get(`${BASE$4}/${id}`);
|
|
141
141
|
}
|
|
142
142
|
function createMist(client, body) {
|
|
143
|
-
return client.post(BASE$
|
|
143
|
+
return client.post(BASE$4, body);
|
|
144
144
|
}
|
|
145
145
|
async function updateMist(client, slug, body) {
|
|
146
146
|
const id = await resolveMistId(client, slug);
|
|
147
|
-
return client.patch(`${BASE$
|
|
147
|
+
return client.patch(`${BASE$4}/${id}`, body);
|
|
148
148
|
}
|
|
149
149
|
async function deleteMist(client, slug) {
|
|
150
150
|
const id = await resolveMistId(client, slug);
|
|
151
|
-
return client.delete(`${BASE$
|
|
151
|
+
return client.delete(`${BASE$4}/${id}`);
|
|
152
152
|
}
|
|
153
153
|
async function restoreMist(client, slug) {
|
|
154
154
|
const id = await resolveMistId(client, slug);
|
|
155
|
-
return client.post(`${BASE$
|
|
155
|
+
return client.post(`${BASE$4}/${id}/restore`);
|
|
156
156
|
}
|
|
157
157
|
//#endregion
|
|
158
158
|
//#region src/lib/hostable.ts
|
|
@@ -542,7 +542,7 @@ function createSilentReporter() {
|
|
|
542
542
|
}
|
|
543
543
|
//#endregion
|
|
544
544
|
//#region src/api/droplets.ts
|
|
545
|
-
const BASE$
|
|
545
|
+
const BASE$3 = "/api/droplets";
|
|
546
546
|
/**
|
|
547
547
|
* Full scope set from `Droplet::Installation::VALID_SCOPES`. We request
|
|
548
548
|
* them all by default so a Mist-created droplet's installations carry write
|
|
@@ -592,11 +592,33 @@ function buildDropletCreateInput(opts) {
|
|
|
592
592
|
return input;
|
|
593
593
|
}
|
|
594
594
|
function createDroplet(client, body) {
|
|
595
|
-
return client.post(BASE$
|
|
595
|
+
return client.post(BASE$3, body);
|
|
596
|
+
}
|
|
597
|
+
/** Page size + runaway guard for walking the page-paginated droplets list. */
|
|
598
|
+
const PER_PAGE$1 = 100;
|
|
599
|
+
const MAX_PAGES$1 = 25;
|
|
600
|
+
/**
|
|
601
|
+
* Recover a droplet's drp_ uuid from its numeric id by paging the company's
|
|
602
|
+
* droplets list (`GET /api/droplets?page=&per_page=` — the list rows carry
|
|
603
|
+
* `id`, unlike the create/show responses). Needed for legacy integration
|
|
604
|
+
* points that only echo `integratable_id`: the show endpoint resolves by
|
|
605
|
+
* uuid, so a numeric id can't be fetched directly. Returns null when no
|
|
606
|
+
* droplet matches within the page cap.
|
|
607
|
+
*/
|
|
608
|
+
async function findDropletUuidByNumericId(client, id) {
|
|
609
|
+
for (let page = 1; page <= MAX_PAGES$1; page++) {
|
|
610
|
+
const resp = await client.get(`${BASE$3}?page=${page}&per_page=${PER_PAGE$1}`);
|
|
611
|
+
const batch = resp.droplets ?? [];
|
|
612
|
+
const match = batch.find((d) => d.id === id);
|
|
613
|
+
if (match) return match.uuid ?? null;
|
|
614
|
+
const totalPages = resp.meta?.pagination?.total_pages;
|
|
615
|
+
if (totalPages != null ? page >= totalPages : batch.length < PER_PAGE$1) break;
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
596
618
|
}
|
|
597
619
|
/** PATCH the droplet's embed URL (+ derived webhook URLs) once known. */
|
|
598
620
|
function updateDroplet(client, uuid, input) {
|
|
599
|
-
return client.patch(`${BASE$
|
|
621
|
+
return client.patch(`${BASE$3}/${encodeURIComponent(uuid)}`, { droplet: input });
|
|
600
622
|
}
|
|
601
623
|
/** Convenience: set embed URL + derived webhook URLs in one PATCH. */
|
|
602
624
|
function setDropletEmbedUrl(client, uuid, embedUrl) {
|
|
@@ -606,6 +628,43 @@ function setDropletEmbedUrl(client, uuid, embedUrl) {
|
|
|
606
628
|
});
|
|
607
629
|
}
|
|
608
630
|
//#endregion
|
|
631
|
+
//#region src/api/droplet_installations.ts
|
|
632
|
+
const BASE$2 = "/api/droplet_installations";
|
|
633
|
+
/** Page size + runaway guard for walking the page-paginated list. */
|
|
634
|
+
const PER_PAGE = 100;
|
|
635
|
+
const MAX_PAGES = 25;
|
|
636
|
+
/**
|
|
637
|
+
* List ALL of the current company's droplet installations, following the
|
|
638
|
+
* endpoint's page-number pagination (`?page=&per_page=` — same shape as
|
|
639
|
+
* fluid-admin's `getCompanyDroplets`). A single-page fetch would miss rows
|
|
640
|
+
* beyond the first page, making the idempotency check (and the duplicate-
|
|
641
|
+
* rejection confirmation) report a false "not installed" forever.
|
|
642
|
+
*/
|
|
643
|
+
async function listAllDropletInstallations(client) {
|
|
644
|
+
const rows = [];
|
|
645
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
646
|
+
const resp = await client.get(`${BASE$2}?page=${page}&per_page=${PER_PAGE}`);
|
|
647
|
+
const batch = resp.droplet_installations ?? [];
|
|
648
|
+
rows.push(...batch);
|
|
649
|
+
const totalPages = resp.meta?.pagination?.total_pages;
|
|
650
|
+
if (totalPages != null ? page >= totalPages : batch.length < PER_PAGE) break;
|
|
651
|
+
}
|
|
652
|
+
return rows;
|
|
653
|
+
}
|
|
654
|
+
/** Install a droplet (by drp_ uuid) for the current company. */
|
|
655
|
+
function createDropletInstallation(client, input) {
|
|
656
|
+
return client.post(BASE$2, input);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The company's installation row for a droplet drp_ uuid, or undefined.
|
|
660
|
+
* The idempotency key for the install step: an existing row means the
|
|
661
|
+
* droplet is already connected (installed via a previous run, Mist Desktop,
|
|
662
|
+
* or the admin marketplace).
|
|
663
|
+
*/
|
|
664
|
+
function findInstallationForDroplet(installations, dropletUuid) {
|
|
665
|
+
return (installations ?? []).find((i) => i.droplet_uuid === dropletUuid);
|
|
666
|
+
}
|
|
667
|
+
//#endregion
|
|
609
668
|
//#region src/api/drop_zones.ts
|
|
610
669
|
const BASE$1 = "/api/drop_zones";
|
|
611
670
|
/**
|
|
@@ -717,6 +776,10 @@ const MANAGED_DROPLET_ENV_KEYS = [
|
|
|
717
776
|
* - Only a droplet attach provisions managed env keys
|
|
718
777
|
* (FLUID_DROPLET_UUID/_SECRET/_WEBHOOK_AUTH_TOKEN); this flow only
|
|
719
778
|
* VERIFIES they landed — it never writes them (the API 422s on write).
|
|
779
|
+
* - A droplet must also be INSTALLED (POST /api/droplet_installations) —
|
|
780
|
+
* create + link alone leave it not-connected/inactive, so install-scoped
|
|
781
|
+
* features (dit/dex exchange, admin embeds, install webhooks) don't work.
|
|
782
|
+
* The install step is idempotent (skips when an installation exists).
|
|
720
783
|
*/
|
|
721
784
|
const DEFAULT_TIMING = {
|
|
722
785
|
publicUrlTimeoutMs: 9e4,
|
|
@@ -866,6 +929,21 @@ async function provisionSurface(client, options) {
|
|
|
866
929
|
if (surfaceNumericId == null) surfaceNumericId = ipResp.integration_point.integratable_id ?? null;
|
|
867
930
|
reporter.succeed(kind === "droplet" ? "Droplet attached — provisioning env + redeploy…" : `${label} attached`);
|
|
868
931
|
}
|
|
932
|
+
let installation = null;
|
|
933
|
+
if (kind === "droplet") {
|
|
934
|
+
if (!surfaceUuid && surfaceNumericId != null) try {
|
|
935
|
+
surfaceUuid = await findDropletUuidByNumericId(client, surfaceNumericId);
|
|
936
|
+
} catch {}
|
|
937
|
+
if (!surfaceUuid) {
|
|
938
|
+
reporter.fail("Droplet uuid unknown — cannot install");
|
|
939
|
+
throw new Error("Couldn't determine the droplet uuid, so the droplet is linked but NOT installed — it stays inactive until an installation exists. Re-run with `--existing <droplet-uuid>`, or install manually: POST /api/droplet_installations with {\"droplet_uuid\":\"<uuid>\"}.");
|
|
940
|
+
}
|
|
941
|
+
installation = await ensureDropletInstalled(client, {
|
|
942
|
+
dropletUuid: surfaceUuid,
|
|
943
|
+
scopes: options.requestedScopes ?? DEFAULT_DROPLET_SCOPES,
|
|
944
|
+
reporter
|
|
945
|
+
});
|
|
946
|
+
}
|
|
869
947
|
const managedEnv = kind === "droplet" ? await verifyManagedEnv(client, {
|
|
870
948
|
slugOrId,
|
|
871
949
|
reporter,
|
|
@@ -894,10 +972,79 @@ async function provisionSurface(client, options) {
|
|
|
894
972
|
},
|
|
895
973
|
surfaceCreated,
|
|
896
974
|
embedUrlSet,
|
|
975
|
+
installation,
|
|
897
976
|
managedEnv
|
|
898
977
|
};
|
|
899
978
|
}
|
|
900
979
|
/**
|
|
980
|
+
* Install the droplet for the current company (idempotent). Checks the
|
|
981
|
+
* installations list first; treats the API's already-installed rejection as
|
|
982
|
+
* success too, so re-runs and races both converge. A hard failure throws with
|
|
983
|
+
* the droplet uuid + recovery steps — the created droplet and mist link stay
|
|
984
|
+
* in place, so an idempotent re-run finishes the job.
|
|
985
|
+
*/
|
|
986
|
+
async function ensureDropletInstalled(client, args) {
|
|
987
|
+
const { reporter, dropletUuid } = args;
|
|
988
|
+
reporter.start("Installing droplet…");
|
|
989
|
+
let existing;
|
|
990
|
+
try {
|
|
991
|
+
existing = findInstallationForDroplet(await listAllDropletInstallations(client), dropletUuid);
|
|
992
|
+
} catch {}
|
|
993
|
+
if (existing) {
|
|
994
|
+
reporter.skip(`Droplet already installed${existing.uuid ? ` (${existing.uuid})` : ""} — skipping`);
|
|
995
|
+
return {
|
|
996
|
+
uuid: existing.uuid ?? null,
|
|
997
|
+
created: false
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const uuid = (await createDropletInstallation(client, {
|
|
1002
|
+
droplet_uuid: dropletUuid,
|
|
1003
|
+
scopes: [...args.scopes]
|
|
1004
|
+
})).droplet_installation?.uuid ?? null;
|
|
1005
|
+
reporter.succeed(`Droplet installed${uuid ? ` (${uuid})` : ""}`);
|
|
1006
|
+
return {
|
|
1007
|
+
uuid,
|
|
1008
|
+
created: true
|
|
1009
|
+
};
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
if (isAlreadyInstalledError(err)) {
|
|
1012
|
+
const confirmed = await lookupInstallation(client, dropletUuid);
|
|
1013
|
+
if (confirmed) {
|
|
1014
|
+
reporter.skip("Droplet already installed — skipping");
|
|
1015
|
+
return {
|
|
1016
|
+
uuid: confirmed.uuid ?? null,
|
|
1017
|
+
created: false
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
reporter.fail("Droplet install failed");
|
|
1022
|
+
throw normalizeInstallError(err, dropletUuid);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function isAlreadyInstalledError(err) {
|
|
1026
|
+
return err instanceof MistApiError && (err.status === 409 || err.status === 422) && /already|taken|installed|exists/i.test(`${err.message} ${JSON.stringify(err.details ?? {})}`);
|
|
1027
|
+
}
|
|
1028
|
+
/** Best-effort installation lookup for a droplet; null when the row can't
|
|
1029
|
+
* be found or the list call fails. */
|
|
1030
|
+
async function lookupInstallation(client, dropletUuid) {
|
|
1031
|
+
try {
|
|
1032
|
+
return findInstallationForDroplet(await listAllDropletInstallations(client), dropletUuid) ?? null;
|
|
1033
|
+
} catch {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Install failures must NOT read as "wired up" — an uninstalled droplet means
|
|
1039
|
+
* dit/dex token exchange, admin embeds, and install webhooks silently don't
|
|
1040
|
+
* work. Surface the droplet uuid + recovery path; keep the MistApiError
|
|
1041
|
+
* status so the error handler exits with the spec'd non-zero code.
|
|
1042
|
+
*/
|
|
1043
|
+
function normalizeInstallError(err, dropletUuid) {
|
|
1044
|
+
const message = `Droplet ${dropletUuid} was created and linked to the mist, but installing it failed: ${err instanceof Error ? err.message : String(err)}. The droplet and its link were left in place — re-run this command to finish the install, or install manually: POST /api/droplet_installations with {"droplet_uuid":"${dropletUuid}"}.`;
|
|
1045
|
+
return err instanceof MistApiError ? new MistApiError(message, err.status, err.details) : new Error(message);
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
901
1048
|
* If the integration-points endpoint rejects the attach identifier (e.g. an
|
|
902
1049
|
* older API that predates #19688's attach-by-uuid support), surface an
|
|
903
1050
|
* actionable message noting the surface WAS created. Other errors pass through.
|
|
@@ -1031,6 +1178,10 @@ function printSurfaceSummary(result) {
|
|
|
1031
1178
|
const ref = result.surface.uuid ?? (result.surface.id != null ? `#${result.surface.id}` : "(unknown)");
|
|
1032
1179
|
const state = result.surfaceCreated ? "created" : "existing";
|
|
1033
1180
|
console.log(chalk.dim(` ${KIND_LABEL[result.kind]}: ${ref} (${state}) · integration ${result.integrationPoint.id}`));
|
|
1181
|
+
if (result.kind === "droplet" && result.installation) {
|
|
1182
|
+
const ref = result.installation.uuid ?? "(uuid unknown)";
|
|
1183
|
+
console.log(chalk.dim(` installation: ${ref} (${result.installation.created ? "installed" : "already installed"})`));
|
|
1184
|
+
}
|
|
1034
1185
|
if (result.kind === "droplet" && result.managedEnv) if (result.managedEnv.ready) console.log(chalk.dim(" env: managed keys provisioned ✓"));
|
|
1035
1186
|
else console.log(chalk.yellow(" env: redeploy in progress — run `fluid mist env list` shortly to confirm managed keys"));
|
|
1036
1187
|
}
|
|
@@ -2035,12 +2186,67 @@ const logsCommand = new Command("logs").description("Fetch deployment logs (use
|
|
|
2035
2186
|
}
|
|
2036
2187
|
}));
|
|
2037
2188
|
//#endregion
|
|
2189
|
+
//#region src/lib/encrypted-env.ts
|
|
2190
|
+
/**
|
|
2191
|
+
* Detects Vercel-encrypted env var ciphertext.
|
|
2192
|
+
*
|
|
2193
|
+
* `GET /api/v202604/mists/:id/env_vars` is a thin proxy to the Mist's
|
|
2194
|
+
* Vercel project env — it returns values ENCRYPTED, never plaintext (see
|
|
2195
|
+
* apps/mist-desktop/src/main/services/mist-env-vars.service.ts:25-28,
|
|
2196
|
+
* which documents the same server behavior for the desktop app's copy of
|
|
2197
|
+
* this API). The ciphertext is a base64-encoded JSON envelope of shape
|
|
2198
|
+
* `{"v":"v2","c":"<base64 ciphertext>","k":[<byte array>]}`.
|
|
2199
|
+
*
|
|
2200
|
+
* `fluid mist env pull` uses this to avoid writing that ciphertext into
|
|
2201
|
+
* `.env.local` as if it were a usable value.
|
|
2202
|
+
*/
|
|
2203
|
+
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
2204
|
+
function looksLikeBase64(value) {
|
|
2205
|
+
return value.length > 0 && value.length % 4 === 0 && BASE64_RE.test(value);
|
|
2206
|
+
}
|
|
2207
|
+
function hasEncryptedEnvelopeShape(value) {
|
|
2208
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "v" in value && "c" in value && "k" in value;
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* True when `value` is the Vercel encrypted-env envelope: a base64 string
|
|
2212
|
+
* that decodes to a JSON object with the `{ v, c, k }` shape. Returns
|
|
2213
|
+
* false for empty strings, non-base64 strings, and base64 strings that
|
|
2214
|
+
* decode to plaintext or to JSON that doesn't match the envelope shape —
|
|
2215
|
+
* so real plaintext secrets that happen to look like base64 are never
|
|
2216
|
+
* misclassified as ciphertext.
|
|
2217
|
+
*/
|
|
2218
|
+
function isVercelEncryptedEnvValue(value) {
|
|
2219
|
+
if (!looksLikeBase64(value)) return false;
|
|
2220
|
+
let decoded;
|
|
2221
|
+
try {
|
|
2222
|
+
decoded = Buffer.from(value, "base64").toString("utf8");
|
|
2223
|
+
} catch {
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
let parsed;
|
|
2227
|
+
try {
|
|
2228
|
+
parsed = JSON.parse(decoded);
|
|
2229
|
+
} catch {
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
return hasEncryptedEnvelopeShape(parsed);
|
|
2233
|
+
}
|
|
2234
|
+
//#endregion
|
|
2038
2235
|
//#region src/commands/env/list.ts
|
|
2039
2236
|
/** `fluid mist env list` — list env vars; mark managed_by entries. Spec §4. */
|
|
2040
2237
|
function maskValue(value) {
|
|
2041
2238
|
if (value.length <= 8) return "•".repeat(value.length);
|
|
2042
2239
|
return `${value.slice(0, 4)}${"•".repeat(8)}${value.slice(-2)}`;
|
|
2043
2240
|
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Values come back Vercel-encrypted ciphertext (see
|
|
2243
|
+
* `../../lib/encrypted-env.ts`) — never show that raw blob, masked or not,
|
|
2244
|
+
* since it isn't the real secret and can run to ~1KB of base64.
|
|
2245
|
+
*/
|
|
2246
|
+
function displayValue(v, showValues) {
|
|
2247
|
+
if (isVercelEncryptedEnvValue(v.value)) return chalk.dim("<encrypted>");
|
|
2248
|
+
return showValues ? v.value : maskValue(v.value);
|
|
2249
|
+
}
|
|
2044
2250
|
const envListCommand = new Command("list").description("List a mist's env vars").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").option("--show-values", "Print full values instead of masking them").option("--json", "Emit raw API JSON").action((slug, opts) => runAction(async () => {
|
|
2045
2251
|
const resolved = resolveSlug(slug);
|
|
2046
2252
|
const response = await listEnvVars(createMistClient(), resolved);
|
|
@@ -2057,7 +2263,7 @@ const envListCommand = new Command("list").description("List a mist's env vars")
|
|
|
2057
2263
|
chalk.bold("MANAGED")
|
|
2058
2264
|
], ...vars.map((v) => [
|
|
2059
2265
|
v.key,
|
|
2060
|
-
opts.showValues
|
|
2266
|
+
displayValue(v, opts.showValues),
|
|
2061
2267
|
v.targets.join(","),
|
|
2062
2268
|
v.managed_by === "mist" ? chalk.magenta("mist") : chalk.dim("user")
|
|
2063
2269
|
])]);
|
|
@@ -2137,21 +2343,79 @@ const envUnsetCommand = new Command("unset").description("Delete one or more env
|
|
|
2137
2343
|
* `fluid mist env pull` — writes `.env.local` with user-managed vars only.
|
|
2138
2344
|
* Skips `managed_by: "mist"` entries (DATABASE_URL, FLUID_*) since local
|
|
2139
2345
|
* dev uses PGlite, not the production database. Spec §4 + §5.
|
|
2346
|
+
*
|
|
2347
|
+
* Also skips user-managed vars whose value comes back as Vercel-encrypted
|
|
2348
|
+
* ciphertext (`GET .../env_vars` is a proxy to Vercel's project env, which
|
|
2349
|
+
* never returns plaintext — see `../../lib/encrypted-env.ts`). Writing that
|
|
2350
|
+
* ciphertext into `.env.local` verbatim would silently produce an unusable
|
|
2351
|
+
* file, so those keys are collected and surfaced as a warning instead.
|
|
2352
|
+
*
|
|
2353
|
+
* The output file is MERGED, not replaced: pulled plaintext values update
|
|
2354
|
+
* their existing lines (or are appended), while every other existing line
|
|
2355
|
+
* — comments, locally maintained secrets for encrypted/managed keys, and
|
|
2356
|
+
* local-only vars — is preserved verbatim. Replacing the file wholesale
|
|
2357
|
+
* would delete manually set values for skipped keys, and an all-encrypted
|
|
2358
|
+
* response would truncate the file to zero bytes right before telling the
|
|
2359
|
+
* user to set those same values locally.
|
|
2140
2360
|
*/
|
|
2141
2361
|
function escapeValue(v) {
|
|
2142
2362
|
if (/[\s#"']/.test(v)) return `"${v.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
2143
2363
|
return v;
|
|
2144
2364
|
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Splits the raw env-var list into what `env pull` can safely write vs.
|
|
2367
|
+
* what it must skip, and why. Pure — no I/O — so it's unit-testable
|
|
2368
|
+
* without mocking the API client or the filesystem.
|
|
2369
|
+
*/
|
|
2370
|
+
function partitionPullEnvVars(envVars) {
|
|
2371
|
+
const plaintext = [];
|
|
2372
|
+
const encrypted = [];
|
|
2373
|
+
const managed = [];
|
|
2374
|
+
for (const v of envVars) if (v.managed_by === "mist") managed.push(v);
|
|
2375
|
+
else if (isVercelEncryptedEnvValue(v.value)) encrypted.push(v);
|
|
2376
|
+
else plaintext.push(v);
|
|
2377
|
+
return {
|
|
2378
|
+
plaintext,
|
|
2379
|
+
encrypted,
|
|
2380
|
+
managed
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
/** Matches a `KEY=` / `export KEY=` assignment line and captures the key. */
|
|
2384
|
+
const ENV_ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
2385
|
+
/**
|
|
2386
|
+
* Merges pulled plaintext vars into the existing env-file contents.
|
|
2387
|
+
* Existing assignment lines for pulled keys are updated in place; every
|
|
2388
|
+
* other line (comments, blank lines, locally maintained values for keys
|
|
2389
|
+
* the pull skipped) is preserved verbatim; new keys are appended. Pure —
|
|
2390
|
+
* no I/O — so it's unit-testable without touching the filesystem.
|
|
2391
|
+
*/
|
|
2392
|
+
function mergeEnvContents(existing, plaintext) {
|
|
2393
|
+
const updates = new Map(plaintext.map((v) => [v.key, `${v.key}=${escapeValue(v.value)}`]));
|
|
2394
|
+
const lines = existing.length > 0 ? existing.split("\n") : [];
|
|
2395
|
+
if (lines.at(-1) === "") lines.pop();
|
|
2396
|
+
const out = [];
|
|
2397
|
+
for (const line of lines) {
|
|
2398
|
+
const key = ENV_ASSIGNMENT.exec(line)?.[1];
|
|
2399
|
+
const replacement = key === void 0 ? void 0 : updates.get(key);
|
|
2400
|
+
if (key !== void 0 && replacement !== void 0) {
|
|
2401
|
+
out.push(replacement);
|
|
2402
|
+
updates.delete(key);
|
|
2403
|
+
} else out.push(line);
|
|
2404
|
+
}
|
|
2405
|
+
for (const rendered of updates.values()) out.push(rendered);
|
|
2406
|
+
return out.length > 0 ? out.join("\n") + "\n" : "";
|
|
2407
|
+
}
|
|
2145
2408
|
const envPullCommand = new Command("pull").description("Write user-managed env vars to .env.local in the CWD").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").option("--out <path>", "Output path (default: .env.local)").action((slug, opts) => runAction(async () => {
|
|
2146
2409
|
const resolved = resolveSlug(slug);
|
|
2147
2410
|
const { env_vars } = await listEnvVars(createMistClient(), resolved);
|
|
2148
|
-
const
|
|
2149
|
-
const
|
|
2150
|
-
const target = join(process.cwd(),
|
|
2151
|
-
const
|
|
2152
|
-
writeFileSync(target,
|
|
2153
|
-
console.log(chalk.green(`Wrote ${
|
|
2154
|
-
if (
|
|
2411
|
+
const { plaintext, encrypted, managed } = partitionPullEnvVars(env_vars);
|
|
2412
|
+
const outName = opts.out ?? ".env.local";
|
|
2413
|
+
const target = join(process.cwd(), outName);
|
|
2414
|
+
const existing = existsSync(target) ? readFileSync(target, "utf8") : "";
|
|
2415
|
+
writeFileSync(target, mergeEnvContents(existing, plaintext), "utf8");
|
|
2416
|
+
console.log(chalk.green(`Wrote ${plaintext.length} var(s) to ${target.replace(process.cwd() + "/", "")}${existing.length > 0 ? " (existing entries for other keys preserved)" : ""}.`));
|
|
2417
|
+
if (managed.length > 0) console.log(chalk.dim(`Skipped ${managed.length} Mist-managed var(s): ${managed.map((v) => v.key).join(", ")} (local dev uses PGlite)`));
|
|
2418
|
+
if (encrypted.length > 0) console.log(chalk.yellow(`Skipped ${encrypted.length} var(s) whose values are stored encrypted and can't be pulled: ${encrypted.map((v) => v.key).join(", ")} — set them locally in ${outName} yourself (values are visible in the Vercel dashboard) or re-set them via \`fluid mist env set\`.`));
|
|
2155
2419
|
}));
|
|
2156
2420
|
//#endregion
|
|
2157
2421
|
//#region src/commands/env/index.ts
|