@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/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ALLOWED_FRAME_ORIGINS: () => ALLOWED_FRAME_ORIGINS,
24
+ APP_DISPLAY_NAME: () => APP_DISPLAY_NAME,
24
25
  HANDLE_ALLOWED_CHARS: () => HANDLE_ALLOWED_CHARS,
25
26
  HANDLE_ERROR: () => HANDLE_ERROR,
26
27
  HANDLE_INPUT_PATTERN: () => HANDLE_INPUT_PATTERN,
@@ -94,12 +95,15 @@ __export(index_exports, {
94
95
  eventsCreatePath: () => eventsCreatePath,
95
96
  eventsDashboardPath: () => eventsDashboardPath,
96
97
  getClientIP: () => getClientIP,
98
+ getForestScopeConfig: () => getForestScopeConfig,
99
+ getNodeSelf: () => getNodeSelf,
97
100
  getPort: () => getPort,
98
101
  getPublicUrl: () => getPublicUrl,
99
102
  getService: () => getService,
100
103
  getServiceUrl: () => getServiceUrl,
101
104
  getSessionCookieName: () => getSessionCookieName,
102
105
  getSessionCookieOptions: () => getSessionCookieOptions,
106
+ hasRegistryServiceUrl: () => hasRegistryServiceUrl,
103
107
  healthPath: () => healthPath,
104
108
  isAllowedOrigin: () => isAllowedOrigin,
105
109
  isReservedHandle: () => isReservedHandle,
@@ -140,8 +144,10 @@ __export(index_exports, {
140
144
  rateLimit: () => rateLimit,
141
145
  registryDocsPath: () => registryDocsPath,
142
146
  registryPath: () => registryPath,
147
+ registryServiceUrl: () => registryServiceUrl,
143
148
  servicesByTier: () => servicesByTier,
144
149
  servicesByVisibility: () => servicesByVisibility,
150
+ standaloneDashboardMiddleware: () => standaloneDashboardMiddleware,
145
151
  subscribePath: () => subscribePath,
146
152
  tier1Headers: () => tier1Headers,
147
153
  tier2Headers: () => tier2Headers,
@@ -153,7 +159,7 @@ __export(index_exports, {
153
159
  module.exports = __toCommonJS(index_exports);
154
160
 
155
161
  // src/cors.ts
156
- var import_server = require("next/server");
162
+ var import_server = require("next/server.js");
157
163
  var ORIGIN_PATTERN = /^https:\/\/(dev-)?[a-z-]+\.imajin\.ai$/;
158
164
  var LOCAL_PATTERN = /^http:\/\/localhost:\d+$/;
159
165
  var ALLOWED_FRAME_ORIGINS = "https://*.imajin.ai https://imajin.ai";
@@ -170,7 +176,7 @@ function corsHeaders(request) {
170
176
  "Access-Control-Allow-Origin": allowed ? origin : "",
171
177
  "Access-Control-Allow-Credentials": "true",
172
178
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
173
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Caller-DID",
179
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Caller-DID, X-App-DID, X-App-Authorization",
174
180
  "Vary": "Origin"
175
181
  };
176
182
  }
@@ -190,26 +196,8 @@ function validateOrigin(request) {
190
196
  return isAllowedOrigin(origin);
191
197
  }
192
198
 
193
- // src/security-headers.ts
194
- function buildFrameAncestors() {
195
- const local = process.env.NODE_ENV !== "production" ? " http://localhost:*" : "";
196
- return `frame-ancestors 'self' ${ALLOWED_FRAME_ORIGINS}${local}`;
197
- }
198
- function tier1Headers() {
199
- return [
200
- { key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
201
- { key: "X-Frame-Options", value: "DENY" }
202
- ];
203
- }
204
- function tier2Headers() {
205
- return [{ key: "Content-Security-Policy", value: buildFrameAncestors() }];
206
- }
207
- function tier3Headers() {
208
- return [
209
- { key: "X-Content-Type-Options", value: "nosniff" },
210
- { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }
211
- ];
212
- }
199
+ // src/dashboard-redirect-middleware.ts
200
+ var import_server2 = require("next/server.js");
213
201
 
214
202
  // src/services.ts
215
203
  var SERVICES = [
@@ -223,6 +211,11 @@ var SERVICES = [
223
211
  { name: "media", label: "Media", icon: "\u{1F4C1}", description: "Files, images, and .fair attribution", devPort: 3e3, prodPort: 7e3, schema: "media", tier: "core", visibility: "authenticated", category: "kernel" },
224
212
  { name: "registry", label: "Registry", icon: "\u{1F4E1}", description: "Service registry and DFOS relay", devPort: 3e3, prodPort: 7e3, schema: "registry", tier: "core", visibility: "public", category: "kernel" },
225
213
  { name: "notify", label: "Notify", icon: "\u{1F514}", description: "Notifications and preferences", devPort: 3e3, prodPort: 7e3, schema: "notify", tier: "core", visibility: "internal", category: "kernel" },
214
+ { name: "usage", label: "Usage", icon: "\u{1F4CA}", description: "Inference cost ledger \u2014 incurred, billed, drift, signed rollup", devPort: 3e3, prodPort: 7e3, schema: "usage", tier: "core", visibility: "authenticated", category: "kernel" },
215
+ { name: "calendar", label: "Calendar", icon: "\u{1F4C5}", description: "Temporal entries \u2014 availability, meetings, events, and reminders", devPort: 3e3, prodPort: 7e3, schema: null, tier: "core", visibility: "authenticated", category: "kernel" },
216
+ { name: "jin", label: "Jin", icon: "\u{1F9ED}", description: "Operator dashboard \u2014 signed approvals, proposals, and usage feed", devPort: 3e3, prodPort: 7e3, schema: "operator", tier: "core", visibility: "internal", category: "kernel" },
217
+ { name: "google", label: "Google Workspace", icon: "\u{1F4E8}", description: "Gmail/Calendar/Drive/Meet connector \u2014 OAuth2 auth-code, sealed refresh token", devPort: 3e3, prodPort: 7e3, schema: null, tier: "core", visibility: "authenticated", category: "kernel" },
218
+ { name: "vault", label: "Vault", icon: "\u{1F510}", description: "Sealed secrets, delegation grants, and vault-minted service credentials", devPort: 3e3, prodPort: 7e3, schema: null, tier: "core", visibility: "internal", category: "kernel" },
226
219
  // Core apps — separate processes
227
220
  { name: "events", label: "Events", icon: "\u{1F3AB}", description: "Event creation, ticketing, and management", devPort: 3006, prodPort: 7006, schema: "events", tier: "core", visibility: "public", category: "core" },
228
221
  // Imajin apps
@@ -236,7 +229,8 @@ var SERVICES = [
236
229
  { name: "github", label: "GitHub", icon: "\u{1F419}", description: "Source code", devPort: 3e3, prodPort: 7e3, schema: "public", tier: "core", visibility: "public", category: "meta", externalUrl: "https://github.com/ima-jin/imajin-ai" },
237
230
  { name: "docs", label: "Docs", icon: "\u{1F4C4}", description: "API documentation", devPort: 3e3, prodPort: 7e3, schema: "public", tier: "core", visibility: "public", category: "meta", wwwPath: "/developer-guide" },
238
231
  // Daemon processes — not web apps, no HTTP port exposed
239
- { name: "broker-agent", label: "Broker Agent", icon: "\u{1F916}", description: "Telegram broker agent \u2014 conversational surface for social coordination (#1101)", devPort: 0, prodPort: 0, schema: null, tier: "core", visibility: "internal", category: "infrastructure" }
232
+ { name: "broker-agent", label: "Broker Agent", icon: "\u{1F916}", description: "Telegram broker agent \u2014 conversational surface for social coordination (#1101)", devPort: 0, prodPort: 0, schema: null, tier: "core", visibility: "internal", category: "infrastructure" },
233
+ { name: "corpus", label: "Corpus", icon: "\u{1F50E}", description: "Per-DID corpus indexing and BM25 search service (#1737)", devPort: 8013, prodPort: 8013, schema: null, tier: "core", visibility: "internal", category: "infrastructure" }
240
234
  // Connected apps (separate repos) will use the plugin architecture (#249).
241
235
  // Not included here — they authenticate via delegated sessions, not the monorepo manifest.
242
236
  ];
@@ -322,6 +316,46 @@ function buildServiceUrlMap(env, mode = "dev") {
322
316
  return map;
323
317
  }
324
318
 
319
+ // src/dashboard-redirect-middleware.ts
320
+ function standaloneDashboardMiddleware(request, { service, cors = true }) {
321
+ if (request.nextUrl.pathname === "/dashboard") {
322
+ const hubUrl = new URL(`${buildPublicUrlAbsolute("kernel")}/auth/${service}`);
323
+ hubUrl.search = request.nextUrl.search;
324
+ return import_server2.NextResponse.redirect(hubUrl, 308);
325
+ }
326
+ if (!cors) {
327
+ return import_server2.NextResponse.next();
328
+ }
329
+ if (request.method === "OPTIONS") {
330
+ return new import_server2.NextResponse(null, { status: 204, headers: corsHeaders(request) });
331
+ }
332
+ return withCors(import_server2.NextResponse.next(), request);
333
+ }
334
+
335
+ // src/security-headers.ts
336
+ function buildFrameAncestors() {
337
+ const local = process.env.NODE_ENV !== "production" ? " http://localhost:*" : "";
338
+ return `frame-ancestors 'self' ${ALLOWED_FRAME_ORIGINS}${local}`;
339
+ }
340
+ function tier1Headers() {
341
+ return [
342
+ { key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
343
+ { key: "X-Frame-Options", value: "DENY" }
344
+ ];
345
+ }
346
+ function tier2Headers() {
347
+ return [{ key: "Content-Security-Policy", value: buildFrameAncestors() }];
348
+ }
349
+ function tier3Headers() {
350
+ return [
351
+ { key: "X-Content-Type-Options", value: "nosniff" },
352
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }
353
+ ];
354
+ }
355
+
356
+ // src/brand.ts
357
+ var APP_DISPLAY_NAME = "imajin (\u03B1)";
358
+
325
359
  // src/session.ts
326
360
  function getSessionCookieName(env) {
327
361
  const resolved = env ?? (typeof process !== "undefined" && (process.env.IMAJIN_ENV === "dev" || process.env.NODE_ENV === "development") ? "dev" : "prod");
@@ -333,8 +367,13 @@ function isLocalhost() {
333
367
  const prefix = process.env.NEXT_PUBLIC_SERVICE_PREFIX ?? "";
334
368
  return prefix.includes("localhost");
335
369
  }
370
+ function isHostScoped() {
371
+ if (typeof process === "undefined") return false;
372
+ return process.env.SESSION_COOKIE_SCOPE === "host";
373
+ }
336
374
  function getSessionCookieOptions(env) {
337
375
  const local = isLocalhost();
376
+ const omitDomain = local || isHostScoped();
338
377
  return {
339
378
  name: getSessionCookieName(env),
340
379
  options: {
@@ -342,7 +381,7 @@ function getSessionCookieOptions(env) {
342
381
  secure: !local,
343
382
  sameSite: local ? "lax" : "none",
344
383
  path: "/",
345
- ...local ? {} : { domain: ".imajin.ai" },
384
+ ...omitDomain ? {} : { domain: ".imajin.ai" },
346
385
  maxAge: 60 * 60 * 24
347
386
  // 24 hours
348
387
  }
@@ -401,11 +440,67 @@ function getClientIP(request) {
401
440
  return "127.0.0.1";
402
441
  }
403
442
 
443
+ // src/registry-service-url.ts
444
+ var warnedAboutLegacyRegistryUrl = false;
445
+ function registryServiceUrl() {
446
+ if (process.env.REGISTRY_SERVICE_URL) return process.env.REGISTRY_SERVICE_URL;
447
+ if (process.env.REGISTRY_URL) {
448
+ if (!warnedAboutLegacyRegistryUrl) {
449
+ console.warn("REGISTRY_URL is deprecated \u2014 set REGISTRY_SERVICE_URL");
450
+ warnedAboutLegacyRegistryUrl = true;
451
+ }
452
+ return process.env.REGISTRY_URL;
453
+ }
454
+ return `http://localhost:${process.env.PORT || 3e3}/registry`;
455
+ }
456
+ function hasRegistryServiceUrl() {
457
+ return Boolean(process.env.REGISTRY_SERVICE_URL || process.env.REGISTRY_URL);
458
+ }
459
+
460
+ // src/node-self.ts
461
+ async function getNodeSelf() {
462
+ const url = `${registryServiceUrl()}/api/node/self`;
463
+ try {
464
+ const res = await fetch(url);
465
+ if (!res.ok) {
466
+ console.warn(`[config] getNodeSelf: registry returned ${res.status} for ${url} \u2014 check REGISTRY_SERVICE_URL`);
467
+ return null;
468
+ }
469
+ return await res.json();
470
+ } catch (err) {
471
+ console.warn(`[config] getNodeSelf: fetch failed for ${url} \u2014 ${err instanceof Error ? err.message : String(err)}`);
472
+ return null;
473
+ }
474
+ }
475
+
476
+ // src/forest-config.ts
477
+ function profileBaseUrl() {
478
+ if (process.env.PROFILE_SERVICE_URL) return process.env.PROFILE_SERVICE_URL;
479
+ const mode = process.env.NODE_ENV === "production" ? "prod" : "dev";
480
+ const base = getServiceUrl("profile", mode) ?? "http://localhost:3000";
481
+ return `${base}/profile`;
482
+ }
483
+ async function getForestScopeConfig(groupDid) {
484
+ const url = `${profileBaseUrl()}/api/forest/${encodeURIComponent(groupDid)}/config/public`;
485
+ try {
486
+ const res = await fetch(url);
487
+ if (!res.ok) {
488
+ console.warn(`[config] getForestScopeConfig: profile service returned ${res.status} for ${url}`);
489
+ return null;
490
+ }
491
+ const data = await res.json();
492
+ return { scopeFeeBps: typeof data?.scopeFeeBps === "number" ? data.scopeFeeBps : null };
493
+ } catch (err) {
494
+ console.warn(`[config] getForestScopeConfig: fetch failed for ${url} \u2014 ${err instanceof Error ? err.message : String(err)}`);
495
+ return null;
496
+ }
497
+ }
498
+
404
499
  // src/handles.ts
405
500
  var HANDLE_PATTERN = /^[a-z0-9._-]{3,30}$/;
406
501
  var HANDLE_EDGE = /^[.-]|[.-]$/;
407
502
  var HANDLE_CONSECUTIVE = /[.-]{2}/;
408
- var HANDLE_INPUT_PATTERN = "[a-z0-9._\\-]{3,30}";
503
+ var HANDLE_INPUT_PATTERN = String.raw`[a-z0-9._\-]{3,30}`;
409
504
  var HANDLE_ALLOWED_CHARS = /[^a-z0-9._-]/g;
410
505
  var RESERVED_HANDLES = /* @__PURE__ */ new Set([
411
506
  "admin",
@@ -551,6 +646,7 @@ var notifySettingsPath = () => `/notify/settings`;
551
646
  // Annotate the CommonJS export names for ESM import in node:
552
647
  0 && (module.exports = {
553
648
  ALLOWED_FRAME_ORIGINS,
649
+ APP_DISPLAY_NAME,
554
650
  HANDLE_ALLOWED_CHARS,
555
651
  HANDLE_ERROR,
556
652
  HANDLE_INPUT_PATTERN,
@@ -624,12 +720,15 @@ var notifySettingsPath = () => `/notify/settings`;
624
720
  eventsCreatePath,
625
721
  eventsDashboardPath,
626
722
  getClientIP,
723
+ getForestScopeConfig,
724
+ getNodeSelf,
627
725
  getPort,
628
726
  getPublicUrl,
629
727
  getService,
630
728
  getServiceUrl,
631
729
  getSessionCookieName,
632
730
  getSessionCookieOptions,
731
+ hasRegistryServiceUrl,
633
732
  healthPath,
634
733
  isAllowedOrigin,
635
734
  isReservedHandle,
@@ -670,8 +769,10 @@ var notifySettingsPath = () => `/notify/settings`;
670
769
  rateLimit,
671
770
  registryDocsPath,
672
771
  registryPath,
772
+ registryServiceUrl,
673
773
  servicesByTier,
674
774
  servicesByVisibility,
775
+ standaloneDashboardMiddleware,
675
776
  subscribePath,
676
777
  tier1Headers,
677
778
  tier2Headers,
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { NextRequest, NextResponse } from 'next/server';
1
+ import { NextRequest, NextResponse } from 'next/server.js';
2
2
 
3
3
  /**
4
4
  * CSP host-source list corresponding to ORIGIN_PATTERN (prod).
@@ -12,6 +12,39 @@ declare function corsOptions(request: NextRequest): NextResponse;
12
12
  declare function withCors(response: NextResponse, request: NextRequest): NextResponse;
13
13
  declare function validateOrigin(request: NextRequest): boolean;
14
14
 
15
+ interface StandaloneDashboardMiddlewareOptions {
16
+ /** Service name used to build the hub tab URL (`/auth/<service>`). */
17
+ service: string;
18
+ /**
19
+ * Whether to also answer CORS preflight and stamp CORS headers on every
20
+ * other request — matches the app's pre-existing `/api/:path*` CORS
21
+ * middleware (#1525). Defaults to `true`; pass `false` for an app (e.g.
22
+ * dykil) that never had one.
23
+ */
24
+ cors?: boolean;
25
+ }
26
+ /**
27
+ * Shared standalone-dashboard-to-hub redirect body (#2332, carved out of
28
+ * #2275), used by every userspace service app's `middleware.ts` so the
29
+ * redirect + CORS pass-through logic isn't duplicated six times over.
30
+ *
31
+ * Each app still declares its own literal `export const config = { matcher:
32
+ * [...] }` — Next.js statically extracts `matcher` from the middleware
33
+ * file's AST at build time (see
34
+ * `next/dist/build/analysis/extract-const-value.js`: it only understands
35
+ * literal object/array/string/etc. expressions, not a function call or a
36
+ * destructured import) — so the matcher itself can't be factored out here
37
+ * without Next silently falling back to its default, unrestricted matcher.
38
+ * Only the function *body* is shared.
39
+ *
40
+ * The redirect is unconditional: a 308 to `/auth/<service>`, preserving the
41
+ * query string. The embedded rendering path `<ServiceEmbed>` loads
42
+ * (`?embed=hub&did=...`, see `apps/kernel/app/auth/lib/service-registry.ts`'s
43
+ * `buildEmbedSrc`) never reaches this function at all — it's excluded
44
+ * declaratively by each app's `matcher.missing` clause.
45
+ */
46
+ declare function standaloneDashboardMiddleware(request: NextRequest, { service, cors }: StandaloneDashboardMiddlewareOptions): NextResponse;
47
+
15
48
  type HeaderEntry = {
16
49
  key: string;
17
50
  value: string;
@@ -35,9 +68,32 @@ declare function tier2Headers(): HeaderEntry[];
35
68
  declare function tier3Headers(): HeaderEntry[];
36
69
 
37
70
  /**
38
- * Canonical service manifest — single source of truth for all Imajin services.
71
+ * Human-facing product display name.
72
+ *
73
+ * This is the ONLY place the "(α)" (alpha) stage marker lives — flip it here
74
+ * (e.g. to "(β)" or drop it for GA) and every consumer (BuildInfo, nav bars,
75
+ * page titles/metadata, auth screens, email templates, etc.) picks it up.
39
76
  *
40
- * Consumers: registry specs, Caddy config, pm2 ecosystem, docs sync, shared nav.
77
+ * Do NOT use this for DIDs, URLs, package names, event names, localStorage
78
+ * keys, env vars, or any other code identifier — those stay as literal
79
+ * 'imajin' strings. This constant is display-text only.
80
+ */
81
+ declare const APP_DISPLAY_NAME = "imajin (\u03B1)";
82
+
83
+ /**
84
+ * Canonical service manifest — infra/build-time topology for all Imajin
85
+ * services (dev/prod ports, Postgres schema, deployment tier, Caddy/pm2
86
+ * routing, docs sync, `/registry/api/specs`). These consumers run without a
87
+ * live identity/DB context (build scripts, edge config, port-based
88
+ * routing), so this stays a static, synchronously-importable array.
89
+ *
90
+ * NOT the nav authority (#2425): which apps an authenticated identity sees
91
+ * in the kernel hub (auth submenu, launcher, home tiles) is now derived
92
+ * from `registry.apps` (#1990) via `apps/kernel/src/lib/kernel/app-nav.ts`,
93
+ * not from this file — a disabled/pruned app disappears from nav by
94
+ * flipping registry state, independent of whether it still has a row here
95
+ * for port/Caddy purposes. Consumers: registry specs, Caddy config, pm2
96
+ * ecosystem, docs sync.
41
97
  */
42
98
  type ServiceTier = "core" | "imajin";
43
99
  type ServiceVisibility = "public" | "authenticated" | "creator" | "internal";
@@ -138,6 +194,9 @@ declare const SESSION_COOKIE_NAME: string;
138
194
  *
139
195
  * Localhost-aware: when SERVICE_PREFIX contains "localhost", uses
140
196
  * settings compatible with HTTP on localhost (no domain, not secure, lax sameSite).
197
+ *
198
+ * See {@link isHostScoped} for the `SESSION_COOKIE_SCOPE=host` opt-in that
199
+ * narrows the deployed cookie to host-only instead of `.imajin.ai`.
141
200
  */
142
201
  declare function getSessionCookieOptions(env?: "dev" | "prod"): {
143
202
  name: string;
@@ -189,17 +248,17 @@ declare function apiUrl(path: string): string;
189
248
  *
190
249
  * Absolute URLs (http://, https://) are passed through unchanged.
191
250
  *
192
- * TODO(WO2/correlation): For server-side service-to-service calls, callers should
193
- * forward the X-Correlation-Id header so the full request chain shares one ID.
194
- * apiFetch is browser-side too, so we can't read headers here automatically —
195
- * instead, pass it explicitly via init.headers when calling from a withLogger handler:
251
+ * For server-side service-to-service calls, forward the X-Correlation-Id header
252
+ * so the full request chain shares one ID. apiFetch is browser-side too, so it
253
+ * can't read headers here automatically — pass it explicitly via init.headers
254
+ * when calling from a withLogger handler:
196
255
  *
197
256
  * apiFetch('/api/something', {
198
257
  * headers: { 'x-correlation-id': correlationId },
199
258
  * });
200
259
  *
201
- * When WO3 instruments services, each withLogger handler receives correlationId in
202
- * ctx and should forward it on any outbound apiFetch/fetch calls.
260
+ * see: automatic correlationId forwarding on outbound apiFetch/fetch calls from
261
+ * withLogger handlers is not yet wired up.
203
262
  */
204
263
  declare function apiFetch(path: string, init?: RequestInit): Promise<Response>;
205
264
 
@@ -213,6 +272,89 @@ declare function rateLimit(key: string, limit: number, windowMs: number): {
213
272
  };
214
273
  declare function getClientIP(request: Request): string;
215
274
 
275
+ /**
276
+ * Minimal public node metadata returned by GET /registry/api/node/self.
277
+ * Mirrors the subset of `relay.relay_config` the registry service exposes
278
+ * publicly — never the private config (profile artifact JWS, etc).
279
+ */
280
+ interface NodeSelfInfo {
281
+ /** This node's own DID (did:imajin:...). */
282
+ did: string;
283
+ /** DID of the human/entity operating this node, if configured. */
284
+ nodeOperatorDid: string | null;
285
+ /** Node fee, in basis points, applied to .fair settlements on this node. */
286
+ nodeFeeBps: number | null;
287
+ /** Buyer credit, in basis points, applied to .fair settlements on this node. */
288
+ buyerCreditBps: number | null;
289
+ }
290
+ /**
291
+ * Fetch this node's public identity + .fair fee config from the registry
292
+ * service (`GET /registry/api/node/self`, #2000).
293
+ *
294
+ * Replaces raw `relay.relay_config` SQL reads that were duplicated across
295
+ * coffee, learn, events, and market (audit item 8 of #1983). Returns null on
296
+ * any failure — network error, non-2xx response (e.g. 503 when the node
297
+ * identity hasn't been bootstrapped yet) — so callers can fall back to
298
+ * defaults exactly as they did when the raw SQL row was missing. A non-2xx
299
+ * response is logged with the URL actually hit (no secrets) so a
300
+ * misconfigured REGISTRY_SERVICE_URL prefix is visible instead of failing
301
+ * silently (#2046).
302
+ */
303
+ declare function getNodeSelf(): Promise<NodeSelfInfo | null>;
304
+
305
+ /**
306
+ * Single resolver for the registry service's base URL (#2061 — kernel was
307
+ * the last `REGISTRY_URL` holdout after #2048 established the
308
+ * `*_SERVICE_URL` convention).
309
+ *
310
+ * The returned value always includes the `/registry` path prefix, like
311
+ * every other `*_SERVICE_URL` (#2046) — callers append only the endpoint
312
+ * path (e.g. `/api/node/self`, `/relay/operations`).
313
+ *
314
+ * Precedence:
315
+ * 1. `REGISTRY_SERVICE_URL` — the current convention.
316
+ * 2. `REGISTRY_URL` — deprecated kernel-only legacy name. Used if set,
317
+ * but logs a one-time deprecation warning per process.
318
+ * 3. `http://localhost:${PORT || 3000}/registry` — the mode-aware
319
+ * fallback (PORT is 3000 in dev, 7000 in prod) that
320
+ * `relay-well-known.ts` already used before this consolidation.
321
+ */
322
+ declare function registryServiceUrl(): string;
323
+ /**
324
+ * True when the registry URL is explicitly configured via either the
325
+ * current (`REGISTRY_SERVICE_URL`) or deprecated (`REGISTRY_URL`) env var.
326
+ *
327
+ * `registryServiceUrl()` always returns a value (it falls back to a
328
+ * localhost guess), so callers that previously guarded on
329
+ * `!process.env.REGISTRY_URL` to skip/no-op when the registry isn't
330
+ * configured should check this instead.
331
+ */
332
+ declare function hasRegistryServiceUrl(): boolean;
333
+
334
+ /**
335
+ * Public forest-group (scope) config exposed by
336
+ * `GET /api/forest/{groupDid}/config/public` on the profile service. Only
337
+ * the field relevant to `.fair` fee-split calculation is modeled here — the
338
+ * rest of the config (enabledServices, landingService, etc.) is a
339
+ * community-settings concern, not a shared SDK concern.
340
+ */
341
+ interface ForestScopeConfig {
342
+ /** Group's configured revenue-share fee, in basis points (0-500). Null if unset. */
343
+ scopeFeeBps: number | null;
344
+ }
345
+ /**
346
+ * Fetch a forest group's public scope-fee config from the profile service
347
+ * (`GET /api/forest/{groupDid}/config/public`, documented in
348
+ * `apps/kernel/api-spec/profile.yaml`).
349
+ *
350
+ * Replaces raw `profile.forest_config` SQL reads that were duplicated
351
+ * across coffee, learn, and market (audit item 9 of #1983, #2001). Returns
352
+ * null on any failure — network error, non-2xx response — so callers can
353
+ * fall back to the same `scopeFeeBps: null` default they used when the raw
354
+ * SQL row was missing.
355
+ */
356
+ declare function getForestScopeConfig(groupDid: string): Promise<ForestScopeConfig | null>;
357
+
216
358
  /**
217
359
  * Handle validation and normalization.
218
360
  *
@@ -223,7 +365,7 @@ declare function getClientIP(request: Request): string;
223
365
  /** Regex: the full allowed character set, length enforced */
224
366
  declare const HANDLE_PATTERN: RegExp;
225
367
  /** HTML input pattern attribute (no anchors, no flags) */
226
- declare const HANDLE_INPUT_PATTERN = "[a-z0-9._\\-]{3,30}";
368
+ declare const HANDLE_INPUT_PATTERN: string;
227
369
  /** Characters allowed in handles — use in onChange filters */
228
370
  declare const HANDLE_ALLOWED_CHARS: RegExp;
229
371
  /** Reserved handles that cannot be claimed */
@@ -340,4 +482,4 @@ declare const projectPath: () => string;
340
482
  declare const notifyPath: () => string;
341
483
  declare const notifySettingsPath: () => string;
342
484
 
343
- export { ALLOWED_FRAME_ORIGINS, HANDLE_ALLOWED_CHARS, HANDLE_ERROR, HANDLE_INPUT_PATTERN, HANDLE_PATTERN, RESERVED_HANDLES, SERVICES, SERVICE_NAMES, SESSION_COOKIE_NAME, type ServiceCategory, type ServiceDefinition, type ServiceTier, type ServiceVisibility, apiFetch, apiUrl, articleAuthorPath, articlePath, articlesPath, authAgentsPath, authAppsPath, authAttestationsPath, authAuthorizePath, authDeveloperAppPath, authDeveloperAppsPath, authGroupSettingsPath, authGroupsPath, authLoginPath, authMembersPath, authNewGroupPath, authNewStubPath, authNotificationsPath, authOnboardPath, authRegisterPath, authSecurityPath, authSettingsPath, authStubsPath, bugsAdminPath, bugsPath, buildPath, buildPublicUrl, buildPublicUrlAbsolute, buildServiceUrlMap, bumpPath, chatConversationPath, chatConversationsPath, chatPath, coffeeDashboardPath, coffeeEditPath, coffeeHandlePath, coffeeSuccessPath, connectionsInvitePath, connectionsPath, connectionsPodPath, corsHeaders, corsOptions, developerGuidePath, docsPath, dykilCreatePath, dykilDashboardPath, dykilEmbedPath, dykilHandlePath, dykilResultsPath, dykilSurveyPath, eventAdminPath, eventCheckoutSuccessPath, eventEditPath, eventEditUrl, eventMyTicketsPath, eventMyTicketsUrl, eventPath, eventRegisterPath, eventRegisterUrl, eventUrl, eventsAdminListPath, eventsCheckoutSuccessPath, eventsCreatePath, eventsDashboardPath, getClientIP, getPort, getPublicUrl, getService, getServiceUrl, getSessionCookieName, getSessionCookieOptions, healthPath, isAllowedOrigin, isReservedHandle, isValidHandle, learnCourseDashboardPath, learnCourseLessonPath, learnCoursePath, learnCoursePresentPath, learnCourseStudentsPath, learnDashboardPath, learnHandlePath, linksDashboardPath, linksEditPath, linksHandlePath, marketCheckoutSuccessPath, marketDashboardPath, marketListingEditPath, marketListingPath, marketNewListingPath, marketSellerPath, marketSettingsPath, mediaPath, normalizeHandleInput, notifyPath, notifySettingsPath, payHistoryPath, payPath, payPayoutsPath, payTopupPath, payTopupSuccessPath, privacyPath, profileEditPath, profileLoginPath, profilePath, profileRegisterPath, profileUrl, projectPath, rateLimit, registryDocsPath, registryPath, servicesByTier, servicesByVisibility, subscribePath, tier1Headers, tier2Headers, tier3Headers, validateOrigin, whitepaperPath, withCors };
485
+ export { ALLOWED_FRAME_ORIGINS, APP_DISPLAY_NAME, type ForestScopeConfig, HANDLE_ALLOWED_CHARS, HANDLE_ERROR, HANDLE_INPUT_PATTERN, HANDLE_PATTERN, type NodeSelfInfo, RESERVED_HANDLES, SERVICES, SERVICE_NAMES, SESSION_COOKIE_NAME, type ServiceCategory, type ServiceDefinition, type ServiceTier, type ServiceVisibility, type StandaloneDashboardMiddlewareOptions, apiFetch, apiUrl, articleAuthorPath, articlePath, articlesPath, authAgentsPath, authAppsPath, authAttestationsPath, authAuthorizePath, authDeveloperAppPath, authDeveloperAppsPath, authGroupSettingsPath, authGroupsPath, authLoginPath, authMembersPath, authNewGroupPath, authNewStubPath, authNotificationsPath, authOnboardPath, authRegisterPath, authSecurityPath, authSettingsPath, authStubsPath, bugsAdminPath, bugsPath, buildPath, buildPublicUrl, buildPublicUrlAbsolute, buildServiceUrlMap, bumpPath, chatConversationPath, chatConversationsPath, chatPath, coffeeDashboardPath, coffeeEditPath, coffeeHandlePath, coffeeSuccessPath, connectionsInvitePath, connectionsPath, connectionsPodPath, corsHeaders, corsOptions, developerGuidePath, docsPath, dykilCreatePath, dykilDashboardPath, dykilEmbedPath, dykilHandlePath, dykilResultsPath, dykilSurveyPath, eventAdminPath, eventCheckoutSuccessPath, eventEditPath, eventEditUrl, eventMyTicketsPath, eventMyTicketsUrl, eventPath, eventRegisterPath, eventRegisterUrl, eventUrl, eventsAdminListPath, eventsCheckoutSuccessPath, eventsCreatePath, eventsDashboardPath, getClientIP, getForestScopeConfig, getNodeSelf, getPort, getPublicUrl, getService, getServiceUrl, getSessionCookieName, getSessionCookieOptions, hasRegistryServiceUrl, healthPath, isAllowedOrigin, isReservedHandle, isValidHandle, learnCourseDashboardPath, learnCourseLessonPath, learnCoursePath, learnCoursePresentPath, learnCourseStudentsPath, learnDashboardPath, learnHandlePath, linksDashboardPath, linksEditPath, linksHandlePath, marketCheckoutSuccessPath, marketDashboardPath, marketListingEditPath, marketListingPath, marketNewListingPath, marketSellerPath, marketSettingsPath, mediaPath, normalizeHandleInput, notifyPath, notifySettingsPath, payHistoryPath, payPath, payPayoutsPath, payTopupPath, payTopupSuccessPath, privacyPath, profileEditPath, profileLoginPath, profilePath, profileRegisterPath, profileUrl, projectPath, rateLimit, registryDocsPath, registryPath, registryServiceUrl, servicesByTier, servicesByVisibility, standaloneDashboardMiddleware, subscribePath, tier1Headers, tier2Headers, tier3Headers, validateOrigin, whitepaperPath, withCors };