Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@forwardimpact/landmark

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@forwardimpact/landmark - npm Package Compare versions

Comparing version
0.1.14
to
0.1.15
+179
src/commands/sources.js
/**
* `fit-landmark sources` — list activity row classes retained about an engineer.
*
* Iterates the SOURCE_CLASSES registry — one entry per RLS'd activity table —
* issuing an asc+desc pair through the authenticated Supabase client. RLS
* clamps both, so results reflect the caller's view. Classes with zero
* visible rows are filtered out; if all classes clamp to zero the command
* renders NO_SOURCES_FOR_PERSON.
*/
import { readRetention } from "@forwardimpact/map/activity/retention";
import { EMPTY_STATES } from "../lib/empty-state.js";
export const needsSupabase = true;
const CLASSES = [
{
id: "organization_people",
label: "Profile",
// organization_people is the null-window class — its retention COMMENT is
// intentionally empty (admitted by activity._validate_retention_blob's
// organization_people exemption), so readRetention returns {window: null,
// clock: null}. The table's actual timestamp column is `updated_at`, so
// declare it here so the asc/desc probes in inventoryClass don't fall
// back to `imported_at` (which doesn't exist on this table).
clock: "updated_at",
plan: async (_s, e) => ({
table: "organization_people",
filter: (q) => q.eq("email", e),
}),
},
{
id: "evidence",
label: "Evidence",
plan: async (_s, e) => ({
table: "evidence",
select: "created_at,github_artifacts!inner(email)",
filter: (q) => q.eq("github_artifacts.email", e),
}),
},
{
id: "github_artifacts",
label: "GitHub artifacts",
plan: async (_s, e) => ({
table: "github_artifacts",
filter: (q) => q.eq("email", e),
}),
},
{
id: "getdx_snapshot_comments",
label: "GetDX comments",
plan: async (_s, e) => ({
table: "getdx_snapshot_comments",
filter: (q) => q.eq("email", e),
}),
},
{
id: "getdx_snapshot_team_scores",
label: "GetDX team scores",
plan: async (s, e) => {
const { data, error } = await s
.from("organization_people")
.select("getdx_team_id")
.eq("email", e)
.maybeSingle();
if (error) throw error;
const t = data?.getdx_team_id;
return t
? {
table: "getdx_snapshot_team_scores",
filter: (q) => q.eq("getdx_team_id", t),
}
: null;
},
},
{
id: "getdx_snapshots",
label: "GetDX snapshot cycles",
plan: async (s, e) => {
const { data, error } = await s.rpc("snapshot_ids_for_person", {
p_email: e,
});
if (error) throw error;
const ids = (data ?? []).map((r) => r.snapshot_id);
return ids.length
? {
table: "getdx_snapshots",
filter: (q) => q.in("snapshot_id", ids),
}
: null;
},
},
];
async function inventoryClass(supabase, cls, email) {
const plan = await cls.plan(supabase, email);
if (!plan) return null;
const ret = await readRetention(supabase, cls.id);
// Resolution order: retention metadata wins (single source of truth for
// classes that declare a window), then per-class override (for null-window
// classes that still have a usable timestamp column), then "imported_at"
// as the documented default the other five tables happen to use.
const clock = ret.clock ?? cls.clock ?? "imported_at";
const sel = plan.select ?? `${clock}`;
const asc = plan.filter(
supabase
.from(plan.table)
.select(sel, { count: "exact" })
.order(clock, { ascending: true })
.limit(1),
);
const desc = plan.filter(
supabase
.from(plan.table)
.select(sel)
.order(clock, { ascending: false })
.limit(1),
);
const [ascRes, descRes] = await Promise.all([asc, desc]);
if (ascRes.error) throw ascRes.error;
if (descRes.error) throw descRes.error;
const { data: oldRows, count } = ascRes;
const { data: newRows } = descRes;
if (!count) return null;
const oldest = oldRows[0]?.[clock] ?? null;
const newest = newRows[0]?.[clock] ?? null;
const falloff = ret.window && oldest ? addIso(oldest, ret.window) : null;
return {
id: cls.id,
label: cls.label,
count,
oldest,
newest,
window: ret.window,
falloff,
};
}
/**
* Run the sources command — list activity row classes retained about `email`.
*
* @param {object} params
* @param {object} params.options
* @param {string} params.options.email
* @param {import("@supabase/supabase-js").SupabaseClient} params.supabase
* @param {string} [params.format]
* @returns {Promise<{view: {email: string, items: object[]}|null, meta: object}>}
*/
export async function runSourcesCommand({ options, supabase, format }) {
const email = options.email;
if (!email) throw new Error("sources: --email <e> is required");
const items = [];
for (const cls of CLASSES) {
const item = await inventoryClass(supabase, cls, email);
if (item) items.push(item);
}
if (!items.length) {
return {
view: null,
meta: {
format,
emptyState: EMPTY_STATES.NO_SOURCES_FOR_PERSON(email),
},
};
}
return { view: { email, items }, meta: { format } };
}
function addIso(ts, p) {
const m = /^P(\d+)([DWMY])$/.exec(p);
if (!m) return null;
const d = new Date(ts);
const n = Number(m[1]);
if (m[2] === "D") d.setUTCDate(d.getUTCDate() + n);
if (m[2] === "W") d.setUTCDate(d.getUTCDate() + 7 * n);
if (m[2] === "M") d.setUTCMonth(d.getUTCMonth() + n);
if (m[2] === "Y") d.setUTCFullYear(d.getUTCFullYear() + n);
return d.toISOString();
}
/**
* Formatters for the `sources` command.
*/
import { renderHeader } from "./shared.js";
/** Render the per-class source inventory as plain text. */
export function toText(view) {
const lines = [renderHeader(`Sources retained about ${view.email}`), ""];
for (const item of view.items) {
lines.push(` ${item.label} (${item.id})`);
lines.push(` count: ${item.count}`);
lines.push(` oldest: ${item.oldest ?? "—"}`);
lines.push(` newest: ${item.newest ?? "—"}`);
lines.push(` window: ${item.window ?? "while employed"}`);
if (item.falloff) lines.push(` falloff: ${item.falloff}`);
lines.push("");
}
return lines.join("\n");
}
/** Serialize the source inventory and metadata as formatted JSON. */
export function toJson(view, meta) {
return JSON.stringify({ ...view, meta }, null, 2);
}
/** Render the source inventory as a markdown table. */
export function toMarkdown(view) {
const lines = [
`# Sources retained about ${view.email}`,
"",
"| Class | Count | Oldest | Newest | Window | Falloff |",
"| --- | --- | --- | --- | --- | --- |",
];
for (const item of view.items) {
lines.push(
`| ${item.label} | ${item.count} | ${item.oldest ?? "—"} | ${
item.newest ?? "—"
} | ${item.window ?? "while employed"} | ${item.falloff ?? "—"} |`,
);
}
lines.push("");
return lines.join("\n");
}
import { createHmac, timingSafeEqual } from "node:crypto";
/** Thrown when no usable caller identity can be derived from the env. */
export class IdentityUnresolvedError extends Error {
/** Wrap the reason in a prefixed message and attach code "LANDMARK_IDENTITY_UNRESOLVED". */
constructor(reason) {
super(`Authentication required: ${reason}`);
this.code = "LANDMARK_IDENTITY_UNRESOLVED";
}
}
// HS256 HMAC-SHA256 digest is fixed at 32 bytes; reject any signature whose
// decoded length deviates before invoking timingSafeEqual.
const HS256_DIGEST_BYTES = 32;
/** Decode a JWT segment as JSON; throws IdentityUnresolvedError on failure. */
function parseJwtSegment(seg, label) {
let raw;
try {
raw = Buffer.from(seg, "base64url").toString("utf8");
} catch {
throw new IdentityUnresolvedError(
`LANDMARK_AUTH_TOKEN ${label} is not valid base64url`,
);
}
try {
return JSON.parse(raw);
} catch {
throw new IdentityUnresolvedError(
`LANDMARK_AUTH_TOKEN ${label} is not valid JSON`,
);
}
}
/**
* Resolve the caller's identity from a Supabase Auth JWT in
* `LANDMARK_AUTH_TOKEN`.
*
* @param {NodeJS.ProcessEnv} [env]
* @returns {{email: string, jwt: string}}
* @throws {IdentityUnresolvedError}
*/
export function resolveIdentity(env = process.env) {
const jwt = env.LANDMARK_AUTH_TOKEN;
if (!jwt)
throw new IdentityUnresolvedError(
"LANDMARK_AUTH_TOKEN is not set. The Landmark CLI requires an authenticated caller.",
);
const parts = jwt.split(".");
if (parts.length !== 3)
throw new IdentityUnresolvedError("LANDMARK_AUTH_TOKEN is not a JWT");
// Header must announce HS256 + JWT — never trust an `alg: none` token,
// even if PostgREST would also reject it; we want the failure surface
// here so downstream code never sees a forged email.
const header = parseJwtSegment(parts[0], "header");
if (header.alg !== "HS256" || header.typ !== "JWT")
throw new IdentityUnresolvedError(
"LANDMARK_AUTH_TOKEN header rejected (HS256 + JWT required)",
);
const claims = parseJwtSegment(parts[1], "payload");
if (typeof claims.email !== "string" || !claims.email)
throw new IdentityUnresolvedError(
"LANDMARK_AUTH_TOKEN missing string email claim",
);
if (typeof claims.exp !== "number" || claims.exp * 1000 <= Date.now())
throw new IdentityUnresolvedError("LANDMARK_AUTH_TOKEN is expired");
// Defense in depth: when MAP_SUPABASE_JWT_SECRET is available (test
// harness, local dev) verify the HMAC ourselves before trusting any
// claim. Production engineer-side callers will not have the secret;
// for them the contract is that `email` is opaque until the first
// PostgREST round-trip succeeds — never log or branch on it before.
if (env.MAP_SUPABASE_JWT_SECRET) {
const actual = Buffer.from(parts[2], "base64url");
if (actual.length !== HS256_DIGEST_BYTES)
throw new IdentityUnresolvedError(
"LANDMARK_AUTH_TOKEN signature does not verify",
);
const expected = createHmac("sha256", env.MAP_SUPABASE_JWT_SECRET)
.update(`${parts[0]}.${parts[1]}`)
.digest();
if (!timingSafeEqual(expected, actual))
throw new IdentityUnresolvedError(
"LANDMARK_AUTH_TOKEN signature does not verify",
);
}
return { email: claims.email, jwt };
}
+30
-0

