@package-broker/ui 2.0.0-rc.2 → 2.0.0-rc.4

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.
Files changed (194) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/api-types.d.ts +8148 -2725
  3. package/dist/api-types.d.ts.map +1 -1
  4. package/dist/api.d.ts +1 -1
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +10 -0
  7. package/dist/api.js.map +1 -1
  8. package/dist/app-path.d.ts +2 -0
  9. package/dist/app-path.d.ts.map +1 -0
  10. package/dist/app-path.js +2 -0
  11. package/dist/app-path.js.map +1 -0
  12. package/dist/app.d.ts +10 -14
  13. package/dist/app.d.ts.map +1 -1
  14. package/dist/app.js +37 -22
  15. package/dist/app.js.map +1 -1
  16. package/dist/assets/access.62e9484517a0.js +1 -0
  17. package/dist/assets/app.45b156168495.css +2 -0
  18. package/dist/assets/app.c064670cdc0d.js +9 -0
  19. package/dist/assets/audit.b7c40b9dc998.js +1 -0
  20. package/dist/assets/chunk.28a4d8aab337.js +1 -0
  21. package/dist/assets/chunk.3fcd6998795a.js +1 -0
  22. package/dist/assets/chunk.51497edc7697.js +4 -0
  23. package/dist/assets/chunk.56d473a1587d.js +1 -0
  24. package/dist/assets/chunk.675b635d9462.js +1 -0
  25. package/dist/assets/chunk.6ea6bdcaa465.js +1 -0
  26. package/dist/assets/chunk.741aec8d1ff9.js +1 -0
  27. package/dist/assets/chunk.815f9a91c741.js +1 -0
  28. package/dist/assets/chunk.8442124f6802.js +1 -0
  29. package/dist/assets/chunk.965b76812604.js +2 -0
  30. package/dist/assets/chunk.a6d755a9789f.js +1 -0
  31. package/dist/assets/chunk.d18497920e4d.js +1 -0
  32. package/dist/assets/chunk.d813fefb410d.js +1 -0
  33. package/dist/assets/chunk.e3b0c44298fc.js +0 -0
  34. package/dist/assets/chunk.f54ea1378c8c.js +1 -0
  35. package/dist/assets/chunk.fd375c54b526.js +1 -0
  36. package/dist/assets/import.e7a1e5834631.js +1 -0
  37. package/dist/assets/index.html +9 -2
  38. package/dist/assets/manifest.json +108 -8
  39. package/dist/assets/notifications.19225fe12044.js +1 -0
  40. package/dist/assets/npm-login.e059678af13d.js +1 -0
  41. package/dist/assets/packages.ab5ab25b1c75.js +2 -0
  42. package/dist/assets/projects.927fe18c785c.js +1 -0
  43. package/dist/assets/setup.8e977608d413.js +1 -0
  44. package/dist/assets/setup.html +3 -3
  45. package/dist/assets/sources.ae50f239f80f.js +1 -0
  46. package/dist/assets/tokens.6b86d8d97e07.js +4 -0
  47. package/dist/auth.d.ts +26 -3
  48. package/dist/auth.d.ts.map +1 -1
  49. package/dist/auth.js +133 -19
  50. package/dist/auth.js.map +1 -1
  51. package/dist/components/product-footer-content.d.ts +21 -0
  52. package/dist/components/product-footer-content.d.ts.map +1 -0
  53. package/dist/components/product-footer-content.js +30 -0
  54. package/dist/components/product-footer-content.js.map +1 -0
  55. package/dist/components/product-footer.d.ts +3 -0
  56. package/dist/components/product-footer.d.ts.map +1 -0
  57. package/dist/components/product-footer.js +6 -0
  58. package/dist/components/product-footer.js.map +1 -0
  59. package/dist/components/ui/status-badge.d.ts +5 -0
  60. package/dist/components/ui/status-badge.d.ts.map +1 -1
  61. package/dist/components/ui/status-badge.js +10 -0
  62. package/dist/components/ui/status-badge.js.map +1 -1
  63. package/dist/manifest.d.ts +32 -0
  64. package/dist/manifest.d.ts.map +1 -1
  65. package/dist/manifest.js +41 -0
  66. package/dist/manifest.js.map +1 -1
  67. package/dist/package-limits.d.ts +3 -0
  68. package/dist/package-limits.d.ts.map +1 -0
  69. package/dist/package-limits.js +3 -0
  70. package/dist/package-limits.js.map +1 -0
  71. package/dist/page-hash.d.ts +1 -1
  72. package/dist/page-hash.d.ts.map +1 -1
  73. package/dist/page-hash.js.map +1 -1
  74. package/dist/page-loader.d.ts +5 -0
  75. package/dist/page-loader.d.ts.map +1 -0
  76. package/dist/page-loader.js +39 -0
  77. package/dist/page-loader.js.map +1 -0
  78. package/dist/pages/access/grant-form.d.ts +48 -0
  79. package/dist/pages/access/grant-form.d.ts.map +1 -0
  80. package/dist/pages/access/grant-form.js +153 -0
  81. package/dist/pages/access/grant-form.js.map +1 -0
  82. package/dist/pages/access/permission-descriptions.d.ts +2 -0
  83. package/dist/pages/access/permission-descriptions.d.ts.map +1 -0
  84. package/dist/pages/access/permission-descriptions.js +36 -0
  85. package/dist/pages/access/permission-descriptions.js.map +1 -0
  86. package/dist/pages/access/role-catalogue.d.ts +22 -0
  87. package/dist/pages/access/role-catalogue.d.ts.map +1 -0
  88. package/dist/pages/access/role-catalogue.js +40 -0
  89. package/dist/pages/access/role-catalogue.js.map +1 -0
  90. package/dist/pages/access.d.ts +6 -0
  91. package/dist/pages/access.d.ts.map +1 -1
  92. package/dist/pages/access.js +66 -31
  93. package/dist/pages/access.js.map +1 -1
  94. package/dist/pages/audit.d.ts.map +1 -1
  95. package/dist/pages/notification-actions.d.ts +41 -0
  96. package/dist/pages/notification-actions.d.ts.map +1 -0
  97. package/dist/pages/notification-actions.js +187 -0
  98. package/dist/pages/notification-actions.js.map +1 -0
  99. package/dist/pages/notification-draft.d.ts +13 -0
  100. package/dist/pages/notification-draft.d.ts.map +1 -0
  101. package/dist/pages/notification-draft.js +75 -0
  102. package/dist/pages/notification-draft.js.map +1 -0
  103. package/dist/pages/notification-form.d.ts +35 -0
  104. package/dist/pages/notification-form.d.ts.map +1 -0
  105. package/dist/pages/notification-form.js +167 -0
  106. package/dist/pages/notification-form.js.map +1 -0
  107. package/dist/pages/notifications-controller.d.ts +41 -0
  108. package/dist/pages/notifications-controller.d.ts.map +1 -0
  109. package/dist/pages/notifications-controller.js +184 -0
  110. package/dist/pages/notifications-controller.js.map +1 -0
  111. package/dist/pages/notifications.d.ts +10 -0
  112. package/dist/pages/notifications.d.ts.map +1 -0
  113. package/dist/pages/notifications.js +301 -0
  114. package/dist/pages/notifications.js.map +1 -0
  115. package/dist/pages/npm-login.d.ts +2 -0
  116. package/dist/pages/npm-login.d.ts.map +1 -0
  117. package/dist/pages/npm-login.js +62 -0
  118. package/dist/pages/npm-login.js.map +1 -0
  119. package/dist/pages/packages/npm-workflow.d.ts +31 -0
  120. package/dist/pages/packages/npm-workflow.d.ts.map +1 -0
  121. package/dist/pages/packages/npm-workflow.js +381 -0
  122. package/dist/pages/packages/npm-workflow.js.map +1 -0
  123. package/dist/pages/packages/package-detail.d.ts +9 -0
  124. package/dist/pages/packages/package-detail.d.ts.map +1 -0
  125. package/dist/pages/packages/package-detail.js +242 -0
  126. package/dist/pages/packages/package-detail.js.map +1 -0
  127. package/dist/pages/packages/source-actions.d.ts +49 -0
  128. package/dist/pages/packages/source-actions.d.ts.map +1 -0
  129. package/dist/pages/packages/source-actions.js +319 -0
  130. package/dist/pages/packages/source-actions.js.map +1 -0
  131. package/dist/pages/packages.d.ts +15 -4
  132. package/dist/pages/packages.d.ts.map +1 -1
  133. package/dist/pages/packages.js +138 -20
  134. package/dist/pages/packages.js.map +1 -1
  135. package/dist/pages/projects/project-form-model.d.ts +25 -0
  136. package/dist/pages/projects/project-form-model.d.ts.map +1 -0
  137. package/dist/pages/projects/project-form-model.js +69 -0
  138. package/dist/pages/projects/project-form-model.js.map +1 -0
  139. package/dist/pages/projects/project-form.d.ts +14 -0
  140. package/dist/pages/projects/project-form.d.ts.map +1 -0
  141. package/dist/pages/projects/project-form.js +91 -0
  142. package/dist/pages/projects/project-form.js.map +1 -0
  143. package/dist/pages/projects.d.ts +3 -2
  144. package/dist/pages/projects.d.ts.map +1 -1
  145. package/dist/pages/projects.js +29 -38
  146. package/dist/pages/projects.js.map +1 -1
  147. package/dist/pages/sources/source-form.d.ts +5 -2
  148. package/dist/pages/sources/source-form.d.ts.map +1 -1
  149. package/dist/pages/sources/source-form.js +39 -27
  150. package/dist/pages/sources/source-form.js.map +1 -1
  151. package/dist/pages/sources/sync-copy.d.ts +5 -0
  152. package/dist/pages/sources/sync-copy.d.ts.map +1 -0
  153. package/dist/pages/sources/sync-copy.js +11 -0
  154. package/dist/pages/sources/sync-copy.js.map +1 -0
  155. package/dist/pages/sources.d.ts +11 -3
  156. package/dist/pages/sources.d.ts.map +1 -1
  157. package/dist/pages/sources.js +103 -25
  158. package/dist/pages/sources.js.map +1 -1
  159. package/dist/pages/tokens/client-config.d.ts +3 -0
  160. package/dist/pages/tokens/client-config.d.ts.map +1 -1
  161. package/dist/pages/tokens/client-config.js +12 -2
  162. package/dist/pages/tokens/client-config.js.map +1 -1
  163. package/dist/pages/tokens/issue-token-form.d.ts +27 -0
  164. package/dist/pages/tokens/issue-token-form.d.ts.map +1 -0
  165. package/dist/pages/tokens/issue-token-form.js +270 -0
  166. package/dist/pages/tokens/issue-token-form.js.map +1 -0
  167. package/dist/pages/tokens/principal-form.d.ts +8 -0
  168. package/dist/pages/tokens/principal-form.d.ts.map +1 -0
  169. package/dist/pages/tokens/principal-form.js +155 -0
  170. package/dist/pages/tokens/principal-form.js.map +1 -0
  171. package/dist/pages/tokens/trusted-publishers-controller.d.ts +42 -0
  172. package/dist/pages/tokens/trusted-publishers-controller.d.ts.map +1 -0
  173. package/dist/pages/tokens/trusted-publishers-controller.js +202 -0
  174. package/dist/pages/tokens/trusted-publishers-controller.js.map +1 -0
  175. package/dist/pages/tokens/trusted-publishers.d.ts +6 -0
  176. package/dist/pages/tokens/trusted-publishers.d.ts.map +1 -0
  177. package/dist/pages/tokens/trusted-publishers.js +116 -0
  178. package/dist/pages/tokens/trusted-publishers.js.map +1 -0
  179. package/dist/pages/tokens.d.ts +100 -6
  180. package/dist/pages/tokens.d.ts.map +1 -1
  181. package/dist/pages/tokens.js +324 -86
  182. package/dist/pages/tokens.js.map +1 -1
  183. package/dist/setup.d.ts +3 -2
  184. package/dist/setup.d.ts.map +1 -1
  185. package/dist/setup.js +70 -10
  186. package/dist/setup.js.map +1 -1
  187. package/dist/token-pending.d.ts +4 -0
  188. package/dist/token-pending.d.ts.map +1 -0
  189. package/dist/token-pending.js +12 -0
  190. package/dist/token-pending.js.map +1 -0
  191. package/package.json +3 -3
  192. package/dist/assets/app.b2abf3c46f6e.js +0 -15
  193. package/dist/assets/app.e8cea8b32d3a.css +0 -2
  194. package/dist/assets/setup.98343039898f.js +0 -1
