@flytedesk/app-kit 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/auth/guards.d.ts +5 -0
  2. package/dist/auth/guards.js +28 -0
  3. package/dist/auth/guards.js.map +1 -0
  4. package/dist/auth/index.d.ts +33 -5
  5. package/dist/auth/index.js +31 -5
  6. package/dist/auth/index.js.map +1 -1
  7. package/dist/auth/oidc-client.d.ts +47 -0
  8. package/dist/auth/oidc-client.js +162 -0
  9. package/dist/auth/oidc-client.js.map +1 -0
  10. package/dist/auth/pkce.d.ts +8 -0
  11. package/dist/auth/pkce.js +12 -0
  12. package/dist/auth/pkce.js.map +1 -0
  13. package/dist/auth/plugin.d.ts +8 -0
  14. package/dist/auth/plugin.js +362 -0
  15. package/dist/auth/plugin.js.map +1 -0
  16. package/dist/auth/tokens.d.ts +24 -0
  17. package/dist/auth/tokens.js +35 -0
  18. package/dist/auth/tokens.js.map +1 -0
  19. package/dist/auth/types.d.ts +156 -0
  20. package/dist/auth/types.js +2 -0
  21. package/dist/auth/types.js.map +1 -0
  22. package/dist/cli/trace-sync.d.ts +2 -0
  23. package/dist/cli/trace-sync.js +362 -0
  24. package/dist/cli/trace-sync.js.map +1 -0
  25. package/dist/rate-limit/index.d.ts +6 -0
  26. package/dist/rate-limit/index.js +7 -0
  27. package/dist/rate-limit/index.js.map +1 -0
  28. package/dist/rate-limit/plugin.d.ts +23 -0
  29. package/dist/rate-limit/plugin.js +75 -0
  30. package/dist/rate-limit/plugin.js.map +1 -0
  31. package/dist/rate-limit/store.d.ts +51 -0
  32. package/dist/rate-limit/store.js +59 -0
  33. package/dist/rate-limit/store.js.map +1 -0
  34. package/dist/trace/client.d.ts +20 -0
  35. package/dist/trace/client.js +21 -0
  36. package/dist/trace/client.js.map +1 -0
  37. package/dist/trace/errors.d.ts +31 -0
  38. package/dist/trace/errors.js +37 -0
  39. package/dist/trace/errors.js.map +1 -0
  40. package/dist/trace/index.d.ts +15 -4
  41. package/dist/trace/index.js +3 -6
  42. package/dist/trace/index.js.map +1 -1
  43. package/dist/trace/queries.d.ts +59 -0
  44. package/dist/trace/queries.js +147 -0
  45. package/dist/trace/queries.js.map +1 -0
  46. package/dist/trace/types.d.ts +72 -0
  47. package/dist/trace/types.js +14 -0
  48. package/dist/trace/types.js.map +1 -0
  49. package/package.json +54 -8
  50. package/prisma/fragments/trace.meta.json +4 -0
  51. package/prisma/fragments/trace.prisma +87 -0
  52. package/prisma/migrations/0001_app_kit_trace_init/migration.sql +91 -0
  53. package/prisma/migrations/manifest.json +8 -0
