@validation-os/api 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -32,12 +32,36 @@ declare function recomputeAllDerived(provider: DataProvider): Promise<number>;
32
32
  */
33
33
 
34
34
  interface AuthResult {
35
- userId: string;
35
+ /**
36
+ * The raw, verified identity-provider subject (e.g. the Clerk user ID). The
37
+ * package treats it as an opaque string and resolves it against the roster;
38
+ * the auth vendor stays confined to the injected `authenticate`.
39
+ */
40
+ subject: string;
41
+ }
42
+ /**
43
+ * One team member the register recognises — the `vocabulary.dashboard_users`
44
+ * entry. `authSubject` is the IdP-neutral mapping a caller's token subject
45
+ * resolves through; `name` is what lands in `Owner` / `Agreed by` (OPS-1343).
46
+ */
47
+ interface DashboardUser {
48
+ name: string;
49
+ authSubject: string;
36
50
  }
37
51
  interface CreateApiOptions {
38
52
  provider: DataProvider;
39
- /** Return the authenticated principal, or null to reject with 401. */
53
+ /**
54
+ * Verify the request's bearer token and return its subject, or null to
55
+ * reject with 401. Roster resolution (subject → member) happens here in the
56
+ * package, so `authenticate` carries no auth-vendor-specific mapping.
57
+ */
40
58
  authenticate: (req: Request) => Promise<AuthResult | null> | AuthResult | null;
59
+ /**
60
+ * The known team roster. A caller whose subject resolves to a member may
61
+ * write (any register); an unmapped subject is a 403. Sourced from
62
+ * `vocabulary.dashboard_users` in the deployment config.
63
+ */
64
+ roster: DashboardUser[];
41
65
  /** Run derive-on-write after writes (default true). */
42
66
  deriveOnWrite?: boolean;
43
67
  }
@@ -63,4 +87,4 @@ interface ValidationOsApi {
63
87
  }
64
88
  declare function createApi(options: CreateApiOptions): ValidationOsApi;
65
89
 