@@ -28,5 +28,10 @@ #!/usr/bin/env node

import { runVoiceCommand } from "../src/commands/voice.js";
import { runSourcesCommand } from "../src/commands/sources.js";
import { resolveDataDir } from "../src/lib/cli.js";
import { buildContext } from "../src/lib/context.js";
import { SupabaseUnavailableError } from "../src/lib/supabase.js";
import {
resolveIdentity,
IdentityUnresolvedError,
} from "../src/lib/identity.js";
import { formatResult } from "../src/formatters/index.js";

@@ -56,2 +61,3 @@

voice: { handler: runVoiceCommand, needsSupabase: true },
sources: { handler: runSourcesCommand, needsSupabase: true },
};

@@ -171,2 +177,12 @@

},
{
name: "sources",
description: "List activity row classes retained about an engineer",
options: {
email: {
type: "string",
description: "Email to inventory sources for",
},
},
},
],

@@ -188,2 +204,3 @@ globalOptions: {

"fit-landmark health --manager alice@example.com",
"fit-landmark sources --email self@example.com",
],

@@ -222,2 +239,8 @@ documentation: [

},
{
title: "List Engineering Data Sources",
url: "https://www.forwardimpact.team/docs/products/engineering-data-sources/index.md",
description:
"List the activity rows retained about an engineer and their fall-off dates.",
},
],

