@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.
@@ -33,20 +33,27 @@ export interface BuildClient {
33
33
 
34
34
  export interface AuthenticatedBuildClientConfig {
35
35
  /**
36
- * API base for every call. Requests go straight to `api.netlify.com`, whose
37
- * `/api/*` CORS policy allows any origin, rather than through a consumer's
38
- * access-control rewrite — that proxy allowlists paths per origin, and site
39
- * creation and deploy reads are not on the list for netlify.com (they 400
40
- * with `x-debug: request origin not allowed to call endpoint`). The zip
41
- * upload could not use that proxy regardless: it runs in a Lambda with a
42
- * ~6MB request body limit.
36
+ * Base for JSON API calls, routed through the consumer's access-control
37
+ * rewrite so the session cookie authenticates them. On netlify.com that
38
+ * rewrite is `/access-control/*` → the app's access-control function, which
39
+ * makes `/access-control/bb-api/api/v1` the API root (the default here).
40
+ */
41
+ proxyBase?: string;
42
+ /**
43
+ * Direct API base for the zip upload. The proxy above runs in a Lambda with a
44
+ * ~6MB request body limit, so the multipart POST has to go straight to
45
+ * `api.netlify.com` (whose `/api/*` CORS policy allows any origin).
43
46
  */
44
47
  apiBase?: string;
45
48
  /**
46
- * Mint a short-lived bearer token authorizing the calls. On netlify.com this
47
- * is `GET /access-control/generate-access-control-token`, whose
49
+ * Mint a bearer token for that direct upload. On netlify.com this is
50
+ * `GET /access-control/generate-access-control-token`, whose
48
51
  * `accessControlToken` the API decrypts back into the user's access token.
49
52
  * Resolving `null` means "no session" and surfaces as `NotAuthenticatedError`.
53
+ *
54
+ * Note the token issued to a non-app origin lives only 30 seconds. It is
55
+ * minted immediately before the upload, but a zip that takes longer than that
56
+ * to transfer will still be rejected mid-flight — see `createBuildFromZip`.
50
57
  */
51
58
  getUploadToken: () => Promise<string | null>;
52
59
  /** Create the site in this account. Omit to use the user's default account. */
@@ -59,6 +66,7 @@ export interface AuthenticatedBuildClientConfig {
59
66
  timeoutMs?: number;
60
67
  }
61
68
 
69
+ const PROXY_BASE = '/access-control/bb-api/api/v1';
62
70
  const API_BASE = 'https://api.netlify.com/api/v1';
63
71
  const POLL_INTERVAL_MS = 1000;
64
72
  const TIMEOUT_MS = 10 * 60 * 1000;
@@ -84,6 +92,7 @@ interface DeployState {
84
92
  * short-lived bearer token because the proxy can't carry a body that large.
85
93
  */
86
94
  export class AuthenticatedBuildClient implements BuildClient {
95
+ private proxyBase: string;
87
96
  private apiBase: string;
88
97
  private getUploadToken: () => Promise<string | null>;
89
98
  private accountSlug?: string;
@@ -92,6 +101,7 @@ export class AuthenticatedBuildClient implements BuildClient {
92
101
  private timeoutMs: number;
93
102
 
94
103
  constructor({
104
+ proxyBase = PROXY_BASE,
95
105
  apiBase = API_BASE,
96
106
  getUploadToken,
97
107
  accountSlug,
@@ -99,6 +109,7 @@ export class AuthenticatedBuildClient implements BuildClient {
99
109
  pollIntervalMs = POLL_INTERVAL_MS,
100
110
  timeoutMs = TIMEOUT_MS,
101
111
  }: AuthenticatedBuildClientConfig) {
112
+ this.proxyBase = proxyBase;
102
113
  this.apiBase = apiBase;
103
114
  this.getUploadToken = getUploadToken;
104
115
  this.accountSlug = accountSlug;
@@ -108,17 +119,16 @@ export class AuthenticatedBuildClient implements BuildClient {
108
119
  }
109
120
 
110
121
  /**
111
- * Create the site the build deploys into. Authorized by the bearer token;
112
- * without an account slug the API picks the user's default account.
122
+ * Create the site the build deploys into. Goes through the proxy with
123
+ * `credentials: 'include'` so the app session authorizes it; without an
124
+ * account slug the API picks the user's default account.
113
125
  */
114
126
  async createSite(): Promise<BuildSite> {
115
- const token = await this.getUploadToken();
116
- if (!token) throw new NotAuthenticatedError();
117
-
118
127
  const path = this.accountSlug ? `/${this.accountSlug}/sites` : '/sites';
119
- const res = await fetch(`${this.apiBase}${path}`, {
128
+ const res = await fetch(`${this.proxyBase}${path}`, {
120
129
  method: 'POST',
121
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
130
+ credentials: 'include',
131
+ headers: { 'Content-Type': 'application/json' },
122
132
  body: JSON.stringify({ created_via: 'drop' }),
123
133
  });
124
134
  // A logged-out visitor gets a 401 here rather than at the token step, since
@@ -133,6 +143,11 @@ export class AuthenticatedBuildClient implements BuildClient {
133
143
  * (see the note on `apiBase`), authorized by a freshly minted bearer token.
134
144
  * `Content-Type` is left unset on purpose so the browser adds the multipart
135
145
  * boundary itself.
146
+ *
147
+ * Known ceiling: this is one request on a 30s token, and re-minting can't
148
+ * help once it is in flight. A zip that takes longer than that to upload will
149
+ * fail — the app origin gets 300s for exactly this reason, and lifting the
150
+ * limit for other origins is a platform-side change.
136
151
  */
137
152
  async createBuildFromZip(siteId: string, zip: File): Promise<BuildResponse> {
138
153
  const token = await this.getUploadToken();
@@ -159,19 +174,18 @@ export class AuthenticatedBuildClient implements BuildClient {
159
174
  * deploy id yet has nothing pollable, so it resolves immediately and the
160
175
  * caller falls back to the site page.
161
176
  *
162
- * Optional in the drop flow, and off by default: a build takes minutes, which
163
- * can outlive the bearer token minted for it. Each poll re-mints so a long
164
- * build keeps reading; a token that can't be minted any more ends the wait as
165
- * a timeout rather than a false failure.
177
+ * Optional in the drop flow, and off by default: a build takes minutes, so
178
+ * the useful hand-off is usually the project page rather than a spinner.
179
+ * Polling rides the session cookie, so it cannot outlive its credentials the
180
+ * way a 30s bearer would.
166
181
  */
167
182
  async waitUntilBuildReady(build: BuildResponse): Promise<void> {
168
183
  if (!build.deploy_id) return;
169
184
 
170
185
  const deadline = this.timeoutMs / this.pollIntervalMs;
171
186
  for (let i = 0; i < deadline; i++) {
172
- const token = await this.getUploadToken();
173
- const res = await fetch(`${this.apiBase}/deploys/${build.deploy_id}`, {
174
- headers: token ? { Authorization: `Bearer ${token}` } : {},
187
+ const res = await fetch(`${this.proxyBase}/deploys/${build.deploy_id}`, {
188
+ credentials: 'include',
175
189
  });
176
190
  if (res.ok) {
177
191
  const deploy: DeployState = await res.json();
@@ -89,6 +89,7 @@ interface UseDropDeployOptions {
89
89
  buildClient?: BuildClient;
90
90
  authenticatedDropClient?: DropClient;
91
91
  getUploadToken?: () => Promise<string | null>;
92
+ proxyBase?: string;
92
93
  accountSlug?: string;
93
94
  buildDeployUrl: (build: DeployedBuild) => string;
94
95
  waitForBuild?: boolean;
@@ -141,6 +142,7 @@ export function useDropDeploy({
141
142
  buildClient,
142
143
  authenticatedDropClient,
143
144
  getUploadToken,
145
+ proxyBase,
144
146
  accountSlug,
145
147
  buildDeployUrl,
146
148
  waitForBuild,
@@ -160,7 +162,9 @@ export function useDropDeploy({
160
162
  // fresh client (and a fresh token cache) to an in-flight deploy.
161
163
  const authedBuildClient = useRef(
162
164
  buildClient ??
163
- (getUploadToken ? new AuthenticatedBuildClient({ getUploadToken, accountSlug }) : null)
165
+ (getUploadToken
166
+ ? new AuthenticatedBuildClient({ proxyBase, getUploadToken, accountSlug })
167
+ : null)
164
168
  ).current;
165
169
 
166
170
  // Same opt-in for static drops: with authenticated wiring present, deploy
@@ -168,7 +172,9 @@ export function useDropDeploy({
168
172
  // every static drop stays anonymous.
169
173
  const authedDropClient = useRef(
170
174
  authenticatedDropClient ??
171
- (getUploadToken ? new AuthenticatedDropClient({ getUploadToken, accountSlug }) : null)
175
+ (getUploadToken
176
+ ? new AuthenticatedDropClient({ proxyBase, getUploadToken, accountSlug })
177
+ : null)
172
178
  ).current;
173
179
 
174
180
  const busy = status.kind !== 'idle' && status.kind !== 'error';