66
- export { type AuthResult, type CreateApiOptions, type RouteContext, type ValidationOsApi, createApi, deriveReadingFields, recomputeAllDerived };
90
+ export { type AuthResult, type CreateApiOptions, type DashboardUser, type RouteContext, type ValidationOsApi, createApi, deriveReadingFields, recomputeAllDerived };
package/dist/index.js CHANGED
@@ -54,6 +54,30 @@ async function recomputeAllDerived(provider) {
54
54
  // src/index.ts
55
55
  var COLLECTIONS = new Set(REGISTERS);
56
56
  var DERIVED_TRIGGERS = /* @__PURE__ */ new Set(["assumptions", "readings", "decisions"]);
57
+ var REGISTERS_WITH_OWNER = /* @__PURE__ */ new Set([
58
+ "assumptions",
59
+ "experiments",
60
+ "readings",
61
+ "decisions"
62
+ ]);
63
+ function ownerAbsent(value) {
64
+ return value === void 0 || value === null || Array.isArray(value) && value.length === 0;
65
+ }
66
+ function assertMembers(field, value, names) {
67
+ if (value === void 0 || value === null) return;
68
+ const entries = Array.isArray(value) ? value : [value];
69
+ for (const v of entries) {
70
+ if (typeof v !== "string" || !names.has(v)) {
71
+ throw new BadRequest(
72
+ `${field} must reference a team member; "${String(v)}" is not one.`
73
+ );
74
+ }
75
+ }
76
+ }
77
+ function assertIdentityFields(data, names) {
78
+ assertMembers("Owner", data.Owner, names);
79
+ assertMembers("Agreed by", data["Agreed by"], names);
80
+ }
57
81
  function json(body, status = 200) {
58
82
  return new Response(JSON.stringify(body), {
59
83
  status,
@@ -117,10 +141,13 @@ function toErrorResponse(e) {
117
141
  function createApi(options) {
118
142
  const { provider } = options;
119
143
  const deriveOn = options.deriveOnWrite ?? true;
144
+ const memberNames = new Set(options.roster.map((u) => u.name));
120
145
  async function guard(req) {
121
146
  const auth = await options.authenticate(req);
122
147
  if (!auth) throw new Unauthorized();
123
- return auth;
148
+ const member = options.roster.find((u) => u.authSubject === auth.subject);
149
+ if (!member) throw new Forbidden();
150
+ return { name: member.name };
124
151
  }
125
152
  async function maybeRecompute(register) {
126
153
  if (deriveOn && DERIVED_TRIGGERS.has(register)) {
@@ -150,9 +177,13 @@ function createApi(options) {
150
177
  },
151
178
  async create(req, ctx) {
152
179
  try {
153
- await guard(req);
180
+ const { name } = await guard(req);
154
181
  const register = assertRegister((await resolveParams(ctx)).register);
155
182
  let data = await req.json();
183
+ assertIdentityFields(data, memberNames);
184
+ if (REGISTERS_WITH_OWNER.has(register) && ownerAbsent(data.Owner)) {
185
+ data = { ...data, Owner: [name] };
186
+ }
156
187
  if (register === "readings") data = deriveReadingFields(data);
157
188
  const created = await provider.create(register, data);
158
189
  await maybeRecompute(register);
@@ -172,6 +203,7 @@ function createApi(options) {
172
203
  if (typeof version !== "number") {
173
204
  throw new BadRequest("A numeric `version` is required for updates.");
174
205
  }
206
+ assertIdentityFields(rawPatch, memberNames);
175
207
  const patch = register === "readings" ? deriveReadingFields(rawPatch) : rawPatch;
176
208
  const updated = await provider.update(register, p.id, patch, version);
177
209
  await maybeRecompute(register);
@@ -222,10 +254,21 @@ function createApi(options) {
222
254
  }
223
255
  var Unauthorized = class extends Error {
224
256
  };
257
+ var Forbidden = class extends Error {
258
+ };
225
259
  function handle(e) {
226
260
  if (e instanceof Unauthorized) {
227
261
  return json({ error: "unauthorized" }, 401);
228
262
  }
263
+ if (e instanceof Forbidden) {
264
+ return json(
265
+ {
266
+ error: "forbidden",
267
+ message: "Your account isn't on this register's team yet \u2014 ask an admin to add you."
268
+ },
269
+ 403
270
+ );
271
+ }
229
272
  return toErrorResponse(e);
230
273
  }
231
274
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/derive-on-write.ts"],"sourcesContent":["/**\n * @validation-os/api — framework-neutral route-handler logic.\n *\n * Handlers operate on the Web `Request`/`Response` types (what Next.js app-\n * router route handlers receive), so the same functions drop into a Next app\n * or any Web-standard runtime. Authentication is injected — the deployed app\n * supplies a Clerk-verifying `authenticate`, so this package stays free of any\n * auth-vendor dependency. Every write runs the derivation module server-side.\n */\nimport {\n REGISTERS,\n RELATIONS,\n isNotFoundError,\n isStaleVersionError,\n type Collection,\n type DataProvider,\n type RecordRef,\n type Relation,\n} from \"@validation-os/core\";\nimport {\n deriveReadingFields,\n recomputeAllDerived,\n} from \"./derive-on-write.js\";\n\nexport { deriveReadingFields, recomputeAllDerived };\n\nconst COLLECTIONS = new Set<string>(REGISTERS);\n/** Writes to these trigger the cross-record derived recompute pass. */\nconst DERIVED_TRIGGERS = new Set<string>([\"assumptions\", \"readings\", \"decisions\"]);\n\nexport interface AuthResult {\n userId: string;\n}\n\nexport interface CreateApiOptions {\n provider: DataProvider;\n /** Return the authenticated principal, or null to reject with 401. */\n authenticate: (req: Request) => Promise<AuthResult | null> | AuthResult | null;\n /** Run derive-on-write after writes (default true). */\n deriveOnWrite?: boolean;\n}\n\ntype Params = Record<string, string | undefined>;\nexport interface RouteContext {\n params: Params | Promise<Params>;\n}\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\nasync function resolveParams(ctx: RouteContext): Promise<Params> {\n return await ctx.params;\n}\n\nfunction assertRegister(value: string | undefined): Collection {\n if (!value || !COLLECTIONS.has(value)) {\n throw new BadRequest(`Unknown register: ${String(value)}`);\n }\n return value as Collection;\n}\n\nclass BadRequest extends Error {}\n\ninterface LinkBody {\n relation: Relation;\n from: RecordRef;\n to: RecordRef;\n}\n\n/** Validate a POST /api/link body against the relation config. */\nfunction parseLinkBody(body: unknown): LinkBody {\n const b = body as Partial<{ relation: string; from: RecordRef; to: RecordRef }>;\n const relation = b.relation;\n if (typeof relation !== \"string\" || !(relation in RELATIONS)) {\n throw new BadRequest(`Unknown relation: ${String(relation)}`);\n }\n const spec = RELATIONS[relation as Relation];\n const from = parseRef(b.from, \"from\");\n const to = parseRef(b.to, \"to\");\n // The `from` end must be the register the relation config names; otherwise a\n // caller could write the wrong field. The `to` end is checked when it is a\n // stored (non-derived) end.\n if (from.register !== spec.from.register) {\n throw new BadRequest(\n `Relation ${relation} starts on ${spec.from.register}, not ${from.register}.`,\n );\n }\n if (to.register !== spec.targetRegister) {\n throw new BadRequest(\n `Relation ${relation} ends on ${spec.targetRegister}, not ${to.register}.`,\n );\n }\n return { relation: relation as Relation, from, to };\n}\n\nfunction parseRef(value: unknown, which: string): RecordRef {\n const ref = value as Partial<RecordRef> | undefined;\n if (!ref || typeof ref.id !== \"string\" || !ref.id) {\n throw new BadRequest(`Missing \\`${which}.id\\`.`);\n }\n return { register: assertRegister(ref.register), id: ref.id };\n}\n\n/** Turn a thrown error into the right HTTP response, in plain language. */\nfunction toErrorResponse(e: unknown): Response {\n if (e instanceof BadRequest) return json({ error: e.message }, 400);\n if (isNotFoundError(e)) return json({ error: e.message }, 404);\n if (isStaleVersionError(e)) {\n return json(\n {\n error: \"conflict\",\n // Plain-language copy — never version jargon (spec user story 12).\n message:\n \"Someone edited this while you had it open — your changes are \" +\n \"safe, take a look before saving again.\",\n },\n 409,\n );\n }\n return json({ error: \"internal\", message: \"Something went wrong.\" }, 500);\n}\n\nexport interface ValidationOsApi {\n /** GET /api/[register] — list every row of a register. */\n list(req: Request, ctx: RouteContext): Promise<Response>;\n /** GET /api/[register]/[id] — one record. */\n get(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/[register] — create; body is the record fields. */\n create(req: Request, ctx: RouteContext): Promise<Response>;\n /** PATCH /api/[register]/[id] — body { version, ...patch }. */\n update(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/link — body { relation, from, to }; sets both ends. */\n link(req: Request): Promise<Response>;\n /** GET /api/counts — per-register row counts (the walking-skeleton value). */\n counts(req: Request): Promise<Response>;\n /** POST /api/recompute — run the derived-fields backstop pass. */\n recompute(req: Request): Promise<Response>;\n}\n\nexport function createApi(options: CreateApiOptions): ValidationOsApi {\n const { provider } = options;\n const deriveOn = options.deriveOnWrite ?? true;\n\n async function guard(req: Request): Promise<AuthResult> {\n const auth = await options.authenticate(req);\n if (!auth) throw new Unauthorized();\n return auth;\n }\n\n async function maybeRecompute(register: Collection): Promise<void> {\n if (deriveOn && DERIVED_TRIGGERS.has(register)) {\n await recomputeAllDerived(provider);\n }\n }\n\n return {\n async list(req, ctx) {\n try {\n await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n return json({ data: await provider.list(register) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async get(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n return json({ data: await provider.get(register, p.id) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async create(req, ctx) {\n try {\n await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n let data = (await req.json()) as Record<string, unknown>;\n if (register === \"readings\") data = deriveReadingFields(data);\n const created = await provider.create(register, data);\n await maybeRecompute(register);\n return json({ data: created }, 201);\n } catch (e) {\n return handle(e);\n }\n },\n\n async update(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n const body = (await req.json()) as Record<string, unknown>;\n const { version, ...rawPatch } = body;\n if (typeof version !== \"number\") {\n throw new BadRequest(\"A numeric `version` is required for updates.\");\n }\n const patch =\n register === \"readings\" ? deriveReadingFields(rawPatch) : rawPatch;\n const updated = await provider.update(register, p.id, patch, version);\n await maybeRecompute(register);\n return json({ data: updated });\n } catch (e) {\n return handle(e);\n }\n },\n\n async link(req) {\n try {\n await guard(req);\n const { relation, from, to } = parseLinkBody(await req.json());\n await provider.link(relation, from, to);\n // A relation can move a derived number (a reading joins a belief, a\n // standing decision lands, a dependency edge appears), so run the same\n // backstop recompute the write routes use whenever an end is a trigger.\n const spec = RELATIONS[relation];\n if (\n DERIVED_TRIGGERS.has(spec.from.register) ||\n (spec.to != null && DERIVED_TRIGGERS.has(spec.to.register))\n ) {\n await recomputeAllDerived(provider);\n }\n return json({ data: { linked: true } });\n } catch (e) {\n return handle(e);\n }\n },\n\n async counts(req) {\n try {\n await guard(req);\n const registers = [...COLLECTIONS] as Collection[];\n const counts: Record<string, number> = {};\n await Promise.all(\n registers.map(async (r) => {\n counts[r] = (await provider.list(r)).length;\n }),\n );\n return json({ counts });\n } catch (e) {\n return handle(e);\n }\n },\n\n async recompute(req) {\n try {\n await guard(req);\n const updated = await recomputeAllDerived(provider);\n return json({ updated });\n } catch (e) {\n return handle(e);\n }\n },\n };\n}\n\nclass Unauthorized extends Error {}\n\nfunction handle(e: unknown): Response {\n if (e instanceof Unauthorized) {\n return json({ error: \"unauthorized\" }, 401);\n }\n return toErrorResponse(e);\n}\n","/**\n * Derive-on-write — the API runs the shared derivation module server-side so\n * stored derived values are always authoritative, whatever the client sent.\n *\n * Two layers:\n * - `deriveReadingFields`: a reading's own Source quality + Strength are pure\n * functions of the record itself — stamped inline on every reading write.\n * - `recomputeAllDerived`: the cross-record pass (Confidence, Derived Impact,\n * Risk over the whole assumptions register) — the backstop the spec keeps\n * for non-dashboard writes, run after any assumption/reading/decision write.\n */\nimport {\n recomputeDerived,\n recomputeSourceQuality,\n recomputeStrength,\n type AnyRecord,\n type AssumptionRecord,\n type DataProvider,\n type DecisionRecord,\n type ReadingRecord,\n} from \"@validation-os/core\";\n\n/** Stamp Source quality + Strength onto a reading's `derived` field. */\nexport function deriveReadingFields(data: Partial<AnyRecord>): Partial<AnyRecord> {\n const r = data as Partial<ReadingRecord>;\n if (r.Rung == null || r.Result == null) return data; // not enough to derive\n const sourceQuality = recomputeSourceQuality(\n Number(r.Representativeness ?? 0),\n Number(r.Credibility ?? 0),\n );\n const strength = recomputeStrength({\n rung: r.Rung,\n result: r.Result,\n magnitudeBand: r.magnitudeBand,\n });\n return { ...data, derived: { sourceQuality, strength } };\n}\n\n/**\n * Recompute Confidence / Derived Impact / Risk for every assumption and write\n * back only those whose derived tuple actually changed (so unrelated rows\n * don't churn their version). Returns the number of rows updated.\n */\nexport async function recomputeAllDerived(\n provider: DataProvider,\n): Promise<number> {\n const [assumptions, readings, decisions] = await Promise.all([\n provider.list(\"assumptions\"),\n provider.list(\"readings\"),\n provider.list(\"decisions\"),\n ]);\n\n const derived = recomputeDerived({\n assumptions: assumptions as unknown as AssumptionRecord[],\n readings: readings as unknown as ReadingRecord[],\n decisions: decisions as unknown as DecisionRecord[],\n });\n\n let updated = 0;\n for (const a of assumptions) {\n const next = derived.get(a.id);\n if (!next) continue;\n const cur = (a.derived ?? {}) as Partial<AssumptionRecord[\"derived\"]>;\n if (\n cur.confidence === next.confidence &&\n cur.risk === next.risk &&\n cur.derivedImpact === next.derivedImpact &&\n cur.completeness === next.completeness\n ) {\n continue;\n }\n await provider.update(\"assumptions\", a.id, { derived: next }, a.version);\n updated += 1;\n }\n return updated;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/derive-on-write.ts"],"sourcesContent":["/**\n * @validation-os/api — framework-neutral route-handler logic.\n *\n * Handlers operate on the Web `Request`/`Response` types (what Next.js app-\n * router route handlers receive), so the same functions drop into a Next app\n * or any Web-standard runtime. Authentication is injected — the deployed app\n * supplies a Clerk-verifying `authenticate`, so this package stays free of any\n * auth-vendor dependency. Every write runs the derivation module server-side.\n */\nimport {\n REGISTERS,\n RELATIONS,\n isNotFoundError,\n isStaleVersionError,\n type Collection,\n type DataProvider,\n type RecordRef,\n type Relation,\n} from \"@validation-os/core\";\nimport {\n deriveReadingFields,\n recomputeAllDerived,\n} from \"./derive-on-write.js\";\n\nexport { deriveReadingFields, recomputeAllDerived };\n\nconst COLLECTIONS = new Set<string>(REGISTERS);\n/** Writes to these trigger the cross-record derived recompute pass. */\nconst DERIVED_TRIGGERS = new Set<string>([\"assumptions\", \"readings\", \"decisions\"]);\n\nexport interface AuthResult {\n /**\n * The raw, verified identity-provider subject (e.g. the Clerk user ID). The\n * package treats it as an opaque string and resolves it against the roster;\n * the auth vendor stays confined to the injected `authenticate`.\n */\n subject: string;\n}\n\n/**\n * One team member the register recognises — the `vocabulary.dashboard_users`\n * entry. `authSubject` is the IdP-neutral mapping a caller's token subject\n * resolves through; `name` is what lands in `Owner` / `Agreed by` (OPS-1343).\n */\nexport interface DashboardUser {\n name: string;\n authSubject: string;\n}\n\nexport interface CreateApiOptions {\n provider: DataProvider;\n /**\n * Verify the request's bearer token and return its subject, or null to\n * reject with 401. Roster resolution (subject → member) happens here in the\n * package, so `authenticate` carries no auth-vendor-specific mapping.\n */\n authenticate: (req: Request) => Promise<AuthResult | null> | AuthResult | null;\n /**\n * The known team roster. A caller whose subject resolves to a member may\n * write (any register); an unmapped subject is a 403. Sourced from\n * `vocabulary.dashboard_users` in the deployment config.\n */\n roster: DashboardUser[];\n /** Run derive-on-write after writes (default true). */\n deriveOnWrite?: boolean;\n}\n\n/** Registers that carry an `Owner`; glossary has none. */\nconst REGISTERS_WITH_OWNER = new Set<string>([\n \"assumptions\",\n \"experiments\",\n \"readings\",\n \"decisions\",\n]);\n\n/** True when a create supplied no Owner to stamp the caller into. */\nfunction ownerAbsent(value: unknown): boolean {\n return (\n value === undefined ||\n value === null ||\n (Array.isArray(value) && value.length === 0)\n );\n}\n\n/**\n * Every identity a write names (Owner, Agreed by) must be a roster member —\n * the body's word for *who* is never trusted. A stranger name is a 400.\n */\nfunction assertMembers(\n field: string,\n value: unknown,\n names: Set<string>,\n): void {\n if (value === undefined || value === null) return;\n const entries = Array.isArray(value) ? value : [value];\n for (const v of entries) {\n if (typeof v !== \"string\" || !names.has(v)) {\n throw new BadRequest(\n `${field} must reference a team member; \"${String(v)}\" is not one.`,\n );\n }\n }\n}\n\n/** Both identity fields a write may carry must name roster members. */\nfunction assertIdentityFields(\n data: Record<string, unknown>,\n names: Set<string>,\n): void {\n assertMembers(\"Owner\", data.Owner, names);\n assertMembers(\"Agreed by\", data[\"Agreed by\"], names);\n}\n\ntype Params = Record<string, string | undefined>;\nexport interface RouteContext {\n params: Params | Promise<Params>;\n}\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\nasync function resolveParams(ctx: RouteContext): Promise<Params> {\n return await ctx.params;\n}\n\nfunction assertRegister(value: string | undefined): Collection {\n if (!value || !COLLECTIONS.has(value)) {\n throw new BadRequest(`Unknown register: ${String(value)}`);\n }\n return value as Collection;\n}\n\nclass BadRequest extends Error {}\n\ninterface LinkBody {\n relation: Relation;\n from: RecordRef;\n to: RecordRef;\n}\n\n/** Validate a POST /api/link body against the relation config. */\nfunction parseLinkBody(body: unknown): LinkBody {\n const b = body as Partial<{ relation: string; from: RecordRef; to: RecordRef }>;\n const relation = b.relation;\n if (typeof relation !== \"string\" || !(relation in RELATIONS)) {\n throw new BadRequest(`Unknown relation: ${String(relation)}`);\n }\n const spec = RELATIONS[relation as Relation];\n const from = parseRef(b.from, \"from\");\n const to = parseRef(b.to, \"to\");\n // The `from` end must be the register the relation config names; otherwise a\n // caller could write the wrong field. The `to` end is checked when it is a\n // stored (non-derived) end.\n if (from.register !== spec.from.register) {\n throw new BadRequest(\n `Relation ${relation} starts on ${spec.from.register}, not ${from.register}.`,\n );\n }\n if (to.register !== spec.targetRegister) {\n throw new BadRequest(\n `Relation ${relation} ends on ${spec.targetRegister}, not ${to.register}.`,\n );\n }\n return { relation: relation as Relation, from, to };\n}\n\nfunction parseRef(value: unknown, which: string): RecordRef {\n const ref = value as Partial<RecordRef> | undefined;\n if (!ref || typeof ref.id !== \"string\" || !ref.id) {\n throw new BadRequest(`Missing \\`${which}.id\\`.`);\n }\n return { register: assertRegister(ref.register), id: ref.id };\n}\n\n/** Turn a thrown error into the right HTTP response, in plain language. */\nfunction toErrorResponse(e: unknown): Response {\n if (e instanceof BadRequest) return json({ error: e.message }, 400);\n if (isNotFoundError(e)) return json({ error: e.message }, 404);\n if (isStaleVersionError(e)) {\n return json(\n {\n error: \"conflict\",\n // Plain-language copy — never version jargon (spec user story 12).\n message:\n \"Someone edited this while you had it open — your changes are \" +\n \"safe, take a look before saving again.\",\n },\n 409,\n );\n }\n return json({ error: \"internal\", message: \"Something went wrong.\" }, 500);\n}\n\nexport interface ValidationOsApi {\n /** GET /api/[register] — list every row of a register. */\n list(req: Request, ctx: RouteContext): Promise<Response>;\n /** GET /api/[register]/[id] — one record. */\n get(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/[register] — create; body is the record fields. */\n create(req: Request, ctx: RouteContext): Promise<Response>;\n /** PATCH /api/[register]/[id] — body { version, ...patch }. */\n update(req: Request, ctx: RouteContext): Promise<Response>;\n /** POST /api/link — body { relation, from, to }; sets both ends. */\n link(req: Request): Promise<Response>;\n /** GET /api/counts — per-register row counts (the walking-skeleton value). */\n counts(req: Request): Promise<Response>;\n /** POST /api/recompute — run the derived-fields backstop pass. */\n recompute(req: Request): Promise<Response>;\n}\n\nexport function createApi(options: CreateApiOptions): ValidationOsApi {\n const { provider } = options;\n const deriveOn = options.deriveOnWrite ?? true;\n\n const memberNames = new Set(options.roster.map((u) => u.name));\n\n /**\n * The membership gate (OPS-1343): a request with no/invalid token is a 401;\n * a valid token whose subject is on no roster entry is a 403. Returns the\n * resolved member — one lookup is both gate and identity source.\n */\n async function guard(req: Request): Promise<{ name: string }> {\n const auth = await options.authenticate(req);\n if (!auth) throw new Unauthorized();\n const member = options.roster.find((u) => u.authSubject === auth.subject);\n if (!member) throw new Forbidden();\n return { name: member.name };\n }\n\n async function maybeRecompute(register: Collection): Promise<void> {\n if (deriveOn && DERIVED_TRIGGERS.has(register)) {\n await recomputeAllDerived(provider);\n }\n }\n\n return {\n async list(req, ctx) {\n try {\n await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n return json({ data: await provider.list(register) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async get(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n return json({ data: await provider.get(register, p.id) });\n } catch (e) {\n return handle(e);\n }\n },\n\n async create(req, ctx) {\n try {\n const { name } = await guard(req);\n const register = assertRegister((await resolveParams(ctx)).register);\n let data = (await req.json()) as Record<string, unknown>;\n // Identity is stamped, not trusted: any Owner/Agreed-by the client sent\n // must be a roster member, and an omitted Owner defaults to the caller.\n assertIdentityFields(data, memberNames);\n if (REGISTERS_WITH_OWNER.has(register) && ownerAbsent(data.Owner)) {\n data = { ...data, Owner: [name] };\n }\n if (register === \"readings\") data = deriveReadingFields(data);\n const created = await provider.create(register, data);\n await maybeRecompute(register);\n return json({ data: created }, 201);\n } catch (e) {\n return handle(e);\n }\n },\n\n async update(req, ctx) {\n try {\n await guard(req);\n const p = await resolveParams(ctx);\n const register = assertRegister(p.register);\n if (!p.id) throw new BadRequest(\"Missing id\");\n const body = (await req.json()) as Record<string, unknown>;\n const { version, ...rawPatch } = body;\n if (typeof version !== \"number\") {\n throw new BadRequest(\"A numeric `version` is required for updates.\");\n }\n // An update may re-attribute Owner/Agreed-by, but only to roster members.\n assertIdentityFields(rawPatch, memberNames);\n const patch =\n register === \"readings\" ? deriveReadingFields(rawPatch) : rawPatch;\n const updated = await provider.update(register, p.id, patch, version);\n await maybeRecompute(register);\n return json({ data: updated });\n } catch (e) {\n return handle(e);\n }\n },\n\n async link(req) {\n try {\n await guard(req);\n const { relation, from, to } = parseLinkBody(await req.json());\n await provider.link(relation, from, to);\n // A relation can move a derived number (a reading joins a belief, a\n // standing decision lands, a dependency edge appears), so run the same\n // backstop recompute the write routes use whenever an end is a trigger.\n const spec = RELATIONS[relation];\n if (\n DERIVED_TRIGGERS.has(spec.from.register) ||\n (spec.to != null && DERIVED_TRIGGERS.has(spec.to.register))\n ) {\n await recomputeAllDerived(provider);\n }\n return json({ data: { linked: true } });\n } catch (e) {\n return handle(e);\n }\n },\n\n async counts(req) {\n try {\n await guard(req);\n const registers = [...COLLECTIONS] as Collection[];\n const counts: Record<string, number> = {};\n await Promise.all(\n registers.map(async (r) => {\n counts[r] = (await provider.list(r)).length;\n }),\n );\n return json({ counts });\n } catch (e) {\n return handle(e);\n }\n },\n\n async recompute(req) {\n try {\n await guard(req);\n const updated = await recomputeAllDerived(provider);\n return json({ updated });\n } catch (e) {\n return handle(e);\n }\n },\n };\n}\n\nclass Unauthorized extends Error {}\n/** Authenticated, but the subject resolves to no roster member (OPS-1343). */\nclass Forbidden extends Error {}\n\nfunction handle(e: unknown): Response {\n if (e instanceof Unauthorized) {\n return json({ error: \"unauthorized\" }, 401);\n }\n if (e instanceof Forbidden) {\n return json(\n {\n error: \"forbidden\",\n message:\n \"Your account isn't on this register's team yet — ask an admin to \" +\n \"add you.\",\n },\n 403,\n );\n }\n return toErrorResponse(e);\n}\n","/**\n * Derive-on-write — the API runs the shared derivation module server-side so\n * stored derived values are always authoritative, whatever the client sent.\n *\n * Two layers:\n * - `deriveReadingFields`: a reading's own Source quality + Strength are pure\n * functions of the record itself — stamped inline on every reading write.\n * - `recomputeAllDerived`: the cross-record pass (Confidence, Derived Impact,\n * Risk over the whole assumptions register) — the backstop the spec keeps\n * for non-dashboard writes, run after any assumption/reading/decision write.\n */\nimport {\n recomputeDerived,\n recomputeSourceQuality,\n recomputeStrength,\n type AnyRecord,\n type AssumptionRecord,\n type DataProvider,\n type DecisionRecord,\n type ReadingRecord,\n} from \"@validation-os/core\";\n\n/** Stamp Source quality + Strength onto a reading's `derived` field. */\nexport function deriveReadingFields(data: Partial<AnyRecord>): Partial<AnyRecord> {\n const r = data as Partial<ReadingRecord>;\n if (r.Rung == null || r.Result == null) return data; // not enough to derive\n const sourceQuality = recomputeSourceQuality(\n Number(r.Representativeness ?? 0),\n Number(r.Credibility ?? 0),\n );\n const strength = recomputeStrength({\n rung: r.Rung,\n result: r.Result,\n magnitudeBand: r.magnitudeBand,\n });\n return { ...data, derived: { sourceQuality, strength } };\n}\n\n/**\n * Recompute Confidence / Derived Impact / Risk for every assumption and write\n * back only those whose derived tuple actually changed (so unrelated rows\n * don't churn their version). Returns the number of rows updated.\n */\nexport async function recomputeAllDerived(\n provider: DataProvider,\n): Promise<number> {\n const [assumptions, readings, decisions] = await Promise.all([\n provider.list(\"assumptions\"),\n provider.list(\"readings\"),\n provider.list(\"decisions\"),\n ]);\n\n const derived = recomputeDerived({\n assumptions: assumptions as unknown as AssumptionRecord[],\n readings: readings as unknown as ReadingRecord[],\n decisions: decisions as unknown as DecisionRecord[],\n });\n\n let updated = 0;\n for (const a of assumptions) {\n const next = derived.get(a.id);\n if (!next) continue;\n const cur = (a.derived ?? {}) as Partial<AssumptionRecord[\"derived\"]>;\n if (\n cur.confidence === next.confidence &&\n cur.risk === next.risk &&\n cur.derivedImpact === next.derivedImpact &&\n cur.completeness === next.completeness\n ) {\n continue;\n }\n await provider.update(\"assumptions\", a.id, { derived: next }, a.version);\n updated += 1;\n }\n return updated;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validation-os/api",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Framework-neutral route-handler logic for validation-os: auth-guarded CRUD over a DataProvider, with derive-on-write via the shared derivation module.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "@validation-os/core": "0.7.1"
24
+ "@validation-os/core": "0.8.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsup": "^8.3.0",