@sanity/access-ui 6.13.0-next.7 → 6.13.0-next.79

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/lib/index.d.ts CHANGED
@@ -76,6 +76,34 @@ type SubmitAccessRequestResult = {
76
76
  type: 'error';
77
77
  error: unknown;
78
78
  };
79
+ /**
80
+ * What the Access API says the request-access screen should show
81
+ * (`GET /access/{resourceType}/{resourceId}/requests/state`).
82
+ *
83
+ * Answered before the user writes anything, so a futile form is never offered.
84
+ * Distinct from {@link AccessRequestState}, which this package derives from the
85
+ * caller's own request history: this one is the server's verdict.
86
+ *
87
+ * - `eligible` — offer the form. Creating can still fail: the create path has
88
+ * its own gates, which this endpoint does not resolve.
89
+ * - `saml-required` — the organization only admits members through its SSO
90
+ * login flow, so no administrator could ever approve a request. `redirectUrl`
91
+ * is where to send them to log in: normally the organization's SSO form,
92
+ * which submits itself, but a confirmation page for an organization with no
93
+ * slug. Both end at the identity provider, so the card treats them alike. It
94
+ * is absent only when neither resolves, leaving no way forward.
95
+ * - `resource-not-available` — the target project or organization is gone.
96
+ *
97
+ * @public
98
+ */
99
+ type AccessRequestEligibilityState = {
100
+ state: 'eligible';
101
+ } | {
102
+ state: 'saml-required';
103
+ redirectUrl?: string;
104
+ } | {
105
+ state: 'resource-not-available';
106
+ };
79
107
  /**
80
108
  * The current user rendered in the request-access screen. A structural subset of
81
109
  * `CurrentUser` from `@sanity/types`, so both studio and app callers can pass
@@ -102,6 +130,25 @@ declare const MAX_ACCESS_REQUEST_NOTE_LENGTH = 150;
102
130
  * @public
103
131
  */
104
132
  declare function listMyAccessRequests(client: SanityClient): Promise<AccessRequest[]>;
133
+ /**
134
+ * Asks what the request-access screen should show
135
+ * (`GET /access/{resourceType}/{resourceId}/requests/state`).
136
+ *
137
+ * Runs before the form is offered, so a user in a SAML-enforced organization is
138
+ * pointed at SSO instead of writing a note no administrator can action.
139
+ *
140
+ * `origin` is carried opaquely to the login page so the user returns where they
141
+ * started. Never throws: an unreachable or older API answers `eligible`,
142
+ * leaving the form in place and the submit-time 403 as the backstop.
143
+ *
144
+ * @public
145
+ */
146
+ declare function fetchAccessRequestStatus(options: {
147
+ client: SanityClient;
148
+ resourceType: AccessResourceType;
149
+ resourceId: string;
150
+ origin?: string;
151
+ }): Promise<AccessRequestEligibilityState>;
105
152
  /**
106
153
  * Submits an access request (`POST /access/{resourceType}/{resourceId}/requests`)
107
154
  * and maps the Access API's error contract to a {@link SubmitAccessRequestResult}.
@@ -157,10 +204,13 @@ interface RequestAccessLabels {
157
204
  message?: string;
158
205
  }) => ReactNode;
159
206
  expiredMessage: ReactNode;
207
+ ssoEnforcedTitle: ReactNode;
160
208
  ssoEnforcedMessage: (context: {
161
209
  providerTitle?: string;
162
210
  }) => ReactNode;
163
211
  ssoSignInCta: ReactNode;
212
+ resourceNotAvailableTitle: ReactNode;
213
+ resourceNotAvailableMessage: ReactNode;
164
214
  submitFailedMessage: ReactNode;
165
215
  wrongAccount: ReactNode;
166
216
  signOut: ReactNode;
@@ -222,5 +272,5 @@ declare function RequestAccessForm(props: RequestAccessFormProps): import("react
222
272
  * @public
223
273
  */
224
274
  type RequestAccessView = 'form' | 'sent' | 'pending' | 'blocked' | 'sso-enforced';
225
- export { type AccessRequest, type AccessRequestState, type AccessResourceType, type AccessUser, MAX_ACCESS_REQUEST_NOTE_LENGTH, RequestAccessForm, type RequestAccessFormProps, type RequestAccessLabels, type RequestAccessView, type SubmitAccessRequestResult, deriveAccessRequestState, getProviderTitle, listMyAccessRequests, submitAccessRequest };
275
+ export { type AccessRequest, type AccessRequestEligibilityState, type AccessRequestState, type AccessResourceType, type AccessUser, MAX_ACCESS_REQUEST_NOTE_LENGTH, RequestAccessForm, type RequestAccessFormProps, type RequestAccessLabels, type RequestAccessView, type SubmitAccessRequestResult, deriveAccessRequestState, fetchAccessRequestStatus, getProviderTitle, listMyAccessRequests, submitAccessRequest };
226
276
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -25,6 +25,31 @@ async function listMyAccessRequests(client) {
25
25
  tag: "access-ui.list-requests"
26
26
  }) ?? [];
27
27
  }
