@ima-jin/config 0.8.0 → 0.8.15

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/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { isAllowedOrigin, corsHeaders, corsOptions, withCors, validateOrigin, ALLOWED_FRAME_ORIGINS } from "./cors";
2
+ export { standaloneDashboardMiddleware } from "./dashboard-redirect-middleware";
3
+ export type { StandaloneDashboardMiddlewareOptions } from "./dashboard-redirect-middleware";
2
4
  export { tier1Headers, tier2Headers, tier3Headers } from './security-headers';
5
+ export { APP_DISPLAY_NAME } from './brand';
3
6
 
4
7
  export {
5
8
  SERVICES,
@@ -31,6 +34,14 @@ export { apiFetch, apiUrl } from "./base-path";
31
34
 
32
35
  export { rateLimit, getClientIP } from "./rate-limit";
33
36
 
37
+ export { getNodeSelf } from "./node-self";
38
+ export type { NodeSelfInfo } from "./node-self";
39
+
40
+ export { registryServiceUrl, hasRegistryServiceUrl } from "./registry-service-url";
41
+
42
+ export { getForestScopeConfig } from "./forest-config";
43
+ export type { ForestScopeConfig } from "./forest-config";
44
+
34
45
  export {
35
46
  HANDLE_PATTERN,
36
47
  HANDLE_INPUT_PATTERN,
@@ -0,0 +1,45 @@
1
+ import { registryServiceUrl } from './registry-service-url';
2
+
3
+ /**
4
+ * Minimal public node metadata returned by GET /registry/api/node/self.
5
+ * Mirrors the subset of `relay.relay_config` the registry service exposes
6
+ * publicly — never the private config (profile artifact JWS, etc).
7
+ */
8
+ export interface NodeSelfInfo {
9
+ /** This node's own DID (did:imajin:...). */
10
+ did: string;
11
+ /** DID of the human/entity operating this node, if configured. */
12
+ nodeOperatorDid: string | null;
13
+ /** Node fee, in basis points, applied to .fair settlements on this node. */
14
+ nodeFeeBps: number | null;
15
+ /** Buyer credit, in basis points, applied to .fair settlements on this node. */
16
+ buyerCreditBps: number | null;
17
+ }
18
+
19
+ /**
20
+ * Fetch this node's public identity + .fair fee config from the registry
21
+ * service (`GET /registry/api/node/self`, #2000).
22
+ *
23
+ * Replaces raw `relay.relay_config` SQL reads that were duplicated across
24
+ * coffee, learn, events, and market (audit item 8 of #1983). Returns null on
25
+ * any failure — network error, non-2xx response (e.g. 503 when the node
26
+ * identity hasn't been bootstrapped yet) — so callers can fall back to
27
+ * defaults exactly as they did when the raw SQL row was missing. A non-2xx
28
+ * response is logged with the URL actually hit (no secrets) so a
29
+ * misconfigured REGISTRY_SERVICE_URL prefix is visible instead of failing
30
+ * silently (#2046).
31
+ */
32
+ export async function getNodeSelf(): Promise<NodeSelfInfo | null> {
33
+ const url = `${registryServiceUrl()}/api/node/self`;
34
+ try {
35
+ const res = await fetch(url);
36
+ if (!res.ok) {
37
+ console.warn(`[config] getNodeSelf: registry returned ${res.status} for ${url} — check REGISTRY_SERVICE_URL`);
38
+ return null;
39
+ }
40
+ return (await res.json()) as NodeSelfInfo;
41
+ } catch (err) {
42
+ console.warn(`[config] getNodeSelf: fetch failed for ${url} — ${err instanceof Error ? err.message : String(err)}`);
43
+ return null;
44
+ }
45
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Single resolver for the registry service's base URL (#2061 — kernel was
3
+ * the last `REGISTRY_URL` holdout after #2048 established the
4
+ * `*_SERVICE_URL` convention).
5
+ *
6
+ * The returned value always includes the `/registry` path prefix, like
7
+ * every other `*_SERVICE_URL` (#2046) — callers append only the endpoint
8
+ * path (e.g. `/api/node/self`, `/relay/operations`).
9
+ *
10
+ * Precedence:
11
+ * 1. `REGISTRY_SERVICE_URL` — the current convention.
12
+ * 2. `REGISTRY_URL` — deprecated kernel-only legacy name. Used if set,
13
+ * but logs a one-time deprecation warning per process.
14
+ * 3. `http://localhost:${PORT || 3000}/registry` — the mode-aware
15
+ * fallback (PORT is 3000 in dev, 7000 in prod) that
16
+ * `relay-well-known.ts` already used before this consolidation.
17
+ */
18
+
19
+ let warnedAboutLegacyRegistryUrl = false;
20
+
21
+ export function registryServiceUrl(): string {
22
+ if (process.env.REGISTRY_SERVICE_URL) return process.env.REGISTRY_SERVICE_URL;
23
+
24
+ if (process.env.REGISTRY_URL) {
25
+ if (!warnedAboutLegacyRegistryUrl) {
26
+ console.warn('REGISTRY_URL is deprecated — set REGISTRY_SERVICE_URL');
27
+ warnedAboutLegacyRegistryUrl = true;
28
+ }
29
+ return process.env.REGISTRY_URL;
30
+ }
31
+
32
+ return `http://localhost:${process.env.PORT || 3000}/registry`;
33
+ }
34
+
35
+ /**
36
+ * True when the registry URL is explicitly configured via either the
37
+ * current (`REGISTRY_SERVICE_URL`) or deprecated (`REGISTRY_URL`) env var.
38
+ *
39
+ * `registryServiceUrl()` always returns a value (it falls back to a
40
+ * localhost guess), so callers that previously guarded on
41
+ * `!process.env.REGISTRY_URL` to skip/no-op when the registry isn't
42
+ * configured should check this instead.
43
+ */
44
+ export function hasRegistryServiceUrl(): boolean {
45
+ return Boolean(process.env.REGISTRY_SERVICE_URL || process.env.REGISTRY_URL);
46
+ }
47
+
48
+ /**
49
+ * Test-only: resets the one-time deprecation warning latch so each test
50
+ * can assert the warning fires exactly once for its own scenario.
51
+ */
52
+ export function __resetRegistryServiceUrlWarningForTests(): void {
53
+ warnedAboutLegacyRegistryUrl = false;
54
+ }
package/src/services.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  /**
2
- * Canonical service manifest — single source of truth for all Imajin services.
2
+ * Canonical service manifest — infra/build-time topology for all Imajin
3
+ * services (dev/prod ports, Postgres schema, deployment tier, Caddy/pm2
4
+ * routing, docs sync, `/registry/api/specs`). These consumers run without a
5
+ * live identity/DB context (build scripts, edge config, port-based
6
+ * routing), so this stays a static, synchronously-importable array.
3
7
  *
4
- * Consumers: registry specs, Caddy config, pm2 ecosystem, docs sync, shared nav.
8
+ * NOT the nav authority (#2425): which apps an authenticated identity sees
9
+ * in the kernel hub (auth submenu, launcher, home tiles) is now derived
10
+ * from `registry.apps` (#1990) via `apps/kernel/src/lib/kernel/app-nav.ts`,
11
+ * not from this file — a disabled/pruned app disappears from nav by
12
+ * flipping registry state, independent of whether it still has a row here
13
+ * for port/Caddy purposes. Consumers: registry specs, Caddy config, pm2
14
+ * ecosystem, docs sync.
5
15
  */
6
16
 
7
17
  export type ServiceTier = "core" | "imajin";
@@ -46,6 +56,11 @@ export const SERVICES: readonly ServiceDefinition[] = [
46
56
  { name: "media", label: "Media", icon: "📁", description: "Files, images, and .fair attribution", devPort: 3000, prodPort: 7000, schema: "media", tier: "core", visibility: "authenticated", category: "kernel" },
47
57
  { name: "registry", label: "Registry", icon: "📡", description: "Service registry and DFOS relay", devPort: 3000, prodPort: 7000, schema: "registry", tier: "core", visibility: "public", category: "kernel" },
48
58
  { name: "notify", label: "Notify", icon: "🔔", description: "Notifications and preferences", devPort: 3000, prodPort: 7000, schema: "notify", tier: "core", visibility: "internal", category: "kernel" },
59
+ { name: "usage", label: "Usage", icon: "📊", description: "Inference cost ledger — incurred, billed, drift, signed rollup", devPort: 3000, prodPort: 7000, schema: "usage", tier: "core", visibility: "authenticated", category: "kernel" },
60
+ { name: "calendar", label: "Calendar", icon: "📅", description: "Temporal entries — availability, meetings, events, and reminders", devPort: 3000, prodPort: 7000, schema: null, tier: "core", visibility: "authenticated", category: "kernel" },
61
+ { name: "jin", label: "Jin", icon: "🧭", description: "Operator dashboard — signed approvals, proposals, and usage feed", devPort: 3000, prodPort: 7000, schema: "operator", tier: "core", visibility: "internal", category: "kernel" },
62
+ { name: "google", label: "Google Workspace", icon: "📨", description: "Gmail/Calendar/Drive/Meet connector — OAuth2 auth-code, sealed refresh token", devPort: 3000, prodPort: 7000, schema: null, tier: "core", visibility: "authenticated", category: "kernel" },
63
+ { name: "vault", label: "Vault", icon: "🔐", description: "Sealed secrets, delegation grants, and vault-minted service credentials", devPort: 3000, prodPort: 7000, schema: null, tier: "core", visibility: "internal", category: "kernel" },
49
64
 
50
65
  // Core apps — separate processes
51
66
  { name: "events", label: "Events", icon: "🎫", description: "Event creation, ticketing, and management", devPort: 3006, prodPort: 7006, schema: "events", tier: "core", visibility: "public", category: "core" },
@@ -64,6 +79,7 @@ export const SERVICES: readonly ServiceDefinition[] = [
64
79
 
65
80
  // Daemon processes — not web apps, no HTTP port exposed
66
81
  { name: 'broker-agent', label: 'Broker Agent', icon: '🤖', description: 'Telegram broker agent — conversational surface for social coordination (#1101)', devPort: 0, prodPort: 0, schema: null, tier: 'core', visibility: 'internal', category: 'infrastructure' },
82
+ { name: 'corpus', label: 'Corpus', icon: '🔎', description: 'Per-DID corpus indexing and BM25 search service (#1737)', devPort: 8013, prodPort: 8013, schema: null, tier: 'core', visibility: 'internal', category: 'infrastructure' },
67
83
 
68
84
  // Connected apps (separate repos) will use the plugin architecture (#249).
69
85
  // Not included here — they authenticate via delegated sessions, not the monorepo manifest.
package/src/session.ts CHANGED
@@ -27,15 +27,42 @@ function isLocalhost(): boolean {
27
27
  return prefix.includes("localhost");
28
28
  }
29
29
 
30
+ /**
31
+ * Cookie domain scope (#1069 Phase 1).
32
+ *
33
+ * `SESSION_COOKIE_SCOPE` controls whether the deployed session cookie is a
34
+ * shared parent-domain cookie or a host-only cookie:
35
+ * - unset / "domain" (default): current behavior — `domain: ".imajin.ai"`,
36
+ * readable by every `*.imajin.ai` subdomain. Nothing changes unless this
37
+ * is explicitly set.
38
+ * - "host": omit the `domain` attribute entirely, so the cookie is scoped
39
+ * to the exact host that set it and is NOT sent to other subdomains.
40
+ * Apps that relied on reading the shared cookie must adopt scoped app
41
+ * tokens first (see `requireSessionOrAppToken` in `@ima-jin/auth` and
42
+ * `docs/security/cookie-isolation.md`) before this is flipped for real.
43
+ *
44
+ * This flag is a primitive for the staged rollout described in
45
+ * `docs/security/cookie-isolation.md` — flipping it is a separate, later
46
+ * phase, not part of shipping the primitive itself.
47
+ */
48
+ function isHostScoped(): boolean {
49
+ if (typeof process === "undefined") return false;
50
+ return process.env.SESSION_COOKIE_SCOPE === "host";
51
+ }
52
+
30
53
  /** Cookie options for cross-subdomain sessions.
31
54
  * When called with no argument, auto-detects from IMAJIN_ENV (same logic as getSessionCookieName).
32
55
  * Accepts optional "dev" | "prod" override for explicit control.
33
56
  *
34
57
  * Localhost-aware: when SERVICE_PREFIX contains "localhost", uses
35
58
  * settings compatible with HTTP on localhost (no domain, not secure, lax sameSite).
59
+ *
60
+ * See {@link isHostScoped} for the `SESSION_COOKIE_SCOPE=host` opt-in that
61
+ * narrows the deployed cookie to host-only instead of `.imajin.ai`.
36
62
  */
37
63
  export function getSessionCookieOptions(env?: "dev" | "prod") {
38
64
  const local = isLocalhost();
65
+ const omitDomain = local || isHostScoped();
39
66
  return {
40
67
  name: getSessionCookieName(env),
41
68
  options: {
@@ -43,7 +70,7 @@ export function getSessionCookieOptions(env?: "dev" | "prod") {
43
70
  secure: !local,
44
71
  sameSite: local ? ("lax" as const) : ("none" as const),
45
72
  path: "/",
46
- ...(local ? {} : { domain: ".imajin.ai" }),
73
+ ...(omitDomain ? {} : { domain: ".imajin.ai" }),
47
74
  maxAge: 60 * 60 * 24, // 24 hours
48
75
  },
49
76
  };