@netlify/spark-ui 1.31.0-alpha.2 → 1.31.0-alpha.3
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/components/preact/DropZone/DropZone.d.ts +8 -1
- package/dist/components/preact/DropZone/DropZone.js +42 -40
- package/dist/components/preact/DropZone/drop-core/authedDropClient.d.ts +57 -34
- package/dist/components/preact/DropZone/drop-core/authedDropClient.js +88 -62
- package/dist/components/preact/DropZone/drop-core/buildClient.d.ts +30 -16
- package/dist/components/preact/DropZone/drop-core/buildClient.js +60 -53
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +2 -1
- package/dist/components/preact/DropZone/useDropDeploy.js +60 -59
- package/package.json +1 -1
- package/packages/components/preact/DropZone/DropZone.tsx +9 -0
- package/packages/components/preact/DropZone/drop-core/authedDropClient.ts +86 -40
- package/packages/components/preact/DropZone/drop-core/buildClient.ts +37 -23
- package/packages/components/preact/DropZone/useDropDeploy.ts +8 -2
|
@@ -1,39 +1,41 @@
|
|
|
1
|
-
var
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
import { NotAuthenticatedError as
|
|
5
|
-
const f = "https://api.netlify.com/api/v1", m = 1e3,
|
|
6
|
-
class
|
|
1
|
+
var d = Object.defineProperty;
|
|
2
|
+
var p = (r, t, e) => t in r ? d(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
|
|
3
|
+
var i = (r, t, e) => p(r, typeof t != "symbol" ? t + "" : t, e);
|
|
4
|
+
import { NotAuthenticatedError as n, SiteCreateError as h, BuildCreateError as u, BuildFailedError as w, BuildTimeoutError as y } from "./errors.js";
|
|
5
|
+
const f = "/access-control/bb-api/api/v1", B = "https://api.netlify.com/api/v1", m = 1e3, S = 10 * 60 * 1e3, T = /* @__PURE__ */ new Set(["ready", "error", "rejected"]);
|
|
6
|
+
class _ {
|
|
7
7
|
constructor({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
proxyBase: t = f,
|
|
9
|
+
apiBase: e = B,
|
|
10
|
+
getUploadToken: a,
|
|
11
|
+
accountSlug: o,
|
|
11
12
|
title: s = "Build from drop deployment",
|
|
12
|
-
pollIntervalMs:
|
|
13
|
-
timeoutMs:
|
|
13
|
+
pollIntervalMs: l = m,
|
|
14
|
+
timeoutMs: c = S
|
|
14
15
|
}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this
|
|
16
|
+
i(this, "proxyBase");
|
|
17
|
+
i(this, "apiBase");
|
|
18
|
+
i(this, "getUploadToken");
|
|
19
|
+
i(this, "accountSlug");
|
|
20
|
+
i(this, "title");
|
|
21
|
+
i(this, "pollIntervalMs");
|
|
22
|
+
i(this, "timeoutMs");
|
|
23
|
+
this.proxyBase = t, this.apiBase = e, this.getUploadToken = a, this.accountSlug = o, this.title = s, this.pollIntervalMs = l, this.timeoutMs = c;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
|
-
* Create the site the build deploys into.
|
|
25
|
-
*
|
|
26
|
+
* Create the site the build deploys into. Goes through the proxy with
|
|
27
|
+
* `credentials: 'include'` so the app session authorizes it; without an
|
|
28
|
+
* account slug the API picks the user's default account.
|
|
26
29
|
*/
|
|
27
30
|
async createSite() {
|
|
28
|
-
const t = await this.
|
|
29
|
-
if (!t) throw new l();
|
|
30
|
-
const o = this.accountSlug ? `/${this.accountSlug}/sites` : "/sites", e = await fetch(`${this.apiBase}${o}`, {
|
|
31
|
+
const t = this.accountSlug ? `/${this.accountSlug}/sites` : "/sites", e = await fetch(`${this.proxyBase}${t}`, {
|
|
31
32
|
method: "POST",
|
|
32
|
-
|
|
33
|
+
credentials: "include",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
33
35
|
body: JSON.stringify({ created_via: "drop" })
|
|
34
36
|
});
|
|
35
|
-
if (e.status === 401 || e.status === 403) throw new
|
|
36
|
-
if (!e.ok) throw new
|
|
37
|
+
if (e.status === 401 || e.status === 403) throw new n();
|
|
38
|
+
if (!e.ok) throw new h(e.status);
|
|
37
39
|
return e.json();
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
@@ -41,20 +43,25 @@ class g {
|
|
|
41
43
|
* (see the note on `apiBase`), authorized by a freshly minted bearer token.
|
|
42
44
|
* `Content-Type` is left unset on purpose so the browser adds the multipart
|
|
43
45
|
* boundary itself.
|
|
46
|
+
*
|
|
47
|
+
* Known ceiling: this is one request on a 30s token, and re-minting can't
|
|
48
|
+
* help once it is in flight. A zip that takes longer than that to upload will
|
|
49
|
+
* fail — the app origin gets 300s for exactly this reason, and lifting the
|
|
50
|
+
* limit for other origins is a platform-side change.
|
|
44
51
|
*/
|
|
45
|
-
async createBuildFromZip(t,
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const
|
|
52
|
+
async createBuildFromZip(t, e) {
|
|
53
|
+
const a = await this.getUploadToken();
|
|
54
|
+
if (!a) throw new n();
|
|
55
|
+
const o = new FormData();
|
|
56
|
+
o.append("zip", e), o.append("title", this.title);
|
|
57
|
+
const s = await fetch(`${this.apiBase}/sites/${t}/builds`, {
|
|
51
58
|
method: "POST",
|
|
52
|
-
headers: { Authorization: `Bearer ${
|
|
53
|
-
body:
|
|
59
|
+
headers: { Authorization: `Bearer ${a}` },
|
|
60
|
+
body: o
|
|
54
61
|
});
|
|
55
|
-
if (
|
|
56
|
-
if (!
|
|
57
|
-
return
|
|
62
|
+
if (s.status === 401 || s.status === 403) throw new n();
|
|
63
|
+
if (!s.ok) throw new u(s.status);
|
|
64
|
+
return s.json();
|
|
58
65
|
}
|
|
59
66
|
/**
|
|
60
67
|
* Poll the build's deploy until it settles, mirroring
|
|
@@ -63,30 +70,30 @@ class g {
|
|
|
63
70
|
* deploy id yet has nothing pollable, so it resolves immediately and the
|
|
64
71
|
* caller falls back to the site page.
|
|
65
72
|
*
|
|
66
|
-
* Optional in the drop flow, and off by default: a build takes minutes,
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* a
|
|
73
|
+
* Optional in the drop flow, and off by default: a build takes minutes, so
|
|
74
|
+
* the useful hand-off is usually the project page rather than a spinner.
|
|
75
|
+
* Polling rides the session cookie, so it cannot outlive its credentials the
|
|
76
|
+
* way a 30s bearer would.
|
|
70
77
|
*/
|
|
71
78
|
async waitUntilBuildReady(t) {
|
|
72
79
|
if (!t.deploy_id) return;
|
|
73
|
-
const
|
|
74
|
-
for (let
|
|
75
|
-
const
|
|
76
|
-
|
|
80
|
+
const e = this.timeoutMs / this.pollIntervalMs;
|
|
81
|
+
for (let a = 0; a < e; a++) {
|
|
82
|
+
const o = await fetch(`${this.proxyBase}/deploys/${t.deploy_id}`, {
|
|
83
|
+
credentials: "include"
|
|
77
84
|
});
|
|
78
|
-
if (
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
82
|
-
throw new
|
|
85
|
+
if (o.ok) {
|
|
86
|
+
const s = await o.json();
|
|
87
|
+
if (s.state && T.has(s.state)) {
|
|
88
|
+
if (s.state === "ready") return;
|
|
89
|
+
throw new w(s.error_message);
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
|
-
await new Promise((
|
|
92
|
+
await new Promise((s) => setTimeout(s, this.pollIntervalMs));
|
|
86
93
|
}
|
|
87
|
-
throw new
|
|
94
|
+
throw new y();
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
97
|
export {
|
|
91
|
-
|
|
98
|
+
_ as AuthenticatedBuildClient
|
|
92
99
|
};
|
|
@@ -68,6 +68,7 @@ interface UseDropDeployOptions {
|
|
|
68
68
|
buildClient?: BuildClient;
|
|
69
69
|
authenticatedDropClient?: DropClient;
|
|
70
70
|
getUploadToken?: () => Promise<string | null>;
|
|
71
|
+
proxyBase?: string;
|
|
71
72
|
accountSlug?: string;
|
|
72
73
|
buildDeployUrl: (build: DeployedBuild) => string;
|
|
73
74
|
waitForBuild?: boolean;
|
|
@@ -81,7 +82,7 @@ interface UseDropDeployOptions {
|
|
|
81
82
|
* Owns the deploy state machine and its side effects (upload, local persistence,
|
|
82
83
|
* analytics, redirect), keeping DropZone a thin view over the returned `status`.
|
|
83
84
|
*/
|
|
84
|
-
export declare function useDropDeploy({ apiBase, client, claimUrl, siteDashboardUrl, buildSignupUrl, buildClient, authenticatedDropClient, getUploadToken, accountSlug, buildDeployUrl, waitForBuild, onBuildRequired, onDeploy, onBuildDeploy, onError, track, }: UseDropDeployOptions): {
|
|
85
|
+
export declare function useDropDeploy({ apiBase, client, claimUrl, siteDashboardUrl, buildSignupUrl, buildClient, authenticatedDropClient, getUploadToken, proxyBase, accountSlug, buildDeployUrl, waitForBuild, onBuildRequired, onDeploy, onBuildDeploy, onError, track, }: UseDropDeployOptions): {
|
|
85
86
|
status: DeployStatus;
|
|
86
87
|
warning: string | null;
|
|
87
88
|
busy: boolean;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { d as
|
|
2
|
-
import { renameSingleNonIndexHtmlToIndex as
|
|
3
|
-
import { detectBuild as
|
|
4
|
-
import { AnonymousDropClient as
|
|
5
|
-
import { AuthenticatedDropClient as
|
|
6
|
-
import { AuthenticatedBuildClient as
|
|
1
|
+
import { d as H, A as I } from "../../../chunks/hooks.module.DRcxknIz.js";
|
|
2
|
+
import { renameSingleNonIndexHtmlToIndex as Y, getSingleNonIndexHtmlFileName as Z, hasMultipleHtmlFilesWithoutIndex as B } from "./drop-core/readFiles.js";
|
|
3
|
+
import { detectBuild as D } from "./drop-core/detectBuild.js";
|
|
4
|
+
import { AnonymousDropClient as T } from "./drop-core/client.js";
|
|
5
|
+
import { AuthenticatedDropClient as U } from "./drop-core/authedDropClient.js";
|
|
6
|
+
import { AuthenticatedBuildClient as tt } from "./drop-core/buildClient.js";
|
|
7
7
|
import "./drop-core/zip.js";
|
|
8
|
-
import { deployFiles as
|
|
9
|
-
import { NotAuthenticatedError as
|
|
8
|
+
import { deployFiles as W, deployBuild as et } from "./drop-core/deploy.js";
|
|
9
|
+
import { NotAuthenticatedError as $, humanizeDropError as nt } from "./drop-core/errors.js";
|
|
10
10
|
function S(o) {
|
|
11
11
|
switch (o.phase) {
|
|
12
12
|
case "zipping":
|
|
@@ -22,38 +22,39 @@ function S(o) {
|
|
|
22
22
|
return { kind: "processing" };
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
function
|
|
26
|
-
const f =
|
|
27
|
-
return f ? `No index.html found — "${f}" won’t be served at the site root.` :
|
|
25
|
+
function it(o) {
|
|
26
|
+
const f = Z(o);
|
|
27
|
+
return f ? `No index.html found — "${f}" won’t be served at the site root.` : B(o) ? "No index.html found — your site may not have a landing page." : null;
|
|
28
28
|
}
|
|
29
|
-
function
|
|
29
|
+
function mt({
|
|
30
30
|
apiBase: o,
|
|
31
31
|
client: f,
|
|
32
|
-
claimUrl:
|
|
33
|
-
siteDashboardUrl:
|
|
34
|
-
buildSignupUrl:
|
|
35
|
-
buildClient:
|
|
36
|
-
authenticatedDropClient:
|
|
32
|
+
claimUrl: v,
|
|
33
|
+
siteDashboardUrl: j,
|
|
34
|
+
buildSignupUrl: q,
|
|
35
|
+
buildClient: M,
|
|
36
|
+
authenticatedDropClient: E,
|
|
37
37
|
getUploadToken: m,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
proxyBase: x,
|
|
39
|
+
accountSlug: z,
|
|
40
|
+
buildDeployUrl: G,
|
|
41
|
+
waitForBuild: J,
|
|
42
|
+
onBuildRequired: C,
|
|
43
|
+
onDeploy: p,
|
|
43
44
|
onBuildDeploy: h,
|
|
44
45
|
onError: b,
|
|
45
46
|
track: c
|
|
46
47
|
}) {
|
|
47
|
-
const [w, n] =
|
|
48
|
-
|
|
49
|
-
).current,
|
|
50
|
-
|
|
51
|
-
).current,
|
|
52
|
-
async function
|
|
48
|
+
const [w, n] = H({ kind: "idle" }), [K, _] = H(null), L = I(f ?? new T(o)).current, A = I(
|
|
49
|
+
M ?? (m ? new tt({ proxyBase: x, getUploadToken: m, accountSlug: z }) : null)
|
|
50
|
+
).current, F = I(
|
|
51
|
+
E ?? (m ? new U({ proxyBase: x, getUploadToken: m, accountSlug: z }) : null)
|
|
52
|
+
).current, O = w.kind !== "idle" && w.kind !== "error";
|
|
53
|
+
async function Q(y, r) {
|
|
53
54
|
try {
|
|
54
|
-
const { site: d, build: l, buildSettings:
|
|
55
|
+
const { site: d, build: l, buildSettings: g, url: k } = await et(y, r, {
|
|
55
56
|
onProgress: (t) => n(S(t)),
|
|
56
|
-
waitForBuild:
|
|
57
|
+
waitForBuild: J
|
|
57
58
|
});
|
|
58
59
|
return {
|
|
59
60
|
url: k,
|
|
@@ -61,25 +62,25 @@ function ft({
|
|
|
61
62
|
siteId: d.id,
|
|
62
63
|
buildId: l.id,
|
|
63
64
|
deployId: l.deploy_id,
|
|
64
|
-
buildSettings:
|
|
65
|
+
buildSettings: g
|
|
65
66
|
};
|
|
66
67
|
} catch (d) {
|
|
67
|
-
if (d instanceof
|
|
68
|
+
if (d instanceof $) return null;
|
|
68
69
|
throw d;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
async function
|
|
72
|
+
async function R(y, r, d) {
|
|
72
73
|
_(null), n({ kind: "reading" });
|
|
73
|
-
const l = d.length ? { file_types: d } : {},
|
|
74
|
+
const l = d.length ? { file_types: d } : {}, g = (t, i, a) => {
|
|
74
75
|
n({ kind: "error", message: t }), c("dropzone_deploy_failed", { method: r, reason: i, ...l, ...a }), b == null || b({ message: t, reason: i });
|
|
75
76
|
}, k = (t, i) => {
|
|
76
77
|
const a = t instanceof Error ? t.message : String(t);
|
|
77
|
-
|
|
78
|
+
g(nt(t), a, i);
|
|
78
79
|
};
|
|
79
80
|
try {
|
|
80
|
-
const t =
|
|
81
|
+
const t = Y(await y());
|
|
81
82
|
if (!t.length) {
|
|
82
|
-
|
|
83
|
+
g("No files found to deploy.", "No files found to deploy.", { file_count: 0 });
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
r === "drag" && c("dropzone_files_dropped", {
|
|
@@ -87,19 +88,19 @@ function ft({
|
|
|
87
88
|
file_count: t.length,
|
|
88
89
|
...l
|
|
89
90
|
});
|
|
90
|
-
const i =
|
|
91
|
+
const i = D(t);
|
|
91
92
|
if (i.buildRequired) {
|
|
92
93
|
if (c("dropzone_build_required", {
|
|
93
94
|
method: r,
|
|
94
95
|
reason: i.reason,
|
|
95
96
|
file_count: t.length,
|
|
96
97
|
...l
|
|
97
|
-
}),
|
|
98
|
-
n({ kind: "idle" }),
|
|
98
|
+
}), C) {
|
|
99
|
+
n({ kind: "idle" }), C(i);
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
|
-
if (
|
|
102
|
-
const e = await
|
|
102
|
+
if (A) {
|
|
103
|
+
const e = await Q(A, t);
|
|
103
104
|
if (e) {
|
|
104
105
|
n({ kind: "redirecting", target: "build" }), c("dropzone_build_deploy_succeeded", {
|
|
105
106
|
method: r,
|
|
@@ -108,28 +109,28 @@ function ft({
|
|
|
108
109
|
file_count: t.length,
|
|
109
110
|
...e.buildSettings ? { framework: e.buildSettings.framework } : {},
|
|
110
111
|
...l
|
|
111
|
-
}), h == null || h(e), window.location.assign(
|
|
112
|
+
}), h == null || h(e), window.location.assign(G(e));
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
|
-
n({ kind: "redirecting", target: "signup" }), window.location.assign(
|
|
116
|
+
n({ kind: "redirecting", target: "signup" }), window.location.assign(q);
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
|
-
_(
|
|
119
|
-
let a = null, s = !1,
|
|
120
|
-
if (
|
|
119
|
+
_(it(t));
|
|
120
|
+
let a = null, s = !1, P = !1;
|
|
121
|
+
if (F)
|
|
121
122
|
try {
|
|
122
|
-
a = await
|
|
123
|
+
a = await W(F, t, {
|
|
123
124
|
onProgress: (e) => n(S(e))
|
|
124
125
|
}), s = !0;
|
|
125
126
|
} catch (e) {
|
|
126
|
-
if (!(e instanceof
|
|
127
|
-
|
|
127
|
+
if (!(e instanceof $)) throw e;
|
|
128
|
+
P = !0;
|
|
128
129
|
}
|
|
129
|
-
a || (a = await
|
|
130
|
+
a || (a = await W(L, t, {
|
|
130
131
|
onProgress: (e) => n(S(e))
|
|
131
132
|
}));
|
|
132
|
-
const { url:
|
|
133
|
+
const { url: X, token: N, deploy: u } = a;
|
|
133
134
|
if (!s)
|
|
134
135
|
try {
|
|
135
136
|
localStorage.setItem("drop-token", N), localStorage.setItem("dropSiteName", u.subdomain);
|
|
@@ -140,24 +141,24 @@ function ft({
|
|
|
140
141
|
site_name: u.subdomain,
|
|
141
142
|
file_count: t.length,
|
|
142
143
|
authenticated: s,
|
|
143
|
-
...
|
|
144
|
+
...P ? { auth_fallback: !0 } : {},
|
|
144
145
|
...l
|
|
145
|
-
}),
|
|
146
|
-
url:
|
|
146
|
+
}), p == null || p({
|
|
147
|
+
url: X,
|
|
147
148
|
...s ? {} : { token: N },
|
|
148
149
|
siteName: u.subdomain,
|
|
149
150
|
siteId: u.id,
|
|
150
151
|
authenticated: s
|
|
151
|
-
}), s ? window.location.assign(
|
|
152
|
+
}), s ? window.location.assign(j(u.subdomain)) : window.location.assign(`${v(u.subdomain)}#drop_token=${N}`);
|
|
152
153
|
} catch (t) {
|
|
153
154
|
k(t);
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
|
-
function
|
|
157
|
+
function V() {
|
|
157
158
|
_(null), n({ kind: "idle" });
|
|
158
159
|
}
|
|
159
|
-
return { status: w, warning:
|
|
160
|
+
return { status: w, warning: K, busy: O, deploy: R, reset: V };
|
|
160
161
|
}
|
|
161
162
|
export {
|
|
162
|
-
|
|
163
|
+
mt as useDropDeploy
|
|
163
164
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/spark-ui",
|
|
3
3
|
"description": "Assets, design tokens, components, and utilities",
|
|
4
|
-
"version": "1.31.0-alpha.
|
|
4
|
+
"version": "1.31.0-alpha.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/components/preact/index.js",
|
|
7
7
|
"types": "dist/components/preact/index.d.ts",
|
|
@@ -93,6 +93,13 @@ interface Props {
|
|
|
93
93
|
* static path. Distinct from `client`, which is the anonymous fallback.
|
|
94
94
|
*/
|
|
95
95
|
authenticatedDropClient?: DropClient;
|
|
96
|
+
/**
|
|
97
|
+
* API root for the authenticated paths' JSON calls, which go through your
|
|
98
|
+
* access-control rewrite so the session cookie authorizes them. Defaults to
|
|
99
|
+
* `/access-control/bb-api/api/v1`. (Uploads always go direct to
|
|
100
|
+
* `api.netlify.com` — the proxy can't carry bodies that large.)
|
|
101
|
+
*/
|
|
102
|
+
proxyBase?: string;
|
|
96
103
|
/** Create the visitor's new project in this account. Omit for their default account. */
|
|
97
104
|
accountSlug?: string;
|
|
98
105
|
/**
|
|
@@ -180,6 +187,7 @@ export default function DropZone({
|
|
|
180
187
|
getUploadToken,
|
|
181
188
|
buildClient,
|
|
182
189
|
authenticatedDropClient,
|
|
190
|
+
proxyBase,
|
|
183
191
|
accountSlug,
|
|
184
192
|
buildDeployUrl = defaultBuildDeployUrl,
|
|
185
193
|
waitForBuild,
|
|
@@ -206,6 +214,7 @@ export default function DropZone({
|
|
|
206
214
|
getUploadToken,
|
|
207
215
|
buildClient,
|
|
208
216
|
authenticatedDropClient,
|
|
217
|
+
proxyBase,
|
|
209
218
|
accountSlug,
|
|
210
219
|
buildDeployUrl,
|
|
211
220
|
waitForBuild,
|
|
@@ -5,18 +5,26 @@ import { NotAuthenticatedError, SiteCreateError } from './errors';
|
|
|
5
5
|
|
|
6
6
|
export interface AuthenticatedDropClientConfig {
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* access-control
|
|
11
|
-
*
|
|
12
|
-
|
|
8
|
+
* Base for JSON API calls, routed through the consumer's access-control
|
|
9
|
+
* rewrite so the session cookie authenticates them. On netlify.com that
|
|
10
|
+
* rewrite is `/access-control/*` → the app's access-control function, which
|
|
11
|
+
* makes `/access-control/bb-api/api/v1` the API root (the default here).
|
|
12
|
+
*/
|
|
13
|
+
proxyBase?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Direct API base for the file uploads. The proxy above runs in a Lambda with
|
|
16
|
+
* a ~6MB request body limit, so file PUTs go straight to `api.netlify.com`
|
|
17
|
+
* (whose `/api/*` CORS policy allows any origin).
|
|
13
18
|
*/
|
|
14
19
|
apiBase?: string;
|
|
15
20
|
/**
|
|
16
|
-
* Mint a
|
|
17
|
-
*
|
|
21
|
+
* Mint a bearer token for those direct uploads. On netlify.com this is
|
|
22
|
+
* `GET /access-control/generate-access-control-token`, whose
|
|
18
23
|
* `accessControlToken` the API decrypts back into the user's access token.
|
|
19
24
|
* Resolving `null` means "no session" and surfaces as `NotAuthenticatedError`.
|
|
25
|
+
*
|
|
26
|
+
* Called repeatedly, not once: the token issued to a non-app origin lives
|
|
27
|
+
* only 30 seconds, which a multi-file upload routinely outlasts.
|
|
20
28
|
*/
|
|
21
29
|
getUploadToken: () => Promise<string | null>;
|
|
22
30
|
/** Create the site in this account. Omit to use the user's default account. */
|
|
@@ -31,10 +39,18 @@ export interface AuthenticatedDropClientConfig {
|
|
|
31
39
|
// (constants + site create) are deliberately duplicated rather than extracted,
|
|
32
40
|
// to avoid churn on the in-review build-client branch. Worth unifying once both
|
|
33
41
|
// have landed.
|
|
42
|
+
const PROXY_BASE = '/access-control/bb-api/api/v1';
|
|
34
43
|
const API_BASE = 'https://api.netlify.com/api/v1';
|
|
35
44
|
const POLL_INTERVAL_MS = 1000;
|
|
36
45
|
const TIMEOUT_MS = 10 * 60 * 1000;
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* How long a minted upload token is reused before minting another. The server
|
|
49
|
+
* issues non-app origins a 30s token, so this leaves a margin for the request
|
|
50
|
+
* it authorizes to finish.
|
|
51
|
+
*/
|
|
52
|
+
const TOKEN_REUSE_MS = 20 * 1000;
|
|
53
|
+
|
|
38
54
|
/** Deploy states that end the wait — `ready` succeeds, the rest are failures. */
|
|
39
55
|
const TERMINAL_STATES = new Set(['ready', 'error', 'rejected']);
|
|
40
56
|
|
|
@@ -59,34 +75,37 @@ interface SiteDeployResponse {
|
|
|
59
75
|
* There is no claim step, so the caller should hand off to the site's dashboard
|
|
60
76
|
* page rather than the claim page.
|
|
61
77
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* reaching the API. Revisit if that allowlist gains these paths.
|
|
78
|
+
* Requests split across two hosts by necessity. JSON calls (create the site,
|
|
79
|
+
* create the deploy, poll it) go through the consumer's access-control proxy on
|
|
80
|
+
* the session cookie — no token involved, and nothing that can expire mid-deploy.
|
|
81
|
+
* File uploads can't: the proxy caps bodies at ~6MB, so they go direct to
|
|
82
|
+
* `api.netlify.com` with a bearer, re-minted as it ages (see `TOKEN_REUSE_MS`).
|
|
68
83
|
*
|
|
69
|
-
* Error-mapping invariant: only pre-mutation failures — a null upload token
|
|
70
|
-
* a 401/403 creating the site — throw `NotAuthenticatedError`,
|
|
71
|
-
* signal the drop flow may safely retry anonymously. Once the site
|
|
72
|
-
* auth failure throws a plain
|
|
73
|
-
*
|
|
84
|
+
* Error-mapping invariant: only pre-mutation failures — a null upload token at
|
|
85
|
+
* the gate, or a 401/403 creating the site — throw `NotAuthenticatedError`,
|
|
86
|
+
* which is the signal the drop flow may safely retry anonymously. Once the site
|
|
87
|
+
* exists, any auth failure throws a plain error instead: falling back at that
|
|
88
|
+
* point would strand an empty site in the account *and* deploy a duplicate
|
|
74
89
|
* anonymously.
|
|
75
90
|
*/
|
|
76
91
|
export class AuthenticatedDropClient implements DropClient {
|
|
92
|
+
private proxyBase: string;
|
|
77
93
|
private apiBase: string;
|
|
78
94
|
private getUploadToken: () => Promise<string | null>;
|
|
79
95
|
private accountSlug?: string;
|
|
80
96
|
private pollIntervalMs: number;
|
|
81
97
|
private timeoutMs: number;
|
|
98
|
+
private cachedToken?: { value: string; mintedAt: number };
|
|
82
99
|
|
|
83
100
|
constructor({
|
|
101
|
+
proxyBase = PROXY_BASE,
|
|
84
102
|
apiBase = API_BASE,
|
|
85
103
|
getUploadToken,
|
|
86
104
|
accountSlug,
|
|
87
105
|
pollIntervalMs = POLL_INTERVAL_MS,
|
|
88
106
|
timeoutMs = TIMEOUT_MS,
|
|
89
107
|
}: AuthenticatedDropClientConfig) {
|
|
108
|
+
this.proxyBase = proxyBase;
|
|
90
109
|
this.apiBase = apiBase;
|
|
91
110
|
this.getUploadToken = getUploadToken;
|
|
92
111
|
this.accountSlug = accountSlug;
|
|
@@ -95,26 +114,50 @@ export class AuthenticatedDropClient implements DropClient {
|
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
/**
|
|
98
|
-
* The
|
|
99
|
-
*
|
|
100
|
-
*
|
|
117
|
+
* The auth gate. Minting here — before anything is created — is what lets a
|
|
118
|
+
* logged-out visitor fail cleanly and fall back to the anonymous flow. The
|
|
119
|
+
* returned token is threaded through `deployFiles`, but only the uploads
|
|
120
|
+
* actually need one, and they re-mint rather than trusting this to still be
|
|
121
|
+
* valid by the time they run.
|
|
101
122
|
*/
|
|
102
123
|
async getToken(): Promise<string> {
|
|
103
124
|
const token = await this.getUploadToken();
|
|
104
125
|
if (!token) throw new NotAuthenticatedError();
|
|
126
|
+
this.cachedToken = { value: token, mintedAt: Date.now() };
|
|
105
127
|
return token;
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
131
|
+
* A token young enough to authorize an upload, minting a new one once the
|
|
132
|
+
* cached one nears its 30s expiry.
|
|
133
|
+
*
|
|
134
|
+
* Deliberately NOT `NotAuthenticatedError` when minting fails: by the time
|
|
135
|
+
* uploads run the site already exists, and that error would send the drop
|
|
136
|
+
* back through the anonymous path — leaving an empty project behind and
|
|
137
|
+
* deploying a duplicate. See the class doc.
|
|
138
|
+
*/
|
|
139
|
+
private async freshToken(): Promise<string> {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
if (this.cachedToken && now - this.cachedToken.mintedAt < TOKEN_REUSE_MS) {
|
|
142
|
+
return this.cachedToken.value;
|
|
143
|
+
}
|
|
144
|
+
const value = await this.getUploadToken();
|
|
145
|
+
if (!value) throw new Error('could not renew the upload token');
|
|
146
|
+
this.cachedToken = { value, mintedAt: now };
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Two proxied calls: create the site in the visitor's account, then the
|
|
152
|
+
* deploy on it. Mapped into the anonymous `DropResponse` shape so
|
|
153
|
+
* `deployFiles` runs unchanged over either client.
|
|
112
154
|
*/
|
|
113
|
-
async createDeploy(files: Digest,
|
|
155
|
+
async createDeploy(files: Digest, _token: string): Promise<DropResponse> {
|
|
114
156
|
const path = this.accountSlug ? `/${this.accountSlug}/sites` : '/sites';
|
|
115
|
-
const siteRes = await fetch(`${this.
|
|
157
|
+
const siteRes = await fetch(`${this.proxyBase}${path}`, {
|
|
116
158
|
method: 'POST',
|
|
117
|
-
|
|
159
|
+
credentials: 'include',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
161
|
body: JSON.stringify({ created_via: 'drop' }),
|
|
119
162
|
});
|
|
120
163
|
// A logged-out visitor 401s here, before anything is created — the one
|
|
@@ -123,9 +166,10 @@ export class AuthenticatedDropClient implements DropClient {
|
|
|
123
166
|
if (!siteRes.ok) throw new SiteCreateError(siteRes.status);
|
|
124
167
|
const site: BuildSite = await siteRes.json();
|
|
125
168
|
|
|
126
|
-
const deployRes = await fetch(`${this.
|
|
169
|
+
const deployRes = await fetch(`${this.proxyBase}/sites/${site.id}/deploys`, {
|
|
127
170
|
method: 'POST',
|
|
128
|
-
|
|
171
|
+
credentials: 'include',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
173
|
body: JSON.stringify({ files, deploy_source: 'drop' }),
|
|
130
174
|
});
|
|
131
175
|
if (!deployRes.ok) throw apiError('deploy create', deployRes.status);
|
|
@@ -139,13 +183,18 @@ export class AuthenticatedDropClient implements DropClient {
|
|
|
139
183
|
};
|
|
140
184
|
}
|
|
141
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Direct to `api.netlify.com` on a freshly-aged bearer — the threaded `token`
|
|
188
|
+
* is ignored because an upload queue easily outlives the 30 seconds it was
|
|
189
|
+
* minted with. PUT (not POST) so paths with "&" etc. work.
|
|
190
|
+
*/
|
|
142
191
|
async uploadFile(
|
|
143
192
|
deployId: string,
|
|
144
193
|
path: string,
|
|
145
194
|
content: ArrayBuffer,
|
|
146
|
-
|
|
195
|
+
_token: string
|
|
147
196
|
): Promise<void> {
|
|
148
|
-
|
|
197
|
+
const token = await this.freshToken();
|
|
149
198
|
const res = await fetch(`${this.apiBase}/deploys/${deployId}/files${path}`, {
|
|
150
199
|
method: 'PUT',
|
|
151
200
|
headers: { 'Content-Type': 'application/octet-stream', Authorization: `Bearer ${token}` },
|
|
@@ -155,18 +204,15 @@ export class AuthenticatedDropClient implements DropClient {
|
|
|
155
204
|
}
|
|
156
205
|
|
|
157
206
|
/**
|
|
158
|
-
* Poll
|
|
159
|
-
* `error`/`rejected`, throw on timeout.
|
|
160
|
-
*
|
|
161
|
-
* 401s here — those are ignored like any other non-ok read and the loop runs
|
|
162
|
-
* to its timeout rather than failing fast. Acceptable while static deploys
|
|
163
|
-
* finish in seconds; re-mint per poll if that stops being true.
|
|
207
|
+
* Poll the deploy through the proxy until it settles: resolve on `ready`,
|
|
208
|
+
* throw on `error`/`rejected`, throw on timeout. On the session cookie rather
|
|
209
|
+
* than a bearer, so a deploy taking minutes can't outlive its own credentials.
|
|
164
210
|
*/
|
|
165
|
-
async waitUntilReady(deploy: DropResponse,
|
|
211
|
+
async waitUntilReady(deploy: DropResponse, _token: string): Promise<void> {
|
|
166
212
|
const deadline = this.timeoutMs / this.pollIntervalMs;
|
|
167
213
|
for (let i = 0; i < deadline; i++) {
|
|
168
|
-
const res = await fetch(`${this.
|
|
169
|
-
|
|
214
|
+
const res = await fetch(`${this.proxyBase}/deploys/${deploy.deploy_id}`, {
|
|
215
|
+
credentials: 'include',
|
|
170
216
|
});
|
|
171
217
|
if (res.ok) {
|
|
172
218
|
const state: DeployState = await res.json();
|