@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.
@@ -47,6 +47,13 @@ interface Props {
47
47
  * static path. Distinct from `client`, which is the anonymous fallback.
48
48
  */
49
49
  authenticatedDropClient?: DropClient;
50
+ /**
51
+ * API root for the authenticated paths' JSON calls, which go through your
52
+ * access-control rewrite so the session cookie authorizes them. Defaults to
53
+ * `/access-control/bb-api/api/v1`. (Uploads always go direct to
54
+ * `api.netlify.com` — the proxy can't carry bodies that large.)
55
+ */
56
+ proxyBase?: string;
50
57
  /** Create the visitor's new project in this account. Omit for their default account. */
51
58
  accountSlug?: string;
52
59
  /**
@@ -123,5 +130,5 @@ interface Props {
123
130
  analyticsId?: string;
124
131
  className?: string;
125
132
  }
126
- export default function DropZone({ children, apiBase, client, claimUrl, siteDashboardUrl, buildSignupUrl, getUploadToken, buildClient, authenticatedDropClient, accountSlug, buildDeployUrl, waitForBuild, onBuildRequired, onDeploy, onBuildDeploy, onError, showStatus, onTrack, analyticsId, className, }: Props): import("preact").JSX.Element;
133
+ export default function DropZone({ children, apiBase, client, claimUrl, siteDashboardUrl, buildSignupUrl, getUploadToken, buildClient, authenticatedDropClient, proxyBase, accountSlug, buildDeployUrl, waitForBuild, onBuildRequired, onDeploy, onBuildDeploy, onError, showStatus, onTrack, analyticsId, className, }: Props): import("preact").JSX.Element;
127
134
  export {};
@@ -1,35 +1,36 @@
1
1
  import { u as e } from "../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
2
- import { A as j, d as V, y as G } from "../../../chunks/hooks.module.DRcxknIz.js";
3
- import { readFileList as K, readDataTransfer as L } from "./drop-core/readFiles.js";
2
+ import { A as j, d as G, y as K } from "../../../chunks/hooks.module.DRcxknIz.js";
3
+ import { readFileList as L, readDataTransfer as Z } from "./drop-core/readFiles.js";
4
4
  import { getFileTypes as D } from "./drop-core/fileTypes.js";
5
5
  import "./drop-core/detectBuild.js";
6
6
  import "./drop-core/zip.js";
7
- import { useDropDeploy as Z } from "./useDropDeploy.js";
7
+ import { useDropDeploy as q } from "./useDropDeploy.js";
8
8
  import { Fragment as w } from "preact";
9
- import '../../../assets/DropZone.css';const q = 120, H = (r) => `https://app.netlify.com/drop/${r}`, J = (r) => `https://app.netlify.com/projects/${r}`, Q = "https://app.netlify.com/signup?next=/drop&utm_campaign=drop", W = ({ siteName: r }) => `https://app.netlify.com/projects/${r}`;
10
- function se({
9
+ import '../../../assets/DropZone.css';const H = 120, J = (r) => `https://app.netlify.com/drop/${r}`, Q = (r) => `https://app.netlify.com/projects/${r}`, W = "https://app.netlify.com/signup?next=/drop&utm_campaign=drop", X = ({ siteName: r }) => `https://app.netlify.com/projects/${r}`;
10
+ function ce({
11
11
  children: r,
12
12
  apiBase: a,
13
13
  client: l,
14
- claimUrl: c = H,
15
- siteDashboardUrl: u = J,
16
- buildSignupUrl: A = Q,
14
+ claimUrl: c = J,
15
+ siteDashboardUrl: u = Q,
16
+ buildSignupUrl: A = W,
17
17
  getUploadToken: T,
18
18
  buildClient: C,
19
19
  authenticatedDropClient: S,
20
- accountSlug: _,
21
- buildDeployUrl: $ = W,
22
- waitForBuild: B,
23
- onBuildRequired: U,
24
- onDeploy: P,
25
- onBuildDeploy: R,
26
- onError: x,
20
+ proxyBase: _,
21
+ accountSlug: $,
22
+ buildDeployUrl: B = X,
23
+ waitForBuild: U,
24
+ onBuildRequired: P,
25
+ onDeploy: R,
26
+ onBuildDeploy: x,
27
+ onError: E,
27
28
  showStatus: f = !0,
28
29
  onTrack: d,
29
30
  analyticsId: g,
30
- className: E
31
+ className: F
31
32
  }) {
32
- const y = (t, n = {}) => d == null ? void 0 : d(t, g ? { dropzone_id: g, ...n } : n), { status: s, warning: b, busy: i, deploy: m, reset: F } = Z({
33
+ const y = (t, n = {}) => d == null ? void 0 : d(t, g ? { dropzone_id: g, ...n } : n), { status: s, warning: b, busy: i, deploy: m, reset: I } = q({
33
34
  apiBase: a,
34
35
  client: l,
35
36
  claimUrl: c,
@@ -38,40 +39,41 @@ function se({
38
39
  getUploadToken: T,
39
40
  buildClient: C,
40
41
  authenticatedDropClient: S,
41
- accountSlug: _,
42
- buildDeployUrl: $,
43
- waitForBuild: B,
44
- onBuildRequired: U,
45
- onDeploy: P,
46
- onBuildDeploy: R,
47
- onError: x,
42
+ proxyBase: _,
43
+ accountSlug: $,
44
+ buildDeployUrl: B,
45
+ waitForBuild: U,
46
+ onBuildRequired: P,
47
+ onDeploy: R,
48
+ onBuildDeploy: x,
49
+ onError: E,
48
50
  track: y
49
- }), { active: p, keepActive: I, endDrag: M } = X(), h = j(null), v = (t) => {
51
+ }), { active: p, keepActive: M, endDrag: N } = Y(), h = j(null), v = (t) => {
50
52
  var n;
51
53
  y("dropzone_browse_opened", { method: t }), (n = h.current) == null || n.click();
52
- }, N = f && (p || i || s.kind === "error"), O = [
54
+ }, O = f && (p || i || s.kind === "error"), V = [
53
55
  "n-dropzone",
54
56
  p && "is-active",
55
57
  i && "is-busy",
56
58
  !f && "is-bare",
57
- E
59
+ F
58
60
  ].filter(Boolean).join(" ");
59
61
  return /* @__PURE__ */ e(
60
62
  "div",
61
63
  {
62
- class: O,
64
+ class: V,
63
65
  "data-state": s.kind,
64
66
  role: "button",
65
67
  tabIndex: i ? -1 : 0,
66
68
  "aria-label": "Drop a site folder or zip here to deploy it, or press Enter to browse for files",
67
69
  "aria-busy": i,
68
70
  onDragOver: (t) => {
69
- i || (t.preventDefault(), I());
71
+ i || (t.preventDefault(), M());
70
72
  },
71
73
  onDrop: (t) => {
72
- if (t.preventDefault(), M(), i) return;
74
+ if (t.preventDefault(), N(), i) return;
73
75
  const n = t.dataTransfer;
74
- n && m(() => L(n), "drag", D(n.files));
76
+ n && m(() => Z(n), "drag", D(n.files));
75
77
  },
76
78
  onClick: (t) => {
77
79
  !i && t.target === t.currentTarget && v("click");
@@ -95,18 +97,18 @@ function se({
95
97
  const n = t.currentTarget;
96
98
  if ((k = n.files) != null && k.length) {
97
99
  const z = n.files;
98
- m(() => K(z), "browse", D(z));
100
+ m(() => L(z), "browse", D(z));
99
101
  }
100
102
  n.value = "";
101
103
  }
102
104
  }
103
105
  ),
104
- N && /* @__PURE__ */ e(
106
+ O && /* @__PURE__ */ e(
105
107
  "div",
106
108
  {
107
109
  class: "n-dropzone-overlay n-fadeIn",
108
110
  children: [
109
- /* @__PURE__ */ e(Y, { status: s, active: p, onReset: F }),
111
+ /* @__PURE__ */ e(ee, { status: s, active: p, onReset: I }),
110
112
  b && s.kind !== "error" && i && /* @__PURE__ */ e("p", { class: "n-dropzone-warning", role: "status", children: b })
111
113
  ]
112
114
  },
@@ -116,15 +118,15 @@ function se({
116
118
  }
117
119
  );
118
120
  }
119
- function X() {
120
- const [r, a] = V(!1), l = j();
121
- return G(() => () => clearTimeout(l.current), []), { active: r, keepActive: () => {
122
- a(!0), clearTimeout(l.current), l.current = setTimeout(() => a(!1), q);
121
+ function Y() {
122
+ const [r, a] = G(!1), l = j();
123
+ return K(() => () => clearTimeout(l.current), []), { active: r, keepActive: () => {
124
+ a(!0), clearTimeout(l.current), l.current = setTimeout(() => a(!1), H);
123
125
  }, endDrag: () => {
124
126
  clearTimeout(l.current), a(!1);
125
127
  } };
126
128
  }
127
- function Y({
129
+ function ee({
128
130
  status: r,
129
131
  active: a,
130
132
  onReset: l
@@ -180,5 +182,5 @@ function o({ label: r }) {
180
182
  ] });
181
183
  }
182
184
  export {
183
- se as default
185
+ ce as default
184
186
  };
@@ -2,18 +2,26 @@ import { DropClient } from './client';
2
2
  import { Digest, DropResponse } from './types';
3
3
  export interface AuthenticatedDropClientConfig {
4
4
  /**
5
- * API base for every call. Requests go straight to `api.netlify.com`, whose
6
- * `/api/*` CORS policy allows any origin, rather than through a consumer's
7
- * access-control rewrite — that proxy allowlists paths per origin, and site
8
- * and deploy creation are not on the list for netlify.com (they 400 with
9
- * `x-debug: request origin not allowed to call endpoint`).
5
+ * Base for JSON API calls, routed through the consumer's access-control
6
+ * rewrite so the session cookie authenticates them. On netlify.com that
7
+ * rewrite is `/access-control/*` → the app's access-control function, which
8
+ * makes `/access-control/bb-api/api/v1` the API root (the default here).
9
+ */
10
+ proxyBase?: string;
11
+ /**
12
+ * Direct API base for the file uploads. The proxy above runs in a Lambda with
13
+ * a ~6MB request body limit, so file PUTs go straight to `api.netlify.com`
14
+ * (whose `/api/*` CORS policy allows any origin).
10
15
  */
11
16
  apiBase?: string;
12
17
  /**
13
- * Mint a short-lived bearer token authorizing the calls. On netlify.com this
14
- * is `GET /access-control/generate-access-control-token`, whose
18
+ * Mint a bearer token for those direct uploads. On netlify.com this is
19
+ * `GET /access-control/generate-access-control-token`, whose
15
20
  * `accessControlToken` the API decrypts back into the user's access token.
16
21
  * Resolving `null` means "no session" and surfaces as `NotAuthenticatedError`.
22
+ *
23
+ * Called repeatedly, not once: the token issued to a non-app origin lives
24
+ * only 30 seconds, which a multi-file upload routinely outlasts.
17
25
  */
18
26
  getUploadToken: () => Promise<string | null>;
19
27
  /** Create the site in this account. Omit to use the user's default account. */
@@ -31,47 +39,62 @@ export interface AuthenticatedDropClientConfig {
31
39
  * There is no claim step, so the caller should hand off to the site's dashboard
32
40
  * page rather than the claim page.
33
41
  *
34
- * Every request goes direct to `api.netlify.com` with the short-lived bearer
35
- * from `getUploadToken()`. Routing the JSON calls through a consumer's
36
- * access-control rewrite instead (cookie session, no token in the browser)
37
- * would be the tighter design, but that proxy allowlists paths per origin and
38
- * site/deploy creation isn't allowed from netlify.com — it 400s before
39
- * reaching the API. Revisit if that allowlist gains these paths.
42
+ * Requests split across two hosts by necessity. JSON calls (create the site,
43
+ * create the deploy, poll it) go through the consumer's access-control proxy on
44
+ * the session cookie — no token involved, and nothing that can expire mid-deploy.
45
+ * File uploads can't: the proxy caps bodies at ~6MB, so they go direct to
46
+ * `api.netlify.com` with a bearer, re-minted as it ages (see `TOKEN_REUSE_MS`).
40
47
  *
41
- * Error-mapping invariant: only pre-mutation failures — a null upload token, or
42
- * a 401/403 creating the site — throw `NotAuthenticatedError`, which is the
43
- * signal the drop flow may safely retry anonymously. Once the site exists, an
44
- * auth failure throws a plain status-carrying error instead: falling back at
45
- * that point would strand an empty site in the account *and* deploy a duplicate
48
+ * Error-mapping invariant: only pre-mutation failures — a null upload token at
49
+ * the gate, or a 401/403 creating the site — throw `NotAuthenticatedError`,
50
+ * which is the signal the drop flow may safely retry anonymously. Once the site
51
+ * exists, any auth failure throws a plain error instead: falling back at that
52
+ * point would strand an empty site in the account *and* deploy a duplicate
46
53
  * anonymously.
47
54
  */
48
55
  export declare class AuthenticatedDropClient implements DropClient {
56
+ private proxyBase;
49
57
  private apiBase;
50
58
  private getUploadToken;
51
59
  private accountSlug?;
52
60
  private pollIntervalMs;
53
61
  private timeoutMs;
54
- constructor({ apiBase, getUploadToken, accountSlug, pollIntervalMs, timeoutMs, }: AuthenticatedDropClientConfig);
62
+ private cachedToken?;
63
+ constructor({ proxyBase, apiBase, getUploadToken, accountSlug, pollIntervalMs, timeoutMs, }: AuthenticatedDropClientConfig);
55
64
  /**
56
- * The token here is the upload bearer, not a drop token — it authorizes every
57
- * call this client makes. Minted once per deploy, before anything is created,
58
- * so a missing session fails the whole attempt cleanly.
65
+ * The auth gate. Minting here — before anything is created — is what lets a
66
+ * logged-out visitor fail cleanly and fall back to the anonymous flow. The
67
+ * returned token is threaded through `deployFiles`, but only the uploads
68
+ * actually need one, and they re-mint rather than trusting this to still be
69
+ * valid by the time they run.
59
70
  */
60
71
  getToken(): Promise<string>;
61
72
  /**
62
- * Two calls: create the site in the visitor's account, then the deploy on it.
63
- * Mapped into the anonymous `DropResponse` shape so `deployFiles` runs
64
- * unchanged over either client.
73
+ * A token young enough to authorize an upload, minting a new one once the
74
+ * cached one nears its 30s expiry.
75
+ *
76
+ * Deliberately NOT `NotAuthenticatedError` when minting fails: by the time
77
+ * uploads run the site already exists, and that error would send the drop
78
+ * back through the anonymous path — leaving an empty project behind and
79
+ * deploying a duplicate. See the class doc.
80
+ */
81
+ private freshToken;
82
+ /**
83
+ * Two proxied calls: create the site in the visitor's account, then the
84
+ * deploy on it. Mapped into the anonymous `DropResponse` shape so
85
+ * `deployFiles` runs unchanged over either client.
86
+ */
87
+ createDeploy(files: Digest, _token: string): Promise<DropResponse>;
88
+ /**
89
+ * Direct to `api.netlify.com` on a freshly-aged bearer — the threaded `token`
90
+ * is ignored because an upload queue easily outlives the 30 seconds it was
91
+ * minted with. PUT (not POST) so paths with "&" etc. work.
65
92
  */
66
- createDeploy(files: Digest, token: string): Promise<DropResponse>;
67
- uploadFile(deployId: string, path: string, content: ArrayBuffer, token: string): Promise<void>;
93
+ uploadFile(deployId: string, path: string, content: ArrayBuffer, _token: string): Promise<void>;
68
94
  /**
69
- * Poll until the deploy settles: resolve on `ready`, throw on
70
- * `error`/`rejected`, throw on timeout. Reuses the bearer minted at the start
71
- * of the deploy, so a deploy that outlives the token's TTL will start seeing
72
- * 401s here — those are ignored like any other non-ok read and the loop runs
73
- * to its timeout rather than failing fast. Acceptable while static deploys
74
- * finish in seconds; re-mint per poll if that stops being true.
95
+ * Poll the deploy through the proxy until it settles: resolve on `ready`,
96
+ * throw on `error`/`rejected`, throw on timeout. On the session cookie rather
97
+ * than a bearer, so a deploy taking minutes can't outlive its own credentials.
75
98
  */
76
- waitUntilReady(deploy: DropResponse, token: string): Promise<void>;
99
+ waitUntilReady(deploy: DropResponse, _token: string): Promise<void>;
77
100
  }
@@ -1,93 +1,119 @@
1
- var c = Object.defineProperty;
2
- var h = (r, t, e) => t in r ? c(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
3
- var n = (r, t, e) => h(r, typeof t != "symbol" ? t + "" : t, e);
4
- import { apiError as l } from "./client.js";
5
- import { NotAuthenticatedError as p, SiteCreateError as u } from "./errors.js";
6
- const y = "https://api.netlify.com/api/v1", w = 1e3, f = 10 * 60 * 1e3, m = /* @__PURE__ */ new Set(["ready", "error", "rejected"]);
1
+ var h = Object.defineProperty;
2
+ var p = (r, e, o) => e in r ? h(r, e, { enumerable: !0, configurable: !0, writable: !0, value: o }) : r[e] = o;
3
+ var i = (r, e, o) => p(r, typeof e != "symbol" ? e + "" : e, o);
4
+ import { apiError as d } from "./client.js";
5
+ import { NotAuthenticatedError as l, SiteCreateError as u } from "./errors.js";
6
+ const y = "/access-control/bb-api/api/v1", w = "https://api.netlify.com/api/v1", T = 1e3, f = 10 * 60 * 1e3, k = 20 * 1e3, m = /* @__PURE__ */ new Set(["ready", "error", "rejected"]);
7
7
  class g {
8
8
  constructor({
9
- apiBase: t = y,
10
- getUploadToken: e,
11
- accountSlug: i,
12
- pollIntervalMs: o = w,
13
- timeoutMs: s = f
9
+ proxyBase: e = y,
10
+ apiBase: o = w,
11
+ getUploadToken: n,
12
+ accountSlug: s,
13
+ pollIntervalMs: a = T,
14
+ timeoutMs: t = f
14
15
  }) {
15
- n(this, "apiBase");
16
- n(this, "getUploadToken");
17
- n(this, "accountSlug");
18
- n(this, "pollIntervalMs");
19
- n(this, "timeoutMs");
20
- this.apiBase = t, this.getUploadToken = e, this.accountSlug = i, this.pollIntervalMs = o, this.timeoutMs = s;
16
+ i(this, "proxyBase");
17
+ i(this, "apiBase");
18
+ i(this, "getUploadToken");
19
+ i(this, "accountSlug");
20
+ i(this, "pollIntervalMs");
21
+ i(this, "timeoutMs");
22
+ i(this, "cachedToken");
23
+ this.proxyBase = e, this.apiBase = o, this.getUploadToken = n, this.accountSlug = s, this.pollIntervalMs = a, this.timeoutMs = t;
21
24
  }
22
25
  /**
23
- * The token here is the upload bearer, not a drop token — it authorizes every
24
- * call this client makes. Minted once per deploy, before anything is created,
25
- * so a missing session fails the whole attempt cleanly.
26
+ * The auth gate. Minting here — before anything is created — is what lets a
27
+ * logged-out visitor fail cleanly and fall back to the anonymous flow. The
28
+ * returned token is threaded through `deployFiles`, but only the uploads
29
+ * actually need one, and they re-mint rather than trusting this to still be
30
+ * valid by the time they run.
26
31
  */
27
32
  async getToken() {
28
- const t = await this.getUploadToken();
29
- if (!t) throw new p();
30
- return t;
33
+ const e = await this.getUploadToken();
34
+ if (!e) throw new l();
35
+ return this.cachedToken = { value: e, mintedAt: Date.now() }, e;
31
36
  }
32
37
  /**
33
- * Two calls: create the site in the visitor's account, then the deploy on it.
34
- * Mapped into the anonymous `DropResponse` shape so `deployFiles` runs
35
- * unchanged over either client.
38
+ * A token young enough to authorize an upload, minting a new one once the
39
+ * cached one nears its 30s expiry.
40
+ *
41
+ * Deliberately NOT `NotAuthenticatedError` when minting fails: by the time
42
+ * uploads run the site already exists, and that error would send the drop
43
+ * back through the anonymous path — leaving an empty project behind and
44
+ * deploying a duplicate. See the class doc.
36
45
  */
37
- async createDeploy(t, e) {
38
- const i = this.accountSlug ? `/${this.accountSlug}/sites` : "/sites", o = await fetch(`${this.apiBase}${i}`, {
46
+ async freshToken() {
47
+ const e = Date.now();
48
+ if (this.cachedToken && e - this.cachedToken.mintedAt < k)
49
+ return this.cachedToken.value;
50
+ const o = await this.getUploadToken();
51
+ if (!o) throw new Error("could not renew the upload token");
52
+ return this.cachedToken = { value: o, mintedAt: e }, o;
53
+ }
54
+ /**
55
+ * Two proxied calls: create the site in the visitor's account, then the
56
+ * deploy on it. Mapped into the anonymous `DropResponse` shape so
57
+ * `deployFiles` runs unchanged over either client.
58
+ */
59
+ async createDeploy(e, o) {
60
+ const n = this.accountSlug ? `/${this.accountSlug}/sites` : "/sites", s = await fetch(`${this.proxyBase}${n}`, {
39
61
  method: "POST",
40
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${e}` },
62
+ credentials: "include",
63
+ headers: { "Content-Type": "application/json" },
41
64
  body: JSON.stringify({ created_via: "drop" })
42
65
  });
43
- if (o.status === 401 || o.status === 403) throw new p();
44
- if (!o.ok) throw new u(o.status);
45
- const s = await o.json(), a = await fetch(`${this.apiBase}/sites/${s.id}/deploys`, {
66
+ if (s.status === 401 || s.status === 403) throw new l();
67
+ if (!s.ok) throw new u(s.status);
68
+ const a = await s.json(), t = await fetch(`${this.proxyBase}/sites/${a.id}/deploys`, {
46
69
  method: "POST",
47
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${e}` },
48
- body: JSON.stringify({ files: t, deploy_source: "drop" })
70
+ credentials: "include",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({ files: e, deploy_source: "drop" })
49
73
  });
50
- if (!a.ok) throw l("deploy create", a.status);
51
- const d = await a.json();
74
+ if (!t.ok) throw d("deploy create", t.status);
75
+ const c = await t.json();
52
76
  return {
53
- id: s.id,
54
- deploy_id: d.id,
55
- subdomain: s.name,
56
- required: d.required ?? []
77
+ id: a.id,
78
+ deploy_id: c.id,
79
+ subdomain: a.name,
80
+ required: c.required ?? []
57
81
  };
58
82
  }
59
- async uploadFile(t, e, i, o) {
60
- const s = await fetch(`${this.apiBase}/deploys/${t}/files${e}`, {
83
+ /**
84
+ * Direct to `api.netlify.com` on a freshly-aged bearer — the threaded `token`
85
+ * is ignored because an upload queue easily outlives the 30 seconds it was
86
+ * minted with. PUT (not POST) so paths with "&" etc. work.
87
+ */
88
+ async uploadFile(e, o, n, s) {
89
+ const a = await this.freshToken(), t = await fetch(`${this.apiBase}/deploys/${e}/files${o}`, {
61
90
  method: "PUT",
62
- headers: { "Content-Type": "application/octet-stream", Authorization: `Bearer ${o}` },
63
- body: i
91
+ headers: { "Content-Type": "application/octet-stream", Authorization: `Bearer ${a}` },
92
+ body: n
64
93
  });
65
- if (!s.ok) throw l(`upload ${e}`, s.status);
94
+ if (!t.ok) throw d(`upload ${o}`, t.status);
66
95
  }
67
96
  /**
68
- * Poll until the deploy settles: resolve on `ready`, throw on
69
- * `error`/`rejected`, throw on timeout. Reuses the bearer minted at the start
70
- * of the deploy, so a deploy that outlives the token's TTL will start seeing
71
- * 401s here — those are ignored like any other non-ok read and the loop runs
72
- * to its timeout rather than failing fast. Acceptable while static deploys
73
- * finish in seconds; re-mint per poll if that stops being true.
97
+ * Poll the deploy through the proxy until it settles: resolve on `ready`,
98
+ * throw on `error`/`rejected`, throw on timeout. On the session cookie rather
99
+ * than a bearer, so a deploy taking minutes can't outlive its own credentials.
74
100
  */
75
- async waitUntilReady(t, e) {
76
- const i = this.timeoutMs / this.pollIntervalMs;
77
- for (let o = 0; o < i; o++) {
78
- const s = await fetch(`${this.apiBase}/deploys/${t.deploy_id}`, {
79
- headers: { Authorization: `Bearer ${e}` }
101
+ async waitUntilReady(e, o) {
102
+ const n = this.timeoutMs / this.pollIntervalMs;
103
+ for (let s = 0; s < n; s++) {
104
+ const a = await fetch(`${this.proxyBase}/deploys/${e.deploy_id}`, {
105
+ credentials: "include"
80
106
  });
81
- if (s.ok) {
82
- const a = await s.json();
83
- if (a.state && m.has(a.state)) {
84
- if (a.state === "ready") return;
107
+ if (a.ok) {
108
+ const t = await a.json();
109
+ if (t.state && m.has(t.state)) {
110
+ if (t.state === "ready") return;
85
111
  throw new Error(
86
- a.error_message ? `deploy failed: ${a.error_message}` : "deploy failed"
112
+ t.error_message ? `deploy failed: ${t.error_message}` : "deploy failed"
87
113
  );
88
114
  }
89
115
  }
90
- await new Promise((a) => setTimeout(a, this.pollIntervalMs));
116
+ await new Promise((t) => setTimeout(t, this.pollIntervalMs));
91
117
  }
92
118
  throw new Error("Deploy did not become ready in time");
93
119
  }
@@ -22,20 +22,27 @@ export interface BuildClient {
22
22
  }
23
23
  export interface AuthenticatedBuildClientConfig {
24
24
  /**
25
- * API base for every call. Requests go straight to `api.netlify.com`, whose
26
- * `/api/*` CORS policy allows any origin, rather than through a consumer's
27
- * access-control rewrite — that proxy allowlists paths per origin, and site
28
- * creation and deploy reads are not on the list for netlify.com (they 400
29
- * with `x-debug: request origin not allowed to call endpoint`). The zip
30
- * upload could not use that proxy regardless: it runs in a Lambda with a
31
- * ~6MB request body limit.
25
+ * Base for JSON API calls, routed through the consumer's access-control
26
+ * rewrite so the session cookie authenticates them. On netlify.com that
27
+ * rewrite is `/access-control/*` → the app's access-control function, which
28
+ * makes `/access-control/bb-api/api/v1` the API root (the default here).
29
+ */
30
+ proxyBase?: string;
31
+ /**
32
+ * Direct API base for the zip upload. The proxy above runs in a Lambda with a
33
+ * ~6MB request body limit, so the multipart POST has to go straight to
34
+ * `api.netlify.com` (whose `/api/*` CORS policy allows any origin).
32
35
  */
33
36
  apiBase?: string;
34
37
  /**
35
- * Mint a short-lived bearer token authorizing the calls. On netlify.com this
36
- * is `GET /access-control/generate-access-control-token`, whose
38
+ * Mint a bearer token for that direct upload. On netlify.com this is
39
+ * `GET /access-control/generate-access-control-token`, whose
37
40
  * `accessControlToken` the API decrypts back into the user's access token.
38
41
  * Resolving `null` means "no session" and surfaces as `NotAuthenticatedError`.
42
+ *
43
+ * Note the token issued to a non-app origin lives only 30 seconds. It is
44
+ * minted immediately before the upload, but a zip that takes longer than that
45
+ * to transfer will still be rejected mid-flight — see `createBuildFromZip`.
39
46
  */
40
47
  getUploadToken: () => Promise<string | null>;
41
48
  /** Create the site in this account. Omit to use the user's default account. */
@@ -60,16 +67,18 @@ export interface AuthenticatedBuildClientConfig {
60
67
  * short-lived bearer token because the proxy can't carry a body that large.
61
68
  */
62
69
  export declare class AuthenticatedBuildClient implements BuildClient {
70
+ private proxyBase;
63
71
  private apiBase;
64
72
  private getUploadToken;
65
73
  private accountSlug?;
66
74
  private title;
67
75
  private pollIntervalMs;
68
76
  private timeoutMs;
69
- constructor({ apiBase, getUploadToken, accountSlug, title, pollIntervalMs, timeoutMs, }: AuthenticatedBuildClientConfig);
77
+ constructor({ proxyBase, apiBase, getUploadToken, accountSlug, title, pollIntervalMs, timeoutMs, }: AuthenticatedBuildClientConfig);
70
78
  /**
71
- * Create the site the build deploys into. Authorized by the bearer token;
72
- * without an account slug the API picks the user's default account.
79
+ * Create the site the build deploys into. Goes through the proxy with
80
+ * `credentials: 'include'` so the app session authorizes it; without an
81
+ * account slug the API picks the user's default account.
73
82
  */
74
83
  createSite(): Promise<BuildSite>;
75
84
  /**
@@ -77,6 +86,11 @@ export declare class AuthenticatedBuildClient implements BuildClient {
77
86
  * (see the note on `apiBase`), authorized by a freshly minted bearer token.
78
87
  * `Content-Type` is left unset on purpose so the browser adds the multipart
79
88
  * boundary itself.
89
+ *
90
+ * Known ceiling: this is one request on a 30s token, and re-minting can't
91
+ * help once it is in flight. A zip that takes longer than that to upload will
92
+ * fail — the app origin gets 300s for exactly this reason, and lifting the
93
+ * limit for other origins is a platform-side change.
80
94
  */
81
95
  createBuildFromZip(siteId: string, zip: File): Promise<BuildResponse>;
82
96
  /**
@@ -86,10 +100,10 @@ export declare class AuthenticatedBuildClient implements BuildClient {
86
100
  * deploy id yet has nothing pollable, so it resolves immediately and the
87
101
  * caller falls back to the site page.
88
102
  *
89
- * Optional in the drop flow, and off by default: a build takes minutes, which
90
- * can outlive the bearer token minted for it. Each poll re-mints so a long
91
- * build keeps reading; a token that can't be minted any more ends the wait as
92
- * a timeout rather than a false failure.
103
+ * Optional in the drop flow, and off by default: a build takes minutes, so
104
+ * the useful hand-off is usually the project page rather than a spinner.
105
+ * Polling rides the session cookie, so it cannot outlive its credentials the
106
+ * way a 30s bearer would.
93
107
  */
94
108
  waitUntilBuildReady(build: BuildResponse): Promise<void>;
95
109
  }