@@ -1,4 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createTrustedPublishersController } from './tokens/trusted-publishers-controller.js';
3
+ import { TrustedPublishers } from './tokens/trusted-publishers.js';
4
+ import { pendingStorage } from '../token-pending.js';
2
5
  import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
3
6
  import { useServices } from '../auth.js';
4
7
  import { loadPending, savePending } from '../pending.js';
@@ -15,7 +18,10 @@ import { Badge } from '../components/ui/badge.js';
15
18
  import { LoadedListFilters } from '../components/ui/toolbar.js';
16
19
  import { ListState } from '../components/ui/list-state.js';
17
20
  import { ConfirmAction } from '../components/ui/confirm-action.js';
21
+ import { IssueTokenForm, accessLabels } from './tokens/issue-token-form.js';
22
+ import { PrincipalForm } from './tokens/principal-form.js';
18
23
  import { tokenClientConfig } from './tokens/client-config.js';
24
+ export const dayLabel = (days) => `${days} ${days === 1 ? 'day' : 'days'}`;
19
25
  export function lifetimeChoices(policy) {
20
26
  return [...new Set([30, 90, 180, 365, policy.maxDays, policy.defaultDays])]
21
27
  .filter((days) => days > 0 && days <= policy.maxDays)
@@ -27,6 +33,7 @@ export function tokensReducer(state, action) {
27
33
  ...state,
28
34
  secret: null,
29
35
  secretProject: null,
36
+ result: null,
30
37
  issuedId: null,
31
38
  oldPersonalId: null,
32
39
  pending: null,
@@ -36,16 +43,6 @@ export function tokensReducer(state, action) {
36
43
  };
37
44
  return state;
38
45
  }
39
- function pendingStorage(storage) {
40
- return {
41
- getItem: (key) => storage.getItem(`tokens:${key}`),
42
- setItem: (key, value) => storage.setItem(`tokens:${key}`, value),
43
- removeItem: (key) => storage.removeItem(`tokens:${key}`),
44
- };
45
- }
46
- export function clearPendingTokenIssue(storage) {
47
- loadPending(pendingStorage(storage), Date.now());
48
- }
49
46
  function record(value) {
50
47
  return value !== null && typeof value === 'object' && !Array.isArray(value);
51
48
  }
@@ -72,6 +69,13 @@ function restoredIssue(value) {
72
69
  target: value.target,
73
70
  project: value.project,
74
71
  key: value.key,
72
+ ...(value.refresh === true ? { refresh: true } : {}),
73
+ ...(['personal', 'project', 'organization'].includes(String(value.listTarget))
74
+ ? {
75
+ listTarget: value.listTarget,
76
+ listProject: typeof value.listProject === 'string' ? value.listProject : null,
77
+ }
78
+ : {}),
75
79
  ...(typeof value.rotate === 'string' ? { rotate: value.rotate } : {}),
76
80
  ...(typeof value.replace === 'string' ? { replace: value.replace } : {}),
77
81
  draft: {
@@ -105,17 +109,48 @@ function publicToken(t) {
105
109
  revokedAt: t.revokedAt,
106
110
  };
107
111
  }
112
+ export function inResultScope(result, project) {
113
+ if (!result)
114
+ return true;
115
+ if (result.target === 'project')
116
+ return project.slug === result.project;
117
+ if (result.target === 'personal')
118
+ return result.draft.projectMode !== 'selected' || result.draft.projectIds.includes(project.id);
119
+ return true;
120
+ }
121
+ export function hasSetupAccess(state, project) {
122
+ const result = state.result;
123
+ if (!result)
124
+ return false;
125
+ const permissions = result.target === 'personal'
126
+ ? project.permissions
127
+ : state.principals.find((p) => p.id === result.draft.servicePrincipalId)?.permissions;
128
+ return (permissions?.includes('project:packages:read') === true ||
129
+ (result.draft.scope !== 'read' &&
130
+ project.cap !== 'read' &&
131
+ permissions?.includes('project:packages:write') === true));
132
+ }
108
133
  export function createTokensController(services, deps) {
109
134
  let state = {
110
135
  target: 'personal',
111
136
  project: null,
137
+ issuanceProject: null,
112
138
  items: [],
113
139
  next: null,
114
140
  principals: [],
115
141
  principalsNext: null,
116
142
  principalsRestricted: false,
143
+ principalsLoading: true,
144
+ principalsError: false,
145
+ organizationPermissions: [],
146
+ issuanceDraft: null,
147
+ issuancePrincipal: null,
148
+ issuanceNonce: 0,
117
149
  names: {},
118
150
  projectSlugs: {},
151
+ projects: [],
152
+ projectsError: false,
153
+ result: null,
119
154
  lifetime: null,
120
155
  userId: null,
121
156
  secret: null,
@@ -132,6 +167,8 @@ export function createTokensController(services, deps) {
132
167
  let epoch = 0;
133
168
  let listing = 0;
134
169
  let navigation = 0;
170
+ const policyQueue = [];
171
+ let policyWorkers = 0;
135
172
  const listeners = new Set();
136
173
  const set = (next) => {
137
174
  state = { ...state, ...next };
@@ -157,6 +194,7 @@ export function createTokensController(services, deps) {
157
194
  busy: true,
158
195
  secret: null,
159
196
  secretProject: null,
197
+ result: null,
160
198
  issuedId: null,
161
199
  oldPersonalId: null,
162
200
  pending: null,
@@ -203,10 +241,15 @@ export function createTokensController(services, deps) {
203
241
  if (result.ok) {
204
242
  set({
205
243
  secret: result.data.token,
206
- secretProject: target === 'project' ? project : null,
244
+ secretProject: target === 'project'
245
+ ? project
246
+ : d.projectMode === 'selected' && d.projectIds.length === 1
247
+ ? (state.projectSlugs[d.projectIds[0]] ?? null)
248
+ : null,
249
+ result: action,
207
250
  oldPersonalId: action.replace ?? null,
208
251
  issuedId: result.data.id,
209
- notice: 'Save this token now; it is shown once.',
252
+ notice: 'Token created.',
210
253
  });
211
254
  }
212
255
  else if (result.code === 'reauth_required') {
@@ -233,7 +276,11 @@ export function createTokensController(services, deps) {
233
276
  failure(result);
234
277
  }
235
278
  const controller = {
279
+ trustedPublishers: createTrustedPublishersController(services),
236
280
  snapshot: () => state,
281
+ announce(notice) {
282
+ set({ notice });
283
+ },
237
284
  subscribe(listener) {
238
285
  listeners.add(listener);
239
286
  return () => {
@@ -241,10 +288,16 @@ export function createTokensController(services, deps) {
241
288
  };
242
289
  },
243
290
  navigate() {
291
+ controller.trustedPublishers.clear();
244
292
  epoch++;
245
293
  navigation++;
246
294
  listing++;
247
- set(tokensReducer(state, { type: 'navigate' }));
295
+ policyQueue.length = 0;
296
+ set({
297
+ ...tokensReducer(state, { type: 'navigate' }),
298
+ principalsLoading: false,
299
+ projects: state.projects.map((p) => p.policyState === 'loading' ? { ...p, policyState: 'idle' } : p),
300
+ });
248
301
  },
249
302
  dispose() {
250
303
  controller.navigate();
@@ -254,24 +307,33 @@ export function createTokensController(services, deps) {
254
307
  if (state.busy)
255
308
  return;
256
309
  epoch++;
257
- set({ secret: null, issuedId: null, oldPersonalId: null, notice: null, renewal: null });
310
+ set({
311
+ secret: null,
312
+ secretProject: null,
313
+ result: null,
314
+ issuedId: null,
315
+ oldPersonalId: null,
316
+ notice: null,
317
+ renewal: null,
318
+ issuanceDraft: state.pending === null ? null : state.issuanceDraft,
319
+ });
258
320
  },
259
321
  restore() {
260
322
  const loaded = loadPending(storage, deps.now());
261
323
  const action = loaded.ok ? restoredIssue(loaded.action?.body) : null;
262
324
  if (action !== null)
263
- set({ target: action.target, project: action.project, pending: action, secret: null });
325
+ set({ pending: action, issuanceDraft: action.refresh ? action : null, secret: null });
264
326
  },
265
327
  async start(target, project) {
328
+ set({ issuanceProject: project });
266
329
  controller.restore();
267
330
  const action = state.pending;
268
- await controller.load(action?.target ?? target, action === null ? project : action.project);
331
+ await controller.load(action?.listTarget ?? action?.target ?? target, action ? (action.listTarget ? (action.listProject ?? null) : action.project) : project);
269
332
  },
270
333
  async selectTarget(target, project) {
271
334
  if (state.busy)
272
335
  return;
273
- epoch++;
274
- set(tokensReducer(state, { type: 'navigate' }));
336
+ set({ pending: null, renewal: null });
275
337
  await controller.load(target, project);
276
338
  },
277
339
  decline() {
@@ -282,11 +344,40 @@ export function createTokensController(services, deps) {
282
344
  if (action === null || state.busy || state.loading)
283
345
  return;
284
346
  if (state.lifetime === null) {
285
- await controller.load(action.target, action.project);
347
+ await controller.load();
286
348
  if (state.pending !== action || state.lifetime === null || state.loading)
287
349
  return;
288
350
  }
289
351
  await send(action);
352
+ if (action.refresh && state.secret !== null)
353
+ await controller.load();
354
+ },
355
+ async issueFor(target, project, draft) {
356
+ const action = {
357
+ target,
358
+ project,
359
+ draft,
360
+ key: deps.newKey(),
361
+ refresh: true,
362
+ listTarget: state.target,
363
+ listProject: state.project,
364
+ };
365
+ set({ issuanceDraft: action });
366
+ await send(action);
367
+ if (state.secret !== null)
368
+ await controller.load();
369
+ },
370
+ configureProject(slug) {
371
+ const result = state.result;
372
+ const project = state.projects.find((p) => p.slug === slug);
373
+ if (!project ||
374
+ !result ||
375
+ (result.target === 'project' && result.project !== slug) ||
376
+ (result.target === 'personal' &&
377
+ result.draft.projectMode === 'selected' &&
378
+ !result.draft.projectIds.includes(project.id)))
379
+ return;
380
+ set({ secretProject: slug });
290
381
  },
291
382
  async issue(draft) {
292
383
  await send({
@@ -316,6 +407,9 @@ export function createTokensController(services, deps) {
316
407
  }
317
408
  set({ renewal: token, secret: null, issuedId: null, oldPersonalId: null, notice: null });
318
409
  },
410
+ invalidProjects() {
411
+ set({ notice: 'Choose between 1 and 200 projects.' });
412
+ },
319
413
  registryOrigin: () => new URL(deps.origin).origin,
320
414
  clientConfig() {
321
415
  return state.secret === null || state.secretProject === null
@@ -338,7 +432,9 @@ export function createTokensController(services, deps) {
338
432
  catch {
339
433
  if (version === epoch)
340
434
  set({
341
- notice: kind === undefined ? 'Unable to copy token.' : 'Unable to copy configuration.',
435
+ notice: kind === undefined
436
+ ? 'Could not copy. Select and copy the token manually.'
437
+ : 'Could not copy. Select and copy the configuration manually.',
342
438
  });
343
439
  }
344
440
  },
@@ -349,7 +445,7 @@ export function createTokensController(services, deps) {
349
445
  project,
350
446
  loading: true,
351
447
  listError: false,
352
- notice: null,
448
+ notice: state.secret === null ? null : state.notice,
353
449
  items: [],
354
450
  next: null,
355
451
  lifetime: null,
@@ -375,36 +471,106 @@ export function createTokensController(services, deps) {
375
471
  },
376
472
  async loadPrincipals(after) {
377
473
  const version = navigation;
378
- const result = await services.client.request('get', '/_pb/service-principals', {
379
- query: { after },
474
+ set({ principalsLoading: true, principalsError: false });
475
+ const principals = [];
476
+ let cursor = after;
477
+ do {
478
+ const result = await services.client.request('get', '/_pb/service-principals', {
479
+ query: { after: cursor },
480
+ });
481
+ if (version !== navigation)
482
+ return;
483
+ if (!result.ok) {
484
+ set({
485
+ principalsLoading: false,
486
+ principalsError: result.status !== 403,
487
+ principalsRestricted: result.status === 403,
488
+ principals: [],
489
+ principalsNext: null,
490
+ });
491
+ return;
492
+ }
493
+ principals.push(...result.data.items.map((p) => ({
494
+ id: p.id,
495
+ name: p.name,
496
+ projectId: p.projectId,
497
+ status: p.status,
498
+ revision: p.revision,
499
+ createdByUserId: p.createdByUserId,
500
+ permissions: p.permissions,
501
+ })));
502
+ cursor = result.data.next ?? undefined;
503
+ } while (cursor !== undefined);
504
+ set({
505
+ principals,
506
+ principalsNext: null,
507
+ principalsRestricted: false,
508
+ principalsLoading: false,
380
509
  });
381
- if (version !== navigation)
382
- return;
383
- if (result.ok)
510
+ },
511
+ loadProjectPolicies(ids, retry = false) {
512
+ const marked = new Set();
513
+ for (const id of ids) {
514
+ const project = state.projects.find((p) => p.id === id);
515
+ if (!project ||
516
+ project.policyState === 'loading' ||
517
+ project.policyState === 'loaded' ||
518
+ (!retry && project.policyState === 'error'))
519
+ continue;
520
+ marked.add(id);
521
+ policyQueue.push(id);
522
+ }
523
+ if (marked.size)
384
524
  set({
385
- principals: result.data.items.map((p) => ({
386
- id: p.id,
387
- name: p.name,
388
- projectId: p.projectId,
389
- status: p.status,
390
- revision: p.revision,
391
- createdByUserId: p.createdByUserId,
392
- permissions: p.permissions,
393
- })),
394
- principalsNext: result.data.next,
395
- principalsRestricted: false,
525
+ projects: state.projects.map((p) => marked.has(p.id) ? { ...p, policyState: 'loading' } : p),
396
526
  });
397
- else if (result.status === 403)
398
- set({ principals: [], principalsNext: null, principalsRestricted: true });
527
+ async function work() {
528
+ policyWorkers++;
529
+ try {
530
+ while (policyQueue.length) {
531
+ const id = policyQueue.shift();
532
+ const project = state.projects.find((p) => p.id === id);
533
+ if (!project)
534
+ continue;
535
+ const version = navigation;
536
+ const detail = await services.client.request('get', '/_pb/projects/{project}', {
537
+ params: { project: project.slug },
538
+ });
539
+ const authority = await services.client.request('get', '/_pb/projects/{project}/permissions', { params: { project: project.slug } });
540
+ if (version !== navigation)
541
+ continue;
542
+ set({
543
+ projects: state.projects.map((p) => p.id === id
544
+ ? {
545
+ ...p,
546
+ cap: detail.ok ? detail.data.tokenMaxScope : null,
547
+ permissions: authority.ok ? authority.data.project : null,
548
+ policyState: detail.ok && authority.ok ? 'loaded' : 'error',
549
+ }
550
+ : p),
551
+ });
552
+ }
553
+ }
554
+ finally {
555
+ policyWorkers--;
556
+ }
557
+ }
558
+ while (policyQueue.length && policyWorkers < 4)
559
+ void work();
560
+ },
561
+ choosePrincipal(id, project, organization = project === null) {
562
+ set({
563
+ issuancePrincipal: id,
564
+ issuanceProject: organization ? '__organization' : (project ?? ''),
565
+ issuanceNonce: state.issuanceNonce + 1,
566
+ });
399
567
  },
400
568
  async identity() {
401
569
  const version = navigation;
402
- const result = await services.client.request('get', '/_pb/session');
403
- if (version !== navigation)
404
- return;
405
- if (result.ok)
406
- set({ userId: result.data.user.id });
570
+ const session = services.client.request('get', '/_pb/session');
407
571
  const projectSlugs = {};
572
+ const catalogue = [];
573
+ let projectsError = false;
408
574
  let projectAfter;
409
575
  do {
410
576
  const projects = await services.client.request('get', '/_pb/projects', {
@@ -412,16 +578,36 @@ export function createTokensController(services, deps) {
412
578
  });
413
579
  if (version !== navigation)
414
580
  return;
415
- if (!projects.ok)
581
+ if (!projects.ok) {
582
+ projectsError = true;
416
583
  break;
417
- for (const project of projects.data.items)
584
+ }
585
+ for (const project of projects.data.items) {
418
586
  projectSlugs[project.id] = project.slug;
587
+ if (project.status === 'archived')
588
+ continue;
589
+ catalogue.push({
590
+ id: project.id,
591
+ slug: project.slug,
592
+ name: project.name ?? project.slug,
593
+ cap: null,
594
+ permissions: null,
595
+ policyState: 'idle',
596
+ });
597
+ }
419
598
  projectAfter = projects.data.next ?? undefined;
599
+ set({ projectSlugs: { ...projectSlugs }, projects: [...catalogue], projectsError });
420
600
  } while (projectAfter !== undefined);
421
- set({ projectSlugs });
601
+ set({ projectSlugs, projects: catalogue, projectsError });
602
+ const result = await session;
603
+ if (version !== navigation)
604
+ return;
605
+ if (result.ok)
606
+ set({ userId: result.data.user.id });
422
607
  const permissions = await services.client.request('get', '/_pb/me/permissions');
423
608
  if (version !== navigation)
424
609
  return;
610
+ set({ organizationPermissions: permissions.ok ? permissions.data.organization : [] });
425
611
  if (!permissions.ok || !permissions.data.organization.includes('org:users:read')) {
426
612
  set({ names: {} });
427
613
  return;
@@ -489,7 +675,8 @@ export function createTokensController(services, deps) {
489
675
  failure(result);
490
676
  else {
491
677
  await controller.loadPrincipals();
492
- set({ notice: 'Service principal created.' });
678
+ set({ notice: 'Service principal created. Create a token to use it.' });
679
+ return result.data;
493
680
  }
494
681
  },
495
682
  async suspendPrincipal(principal) {
@@ -518,7 +705,9 @@ export function createTokensController(services, deps) {
518
705
  }
519
706
  export function filterTokens(items, filters) {
520
707
  return items.filter((item) => item.name.toLowerCase().includes(filters.q.toLowerCase()) &&
521
- (!filters.scope || item.scope === filters.scope) &&
708
+ (!filters.scope ||
709
+ item.scope === filters.scope ||
710
+ accessLabels[item.scope] === filters.scope) &&
522
711
  (!filters.status || item.status === filters.status));
523
712
  }
524
713
  export function TokenList({ state: s, controller: c }) {
@@ -532,7 +721,7 @@ export function TokenList({ state: s, controller: c }) {
532
721
  : 'Organization';
533
722
  return (_jsxs(_Fragment, { children: [_jsx(LoadedListFilters, { values: filters, fields: [
534
723
  { key: 'q', label: 'Name (this page)' },
535
- { key: 'scope', label: 'Scope', options: ['read', 'write', 'full'] },
724
+ { key: 'scope', label: 'Scope', options: Object.values(accessLabels) },
536
725
  { key: 'status', label: 'Status', options: ['active', 'expired', 'revoked'] },
537
726
  ], onChange: setFilters }), _jsxs("div", { className: "my-3 flex flex-wrap items-center justify-between gap-3", children: [_jsxs("p", { className: "text-sm text-neutral-600", children: [items.length, " tokens on this page \u00B7", ' ', items.filter((token) => token.status === 'active').length, " active"] }), _jsx(Button, { type: "button", variant: "outline", disabled: s.loading || s.busy, onClick: () => void c.load(), children: "Reload tokens" })] }), s.loading ? (_jsx(ListState, { kind: "loading", title: "Loading tokens\u2026" })) : s.listError ? (_jsx(ListState, { kind: "error", title: "Unable to load tokens." })) : items.length === 0 ? (_jsx(ListState, { kind: filtered ? 'filtered-empty' : 'empty', title: filtered ? 'No tokens match these filters on this page.' : 'No tokens on this page.' })) : (_jsx(DataTable, { label: "Tokens", items: items, columns: [
538
727
  {
@@ -551,7 +740,7 @@ export function TokenList({ state: s, controller: c }) {
551
740
  key: 'scope',
552
741
  label: 'Scope',
553
742
  value: (token) => token.scope,
554
- render: (token) => _jsx(Badge, { variant: "outline", children: token.scope }),
743
+ render: (token) => _jsx(Badge, { variant: "outline", children: accessLabels[token.scope] }),
555
744
  },
556
745
  {
557
746
  key: 'project',
@@ -599,8 +788,25 @@ export function TokenList({ state: s, controller: c }) {
599
788
  },
600
789
  ] })), s.next === null ? null : (_jsx(Button, { type: "button", disabled: s.busy || s.loading, onClick: () => void c.load(s.target, s.project, s.next), children: "Next tokens" })), _jsx("p", { className: "mt-3 text-xs text-neutral-600", children: "Secrets are never listed. Only tokens in your accessible scope appear here." })] }));
601
790
  }
791
+ function focusResult(node) {
792
+ node?.focus();
793
+ }
794
+ function SetupProjectAccess({ state: s, controller: c }) {
795
+ const [setupCandidate, setSetupCandidate] = useState('');
796
+ const candidate = s.projects.find((p) => p.id === setupCandidate);
797
+ useEffect(() => {
798
+ if (s.secret && candidate?.policyState === 'loaded' && hasSetupAccess(s, candidate))
799
+ c.configureProject(candidate.slug);
800
+ }, [c, s.secret, candidate]);
801
+ return (_jsxs(_Fragment, { children: [' ', s.projects.some((p) => p.permissions === null) && (_jsxs("details", { children: [_jsx("summary", { className: "min-h-11 cursor-pointer", children: "Check another project's package access" }), _jsx(Label, { htmlFor: "setup-candidate", children: "Project to configure" }), _jsxs(Select, { id: "setup-candidate", className: "min-h-11", value: setupCandidate, onChange: (event) => {
802
+ setSetupCandidate(event.target.value);
803
+ c.loadProjectPolicies([event.target.value]);
804
+ }, children: [_jsx("option", { value: "", children: "Choose a project" }), s.projects
805
+ .filter((p) => inResultScope(s.result, p))
806
+ .map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id)))] }), candidate?.policyState === 'loading' && _jsx("p", { children: "Loading project access\u2026" }), candidate?.policyState === 'error' && (_jsx(Button, { type: "button", onClick: () => c.loadProjectPolicies([candidate.id], true), children: "Try again loading project access" })), candidate?.policyState === 'loaded' && !hasSetupAccess(s, candidate) && (_jsx("p", { children: "This token does not have package access to this project." }))] }))] }));
807
+ }
602
808
  export function TokensView({ state: s, controller: c }) {
603
- const personal = s.renewal?.kind === 'pat' || s.target === 'personal';
809
+ const personal = s.renewal !== null ? s.renewal.kind === 'pat' : s.target === 'personal';
604
810
  const policy = s.lifetime?.[personal ? 'personal' : 'service'];
605
811
  const renewal = s.renewal;
606
812
  const clientConfig = c.clientConfig();
@@ -609,26 +815,20 @@ export function TokensView({ state: s, controller: c }) {
609
815
  : s.target === 'project'
610
816
  ? Object.keys(s.projectSlugs).find((id) => s.projectSlugs[id] === s.project)
611
817
  : null;
612
- const principalChoices = s.principalsRestricted
613
- ? [
614
- ...new Map(s.items
615
- .filter((t) => t.kind === 'svc' && t.status === 'active' && t.servicePrincipalId !== null)
616
- .map((t) => [
617
- t.servicePrincipalId,
618
- {
619
- id: t.servicePrincipalId,
620
- name: t.servicePrincipalId,
621
- projectId: t.projectId,
622
- },
623
- ])).values(),
624
- ]
625
- : s.principals.filter((p) => p.status === 'active');
818
+ const principalChoices = s.principals.filter((p) => p.status === 'active');
626
819
  function issue(event) {
627
820
  event.preventDefault();
628
821
  if (s.busy)
629
822
  return;
630
823
  const form = event.currentTarget;
631
824
  const data = new FormData(form);
825
+ const selectedIds = data.getAll('projectIds').map(String);
826
+ if (personal &&
827
+ data.get('projectMode') === 'selected' &&
828
+ (selectedIds.length < 1 || selectedIds.length > 200)) {
829
+ c.invalidProjects();
830
+ return;
831
+ }
632
832
  const scope = renewal?.kind === 'svc' ? renewal.scope : String(data.get('scope'));
633
833
  void c.issue({
634
834
  name: String(data.get('name')),
@@ -636,28 +836,49 @@ export function TokensView({ state: s, controller: c }) {
636
836
  lifetimeDays: Number(data.get('lifetimeDays')),
637
837
  servicePrincipalId: String(data.get('principal') ?? ''),
638
838
  projectMode: data.get('projectMode') === 'selected' ? 'selected' : 'all',
639
- projectIds: String(data.get('projectIds') ?? '')
640
- .split(/\s+/)
641
- .filter(Boolean),
839
+ projectIds: data.getAll('projectIds').map(String),
642
840
  overlapDays: Number(data.get('overlap') ?? 0),
643
841
  });
644
- form.reset();
645
842
  }
646
- return (_jsxs("section", { "aria-labelledby": "page-heading", className: "space-y-4", children: [_jsx("h1", { tabIndex: -1, id: "page-heading", className: "text-xl font-semibold", children: "Tokens" }), _jsx("p", { className: "text-neutral-600", children: "Credentials for Composer and npm clients. Management writes use your signed-in session. Secrets are shown once." }), _jsx(Label, { htmlFor: "token-list", children: "Token list" }), _jsxs(Select, { id: "token-list", value: s.target, disabled: s.busy, onChange: (e) => {
647
- void c.selectTarget(e.target.value, s.project);
648
- }, children: [_jsx("option", { value: "personal", children: "My personal tokens" }), s.project === null ? null : _jsx("option", { value: "project", children: "Project tokens" }), _jsx("option", { value: "organization", children: "Organization tokens" })] }), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-medium", children: "Token lifetimes and access" }), _jsx("p", { className: "mt-3", children: "Your personal tokens are revoked when you leave the organization or your account is suspended. Access stops at once; the tokens are marked revoked shortly after." }), _jsx("p", { children: "Service tokens keep working when the person who created them leaves. Project owners can see, renew and revoke every service token of their project." }), _jsxs("dl", { className: "detail-list mt-3", children: [_jsx("dt", { children: "Personal maximum lifetime" }), _jsx("dd", { children: "1 year (the operator can set less)" }), _jsx("dt", { children: "Service maximum lifetime" }), _jsx("dd", { children: "1 year for every scope (the operator can change it)" })] })] }), s.notice === null || s.listError ? null : (_jsx(Alert, { children: _jsx(AlertDescription, { children: s.notice }) })), s.pending === null ? null : (_jsxs("section", { "aria-label": "Confirm token issue", className: "space-y-2", children: [_jsx("h2", { children: "Confirm token issue" }), _jsx("p", { children: "Your identity was checked. Issue this token now?" }), _jsx(Button, { type: "button", disabled: s.busy || s.loading || s.lifetime === null, onClick: () => void c.confirm(), children: "Confirm token issue" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.decline(), children: "Cancel" })] })), s.secret === null ? null : (_jsxs("section", { "aria-label": "Token secret", className: "detail-group space-y-3 border-emerald-200", children: [_jsx("h2", { children: "Token secret \u2014 shown once" }), _jsx("p", { children: "Copy it now. This value will not be shown again. Closing this panel keeps the token active." }), _jsx("pre", { className: "rounded-md border bg-neutral-50 p-3 overflow-x-auto whitespace-pre-wrap break-all", children: _jsx("code", { children: s.secret }) }), _jsx(Button, { type: "button", onClick: () => void c.copy(), children: "Copy token" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.closeSecret(), children: "Close secret panel" }), clientConfig === null ? (_jsxs("p", { children: ["Choose a project this token can access to configure Composer or npm. Open that project\u2019s registry at ", c.registryOrigin(), "/<project> for Composer or", ' ', c.registryOrigin(), "/<project>/npm/ for npm."] })) : (_jsxs(_Fragment, { children: [_jsx("h3", { children: "Composer" }), _jsx("pre", { className: "overflow-x-auto rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig.composer }) }), _jsx(Button, { type: "button", variant: "outline", onClick: () => void c.copy('composer'), children: "Copy Composer configuration" }), _jsx("h3", { children: "npm (.npmrc)" }), _jsx("pre", { className: "overflow-x-auto rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig.npm }) }), _jsx(Button, { type: "button", variant: "outline", onClick: () => void c.copy('npm'), children: "Copy npm configuration" })] })), s.oldPersonalId !== null ? (_jsx(ConfirmAction, { action: "Revoke old personal token", target: s.oldPersonalId, scope: "My personal tokens", description: "Clients using the old token will lose access immediately. This cannot be undone. Update clients to the new token before revoking.", disabled: s.busy, onConfirm: () => void c.revoke(s.oldPersonalId, true) })) : null] })), s.pending !== null || s.secret !== null ? null : (_jsxs("details", { open: renewal !== null, className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-semibold", children: renewal === null ? 'Issue token' : 'Renew token' }), _jsxs("form", { onSubmit: issue, className: "grid gap-2", children: [_jsx("h2", { children: renewal === null ? 'Issue token' : 'Renew token' }), _jsx(Label, { htmlFor: "token-name", children: "Name" }), _jsx(Input, { id: "token-name", name: "name", defaultValue: renewal?.name ?? '', required: true }), _jsx(Label, { htmlFor: "token-scope", children: "Scope" }), _jsxs(Select, { id: "token-scope", name: "scope", defaultValue: renewal?.scope ?? 'read', disabled: renewal?.kind === 'svc', children: [_jsx("option", { value: "read", children: "Read" }), _jsx("option", { value: "write", children: "Write" }), _jsx("option", { value: "full", children: "Full" })] }), _jsx(Label, { htmlFor: "token-lifetime", children: "Lifetime" }), _jsx(Select, { id: "token-lifetime", name: "lifetimeDays", defaultValue: policy?.defaultDays, required: true, children: policy === undefined
843
+ return (_jsxs("section", { "aria-labelledby": "page-heading", className: "space-y-4", children: [_jsx("h1", { tabIndex: -1, id: "page-heading", className: "text-xl font-semibold", children: "Tokens" }), _jsx("p", { className: "text-neutral-600", children: "Connect your tools and CI pipelines to your packages." }), s.notice === null || s.listError ? null : (_jsx(Alert, { role: "status", "aria-live": "polite", children: _jsx(AlertDescription, { children: s.notice }) })), s.pending === null ? null : (_jsxs("section", { "aria-label": "Confirm token issue", className: "space-y-2", children: [_jsx("h2", { children: "Confirm token issue" }), _jsx("p", { children: "Your identity was checked. Issue this token now?" }), _jsx(Button, { type: "button", disabled: s.busy || s.loading || s.lifetime === null, onClick: () => void c.confirm(), children: "Confirm token issue" }), _jsx(Button, { type: "button", variant: "outline", onClick: () => c.decline(), children: "Cancel" })] })), s.secret === null ? null : (_jsxs("section", { "aria-label": "Token secret", className: "detail-group space-y-3 border-emerald-200", children: [_jsx("h2", { tabIndex: -1, ref: focusResult, children: "Token created" }), s.result && (_jsxs("p", { className: "break-words", children: [s.result.draft.name, " \u00B7", ' ', s.result.target === 'project'
844
+ ? (s.projects.find((p) => p.slug === s.result?.project)?.name ??
845
+ 'Project name unavailable')
846
+ : s.result.target === 'organization'
847
+ ? 'Organization-wide'
848
+ : s.result.draft.projectMode === 'all'
849
+ ? 'All accessible projects'
850
+ : s.result.draft.projectIds.length === 1
851
+ ? (s.projects.find((p) => p.id === s.result?.draft.projectIds[0])?.name ??
852
+ '1 selected project')
853
+ : `${s.result.draft.projectIds.length} selected projects`, ' ', "\u00B7 ", accessLabels[s.result.draft.scope], " \u00B7 Expires after", ' ', dayLabel(s.result.draft.lifetimeDays)] })), _jsx("p", { children: "Copy this token now. You cannot view it again. Closing this panel keeps it active." }), _jsx("pre", { className: "rounded-md border bg-neutral-50 p-3 overflow-x-auto whitespace-pre-wrap break-all", children: _jsx("code", { role: "region", "aria-label": "Created token", children: s.secret }) }), _jsx(Button, { className: "min-h-11", type: "button", onClick: () => void c.copy(), children: "Copy token" }), _jsx(Button, { type: "button", variant: "outline", className: "min-h-11", onClick: () => {
854
+ c.closeSecret();
855
+ requestAnimationFrame(() => document.getElementById('new-token')?.focus());
856
+ }, children: "Done" }), _jsx(SetupProjectAccess, { state: s, controller: c }), _jsx(Label, { htmlFor: "configure-project", children: "Configure for project" }), _jsxs(Select, { className: "min-h-11", id: "configure-project", value: s.secretProject ?? '', onChange: (event) => c.configureProject(event.target.value), children: [_jsx("option", { value: "", children: "Choose a project" }), s.projects
857
+ .filter((p) => hasSetupAccess(s, p))
858
+ .filter((p) => inResultScope(s.result, p))
859
+ .map((p) => (_jsx("option", { value: p.slug, children: p.name }, p.id)))] }), clientConfig === null ? (_jsx("p", { children: "Choose a project this token can access to configure Composer or npm." })) : (_jsxs(_Fragment, { children: [_jsx("p", { children: "Composer sends this credential to this registry host. The token still limits which projects it can access." }), [
860
+ ['Composer', ['composer', 'composerAuth']],
861
+ ['npm', ['npm']],
862
+ ['CI environment', ['ciComposer', 'ciNpm']],
863
+ ].map(([title, kinds], index) => (_jsxs("details", { open: index === 0, className: "min-w-0", children: [_jsx("summary", { className: "min-h-11 cursor-pointer", children: title }), kinds.map((kind) => (_jsxs("div", { children: [_jsx("p", { children: {
864
+ composer: 'Composer repository command',
865
+ composerAuth: 'Merge this into your local auth.json. Do not commit credentials.',
866
+ npm: 'Use this on your computer. For CI, use the environment-variable version below.',
867
+ ciComposer: 'Save PACKAGE_BROKER_TOKEN as a secret in your CI settings. These examples install packages; they do not publish them.',
868
+ ciNpm: 'Save this as .npmrc, then run npm ci.',
869
+ }[kind] }), _jsx("pre", { className: "whitespace-pre-wrap break-all rounded-md border bg-neutral-50 p-3", children: _jsx("code", { children: clientConfig[kind] }) }), _jsx(Button, { type: "button", variant: "outline", className: "min-h-11", onClick: () => void c.copy(kind), children: {
870
+ composer: 'Copy Composer configuration',
871
+ composerAuth: 'Copy auth.json',
872
+ npm: 'Copy npm configuration',
873
+ ciComposer: 'Copy CI Composer',
874
+ ciNpm: 'Copy CI npm',
875
+ }[kind] })] }, kind)))] }, title)))] })), s.oldPersonalId !== null ? (_jsx(ConfirmAction, { action: "Revoke old personal token", target: s.oldPersonalId, scope: "My personal tokens", description: "Clients using the old token will lose access immediately. This cannot be undone. Update clients to the new token before revoking.", disabled: s.busy, onConfirm: () => void c.revoke(s.oldPersonalId, true) })) : null] })), _jsx("div", { hidden: s.pending !== null || s.secret !== null || renewal !== null, children: _jsx(IssueTokenForm, { state: s, controller: c }) }), s.pending !== null || s.secret !== null || renewal === null ? null : (_jsxs("details", { open: renewal !== null, className: "detail-group", children: [_jsx("summary", { className: "min-h-11 cursor-pointer font-semibold", children: renewal === null ? 'Issue token' : 'Renew token' }), _jsxs("form", { onSubmit: issue, className: "grid gap-2", children: [_jsx(Label, { htmlFor: "token-name", children: "Name" }), _jsx(Input, { id: "token-name", name: "name", defaultValue: renewal?.name ?? '', required: true }), _jsx(Label, { htmlFor: "token-scope", children: "Scope" }), _jsxs(Select, { id: "token-scope", name: "scope", defaultValue: renewal?.scope ?? 'read', disabled: renewal?.kind === 'svc', children: [_jsx("option", { value: "read", children: "Install and download" }), _jsx("option", { value: "write", children: "Include publish permission" }), _jsx("option", { value: "full", children: "Include grants, sources and audit access" })] }), _jsx(Label, { htmlFor: "token-lifetime", children: "Lifetime" }), _jsx(Select, { id: "token-lifetime", name: "lifetimeDays", defaultValue: policy?.defaultDays, required: true, children: policy === undefined
649
876
  ? null
650
- : lifetimeChoices(policy).map((days) => (_jsxs("option", { value: days, children: [days, " days"] }, days))) }), personal ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-project-mode", children: "Projects" }), _jsxs(Select, { id: "token-project-mode", name: "projectMode", defaultValue: renewal?.projectMode ?? 'all', children: [_jsx("option", { value: "all", children: "All accessible projects" }), _jsx("option", { value: "selected", children: "Selected projects" })] }), _jsx(Label, { htmlFor: "token-project-ids", children: "Selected project IDs (space separated; re-enter when renewing)" }), _jsx(Input, { id: "token-project-ids", name: "projectIds", required: renewal?.projectMode === 'selected' })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-principal", children: "Service principal" }), _jsxs(Select, { id: "token-principal", name: "principal", defaultValue: renewal?.servicePrincipalId ?? '', disabled: renewal?.kind === 'svc', required: renewal?.kind !== 'svc', children: [_jsx("option", { value: "", disabled: true, children: "Select a principal" }), principalChoices
877
+ : lifetimeChoices(policy).map((days) => (_jsx("option", { value: days, children: dayLabel(days) }, days))) }), personal ? (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-project-mode", children: "Projects" }), _jsxs(Select, { id: "token-project-mode", name: "projectMode", defaultValue: renewal?.projectMode ?? 'all', children: [_jsx("option", { value: "all", children: "All accessible projects" }), _jsx("option", { value: "selected", children: "Selected projects" })] }), _jsxs("fieldset", { children: [_jsx("legend", { children: "Choose projects again when renewing a selected token" }), s.projects.map((p) => (_jsxs("label", { className: "flex min-h-11 items-center gap-2", children: [_jsx("input", { type: "checkbox", name: "projectIds", value: p.id }), p.name] }, p.id)))] })] })) : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-principal", children: "Service principal" }), _jsxs(Select, { id: "token-principal", name: "principal", defaultValue: renewal?.servicePrincipalId ?? '', disabled: renewal?.kind === 'svc', required: renewal?.kind !== 'svc', children: [_jsx("option", { value: "", disabled: true, children: "Select a principal" }), principalChoices
651
878
  .filter((p) => p.projectId === projectId)
652
- .map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id)))] }), s.principalsRestricted ? (_jsx("p", { children: "Principal discovery is limited to principals referenced by the tokens you can see." })) : null] })), renewal?.kind !== 'svc' ? null : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-overlap", children: "Overlap" }), _jsxs(Select, { id: "token-overlap", name: "overlap", defaultValue: "0", children: [_jsx("option", { value: "0", children: "None" }), _jsx("option", { value: "1", children: "1 day" }), _jsx("option", { value: "7", children: "7 days" })] }), _jsx("p", { children: "The old token stops at the end of the overlap or when the new token expires, whichever is first." })] })), _jsx(Button, { type: "submit", disabled: s.busy || s.loading || policy === undefined, children: renewal === null ? 'Issue token' : 'Renew token' }), renewal === null ? null : (_jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => c.closeSecret(), children: "Cancel renewal" }))] }, `${s.target}:${renewal?.id ?? ''}:${policy?.defaultDays}`)] }, `${s.target}:${renewal?.id ?? ''}`)), _jsx(TokenList, { state: s, controller: c }, `${s.target}:${s.project}`), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "cursor-pointer font-semibold", children: "Service principals" }), _jsx("h2", { children: "Service principals" }), _jsxs("form", { className: "grid gap-2", onSubmit: (event) => {
653
- event.preventDefault();
654
- if (s.busy)
655
- return;
656
- const form = event.currentTarget;
657
- const data = new FormData(form);
658
- void c.createPrincipal(String(data.get('name')), String(data.get('project')), String(data.get('permissions')).split(/\s+/).filter(Boolean));
659
- form.reset();
660
- }, children: [_jsx(Label, { htmlFor: "principal-name", children: "Principal name" }), _jsx(Input, { id: "principal-name", name: "name", required: true }), _jsx(Label, { htmlFor: "principal-project", children: "Project ID (empty for organization)" }), _jsx(Input, { id: "principal-project", name: "project" }), _jsx(Label, { htmlFor: "principal-permissions", children: "Permissions (space separated)" }), _jsx(Input, { id: "principal-permissions", name: "permissions" }), _jsx(Button, { type: "submit", disabled: s.busy, children: "Create service principal" })] }), _jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => void c.loadPrincipals(), children: "Reload service principals" }), s.principalsRestricted ? (_jsx(ListState, { kind: "placeholder", title: "Service principal discovery is restricted." })) : s.principals.length === 0 ? (_jsx(ListState, { kind: "empty", title: "No service principals loaded." })) : (_jsx(DataTable, { label: "Service principals", items: s.principals, columns: [
879
+ .map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id)))] }), s.principalsRestricted ? (_jsx("p", { children: "Ask an organization administrator to create a service identity for this project." })) : null] })), renewal?.kind !== 'svc' ? null : (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "token-overlap", children: "Overlap" }), _jsxs(Select, { id: "token-overlap", name: "overlap", defaultValue: "0", children: [_jsx("option", { value: "0", children: "None" }), _jsx("option", { value: "1", children: "1 day" }), _jsx("option", { value: "7", children: "7 days" })] }), _jsx("p", { children: "The old token stops at the end of the overlap or when the new token expires, whichever is first." })] })), _jsx(Button, { type: "submit", disabled: s.busy || s.loading || policy === undefined, children: renewal === null ? 'Issue token' : 'Renew token' }), renewal === null ? null : (_jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => c.closeSecret(), children: "Cancel renewal" }))] }, `${s.target}:${renewal?.id ?? ''}:${policy?.defaultDays}`)] }, `${s.target}:${renewal?.id ?? ''}`)), _jsx(Label, { htmlFor: "token-list", children: "Token list" }), _jsxs(Select, { id: "token-list", value: s.target, disabled: s.busy, onChange: (e) => {
880
+ void c.selectTarget(e.target.value, s.project);
881
+ }, children: [_jsx("option", { value: "personal", children: "My personal tokens" }), s.project === null ? null : _jsx("option", { value: "project", children: "Project tokens" }), _jsx("option", { value: "organization", children: "Organization tokens" })] }), _jsxs("details", { className: "detail-group", children: [_jsx("summary", { children: "How token access works" }), _jsx("p", { children: "Personal tokens use your access and stop working if you leave or your account is suspended. Service tokens use a service identity and keep working when their creator leaves. Each project's access policy can limit a token further." })] }), _jsx(TokenList, { state: s, controller: c }, `${s.target}:${s.project}`), s.project === null && s.organizationPermissions.includes('org:service-principals:write') ? (_jsx(TrustedPublishers, { controller: c.trustedPublishers, permissions: s.organizationPermissions })) : null, _jsxs("details", { className: "detail-group", children: [_jsx("summary", { className: "min-h-11 cursor-pointer font-semibold", children: "Service principals" }), _jsx(PrincipalForm, { state: s, controller: c }), _jsx(Button, { type: "button", variant: "outline", disabled: s.busy, onClick: () => void c.loadPrincipals(), children: "Reload service principals" }), s.principalsRestricted ? (_jsx(ListState, { kind: "placeholder", title: "You cannot browse service principals here." })) : s.principalsLoading ? (_jsx(ListState, { kind: "loading", title: "Loading service principals\u2026" })) : s.principals.length === 0 ? (_jsx(ListState, { kind: "empty", title: "No service principals loaded." })) : (_jsx(DataTable, { label: "Service principals", items: s.principals, columns: [
661
882
  {
662
883
  key: 'name',
663
884
  label: 'Name',
@@ -687,13 +908,21 @@ export function bindTokensLifecycle(controller, events, onNavigate) {
687
908
  onNavigate();
688
909
  };
689
910
  const clear = () => controller.navigate();
911
+ const show = (event) => {
912
+ clear();
913
+ if (event.persisted === true) {
914
+ onNavigate();
915
+ if (controller.snapshot().organizationPermissions.includes('org:service-principals:write'))
916
+ void controller.trustedPublishers.load();
917
+ }
918
+ };
690
919
  events.addEventListener('hashchange', change);
691
920
  events.addEventListener('pagehide', clear);
692
- events.addEventListener('pageshow', clear);
921
+ events.addEventListener('pageshow', show);
693
922
  return () => {
694
923
  events.removeEventListener('hashchange', change);
695
924
  events.removeEventListener('pagehide', clear);
696
- events.removeEventListener('pageshow', clear);
925
+ events.removeEventListener('pageshow', show);
697
926
  controller.dispose();
698
927
  };
699
928
  }
@@ -733,10 +962,19 @@ export function TokensPage() {
733
962
  if (window.location.hash.startsWith('#/tokens'))
734
963
  void controller.start(project === null ? 'personal' : 'project', project);
735
964
  }
965
+ function reload() {
966
+ if (!window.location.hash.startsWith('#/tokens'))
967
+ return;
968
+ void controller.identity();
969
+ void controller.loadPrincipals();
970
+ }
736
971
  change();
737
972
  void controller.identity();
738
973
  void controller.loadPrincipals();
739
- return bindTokensLifecycle(controller, window, change);
974
+ return bindTokensLifecycle(controller, window, () => {
975
+ change();
976
+ reload();
977
+ });
740
978
  }, [controller]);
741
979
  useEffect(() => bindTokensReplay(controller, services), [controller, services]);
742
980
  return _jsx(TokensView, { state: state, controller: controller });