@alliance-droid/status-feedback-system 4.0.2 → 4.1.0
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/README.md +8 -2
- package/dist/api.d.ts +2 -0
- package/dist/auth-token.d.ts +42 -0
- package/dist/auth-token.js +89 -0
- package/dist/components/board/state.svelte.js +6 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,7 @@ The board is composed from parts around **one shared state** (`createFeedbackBoa
|
|
|
163
163
|
<script lang="ts">
|
|
164
164
|
import {
|
|
165
165
|
createFeedbackBoard,
|
|
166
|
+
createAuthTokenGetter,
|
|
166
167
|
FeedbackSidebar,
|
|
167
168
|
FeedbackList,
|
|
168
169
|
FeedbackDetail,
|
|
@@ -175,7 +176,7 @@ The board is composed from parts around **one shared state** (`createFeedbackBoa
|
|
|
175
176
|
apiUrl: 'https://status-admin.example.com',
|
|
176
177
|
apiKey: 'sf_abc123...',
|
|
177
178
|
userEmail: data.user?.email,
|
|
178
|
-
getAuthToken: ()
|
|
179
|
+
getAuthToken: createAuthTokenGetter(), // optional — enables status management
|
|
179
180
|
categoryConfig: {
|
|
180
181
|
Bug: { icon: 'fa-solid fa-bug', label: 'Bugs' },
|
|
181
182
|
'Feature Request': { icon: 'fa-solid fa-lightbulb', label: 'Ideas' },
|
|
@@ -213,6 +214,7 @@ Categories are fixed by the system (**Bug**, **Feature Request**) — `categoryC
|
|
|
213
214
|
When you pass `getAuthToken`, a signed-in **Admin** or **ProductOwner** can change an item's status from the board (via the status badge → menu) — Open → Considering → Accepted, or a decline with a reason — no trip to the admin dashboard.
|
|
214
215
|
|
|
215
216
|
- `getAuthToken` must return the user's **Alliance OIDC access token** whose audience covers **`status-feedback-admin`** (an *id token*, whose `aud` is your app's client id, is rejected). It's called on demand; return `null` for anonymous / non-privileged users and the control simply won't appear.
|
|
217
|
+
- **Use the built-in `createAuthTokenGetter()`** for this — it fetches a same-origin token endpoint (`/auth/access-token` by default), caches until just before expiry, single-flights, and returns `null` on any failure. With `@alliance-droid/svelte-auth-core`, set `enableAccessTokenEndpoint: true` and request the `status-feedback-admin` scope so that endpoint exists. Create one instance at module level: `export const getAuthToken = createAuthTokenGetter();`
|
|
216
218
|
- The token travels as `Authorization: Bearer …` to the admin backend, which **verifies it against the IdP** and derives the user's roles. Authorization (per-project access + which transitions a role may make, including reason prompts on declines/reopens) is enforced **server-side** by the same logic as the dashboard — the client never decides permissions.
|
|
217
219
|
- The available transitions are returned by the backend per item, so the menu always reflects exactly what that user may do.
|
|
218
220
|
|
|
@@ -299,9 +301,13 @@ import {
|
|
|
299
301
|
// Feedback board (composable)
|
|
300
302
|
import {
|
|
301
303
|
createFeedbackBoard,
|
|
304
|
+
createAuthTokenGetter,
|
|
302
305
|
FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm,
|
|
303
306
|
} from '@alliance-droid/status-feedback-system';
|
|
304
|
-
import type {
|
|
307
|
+
import type {
|
|
308
|
+
FeedbackBoardState, FeedbackBoardOptions, CategoryConfig,
|
|
309
|
+
AuthTokenGetter, AuthTokenGetterOptions,
|
|
310
|
+
} from '@alliance-droid/status-feedback-system';
|
|
305
311
|
|
|
306
312
|
// API Client
|
|
307
313
|
import { createStatusClient } from '@alliance-droid/status-feedback-system';
|
package/dist/api.d.ts
CHANGED
|
@@ -70,6 +70,8 @@ export interface BoardTransitions {
|
|
|
70
70
|
publicStatus: PublicStatus;
|
|
71
71
|
};
|
|
72
72
|
targets: BoardTransition[];
|
|
73
|
+
/** Whether this user may manage status on this project (drives the UI affordance). */
|
|
74
|
+
canManage: boolean;
|
|
73
75
|
}
|
|
74
76
|
/** Result of applying a status change from the board. */
|
|
75
77
|
export interface BoardStatusUpdate {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side accessor for the bearer token the board's status controls send to
|
|
3
|
+
* the `status-feedback-admin` backend — the canonical `getAuthToken` for
|
|
4
|
+
* `createFeedbackBoard`.
|
|
5
|
+
*
|
|
6
|
+
* The backend verifies the token against the IdP and requires its audience to
|
|
7
|
+
* cover `status-feedback-admin`, so it must be an **access token** scoped to that
|
|
8
|
+
* resource (an id token, whose `aud` is the app's client id, is rejected).
|
|
9
|
+
*
|
|
10
|
+
* A stateless OIDC relying party never exposes the long-lived refresh token to
|
|
11
|
+
* the browser, so a same-origin host endpoint mints a fresh, short-lived token
|
|
12
|
+
* server-side and returns only that. With `@alliance-droid/svelte-auth-core`,
|
|
13
|
+
* enabling `enableAccessTokenEndpoint` serves exactly this at
|
|
14
|
+
* `GET /auth/access-token` (returns `{ accessToken, expiresAt }`).
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* // one module-level instance per app (shares the cache)
|
|
18
|
+
* export const getAuthToken = createAuthTokenGetter();
|
|
19
|
+
* // → createFeedbackBoard({ ..., getAuthToken })
|
|
20
|
+
*/
|
|
21
|
+
export interface AuthTokenGetterOptions {
|
|
22
|
+
/** Endpoint that mints a fresh token. Default: `/auth/access-token`. */
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
/** Refresh this many ms before the token's `expiresAt`. Default: 30000. */
|
|
25
|
+
expirySkewMs?: number;
|
|
26
|
+
/** Custom fetch (e.g. for tests). Default: `globalThis.fetch`. */
|
|
27
|
+
fetch?: typeof globalThis.fetch;
|
|
28
|
+
}
|
|
29
|
+
/** A `getAuthToken` function with a `clear()` to drop the cache (e.g. after logout). */
|
|
30
|
+
export interface AuthTokenGetter {
|
|
31
|
+
(): Promise<string | null>;
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a cached, single-flight token accessor.
|
|
36
|
+
*
|
|
37
|
+
* Safe to call repeatedly: an in-flight request is shared, and a still-valid
|
|
38
|
+
* token is served from memory without a network round-trip. Resolves `null` when
|
|
39
|
+
* the user isn't signed in / not privileged (any non-OK response) or on SSR —
|
|
40
|
+
* which the board reads as "no manage-status control".
|
|
41
|
+
*/
|
|
42
|
+
export declare function createAuthTokenGetter(options?: AuthTokenGetterOptions): AuthTokenGetter;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side accessor for the bearer token the board's status controls send to
|
|
3
|
+
* the `status-feedback-admin` backend — the canonical `getAuthToken` for
|
|
4
|
+
* `createFeedbackBoard`.
|
|
5
|
+
*
|
|
6
|
+
* The backend verifies the token against the IdP and requires its audience to
|
|
7
|
+
* cover `status-feedback-admin`, so it must be an **access token** scoped to that
|
|
8
|
+
* resource (an id token, whose `aud` is the app's client id, is rejected).
|
|
9
|
+
*
|
|
10
|
+
* A stateless OIDC relying party never exposes the long-lived refresh token to
|
|
11
|
+
* the browser, so a same-origin host endpoint mints a fresh, short-lived token
|
|
12
|
+
* server-side and returns only that. With `@alliance-droid/svelte-auth-core`,
|
|
13
|
+
* enabling `enableAccessTokenEndpoint` serves exactly this at
|
|
14
|
+
* `GET /auth/access-token` (returns `{ accessToken, expiresAt }`).
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* // one module-level instance per app (shares the cache)
|
|
18
|
+
* export const getAuthToken = createAuthTokenGetter();
|
|
19
|
+
* // → createFeedbackBoard({ ..., getAuthToken })
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Create a cached, single-flight token accessor.
|
|
23
|
+
*
|
|
24
|
+
* Safe to call repeatedly: an in-flight request is shared, and a still-valid
|
|
25
|
+
* token is served from memory without a network round-trip. Resolves `null` when
|
|
26
|
+
* the user isn't signed in / not privileged (any non-OK response) or on SSR —
|
|
27
|
+
* which the board reads as "no manage-status control".
|
|
28
|
+
*/
|
|
29
|
+
export function createAuthTokenGetter(options = {}) {
|
|
30
|
+
const endpoint = options.endpoint ?? '/auth/access-token';
|
|
31
|
+
const skewMs = options.expirySkewMs ?? 30_000;
|
|
32
|
+
const fetchFn = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
33
|
+
let cached = null;
|
|
34
|
+
let inflight = null;
|
|
35
|
+
const getToken = (async () => {
|
|
36
|
+
// SSR has no session cookies to send and no relative-fetch origin; the board
|
|
37
|
+
// only calls this on client interaction anyway.
|
|
38
|
+
if (typeof window === 'undefined')
|
|
39
|
+
return null;
|
|
40
|
+
if (cached && Date.now() < cached.expiresAt - skewMs)
|
|
41
|
+
return cached.token;
|
|
42
|
+
if (inflight)
|
|
43
|
+
return inflight;
|
|
44
|
+
inflight = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetchFn(endpoint, {
|
|
47
|
+
headers: { accept: 'application/json' },
|
|
48
|
+
credentials: 'same-origin',
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
cached = null;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const data = (await res.json());
|
|
55
|
+
const token = data.accessToken ?? data.idToken ?? data.token ?? null;
|
|
56
|
+
if (!token) {
|
|
57
|
+
cached = null;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
cached = { token, expiresAt: normalizeExpiry(data.expiresAt) };
|
|
61
|
+
return token;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Network/parse failure → treat as no token; the control simply won't show.
|
|
65
|
+
cached = null;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
inflight = null;
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
return inflight;
|
|
73
|
+
});
|
|
74
|
+
getToken.clear = () => {
|
|
75
|
+
cached = null;
|
|
76
|
+
};
|
|
77
|
+
return getToken;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Coerce the endpoint's `expiresAt` to epoch ms. Accepts ms or seconds; anything
|
|
81
|
+
* missing/invalid becomes 0 ("already expired") so a token we can't reason about
|
|
82
|
+
* is never cached.
|
|
83
|
+
*/
|
|
84
|
+
function normalizeExpiry(expiresAt) {
|
|
85
|
+
if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt))
|
|
86
|
+
return 0;
|
|
87
|
+
// Epoch seconds are ~1.7e9; epoch ms are ~1.7e12. Treat 10-digit values as seconds.
|
|
88
|
+
return expiresAt < 1e12 ? expiresAt * 1000 : expiresAt;
|
|
89
|
+
}
|
|
@@ -184,11 +184,13 @@ export function createFeedbackBoard(options) {
|
|
|
184
184
|
try {
|
|
185
185
|
const result = await client.getBoardTransitions(itemId, token);
|
|
186
186
|
transitions = { ...transitions, [itemId]: result };
|
|
187
|
-
|
|
187
|
+
// The backend tells us whether this user may manage status (200 either
|
|
188
|
+
// way — no 403 console noise for ordinary signed-in users).
|
|
189
|
+
statusPrivilege = result.canManage;
|
|
188
190
|
return result;
|
|
189
191
|
}
|
|
190
192
|
catch {
|
|
191
|
-
// 401/
|
|
193
|
+
// 401 (no/invalid token) or network error → no control.
|
|
192
194
|
return null;
|
|
193
195
|
}
|
|
194
196
|
}
|
|
@@ -205,7 +207,8 @@ export function createFeedbackBoard(options) {
|
|
|
205
207
|
if (!sample)
|
|
206
208
|
return;
|
|
207
209
|
privilegeProbed = true;
|
|
208
|
-
statusPrivilege
|
|
210
|
+
// loadTransitions sets statusPrivilege from the result's canManage flag.
|
|
211
|
+
await loadTransitions(sample.id);
|
|
209
212
|
}
|
|
210
213
|
async function setStatus(itemId, status, reason) {
|
|
211
214
|
const token = await resolveToken();
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export type { Project, System, Incident, IncidentUpdate, Feedback, FeedbackResponse, FeedbackVisibility, StatusHistoryEntry, SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus, ApiResponse, } from './types.js';
|
|
11
11
|
export { deriveOverallStatus } from './utils.js';
|
|
12
|
+
export { createAuthTokenGetter } from './auth-token.js';
|
|
13
|
+
export type { AuthTokenGetter, AuthTokenGetterOptions } from './auth-token.js';
|
|
12
14
|
export { createStatusClient } from './api.js';
|
|
13
15
|
export type { StatusClient, StatusClientConfig, StatusData, IncidentData, UptimeData, BoardData, BoardItem, BoardCategory } from './api.js';
|
|
14
16
|
export { default as StatusPage } from './components/StatusPage.svelte';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
// ── Utilities ──
|
|
11
11
|
export { deriveOverallStatus } from './utils.js';
|
|
12
|
+
// ── Auth token (getAuthToken for the board's status controls) ──
|
|
13
|
+
export { createAuthTokenGetter } from './auth-token.js';
|
|
12
14
|
// ── API Client ──
|
|
13
15
|
export { createStatusClient } from './api.js';
|
|
14
16
|
// ── Client Components ──
|
package/package.json
CHANGED