@@ -0,0 +1,21 @@
1
+ /**
2
+ * createTraceClient(prisma) — bundles the query helpers and captureError into one
3
+ * object bound to the consumer's PrismaClient (or any TracePrismaClient-shaped
4
+ * object), so call sites don't have to thread `prisma` through every call.
5
+ */
6
+ import { captureError } from "./errors.js";
7
+ import { getAuditHistoryForModel, getRecentApiCallsForRoute, getTraceChainForRoot, insertApiCallLog, insertModelAudit, insertTraceLink, insertTraceNode, setTraceNodeRoot, } from "./queries.js";
8
+ export function createTraceClient(prisma) {
9
+ return {
10
+ insertTraceNode: (input) => insertTraceNode(prisma, input),
11
+ setTraceNodeRoot: (id, rootId) => setTraceNodeRoot(prisma, id, rootId),
12
+ getTraceChainForRoot: (rootId) => getTraceChainForRoot(prisma, rootId),
13
+ insertModelAudit: (input) => insertModelAudit(prisma, input),
14
+ getAuditHistoryForModel: (modelType, modelId, limit) => getAuditHistoryForModel(prisma, modelType, modelId, limit),
15
+ insertApiCallLog: (input) => insertApiCallLog(prisma, input),
16
+ getRecentApiCallsForRoute: (route, sinceMinutes) => getRecentApiCallsForRoute(prisma, route, sinceMinutes),
17
+ insertTraceLink: (input) => insertTraceLink(prisma, input),
18
+ captureError: (options) => captureError(prisma, options),
19
+ };
20
+ }
21
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/trace/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAA4B,MAAM,aAAa,CAAC;AACrE,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,gBAAgB,GAKjB,MAAM,cAAc,CAAC;AA4BtB,MAAM,UAAU,iBAAiB,CAAC,MAAyB;IACzD,OAAO;QACL,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC;QAC1D,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC;QACtE,oBAAoB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC;QACtE,gBAAgB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC;QAC5D,uBAAuB,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CACrD,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC;QAC5D,gBAAgB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC;QAC5D,yBAAyB,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,CACjD,yBAAyB,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC;QACxD,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC;QAC1D,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC;KACzD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { TraceNodeRow, TracePrismaClient } from "./types.js";
2
+ export interface CapturedErrorPayload {
3
+ message: string;
4
+ stack: string | null;
5
+ context: unknown;
6
+ }
7
+ export interface TraceErrorAlertPayload {
8
+ traceNode: TraceNodeRow;
9
+ error: Error;
10
+ actorId?: string | undefined;
11
+ route?: string | undefined;
12
+ }
13
+ export type TraceErrorAlertHook = (payload: TraceErrorAlertPayload) => Promise<void>;
14
+ export declare const defaultTraceErrorAlertHook: TraceErrorAlertHook;
15
+ export interface CaptureErrorOptions {
16
+ error: Error;
17
+ actorId?: string;
18
+ route?: string;
19
+ /** Existing trace root to hang this error node off of. Omit to make the error
20
+ * node self-rooted (`root_id = id`) — e.g. an uncaught exception with no
21
+ * in-flight trace context. */
22
+ rootId?: bigint;
23
+ /** Arbitrary structured context to store alongside the error (request body,
24
+ * job payload, ids, ...). Stored as-is in `request_data.context`. */
25
+ context?: unknown;
26
+ /** Defaults to logging to console.error. The real Slack-webhook (or other
27
+ * alerting) wiring is consumer-specific and lives outside this module —
28
+ * this is just the injectable, typed seam for it. */
29
+ alertHook?: TraceErrorAlertHook;
30
+ }
31
+ export declare function captureError(prisma: TracePrismaClient, options: CaptureErrorOptions): Promise<TraceNodeRow>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Error capture (DEC-49): folded into trace rather than shipped as a separate
3
+ * module. An `ERROR`-kind TraceNode is the record; the alert hook is the seam for
4
+ * wiring in wherever the consumer wants notifications (Slack, PagerDuty, email,
5
+ * ...) — that wiring is consumer-specific and out of scope for this module, so the
6
+ * default hook just logs, and consumers pass their own `alertHook`.
7
+ */
8
+ import { insertTraceNode, setTraceNodeRoot } from "./queries.js";
9
+ export const defaultTraceErrorAlertHook = async (payload) => {
10
+ console.error(`[app-kit/trace] captured error${payload.route ? ` on ${payload.route}` : ""}:`, payload.error);
11
+ };
12
+ export async function captureError(prisma, options) {
13
+ const { error, actorId, route, context, alertHook = defaultTraceErrorAlertHook, } = options;
14
+ const requestData = {
15
+ message: error.message,
16
+ stack: error.stack ?? null,
17
+ context: context ?? null,
18
+ };
19
+ let node = await insertTraceNode(prisma, {
20
+ // rootId is NOT NULL on TraceNode; when the caller has no root to attach to,
21
+ // insert self-referentially rooted below once the id is known.
22
+ rootId: options.rootId ?? 0n,
23
+ parentId: null,
24
+ kind: "ERROR",
25
+ actorId: actorId ?? null,
26
+ route: route ?? null,
27
+ requestData,
28
+ responseData: null,
29
+ statusCode: null,
30
+ });
31
+ if (options.rootId === undefined) {
32
+ node = await setTraceNodeRoot(prisma, node.id, node.id);
33
+ }
34
+ await alertHook({ traceNode: node, error, actorId, route });
35
+ return node;
36
+ }
37
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/trace/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAoBjE,MAAM,CAAC,MAAM,0BAA0B,GAAwB,KAAK,EAClE,OAAO,EACP,EAAE;IACF,OAAO,CAAC,KAAK,CACX,iCAAiC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAC/E,OAAO,CAAC,KAAK,CACd,CAAC;AACJ,CAAC,CAAC;AAmBF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAyB,EACzB,OAA4B;IAE5B,MAAM,EACJ,KAAK,EACL,OAAO,EACP,KAAK,EACL,OAAO,EACP,SAAS,GAAG,0BAA0B,GACvC,GAAG,OAAO,CAAC;IAEZ,MAAM,WAAW,GAAyB;QACxC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,IAAI;QAC1B,OAAO,EAAE,OAAO,IAAI,IAAI;KACzB,CAAC;IAEF,IAAI,IAAI,GAAG,MAAM,eAAe,CAAC,MAAM,EAAE;QACvC,6EAA6E;QAC7E,+DAA+D;QAC/D,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;QAC5B,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,OAAO,IAAI,IAAI;QACxB,KAAK,EAAE,KAAK,IAAI,IAAI;QACpB,WAAW;QACX,YAAY,EAAE,IAAI;QAClB,UAAU,EAAE,IAAI;KACjB,CAAC,CAAC;IAEH,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAE5D,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -1,6 +1,17 @@
1
1
  /**
2
- * Postgres-native trace/audit layer — placeholder for the real module (DEC-16, DEC-22,
3
- * DEC-26, DEC-31). Ships Prisma schema fragments + SQL migrations + a `trace:sync` CLI,
4
- * not a generated Prisma Client — see DEC-31 for why.
2
+ * Postgres-native trace/audit layer (DEC-16, DEC-22, DEC-26, DEC-31, DEC-39,
3
+ * DEC-49). Ships Prisma schema fragments + raw SQL migrations + a `trace-sync`
4
+ * CLI, not a generated Prisma Client — see prisma/fragments/trace.prisma and
5
+ * src/cli/trace-sync.ts for why and how.
6
+ *
7
+ * Consumers bring their own generated PrismaClient (after running
8
+ * `trace-sync init`) and get a bound query surface from createTraceClient, or use
9
+ * the individual query helpers directly.
5
10
  */