28
+ /**
29
+ * Asks what the request-access screen should show
30
+ * (`GET /access/{resourceType}/{resourceId}/requests/state`).
31
+ *
32
+ * Runs before the form is offered, so a user in a SAML-enforced organization is
33
+ * pointed at SSO instead of writing a note no administrator can action.
34
+ *
35
+ * `origin` is carried opaquely to the login page so the user returns where they
36
+ * started. Never throws: an unreachable or older API answers `eligible`,
37
+ * leaving the form in place and the submit-time 403 as the backstop.
38
+ *
39
+ * @public
40
+ */
41
+ async function fetchAccessRequestStatus(options) {
42
+ let { client, resourceType, resourceId, origin } = options;
43
+ try {
44
+ return await withAccessApiVersion(client).request({
45
+ url: `/access/${resourceType}/${resourceId}/requests/state`,
46
+ tag: "access-ui.request-state",
47
+ query: origin ? { returnQuery: new URLSearchParams({ origin }).toString() } : void 0
48
+ }) ?? { state: "eligible" };
49
+ } catch {
50
+ return { state: "eligible" };
51
+ }
52
+ }
28
53
  function getErrorResponseDetails(err) {
29
54
  if (typeof err != "object" || !err) return {};
30
55
  let response = err.response;
@@ -140,12 +165,15 @@ const defaultLabels = {
140
165
  deniedMessage: ({ message }) => message ?? "Your request to access this content has been declined.",
141
166
  overLimitMessage: ({ message }) => message ?? "You’ve reached the limit for access requests across all projects. Please wait before submitting more requests, or contact an admin.",
142
167
  expiredMessage: "Your previous request has expired. You may request access again below.",
168
+ ssoEnforcedTitle: "Sign in with SSO required",
143
169
  ssoEnforcedMessage: ({ providerTitle }) => providerTitle ? /* @__PURE__ */ jsxs(Fragment, { children: [
144
170
  "You’re signed in with ",
145
171
  /* @__PURE__ */ jsx("strong", { children: providerTitle }),
146
172
  ", but this organization requires signing in with SSO. Access can’t be requested with this account."
147
173
  ] }) : /* @__PURE__ */ jsx(Fragment, { children: "This organization requires signing in with SSO. Access can’t be requested with this account." }),
148
174
  ssoSignInCta: "Sign in with SSO",
175
+ resourceNotAvailableTitle: "Access can’t be requested",
176
+ resourceNotAvailableMessage: "The resource currently being requested is no longer available.",
149
177
  submitFailedMessage: "There was a problem submitting your request. Please try again.",
150
178
  wrongAccount: "Wrong account?",
151
179
  signOut: "Sign out"
@@ -162,100 +190,129 @@ const defaultLabels = {
162
190
  * @public
163
191
  */
164
192
  function RequestAccessForm(props) {
165
- let $ = c(6), { client } = props, t0;
166
- $[0] === client ? t0 = $[1] : (t0 = () => listMyAccessRequests(client).catch(_temp), $[0] = client, $[1] = t0);
167
- let [requestsPromise] = useState(t0), t1;
168
- $[2] === Symbol.for("react.memo_cache_sentinel") ? (t1 = /* @__PURE__ */ jsx(Flex, {
193
+ let $ = c(11), { client, resourceType: t0, resourceId } = props, resourceType = t0 === void 0 ? "project" : t0, t1;
194
+ $[0] === client ? t1 = $[1] : (t1 = () => listMyAccessRequests(client).catch(_temp), $[0] = client, $[1] = t1);
195
+ let [requestsPromise] = useState(t1), t2;
196
+ $[2] !== client || $[3] !== resourceId || $[4] !== resourceType ? (t2 = () => fetchAccessRequestStatus({
197
+ client,
198
+ resourceType,
199
+ resourceId,
200
+ origin: getRequestUrl()
201
+ }), $[2] = client, $[3] = resourceId, $[4] = resourceType, $[5] = t2) : t2 = $[5];
202
+ let [statusPromise] = useState(t2), t3;
203
+ $[6] === Symbol.for("react.memo_cache_sentinel") ? (t3 = /* @__PURE__ */ jsx(Flex, {
169
204
  align: "center",
170
205
  height: "fill",
171
206
  justify: "center",
172
207
  padding: 5,
173
208
  children: /* @__PURE__ */ jsx(Spinner, { muted: !0 })
174
- }), $[2] = t1) : t1 = $[2];
175
- let t2;
176
- return $[3] !== props || $[4] !== requestsPromise ? (t2 = /* @__PURE__ */ jsx(Card, {
209
+ }), $[6] = t3) : t3 = $[6];
210
+ let t4;
211
+ return $[7] !== props || $[8] !== requestsPromise || $[9] !== statusPromise ? (t4 = /* @__PURE__ */ jsx(Card, {
177
212
  border: !0,
178
213
  height: "fill",
179
214
  overflow: "hidden",
180
215
  radius: 3,
181
216
  tone: "default",
182
217
  children: /* @__PURE__ */ jsx(Suspense, {
183
- fallback: t1,
218
+ fallback: t3,
184
219
  children: /* @__PURE__ */ jsx(RequestAccessFormContent, {
185
220
  ...props,
186
- requestsPromise
221
+ requestsPromise,
222
+ statusPromise
187
223
  })
188
224
  })
189
- }), $[3] = props, $[4] = requestsPromise, $[5] = t2) : t2 = $[5], t2;
225
+ }), $[7] = props, $[8] = requestsPromise, $[9] = statusPromise, $[10] = t4) : t4 = $[10], t4;
190
226
  }
191
227
  function _temp() {
192
228
  return null;
193
229
  }
194
- function deriveViewState(options) {
195
- let { fetchedRequests, resourceId, submitResult, labels } = options;
196
- if (submitResult) switch (submitResult.type) {
197
- case "submitted": return { view: "sent" };
198
- case "sso-enforced": return {
199
- view: "sso-enforced",
200
- redirectUrl: submitResult.redirectUrl
201
- };
202
- case "denied": return {
230
+ /**
231
+ * The server's verdict. `null` hands the decision back to the caller's own
232
+ * request history, which resolves the states this endpoint does not.
233
+ */
234
+ function deriveServerViewState(status, labels, providerTitle) {
235
+ switch (status.state) {
236
+ case "saml-required": return ssoEnforcedState({
237
+ labels,
238
+ providerTitle,
239
+ redirectUrl: status.redirectUrl
240
+ });
241
+ case "resource-not-available": return {
203
242
  view: "blocked",
204
- title: labels.errorTitle,
205
- message: labels.deniedMessage({ message: submitResult.message })
243
+ title: labels.resourceNotAvailableTitle,
244
+ description: null,
245
+ message: labels.resourceNotAvailableMessage
206
246
  };
207
- case "over-limit": return {
208
- view: "blocked",
209
- title: labels.errorTitle,
210
- message: labels.overLimitMessage({ message: submitResult.message })
247
+ case "eligible": return null;
248
+ default: return null;
249
+ }
250
+ }
251
+ function ssoEnforcedState(options) {
252
+ let { labels, providerTitle, redirectUrl } = options;
253
+ return {
254
+ view: "sso-enforced",
255
+ title: labels.ssoEnforcedTitle,
256
+ description: null,
257
+ message: labels.ssoEnforcedMessage({ providerTitle }),
258
+ redirectUrl
259
+ };
260
+ }
261
+ function deriveViewState(options) {
262
+ let { accessRequestsHistory, accessRequestEligibilityState, resourceId, submitAccessRequestResult, currentUser, providerTitle, labels } = options, submitFailure = (message) => ({
263
+ view: "blocked",
264
+ title: labels.errorTitle,
265
+ description: null,
266
+ message
267
+ });
268
+ if (submitAccessRequestResult) switch (submitAccessRequestResult.type) {
269
+ case "submitted": return {
270
+ view: "sent",
271
+ title: labels.sentTitle,
272
+ description: labels.sentDescription
211
273
  };
274
+ case "sso-enforced": return ssoEnforcedState({
275
+ labels,
276
+ providerTitle,
277
+ redirectUrl: submitAccessRequestResult.redirectUrl
278
+ });
279
+ case "denied": return submitFailure(labels.deniedMessage({ message: submitAccessRequestResult.message }));
280
+ case "over-limit": return submitFailure(labels.overLimitMessage({ message: submitAccessRequestResult.message }));
212
281
  case "email-domain-blocked":
213
- case "requests-disabled": return {
214
- view: "blocked",
215
- title: labels.errorTitle,
216
- message: submitResult.message
217
- };
282
+ case "requests-disabled": return submitFailure(submitAccessRequestResult.message);
218
283
  }
219
- let state = deriveAccessRequestState(fetchedRequests, resourceId);
220
- return state === "pending" ? { view: "pending" } : state === "denied" ? {
284
+ let serverState = deriveServerViewState(accessRequestEligibilityState, labels, providerTitle);
285
+ if (serverState) return serverState;
286
+ let state = deriveAccessRequestState(accessRequestsHistory, resourceId);
287
+ return state === "pending" ? {
288
+ view: "pending",
289
+ title: labels.sentTitle,
290
+ description: labels.pendingMessage
291
+ } : state === "denied" ? {
221
292
  view: "blocked",
222
293
  title: labels.deniedTitle,
294
+ description: null,
223
295
  message: labels.deniedMessage({})
224
296
  } : {
225
297
  view: "form",
298
+ title: labels.title,
299
+ description: labels.describeNoAccess({ email: currentUser?.email }),
226
300
  expired: state === "expired"
227
301
  };
228
302
  }
229
303
  function RequestAccessFormContent(props) {
230
- let $ = c(9), { client, resourceType: t0, resourceId, currentUser, onSignOut, onRequestSubmitted, preview, renderAction, requestsPromise } = props, resourceType = t0 === void 0 ? "project" : t0, labels = {
304
+ let $ = c(9), { client, resourceType: t0, resourceId, currentUser, onSignOut, onRequestSubmitted, preview, renderAction, requestsPromise, statusPromise } = props, resourceType = t0 === void 0 ? "project" : t0, labels = {
231
305
  ...defaultLabels,
232
306
  ...props.labels
233
- }, fetchedRequests = use(requestsPromise), titleId = useId(), [note, setNote] = useState(""), [submitResult, setSubmitResult] = useState(null), [isSubmitting, startSubmit] = useTransition(), state = deriveViewState({
234
- fetchedRequests,
307
+ }, accessRequestsHistory = use(requestsPromise), accessRequestEligibilityState = use(statusPromise), titleId = useId(), [note, setNote] = useState(""), [submitAccessRequestResult, setSubmitAccessRequestResult] = useState(null), [isSubmitting, startSubmit] = useTransition(), providerTitle = getProviderTitle(currentUser?.provider), state = deriveViewState({
308
+ accessRequestsHistory,
309
+ accessRequestEligibilityState,
235
310
  resourceId,
236
- submitResult,
311
+ submitAccessRequestResult,
312
+ currentUser,
313
+ providerTitle,
237
314
  labels
238
- }), providerTitle = getProviderTitle(currentUser?.provider), submitFailed = submitResult?.type === "error", heading = {
239
- form: {
240
- title: labels.title,
241
- description: labels.describeNoAccess({ email: currentUser?.email })
242
- },
243
- sent: {
244
- title: labels.sentTitle,
245
- description: labels.sentDescription
246
- },
247
- pending: {
248
- title: labels.sentTitle,
249
- description: labels.pendingMessage
250
- },
251
- "sso-enforced": {
252
- title: labels.errorTitle,
253
- description: null
254
- }
255
- }, { title, description } = state.view === "blocked" ? {
256
- title: state.title,
257
- description: null
258
- } : heading[state.view], t1;
315
+ }), submitFailed = submitAccessRequestResult?.type === "error", t1;
259
316
  $[0] !== client || $[1] !== isSubmitting || $[2] !== note || $[3] !== onRequestSubmitted || $[4] !== resourceId || $[5] !== resourceType ? (t1 = (event) => {
260
317
  event.preventDefault(), !isSubmitting && startSubmit(async () => {
261
318
  let trimmedNote = note.trim() || void 0, result = await submitAccessRequest({
@@ -265,7 +322,7 @@ function RequestAccessFormContent(props) {
265
322
  note: trimmedNote,
266
323
  requestUrl: getRequestUrl()
267
324
  });
268
- setSubmitResult(result), result.type === "submitted" && onRequestSubmitted?.({ note: trimmedNote });
325
+ setSubmitAccessRequestResult(result), result.type === "submitted" && onRequestSubmitted?.({ note: trimmedNote });
269
326
  });
270
327
  }, $[0] = client, $[1] = isSubmitting, $[2] = note, $[3] = onRequestSubmitted, $[4] = resourceId, $[5] = resourceType, $[6] = t1) : t1 = $[6];
271
328
  let handleSubmit = t1, t2;
@@ -288,28 +345,15 @@ function RequestAccessFormContent(props) {
288
345
  id: titleId,
289
346
  size: 2,
290
347
  weight: "semibold",
291
- children: title
348
+ children: state.title
292
349
  }),
293
- description === null ? null : /* @__PURE__ */ jsx(Text, {
350
+ state.description === null ? null : /* @__PURE__ */ jsx(Text, {
294
351
  as: "p",
295
352
  muted: !0,
296
353
  size: 1,
297
- children: description
354
+ children: state.description
298
355
  }),
299
- state.view === "blocked" ? /* @__PURE__ */ jsx(Card, {
300
- border: !0,
301
- padding: 3,
302
- radius: 2,
303
- role: "alert",
304
- tone: "caution",
305
- children: /* @__PURE__ */ jsx(Text, {
306
- as: "p",
307
- muted: !0,
308
- size: 1,
309
- children: state.message
310
- })
311
- }) : null,
312
- state.view === "sso-enforced" ? /* @__PURE__ */ jsxs(Stack, {
356
+ state.view === "blocked" || state.view === "sso-enforced" ? /* @__PURE__ */ jsxs(Stack, {
313
357
  gap: 4,
314
358
  children: [/* @__PURE__ */ jsx(Card, {
315
359
  border: !0,
@@ -321,9 +365,9 @@ function RequestAccessFormContent(props) {
321
365
  as: "p",
322
366
  muted: !0,
323
367
  size: 1,
324
- children: labels.ssoEnforcedMessage({ providerTitle })
368
+ children: state.message
325
369
  })
326
- }), state.redirectUrl ? /* @__PURE__ */ jsx(Button, {
370
+ }), state.view === "sso-enforced" && state.redirectUrl ? /* @__PURE__ */ jsx(Button, {
327
371
  as: "a",
328
372
  href: state.redirectUrl,
329
373
  iconRight: LaunchIcon,
@@ -437,6 +481,6 @@ function getInitials(user) {
437
481
  let initials = source.trim().split(/\s+/).slice(0, 2).map((part) => part[0]).join("");
438
482
  return initials ? initials.toUpperCase() : void 0;
439
483
  }
440
- export { MAX_ACCESS_REQUEST_NOTE_LENGTH, RequestAccessForm, deriveAccessRequestState, getProviderTitle, listMyAccessRequests, submitAccessRequest };
484
+ export { MAX_ACCESS_REQUEST_NOTE_LENGTH, RequestAccessForm, deriveAccessRequestState, fetchAccessRequestStatus, getProviderTitle, listMyAccessRequests, submitAccessRequest };
441
485
 
442
486
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["() =>\n listMyAccessRequests(client).catch((): AccessRequest[] | null => null)","(): AccessRequest[] | null => null","<Flex align=\"center\" height=\"fill\" justify=\"center\" padding={5}>\n <Spinner muted />\n </Flex>","<Card border height=\"fill\" overflow=\"hidden\" radius={3} tone=\"default\">\n <Suspense\n fallback={\n <Flex align=\"center\" height=\"fill\" justify=\"center\" padding={5}>\n <Spinner muted />\n </Flex>\n }\n >\n <RequestAccessFormContent {...props} requestsPromise={requestsPromise} />\n </Suspense>\n </Card>","resourceType = 'project'","(event: SubmitEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (isSubmitting) return\n startSubmit(async () => {\n const trimmedNote = note.trim() || undefined\n const result = await submitAccessRequest({\n client,\n resourceType,\n resourceId,\n note: trimmedNote,\n requestUrl: getRequestUrl(),\n })\n setSubmitResult(result)\n if (result.type === 'submitted') onRequestSubmitted?.({note: trimmedNote})\n })\n }","preview ? (\n <Flex justify=\"center\" padding={2}>\n {preview}\n </Flex>\n ) : null","event"],"sources":["../src/accessRequests.ts","../src/deriveAccessRequestState.ts","../src/providerTitle.ts","../src/labels.tsx","../src/RequestAccessForm.tsx"],"sourcesContent":["import {type SanityClient} from '@sanity/client'\n\nimport {type AccessRequest, type AccessResourceType, type SubmitAccessRequestResult} from './types'\n\n/**\n * The Access API only accepts notes up to this length.\n *\n * @public\n */\nexport const MAX_ACCESS_REQUEST_NOTE_LENGTH = 150\n\nconst ACCESS_API_VERSION = '2024-07-01'\n\n/**\n * Structured 403 code thrown by the Access API when the target organization\n * only admits members through its SSO login flow.\n */\nconst SAML_ENFORCEMENT_REQUIRED = 'saml_enforcement_required'\n\nfunction withAccessApiVersion(client: SanityClient): SanityClient {\n return client.withConfig({apiVersion: ACCESS_API_VERSION})\n}\n\n/**\n * Fetches the caller's own access requests across all resources\n * (`GET /access/requests/me`).\n *\n * @public\n */\nexport async function listMyAccessRequests(client: SanityClient): Promise<AccessRequest[]> {\n const requests = await withAccessApiVersion(client).request<AccessRequest[] | null>({\n url: '/access/requests/me',\n tag: 'access-ui.list-requests',\n })\n return requests ?? []\n}\n\ninterface ErrorResponseDetails {\n statusCode?: number\n message?: string\n code?: string\n redirectUrl?: string\n}\n\nfunction getErrorResponseDetails(err: unknown): ErrorResponseDetails {\n if (typeof err !== 'object' || err === null) return {}\n const response = (err as {response?: unknown}).response\n if (typeof response !== 'object' || response === null) return {}\n const {statusCode} = response as {statusCode?: unknown}\n const body = (response as {body?: unknown}).body\n const details: ErrorResponseDetails = {\n statusCode: typeof statusCode === 'number' ? statusCode : undefined,\n }\n if (typeof body === 'object' && body !== null) {\n const {message, code, redirectUrl} = body as {\n message?: unknown\n code?: unknown\n redirectUrl?: unknown\n }\n details.message = typeof message === 'string' ? message : undefined\n details.code = typeof code === 'string' ? code : undefined\n details.redirectUrl = typeof redirectUrl === 'string' ? redirectUrl : undefined\n }\n return details\n}\n\nfunction mapSubmitError(err: unknown): SubmitAccessRequestResult {\n const {statusCode, message, code, redirectUrl} = getErrorResponseDetails(err)\n\n if (statusCode === 403 && code === SAML_ENFORCEMENT_REQUIRED) {\n return {type: 'sso-enforced', redirectUrl, message}\n }\n if (statusCode === 429) {\n return {type: 'over-limit', message}\n }\n if (statusCode === 409) {\n if (message?.includes('email domain')) return {type: 'email-domain-blocked', message}\n if (message?.includes('disabled for organization')) return {type: 'requests-disabled', message}\n return {type: 'denied', message: message?.replace(/^Conflict -\\s*/, '')}\n }\n return {type: 'error', error: err}\n}\n\n/**\n * Submits an access request (`POST /access/{resourceType}/{resourceId}/requests`)\n * and maps the Access API's error contract to a {@link SubmitAccessRequestResult}.\n * Never throws for API rejections; unexpected failures come back as\n * `{type: 'error'}` so callers decide how to surface them.\n *\n * @public\n */\nexport async function submitAccessRequest(options: {\n client: SanityClient\n resourceType: AccessResourceType\n resourceId: string\n note?: string\n requestUrl?: string\n}): Promise<SubmitAccessRequestResult> {\n const {client, resourceType, resourceId, note, requestUrl} = options\n try {\n const request = await withAccessApiVersion(client).request<AccessRequest | null>({\n url: `/access/${resourceType}/${resourceId}/requests`,\n method: 'post',\n tag: 'access-ui.submit-request',\n body: {note, requestUrl, type: 'access'},\n })\n return {type: 'submitted', request}\n } catch (err) {\n return mapSubmitError(err)\n }\n}\n","import {type AccessRequest, type AccessRequestState} from './types'\n\n/**\n * Access requests are considered active for two weeks, matching the Access\n * API's request lifetime.\n */\nconst REQUEST_LIFETIME_MS = 14 * 24 * 60 * 60 * 1000\n\n/**\n * Derives where the caller stands on requesting access to a resource from\n * their existing access requests.\n *\n * A declined request blocks re-requesting for two weeks. A pending request\n * younger than two weeks is in review; older pending requests count as\n * expired, and the caller may request again.\n *\n * @public\n */\nexport function deriveAccessRequestState(\n requests: AccessRequest[] | null | undefined,\n resourceId: string,\n now: number = Date.now(),\n): AccessRequestState {\n if (!requests || requests.length === 0) return 'none'\n\n const isRecent = (request: AccessRequest) =>\n now - new Date(request.createdAt).getTime() < REQUEST_LIFETIME_MS\n\n const forResource = requests.filter((request) => request.resourceId === resourceId)\n\n if (forResource.some((request) => request.status === 'declined' && isRecent(request))) {\n return 'denied'\n }\n if (forResource.some((request) => request.status === 'pending' && isRecent(request))) {\n return 'pending'\n }\n if (forResource.some((request) => request.status === 'pending')) {\n return 'expired'\n }\n return 'none'\n}\n","/**\n * Human-readable title for a login provider id, e.g. `google` → `Google`,\n * `saml-xyz` → `SAML/SSO`.\n *\n * @public\n */\nexport function getProviderTitle(provider?: string): string | undefined {\n if (provider === 'google') return 'Google'\n if (provider === 'github') return 'GitHub'\n if (provider === 'sanity') return 'Sanity'\n if (provider === 'vercel') return 'Vercel'\n if (provider?.startsWith('saml-')) return 'SAML/SSO'\n return undefined\n}\n","import {type ReactNode} from 'react'\n\n/**\n * All user-facing strings in the request-access screen. Every label can be\n * overridden, so hosts with their own i18n stack (studio i18n, react-i18next)\n * inject translated copy while standalone hosts get the English defaults.\n *\n * @public\n */\nexport interface RequestAccessLabels {\n title: ReactNode\n sentTitle: ReactNode\n deniedTitle: ReactNode\n errorTitle: ReactNode\n describeNoAccess: (context: {email?: string}) => ReactNode\n promptProject: ReactNode\n promptOrganization: ReactNode\n notePlaceholder: string\n noteAriaLabel: string\n submit: ReactNode\n sentDescription: ReactNode\n pendingMessage: ReactNode\n deniedMessage: (context: {message?: string}) => ReactNode\n overLimitMessage: (context: {message?: string}) => ReactNode\n expiredMessage: ReactNode\n ssoEnforcedMessage: (context: {providerTitle?: string}) => ReactNode\n ssoSignInCta: ReactNode\n submitFailedMessage: ReactNode\n wrongAccount: ReactNode\n signOut: ReactNode\n}\n\n/** @internal */\nexport const defaultLabels: RequestAccessLabels = {\n title: 'Request access',\n sentTitle: 'Access request sent',\n deniedTitle: 'Access request declined',\n errorTitle: 'Access request couldn’t be sent',\n describeNoAccess: ({email}) =>\n email ? (\n <>\n Your account <strong>({email})</strong> doesn’t have access to this content.\n </>\n ) : (\n <>Your account doesn’t have access to this content.</>\n ),\n promptProject: 'Send a request to the project admin(s).',\n promptOrganization: 'Send a request to the organization admin(s).',\n notePlaceholder: 'Message (optional)',\n noteAriaLabel: 'Message',\n submit: 'Request access',\n sentDescription:\n 'Your request has been sent. You will receive a notification if access is approved.',\n pendingMessage: 'Your request to access this content is pending approval.',\n deniedMessage: ({message}) => message ?? 'Your request to access this content has been declined.',\n overLimitMessage: ({message}) =>\n message ??\n 'You’ve reached the limit for access requests across all projects. Please wait before submitting more requests, or contact an admin.',\n expiredMessage: 'Your previous request has expired. You may request access again below.',\n ssoEnforcedMessage: ({providerTitle}) =>\n providerTitle ? (\n <>\n You’re signed in with <strong>{providerTitle}</strong>, but this organization requires\n signing in with SSO. Access can’t be requested with this account.\n </>\n ) : (\n <>\n This organization requires signing in with SSO. Access can’t be requested with this account.\n </>\n ),\n ssoSignInCta: 'Sign in with SSO',\n submitFailedMessage: 'There was a problem submitting your request. Please try again.',\n wrongAccount: 'Wrong account?',\n signOut: 'Sign out',\n}\n","import {type SanityClient} from '@sanity/client'\nimport {LaunchIcon} from '@sanity/icons/Launch'\nimport {Avatar, Button, Card, Flex, Spinner, Stack, Text, TextArea} from '@sanity/ui'\nimport {\n type ReactNode,\n type SubmitEvent,\n Suspense,\n use,\n useId,\n useState,\n useTransition,\n} from 'react'\nimport {Box} from 'ui5'\n\nimport {\n listMyAccessRequests,\n MAX_ACCESS_REQUEST_NOTE_LENGTH,\n submitAccessRequest,\n} from './accessRequests'\nimport {deriveAccessRequestState} from './deriveAccessRequestState'\nimport {defaultLabels, type RequestAccessLabels} from './labels'\nimport {getProviderTitle} from './providerTitle'\nimport {\n type AccessRequest,\n type AccessResourceType,\n type AccessUser,\n type SubmitAccessRequestResult,\n} from './types'\n\n/** @public */\nexport interface RequestAccessFormProps {\n /** Client authenticated as the requesting user. The Access API version is applied internally. */\n client: SanityClient\n resourceType?: AccessResourceType\n /** Project or organization id to request access to. */\n resourceId: string\n /** The signed-in user, rendered in the description and account footer. */\n currentUser?: AccessUser | null\n /**\n * Called when the user chooses \"Sign out\". The account footer's sign-out\n * action is only rendered when provided; hosts own the actual sign-out\n * mechanism (studio: `auth.logout()`, dashboard: logout route navigation).\n */\n onSignOut?: () => void\n /** Called after a request is successfully submitted, e.g. for analytics. */\n onRequestSubmitted?: (details: {note?: string}) => void\n /** Optional slot rendered above the title, e.g. a resource preview. */\n preview?: ReactNode\n /**\n * Renders an optional action area at the bottom of the card's content, e.g.\n * a navigation CTA. Called with the current view so the action can differ\n * per state (or be omitted for some); return null to render nothing.\n */\n renderAction?: (context: {view: RequestAccessView}) => ReactNode\n /** Label overrides for hosts with their own i18n stack. */\n labels?: Partial<RequestAccessLabels>\n}\n\n/**\n * The shared request-access screen: explains that the signed-in account lacks\n * access, lets the user request it with an optional note, and reflects the\n * request lifecycle (pending, denied, expired, over-limit, SSO-enforced).\n *\n * Fetches the caller's existing requests on mount and suspends while loading;\n * an internal `Suspense` boundary renders a spinner, so hosts can mount it\n * directly. Remount with a `key` when `client` or `resourceId` change.\n *\n * @public\n */\nexport function RequestAccessForm(props: RequestAccessFormProps) {\n const {client} = props\n\n // Created once (lazy init): recreating the promise per render would refetch\n // and re-suspend forever. Callers remount with `key` to reset.\n const [requestsPromise] = useState(() =>\n listMyAccessRequests(client).catch((): AccessRequest[] | null => null),\n )\n\n return (\n <Card border height=\"fill\" overflow=\"hidden\" radius={3} tone=\"default\">\n <Suspense\n fallback={\n <Flex align=\"center\" height=\"fill\" justify=\"center\" padding={5}>\n <Spinner muted />\n </Flex>\n }\n >\n <RequestAccessFormContent {...props} requestsPromise={requestsPromise} />\n </Suspense>\n </Card>\n )\n}\n\n/**\n * The view the request-access card is currently showing.\n *\n * @public\n */\nexport type RequestAccessView = 'form' | 'sent' | 'pending' | 'blocked' | 'sso-enforced'\n\ntype ViewState =\n | {view: 'form'; expired: boolean}\n | {view: 'sent'}\n | {view: 'pending'}\n | {view: 'blocked'; title: ReactNode; message: ReactNode}\n | {view: 'sso-enforced'; redirectUrl?: string}\n\nfunction deriveViewState(options: {\n fetchedRequests: AccessRequest[] | null\n resourceId: string\n submitResult: SubmitAccessRequestResult | null\n labels: RequestAccessLabels\n}): ViewState {\n const {fetchedRequests, resourceId, submitResult, labels} = options\n\n if (submitResult) {\n switch (submitResult.type) {\n case 'submitted':\n return {view: 'sent'}\n case 'sso-enforced':\n return {view: 'sso-enforced', redirectUrl: submitResult.redirectUrl}\n case 'denied':\n return {\n view: 'blocked',\n title: labels.errorTitle,\n message: labels.deniedMessage({message: submitResult.message}),\n }\n case 'over-limit':\n return {\n view: 'blocked',\n title: labels.errorTitle,\n message: labels.overLimitMessage({message: submitResult.message}),\n }\n case 'email-domain-blocked':\n case 'requests-disabled':\n return {view: 'blocked', title: labels.errorTitle, message: submitResult.message}\n case 'error':\n // Fall through to the fetched state; the form stays up with an inline error.\n break\n default:\n }\n }\n\n const state = deriveAccessRequestState(fetchedRequests, resourceId)\n if (state === 'pending') return {view: 'pending'}\n // Derived from prefetch: the user hasn't submitted anything this session,\n // so the title must describe the prior decline, not a failed send.\n if (state === 'denied') {\n return {view: 'blocked', title: labels.deniedTitle, message: labels.deniedMessage({})}\n }\n return {view: 'form', expired: state === 'expired'}\n}\n\nfunction RequestAccessFormContent(\n props: RequestAccessFormProps & {\n requestsPromise: Promise<AccessRequest[] | null>\n },\n) {\n const {\n client,\n resourceType = 'project',\n resourceId,\n currentUser,\n onSignOut,\n onRequestSubmitted,\n preview,\n renderAction,\n requestsPromise,\n } = props\n\n const labels = {...defaultLabels, ...props.labels}\n const fetchedRequests = use(requestsPromise)\n const titleId = useId()\n\n const [note, setNote] = useState('')\n const [submitResult, setSubmitResult] = useState<SubmitAccessRequestResult | null>(null)\n const [isSubmitting, startSubmit] = useTransition()\n\n const state = deriveViewState({\n fetchedRequests,\n resourceId,\n submitResult,\n labels,\n })\n const providerTitle = getProviderTitle(currentUser?.provider)\n const submitFailed = submitResult?.type === 'error'\n\n const heading: Record<\n Exclude<ViewState['view'], 'blocked'>,\n {title: ReactNode; description: ReactNode | null}\n > = {\n 'form': {\n title: labels.title,\n description: labels.describeNoAccess({email: currentUser?.email}),\n },\n 'sent': {title: labels.sentTitle, description: labels.sentDescription},\n 'pending': {title: labels.sentTitle, description: labels.pendingMessage},\n 'sso-enforced': {title: labels.errorTitle, description: null},\n }\n const {title, description} =\n state.view === 'blocked' ? {title: state.title, description: null} : heading[state.view]\n\n const handleSubmit = (event: SubmitEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (isSubmitting) return\n startSubmit(async () => {\n const trimmedNote = note.trim() || undefined\n const result = await submitAccessRequest({\n client,\n resourceType,\n resourceId,\n note: trimmedNote,\n requestUrl: getRequestUrl(),\n })\n setSubmitResult(result)\n if (result.type === 'submitted') onRequestSubmitted?.({note: trimmedNote})\n })\n }\n\n return (\n <Flex direction=\"column\" height=\"fill\">\n <Flex direction=\"column\" flex={1} gap={4} padding={4}>\n {preview ? (\n <Flex justify=\"center\" padding={2}>\n {preview}\n </Flex>\n ) : null}\n\n <Text as=\"h1\" id={titleId} size={2} weight=\"semibold\">\n {title}\n </Text>\n\n {description !== null ? (\n <Text as=\"p\" muted size={1}>\n {description}\n </Text>\n ) : null}\n\n {state.view === 'blocked' ? (\n <Card border padding={3} radius={2} role=\"alert\" tone=\"caution\">\n <Text as=\"p\" muted size={1}>\n {state.message}\n </Text>\n </Card>\n ) : null}\n\n {state.view === 'sso-enforced' ? (\n <Stack gap={4}>\n <Card border padding={3} radius={2} role=\"alert\" tone=\"caution\">\n <Text as=\"p\" muted size={1}>\n {labels.ssoEnforcedMessage({providerTitle})}\n </Text>\n </Card>\n {state.redirectUrl ? (\n <Button\n as=\"a\"\n href={state.redirectUrl}\n iconRight={LaunchIcon}\n mode=\"ghost\"\n text={labels.ssoSignInCta}\n width=\"fill\"\n />\n ) : null}\n </Stack>\n ) : null}\n\n {state.view === 'form' ? (\n <Stack as=\"form\" aria-labelledby={titleId} onSubmit={handleSubmit} gap={4}>\n <Text as=\"p\" size={1}>\n {state.expired\n ? labels.expiredMessage\n : resourceType === 'organization'\n ? labels.promptOrganization\n : labels.promptProject}\n </Text>\n <Stack gap={2}>\n <TextArea\n aria-label={labels.noteAriaLabel}\n disabled={isSubmitting}\n fontSize={1}\n maxLength={MAX_ACCESS_REQUEST_NOTE_LENGTH}\n onChange={(event) => setNote(event.currentTarget.value)}\n placeholder={labels.notePlaceholder}\n rows={3}\n value={note}\n />\n <Text align=\"right\" muted size={0}>\n {`${note.length}/${MAX_ACCESS_REQUEST_NOTE_LENGTH}`}\n </Text>\n </Stack>\n {submitFailed ? (\n <Card border padding={3} radius={2} role=\"alert\" tone=\"critical\">\n <Text as=\"p\" muted size={1}>\n {labels.submitFailedMessage}\n </Text>\n </Card>\n ) : null}\n <Button\n disabled={isSubmitting}\n loading={isSubmitting}\n text={labels.submit}\n type=\"submit\"\n width=\"fill\"\n />\n </Stack>\n ) : null}\n\n {renderAction?.({view: state.view})}\n </Flex>\n\n {currentUser ? (\n <Card borderTop padding={3}>\n <Flex align=\"center\" direction=\"column\" gap={3}>\n <Flex align=\"center\" gap={2} justify=\"center\">\n <Avatar initials={getInitials(currentUser)} size={0} src={currentUser.profileImage} />\n <Box>\n <Text muted size={1} textOverflow=\"ellipsis\">\n {currentUser.email ?? currentUser.name}\n {providerTitle ? ` · ${providerTitle}` : ''}\n </Text>\n </Box>\n </Flex>\n {onSignOut ? (\n <Button\n fontSize={0}\n mode=\"bleed\"\n onClick={onSignOut}\n padding={2}\n textWeight=\"regular\"\n >\n <Text muted size={1}>\n {labels.wrongAccount} <strong>{labels.signOut}</strong>\n </Text>\n </Button>\n ) : null}\n </Flex>\n </Card>\n ) : null}\n </Flex>\n )\n}\n\n// The URL fragment can carry auth tokens (e.g. the #token= login handoff),\n// so it must never reach the Access API's logs.\nfunction getRequestUrl(): string | undefined {\n if (typeof window === 'undefined') return undefined\n const url = new URL(window.location.href)\n url.hash = ''\n return url.toString()\n}\n\nfunction getInitials(user: AccessUser): string | undefined {\n const source = user.name ?? user.email\n if (!source) return undefined\n const parts = source.trim().split(/\\s+/)\n const initials = parts\n .slice(0, 2)\n .map((part) => part[0])\n .join('')\n return initials ? initials.toUpperCase() : undefined\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["resourceType = 'project'","() =>\n listMyAccessRequests(client).catch((): AccessRequest[] | null => null)","(): AccessRequest[] | null => null","() =>\n fetchAccessRequestStatus({\n client,\n resourceType,\n resourceId,\n origin: getRequestUrl(),\n })","<Flex align=\"center\" height=\"fill\" justify=\"center\" padding={5}>\n <Spinner muted />\n </Flex>","<Card border height=\"fill\" overflow=\"hidden\" radius={3} tone=\"default\">\n <Suspense\n fallback={\n <Flex align=\"center\" height=\"fill\" justify=\"center\" padding={5}>\n <Spinner muted />\n </Flex>\n }\n >\n <RequestAccessFormContent\n {...props}\n requestsPromise={requestsPromise}\n statusPromise={statusPromise}\n />\n </Suspense>\n </Card>","(event: SubmitEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (isSubmitting) return\n startSubmit(async () => {\n const trimmedNote = note.trim() || undefined\n const result = await submitAccessRequest({\n client,\n resourceType,\n resourceId,\n note: trimmedNote,\n requestUrl: getRequestUrl(),\n })\n setSubmitAccessRequestResult(result)\n if (result.type === 'submitted') onRequestSubmitted?.({note: trimmedNote})\n })\n }","preview ? (\n <Flex justify=\"center\" padding={2}>\n {preview}\n </Flex>\n ) : null","event"],"sources":["../src/accessRequests.ts","../src/deriveAccessRequestState.ts","../src/providerTitle.ts","../src/labels.tsx","../src/RequestAccessForm.tsx"],"sourcesContent":["import {type SanityClient} from '@sanity/client'\n\nimport {\n type AccessRequest,\n type AccessRequestEligibilityState,\n type AccessResourceType,\n type SubmitAccessRequestResult,\n} from './types'\n\n/**\n * The Access API only accepts notes up to this length.\n *\n * @public\n */\nexport const MAX_ACCESS_REQUEST_NOTE_LENGTH = 150\n\nconst ACCESS_API_VERSION = '2024-07-01'\n\n/**\n * Structured 403 code thrown by the Access API when the target organization\n * only admits members through its SSO login flow.\n */\nconst SAML_ENFORCEMENT_REQUIRED = 'saml_enforcement_required'\n\nfunction withAccessApiVersion(client: SanityClient): SanityClient {\n return client.withConfig({apiVersion: ACCESS_API_VERSION})\n}\n\n/**\n * Fetches the caller's own access requests across all resources\n * (`GET /access/requests/me`).\n *\n * @public\n */\nexport async function listMyAccessRequests(client: SanityClient): Promise<AccessRequest[]> {\n const requests = await withAccessApiVersion(client).request<AccessRequest[] | null>({\n url: '/access/requests/me',\n tag: 'access-ui.list-requests',\n })\n return requests ?? []\n}\n\n/**\n * Asks what the request-access screen should show\n * (`GET /access/{resourceType}/{resourceId}/requests/state`).\n *\n * Runs before the form is offered, so a user in a SAML-enforced organization is\n * pointed at SSO instead of writing a note no administrator can action.\n *\n * `origin` is carried opaquely to the login page so the user returns where they\n * started. Never throws: an unreachable or older API answers `eligible`,\n * leaving the form in place and the submit-time 403 as the backstop.\n *\n * @public\n */\nexport async function fetchAccessRequestStatus(options: {\n client: SanityClient\n resourceType: AccessResourceType\n resourceId: string\n origin?: string\n}): Promise<AccessRequestEligibilityState> {\n const {client, resourceType, resourceId, origin} = options\n try {\n const status = await withAccessApiVersion(client).request<AccessRequestEligibilityState | null>(\n {\n url: `/access/${resourceType}/${resourceId}/requests/state`,\n tag: 'access-ui.request-state',\n query: origin ? {returnQuery: new URLSearchParams({origin}).toString()} : undefined,\n },\n )\n return status ?? {state: 'eligible'}\n } catch {\n return {state: 'eligible'}\n }\n}\n\ninterface ErrorResponseDetails {\n statusCode?: number\n message?: string\n code?: string\n redirectUrl?: string\n}\n\nfunction getErrorResponseDetails(err: unknown): ErrorResponseDetails {\n if (typeof err !== 'object' || err === null) return {}\n const response = (err as {response?: unknown}).response\n if (typeof response !== 'object' || response === null) return {}\n const {statusCode} = response as {statusCode?: unknown}\n const body = (response as {body?: unknown}).body\n const details: ErrorResponseDetails = {\n statusCode: typeof statusCode === 'number' ? statusCode : undefined,\n }\n if (typeof body === 'object' && body !== null) {\n const {message, code, redirectUrl} = body as {\n message?: unknown\n code?: unknown\n redirectUrl?: unknown\n }\n details.message = typeof message === 'string' ? message : undefined\n details.code = typeof code === 'string' ? code : undefined\n details.redirectUrl = typeof redirectUrl === 'string' ? redirectUrl : undefined\n }\n return details\n}\n\nfunction mapSubmitError(err: unknown): SubmitAccessRequestResult {\n const {statusCode, message, code, redirectUrl} = getErrorResponseDetails(err)\n\n if (statusCode === 403 && code === SAML_ENFORCEMENT_REQUIRED) {\n return {type: 'sso-enforced', redirectUrl, message}\n }\n if (statusCode === 429) {\n return {type: 'over-limit', message}\n }\n if (statusCode === 409) {\n if (message?.includes('email domain')) return {type: 'email-domain-blocked', message}\n if (message?.includes('disabled for organization')) return {type: 'requests-disabled', message}\n return {type: 'denied', message: message?.replace(/^Conflict -\\s*/, '')}\n }\n return {type: 'error', error: err}\n}\n\n/**\n * Submits an access request (`POST /access/{resourceType}/{resourceId}/requests`)\n * and maps the Access API's error contract to a {@link SubmitAccessRequestResult}.\n * Never throws for API rejections; unexpected failures come back as\n * `{type: 'error'}` so callers decide how to surface them.\n *\n * @public\n */\nexport async function submitAccessRequest(options: {\n client: SanityClient\n resourceType: AccessResourceType\n resourceId: string\n note?: string\n requestUrl?: string\n}): Promise<SubmitAccessRequestResult> {\n const {client, resourceType, resourceId, note, requestUrl} = options\n try {\n const request = await withAccessApiVersion(client).request<AccessRequest | null>({\n url: `/access/${resourceType}/${resourceId}/requests`,\n method: 'post',\n tag: 'access-ui.submit-request',\n body: {note, requestUrl, type: 'access'},\n })\n return {type: 'submitted', request}\n } catch (err) {\n return mapSubmitError(err)\n }\n}\n","import {type AccessRequest, type AccessRequestState} from './types'\n\n/**\n * Access requests are considered active for two weeks, matching the Access\n * API's request lifetime.\n */\nconst REQUEST_LIFETIME_MS = 14 * 24 * 60 * 60 * 1000\n\n/**\n * Derives where the caller stands on requesting access to a resource from\n * their existing access requests.\n *\n * A declined request blocks re-requesting for two weeks. A pending request\n * younger than two weeks is in review; older pending requests count as\n * expired, and the caller may request again.\n *\n * @public\n */\nexport function deriveAccessRequestState(\n requests: AccessRequest[] | null | undefined,\n resourceId: string,\n now: number = Date.now(),\n): AccessRequestState {\n if (!requests || requests.length === 0) return 'none'\n\n const isRecent = (request: AccessRequest) =>\n now - new Date(request.createdAt).getTime() < REQUEST_LIFETIME_MS\n\n const forResource = requests.filter((request) => request.resourceId === resourceId)\n\n if (forResource.some((request) => request.status === 'declined' && isRecent(request))) {\n return 'denied'\n }\n if (forResource.some((request) => request.status === 'pending' && isRecent(request))) {\n return 'pending'\n }\n if (forResource.some((request) => request.status === 'pending')) {\n return 'expired'\n }\n return 'none'\n}\n","/**\n * Human-readable title for a login provider id, e.g. `google` → `Google`,\n * `saml-xyz` → `SAML/SSO`.\n *\n * @public\n */\nexport function getProviderTitle(provider?: string): string | undefined {\n if (provider === 'google') return 'Google'\n if (provider === 'github') return 'GitHub'\n if (provider === 'sanity') return 'Sanity'\n if (provider === 'vercel') return 'Vercel'\n if (provider?.startsWith('saml-')) return 'SAML/SSO'\n return undefined\n}\n","import {type ReactNode} from 'react'\n\n/**\n * All user-facing strings in the request-access screen. Every label can be\n * overridden, so hosts with their own i18n stack (studio i18n, react-i18next)\n * inject translated copy while standalone hosts get the English defaults.\n *\n * @public\n */\nexport interface RequestAccessLabels {\n title: ReactNode\n sentTitle: ReactNode\n deniedTitle: ReactNode\n errorTitle: ReactNode\n describeNoAccess: (context: {email?: string}) => ReactNode\n promptProject: ReactNode\n promptOrganization: ReactNode\n notePlaceholder: string\n noteAriaLabel: string\n submit: ReactNode\n sentDescription: ReactNode\n pendingMessage: ReactNode\n deniedMessage: (context: {message?: string}) => ReactNode\n overLimitMessage: (context: {message?: string}) => ReactNode\n expiredMessage: ReactNode\n ssoEnforcedTitle: ReactNode\n ssoEnforcedMessage: (context: {providerTitle?: string}) => ReactNode\n ssoSignInCta: ReactNode\n resourceNotAvailableTitle: ReactNode\n resourceNotAvailableMessage: ReactNode\n submitFailedMessage: ReactNode\n wrongAccount: ReactNode\n signOut: ReactNode\n}\n\n/** @internal */\nexport const defaultLabels: RequestAccessLabels = {\n title: 'Request access',\n sentTitle: 'Access request sent',\n deniedTitle: 'Access request declined',\n errorTitle: 'Access request couldn’t be sent',\n describeNoAccess: ({email}) =>\n email ? (\n <>\n Your account <strong>({email})</strong> doesn’t have access to this content.\n </>\n ) : (\n <>Your account doesn’t have access to this content.</>\n ),\n promptProject: 'Send a request to the project admin(s).',\n promptOrganization: 'Send a request to the organization admin(s).',\n notePlaceholder: 'Message (optional)',\n noteAriaLabel: 'Message',\n submit: 'Request access',\n sentDescription:\n 'Your request has been sent. You will receive a notification if access is approved.',\n pendingMessage: 'Your request to access this content is pending approval.',\n deniedMessage: ({message}) => message ?? 'Your request to access this content has been declined.',\n overLimitMessage: ({message}) =>\n message ??\n 'You’ve reached the limit for access requests across all projects. Please wait before submitting more requests, or contact an admin.',\n expiredMessage: 'Your previous request has expired. You may request access again below.',\n // Reached both before and after a submit, so it must not claim a send failed.\n ssoEnforcedTitle: 'Sign in with SSO required',\n ssoEnforcedMessage: ({providerTitle}) =>\n providerTitle ? (\n <>\n You’re signed in with <strong>{providerTitle}</strong>, but this organization requires\n signing in with SSO. Access can’t be requested with this account.\n </>\n ) : (\n <>\n This organization requires signing in with SSO. Access can’t be requested with this account.\n </>\n ),\n ssoSignInCta: 'Sign in with SSO',\n resourceNotAvailableTitle: 'Access can’t be requested',\n resourceNotAvailableMessage: 'The resource currently being requested is no longer available.',\n submitFailedMessage: 'There was a problem submitting your request. Please try again.',\n wrongAccount: 'Wrong account?',\n signOut: 'Sign out',\n}\n","import {type SanityClient} from '@sanity/client'\nimport {LaunchIcon} from '@sanity/icons/Launch'\nimport {Avatar, Button, Card, Flex, Spinner, Stack, Text, TextArea} from '@sanity/ui'\nimport {\n type ReactNode,\n type SubmitEvent,\n Suspense,\n use,\n useId,\n useState,\n useTransition,\n} from 'react'\nimport {Box} from 'ui5'\n\nimport {\n fetchAccessRequestStatus,\n listMyAccessRequests,\n MAX_ACCESS_REQUEST_NOTE_LENGTH,\n submitAccessRequest,\n} from './accessRequests'\nimport {deriveAccessRequestState} from './deriveAccessRequestState'\nimport {defaultLabels, type RequestAccessLabels} from './labels'\nimport {getProviderTitle} from './providerTitle'\nimport {\n type AccessRequest,\n type AccessRequestEligibilityState,\n type AccessResourceType,\n type AccessUser,\n type SubmitAccessRequestResult,\n} from './types'\n\n/** @public */\nexport interface RequestAccessFormProps {\n /** Client authenticated as the requesting user. The Access API version is applied internally. */\n client: SanityClient\n resourceType?: AccessResourceType\n /** Project or organization id to request access to. */\n resourceId: string\n /** The signed-in user, rendered in the description and account footer. */\n currentUser?: AccessUser | null\n /**\n * Called when the user chooses \"Sign out\". The account footer's sign-out\n * action is only rendered when provided; hosts own the actual sign-out\n * mechanism (studio: `auth.logout()`, dashboard: logout route navigation).\n */\n onSignOut?: () => void\n /** Called after a request is successfully submitted, e.g. for analytics. */\n onRequestSubmitted?: (details: {note?: string}) => void\n /** Optional slot rendered above the title, e.g. a resource preview. */\n preview?: ReactNode\n /**\n * Renders an optional action area at the bottom of the card's content, e.g.\n * a navigation CTA. Called with the current view so the action can differ\n * per state (or be omitted for some); return null to render nothing.\n */\n renderAction?: (context: {view: RequestAccessView}) => ReactNode\n /** Label overrides for hosts with their own i18n stack. */\n labels?: Partial<RequestAccessLabels>\n}\n\n/**\n * The shared request-access screen: explains that the signed-in account lacks\n * access, lets the user request it with an optional note, and reflects the\n * request lifecycle (pending, denied, expired, over-limit, SSO-enforced).\n *\n * Fetches the caller's existing requests on mount and suspends while loading;\n * an internal `Suspense` boundary renders a spinner, so hosts can mount it\n * directly. Remount with a `key` when `client` or `resourceId` change.\n *\n * @public\n */\nexport function RequestAccessForm(props: RequestAccessFormProps) {\n const {client, resourceType = 'project', resourceId} = props\n\n // Created once (lazy init): recreating the promise per render would refetch\n // and re-suspend forever. Callers remount with `key` to reset.\n const [requestsPromise] = useState(() =>\n listMyAccessRequests(client).catch((): AccessRequest[] | null => null),\n )\n const [statusPromise] = useState(() =>\n fetchAccessRequestStatus({\n client,\n resourceType,\n resourceId,\n origin: getRequestUrl(),\n }),\n )\n\n return (\n <Card border height=\"fill\" overflow=\"hidden\" radius={3} tone=\"default\">\n <Suspense\n fallback={\n <Flex align=\"center\" height=\"fill\" justify=\"center\" padding={5}>\n <Spinner muted />\n </Flex>\n }\n >\n <RequestAccessFormContent\n {...props}\n requestsPromise={requestsPromise}\n statusPromise={statusPromise}\n />\n </Suspense>\n </Card>\n )\n}\n\n/**\n * The view the request-access card is currently showing.\n *\n * @public\n */\nexport type RequestAccessView = 'form' | 'sent' | 'pending' | 'blocked' | 'sso-enforced'\n\n/**\n * Everything the card renders, copy included: the same state that picks a view\n * is the only thing that knows which words that view needs. Keying copy off\n * `view` alone cannot work, because `blocked` covers a prior decline, a gone\n * resource and four submit failures, each with its own copy.\n */\ntype ViewState = {title: ReactNode; description: ReactNode | null} & (\n | {view: 'form'; expired: boolean}\n | {view: 'sent'}\n | {view: 'pending'}\n | {view: 'blocked'; message: ReactNode}\n | {view: 'sso-enforced'; message: ReactNode; redirectUrl?: string}\n)\n\n/**\n * The server's verdict. `null` hands the decision back to the caller's own\n * request history, which resolves the states this endpoint does not.\n */\nfunction deriveServerViewState(\n status: AccessRequestEligibilityState,\n labels: RequestAccessLabels,\n providerTitle?: string,\n): ViewState | null {\n switch (status.state) {\n case 'saml-required':\n return ssoEnforcedState({labels, providerTitle, redirectUrl: status.redirectUrl})\n case 'resource-not-available':\n // Nothing was submitted, so the submit-failure copy would misdescribe it.\n return {\n view: 'blocked',\n title: labels.resourceNotAvailableTitle,\n description: null,\n message: labels.resourceNotAvailableMessage,\n }\n case 'eligible':\n return null\n default:\n return null\n }\n}\n\n// Reached on mount and after a submit 403, so the title claims neither.\nfunction ssoEnforcedState(options: {\n labels: RequestAccessLabels\n providerTitle?: string\n redirectUrl?: string\n}): ViewState {\n const {labels, providerTitle, redirectUrl} = options\n return {\n view: 'sso-enforced',\n title: labels.ssoEnforcedTitle,\n description: null,\n message: labels.ssoEnforcedMessage({providerTitle}),\n redirectUrl,\n }\n}\n\nfunction deriveViewState(options: {\n accessRequestsHistory: AccessRequest[] | null\n accessRequestEligibilityState: AccessRequestEligibilityState\n resourceId: string\n submitAccessRequestResult: SubmitAccessRequestResult | null\n currentUser?: AccessUser | null\n providerTitle?: string\n labels: RequestAccessLabels\n}): ViewState {\n const {\n accessRequestsHistory,\n accessRequestEligibilityState,\n resourceId,\n submitAccessRequestResult,\n currentUser,\n providerTitle,\n labels,\n } = options\n const submitFailure = (message: ReactNode): ViewState => ({\n view: 'blocked',\n title: labels.errorTitle,\n description: null,\n message,\n })\n\n if (submitAccessRequestResult) {\n switch (submitAccessRequestResult.type) {\n case 'submitted':\n return {view: 'sent', title: labels.sentTitle, description: labels.sentDescription}\n case 'sso-enforced':\n return ssoEnforcedState({\n labels,\n providerTitle,\n redirectUrl: submitAccessRequestResult.redirectUrl,\n })\n case 'denied':\n return submitFailure(labels.deniedMessage({message: submitAccessRequestResult.message}))\n case 'over-limit':\n return submitFailure(labels.overLimitMessage({message: submitAccessRequestResult.message}))\n case 'email-domain-blocked':\n case 'requests-disabled':\n return submitFailure(submitAccessRequestResult.message)\n case 'error':\n // Fall through to the fetched state; the form stays up with an inline error.\n break\n default:\n }\n }\n\n // The server's verdict outranks the request history: a pending request in an\n // enforced org is already dead, so \"pending approval\" would be a false\n // promise. It answers `eligible` when it has nothing to say.\n const serverState = deriveServerViewState(accessRequestEligibilityState, labels, providerTitle)\n if (serverState) return serverState\n\n // TODO: `pending` and `denied` will be replaced by future content in `accessRequestEligibilityState`\n const state = deriveAccessRequestState(accessRequestsHistory, resourceId)\n if (state === 'pending') {\n return {view: 'pending', title: labels.sentTitle, description: labels.pendingMessage}\n }\n // Derived from prefetch: the user hasn't submitted anything this session,\n // so the title must describe the prior decline, not a failed send.\n if (state === 'denied') {\n return {\n view: 'blocked',\n title: labels.deniedTitle,\n description: null,\n message: labels.deniedMessage({}),\n }\n }\n return {\n view: 'form',\n title: labels.title,\n description: labels.describeNoAccess({email: currentUser?.email}),\n expired: state === 'expired',\n }\n}\n\nfunction RequestAccessFormContent(\n props: RequestAccessFormProps & {\n requestsPromise: Promise<AccessRequest[] | null>\n statusPromise: Promise<AccessRequestEligibilityState>\n },\n) {\n const {\n client,\n resourceType = 'project',\n resourceId,\n currentUser,\n onSignOut,\n onRequestSubmitted,\n preview,\n renderAction,\n requestsPromise,\n statusPromise,\n } = props\n\n const labels = {...defaultLabels, ...props.labels}\n const accessRequestsHistory = use(requestsPromise)\n const accessRequestEligibilityState = use(statusPromise)\n const titleId = useId()\n\n const [note, setNote] = useState('')\n const [submitAccessRequestResult, setSubmitAccessRequestResult] =\n useState<SubmitAccessRequestResult | null>(null)\n const [isSubmitting, startSubmit] = useTransition()\n\n const providerTitle = getProviderTitle(currentUser?.provider)\n const state = deriveViewState({\n accessRequestsHistory,\n accessRequestEligibilityState,\n resourceId,\n submitAccessRequestResult,\n currentUser,\n providerTitle,\n labels,\n })\n\n const submitFailed = submitAccessRequestResult?.type === 'error'\n\n const handleSubmit = (event: SubmitEvent<HTMLFormElement>) => {\n event.preventDefault()\n if (isSubmitting) return\n startSubmit(async () => {\n const trimmedNote = note.trim() || undefined\n const result = await submitAccessRequest({\n client,\n resourceType,\n resourceId,\n note: trimmedNote,\n requestUrl: getRequestUrl(),\n })\n setSubmitAccessRequestResult(result)\n if (result.type === 'submitted') onRequestSubmitted?.({note: trimmedNote})\n })\n }\n\n return (\n <Flex direction=\"column\" height=\"fill\">\n <Flex direction=\"column\" flex={1} gap={4} padding={4}>\n {preview ? (\n <Flex justify=\"center\" padding={2}>\n {preview}\n </Flex>\n ) : null}\n\n <Text as=\"h1\" id={titleId} size={2} weight=\"semibold\">\n {state.title}\n </Text>\n\n {state.description !== null ? (\n <Text as=\"p\" muted size={1}>\n {state.description}\n </Text>\n ) : null}\n\n {state.view === 'blocked' || state.view === 'sso-enforced' ? (\n <Stack gap={4}>\n <Card border padding={3} radius={2} role=\"alert\" tone=\"caution\">\n <Text as=\"p\" muted size={1}>\n {state.message}\n </Text>\n </Card>\n {state.view === 'sso-enforced' && state.redirectUrl ? (\n <Button\n as=\"a\"\n href={state.redirectUrl}\n iconRight={LaunchIcon}\n mode=\"ghost\"\n text={labels.ssoSignInCta}\n width=\"fill\"\n />\n ) : null}\n </Stack>\n ) : null}\n\n {state.view === 'form' ? (\n <Stack as=\"form\" aria-labelledby={titleId} onSubmit={handleSubmit} gap={4}>\n <Text as=\"p\" size={1}>\n {state.expired\n ? labels.expiredMessage\n : resourceType === 'organization'\n ? labels.promptOrganization\n : labels.promptProject}\n </Text>\n <Stack gap={2}>\n <TextArea\n aria-label={labels.noteAriaLabel}\n disabled={isSubmitting}\n fontSize={1}\n maxLength={MAX_ACCESS_REQUEST_NOTE_LENGTH}\n onChange={(event) => setNote(event.currentTarget.value)}\n placeholder={labels.notePlaceholder}\n rows={3}\n value={note}\n />\n <Text align=\"right\" muted size={0}>\n {`${note.length}/${MAX_ACCESS_REQUEST_NOTE_LENGTH}`}\n </Text>\n </Stack>\n {submitFailed ? (\n <Card border padding={3} radius={2} role=\"alert\" tone=\"critical\">\n <Text as=\"p\" muted size={1}>\n {labels.submitFailedMessage}\n </Text>\n </Card>\n ) : null}\n <Button\n disabled={isSubmitting}\n loading={isSubmitting}\n text={labels.submit}\n type=\"submit\"\n width=\"fill\"\n />\n </Stack>\n ) : null}\n\n {renderAction?.({view: state.view})}\n </Flex>\n\n {currentUser ? (\n <Card borderTop padding={3}>\n <Flex align=\"center\" direction=\"column\" gap={3}>\n <Flex align=\"center\" gap={2} justify=\"center\">\n <Avatar initials={getInitials(currentUser)} size={0} src={currentUser.profileImage} />\n <Box>\n <Text muted size={1} textOverflow=\"ellipsis\">\n {currentUser.email ?? currentUser.name}\n {providerTitle ? ` · ${providerTitle}` : ''}\n </Text>\n </Box>\n </Flex>\n {onSignOut ? (\n <Button\n fontSize={0}\n mode=\"bleed\"\n onClick={onSignOut}\n padding={2}\n textWeight=\"regular\"\n >\n <Text muted size={1}>\n {labels.wrongAccount} <strong>{labels.signOut}</strong>\n </Text>\n </Button>\n ) : null}\n </Flex>\n </Card>\n ) : null}\n </Flex>\n )\n}\n\n// The URL fragment can carry auth tokens (e.g. the #token= login handoff),\n// so it must never reach the Access API's logs.\nfunction getRequestUrl(): string | undefined {\n if (typeof window === 'undefined') return undefined\n const url = new URL(window.location.href)\n url.hash = ''\n return url.toString()\n}\n\nfunction getInitials(user: AccessUser): string | undefined {\n const source = user.name ?? user.email\n if (!source) return undefined\n const parts = source.trim().split(/\\s+/)\n const initials = parts\n .slice(0, 2)\n .map((part) => part[0])\n .join('')\n return initials ? initials.toUpperCase() : undefined\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/access-ui",
3
- "version": "6.13.0-next.7",
3
+ "version": "6.13.0-next.79",
4
4
  "description": "Shared request-access screen and access-request logic for Sanity applications",
5
5
  "keywords": [
6
6
  "access",
@@ -33,9 +33,9 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@sanity/client": "^8.4.0",
36
+ "@sanity/client": "^8.5.0",
37
37
  "@sanity/icons": "^5.2.1",
38
- "@sanity/ui": "^4.0.7",
38
+ "@sanity/ui": "^4.1.0",
39
39
  "ui5": "npm:@sanity/ui@5.0.0-alpha.8"
40
40
  },
41
41
  "devDependencies": {
@@ -44,7 +44,7 @@
44
44
  "@repo/tsdown.config": "6.9.2",
45
45
  "@testing-library/jest-dom": "^7.0.1",
46
46
  "@testing-library/react": "^16.3.3",
47
- "@testing-library/user-event": "^14.6.6",
47
+ "@testing-library/user-event": "^14.6.7",
48
48
  "@types/react": "^19.2.18",
49
49
  "@vitejs/plugin-react": "^6.1.1",
50
50
  "jsdom": "^29.1.1",