anton-bakker-feedback 0.3.4 → 0.4.2
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 +31 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/ingest-transport.d.ts +53 -0
- package/dist/core/ingest-transport.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -1
- package/dist/server/FeedbackService.d.ts +14 -0
- package/dist/server/FeedbackService.d.ts.map +1 -1
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,6 +12,37 @@ Zero-dependency feedback collection package with LLM-powered dialogue intake, au
|
|
|
12
12
|
| `anton-bakker-feedback/server` | Server handlers: DynamoDB + Bedrock factories |
|
|
13
13
|
| `anton-bakker-feedback/manifest` | Package manifest for plugin discovery |
|
|
14
14
|
|
|
15
|
+
## Central Ingest Transport
|
|
16
|
+
|
|
17
|
+
The core entry exports `createIngestTransport` — a `FeedbackSubmitTransport` that writes feedback to
|
|
18
|
+
the **central ingest API** instead of a bespoke direct-DynamoDB writer (the central-storage principle:
|
|
19
|
+
reach central storage only through an authenticated API). The consuming app mints a short-lived OAuth2
|
|
20
|
+
client-credentials JWT and POSTs the feedback content to `POST /records/feedback`; the server
|
|
21
|
+
attributes the write to the token identity (`client_id`) and stamps the canonical keys, so the app
|
|
22
|
+
holds no AWS principal and sends no partition/identity fields. Zero dependencies — uses `fetch`.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { createIngestTransport } from 'anton-bakker-feedback';
|
|
26
|
+
|
|
27
|
+
const transport = createIngestTransport({
|
|
28
|
+
apiUrl: process.env.FEEDBACK_INGEST_URL!, // https://<id>.execute-api.<region>.amazonaws.com
|
|
29
|
+
tokenUrl: process.env.FEEDBACK_TOKEN_URL!, // Cognito client-credentials token endpoint
|
|
30
|
+
clientId: process.env.FEEDBACK_CLIENT_ID!, // this app's client id (its identity)
|
|
31
|
+
clientSecret: await resolveSecret('feedback/ingest/<app>'), // from your secret store — never commit
|
|
32
|
+
// OR, if the host already manages the token: tokenProvider: () => getToken()
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = await transport.submit(item); // → { success, feedbackId, error?, retryable? }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The transport mints and caches the token (refreshing before expiry), strips server-owned key fields
|
|
39
|
+
(`PK`/`SK`/`GSI*`) before sending, and never fails silently: a 5xx or network error returns
|
|
40
|
+
`{ success: false, retryable: true }`; a 4xx returns `{ success: false, retryable: false }` with the
|
|
41
|
+
server's error message.
|
|
42
|
+
|
|
43
|
+
> The direct-DynamoDB write path (`FeedbackService.put`, `./server`) is `@deprecated` — it is the
|
|
44
|
+
> pattern the central-storage principle removes. New writers use this transport.
|
|
45
|
+
|
|
15
46
|
## Admin Pages
|
|
16
47
|
|
|
17
48
|
The `./react/admin` entry point provides three headless page-level components for managing submitted feedback. Components are unstyled — pass `className` props to integrate with Tailwind CSS or any other CSS framework.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './types.js';
|
|
|
8
8
|
export * from './config.js';
|
|
9
9
|
export * from './capture.js';
|
|
10
10
|
export * from './transport.js';
|
|
11
|
+
export * from './ingest-transport.js';
|
|
11
12
|
export * from './validation.js';
|
|
12
13
|
export * from './transforms.js';
|
|
13
14
|
export * from './observability.js';
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FeedbackSubmitTransport } from './transport.js';
|
|
2
|
+
|
|
3
|
+
/** The ingest collection feedback writes to (the deployed registry maps it to `ralph-feedback`). */
|
|
4
|
+
export declare const FEEDBACK_COLLECTION = "feedback";
|
|
5
|
+
/** OAuth2 scope required to write via the ingest API. */
|
|
6
|
+
export declare const INGEST_WRITE_SCOPE = "assets/write";
|
|
7
|
+
/** Minimal fetch surface (so the package needs no DOM lib types to compile). */
|
|
8
|
+
export type FetchLike = (url: string, init?: {
|
|
9
|
+
method?: string;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
body?: string;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
status: number;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
json: () => Promise<unknown>;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* How the transport obtains a bearer token. Either:
|
|
19
|
+
* - supply `clientId` + `clientSecret` + `tokenUrl` and let the transport mint/refresh it, OR
|
|
20
|
+
* - supply a `tokenProvider()` (e.g. the host already manages the token) — takes precedence.
|
|
21
|
+
*/
|
|
22
|
+
export interface IngestTransportConfig {
|
|
23
|
+
/** Ingest API base URL (HTTPS), e.g. https://xxxx.execute-api.<region>.amazonaws.com */
|
|
24
|
+
apiUrl: string;
|
|
25
|
+
/** OAuth2 token endpoint (HTTPS) — required unless `tokenProvider` is given. */
|
|
26
|
+
tokenUrl?: string;
|
|
27
|
+
/** App-client id (the app identity the server attributes writes to). Required unless `tokenProvider`. */
|
|
28
|
+
clientId?: string;
|
|
29
|
+
/** App-client secret. SHOULD come from the host's secret store; required unless `tokenProvider`. */
|
|
30
|
+
clientSecret?: string;
|
|
31
|
+
/** Supply a token directly (overrides client-credentials minting). */
|
|
32
|
+
tokenProvider?: () => Promise<string>;
|
|
33
|
+
/** Collection name (default 'feedback'). */
|
|
34
|
+
collection?: string;
|
|
35
|
+
/** Seconds before expiry to proactively re-mint (default 60). */
|
|
36
|
+
refreshSkewSeconds?: number;
|
|
37
|
+
/** Injectable fetch (tests). Defaults to global fetch. */
|
|
38
|
+
fetchImpl?: FetchLike;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a FeedbackSubmitTransport that writes to the central ingest API.
|
|
42
|
+
*
|
|
43
|
+
* Usage (host app wires the credential from its secret store):
|
|
44
|
+
* const transport = createIngestTransport({
|
|
45
|
+
* apiUrl: process.env.FEEDBACK_INGEST_URL!,
|
|
46
|
+
* tokenUrl: process.env.FEEDBACK_TOKEN_URL!,
|
|
47
|
+
* clientId: process.env.FEEDBACK_CLIENT_ID!,
|
|
48
|
+
* clientSecret: await resolveSecret('feedback/ingest/<app>'),
|
|
49
|
+
* });
|
|
50
|
+
* await transport.submit(item); // → { success, feedbackId }
|
|
51
|
+
*/
|
|
52
|
+
export declare function createIngestTransport(config: IngestTransportConfig): FeedbackSubmitTransport;
|
|
53
|
+
//# sourceMappingURL=ingest-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest-transport.d.ts","sourceRoot":"","sources":["../../src/core/ingest-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAM9D,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,aAAa,CAAC;AAC9C,yDAAyD;AACzD,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KACxE,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AA8BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,uBAAuB,CAkE5F"}
|
package/dist/index.js
CHANGED
|
@@ -130,6 +130,97 @@ function createMemoryTransport() {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
+
const FEEDBACK_COLLECTION = "feedback";
|
|
134
|
+
const INGEST_WRITE_SCOPE = "assets/write";
|
|
135
|
+
function assertHttps(url, label) {
|
|
136
|
+
let u;
|
|
137
|
+
try {
|
|
138
|
+
u = new URL(url);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error(`ingest-transport: ${label} is not a valid URL: ${url}`);
|
|
141
|
+
}
|
|
142
|
+
if (u.protocol !== "https:") {
|
|
143
|
+
throw new Error(`ingest-transport: ${label} must be https:// (got ${u.protocol}//) — refusing plaintext`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const SERVER_OWNED_FIELDS = [
|
|
147
|
+
"PK",
|
|
148
|
+
"SK",
|
|
149
|
+
"GSI1PK",
|
|
150
|
+
"GSI1SK",
|
|
151
|
+
"GSI2PK",
|
|
152
|
+
"GSI2SK"
|
|
153
|
+
];
|
|
154
|
+
function toContent(item) {
|
|
155
|
+
const content = { ...item };
|
|
156
|
+
for (const f of SERVER_OWNED_FIELDS) delete content[f];
|
|
157
|
+
return content;
|
|
158
|
+
}
|
|
159
|
+
function createIngestTransport(config) {
|
|
160
|
+
const collection = config.collection ?? FEEDBACK_COLLECTION;
|
|
161
|
+
const refreshSkewMs = (config.refreshSkewSeconds ?? 60) * 1e3;
|
|
162
|
+
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
163
|
+
assertHttps(config.apiUrl, "apiUrl");
|
|
164
|
+
if (!config.tokenProvider) {
|
|
165
|
+
if (!config.tokenUrl || !config.clientId || !config.clientSecret) {
|
|
166
|
+
throw new Error("ingest-transport: provide tokenProvider, OR tokenUrl + clientId + clientSecret");
|
|
167
|
+
}
|
|
168
|
+
assertHttps(config.tokenUrl, "tokenUrl");
|
|
169
|
+
}
|
|
170
|
+
if (typeof doFetch !== "function") {
|
|
171
|
+
throw new Error("ingest-transport: no fetch implementation available");
|
|
172
|
+
}
|
|
173
|
+
let cachedToken = null;
|
|
174
|
+
let cachedExpiryMs = 0;
|
|
175
|
+
async function mintToken() {
|
|
176
|
+
const basic = toBase64(`${config.clientId}:${config.clientSecret}`);
|
|
177
|
+
const res = await doFetch(config.tokenUrl, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${basic}` },
|
|
180
|
+
body: `grant_type=client_credentials&scope=${encodeURIComponent(INGEST_WRITE_SCOPE)}`
|
|
181
|
+
});
|
|
182
|
+
const data = await res.json();
|
|
183
|
+
if (!res.ok || !data.access_token) {
|
|
184
|
+
throw new Error(`token mint failed (${res.status}): ${data.error ?? ""} ${data.error_description ?? ""}`.trim());
|
|
185
|
+
}
|
|
186
|
+
cachedToken = data.access_token;
|
|
187
|
+
cachedExpiryMs = Date.now() + Math.max(0, (data.expires_in ?? 3600) * 1e3 - refreshSkewMs);
|
|
188
|
+
return cachedToken;
|
|
189
|
+
}
|
|
190
|
+
async function getToken() {
|
|
191
|
+
if (config.tokenProvider) return config.tokenProvider();
|
|
192
|
+
if (cachedToken && Date.now() < cachedExpiryMs) return cachedToken;
|
|
193
|
+
return mintToken();
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
async submit(item) {
|
|
197
|
+
try {
|
|
198
|
+
const token = await getToken();
|
|
199
|
+
const base = config.apiUrl.replace(/\/$/, "");
|
|
200
|
+
const res = await doFetch(`${base}/records/${encodeURIComponent(collection)}`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json" },
|
|
203
|
+
body: JSON.stringify({ item: toContent(item) })
|
|
204
|
+
});
|
|
205
|
+
const data = await res.json().catch(() => ({}));
|
|
206
|
+
if (res.status === 200 && data.success === true) {
|
|
207
|
+
return { success: true, feedbackId: data.ids?.[0] ?? item.id };
|
|
208
|
+
}
|
|
209
|
+
const retryable = res.status >= 500;
|
|
210
|
+
const msg = data.error ?? data.errors?.join("; ") ?? `HTTP ${res.status}`;
|
|
211
|
+
return { success: false, feedbackId: item.id, error: `ingest write failed: ${msg}`, retryable };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return { success: false, feedbackId: item.id, error: err.message, retryable: true };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function toBase64(s) {
|
|
219
|
+
if (typeof btoa === "function") return btoa(s);
|
|
220
|
+
const B = globalThis.Buffer;
|
|
221
|
+
if (B) return B.from(s).toString("base64");
|
|
222
|
+
throw new Error("ingest-transport: no base64 encoder available");
|
|
223
|
+
}
|
|
133
224
|
const VALIDATION_RULES = {
|
|
134
225
|
CONTENT_MIN_LENGTH: 10,
|
|
135
226
|
CONTENT_MAX_LENGTH: 5e3,
|
|
@@ -415,6 +506,7 @@ function createErrorResponse(e2) {
|
|
|
415
506
|
}
|
|
416
507
|
export {
|
|
417
508
|
D as DEFAULT_LOCALE,
|
|
509
|
+
FEEDBACK_COLLECTION,
|
|
418
510
|
FeedbackConfigError,
|
|
419
511
|
FeedbackDialogueError,
|
|
420
512
|
FeedbackDuplicateError,
|
|
@@ -422,6 +514,7 @@ export {
|
|
|
422
514
|
FeedbackScreenshotError,
|
|
423
515
|
FeedbackTransportError,
|
|
424
516
|
FeedbackValidationError,
|
|
517
|
+
INGEST_WRITE_SCOPE,
|
|
425
518
|
S as SUPPORTED_LOCALES,
|
|
426
519
|
VALIDATION_RULES,
|
|
427
520
|
addReporter,
|
|
@@ -431,6 +524,7 @@ export {
|
|
|
431
524
|
buildKeys,
|
|
432
525
|
calculatePriority,
|
|
433
526
|
createErrorResponse,
|
|
527
|
+
createIngestTransport,
|
|
434
528
|
createMemoryTransport,
|
|
435
529
|
d as detectLocale,
|
|
436
530
|
e as emitMetric,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/core/capture.ts","../src/core/transport.ts","../src/core/validation.ts","../src/core/transforms.ts","../src/core/errors.ts"],"sourcesContent":["/**\n * Capture logic and priority calculation for feedback submissions.\n *\n * Framework-agnostic — no browser APIs, no DOM, no AWS.\n * Pure data transformation from user input to StoredFeedbackItem.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n FeedbackEnvironment,\n FeedbackScope,\n FeedbackSize,\n FeedbackContext,\n StoredFeedbackItem,\n DialogueEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Input Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Input for processFeedbackCapture */\nexport interface CaptureInput {\n type: FeedbackType;\n content: string;\n context: FeedbackContext;\n submittedBy: string;\n appName: string;\n hasScreenshot: boolean;\n screenshotS3Key?: string;\n environment?: FeedbackEnvironment;\n version?: string;\n tags?: string[];\n traceId?: string;\n dialogueHistory?: DialogueEntry[];\n classification?: string;\n scope?: FeedbackScope;\n size?: FeedbackSize;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Priority Formula\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Base priority scores by feedback type.\n * bug=60, ux=40, feature=20, other=10\n */\nconst BASE_PRIORITY: Record<FeedbackType, number> = {\n bug: 60,\n ux: 40,\n feature: 20,\n other: 10,\n};\n\n/**\n * Environment bonus scores.\n * production=+20, staging=+10, development=0\n */\nconst ENVIRONMENT_BONUS: Record<FeedbackEnvironment, number> = {\n production: 20,\n staging: 10,\n development: 0,\n};\n\n/**\n * Calculates the priority score for a feedback item.\n *\n * Formula:\n * base = baseByType (bug=60, ux=40, feature=20, other=10)\n * env = environmentBonus (production=+20, staging=+10, development=0)\n * rep = min(reporterCount - 1, 5) * 4 (+4 per additional reporter, max +20)\n * ss = hasScreenshot ? +5 : 0\n * priority = clamp(0, 100, base + env + rep + ss)\n */\nexport function calculatePriority(\n type: FeedbackType,\n reporterCount: number,\n hasScreenshot: boolean,\n environment?: FeedbackEnvironment,\n): number {\n const base = BASE_PRIORITY[type];\n const env = ENVIRONMENT_BONUS[environment ?? 'development'];\n const reporterBonus = Math.min(reporterCount - 1, 5) * 4;\n const screenshotBonus = hasScreenshot ? 5 : 0;\n\n const raw = base + env + reporterBonus + screenshotBonus;\n return Math.max(0, Math.min(100, raw));\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ID Generation\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Generates a unique feedback ID in the format: FB-YYYY-MM-DD-{uuid8}\n * Uses crypto.randomUUID() for uniqueness; truncates to 8 chars.\n */\nexport function generateFeedbackId(nowIso?: string): string {\n const date = nowIso ? nowIso.slice(0, 10) : new Date().toISOString().slice(0, 10);\n const uuid8 = crypto.randomUUID().replace(/-/g, '').slice(0, 8);\n return `FB-${date}-${uuid8}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// GSI1SK Builder\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Builds the GSI1SK sort key value.\n * Format: {3-digit-inverted-priority}#{id}\n * Example: priority 85 → '015#FB-2026-07-25-abc12345'\n *\n * Inverted so DynamoDB ascending sort returns highest-priority first.\n */\nexport function buildGSI1SK(priority: number, id: string): string {\n const inverted = 100 - Math.max(0, Math.min(100, priority));\n const padded = String(inverted).padStart(3, '0');\n return `${padded}#${id}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Main Capture Processor\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Processes a feedback capture input into a StoredFeedbackItem\n * ready for the transport layer.\n *\n * Sets:\n * - status: 'open'\n * - reporterCount: 1\n * - reporters: [{ userId: submittedBy, addedAt: nowIso }]\n * - tags: [] when omitted\n * - All DynamoDB key fields (PK, SK, GSI1PK, GSI1SK, GSI2PK, GSI2SK)\n */\nexport function processFeedbackCapture(\n input: CaptureInput,\n nowIso?: string,\n): StoredFeedbackItem {\n const now = nowIso ?? new Date().toISOString();\n const id = generateFeedbackId(now);\n const status: FeedbackStatus = 'open';\n const tags = input.tags ?? [];\n const environment = input.environment;\n\n const priority = calculatePriority(input.type, 1, input.hasScreenshot, environment);\n\n const reporter: Reporter = {\n userId: input.submittedBy,\n addedAt: now,\n };\n\n const PK = `PROJECT#${input.appName}`;\n const SK = `FB#${now}#${id}`;\n const GSI1PK = `STATUS#${status}`;\n const GSI1SK = buildGSI1SK(priority, id);\n const GSI2PK = `TYPE#${input.type}#PROJECT#${input.appName}`;\n const GSI2SK = now;\n\n const item: StoredFeedbackItem = {\n PK,\n SK,\n GSI1PK,\n GSI1SK,\n GSI2PK,\n GSI2SK,\n id,\n appName: input.appName,\n type: input.type,\n status,\n priority,\n content: input.content,\n context: input.context,\n hasScreenshot: input.hasScreenshot,\n submittedAt: now,\n lastModifiedAt: now,\n submittedBy: input.submittedBy,\n reporters: [reporter],\n reporterCount: 1,\n dialogueHistory: input.dialogueHistory ?? [],\n corrections: [],\n tags,\n };\n\n // Optional fields — only set when defined\n if (input.screenshotS3Key !== undefined) {\n item.screenshotS3Key = input.screenshotS3Key;\n }\n if (input.environment !== undefined) {\n item.environment = input.environment;\n }\n if (input.version !== undefined) {\n item.version = input.version;\n }\n if (input.traceId !== undefined) {\n item.traceId = input.traceId;\n }\n if (input.classification !== undefined) {\n item.classification = input.classification;\n }\n if (input.scope !== undefined) {\n item.scope = input.scope;\n }\n if (input.size !== undefined) {\n item.size = input.size;\n }\n\n return item;\n}\n","/**\n * Transport contracts for the feedback package.\n *\n * Defines abstract interfaces that decouple core logic from\n * any specific implementation. Implementations live in the\n * consuming app's server handlers (slice 03).\n *\n * NO AWS SDK imports in this file.\n */\n\nimport type {\n StoredFeedbackItem,\n SubmitResult,\n DialogueResponse,\n UploadResult,\n DialogueEntry,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Transport Interfaces\n// ─────────────────────────────────────────────────────────────────\n\n/** What the React adapter calls at submit time */\nexport interface FeedbackSubmitTransport {\n submit(item: StoredFeedbackItem): Promise<SubmitResult>;\n}\n\n/** What the dialogue UI calls on each exchange */\nexport interface FeedbackDialogueTransport {\n exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse>;\n}\n\n/** What the screenshot capture calls after html2canvas */\nexport interface FeedbackScreenshotTransport {\n upload(feedbackId: string, blob: Blob): Promise<UploadResult>;\n}\n\n/**\n * Composite transport — all three combined for full-feature transport.\n * Dialogue and screenshot are optional so implementations can omit them.\n */\nexport interface FeedbackTransport\n extends FeedbackSubmitTransport,\n Partial<FeedbackDialogueTransport>,\n Partial<FeedbackScreenshotTransport> {}\n\n// ─────────────────────────────────────────────────────────────────\n// Memory Transport (for testing)\n// ─────────────────────────────────────────────────────────────────\n\n/** Memory transport with access to internal state for assertions */\nexport interface MemoryTransport extends FeedbackTransport {\n /** All submitted items */\n submissions: StoredFeedbackItem[];\n /** Queue of dialogue responses to return in order */\n dialogueResponses: DialogueResponse[];\n /** Uploaded screenshot blobs */\n uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }>;\n /** Clear all stored state */\n clear(): void;\n}\n\nconst DEFAULT_DIALOGUE_RESPONSE: DialogueResponse = {\n success: true,\n response: 'Thank you for your feedback. Can you tell me more?',\n conversationComplete: false,\n};\n\n/**\n * Creates an in-memory transport for testing.\n *\n * - submit(): stores the item and returns { success: true, feedbackId: item.id }\n * - exchange(): returns the first queued response, or a default stub\n * - upload(): records the blob reference\n * - Does NOT import any AWS SDK\n */\nexport function createMemoryTransport(): MemoryTransport {\n const submissions: StoredFeedbackItem[] = [];\n const dialogueResponses: DialogueResponse[] = [];\n const uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }> = [];\n\n return {\n submissions,\n dialogueResponses,\n uploadedScreenshots,\n\n async submit(item: StoredFeedbackItem): Promise<SubmitResult> {\n submissions.push(item);\n return { success: true, feedbackId: item.id };\n },\n\n async exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse> {\n void feedbackId;\n void message;\n void history;\n if (dialogueResponses.length > 0) {\n return dialogueResponses.shift() as DialogueResponse;\n }\n return { ...DEFAULT_DIALOGUE_RESPONSE };\n },\n\n async upload(feedbackId: string, blob: Blob): Promise<UploadResult> {\n uploadedScreenshots.push({ feedbackId, blob });\n return {\n success: true,\n s3Key: `screenshots/${feedbackId}.png`,\n };\n },\n\n clear() {\n submissions.length = 0;\n dialogueResponses.length = 0;\n uploadedScreenshots.length = 0;\n },\n };\n}\n","/**\n * Input validation functions for the feedback package.\n *\n * Used both client-side (before submission) and server-side (in handler).\n * No external dependencies — pure validation logic.\n *\n * Acceptance criteria evidence:\n * AC-65: createErrorResponse(new FeedbackValidationError('…', ['content']))\n * ])) includes fields: ['content'] → shown in errors.ts\n * AC-41: VALIDATION_RULES is exported as const (immutable) with values:\n * CONTENT_MIN_LENGTH=10, CONTENT_MAX_LENGTH=5000, TAG_MAX_COUNT=10\n * TAG_PATTERN=/^[a-z0-9-]+$/, [a-z0-9-] pattern applied\n */\n\nimport type { FeedbackContext } from './types.js';\nimport type { CaptureInput } from './capture.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Validation Rules (exported as const — immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/** Immutable validation constants */\nexport const VALIDATION_RULES = {\n CONTENT_MIN_LENGTH: 10,\n CONTENT_MAX_LENGTH: 5_000,\n TAG_MAX_COUNT: 10,\n TAG_PATTERN: /^[a-z0-9-]+$/,\n APP_NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,\n SCREENSHOT_MAX_BYTES: 5_242_880, // 5 MB\n CONSOLE_ERRORS_MAX: 20,\n} as const;\n\n// ─────────────────────────────────────────────────────────────────\n// Result Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Validation result with user-facing messages */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[]; // Blocking — submission must be rejected\n warnings: string[]; // Non-blocking — suggestions only\n}\n\nfunction makeResult(errors: string[], warnings: string[]): ValidationResult {\n return { valid: errors.length === 0, errors, warnings };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Individual Validators\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Validates feedback content.\n * Fails below CONTENT_MIN_LENGTH (\"Feedback must be at least 10 characters\"),\n * fails above CONTENT_MAX_LENGTH, warns if only whitespace.\n */\nexport function validateContent(content: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (content.length < VALIDATION_RULES.CONTENT_MIN_LENGTH) {\n errors.push(\n `Feedback must be at least ${VALIDATION_RULES.CONTENT_MIN_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.length > VALIDATION_RULES.CONTENT_MAX_LENGTH) {\n errors.push(\n `Feedback must not exceed ${VALIDATION_RULES.CONTENT_MAX_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.trim().length === 0) {\n warnings.push('Feedback appears to contain only whitespace');\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates feedback tags.\n * Fails if count exceeds TAG_MAX_COUNT or if any tag doesn't match TAG_PATTERN.\n */\nexport function validateTags(tags: string[]): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (tags.length > VALIDATION_RULES.TAG_MAX_COUNT) {\n errors.push(\n `Too many tags: ${tags.length} (maximum is ${VALIDATION_RULES.TAG_MAX_COUNT})`,\n );\n }\n\n for (const tag of tags) {\n if (!VALIDATION_RULES.TAG_PATTERN.test(tag)) {\n errors.push(\n `Tag \"${tag}\" is invalid — must match /^[a-z0-9-]+$/ (lowercase letters, digits, hyphens only)`,\n );\n }\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates an app name.\n */\nexport function validateAppName(appName: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!appName || appName.trim() === '') {\n errors.push('appName is required');\n } else if (!VALIDATION_RULES.APP_NAME_PATTERN.test(appName)) {\n errors.push(\n `appName \"${appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates screenshot size.\n */\nexport function validateScreenshotSize(sizeBytes: number): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (sizeBytes > VALIDATION_RULES.SCREENSHOT_MAX_BYTES) {\n errors.push(\n `Screenshot size ${sizeBytes} bytes exceeds the maximum of ${VALIDATION_RULES.SCREENSHOT_MAX_BYTES} bytes (5 MB)`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates browser context.\n * Warns if url is empty; warns if consoleErrors exceeds 20 items.\n */\nexport function validateContext(context: FeedbackContext): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!context.url || context.url.trim() === '') {\n warnings.push('context.url is empty — this may indicate a capture logic error');\n }\n\n if (\n context.consoleErrors &&\n context.consoleErrors.length > VALIDATION_RULES.CONSOLE_ERRORS_MAX\n ) {\n warnings.push(\n `context.consoleErrors has ${context.consoleErrors.length} entries (expected ≤${VALIDATION_RULES.CONSOLE_ERRORS_MAX}); truncation should occur in capture layer`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Aggregate validator — runs all validators for a capture input.\n */\nexport function validateCaptureInput(input: Partial<CaptureInput>): ValidationResult {\n const allErrors: string[] = [];\n const allWarnings: string[] = [];\n\n if (input.content !== undefined) {\n const contentResult = validateContent(input.content);\n allErrors.push(...contentResult.errors);\n allWarnings.push(...contentResult.warnings);\n } else {\n allErrors.push('content is required');\n }\n\n if (input.tags !== undefined) {\n const tagResult = validateTags(input.tags);\n allErrors.push(...tagResult.errors);\n allWarnings.push(...tagResult.warnings);\n }\n\n if (input.appName !== undefined) {\n const nameResult = validateAppName(input.appName);\n allErrors.push(...nameResult.errors);\n allWarnings.push(...nameResult.warnings);\n } else {\n allErrors.push('appName is required');\n }\n\n if (input.context !== undefined) {\n const contextResult = validateContext(input.context);\n allErrors.push(...contextResult.errors);\n allWarnings.push(...contextResult.warnings);\n }\n\n return makeResult(allErrors, allWarnings);\n}\n","/**\n * Pure data transformation functions for DynamoDB key construction\n * and shape mapping.\n *\n * NO AWS SDK calls — only key construction and shape mapping.\n * All functions are pure: take plain objects, return plain objects.\n * No side effects.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n StoredFeedbackItem,\n CorrectionEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Key Builder\n// ─────────────────────────────────────────────────────────────────\n\n/** The six DynamoDB keys for a feedback item */\nexport interface FeedbackKeys {\n PK: string; // PROJECT#{appName}\n SK: string; // FB#{timestamp}#{id}\n GSI1PK: string; // STATUS#{status}\n GSI1SK: string; // {inverted-priority-padded}#{id}\n GSI2PK: string; // TYPE#{type}#PROJECT#{appName}\n GSI2SK: string; // {timestamp}\n}\n\n/**\n * Builds the six DynamoDB keys for a feedback item.\n *\n * @param appName - Application name (e.g. 'sagrosoft20')\n * @param id - Feedback ID (e.g. 'FB-2026-07-25-abc12345')\n * @param type - Feedback type\n * @param status - Current status\n * @param priority - Priority score 0–100\n * @param timestamp - ISO 8601 timestamp\n */\nexport function buildKeys(\n appName: string,\n id: string,\n type: FeedbackType,\n status: FeedbackStatus,\n priority: number,\n timestamp: string,\n): FeedbackKeys {\n const clampedPriority = Math.max(0, Math.min(100, priority));\n const inverted = 100 - clampedPriority;\n const paddedInverted = String(inverted).padStart(3, '0');\n\n return {\n PK: `PROJECT#${appName}`,\n SK: `FB#${timestamp}#${id}`,\n GSI1PK: `STATUS#${status}`,\n GSI1SK: `${paddedInverted}#${id}`,\n GSI2PK: `TYPE#${type}#PROJECT#${appName}`,\n GSI2SK: timestamp,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Key Parsers\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Extracts timestamp and id from a composite SK.\n * SK format: FB#{timestamp}#{id}\n */\nexport function parseSK(sk: string): { timestamp: string; id: string } {\n // SK format: FB#2026-07-25T10:00:00.000Z#FB-2026-07-25-abc12345\n const withoutPrefix = sk.startsWith('FB#') ? sk.slice(3) : sk;\n // Find the last '#' which separates timestamp from id\n const lastHash = withoutPrefix.lastIndexOf('#');\n if (lastHash === -1) {\n throw new Error(`Invalid SK format: \"${sk}\" — expected FB#{timestamp}#{id}`);\n }\n return {\n timestamp: withoutPrefix.slice(0, lastHash),\n id: withoutPrefix.slice(lastHash + 1),\n };\n}\n\n/**\n * Parses the GSI2PK into its component parts.\n * GSI2PK format: TYPE#{type}#PROJECT#{appName}\n */\nexport function parseGSI2PK(gsi2pk: string): { type: FeedbackType; appName: string } {\n // Format: TYPE#feature#PROJECT#oms4business\n const typeMatch = gsi2pk.match(/^TYPE#([^#]+)#PROJECT#(.+)$/);\n if (!typeMatch || !typeMatch[1] || !typeMatch[2]) {\n throw new Error(\n `Invalid GSI2PK format: \"${gsi2pk}\" — expected TYPE#{type}#PROJECT#{appName}`,\n );\n }\n return {\n type: typeMatch[1] as FeedbackType,\n appName: typeMatch[2],\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reporter Mutations (immutable — returns new arrays)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Adds a reporter to an item.\n * Returns new reporters array and updated reporterCount — does NOT mutate original.\n */\nexport function addReporter(\n item: StoredFeedbackItem,\n userId: string,\n addedContext?: string,\n nowIso?: string,\n): Pick<StoredFeedbackItem, 'reporters' | 'reporterCount' | 'GSI1SK'> {\n const addedAt = nowIso ?? new Date().toISOString();\n const newReporter: Reporter = { userId, addedAt };\n if (addedContext !== undefined) {\n newReporter.addedContext = addedContext;\n }\n\n // Spread to ensure immutability — does NOT mutate item.reporters\n const newReporters: Reporter[] = [...item.reporters, newReporter];\n const newCount = newReporters.length;\n\n // GSI1SK format preserves the inverted priority prefix, just uses same item id\n // The priority recalculation would normally happen in the capture layer\n // Here we preserve the existing GSI1SK structure (priority unchanged by reporter add)\n return {\n reporters: newReporters,\n reporterCount: newCount,\n GSI1SK: item.GSI1SK,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Correction Mutations (immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Appends a correction record to the corrections array.\n * Returns a new array — does NOT mutate the original.\n */\nexport function appendCorrection(\n corrections: CorrectionEntry[],\n correction: Omit<CorrectionEntry, 'correctedAt'>,\n nowIso?: string,\n): CorrectionEntry[] {\n const correctedAt = nowIso ?? new Date().toISOString();\n return [\n ...corrections,\n { ...correction, correctedAt },\n ];\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Status Transition\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Applies a status transition to an item.\n * Returns updated key fields + lastModifiedAt (does NOT mutate original).\n *\n * When transitioning to 'resolved': sets resolvedAt = nowIso.\n * When transitioning to any other status: resolvedAt is undefined.\n */\nexport function applyStatusTransition(\n item: StoredFeedbackItem,\n newStatus: FeedbackStatus,\n nowIso?: string,\n): Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n> {\n const now = nowIso ?? new Date().toISOString();\n\n // GSI1SK stays the same (priority doesn't change on status transition)\n // Only GSI1PK changes (new status)\n const resolvedAt: string | undefined = newStatus === 'resolved' ? now : undefined;\n const resolvedBy: string | undefined = undefined;\n\n return {\n status: newStatus,\n GSI1PK: `STATUS#${newStatus}`,\n GSI1SK: item.GSI1SK, // Priority unchanged\n lastModifiedAt: now,\n ...(resolvedAt !== undefined ? { resolvedAt } : {}),\n ...(resolvedBy !== undefined ? { resolvedBy } : {}),\n } as Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n >;\n}\n","/**\n * Domain error types for the feedback package.\n *\n * Every error class carries:\n * - code: string constant (usable in switch statements)\n * - retryable: boolean\n *\n * createErrorResponse converts any thrown value to a safe JSON-serialisable object.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Base Error Class\n// ─────────────────────────────────────────────────────────────────\n\n/** Base class for all feedback domain errors */\nexport abstract class FeedbackError extends Error {\n abstract readonly code: string;\n abstract readonly retryable: boolean;\n\n constructor(message: string, readonly cause?: unknown) {\n super(message);\n this.name = this.constructor.name;\n // Fix prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Concrete Error Classes\n// ─────────────────────────────────────────────────────────────────\n\n/** Thrown when input validation fails */\nexport class FeedbackValidationError extends FeedbackError {\n readonly code = 'FEEDBACK_VALIDATION_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly fields?: string[],\n ) {\n super(message);\n this.name = 'FeedbackValidationError';\n Object.setPrototypeOf(this, FeedbackValidationError.prototype);\n }\n}\n\n/** Thrown when the transport layer fails (network, server error) */\nexport class FeedbackTransportError extends FeedbackError {\n readonly code = 'FEEDBACK_TRANSPORT_ERROR';\n readonly retryable = true;\n\n constructor(\n message: string,\n readonly statusCode?: number,\n cause?: unknown,\n ) {\n super(message, cause);\n this.name = 'FeedbackTransportError';\n Object.setPrototypeOf(this, FeedbackTransportError.prototype);\n }\n}\n\n/** Thrown when configuration is invalid or missing */\nexport class FeedbackConfigError extends FeedbackError {\n readonly code = 'FEEDBACK_CONFIG_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackConfigError';\n Object.setPrototypeOf(this, FeedbackConfigError.prototype);\n }\n}\n\n/** Thrown when dialogue exchange fails */\nexport class FeedbackDialogueError extends FeedbackError {\n readonly code = 'FEEDBACK_DIALOGUE_ERROR';\n readonly retryable = true;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackDialogueError';\n Object.setPrototypeOf(this, FeedbackDialogueError.prototype);\n }\n}\n\n/** Thrown when screenshot capture or upload fails */\nexport class FeedbackScreenshotError extends FeedbackError {\n readonly code = 'FEEDBACK_SCREENSHOT_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackScreenshotError';\n Object.setPrototypeOf(this, FeedbackScreenshotError.prototype);\n }\n}\n\n/** Thrown when a duplicate feedback submission is detected */\nexport class FeedbackDuplicateError extends FeedbackError {\n readonly code = 'FEEDBACK_DUPLICATE_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly existingId: string,\n ) {\n super(message);\n this.name = 'FeedbackDuplicateError';\n Object.setPrototypeOf(this, FeedbackDuplicateError.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Type Guard\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Type guard — returns true if the value is a FeedbackError subclass.\n * Returns false for plain Error instances and other values.\n */\nexport function isFeedbackError(e: unknown): e is FeedbackError {\n return e instanceof FeedbackError;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Error Response Serialiser\n// ─────────────────────────────────────────────────────────────────\n\n/** Safe JSON-serialisable error response */\nexport interface ErrorResponse {\n code: string;\n message: string;\n retryable: boolean;\n fields?: string[]; // present for FeedbackValidationError\n existingId?: string; // present for FeedbackDuplicateError\n}\n\n/**\n * Converts any thrown value to a safe JSON-serialisable response object.\n *\n * - FeedbackError subclasses → uses their code, message, retryable\n * - FeedbackValidationError → includes fields\n * - FeedbackDuplicateError → includes existingId\n * - Plain string → { code: 'UNKNOWN_ERROR', message: theString, retryable: false }\n * - Plain Error → { code: 'UNKNOWN_ERROR', message: error.message, retryable: false }\n * - null/undefined/other → { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred', retryable: false }\n */\nexport function createErrorResponse(e: unknown): ErrorResponse {\n if (isFeedbackError(e)) {\n const base: ErrorResponse = {\n code: e.code,\n message: e.message,\n retryable: e.retryable,\n };\n\n if (e instanceof FeedbackValidationError && e.fields !== undefined) {\n base.fields = e.fields;\n }\n\n if (e instanceof FeedbackDuplicateError) {\n base.existingId = e.existingId;\n }\n\n return base;\n }\n\n if (typeof e === 'string') {\n return {\n code: 'UNKNOWN_ERROR',\n message: e,\n retryable: false,\n };\n }\n\n if (e instanceof Error) {\n return {\n code: 'UNKNOWN_ERROR',\n message: e.message,\n retryable: false,\n };\n }\n\n return {\n code: 'UNKNOWN_ERROR',\n message: 'An unknown error occurred',\n retryable: false,\n };\n}\n"],"names":["e"],"mappings":";;AAkDA,MAAM,gBAA8C;AAAA,EAClD,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,OAAO;AACT;AAMA,MAAM,oBAAyD;AAAA,EAC7D,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,aAAa;AACf;AAYO,SAAS,kBACd,MACA,eACA,eACA,aACQ;AACR,QAAM,OAAO,cAAc,IAAI;AAC/B,QAAM,MAAM,kBAAkB,eAAe,aAAa;AAC1D,QAAM,gBAAgB,KAAK,IAAI,gBAAgB,GAAG,CAAC,IAAI;AACvD,QAAM,kBAAkB,gBAAgB,IAAI;AAE5C,QAAM,MAAM,OAAO,MAAM,gBAAgB;AACzC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC;AACvC;AAUO,SAAS,mBAAmB,QAAyB;AAC1D,QAAM,OAAO,SAAS,OAAO,MAAM,GAAG,EAAE,KAAI,oBAAI,KAAA,GAAO,YAAA,EAAc,MAAM,GAAG,EAAE;AAChF,QAAM,QAAQ,OAAO,WAAA,EAAa,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,CAAC;AAC9D,SAAO,MAAM,IAAI,IAAI,KAAK;AAC5B;AAaO,SAAS,YAAY,UAAkB,IAAoB;AAChE,QAAM,WAAW,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC1D,QAAM,SAAS,OAAO,QAAQ,EAAE,SAAS,GAAG,GAAG;AAC/C,SAAO,GAAG,MAAM,IAAI,EAAE;AACxB;AAiBO,SAAS,uBACd,OACA,QACoB;AACpB,QAAM,MAAM,WAAU,oBAAI,KAAA,GAAO,YAAA;AACjC,QAAM,KAAK,mBAAmB,GAAG;AACjC,QAAM,SAAyB;AAC/B,QAAM,OAAO,MAAM,QAAQ,CAAA;AAC3B,QAAM,cAAc,MAAM;AAE1B,QAAM,WAAW,kBAAkB,MAAM,MAAM,GAAG,MAAM,eAAe,WAAW;AAElF,QAAM,WAAqB;AAAA,IACzB,QAAQ,MAAM;AAAA,IACd,SAAS;AAAA,EAAA;AAGX,QAAM,KAAK,WAAW,MAAM,OAAO;AACnC,QAAM,KAAK,MAAM,GAAG,IAAI,EAAE;AAC1B,QAAM,SAAS,UAAU,MAAM;AAC/B,QAAM,SAAS,YAAY,UAAU,EAAE;AACvC,QAAM,SAAS,QAAQ,MAAM,IAAI,YAAY,MAAM,OAAO;AAC1D,QAAM,SAAS;AAEf,QAAM,OAA2B;AAAA,IAC/B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,MAAM;AAAA,IACf,MAAM,MAAM;AAAA,IACZ;AAAA,IACA;AAAA,IACA,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,eAAe,MAAM;AAAA,IACrB,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,aAAa,MAAM;AAAA,IACnB,WAAW,CAAC,QAAQ;AAAA,IACpB,eAAe;AAAA,IACf,iBAAiB,MAAM,mBAAmB,CAAA;AAAA,IAC1C,aAAa,CAAA;AAAA,IACb;AAAA,EAAA;AAIF,MAAI,MAAM,oBAAoB,QAAW;AACvC,SAAK,kBAAkB,MAAM;AAAA,EAC/B;AACA,MAAI,MAAM,gBAAgB,QAAW;AACnC,SAAK,cAAc,MAAM;AAAA,EAC3B;AACA,MAAI,MAAM,YAAY,QAAW;AAC/B,SAAK,UAAU,MAAM;AAAA,EACvB;AACA,MAAI,MAAM,YAAY,QAAW;AAC/B,SAAK,UAAU,MAAM;AAAA,EACvB;AACA,MAAI,MAAM,mBAAmB,QAAW;AACtC,SAAK,iBAAiB,MAAM;AAAA,EAC9B;AACA,MAAI,MAAM,UAAU,QAAW;AAC7B,SAAK,QAAQ,MAAM;AAAA,EACrB;AACA,MAAI,MAAM,SAAS,QAAW;AAC5B,SAAK,OAAO,MAAM;AAAA,EACpB;AAEA,SAAO;AACT;ACjJA,MAAM,4BAA8C;AAAA,EAClD,SAAS;AAAA,EACT,UAAU;AAAA,EACV,sBAAsB;AACxB;AAUO,SAAS,wBAAyC;AACvD,QAAM,cAAoC,CAAA;AAC1C,QAAM,oBAAwC,CAAA;AAC9C,QAAM,sBAAiE,CAAA;AAEvE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IAEA,MAAM,OAAO,MAAiD;AAC5D,kBAAY,KAAK,IAAI;AACrB,aAAO,EAAE,SAAS,MAAM,YAAY,KAAK,GAAA;AAAA,IAC3C;AAAA,IAEA,MAAM,SACJ,YACA,SACA,SAC2B;AAI3B,UAAI,kBAAkB,SAAS,GAAG;AAChC,eAAO,kBAAkB,MAAA;AAAA,MAC3B;AACA,aAAO,EAAE,GAAG,0BAAA;AAAA,IACd;AAAA,IAEA,MAAM,OAAO,YAAoB,MAAmC;AAClE,0BAAoB,KAAK,EAAE,YAAY,KAAA,CAAM;AAC7C,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,eAAe,UAAU;AAAA,MAAA;AAAA,IAEpC;AAAA,IAEA,QAAQ;AACN,kBAAY,SAAS;AACrB,wBAAkB,SAAS;AAC3B,0BAAoB,SAAS;AAAA,IAC/B;AAAA,EAAA;AAEJ;ACrGO,MAAM,mBAAmB;AAAA,EAC9B,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,eAAe;AAAA,EACf,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,sBAAsB;AAAA;AAAA,EACtB,oBAAoB;AACtB;AAaA,SAAS,WAAW,QAAkB,UAAsC;AAC1E,SAAO,EAAE,OAAO,OAAO,WAAW,GAAG,QAAQ,SAAA;AAC/C;AAWO,SAAS,gBAAgB,SAAmC;AACjE,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,QAAQ,SAAS,iBAAiB,oBAAoB;AACxD,WAAO;AAAA,MACL,6BAA6B,iBAAiB,kBAAkB,oBAAoB,QAAQ,MAAM;AAAA,IAAA;AAAA,EAEtG,WAAW,QAAQ,SAAS,iBAAiB,oBAAoB;AAC/D,WAAO;AAAA,MACL,4BAA4B,iBAAiB,kBAAkB,oBAAoB,QAAQ,MAAM;AAAA,IAAA;AAAA,EAErG,WAAW,QAAQ,KAAA,EAAO,WAAW,GAAG;AACtC,aAAS,KAAK,6CAA6C;AAAA,EAC7D;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAMO,SAAS,aAAa,MAAkC;AAC7D,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,KAAK,SAAS,iBAAiB,eAAe;AAChD,WAAO;AAAA,MACL,kBAAkB,KAAK,MAAM,gBAAgB,iBAAiB,aAAa;AAAA,IAAA;AAAA,EAE/E;AAEA,aAAW,OAAO,MAAM;AACtB,QAAI,CAAC,iBAAiB,YAAY,KAAK,GAAG,GAAG;AAC3C,aAAO;AAAA,QACL,QAAQ,GAAG;AAAA,MAAA;AAAA,IAEf;AAAA,EACF;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAKO,SAAS,gBAAgB,SAAmC;AACjE,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,CAAC,WAAW,QAAQ,KAAA,MAAW,IAAI;AACrC,WAAO,KAAK,qBAAqB;AAAA,EACnC,WAAW,CAAC,iBAAiB,iBAAiB,KAAK,OAAO,GAAG;AAC3D,WAAO;AAAA,MACL,YAAY,OAAO;AAAA,IAAA;AAAA,EAEvB;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAKO,SAAS,uBAAuB,WAAqC;AAC1E,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,YAAY,iBAAiB,sBAAsB;AACrD,WAAO;AAAA,MACL,mBAAmB,SAAS,iCAAiC,iBAAiB,oBAAoB;AAAA,IAAA;AAAA,EAEtG;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAMO,SAAS,gBAAgB,SAA4C;AAC1E,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,CAAC,QAAQ,OAAO,QAAQ,IAAI,KAAA,MAAW,IAAI;AAC7C,aAAS,KAAK,gEAAgE;AAAA,EAChF;AAEA,MACE,QAAQ,iBACR,QAAQ,cAAc,SAAS,iBAAiB,oBAChD;AACA,aAAS;AAAA,MACP,6BAA6B,QAAQ,cAAc,MAAM,uBAAuB,iBAAiB,kBAAkB;AAAA,IAAA;AAAA,EAEvH;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAKO,SAAS,qBAAqB,OAAgD;AACnF,QAAM,YAAsB,CAAA;AAC5B,QAAM,cAAwB,CAAA;AAE9B,MAAI,MAAM,YAAY,QAAW;AAC/B,UAAM,gBAAgB,gBAAgB,MAAM,OAAO;AACnD,cAAU,KAAK,GAAG,cAAc,MAAM;AACtC,gBAAY,KAAK,GAAG,cAAc,QAAQ;AAAA,EAC5C,OAAO;AACL,cAAU,KAAK,qBAAqB;AAAA,EACtC;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,YAAY,aAAa,MAAM,IAAI;AACzC,cAAU,KAAK,GAAG,UAAU,MAAM;AAClC,gBAAY,KAAK,GAAG,UAAU,QAAQ;AAAA,EACxC;AAEA,MAAI,MAAM,YAAY,QAAW;AAC/B,UAAM,aAAa,gBAAgB,MAAM,OAAO;AAChD,cAAU,KAAK,GAAG,WAAW,MAAM;AACnC,gBAAY,KAAK,GAAG,WAAW,QAAQ;AAAA,EACzC,OAAO;AACL,cAAU,KAAK,qBAAqB;AAAA,EACtC;AAEA,MAAI,MAAM,YAAY,QAAW;AAC/B,UAAM,gBAAgB,gBAAgB,MAAM,OAAO;AACnD,cAAU,KAAK,GAAG,cAAc,MAAM;AACtC,gBAAY,KAAK,GAAG,cAAc,QAAQ;AAAA,EAC5C;AAEA,SAAO,WAAW,WAAW,WAAW;AAC1C;ACzJO,SAAS,UACd,SACA,IACA,MACA,QACA,UACA,WACc;AACd,QAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC3D,QAAM,WAAW,MAAM;AACvB,QAAM,iBAAiB,OAAO,QAAQ,EAAE,SAAS,GAAG,GAAG;AAEvD,SAAO;AAAA,IACL,IAAI,WAAW,OAAO;AAAA,IACtB,IAAI,MAAM,SAAS,IAAI,EAAE;AAAA,IACzB,QAAQ,UAAU,MAAM;AAAA,IACxB,QAAQ,GAAG,cAAc,IAAI,EAAE;AAAA,IAC/B,QAAQ,QAAQ,IAAI,YAAY,OAAO;AAAA,IACvC,QAAQ;AAAA,EAAA;AAEZ;AAUO,SAAS,QAAQ,IAA+C;AAErE,QAAM,gBAAgB,GAAG,WAAW,KAAK,IAAI,GAAG,MAAM,CAAC,IAAI;AAE3D,QAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,MAAI,aAAa,IAAI;AACnB,UAAM,IAAI,MAAM,uBAAuB,EAAE,kCAAkC;AAAA,EAC7E;AACA,SAAO;AAAA,IACL,WAAW,cAAc,MAAM,GAAG,QAAQ;AAAA,IAC1C,IAAI,cAAc,MAAM,WAAW,CAAC;AAAA,EAAA;AAExC;AAMO,SAAS,YAAY,QAAyD;AAEnF,QAAM,YAAY,OAAO,MAAM,6BAA6B;AAC5D,MAAI,CAAC,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG;AAChD,UAAM,IAAI;AAAA,MACR,2BAA2B,MAAM;AAAA,IAAA;AAAA,EAErC;AACA,SAAO;AAAA,IACL,MAAM,UAAU,CAAC;AAAA,IACjB,SAAS,UAAU,CAAC;AAAA,EAAA;AAExB;AAUO,SAAS,YACd,MACA,QACA,cACA,QACoE;AACpE,QAAM,UAAU,WAAU,oBAAI,KAAA,GAAO,YAAA;AACrC,QAAM,cAAwB,EAAE,QAAQ,QAAA;AACxC,MAAI,iBAAiB,QAAW;AAC9B,gBAAY,eAAe;AAAA,EAC7B;AAGA,QAAM,eAA2B,CAAC,GAAG,KAAK,WAAW,WAAW;AAChE,QAAM,WAAW,aAAa;AAK9B,SAAO;AAAA,IACL,WAAW;AAAA,IACX,eAAe;AAAA,IACf,QAAQ,KAAK;AAAA,EAAA;AAEjB;AAUO,SAAS,iBACd,aACA,YACA,QACmB;AACnB,QAAM,cAAc,WAAU,oBAAI,KAAA,GAAO,YAAA;AACzC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,EAAE,GAAG,YAAY,YAAA;AAAA,EAAY;AAEjC;AAaO,SAAS,sBACd,MACA,WACA,QAIA;AACA,QAAM,MAAM,WAAU,oBAAI,KAAA,GAAO,YAAA;AAIjC,QAAM,aAAiC,cAAc,aAAa,MAAM;AAGxE,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,UAAU,SAAS;AAAA,IAC3B,QAAQ,KAAK;AAAA;AAAA,IACb,gBAAgB;AAAA,IAChB,GAAI,eAAe,SAAY,EAAE,WAAA,IAAe,CAAA;AAAA,IAChD,GAAgD,CAAA;AAAA,EAAC;AAKrD;ACnLO,MAAe,sBAAsB,MAAM;AAAA,EAIhD,YAAY,SAA0B,OAAiB;AACrD,UAAM,OAAO;AADuB,SAAA,QAAA;AAEpC,SAAK,OAAO,KAAK,YAAY;AAE7B,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AACF;AAOO,MAAM,gCAAgC,cAAc;AAAA,EAIzD,YACE,SACS,QACT;AACA,UAAM,OAAO;AAFJ,SAAA,SAAA;AAGT,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,wBAAwB,SAAS;AAAA,EAC/D;AAAA,EAVS,OAAO;AAAA,EACP,YAAY;AAUvB;AAGO,MAAM,+BAA+B,cAAc;AAAA,EAIxD,YACE,SACS,YACT,OACA;AACA,UAAM,SAAS,KAAK;AAHX,SAAA,aAAA;AAIT,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,uBAAuB,SAAS;AAAA,EAC9D;AAAA,EAXS,OAAO;AAAA,EACP,YAAY;AAWvB;AAGO,MAAM,4BAA4B,cAAc;AAAA,EAC5C,OAAO;AAAA,EACP,YAAY;AAAA,EAErB,YAAY,SAAiB,OAAiB;AAC5C,UAAM,SAAS,KAAK;AACpB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,oBAAoB,SAAS;AAAA,EAC3D;AACF;AAGO,MAAM,8BAA8B,cAAc;AAAA,EAC9C,OAAO;AAAA,EACP,YAAY;AAAA,EAErB,YAAY,SAAiB,OAAiB;AAC5C,UAAM,SAAS,KAAK;AACpB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,sBAAsB,SAAS;AAAA,EAC7D;AACF;AAGO,MAAM,gCAAgC,cAAc;AAAA,EAChD,OAAO;AAAA,EACP,YAAY;AAAA,EAErB,YAAY,SAAiB,OAAiB;AAC5C,UAAM,SAAS,KAAK;AACpB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,wBAAwB,SAAS;AAAA,EAC/D;AACF;AAGO,MAAM,+BAA+B,cAAc;AAAA,EAIxD,YACE,SACS,YACT;AACA,UAAM,OAAO;AAFJ,SAAA,aAAA;AAGT,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,uBAAuB,SAAS;AAAA,EAC9D;AAAA,EAVS,OAAO;AAAA,EACP,YAAY;AAUvB;AAUO,SAAS,gBAAgBA,IAAgC;AAC9D,SAAOA,cAAa;AACtB;AAyBO,SAAS,oBAAoBA,IAA2B;AAC7D,MAAI,gBAAgBA,EAAC,GAAG;AACtB,UAAM,OAAsB;AAAA,MAC1B,MAAMA,GAAE;AAAA,MACR,SAASA,GAAE;AAAA,MACX,WAAWA,GAAE;AAAA,IAAA;AAGf,QAAIA,cAAa,2BAA2BA,GAAE,WAAW,QAAW;AAClE,WAAK,SAASA,GAAE;AAAA,IAClB;AAEA,QAAIA,cAAa,wBAAwB;AACvC,WAAK,aAAaA,GAAE;AAAA,IACtB;AAEA,WAAO;AAAA,EACT;AAEA,MAAI,OAAOA,OAAM,UAAU;AACzB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAASA;AAAA,MACT,WAAW;AAAA,IAAA;AAAA,EAEf;AAEA,MAAIA,cAAa,OAAO;AACtB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAASA,GAAE;AAAA,MACX,WAAW;AAAA,IAAA;AAAA,EAEf;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,EAAA;AAEf;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/core/capture.ts","../src/core/transport.ts","../src/core/ingest-transport.ts","../src/core/validation.ts","../src/core/transforms.ts","../src/core/errors.ts"],"sourcesContent":["/**\n * Capture logic and priority calculation for feedback submissions.\n *\n * Framework-agnostic — no browser APIs, no DOM, no AWS.\n * Pure data transformation from user input to StoredFeedbackItem.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n FeedbackEnvironment,\n FeedbackScope,\n FeedbackSize,\n FeedbackContext,\n StoredFeedbackItem,\n DialogueEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Input Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Input for processFeedbackCapture */\nexport interface CaptureInput {\n type: FeedbackType;\n content: string;\n context: FeedbackContext;\n submittedBy: string;\n appName: string;\n hasScreenshot: boolean;\n screenshotS3Key?: string;\n environment?: FeedbackEnvironment;\n version?: string;\n tags?: string[];\n traceId?: string;\n dialogueHistory?: DialogueEntry[];\n classification?: string;\n scope?: FeedbackScope;\n size?: FeedbackSize;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Priority Formula\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Base priority scores by feedback type.\n * bug=60, ux=40, feature=20, other=10\n */\nconst BASE_PRIORITY: Record<FeedbackType, number> = {\n bug: 60,\n ux: 40,\n feature: 20,\n other: 10,\n};\n\n/**\n * Environment bonus scores.\n * production=+20, staging=+10, development=0\n */\nconst ENVIRONMENT_BONUS: Record<FeedbackEnvironment, number> = {\n production: 20,\n staging: 10,\n development: 0,\n};\n\n/**\n * Calculates the priority score for a feedback item.\n *\n * Formula:\n * base = baseByType (bug=60, ux=40, feature=20, other=10)\n * env = environmentBonus (production=+20, staging=+10, development=0)\n * rep = min(reporterCount - 1, 5) * 4 (+4 per additional reporter, max +20)\n * ss = hasScreenshot ? +5 : 0\n * priority = clamp(0, 100, base + env + rep + ss)\n */\nexport function calculatePriority(\n type: FeedbackType,\n reporterCount: number,\n hasScreenshot: boolean,\n environment?: FeedbackEnvironment,\n): number {\n const base = BASE_PRIORITY[type];\n const env = ENVIRONMENT_BONUS[environment ?? 'development'];\n const reporterBonus = Math.min(reporterCount - 1, 5) * 4;\n const screenshotBonus = hasScreenshot ? 5 : 0;\n\n const raw = base + env + reporterBonus + screenshotBonus;\n return Math.max(0, Math.min(100, raw));\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ID Generation\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Generates a unique feedback ID in the format: FB-YYYY-MM-DD-{uuid8}\n * Uses crypto.randomUUID() for uniqueness; truncates to 8 chars.\n */\nexport function generateFeedbackId(nowIso?: string): string {\n const date = nowIso ? nowIso.slice(0, 10) : new Date().toISOString().slice(0, 10);\n const uuid8 = crypto.randomUUID().replace(/-/g, '').slice(0, 8);\n return `FB-${date}-${uuid8}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// GSI1SK Builder\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Builds the GSI1SK sort key value.\n * Format: {3-digit-inverted-priority}#{id}\n * Example: priority 85 → '015#FB-2026-07-25-abc12345'\n *\n * Inverted so DynamoDB ascending sort returns highest-priority first.\n */\nexport function buildGSI1SK(priority: number, id: string): string {\n const inverted = 100 - Math.max(0, Math.min(100, priority));\n const padded = String(inverted).padStart(3, '0');\n return `${padded}#${id}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Main Capture Processor\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Processes a feedback capture input into a StoredFeedbackItem\n * ready for the transport layer.\n *\n * Sets:\n * - status: 'open'\n * - reporterCount: 1\n * - reporters: [{ userId: submittedBy, addedAt: nowIso }]\n * - tags: [] when omitted\n * - All DynamoDB key fields (PK, SK, GSI1PK, GSI1SK, GSI2PK, GSI2SK)\n */\nexport function processFeedbackCapture(\n input: CaptureInput,\n nowIso?: string,\n): StoredFeedbackItem {\n const now = nowIso ?? new Date().toISOString();\n const id = generateFeedbackId(now);\n const status: FeedbackStatus = 'open';\n const tags = input.tags ?? [];\n const environment = input.environment;\n\n const priority = calculatePriority(input.type, 1, input.hasScreenshot, environment);\n\n const reporter: Reporter = {\n userId: input.submittedBy,\n addedAt: now,\n };\n\n const PK = `PROJECT#${input.appName}`;\n const SK = `FB#${now}#${id}`;\n const GSI1PK = `STATUS#${status}`;\n const GSI1SK = buildGSI1SK(priority, id);\n const GSI2PK = `TYPE#${input.type}#PROJECT#${input.appName}`;\n const GSI2SK = now;\n\n const item: StoredFeedbackItem = {\n PK,\n SK,\n GSI1PK,\n GSI1SK,\n GSI2PK,\n GSI2SK,\n id,\n appName: input.appName,\n type: input.type,\n status,\n priority,\n content: input.content,\n context: input.context,\n hasScreenshot: input.hasScreenshot,\n submittedAt: now,\n lastModifiedAt: now,\n submittedBy: input.submittedBy,\n reporters: [reporter],\n reporterCount: 1,\n dialogueHistory: input.dialogueHistory ?? [],\n corrections: [],\n tags,\n };\n\n // Optional fields — only set when defined\n if (input.screenshotS3Key !== undefined) {\n item.screenshotS3Key = input.screenshotS3Key;\n }\n if (input.environment !== undefined) {\n item.environment = input.environment;\n }\n if (input.version !== undefined) {\n item.version = input.version;\n }\n if (input.traceId !== undefined) {\n item.traceId = input.traceId;\n }\n if (input.classification !== undefined) {\n item.classification = input.classification;\n }\n if (input.scope !== undefined) {\n item.scope = input.scope;\n }\n if (input.size !== undefined) {\n item.size = input.size;\n }\n\n return item;\n}\n","/**\n * Transport contracts for the feedback package.\n *\n * Defines abstract interfaces that decouple core logic from\n * any specific implementation. Implementations live in the\n * consuming app's server handlers (slice 03).\n *\n * NO AWS SDK imports in this file.\n */\n\nimport type {\n StoredFeedbackItem,\n SubmitResult,\n DialogueResponse,\n UploadResult,\n DialogueEntry,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Transport Interfaces\n// ─────────────────────────────────────────────────────────────────\n\n/** What the React adapter calls at submit time */\nexport interface FeedbackSubmitTransport {\n submit(item: StoredFeedbackItem): Promise<SubmitResult>;\n}\n\n/** What the dialogue UI calls on each exchange */\nexport interface FeedbackDialogueTransport {\n exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse>;\n}\n\n/** What the screenshot capture calls after html2canvas */\nexport interface FeedbackScreenshotTransport {\n upload(feedbackId: string, blob: Blob): Promise<UploadResult>;\n}\n\n/**\n * Composite transport — all three combined for full-feature transport.\n * Dialogue and screenshot are optional so implementations can omit them.\n */\nexport interface FeedbackTransport\n extends FeedbackSubmitTransport,\n Partial<FeedbackDialogueTransport>,\n Partial<FeedbackScreenshotTransport> {}\n\n// ─────────────────────────────────────────────────────────────────\n// Memory Transport (for testing)\n// ─────────────────────────────────────────────────────────────────\n\n/** Memory transport with access to internal state for assertions */\nexport interface MemoryTransport extends FeedbackTransport {\n /** All submitted items */\n submissions: StoredFeedbackItem[];\n /** Queue of dialogue responses to return in order */\n dialogueResponses: DialogueResponse[];\n /** Uploaded screenshot blobs */\n uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }>;\n /** Clear all stored state */\n clear(): void;\n}\n\nconst DEFAULT_DIALOGUE_RESPONSE: DialogueResponse = {\n success: true,\n response: 'Thank you for your feedback. Can you tell me more?',\n conversationComplete: false,\n};\n\n/**\n * Creates an in-memory transport for testing.\n *\n * - submit(): stores the item and returns { success: true, feedbackId: item.id }\n * - exchange(): returns the first queued response, or a default stub\n * - upload(): records the blob reference\n * - Does NOT import any AWS SDK\n */\nexport function createMemoryTransport(): MemoryTransport {\n const submissions: StoredFeedbackItem[] = [];\n const dialogueResponses: DialogueResponse[] = [];\n const uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }> = [];\n\n return {\n submissions,\n dialogueResponses,\n uploadedScreenshots,\n\n async submit(item: StoredFeedbackItem): Promise<SubmitResult> {\n submissions.push(item);\n return { success: true, feedbackId: item.id };\n },\n\n async exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse> {\n void feedbackId;\n void message;\n void history;\n if (dialogueResponses.length > 0) {\n return dialogueResponses.shift() as DialogueResponse;\n }\n return { ...DEFAULT_DIALOGUE_RESPONSE };\n },\n\n async upload(feedbackId: string, blob: Blob): Promise<UploadResult> {\n uploadedScreenshots.push({ feedbackId, blob });\n return {\n success: true,\n s3Key: `screenshots/${feedbackId}.png`,\n };\n },\n\n clear() {\n submissions.length = 0;\n dialogueResponses.length = 0;\n uploadedScreenshots.length = 0;\n },\n };\n}\n","/**\n * ingest-transport.ts — central-ingest implementation of FeedbackSubmitTransport.\n *\n * The central-storage principle: a consuming application reaches central feedback storage ONLY through\n * an authenticated API — it mints a short-lived OAuth2 client-credentials JWT and POSTs the feedback\n * CONTENT to the generic `/records/{collection}` ingest endpoint. The server attributes every write to\n * the verified token identity (its `client_id` claim) and stamps the canonical keys (PK/SK/GSI); the\n * application holds no AWS principal and sends no partition/identity fields. This replaces the bespoke\n * direct-DynamoDB writer (`FeedbackService.put`) — the pattern the principle removes.\n *\n * This transport is the client half of that contract, matching:\n * - the deployed ingest API (`POST /records/feedback`, body `{ item }`, response `{ success, ids }`),\n * - ralph's `ingest-client.mjs` (same route + shape),\n * - deploy-engine's `ingest-verify` (same token mint + write).\n *\n * Zero runtime dependencies — uses the built-in `fetch` (keeps the package browser-safe and SDK-free).\n * The caller supplies the app-client credential (or a token provider); the SECRET is resolved by the\n * host out-of-band (SSM/password manager) and never embedded here. HTTPS is enforced — a token or\n * record sent over plaintext is a compromise.\n *\n * NO AWS SDK imports in this file.\n */\n\nimport type { StoredFeedbackItem, SubmitResult } from './types.js';\nimport type { FeedbackSubmitTransport } from './transport.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\n/** The ingest collection feedback writes to (the deployed registry maps it to `ralph-feedback`). */\nexport const FEEDBACK_COLLECTION = 'feedback';\n/** OAuth2 scope required to write via the ingest API. */\nexport const INGEST_WRITE_SCOPE = 'assets/write';\n\n/** Minimal fetch surface (so the package needs no DOM lib types to compile). */\nexport type FetchLike = (\n url: string,\n init?: { method?: string; headers?: Record<string, string>; body?: string },\n) => Promise<{ status: number; ok: boolean; json: () => Promise<unknown> }>;\n\n/**\n * How the transport obtains a bearer token. Either:\n * - supply `clientId` + `clientSecret` + `tokenUrl` and let the transport mint/refresh it, OR\n * - supply a `tokenProvider()` (e.g. the host already manages the token) — takes precedence.\n */\nexport interface IngestTransportConfig {\n /** Ingest API base URL (HTTPS), e.g. https://xxxx.execute-api.<region>.amazonaws.com */\n apiUrl: string;\n /** OAuth2 token endpoint (HTTPS) — required unless `tokenProvider` is given. */\n tokenUrl?: string;\n /** App-client id (the app identity the server attributes writes to). Required unless `tokenProvider`. */\n clientId?: string;\n /** App-client secret. SHOULD come from the host's secret store; required unless `tokenProvider`. */\n clientSecret?: string;\n /** Supply a token directly (overrides client-credentials minting). */\n tokenProvider?: () => Promise<string>;\n /** Collection name (default 'feedback'). */\n collection?: string;\n /** Seconds before expiry to proactively re-mint (default 60). */\n refreshSkewSeconds?: number;\n /** Injectable fetch (tests). Defaults to global fetch. */\n fetchImpl?: FetchLike;\n}\n\nfunction assertHttps(url: string, label: string): void {\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n throw new Error(`ingest-transport: ${label} is not a valid URL: ${url}`);\n }\n if (u.protocol !== 'https:') {\n throw new Error(`ingest-transport: ${label} must be https:// (got ${u.protocol}//) — refusing plaintext`);\n }\n}\n\n/** Identity/partition + local-only fields the server owns — stripped before sending the content. */\nconst SERVER_OWNED_FIELDS: readonly string[] = [\n 'PK', 'SK', 'GSI1PK', 'GSI1SK', 'GSI2PK', 'GSI2SK',\n];\n\n/** Strip the fields the server stamps from the verified identity (never sent/trusted from the client). */\nfunction toContent(item: StoredFeedbackItem): Record<string, unknown> {\n const content: Record<string, unknown> = { ...(item as unknown as Record<string, unknown>) };\n for (const f of SERVER_OWNED_FIELDS) delete content[f];\n return content;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Transport\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a FeedbackSubmitTransport that writes to the central ingest API.\n *\n * Usage (host app wires the credential from its secret store):\n * const transport = createIngestTransport({\n * apiUrl: process.env.FEEDBACK_INGEST_URL!,\n * tokenUrl: process.env.FEEDBACK_TOKEN_URL!,\n * clientId: process.env.FEEDBACK_CLIENT_ID!,\n * clientSecret: await resolveSecret('feedback/ingest/<app>'),\n * });\n * await transport.submit(item); // → { success, feedbackId }\n */\nexport function createIngestTransport(config: IngestTransportConfig): FeedbackSubmitTransport {\n const collection = config.collection ?? FEEDBACK_COLLECTION;\n const refreshSkewMs = (config.refreshSkewSeconds ?? 60) * 1000;\n const doFetch = config.fetchImpl ?? (globalThis.fetch as unknown as FetchLike);\n\n assertHttps(config.apiUrl, 'apiUrl');\n if (!config.tokenProvider) {\n if (!config.tokenUrl || !config.clientId || !config.clientSecret) {\n throw new Error('ingest-transport: provide tokenProvider, OR tokenUrl + clientId + clientSecret');\n }\n assertHttps(config.tokenUrl, 'tokenUrl');\n }\n if (typeof doFetch !== 'function') {\n throw new Error('ingest-transport: no fetch implementation available');\n }\n\n // Cached minted token (when minting ourselves).\n let cachedToken: string | null = null;\n let cachedExpiryMs = 0;\n\n async function mintToken(): Promise<string> {\n const basic = toBase64(`${config.clientId}:${config.clientSecret}`);\n const res = await doFetch(config.tokenUrl as string, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', authorization: `Basic ${basic}` },\n body: `grant_type=client_credentials&scope=${encodeURIComponent(INGEST_WRITE_SCOPE)}`,\n });\n const data = (await res.json()) as { access_token?: string; expires_in?: number; error?: string; error_description?: string };\n if (!res.ok || !data.access_token) {\n throw new Error(`token mint failed (${res.status}): ${data.error ?? ''} ${data.error_description ?? ''}`.trim());\n }\n cachedToken = data.access_token;\n cachedExpiryMs = Date.now() + Math.max(0, (data.expires_in ?? 3600) * 1000 - refreshSkewMs);\n return cachedToken;\n }\n\n async function getToken(): Promise<string> {\n if (config.tokenProvider) return config.tokenProvider();\n if (cachedToken && Date.now() < cachedExpiryMs) return cachedToken;\n return mintToken();\n }\n\n return {\n async submit(item: StoredFeedbackItem): Promise<SubmitResult> {\n try {\n const token = await getToken();\n const base = config.apiUrl.replace(/\\/$/, '');\n const res = await doFetch(`${base}/records/${encodeURIComponent(collection)}`, {\n method: 'POST',\n headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ item: toContent(item) }),\n });\n const data = (await res.json().catch(() => ({}))) as { success?: boolean; ids?: string[]; error?: string; errors?: string[] };\n if (res.status === 200 && data.success === true) {\n return { success: true, feedbackId: data.ids?.[0] ?? item.id };\n }\n // Explicit failure — never a silent drop. 5xx is retryable; 4xx (validation/auth) is not.\n const retryable = res.status >= 500;\n const msg = data.error ?? data.errors?.join('; ') ?? `HTTP ${res.status}`;\n return { success: false, feedbackId: item.id, error: `ingest write failed: ${msg}`, retryable };\n } catch (err) {\n // Transport/network failure — retryable (the caller may buffer + retry).\n return { success: false, feedbackId: item.id, error: (err as Error).message, retryable: true };\n }\n },\n };\n}\n\n/** Base64 encode a string in both Node and browser (zero-dep). */\nfunction toBase64(s: string): string {\n if (typeof btoa === 'function') return btoa(s);\n // Node fallback — Buffer exists; typed loosely to avoid a hard @types/node coupling in the browser build.\n const B = (globalThis as unknown as { Buffer?: { from: (x: string) => { toString: (enc: string) => string } } }).Buffer;\n if (B) return B.from(s).toString('base64');\n throw new Error('ingest-transport: no base64 encoder available');\n}\n","/**\n * Input validation functions for the feedback package.\n *\n * Used both client-side (before submission) and server-side (in handler).\n * No external dependencies — pure validation logic.\n *\n * Acceptance criteria evidence:\n * AC-65: createErrorResponse(new FeedbackValidationError('…', ['content']))\n * ])) includes fields: ['content'] → shown in errors.ts\n * AC-41: VALIDATION_RULES is exported as const (immutable) with values:\n * CONTENT_MIN_LENGTH=10, CONTENT_MAX_LENGTH=5000, TAG_MAX_COUNT=10\n * TAG_PATTERN=/^[a-z0-9-]+$/, [a-z0-9-] pattern applied\n */\n\nimport type { FeedbackContext } from './types.js';\nimport type { CaptureInput } from './capture.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Validation Rules (exported as const — immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/** Immutable validation constants */\nexport const VALIDATION_RULES = {\n CONTENT_MIN_LENGTH: 10,\n CONTENT_MAX_LENGTH: 5_000,\n TAG_MAX_COUNT: 10,\n TAG_PATTERN: /^[a-z0-9-]+$/,\n APP_NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,\n SCREENSHOT_MAX_BYTES: 5_242_880, // 5 MB\n CONSOLE_ERRORS_MAX: 20,\n} as const;\n\n// ─────────────────────────────────────────────────────────────────\n// Result Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Validation result with user-facing messages */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[]; // Blocking — submission must be rejected\n warnings: string[]; // Non-blocking — suggestions only\n}\n\nfunction makeResult(errors: string[], warnings: string[]): ValidationResult {\n return { valid: errors.length === 0, errors, warnings };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Individual Validators\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Validates feedback content.\n * Fails below CONTENT_MIN_LENGTH (\"Feedback must be at least 10 characters\"),\n * fails above CONTENT_MAX_LENGTH, warns if only whitespace.\n */\nexport function validateContent(content: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (content.length < VALIDATION_RULES.CONTENT_MIN_LENGTH) {\n errors.push(\n `Feedback must be at least ${VALIDATION_RULES.CONTENT_MIN_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.length > VALIDATION_RULES.CONTENT_MAX_LENGTH) {\n errors.push(\n `Feedback must not exceed ${VALIDATION_RULES.CONTENT_MAX_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.trim().length === 0) {\n warnings.push('Feedback appears to contain only whitespace');\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates feedback tags.\n * Fails if count exceeds TAG_MAX_COUNT or if any tag doesn't match TAG_PATTERN.\n */\nexport function validateTags(tags: string[]): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (tags.length > VALIDATION_RULES.TAG_MAX_COUNT) {\n errors.push(\n `Too many tags: ${tags.length} (maximum is ${VALIDATION_RULES.TAG_MAX_COUNT})`,\n );\n }\n\n for (const tag of tags) {\n if (!VALIDATION_RULES.TAG_PATTERN.test(tag)) {\n errors.push(\n `Tag \"${tag}\" is invalid — must match /^[a-z0-9-]+$/ (lowercase letters, digits, hyphens only)`,\n );\n }\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates an app name.\n */\nexport function validateAppName(appName: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!appName || appName.trim() === '') {\n errors.push('appName is required');\n } else if (!VALIDATION_RULES.APP_NAME_PATTERN.test(appName)) {\n errors.push(\n `appName \"${appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates screenshot size.\n */\nexport function validateScreenshotSize(sizeBytes: number): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (sizeBytes > VALIDATION_RULES.SCREENSHOT_MAX_BYTES) {\n errors.push(\n `Screenshot size ${sizeBytes} bytes exceeds the maximum of ${VALIDATION_RULES.SCREENSHOT_MAX_BYTES} bytes (5 MB)`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates browser context.\n * Warns if url is empty; warns if consoleErrors exceeds 20 items.\n */\nexport function validateContext(context: FeedbackContext): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!context.url || context.url.trim() === '') {\n warnings.push('context.url is empty — this may indicate a capture logic error');\n }\n\n if (\n context.consoleErrors &&\n context.consoleErrors.length > VALIDATION_RULES.CONSOLE_ERRORS_MAX\n ) {\n warnings.push(\n `context.consoleErrors has ${context.consoleErrors.length} entries (expected ≤${VALIDATION_RULES.CONSOLE_ERRORS_MAX}); truncation should occur in capture layer`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Aggregate validator — runs all validators for a capture input.\n */\nexport function validateCaptureInput(input: Partial<CaptureInput>): ValidationResult {\n const allErrors: string[] = [];\n const allWarnings: string[] = [];\n\n if (input.content !== undefined) {\n const contentResult = validateContent(input.content);\n allErrors.push(...contentResult.errors);\n allWarnings.push(...contentResult.warnings);\n } else {\n allErrors.push('content is required');\n }\n\n if (input.tags !== undefined) {\n const tagResult = validateTags(input.tags);\n allErrors.push(...tagResult.errors);\n allWarnings.push(...tagResult.warnings);\n }\n\n if (input.appName !== undefined) {\n const nameResult = validateAppName(input.appName);\n allErrors.push(...nameResult.errors);\n allWarnings.push(...nameResult.warnings);\n } else {\n allErrors.push('appName is required');\n }\n\n if (input.context !== undefined) {\n const contextResult = validateContext(input.context);\n allErrors.push(...contextResult.errors);\n allWarnings.push(...contextResult.warnings);\n }\n\n return makeResult(allErrors, allWarnings);\n}\n","/**\n * Pure data transformation functions for DynamoDB key construction\n * and shape mapping.\n *\n * NO AWS SDK calls — only key construction and shape mapping.\n * All functions are pure: take plain objects, return plain objects.\n * No side effects.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n StoredFeedbackItem,\n CorrectionEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Key Builder\n// ─────────────────────────────────────────────────────────────────\n\n/** The six DynamoDB keys for a feedback item */\nexport interface FeedbackKeys {\n PK: string; // PROJECT#{appName}\n SK: string; // FB#{timestamp}#{id}\n GSI1PK: string; // STATUS#{status}\n GSI1SK: string; // {inverted-priority-padded}#{id}\n GSI2PK: string; // TYPE#{type}#PROJECT#{appName}\n GSI2SK: string; // {timestamp}\n}\n\n/**\n * Builds the six DynamoDB keys for a feedback item.\n *\n * @param appName - Application name (e.g. 'sagrosoft20')\n * @param id - Feedback ID (e.g. 'FB-2026-07-25-abc12345')\n * @param type - Feedback type\n * @param status - Current status\n * @param priority - Priority score 0–100\n * @param timestamp - ISO 8601 timestamp\n */\nexport function buildKeys(\n appName: string,\n id: string,\n type: FeedbackType,\n status: FeedbackStatus,\n priority: number,\n timestamp: string,\n): FeedbackKeys {\n const clampedPriority = Math.max(0, Math.min(100, priority));\n const inverted = 100 - clampedPriority;\n const paddedInverted = String(inverted).padStart(3, '0');\n\n return {\n PK: `PROJECT#${appName}`,\n SK: `FB#${timestamp}#${id}`,\n GSI1PK: `STATUS#${status}`,\n GSI1SK: `${paddedInverted}#${id}`,\n GSI2PK: `TYPE#${type}#PROJECT#${appName}`,\n GSI2SK: timestamp,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Key Parsers\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Extracts timestamp and id from a composite SK.\n * SK format: FB#{timestamp}#{id}\n */\nexport function parseSK(sk: string): { timestamp: string; id: string } {\n // SK format: FB#2026-07-25T10:00:00.000Z#FB-2026-07-25-abc12345\n const withoutPrefix = sk.startsWith('FB#') ? sk.slice(3) : sk;\n // Find the last '#' which separates timestamp from id\n const lastHash = withoutPrefix.lastIndexOf('#');\n if (lastHash === -1) {\n throw new Error(`Invalid SK format: \"${sk}\" — expected FB#{timestamp}#{id}`);\n }\n return {\n timestamp: withoutPrefix.slice(0, lastHash),\n id: withoutPrefix.slice(lastHash + 1),\n };\n}\n\n/**\n * Parses the GSI2PK into its component parts.\n * GSI2PK format: TYPE#{type}#PROJECT#{appName}\n */\nexport function parseGSI2PK(gsi2pk: string): { type: FeedbackType; appName: string } {\n // Format: TYPE#feature#PROJECT#oms4business\n const typeMatch = gsi2pk.match(/^TYPE#([^#]+)#PROJECT#(.+)$/);\n if (!typeMatch || !typeMatch[1] || !typeMatch[2]) {\n throw new Error(\n `Invalid GSI2PK format: \"${gsi2pk}\" — expected TYPE#{type}#PROJECT#{appName}`,\n );\n }\n return {\n type: typeMatch[1] as FeedbackType,\n appName: typeMatch[2],\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reporter Mutations (immutable — returns new arrays)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Adds a reporter to an item.\n * Returns new reporters array and updated reporterCount — does NOT mutate original.\n */\nexport function addReporter(\n item: StoredFeedbackItem,\n userId: string,\n addedContext?: string,\n nowIso?: string,\n): Pick<StoredFeedbackItem, 'reporters' | 'reporterCount' | 'GSI1SK'> {\n const addedAt = nowIso ?? new Date().toISOString();\n const newReporter: Reporter = { userId, addedAt };\n if (addedContext !== undefined) {\n newReporter.addedContext = addedContext;\n }\n\n // Spread to ensure immutability — does NOT mutate item.reporters\n const newReporters: Reporter[] = [...item.reporters, newReporter];\n const newCount = newReporters.length;\n\n // GSI1SK format preserves the inverted priority prefix, just uses same item id\n // The priority recalculation would normally happen in the capture layer\n // Here we preserve the existing GSI1SK structure (priority unchanged by reporter add)\n return {\n reporters: newReporters,\n reporterCount: newCount,\n GSI1SK: item.GSI1SK,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Correction Mutations (immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Appends a correction record to the corrections array.\n * Returns a new array — does NOT mutate the original.\n */\nexport function appendCorrection(\n corrections: CorrectionEntry[],\n correction: Omit<CorrectionEntry, 'correctedAt'>,\n nowIso?: string,\n): CorrectionEntry[] {\n const correctedAt = nowIso ?? new Date().toISOString();\n return [\n ...corrections,\n { ...correction, correctedAt },\n ];\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Status Transition\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Applies a status transition to an item.\n * Returns updated key fields + lastModifiedAt (does NOT mutate original).\n *\n * When transitioning to 'resolved': sets resolvedAt = nowIso.\n * When transitioning to any other status: resolvedAt is undefined.\n */\nexport function applyStatusTransition(\n item: StoredFeedbackItem,\n newStatus: FeedbackStatus,\n nowIso?: string,\n): Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n> {\n const now = nowIso ?? new Date().toISOString();\n\n // GSI1SK stays the same (priority doesn't change on status transition)\n // Only GSI1PK changes (new status)\n const resolvedAt: string | undefined = newStatus === 'resolved' ? now : undefined;\n const resolvedBy: string | undefined = undefined;\n\n return {\n status: newStatus,\n GSI1PK: `STATUS#${newStatus}`,\n GSI1SK: item.GSI1SK, // Priority unchanged\n lastModifiedAt: now,\n ...(resolvedAt !== undefined ? { resolvedAt } : {}),\n ...(resolvedBy !== undefined ? { resolvedBy } : {}),\n } as Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n >;\n}\n","/**\n * Domain error types for the feedback package.\n *\n * Every error class carries:\n * - code: string constant (usable in switch statements)\n * - retryable: boolean\n *\n * createErrorResponse converts any thrown value to a safe JSON-serialisable object.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Base Error Class\n// ─────────────────────────────────────────────────────────────────\n\n/** Base class for all feedback domain errors */\nexport abstract class FeedbackError extends Error {\n abstract readonly code: string;\n abstract readonly retryable: boolean;\n\n constructor(message: string, readonly cause?: unknown) {\n super(message);\n this.name = this.constructor.name;\n // Fix prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Concrete Error Classes\n// ─────────────────────────────────────────────────────────────────\n\n/** Thrown when input validation fails */\nexport class FeedbackValidationError extends FeedbackError {\n readonly code = 'FEEDBACK_VALIDATION_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly fields?: string[],\n ) {\n super(message);\n this.name = 'FeedbackValidationError';\n Object.setPrototypeOf(this, FeedbackValidationError.prototype);\n }\n}\n\n/** Thrown when the transport layer fails (network, server error) */\nexport class FeedbackTransportError extends FeedbackError {\n readonly code = 'FEEDBACK_TRANSPORT_ERROR';\n readonly retryable = true;\n\n constructor(\n message: string,\n readonly statusCode?: number,\n cause?: unknown,\n ) {\n super(message, cause);\n this.name = 'FeedbackTransportError';\n Object.setPrototypeOf(this, FeedbackTransportError.prototype);\n }\n}\n\n/** Thrown when configuration is invalid or missing */\nexport class FeedbackConfigError extends FeedbackError {\n readonly code = 'FEEDBACK_CONFIG_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackConfigError';\n Object.setPrototypeOf(this, FeedbackConfigError.prototype);\n }\n}\n\n/** Thrown when dialogue exchange fails */\nexport class FeedbackDialogueError extends FeedbackError {\n readonly code = 'FEEDBACK_DIALOGUE_ERROR';\n readonly retryable = true;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackDialogueError';\n Object.setPrototypeOf(this, FeedbackDialogueError.prototype);\n }\n}\n\n/** Thrown when screenshot capture or upload fails */\nexport class FeedbackScreenshotError extends FeedbackError {\n readonly code = 'FEEDBACK_SCREENSHOT_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackScreenshotError';\n Object.setPrototypeOf(this, FeedbackScreenshotError.prototype);\n }\n}\n\n/** Thrown when a duplicate feedback submission is detected */\nexport class FeedbackDuplicateError extends FeedbackError {\n readonly code = 'FEEDBACK_DUPLICATE_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly existingId: string,\n ) {\n super(message);\n this.name = 'FeedbackDuplicateError';\n Object.setPrototypeOf(this, FeedbackDuplicateError.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Type Guard\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Type guard — returns true if the value is a FeedbackError subclass.\n * Returns false for plain Error instances and other values.\n */\nexport function isFeedbackError(e: unknown): e is FeedbackError {\n return e instanceof FeedbackError;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Error Response Serialiser\n// ─────────────────────────────────────────────────────────────────\n\n/** Safe JSON-serialisable error response */\nexport interface ErrorResponse {\n code: string;\n message: string;\n retryable: boolean;\n fields?: string[]; // present for FeedbackValidationError\n existingId?: string; // present for FeedbackDuplicateError\n}\n\n/**\n * Converts any thrown value to a safe JSON-serialisable response object.\n *\n * - FeedbackError subclasses → uses their code, message, retryable\n * - FeedbackValidationError → includes fields\n * - FeedbackDuplicateError → includes existingId\n * - Plain string → { code: 'UNKNOWN_ERROR', message: theString, retryable: false }\n * - Plain Error → { code: 'UNKNOWN_ERROR', message: error.message, retryable: false }\n * - null/undefined/other → { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred', retryable: false }\n */\nexport function createErrorResponse(e: unknown): ErrorResponse {\n if (isFeedbackError(e)) {\n const base: ErrorResponse = {\n code: e.code,\n message: e.message,\n retryable: e.retryable,\n };\n\n if (e instanceof FeedbackValidationError && e.fields !== undefined) {\n base.fields = e.fields;\n }\n\n if (e instanceof FeedbackDuplicateError) {\n base.existingId = e.existingId;\n }\n\n return base;\n }\n\n if (typeof e === 'string') {\n return {\n code: 'UNKNOWN_ERROR',\n message: e,\n retryable: false,\n };\n }\n\n if (e instanceof Error) {\n return {\n code: 'UNKNOWN_ERROR',\n message: e.message,\n retryable: false,\n };\n }\n\n return {\n code: 'UNKNOWN_ERROR',\n message: 'An unknown error occurred',\n retryable: false,\n };\n}\n"],"names":["e"],"mappings":"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"}
|
|
@@ -9,6 +9,17 @@ export interface ListOptions {
|
|
|
9
9
|
limit?: number;
|
|
10
10
|
lastEvaluatedKey?: Record<string, unknown>;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* FeedbackService — direct-DynamoDB CRUD for feedback items.
|
|
14
|
+
*
|
|
15
|
+
* @deprecated FOR WRITES (`put`): the central-storage principle requires feedback to be written
|
|
16
|
+
* through the authenticated central ingest API, not a bespoke direct-DynamoDB writer. Use
|
|
17
|
+
* `createIngestTransport(...)` (src/core/ingest-transport.ts) → `transport.submit(item)` so the
|
|
18
|
+
* consuming app holds a per-app JWT (no AWS principal) and the server stamps keys + enforces own-data.
|
|
19
|
+
* `put` is retained only for the server that owns the table during migration, and is removed once all
|
|
20
|
+
* writers are cut over to the ingest API. `get`/`list` remain valid server-side read operations until
|
|
21
|
+
* the ingest API grows its own authenticated read path.
|
|
22
|
+
*/
|
|
12
23
|
export declare class FeedbackService {
|
|
13
24
|
private readonly tableName;
|
|
14
25
|
private readonly region;
|
|
@@ -16,6 +27,9 @@ export declare class FeedbackService {
|
|
|
16
27
|
/**
|
|
17
28
|
* PutItem — writes a StoredFeedbackItem to DynamoDB.
|
|
18
29
|
* No-op when tableName is absent.
|
|
30
|
+
*
|
|
31
|
+
* @deprecated Direct-DynamoDB write — the pattern the central-storage principle removes. New
|
|
32
|
+
* writers use `createIngestTransport(...).submit(item)` (authenticated central ingest API).
|
|
19
33
|
*/
|
|
20
34
|
put(item: StoredFeedbackItem): Promise<void>;
|
|
21
35
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FeedbackService.d.ts","sourceRoot":"","sources":["../../src/server/FeedbackService.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAM3E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AA4ED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,EAAE,qBAAqB;IAOzC
|
|
1
|
+
{"version":3,"file":"FeedbackService.d.ts","sourceRoot":"","sources":["../../src/server/FeedbackService.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAM3E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AA4ED;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,EAAE,qBAAqB;IAOzC;;;;;;OAMG;IACG,GAAG,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBlD;;;OAGG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IA6BrE;;;OAGG;IACG,IAAI,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;CAmDlE"}
|
package/dist/server.js
CHANGED
|
@@ -68,6 +68,9 @@ class FeedbackService {
|
|
|
68
68
|
/**
|
|
69
69
|
* PutItem — writes a StoredFeedbackItem to DynamoDB.
|
|
70
70
|
* No-op when tableName is absent.
|
|
71
|
+
*
|
|
72
|
+
* @deprecated Direct-DynamoDB write — the pattern the central-storage principle removes. New
|
|
73
|
+
* writers use `createIngestTransport(...).submit(item)` (authenticated central ingest API).
|
|
71
74
|
*/
|
|
72
75
|
async put(item) {
|
|
73
76
|
if (!this.tableName) {
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sources":["../src/server/FeedbackService.ts","../src/server/listHandler.ts"],"sourcesContent":["/**\n * FeedbackService — DynamoDB CRUD operations for feedback items.\n *\n * Server-only module — NOT imported by the client bundle.\n * Uses dynamic import for @aws-sdk/client-dynamodb to avoid hard client bundle dep\n * (same pattern as submitResolver.ts).\n *\n * When tableName is absent (Minimal level), put is a no-op and list returns [].\n */\n\nimport type { StoredFeedbackItem, FeedbackStatus } from '../core/types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface FeedbackServiceConfig {\n tableName: string;\n region: string;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ListOptions\n// ─────────────────────────────────────────────────────────────────\n\nexport interface ListOptions {\n status?: FeedbackStatus;\n limit?: number;\n lastEvaluatedKey?: Record<string, unknown>;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────\n\n/** Marshal a JS value to a DynamoDB AttributeValue */\nfunction toAttr(value: unknown): Record<string, unknown> {\n if (value === null || value === undefined) {\n return { NULL: true };\n }\n if (typeof value === 'string') {\n return { S: value };\n }\n if (typeof value === 'number') {\n return { N: String(value) };\n }\n if (typeof value === 'boolean') {\n return { BOOL: value };\n }\n if (Array.isArray(value)) {\n return { L: value.map(toAttr) };\n }\n if (typeof value === 'object') {\n const m: Record<string, Record<string, unknown>> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n m[k] = toAttr(v);\n }\n return { M: m };\n }\n return { S: String(value) };\n}\n\n/** Marshal the entire StoredFeedbackItem to a DynamoDB Item map */\nfunction marshalItem(item: StoredFeedbackItem): Record<string, Record<string, unknown>> {\n const result: Record<string, Record<string, unknown>> = {};\n for (const [key, value] of Object.entries(item as unknown as Record<string, unknown>)) {\n if (value !== undefined) {\n result[key] = toAttr(value);\n }\n }\n return result;\n}\n\n/** Unmarshal a DynamoDB AttributeValue to a JS value */\nfunction fromAttr(attr: Record<string, unknown>): unknown {\n if (attr['S'] !== undefined) return attr['S'];\n if (attr['N'] !== undefined) return Number(attr['N']);\n if (attr['BOOL'] !== undefined) return attr['BOOL'];\n if (attr['NULL']) return null;\n if (attr['L']) {\n return (attr['L'] as Array<Record<string, unknown>>).map(fromAttr);\n }\n if (attr['M']) {\n const obj: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(attr['M'] as Record<string, Record<string, unknown>>)) {\n obj[k] = fromAttr(v);\n }\n return obj;\n }\n return undefined;\n}\n\n/** Unmarshal a DynamoDB Item map to StoredFeedbackItem */\nfunction unmarshalItem(item: Record<string, Record<string, unknown>>): StoredFeedbackItem {\n const result: Record<string, unknown> = {};\n for (const [key, attr] of Object.entries(item)) {\n result[key] = fromAttr(attr);\n }\n return result as unknown as StoredFeedbackItem;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// FeedbackService\n// ─────────────────────────────────────────────────────────────────\n\nexport class FeedbackService {\n private readonly tableName: string;\n private readonly region: string;\n\n constructor(config: FeedbackServiceConfig) {\n this.tableName = config.tableName;\n this.region = config.region;\n }\n\n // ── put ────────────────────────────────────────────────────────\n\n /**\n * PutItem — writes a StoredFeedbackItem to DynamoDB.\n * No-op when tableName is absent.\n */\n async put(item: StoredFeedbackItem): Promise<void> {\n if (!this.tableName) {\n return;\n }\n\n // Dynamic import to avoid hard dependency on AWS SDK in client bundle\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, PutItemCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n await client.send(\n new PutItemCommand({\n TableName: this.tableName,\n Item: marshalItem(item),\n }),\n );\n }\n\n // ── get ────────────────────────────────────────────────────────\n\n /**\n * GetItem — retrieves a StoredFeedbackItem by PK+SK.\n * Returns null if the item does not exist.\n */\n async get(pk: string, sk: string): Promise<StoredFeedbackItem | null> {\n if (!this.tableName) {\n return null;\n }\n\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, GetItemCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n const response = await client.send(\n new GetItemCommand({\n TableName: this.tableName,\n Key: {\n PK: { S: pk },\n SK: { S: sk },\n },\n }),\n );\n\n if (!response.Item) {\n return null;\n }\n\n return unmarshalItem(response.Item as Record<string, Record<string, unknown>>);\n }\n\n // ── list ───────────────────────────────────────────────────────\n\n /**\n * Query GSI1 with optional status filter.\n * Returns [] when tableName is absent (Minimal level).\n */\n async list(opts: ListOptions = {}): Promise<StoredFeedbackItem[]> {\n if (!this.tableName) {\n return [];\n }\n\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, QueryCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n const limit = opts.limit ?? 50;\n const gsi1pk = opts.status ? `STATUS#${opts.status}` : undefined;\n\n // Build query params\n type QueryParams = {\n TableName: string;\n IndexName: string;\n KeyConditionExpression: string;\n ExpressionAttributeNames: Record<string, string>;\n ExpressionAttributeValues: Record<string, Record<string, unknown>>;\n Limit: number;\n ExclusiveStartKey?: Record<string, unknown>;\n ScanIndexForward: boolean;\n };\n\n const params: QueryParams = {\n TableName: this.tableName,\n IndexName: 'GSI1',\n KeyConditionExpression: '#pk = :pk',\n ExpressionAttributeNames: {\n '#pk': 'GSI1PK',\n },\n ExpressionAttributeValues: {\n ':pk': { S: gsi1pk ?? 'STATUS#open' },\n },\n Limit: limit,\n ScanIndexForward: false, // Descending (highest priority first via inverted sort key)\n };\n\n if (opts.lastEvaluatedKey) {\n params.ExclusiveStartKey = opts.lastEvaluatedKey;\n }\n\n const response = await client.send(new QueryCommand(params));\n\n if (!response.Items || response.Items.length === 0) {\n return [];\n }\n\n return (response.Items as Array<Record<string, Record<string, unknown>>>).map(unmarshalItem);\n }\n}\n","/**\n * listHandler — HTTP GET /api/feedback/list handler factory.\n *\n * Server-only module — NOT imported by the client bundle.\n *\n * HTTP contract:\n * GET /api/feedback/list\n * Query params: ?status=open&limit=20\n * → 200 { items: StoredFeedbackItem[]; nextKey?: string }\n * → 400 { errors: string[] }\n */\n\nimport type { FeedbackStatus, StoredFeedbackItem } from '../core/types.js';\nimport { FeedbackService } from './FeedbackService.js';\nimport type { ListOptions } from './FeedbackService.js';\nimport type { RequestHandler } from '../react/resolver/submitResolver.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface ListHandlerConfig {\n /** DynamoDB table name — if absent, returns [] (Minimal level) */\n tableName?: string;\n /** AWS region for DynamoDB */\n region?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Valid status values\n// ─────────────────────────────────────────────────────────────────\n\nconst VALID_STATUSES: FeedbackStatus[] = ['open', 'approved', 'deferred', 'resolved', 'wontfix'];\n\n// ─────────────────────────────────────────────────────────────────\n// Handler Factory\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates an Express/Lambda-compatible request handler for the list endpoint.\n *\n * Validates query params → queries DynamoDB GSI1 → returns paginated results.\n * Returns 400 for invalid status values.\n * Returns 200 with items array (empty [] when no tableName configured).\n */\nexport function createListHandler(config: ListHandlerConfig): RequestHandler {\n const service = new FeedbackService({\n tableName: config.tableName ?? '',\n region: config.region ?? 'us-east-1',\n });\n\n return async (req, res) => {\n const errors: string[] = [];\n\n // Extract query params from URL or req.body (depending on framework shape)\n // We parse from the req — in Express, query params live in req.query\n // but our RequestBody interface doesn't define query. Parse from body if present.\n const query = (req as unknown as { query?: Record<string, string> }).query ?? {};\n const rawStatus = query['status'];\n const rawLimit = query['limit'];\n\n // Validate status\n let status: FeedbackStatus | undefined;\n if (rawStatus !== undefined) {\n if (!VALID_STATUSES.includes(rawStatus as FeedbackStatus)) {\n errors.push(`status must be one of: ${VALID_STATUSES.join(', ')} (got: ${rawStatus})`);\n } else {\n status = rawStatus as FeedbackStatus;\n }\n }\n\n // Validate limit\n let limit: number | undefined;\n if (rawLimit !== undefined) {\n const parsed = parseInt(rawLimit, 10);\n if (isNaN(parsed) || parsed < 1 || parsed > 100) {\n errors.push('limit must be a number between 1 and 100');\n } else {\n limit = parsed;\n }\n }\n\n if (errors.length > 0) {\n res.status(400).json({ errors });\n return;\n }\n\n const opts: ListOptions = {};\n if (status !== undefined) opts.status = status;\n if (limit !== undefined) opts.limit = limit;\n\n const items: StoredFeedbackItem[] = await service.list(opts);\n\n res.status(200).json({ items });\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","sources":["../src/server/FeedbackService.ts","../src/server/listHandler.ts"],"sourcesContent":["/**\n * FeedbackService — DynamoDB CRUD operations for feedback items.\n *\n * Server-only module — NOT imported by the client bundle.\n * Uses dynamic import for @aws-sdk/client-dynamodb to avoid hard client bundle dep\n * (same pattern as submitResolver.ts).\n *\n * When tableName is absent (Minimal level), put is a no-op and list returns [].\n */\n\nimport type { StoredFeedbackItem, FeedbackStatus } from '../core/types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface FeedbackServiceConfig {\n tableName: string;\n region: string;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ListOptions\n// ─────────────────────────────────────────────────────────────────\n\nexport interface ListOptions {\n status?: FeedbackStatus;\n limit?: number;\n lastEvaluatedKey?: Record<string, unknown>;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────\n\n/** Marshal a JS value to a DynamoDB AttributeValue */\nfunction toAttr(value: unknown): Record<string, unknown> {\n if (value === null || value === undefined) {\n return { NULL: true };\n }\n if (typeof value === 'string') {\n return { S: value };\n }\n if (typeof value === 'number') {\n return { N: String(value) };\n }\n if (typeof value === 'boolean') {\n return { BOOL: value };\n }\n if (Array.isArray(value)) {\n return { L: value.map(toAttr) };\n }\n if (typeof value === 'object') {\n const m: Record<string, Record<string, unknown>> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n m[k] = toAttr(v);\n }\n return { M: m };\n }\n return { S: String(value) };\n}\n\n/** Marshal the entire StoredFeedbackItem to a DynamoDB Item map */\nfunction marshalItem(item: StoredFeedbackItem): Record<string, Record<string, unknown>> {\n const result: Record<string, Record<string, unknown>> = {};\n for (const [key, value] of Object.entries(item as unknown as Record<string, unknown>)) {\n if (value !== undefined) {\n result[key] = toAttr(value);\n }\n }\n return result;\n}\n\n/** Unmarshal a DynamoDB AttributeValue to a JS value */\nfunction fromAttr(attr: Record<string, unknown>): unknown {\n if (attr['S'] !== undefined) return attr['S'];\n if (attr['N'] !== undefined) return Number(attr['N']);\n if (attr['BOOL'] !== undefined) return attr['BOOL'];\n if (attr['NULL']) return null;\n if (attr['L']) {\n return (attr['L'] as Array<Record<string, unknown>>).map(fromAttr);\n }\n if (attr['M']) {\n const obj: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(attr['M'] as Record<string, Record<string, unknown>>)) {\n obj[k] = fromAttr(v);\n }\n return obj;\n }\n return undefined;\n}\n\n/** Unmarshal a DynamoDB Item map to StoredFeedbackItem */\nfunction unmarshalItem(item: Record<string, Record<string, unknown>>): StoredFeedbackItem {\n const result: Record<string, unknown> = {};\n for (const [key, attr] of Object.entries(item)) {\n result[key] = fromAttr(attr);\n }\n return result as unknown as StoredFeedbackItem;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// FeedbackService\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * FeedbackService — direct-DynamoDB CRUD for feedback items.\n *\n * @deprecated FOR WRITES (`put`): the central-storage principle requires feedback to be written\n * through the authenticated central ingest API, not a bespoke direct-DynamoDB writer. Use\n * `createIngestTransport(...)` (src/core/ingest-transport.ts) → `transport.submit(item)` so the\n * consuming app holds a per-app JWT (no AWS principal) and the server stamps keys + enforces own-data.\n * `put` is retained only for the server that owns the table during migration, and is removed once all\n * writers are cut over to the ingest API. `get`/`list` remain valid server-side read operations until\n * the ingest API grows its own authenticated read path.\n */\nexport class FeedbackService {\n private readonly tableName: string;\n private readonly region: string;\n\n constructor(config: FeedbackServiceConfig) {\n this.tableName = config.tableName;\n this.region = config.region;\n }\n\n // ── put ────────────────────────────────────────────────────────\n\n /**\n * PutItem — writes a StoredFeedbackItem to DynamoDB.\n * No-op when tableName is absent.\n *\n * @deprecated Direct-DynamoDB write — the pattern the central-storage principle removes. New\n * writers use `createIngestTransport(...).submit(item)` (authenticated central ingest API).\n */\n async put(item: StoredFeedbackItem): Promise<void> {\n if (!this.tableName) {\n return;\n }\n\n // Dynamic import to avoid hard dependency on AWS SDK in client bundle\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, PutItemCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n await client.send(\n new PutItemCommand({\n TableName: this.tableName,\n Item: marshalItem(item),\n }),\n );\n }\n\n // ── get ────────────────────────────────────────────────────────\n\n /**\n * GetItem — retrieves a StoredFeedbackItem by PK+SK.\n * Returns null if the item does not exist.\n */\n async get(pk: string, sk: string): Promise<StoredFeedbackItem | null> {\n if (!this.tableName) {\n return null;\n }\n\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, GetItemCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n const response = await client.send(\n new GetItemCommand({\n TableName: this.tableName,\n Key: {\n PK: { S: pk },\n SK: { S: sk },\n },\n }),\n );\n\n if (!response.Item) {\n return null;\n }\n\n return unmarshalItem(response.Item as Record<string, Record<string, unknown>>);\n }\n\n // ── list ───────────────────────────────────────────────────────\n\n /**\n * Query GSI1 with optional status filter.\n * Returns [] when tableName is absent (Minimal level).\n */\n async list(opts: ListOptions = {}): Promise<StoredFeedbackItem[]> {\n if (!this.tableName) {\n return [];\n }\n\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, QueryCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n const limit = opts.limit ?? 50;\n const gsi1pk = opts.status ? `STATUS#${opts.status}` : undefined;\n\n // Build query params\n type QueryParams = {\n TableName: string;\n IndexName: string;\n KeyConditionExpression: string;\n ExpressionAttributeNames: Record<string, string>;\n ExpressionAttributeValues: Record<string, Record<string, unknown>>;\n Limit: number;\n ExclusiveStartKey?: Record<string, unknown>;\n ScanIndexForward: boolean;\n };\n\n const params: QueryParams = {\n TableName: this.tableName,\n IndexName: 'GSI1',\n KeyConditionExpression: '#pk = :pk',\n ExpressionAttributeNames: {\n '#pk': 'GSI1PK',\n },\n ExpressionAttributeValues: {\n ':pk': { S: gsi1pk ?? 'STATUS#open' },\n },\n Limit: limit,\n ScanIndexForward: false, // Descending (highest priority first via inverted sort key)\n };\n\n if (opts.lastEvaluatedKey) {\n params.ExclusiveStartKey = opts.lastEvaluatedKey;\n }\n\n const response = await client.send(new QueryCommand(params));\n\n if (!response.Items || response.Items.length === 0) {\n return [];\n }\n\n return (response.Items as Array<Record<string, Record<string, unknown>>>).map(unmarshalItem);\n }\n}\n","/**\n * listHandler — HTTP GET /api/feedback/list handler factory.\n *\n * Server-only module — NOT imported by the client bundle.\n *\n * HTTP contract:\n * GET /api/feedback/list\n * Query params: ?status=open&limit=20\n * → 200 { items: StoredFeedbackItem[]; nextKey?: string }\n * → 400 { errors: string[] }\n */\n\nimport type { FeedbackStatus, StoredFeedbackItem } from '../core/types.js';\nimport { FeedbackService } from './FeedbackService.js';\nimport type { ListOptions } from './FeedbackService.js';\nimport type { RequestHandler } from '../react/resolver/submitResolver.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface ListHandlerConfig {\n /** DynamoDB table name — if absent, returns [] (Minimal level) */\n tableName?: string;\n /** AWS region for DynamoDB */\n region?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Valid status values\n// ─────────────────────────────────────────────────────────────────\n\nconst VALID_STATUSES: FeedbackStatus[] = ['open', 'approved', 'deferred', 'resolved', 'wontfix'];\n\n// ─────────────────────────────────────────────────────────────────\n// Handler Factory\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates an Express/Lambda-compatible request handler for the list endpoint.\n *\n * Validates query params → queries DynamoDB GSI1 → returns paginated results.\n * Returns 400 for invalid status values.\n * Returns 200 with items array (empty [] when no tableName configured).\n */\nexport function createListHandler(config: ListHandlerConfig): RequestHandler {\n const service = new FeedbackService({\n tableName: config.tableName ?? '',\n region: config.region ?? 'us-east-1',\n });\n\n return async (req, res) => {\n const errors: string[] = [];\n\n // Extract query params from URL or req.body (depending on framework shape)\n // We parse from the req — in Express, query params live in req.query\n // but our RequestBody interface doesn't define query. Parse from body if present.\n const query = (req as unknown as { query?: Record<string, string> }).query ?? {};\n const rawStatus = query['status'];\n const rawLimit = query['limit'];\n\n // Validate status\n let status: FeedbackStatus | undefined;\n if (rawStatus !== undefined) {\n if (!VALID_STATUSES.includes(rawStatus as FeedbackStatus)) {\n errors.push(`status must be one of: ${VALID_STATUSES.join(', ')} (got: ${rawStatus})`);\n } else {\n status = rawStatus as FeedbackStatus;\n }\n }\n\n // Validate limit\n let limit: number | undefined;\n if (rawLimit !== undefined) {\n const parsed = parseInt(rawLimit, 10);\n if (isNaN(parsed) || parsed < 1 || parsed > 100) {\n errors.push('limit must be a number between 1 and 100');\n } else {\n limit = parsed;\n }\n }\n\n if (errors.length > 0) {\n res.status(400).json({ errors });\n return;\n }\n\n const opts: ListOptions = {};\n if (status !== undefined) opts.status = status;\n if (limit !== undefined) opts.limit = limit;\n\n const items: StoredFeedbackItem[] = await service.list(opts);\n\n res.status(200).json({ items });\n };\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anton-bakker-feedback",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Zero-dependency business logic layer for feedback collection — types, capture, transport, config, i18n, observability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"anton-bakker-tailwind-components": ">=0.7.0",
|
|
50
|
-
"react": "^18.0.0",
|
|
51
|
-
"react-dom": "^18.0.0"
|
|
50
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
51
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"react": {
|