6
- export declare const APP_KIT_TRACE_PLACEHOLDER = true;
11
+ export type { TracePrismaClient, TraceNodeKind, TraceNodeRow, ModelAuditRow, ApiCallLogRow, TraceLinkRow, } from "./types.js";
12
+ export { insertTraceNode, setTraceNodeRoot, getTraceChainForRoot, insertModelAudit, getAuditHistoryForModel, insertApiCallLog, getRecentApiCallsForRoute, insertTraceLink, } from "./queries.js";
13
+ export type { NewTraceNodeInput, NewModelAuditInput, NewApiCallLogInput, NewTraceLinkInput, } from "./queries.js";
14
+ export { captureError, defaultTraceErrorAlertHook } from "./errors.js";
15
+ export type { CaptureErrorOptions, CapturedErrorPayload, TraceErrorAlertHook, TraceErrorAlertPayload, } from "./errors.js";
16
+ export { createTraceClient } from "./client.js";
17
+ export type { TraceClient } from "./client.js";
@@ -1,7 +1,4 @@
1
- /**
2
- * Postgres-native trace/audit layer — placeholder for the real module (DEC-16, DEC-22,
3
- * DEC-26, DEC-31). Ships Prisma schema fragments + SQL migrations + a `trace:sync` CLI,
4
- * not a generated Prisma Client — see DEC-31 for why.
5
- */
6
- export const APP_KIT_TRACE_PLACEHOLDER = true;
1
+ export { insertTraceNode, setTraceNodeRoot, getTraceChainForRoot, insertModelAudit, getAuditHistoryForModel, insertApiCallLog, getRecentApiCallsForRoute, insertTraceLink, } from "./queries.js";
2
+ export { captureError, defaultTraceErrorAlertHook } from "./errors.js";
3
+ export { createTraceClient } from "./client.js";
7
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/trace/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/trace/index.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,yBAAyB,EACzB,eAAe,GAChB,MAAM,cAAc,CAAC;AAQtB,OAAO,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAQvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,59 @@
1
+ import type { ApiCallLogRow, ModelAuditRow, TraceLinkRow, TraceNodeRow, TracePrismaClient } from "./types.js";
2
+ export interface NewTraceNodeInput {
3
+ externalId?: string;
4
+ parentId?: bigint | null;
5
+ rootId: bigint;
6
+ kind: string;
7
+ actorId?: string | null;
8
+ route?: string | null;
9
+ requestData?: unknown;
10
+ responseData?: unknown;
11
+ statusCode?: number | null;
12
+ startedAt?: Date;
13
+ endedAt?: Date | null;
14
+ durationMs?: number | null;
15
+ }
16
+ export declare function insertTraceNode(prisma: TracePrismaClient, input: NewTraceNodeInput): Promise<TraceNodeRow>;
17
+ /**
18
+ * Point an already-inserted TraceNode's `root_id` at a (possibly different) id.
19
+ * Used by src/trace/errors.ts to make a standalone captured error self-rooted
20
+ * (`root_id = id`) when the caller has no existing trace root to hang it off of —
21
+ * `TraceNode.rootId` is `NOT NULL`, so an orphan error always needs *some* root.
22
+ */
23
+ export declare function setTraceNodeRoot(prisma: TracePrismaClient, id: bigint, rootId: bigint): Promise<TraceNodeRow>;
24
+ export declare function getTraceChainForRoot(prisma: TracePrismaClient, rootId: bigint): Promise<TraceNodeRow[]>;
25
+ export interface NewModelAuditInput {
26
+ traceNodeId?: bigint | null;
27
+ modelType: string;
28
+ modelId: string;
29
+ event: string;
30
+ actorId?: string | null;
31
+ oldValues?: unknown;
32
+ newValues?: unknown;
33
+ createdAt?: Date;
34
+ }
35
+ export declare function insertModelAudit(prisma: TracePrismaClient, input: NewModelAuditInput): Promise<ModelAuditRow>;
36
+ export declare function getAuditHistoryForModel(prisma: TracePrismaClient, modelType: string, modelId: string, limit?: number): Promise<ModelAuditRow[]>;
37
+ export interface NewApiCallLogInput {
38
+ traceNodeId?: bigint | null;
39
+ route: string;
40
+ method: string;
41
+ statusCode?: number | null;
42
+ actorId?: string | null;
43
+ durationMs?: number | null;
44
+ createdAt?: Date;
45
+ }
46
+ export declare function insertApiCallLog(prisma: TracePrismaClient, input: NewApiCallLogInput): Promise<ApiCallLogRow>;
47
+ export declare function getRecentApiCallsForRoute(prisma: TracePrismaClient, route: string, sinceMinutes?: number): Promise<ApiCallLogRow[]>;
48
+ export interface NewTraceLinkInput {
49
+ fromId: bigint;
50
+ toId: bigint;
51
+ kind: string;
52
+ createdAt?: Date;
53
+ }
54
+ /**
55
+ * Upsert on the (fromId, toId, kind) unique constraint: calling this twice for
56
+ * the same triple updates the one row in place (a no-op `SET kind = EXCLUDED.kind`)
57
+ * instead of raising a unique-violation or silently inserting a duplicate.
58
+ */
59
+ export declare function insertTraceLink(prisma: TracePrismaClient, input: NewTraceLinkInput): Promise<TraceLinkRow>;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Typed $queryRaw/$executeRaw helpers for @flytedesk/app-kit/trace.
3
+ *
4
+ * Every helper writes/reads the tables created by
5
+ * prisma/migrations/0001_app_kit_trace_init/migration.sql directly. Column lists
6
+ * are written out literally in each query's tagged template (rather than shared
7
+ * via a string constant interpolated into the template) because a `${...}`
8
+ * interpolation inside a $queryRaw tagged template becomes a bound *parameter*,
9
+ * not raw SQL text — the TracePrismaClient structural interface has no
10
+ * `Prisma.raw`/`$queryRawUnsafe` escape hatch, deliberately, so there is no way to
11
+ * splice SQL fragments in from a variable. That's fine: the column lists are
12
+ * static.
13
+ */
14
+ import { randomUUID } from "node:crypto";
15
+ function toJsonParam(value) {
16
+ return value === undefined || value === null ? null : JSON.stringify(value);
17
+ }
18
+ function requireOne(rows, what) {
19
+ const row = rows[0];
20
+ if (!row) {
21
+ throw new Error(`${what}: expected a returned row but got none`);
22
+ }
23
+ return row;
24
+ }
25
+ export async function insertTraceNode(prisma, input) {
26
+ const externalId = input.externalId ?? randomUUID();
27
+ const startedAt = input.startedAt ?? new Date();
28
+ const rows = await prisma.$queryRaw `
29
+ INSERT INTO app_kit_trace_nodes (
30
+ external_id, parent_id, root_id, kind, actor_id, route,
31
+ request_data, response_data, status_code, started_at, ended_at, duration_ms
32
+ ) VALUES (
33
+ ${externalId}::uuid, ${input.parentId ?? null}, ${input.rootId}, ${input.kind},
34
+ ${input.actorId ?? null}, ${input.route ?? null},
35
+ ${toJsonParam(input.requestData)}::jsonb, ${toJsonParam(input.responseData)}::jsonb,
36
+ ${input.statusCode ?? null}, ${startedAt}, ${input.endedAt ?? null}, ${input.durationMs ?? null}
37
+ )
38
+ RETURNING
39
+ id, external_id AS "externalId", parent_id AS "parentId", root_id AS "rootId", kind,
40
+ actor_id AS "actorId", route, request_data AS "requestData", response_data AS "responseData",
41
+ status_code AS "statusCode", started_at AS "startedAt", ended_at AS "endedAt",
42
+ duration_ms AS "durationMs"
43
+ `;
44
+ return requireOne(rows, "insertTraceNode");
45
+ }
46
+ /**
47
+ * Point an already-inserted TraceNode's `root_id` at a (possibly different) id.
48
+ * Used by src/trace/errors.ts to make a standalone captured error self-rooted
49
+ * (`root_id = id`) when the caller has no existing trace root to hang it off of —
50
+ * `TraceNode.rootId` is `NOT NULL`, so an orphan error always needs *some* root.
51
+ */
52
+ export async function setTraceNodeRoot(prisma, id, rootId) {
53
+ const rows = await prisma.$queryRaw `
54
+ UPDATE app_kit_trace_nodes
55
+ SET root_id = ${rootId}
56
+ WHERE id = ${id}
57
+ RETURNING
58
+ id, external_id AS "externalId", parent_id AS "parentId", root_id AS "rootId", kind,
59
+ actor_id AS "actorId", route, request_data AS "requestData", response_data AS "responseData",
60
+ status_code AS "statusCode", started_at AS "startedAt", ended_at AS "endedAt",
61
+ duration_ms AS "durationMs"
62
+ `;
63
+ return requireOne(rows, "setTraceNodeRoot");
64
+ }
65
+ export async function getTraceChainForRoot(prisma, rootId) {
66
+ return prisma.$queryRaw `
67
+ SELECT
68
+ id, external_id AS "externalId", parent_id AS "parentId", root_id AS "rootId", kind,
69
+ actor_id AS "actorId", route, request_data AS "requestData", response_data AS "responseData",
70
+ status_code AS "statusCode", started_at AS "startedAt", ended_at AS "endedAt",
71
+ duration_ms AS "durationMs"
72
+ FROM app_kit_trace_nodes
73
+ WHERE root_id = ${rootId}
74
+ ORDER BY started_at ASC
75
+ `;
76
+ }
77
+ export async function insertModelAudit(prisma, input) {
78
+ const createdAt = input.createdAt ?? new Date();
79
+ const rows = await prisma.$queryRaw `
80
+ INSERT INTO app_kit_trace_model_audits (
81
+ trace_node_id, model_type, model_id, event, actor_id, old_values, new_values, created_at
82
+ ) VALUES (
83
+ ${input.traceNodeId ?? null}, ${input.modelType}, ${input.modelId}, ${input.event},
84
+ ${input.actorId ?? null}, ${toJsonParam(input.oldValues)}::jsonb,
85
+ ${toJsonParam(input.newValues)}::jsonb, ${createdAt}
86
+ )
87
+ RETURNING
88
+ id, trace_node_id AS "traceNodeId", model_type AS "modelType", model_id AS "modelId",
89
+ event, actor_id AS "actorId", old_values AS "oldValues", new_values AS "newValues",
90
+ created_at AS "createdAt"
91
+ `;
92
+ return requireOne(rows, "insertModelAudit");
93
+ }
94
+ export async function getAuditHistoryForModel(prisma, modelType, modelId, limit = 50) {
95
+ return prisma.$queryRaw `
96
+ SELECT
97
+ id, trace_node_id AS "traceNodeId", model_type AS "modelType", model_id AS "modelId",
98
+ event, actor_id AS "actorId", old_values AS "oldValues", new_values AS "newValues",
99
+ created_at AS "createdAt"
100
+ FROM app_kit_trace_model_audits
101
+ WHERE model_type = ${modelType} AND model_id = ${modelId}
102
+ ORDER BY created_at DESC
103
+ LIMIT ${limit}
104
+ `;
105
+ }
106
+ export async function insertApiCallLog(prisma, input) {
107
+ const createdAt = input.createdAt ?? new Date();
108
+ const rows = await prisma.$queryRaw `
109
+ INSERT INTO app_kit_trace_api_call_logs (
110
+ trace_node_id, route, method, status_code, actor_id, duration_ms, created_at
111
+ ) VALUES (
112
+ ${input.traceNodeId ?? null}, ${input.route}, ${input.method}, ${input.statusCode ?? null},
113
+ ${input.actorId ?? null}, ${input.durationMs ?? null}, ${createdAt}
114
+ )
115
+ RETURNING
116
+ id, trace_node_id AS "traceNodeId", route, method, status_code AS "statusCode",
117
+ actor_id AS "actorId", duration_ms AS "durationMs", created_at AS "createdAt"
118
+ `;
119
+ return requireOne(rows, "insertApiCallLog");
120
+ }
121
+ export async function getRecentApiCallsForRoute(prisma, route, sinceMinutes = 30) {
122
+ const since = new Date(Date.now() - sinceMinutes * 60_000);
123
+ return prisma.$queryRaw `
124
+ SELECT
125
+ id, trace_node_id AS "traceNodeId", route, method, status_code AS "statusCode",
126
+ actor_id AS "actorId", duration_ms AS "durationMs", created_at AS "createdAt"
127
+ FROM app_kit_trace_api_call_logs
128
+ WHERE route = ${route} AND created_at >= ${since}
129
+ ORDER BY created_at DESC
130
+ `;
131
+ }
132
+ /**
133
+ * Upsert on the (fromId, toId, kind) unique constraint: calling this twice for
134
+ * the same triple updates the one row in place (a no-op `SET kind = EXCLUDED.kind`)
135
+ * instead of raising a unique-violation or silently inserting a duplicate.
136
+ */
137
+ export async function insertTraceLink(prisma, input) {
138
+ const createdAt = input.createdAt ?? new Date();
139
+ const rows = await prisma.$queryRaw `
140
+ INSERT INTO app_kit_trace_links (from_id, to_id, kind, created_at)
141
+ VALUES (${input.fromId}, ${input.toId}, ${input.kind}, ${createdAt})
142
+ ON CONFLICT (from_id, to_id, kind) DO UPDATE SET kind = EXCLUDED.kind
143
+ RETURNING id, from_id AS "fromId", to_id AS "toId", kind, created_at AS "createdAt"
144
+ `;
145
+ return requireOne(rows, "insertTraceLink");
146
+ }
147
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/trace/queries.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Hand-written row/client types for @flytedesk/app-kit/trace.
3
+ *
4
+ * These are written by hand, not generated by Prisma, because this package never
5
+ * builds its own PrismaClient (DEC-39): the consumer generates their own client
6
+ * from their own schema plus the fragment copied in by `trace-sync init`, and this
7
+ * package only ever talks to that client through the minimal structural interface
8
+ * below, via raw SQL. Field names here mirror the field names in
9
+ * prisma/fragments/trace.prisma; query helpers (src/trace/queries.ts) are
10
+ * responsible for aliasing the snake_case SQL columns back onto these camelCase
11
+ * shapes with `AS "camelCaseName"`.
12
+ */
13
+ /**
14
+ * The structural subset of a generated PrismaClient this package actually needs.
15
+ * Deliberately minimal and decoupled from `@prisma/client`'s own types: any object
16
+ * exposing tagged-template `$queryRaw`/`$executeRaw` methods satisfies this,
17
+ * including a real PrismaClient instance, a lightly-wrapped one (e.g. request
18
+ * logging middleware), or a hand-rolled test double.
19
+ */
20
+ export interface TracePrismaClient {
21
+ $queryRaw<T = unknown>(query: TemplateStringsArray, ...values: unknown[]): Promise<T>;
22
+ $executeRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<number>;
23
+ }
24
+ /**
25
+ * Valid `TraceNode.kind` values this package knows how to write.
26
+ * The column itself is a plain `String` (see trace.prisma for why), so consumers
27
+ * remain free to write their own kinds; this union is what src/trace helpers use.
28
+ */
29
+ export type TraceNodeKind = "HTTP_REQUEST" | "JOB" | "EVENT" | "SYSTEM" | "ERROR";
30
+ export interface TraceNodeRow {
31
+ id: bigint;
32
+ externalId: string;
33
+ parentId: bigint | null;
34
+ rootId: bigint;
35
+ kind: string;
36
+ actorId: string | null;
37
+ route: string | null;
38
+ requestData: unknown;
39
+ responseData: unknown;
40
+ statusCode: number | null;
41
+ startedAt: Date;
42
+ endedAt: Date | null;
43
+ durationMs: number | null;
44
+ }
45
+ export interface ModelAuditRow {
46
+ id: bigint;
47
+ traceNodeId: bigint | null;
48
+ modelType: string;
49
+ modelId: string;
50
+ event: string;
51
+ actorId: string | null;
52
+ oldValues: unknown;
53
+ newValues: unknown;
54
+ createdAt: Date;
55
+ }
56
+ export interface ApiCallLogRow {
57
+ id: bigint;
58
+ traceNodeId: bigint | null;
59
+ route: string;
60
+ method: string;
61
+ statusCode: number | null;
62
+ actorId: string | null;
63
+ durationMs: number | null;
64
+ createdAt: Date;
65
+ }
66
+ export interface TraceLinkRow {
67
+ id: bigint;
68
+ fromId: bigint;
69
+ toId: bigint;
70
+ kind: string;
71
+ createdAt: Date;
72
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Hand-written row/client types for @flytedesk/app-kit/trace.
3
+ *
4
+ * These are written by hand, not generated by Prisma, because this package never
5
+ * builds its own PrismaClient (DEC-39): the consumer generates their own client
6
+ * from their own schema plus the fragment copied in by `trace-sync init`, and this
7
+ * package only ever talks to that client through the minimal structural interface
8
+ * below, via raw SQL. Field names here mirror the field names in
9
+ * prisma/fragments/trace.prisma; query helpers (src/trace/queries.ts) are
10
+ * responsible for aliasing the snake_case SQL columns back onto these camelCase
11
+ * shapes with `AS "camelCaseName"`.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/trace/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flytedesk/app-kit",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "Shared platform kit for flytedesk apps: flytedesk-id auth (BFF/OIDC client) and a Postgres-native trace/audit layer.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -19,12 +19,16 @@
19
19
  "pnpm": ">=12.0.0"
20
20
  },