@@ -247,2 +270,4 @@ };

const dataDir = resolveDataDir(values);
let identity = null;
if (entry.needsSupabase) identity = resolveIdentity();
const ctx = await buildContext({

@@ -252,2 +277,3 @@ dataDir,

needsSupabase: entry.needsSupabase,
identity,
});

@@ -276,2 +302,6 @@

} catch (error) {
if (error instanceof IdentityUnresolvedError) {
cli.error(error.message);
process.exit(4);
}
if (error instanceof SupabaseUnavailableError) {

@@ -278,0 +308,0 @@ cli.error(error.message);

+1
-1
{
"name": "@forwardimpact/landmark",
"version": "0.1.14",
"version": "0.1.15",
"description": "Surface engineering progress from activity evidence — outcomes visible without singling out individuals.",

@@ -5,0 +5,0 @@ "keywords": [

@@ -20,2 +20,3 @@ /**

import * as voiceFormatter from "./voice.js";
import * as sourcesFormatter from "./sources.js";
const formatters = {

@@ -33,2 +34,3 @@ org: orgFormatter,

voice: voiceFormatter,
sources: sourcesFormatter,
};

@@ -35,0 +37,0 @@

@@ -17,9 +17,17 @@ /**

* @param {boolean} params.needsSupabase - Whether to open a Supabase client.
* @returns {Promise<{mapData: object, supabase: object|null, format: string, options: object}>}
* @param {{email: string, jwt: string}|null} [params.identity] - Resolved caller identity (required when needsSupabase).
* @returns {Promise<{mapData: object, supabase: object|null, format: string, options: object, identity: object|null}>}
*/
export async function buildContext({ dataDir, options, needsSupabase }) {
export async function buildContext({
dataDir,
options,
needsSupabase,
identity = null,
}) {
const mapData = await loadMapData(dataDir);
const supabase = needsSupabase ? createLandmarkClient() : null;
const supabase = needsSupabase
? createLandmarkClient({ jwt: identity?.jwt })
: null;
const format = resolveFormat(options);
return { mapData, supabase, format, options };
return { mapData, supabase, format, options, identity };
}

@@ -32,2 +32,4 @@ /**

"No evidence data found. Evidenced depth reflects Guide-interpreted artifacts only.",
NO_SOURCES_FOR_PERSON: (email) =>
`No sources retained for ${email} that you can see.`,
};
/**
* Supabase client factory for Landmark.
*
* Mirrors the env-var contract used by Summit's createSummitClient.
* Landmark reads via the per-caller JWT minted under Supabase Auth — never
* the service-role key (criterion 3a). The service-role client lives only
* under products/map/src/ for ingestion.
*/

@@ -19,23 +21,29 @@

/**
* Create a Supabase client configured for Map's activity schema.
* Create a Supabase client bound to the caller's JWT.
*
* @param {object} [opts]
* @param {object} opts
* @param {string} opts.jwt - Supabase Auth JWT; transports as Authorization: Bearer.
* @param {string} [opts.url] - Override for MAP_SUPABASE_URL.
* @param {string} [opts.serviceRoleKey] - Override for MAP_SUPABASE_SERVICE_ROLE_KEY.
* @param {string} [opts.anonKey] - Override for MAP_SUPABASE_ANON_KEY.
* @param {string} [opts.schema] - Database schema (default: "activity").
* @returns {import("@supabase/supabase-js").SupabaseClient}
*/
export function createLandmarkClient(opts = {}) {
const url = opts.url ?? process.env.MAP_SUPABASE_URL;
const key = opts.serviceRoleKey ?? process.env.MAP_SUPABASE_SERVICE_ROLE_KEY;
const schema = opts.schema ?? "activity";
if (!url || !key) {
export function createLandmarkClient({
jwt,
url = process.env.MAP_SUPABASE_URL,
anonKey = process.env.MAP_SUPABASE_ANON_KEY,
schema = "activity",
} = {}) {
if (!url || !anonKey)
throw new SupabaseUnavailableError(
"MAP_SUPABASE_URL / MAP_SUPABASE_SERVICE_ROLE_KEY not set. " +
"Run `fit-map activity start` and export the URL + key it prints.",
"MAP_SUPABASE_URL / MAP_SUPABASE_ANON_KEY not set. Run `fit-map activity start` and export the URL + anon key it prints.",
);
}
return createClient(url, key, { db: { schema } });
if (!jwt)
throw new SupabaseUnavailableError(
"missing JWT — resolveIdentity must run first",
);
return createClient(url, anonKey, {
db: { schema },
global: { headers: { Authorization: `Bearer ${jwt}` } },
});
}

@@ -42,0 +50,0 @@