@alliance-droid/status-feedback-system 4.0.3 → 4.1.1
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/CHANGELOG.md +46 -0
- package/README.md +8 -2
- package/dist/auth-token.d.ts +42 -0
- package/dist/auth-token.js +89 -0
- package/dist/components/board/FeedbackSidebar.svelte +13 -18
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.1.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `<FeedbackSidebar>`: removed the divider between the **Status** and **Sort By** selects so they read as one filter group, and gave the group consistent padding (`px-5 py-2` + `space-y-3`) so the labels line up with the "All" filter above and the dropdowns aren't flush to the rail edge. Styling only — no behavior change.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Raised the `@alliance-droid/svelte-component-library` peer dependency to `>=1.3.2`. Earlier versions (e.g. 1.1.21) are incompatible with Svelte `5.56+` and trigger `target.exclude.has is not a function` when board components render. Consumers should be on `1.3.2` or later.
|
|
12
|
+
|
|
13
|
+
## 4.1.0
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `createAuthTokenGetter(options?)` — a shared client accessor for the bearer token the board's status controls send (the `getAuthToken` for `createFeedbackBoard`). Fetches a same-origin token endpoint (`/auth/access-token` by default), caches until just before expiry, single-flights, and returns `null` on failure. Replaces the per-app copy of this helper.
|
|
18
|
+
|
|
19
|
+
## 4.0.3
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Board status: the transitions read (`GET /api/board/feedback/[id]/transitions`) now returns `200` with `{ canManage: false, targets: [] }` for signed-in users without management rights, instead of `403` — keeping the browser console clean. The client gates the status control on `canManage`. (Requires the matching `status-feedback-admin` deploy.)
|
|
24
|
+
|
|
25
|
+
## 4.0.2
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- The status control is shown only to users the backend actually authorizes (Admin / ProductOwner), resolved via a one-time privilege probe — not merely whenever a `getAuthToken` getter is provided.
|
|
30
|
+
|
|
31
|
+
## 4.0.1
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Corrected the `getAuthToken` token-type docs to specify an **access token** scoped to `status-feedback-admin` (an id token is rejected by the backend).
|
|
36
|
+
- `loadMore` failures are now surfaced (`board.loadMoreError`) with a Retry instead of failing silently.
|
|
37
|
+
|
|
38
|
+
## 4.0.0
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Inline status management**: signed-in Admin / ProductOwner users can change a feedback item's status from the board (status badge → menu), via a `getAuthToken` bearer-token bridge to the `status-feedback-admin` backend. Authorization (per-project access + the status state machine, with reason prompts on declines/reopens) is enforced server-side.
|
|
43
|
+
|
|
44
|
+
### Changed (BREAKING)
|
|
45
|
+
|
|
46
|
+
- Removed the all-in-one `FeedbackBoard` component. Compose the board from the exported parts instead — `createFeedbackBoard` + `FeedbackSidebar` / `FeedbackList` / `FeedbackDetail` / `FeedbackSubmitForm`. See [docs/custom-feedback-integration.md](docs/custom-feedback-integration.md).
|
|
47
|
+
- Feedback categories are system-owned (**Bug** / **Feature Request**): `<FeedbackSubmitForm>` no longer accepts a `categories` prop and the board no longer accepts `submitCategories`. Use `categoryConfig` to relabel / add icons.
|
|
48
|
+
|
|
3
49
|
## 3.0.0
|
|
4
50
|
|
|
5
51
|
### Changed (BREAKING)
|
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';
|
|
@@ -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
|
+
}
|
|
@@ -60,24 +60,19 @@
|
|
|
60
60
|
|
|
61
61
|
<Divider class="my-2" />
|
|
62
62
|
|
|
63
|
-
<div class="space-y-3">
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class="rounded-lg border-0 bg-surface-hover"
|
|
77
|
-
options={sortOptions}
|
|
78
|
-
bind:value={board.sortBy}
|
|
79
|
-
/>
|
|
80
|
-
</div>
|
|
63
|
+
<div class="space-y-3 px-5 py-2">
|
|
64
|
+
<Select
|
|
65
|
+
label="Status"
|
|
66
|
+
class="rounded-lg border-0 bg-surface-hover"
|
|
67
|
+
options={statusOptions}
|
|
68
|
+
bind:value={board.statusFilter}
|
|
69
|
+
/>
|
|
70
|
+
<Select
|
|
71
|
+
label="Sort By"
|
|
72
|
+
class="rounded-lg border-0 bg-surface-hover"
|
|
73
|
+
options={sortOptions}
|
|
74
|
+
bind:value={board.sortBy}
|
|
75
|
+
/>
|
|
81
76
|
</div>
|
|
82
77
|
</aside>
|
|
83
78
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alliance-droid/status-feedback-system",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@alliance-droid/svelte-component-library": ">=1.
|
|
29
|
-
"svelte": "^5.
|
|
28
|
+
"@alliance-droid/svelte-component-library": ">=1.3.2",
|
|
29
|
+
"svelte": "^5.48.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@alliance-droid/svelte-component-library": "^1.
|
|
32
|
+
"@alliance-droid/svelte-component-library": "^1.3.2",
|
|
33
33
|
"@eslint/js": "^10.0.1",
|
|
34
34
|
"@sveltejs/package": "^2.3.0",
|
|
35
35
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|