21
21
  "publishConfig": {
22
- "access": "restricted",
22
+ "access": "public",
23
23
  "registry": "https://registry.npmjs.org/"
24
24
  },
25
25
  "files": [
26
- "dist"
26
+ "dist",
27
+ "prisma"
27
28
  ],
29
+ "bin": {
30
+ "trace-sync": "./dist/cli/trace-sync.js"
31
+ },
28
32
  "exports": {
29
33
  "./auth": {
30
34
  "types": "./dist/auth/index.d.ts",
@@ -33,26 +37,68 @@
33
37
  "./trace": {
34
38
  "types": "./dist/trace/index.d.ts",
35
39
  "import": "./dist/trace/index.js"
40
+ },
41
+ "./rate-limit": {
42
+ "types": "./dist/rate-limit/index.d.ts",
43
+ "import": "./dist/rate-limit/index.js"
36
44
  }
37
45
  },
38
46
  "scripts": {
39
- "build": "tsc -p tsconfig.json",
47
+ "build": "tsc -p tsconfig.build.json",
40
48
  "typecheck": "tsc -p tsconfig.json --noEmit",
41
- "prepublishOnly": "npm run build"
49
+ "prepublishOnly": "npm run build",
50
+ "prepare": "node scripts/postinstall-eslint-ts6-shim.cjs",
51
+ "test": "vitest run",
52
+ "test:integration": "vitest run --config vitest.integration.config.ts",
53
+ "lint": "eslint .",
54
+ "format": "prettier --check .",
55
+ "check:exports": "publint && attw --pack . --profile esm-only",
56
+ "changeset": "changeset",
57
+ "version-packages": "changeset version",
58
+ "release": "npm run build && npm run check:exports && changeset publish"
42
59
  },
43
60
  "peerDependencies": {
44
- "fastify": "^4 || ^5",
45
- "@prisma/client": "^7"
61
+ "@prisma/client": "^7",
62
+ "fastify": "^4 || ^5"
46
63
  },
47
64
  "peerDependenciesMeta": {
48
65
  "@prisma/client": {
49
66
  "optional": true
50
67
  }
51
68
  },
69
+ "overrides": {
70
+ "typescript-eslint": {
71
+ "typescript": "$typescript"
72
+ }
73
+ },
52
74
  "devDependencies": {
75
+ "@arethetypeswrong/cli": "^0.18.5",
76
+ "@changesets/changelog-github": "^1.0.0",
77
+ "@changesets/cli": "^3.0.1",
53
78
  "@prisma/client": "^7.10.0",
79
+ "@testcontainers/postgresql": "^12.1.0",
80
+ "@types/cross-spawn": "^6.0.6",
54
81
  "@types/node": "^24.0.0",
82
+ "@types/pg": "^8.23.1",
83
+ "@typescript/typescript6": "^6.0.2",
84
+ "@vitest/coverage-v8": "^4.1.11",
85
+ "eslint": "^10.9.1",
86
+ "eslint-config-prettier": "^10.1.8",
87
+ "eslint-plugin-n": "^18.3.0",
55
88
  "fastify": "^5.12.0",
56
- "typescript": "^5.7.0"
89
+ "globals": "^17.12.0",
90
+ "pg": "^8.23.0",
91
+ "prettier": "^3.9.6",
92
+ "publint": "^0.3.24",
93
+ "testcontainers": "^12.1.0",
94
+ "typescript": "^7.0.2",
95
+ "typescript-eslint": "^8.69.0",
96
+ "vitest": "^4.1.11"
97
+ },
98
+ "dependencies": {
99
+ "@fastify/cookie": "^11.0.1",
100
+ "cross-spawn": "^7.0.6",
101
+ "fastify-plugin": "^6.0.0",
102
+ "jose": "^6.2.10"
57
103
  }
58
104
  }
@@ -0,0 +1,4 @@
1
+ {
2
+ "fragmentVersion": "1.0.0",
3
+ "minCliVersion": "1.0.0"
4
+ }
@@ -0,0 +1,87 @@
1
+ // @flytedesk/app-kit/trace — canonical Prisma model fragment (DEC-39).
2
+ //
3
+ // This fragment is NOT built into a Prisma Client by this package. It is copied
4
+ // verbatim into the consumer's own multi-file Prisma schema folder by `trace-sync
5
+ // init` (see src/cli/trace-sync.ts), and the consumer generates their own client
6
+ // against their own schema + this fragment merged in.
7
+ //
8
+ // Design constraints (deliberate, see DEC-39):
9
+ // - No `@relation`/FK attributes anywhere. These are plain scalar columns. A
10
+ // package that ships models into a consumer's schema cannot see the consumer's
11
+ // other models, and dropping relation attributes avoids lock contention on the
12
+ // hottest insert path (trace nodes are written on every request/job).
13
+ // - No Prisma `enum`. Enums are schema-global in Prisma and would collide across
14
+ // packages that both want to declare, say, an `EventKind`. `kind`/`event` are
15
+ // plain `String` columns; the TypeScript layer (src/trace/types.ts) provides a
16
+ // union type for the values this package writes.
17
+ // - `BigInt @id @default(autoincrement())` primary keys, not UUID, for insert
18
+ // locality on a high-write append log.
19
+ //
20
+ // Table names are prefixed `app_kit_trace_` via `@@map` so they can't collide with
21
+ // consumer tables. Column names are snake_case via `@map`, matching the raw SQL in
22
+ // prisma/migrations/0001_app_kit_trace_init/migration.sql exactly.
23
+
24
+ model TraceNode {
25
+ id BigInt @id @default(autoincrement())
26
+ externalId String @default(uuid()) @db.Uuid @map("external_id")
27
+ parentId BigInt? @map("parent_id")
28
+ rootId BigInt @map("root_id")
29
+ kind String
30
+ actorId String? @map("actor_id")
31
+ route String?
32
+ requestData Json? @map("request_data")
33
+ responseData Json? @map("response_data")
34
+ statusCode Int? @map("status_code")
35
+ startedAt DateTime @default(now()) @map("started_at")
36
+ endedAt DateTime? @map("ended_at")
37
+ durationMs Int? @map("duration_ms")
38
+
39
+ @@index([rootId])
40
+ @@index([parentId])
41
+ @@index([route, startedAt(sort: Desc)])
42
+ @@index([actorId, startedAt(sort: Desc)])
43
+ @@map("app_kit_trace_nodes")
44
+ }
45
+
46
+ model ModelAudit {
47
+ id BigInt @id @default(autoincrement())
48
+ traceNodeId BigInt? @map("trace_node_id")
49
+ modelType String @map("model_type")
50
+ modelId String @map("model_id")
51
+ event String
52
+ actorId String? @map("actor_id")
53
+ oldValues Json? @map("old_values")
54
+ newValues Json? @map("new_values")
55
+ createdAt DateTime @default(now()) @map("created_at")
56
+
57
+ @@index([modelType, modelId, createdAt(sort: Desc)])
58
+ @@index([traceNodeId])
59
+ @@map("app_kit_trace_model_audits")
60
+ }
61
+
62
+ model ApiCallLog {
63
+ id BigInt @id @default(autoincrement())
64
+ traceNodeId BigInt? @map("trace_node_id")
65
+ route String
66
+ method String
67
+ statusCode Int? @map("status_code")
68
+ actorId String? @map("actor_id")
69
+ durationMs Int? @map("duration_ms")
70
+ createdAt DateTime @default(now()) @map("created_at")
71
+
72
+ @@index([route, createdAt(sort: Desc)])
73
+ @@index([actorId, createdAt(sort: Desc)])
74
+ @@map("app_kit_trace_api_call_logs")
75
+ }
76
+
77
+ model TraceLink {
78
+ id BigInt @id @default(autoincrement())
79
+ fromId BigInt @map("from_id")
80
+ toId BigInt @map("to_id")
81
+ kind String
82
+ createdAt DateTime @default(now()) @map("created_at")
83
+
84
+ @@unique([fromId, toId, kind])
85
+ @@index([toId])
86
+ @@map("app_kit_trace_links")
87
+ }