@ima-jin/config 0.7.0 → 0.8.13

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.js CHANGED
@@ -1,153 +1,8 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __export = (target, all) => {
6
- for (var name in all)
7
- __defProp(target, name, { get: all[name], enumerable: true });
8
- };
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
- }
15
- return to;
16
- };
17
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
-
19
- // src/index.ts
20
- var index_exports = {};
21
- __export(index_exports, {
22
- HANDLE_ALLOWED_CHARS: () => HANDLE_ALLOWED_CHARS,
23
- HANDLE_ERROR: () => HANDLE_ERROR,
24
- HANDLE_INPUT_PATTERN: () => HANDLE_INPUT_PATTERN,
25
- HANDLE_PATTERN: () => HANDLE_PATTERN,
26
- RESERVED_HANDLES: () => RESERVED_HANDLES,
27
- SERVICES: () => SERVICES,
28
- SERVICE_NAMES: () => SERVICE_NAMES,
29
- SESSION_COOKIE_NAME: () => SESSION_COOKIE_NAME,
30
- apiFetch: () => apiFetch,
31
- apiUrl: () => apiUrl,
32
- articleAuthorPath: () => articleAuthorPath,
33
- articlePath: () => articlePath,
34
- articlesPath: () => articlesPath,
35
- authAgentsPath: () => authAgentsPath,
36
- authAppsPath: () => authAppsPath,
37
- authAttestationsPath: () => authAttestationsPath,
38
- authAuthorizePath: () => authAuthorizePath,
39
- authDeveloperAppPath: () => authDeveloperAppPath,
40
- authDeveloperAppsPath: () => authDeveloperAppsPath,
41
- authGroupSettingsPath: () => authGroupSettingsPath,
42
- authGroupsPath: () => authGroupsPath,
43
- authLoginPath: () => authLoginPath,
44
- authMembersPath: () => authMembersPath,
45
- authNewGroupPath: () => authNewGroupPath,
46
- authNewStubPath: () => authNewStubPath,
47
- authNotificationsPath: () => authNotificationsPath,
48
- authOnboardPath: () => authOnboardPath,
49
- authRegisterPath: () => authRegisterPath,
50
- authSecurityPath: () => authSecurityPath,
51
- authSettingsPath: () => authSettingsPath,
52
- authStubsPath: () => authStubsPath,
53
- bugsAdminPath: () => bugsAdminPath,
54
- bugsPath: () => bugsPath,
55
- buildPath: () => buildPath,
56
- buildPublicUrl: () => buildPublicUrl,
57
- buildServiceUrlMap: () => buildServiceUrlMap,
58
- bumpPath: () => bumpPath,
59
- chatConversationPath: () => chatConversationPath,
60
- chatConversationsPath: () => chatConversationsPath,
61
- chatPath: () => chatPath,
62
- coffeeDashboardPath: () => coffeeDashboardPath,
63
- coffeeEditPath: () => coffeeEditPath,
64
- coffeeHandlePath: () => coffeeHandlePath,
65
- coffeeSuccessPath: () => coffeeSuccessPath,
66
- connectionsInvitePath: () => connectionsInvitePath,
67
- connectionsPath: () => connectionsPath,
68
- connectionsPodPath: () => connectionsPodPath,
69
- corsHeaders: () => corsHeaders,
70
- corsOptions: () => corsOptions,
71
- developerGuidePath: () => developerGuidePath,
72
- docsPath: () => docsPath,
73
- dykilCreatePath: () => dykilCreatePath,
74
- dykilDashboardPath: () => dykilDashboardPath,
75
- dykilEmbedPath: () => dykilEmbedPath,
76
- dykilHandlePath: () => dykilHandlePath,
77
- dykilResultsPath: () => dykilResultsPath,
78
- dykilSurveyPath: () => dykilSurveyPath,
79
- eventAdminPath: () => eventAdminPath,
80
- eventCheckoutSuccessPath: () => eventCheckoutSuccessPath,
81
- eventEditPath: () => eventEditPath,
82
- eventEditUrl: () => eventEditUrl,
83
- eventMyTicketsPath: () => eventMyTicketsPath,
84
- eventMyTicketsUrl: () => eventMyTicketsUrl,
85
- eventPath: () => eventPath,
86
- eventRegisterPath: () => eventRegisterPath,
87
- eventRegisterUrl: () => eventRegisterUrl,
88
- eventUrl: () => eventUrl,
89
- eventsAdminListPath: () => eventsAdminListPath,
90
- eventsCheckoutSuccessPath: () => eventsCheckoutSuccessPath,
91
- eventsCreatePath: () => eventsCreatePath,
92
- eventsDashboardPath: () => eventsDashboardPath,
93
- getPort: () => getPort,
94
- getPublicUrl: () => getPublicUrl,
95
- getService: () => getService,
96
- getServiceUrl: () => getServiceUrl,
97
- getSessionCookieName: () => getSessionCookieName,
98
- getSessionCookieOptions: () => getSessionCookieOptions,
99
- healthPath: () => healthPath,
100
- isAllowedOrigin: () => isAllowedOrigin,
101
- isReservedHandle: () => isReservedHandle,
102
- isValidHandle: () => isValidHandle,
103
- learnCourseDashboardPath: () => learnCourseDashboardPath,
104
- learnCourseLessonPath: () => learnCourseLessonPath,
105
- learnCoursePath: () => learnCoursePath,
106
- learnCoursePresentPath: () => learnCoursePresentPath,
107
- learnCourseStudentsPath: () => learnCourseStudentsPath,
108
- learnDashboardPath: () => learnDashboardPath,
109
- learnHandlePath: () => learnHandlePath,
110
- linksDashboardPath: () => linksDashboardPath,
111
- linksEditPath: () => linksEditPath,
112
- linksHandlePath: () => linksHandlePath,
113
- marketCheckoutSuccessPath: () => marketCheckoutSuccessPath,
114
- marketDashboardPath: () => marketDashboardPath,
115
- marketListingEditPath: () => marketListingEditPath,
116
- marketListingPath: () => marketListingPath,
117
- marketNewListingPath: () => marketNewListingPath,
118
- marketSellerPath: () => marketSellerPath,
119
- marketSettingsPath: () => marketSettingsPath,
120
- mediaPath: () => mediaPath,
121
- normalizeHandleInput: () => normalizeHandleInput,
122
- notifyPath: () => notifyPath,
123
- notifySettingsPath: () => notifySettingsPath,
124
- payHistoryPath: () => payHistoryPath,
125
- payPath: () => payPath,
126
- payPayoutsPath: () => payPayoutsPath,
127
- payTopupPath: () => payTopupPath,
128
- payTopupSuccessPath: () => payTopupSuccessPath,
129
- privacyPath: () => privacyPath,
130
- profileEditPath: () => profileEditPath,
131
- profileLoginPath: () => profileLoginPath,
132
- profilePath: () => profilePath,
133
- profileRegisterPath: () => profileRegisterPath,
134
- profileUrl: () => profileUrl,
135
- projectPath: () => projectPath,
136
- registryDocsPath: () => registryDocsPath,
137
- registryPath: () => registryPath,
138
- servicesByTier: () => servicesByTier,
139
- servicesByVisibility: () => servicesByVisibility,
140
- subscribePath: () => subscribePath,
141
- validateOrigin: () => validateOrigin,
142
- whitepaperPath: () => whitepaperPath,
143
- withCors: () => withCors
144
- });
145
- module.exports = __toCommonJS(index_exports);
146
-
147
1
  // src/cors.ts
148
- var import_server = require("next/server");
2
+ import { NextResponse } from "next/server";
149
3
  var ORIGIN_PATTERN = /^https:\/\/(dev-)?[a-z-]+\.imajin\.ai$/;
150
4
  var LOCAL_PATTERN = /^http:\/\/localhost:\d+$/;
5
+ var ALLOWED_FRAME_ORIGINS = "https://*.imajin.ai https://imajin.ai";
151
6
  function isAllowedOrigin(origin) {
152
7
  if (!origin) return false;
153
8
  if (ORIGIN_PATTERN.test(origin)) return true;
@@ -161,12 +16,12 @@ function corsHeaders(request) {
161
16
  "Access-Control-Allow-Origin": allowed ? origin : "",
162
17
  "Access-Control-Allow-Credentials": "true",
163
18
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
164
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Caller-DID",
19
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Caller-DID, X-App-DID, X-App-Authorization",
165
20
  "Vary": "Origin"
166
21
  };
167
22
  }
168
23
  function corsOptions(request) {
169
- return new import_server.NextResponse(null, { status: 204, headers: corsHeaders(request) });
24
+ return new NextResponse(null, { status: 204, headers: corsHeaders(request) });
170
25
  }
171
26
  function withCors(response, request) {
172
27
  const headers = corsHeaders(request);
@@ -181,6 +36,9 @@ function validateOrigin(request) {
181
36
  return isAllowedOrigin(origin);
182
37
  }
183
38
 
39
+ // src/dashboard-redirect-middleware.ts
40
+ import { NextResponse as NextResponse2 } from "next/server";
41
+
184
42
  // src/services.ts
185
43
  var SERVICES = [
186
44
  // Kernel services — individually visible, all run on the kernel process (port 3000/7000)
@@ -193,6 +51,11 @@ var SERVICES = [
193
51
  { 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" },
194
52
  { 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" },
195
53
  { name: "notify", label: "Notify", icon: "\u{1F514}", description: "Notifications and preferences", devPort: 3e3, prodPort: 7e3, schema: "notify", tier: "core", visibility: "internal", category: "kernel" },
54
+ { 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" },
55
+ { 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" },
56
+ { 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" },
57
+ { 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" },
58
+ { 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" },
196
59
  // Core apps — separate processes
197
60
  { 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" },
198
61
  // Imajin apps
@@ -204,7 +67,10 @@ var SERVICES = [
204
67
  // Meta — project info and external resources surfaced in the launcher
205
68
  { name: "project", label: "Project", icon: "\u{1F4D6}", description: "Thesis, whitepaper, essays, RFCs", devPort: 3e3, prodPort: 7e3, schema: "public", tier: "core", visibility: "public", category: "meta", wwwPath: "/project" },
206
69
  { 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" },
207
- { 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" }
70
+ { 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" },
71
+ // Daemon processes — not web apps, no HTTP port exposed
72
+ { 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" },
73
+ { 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" }
208
74
  // Connected apps (separate repos) will use the plugin architecture (#249).
209
75
  // Not included here — they authenticate via delegated sessions, not the monorepo manifest.
210
76
  ];
@@ -213,7 +79,8 @@ function getService(name) {
213
79
  }
214
80
  function getPort(name, env = "dev") {
215
81
  const svc = getService(name);
216
- return svc ? env === "prod" ? svc.prodPort : svc.devPort : void 0;
82
+ if (!svc) return void 0;
83
+ return env === "prod" ? svc.prodPort : svc.devPort;
217
84
  }
218
85
  function getServiceUrl(name, env = "dev") {
219
86
  const port = getPort(name, env);
@@ -221,18 +88,18 @@ function getServiceUrl(name, env = "dev") {
221
88
  }
222
89
  function getPublicUrl(name, options) {
223
90
  const svc = SERVICES.find((s) => s.name === name);
224
- if (svc == null ? void 0 : svc.externalUrl) return svc.externalUrl;
225
- const domain = (options == null ? void 0 : options.domain) || "imajin.ai";
226
- const prefix = options == null ? void 0 : options.prefix;
227
- if (svc == null ? void 0 : svc.wwwPath) {
91
+ if (svc?.externalUrl) return svc.externalUrl;
92
+ const domain = options?.domain || "imajin.ai";
93
+ const prefix = options?.prefix;
94
+ if (svc?.wwwPath) {
228
95
  const wwwUrl = getPublicUrl("www", options);
229
96
  return `${wwwUrl}${svc.wwwPath}`;
230
97
  }
231
- if (domain.includes("localhost") || (prefix == null ? void 0 : prefix.includes("localhost"))) {
98
+ if (domain.includes("localhost") || prefix?.includes("localhost")) {
232
99
  const port = getPort(name, "dev");
233
100
  return port ? `http://localhost:${port}` : `http://localhost:3000`;
234
101
  }
235
- if (prefix && prefix.includes(".")) {
102
+ if (prefix?.includes(".")) {
236
103
  const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
237
104
  if (name === "www" || name === "kernel") return `https://${base}`;
238
105
  return `https://${base}/${name}`;
@@ -246,8 +113,8 @@ function buildPublicUrl(name, servicePrefix, domain) {
246
113
  const explicit = process.env[envKey];
247
114
  if (explicit) return explicit;
248
115
  }
249
- const p = servicePrefix ?? (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_SERVICE_PREFIX : void 0) ?? "https://";
250
- const d = domain ?? (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_DOMAIN : void 0) ?? "imajin.ai";
116
+ const p = servicePrefix ?? (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_SERVICE_PREFIX) ?? "https://";
117
+ const d = domain ?? (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_DOMAIN) ?? "imajin.ai";
251
118
  if (p.includes("localhost") || d.includes("localhost")) {
252
119
  const port = getPort(name, "dev");
253
120
  return port ? `http://localhost:${port}` : `http://localhost:3000`;
@@ -258,6 +125,20 @@ function buildPublicUrl(name, servicePrefix, domain) {
258
125
  const match = p.replace(/^https?:\/\//, "").replace(/-$/, "") || void 0;
259
126
  return getPublicUrl(name, { prefix: match, domain: d });
260
127
  }
128
+ function buildPublicUrlAbsolute(name) {
129
+ if (typeof process !== "undefined") {
130
+ const explicit = process.env[`NEXT_PUBLIC_${name.toUpperCase()}_URL`];
131
+ if (explicit && /^https?:\/\//.test(explicit)) return explicit;
132
+ }
133
+ const prefix = (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_SERVICE_PREFIX) ?? "https://";
134
+ const domain = (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_DOMAIN) ?? "imajin.ai";
135
+ if (prefix.includes("localhost") || domain.includes("localhost")) {
136
+ const port = getPort(name, "dev");
137
+ return port ? `http://localhost:${port}` : "http://localhost:3000";
138
+ }
139
+ const match = prefix.replace(/^https?:\/\//, "").replace(/-$/, "") || void 0;
140
+ return getPublicUrl(name, { prefix: match, domain });
141
+ }
261
142
  var SERVICE_NAMES = SERVICES.map((s) => s.name);
262
143
  function servicesByTier(tier) {
263
144
  return SERVICES.filter((s) => s.tier === tier);
@@ -275,6 +156,46 @@ function buildServiceUrlMap(env, mode = "dev") {
275
156
  return map;
276
157
  }
277
158
 
159
+ // src/dashboard-redirect-middleware.ts
160
+ function standaloneDashboardMiddleware(request, { service, cors = true }) {
161
+ if (request.nextUrl.pathname === "/dashboard") {
162
+ const hubUrl = new URL(`${buildPublicUrlAbsolute("kernel")}/auth/${service}`);
163
+ hubUrl.search = request.nextUrl.search;
164
+ return NextResponse2.redirect(hubUrl, 308);
165
+ }
166
+ if (!cors) {
167
+ return NextResponse2.next();
168
+ }
169
+ if (request.method === "OPTIONS") {
170
+ return new NextResponse2(null, { status: 204, headers: corsHeaders(request) });
171
+ }
172
+ return withCors(NextResponse2.next(), request);
173
+ }
174
+
175
+ // src/security-headers.ts
176
+ function buildFrameAncestors() {
177
+ const local = process.env.NODE_ENV !== "production" ? " http://localhost:*" : "";
178
+ return `frame-ancestors 'self' ${ALLOWED_FRAME_ORIGINS}${local}`;
179
+ }
180
+ function tier1Headers() {
181
+ return [
182
+ { key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
183
+ { key: "X-Frame-Options", value: "DENY" }
184
+ ];
185
+ }
186
+ function tier2Headers() {
187
+ return [{ key: "Content-Security-Policy", value: buildFrameAncestors() }];
188
+ }
189
+ function tier3Headers() {
190
+ return [
191
+ { key: "X-Content-Type-Options", value: "nosniff" },
192
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }
193
+ ];
194
+ }
195
+
196
+ // src/brand.ts
197
+ var APP_DISPLAY_NAME = "imajin (\u03B1)";
198
+
278
199
  // src/session.ts
279
200
  function getSessionCookieName(env) {
280
201
  const resolved = env ?? (typeof process !== "undefined" && (process.env.IMAJIN_ENV === "dev" || process.env.NODE_ENV === "development") ? "dev" : "prod");
@@ -286,8 +207,13 @@ function isLocalhost() {
286
207
  const prefix = process.env.NEXT_PUBLIC_SERVICE_PREFIX ?? "";
287
208
  return prefix.includes("localhost");
288
209
  }
210
+ function isHostScoped() {
211
+ if (typeof process === "undefined") return false;
212
+ return process.env.SESSION_COOKIE_SCOPE === "host";
213
+ }
289
214
  function getSessionCookieOptions(env) {
290
215
  const local = isLocalhost();
216
+ const omitDomain = local || isHostScoped();
291
217
  return {
292
218
  name: getSessionCookieName(env),
293
219
  options: {
@@ -295,7 +221,7 @@ function getSessionCookieOptions(env) {
295
221
  secure: !local,
296
222
  sameSite: local ? "lax" : "none",
297
223
  path: "/",
298
- ...local ? {} : { domain: ".imajin.ai" },
224
+ ...omitDomain ? {} : { domain: ".imajin.ai" },
299
225
  maxAge: 60 * 60 * 24
300
226
  // 24 hours
301
227
  }
@@ -319,11 +245,102 @@ function apiFetch(path, init) {
319
245
  return fetch(apiUrl(path), init);
320
246
  }
321
247
 
248
+ // src/rate-limit.ts
249
+ var store = /* @__PURE__ */ new Map();
250
+ var lastGlobalCleanup = Date.now();
251
+ function pruneStore(now) {
252
+ if (now - lastGlobalCleanup < 6e4) return;
253
+ store.forEach((entry, key) => {
254
+ if (now - entry.lastSeen > 12e4) {
255
+ store.delete(key);
256
+ }
257
+ });
258
+ lastGlobalCleanup = now;
259
+ }
260
+ function rateLimit(key, limit, windowMs) {
261
+ const now = Date.now();
262
+ pruneStore(now);
263
+ const windowStart = now - windowMs;
264
+ const entry = store.get(key) ?? { timestamps: [], lastSeen: now };
265
+ entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
266
+ entry.lastSeen = now;
267
+ if (entry.timestamps.length >= limit) {
268
+ const oldest = entry.timestamps[0];
269
+ const retryAfter = Math.ceil((oldest + windowMs - now) / 1e3);
270
+ store.set(key, entry);
271
+ return { limited: true, retryAfter: Math.max(1, retryAfter) };
272
+ }
273
+ entry.timestamps.push(now);
274
+ store.set(key, entry);
275
+ return { limited: false, retryAfter: 0 };
276
+ }
277
+ function getClientIP(request) {
278
+ const forwarded = request.headers.get("x-forwarded-for");
279
+ if (forwarded) return forwarded.split(",")[0].trim();
280
+ return "127.0.0.1";
281
+ }
282
+
283
+ // src/registry-service-url.ts
284
+ var warnedAboutLegacyRegistryUrl = false;
285
+ function registryServiceUrl() {
286
+ if (process.env.REGISTRY_SERVICE_URL) return process.env.REGISTRY_SERVICE_URL;
287
+ if (process.env.REGISTRY_URL) {
288
+ if (!warnedAboutLegacyRegistryUrl) {
289
+ console.warn("REGISTRY_URL is deprecated \u2014 set REGISTRY_SERVICE_URL");
290
+ warnedAboutLegacyRegistryUrl = true;
291
+ }
292
+ return process.env.REGISTRY_URL;
293
+ }
294
+ return `http://localhost:${process.env.PORT || 3e3}/registry`;
295
+ }
296
+ function hasRegistryServiceUrl() {
297
+ return Boolean(process.env.REGISTRY_SERVICE_URL || process.env.REGISTRY_URL);
298
+ }
299
+
300
+ // src/node-self.ts
301
+ async function getNodeSelf() {
302
+ const url = `${registryServiceUrl()}/api/node/self`;
303
+ try {
304
+ const res = await fetch(url);
305
+ if (!res.ok) {
306
+ console.warn(`[config] getNodeSelf: registry returned ${res.status} for ${url} \u2014 check REGISTRY_SERVICE_URL`);
307
+ return null;
308
+ }
309
+ return await res.json();
310
+ } catch (err) {
311
+ console.warn(`[config] getNodeSelf: fetch failed for ${url} \u2014 ${err instanceof Error ? err.message : String(err)}`);
312
+ return null;
313
+ }
314
+ }
315
+
316
+ // src/forest-config.ts
317
+ function profileBaseUrl() {
318
+ if (process.env.PROFILE_SERVICE_URL) return process.env.PROFILE_SERVICE_URL;
319
+ const mode = process.env.NODE_ENV === "production" ? "prod" : "dev";
320
+ const base = getServiceUrl("profile", mode) ?? "http://localhost:3000";
321
+ return `${base}/profile`;
322
+ }
323
+ async function getForestScopeConfig(groupDid) {
324
+ const url = `${profileBaseUrl()}/api/forest/${encodeURIComponent(groupDid)}/config/public`;
325
+ try {
326
+ const res = await fetch(url);
327
+ if (!res.ok) {
328
+ console.warn(`[config] getForestScopeConfig: profile service returned ${res.status} for ${url}`);
329
+ return null;
330
+ }
331
+ const data = await res.json();
332
+ return { scopeFeeBps: typeof data?.scopeFeeBps === "number" ? data.scopeFeeBps : null };
333
+ } catch (err) {
334
+ console.warn(`[config] getForestScopeConfig: fetch failed for ${url} \u2014 ${err instanceof Error ? err.message : String(err)}`);
335
+ return null;
336
+ }
337
+ }
338
+
322
339
  // src/handles.ts
323
340
  var HANDLE_PATTERN = /^[a-z0-9._-]{3,30}$/;
324
- var HANDLE_EDGE = /^[.\-]|[.\-]$/;
325
- var HANDLE_CONSECUTIVE = /[.\-]{2}/;
326
- var HANDLE_INPUT_PATTERN = "[a-z0-9._\\-]{3,30}";
341
+ var HANDLE_EDGE = /^[.-]|[.-]$/;
342
+ var HANDLE_CONSECUTIVE = /[.-]{2}/;
343
+ var HANDLE_INPUT_PATTERN = String.raw`[a-z0-9._\-]{3,30}`;
327
344
  var HANDLE_ALLOWED_CHARS = /[^a-z0-9._-]/g;
328
345
  var RESERVED_HANDLES = /* @__PURE__ */ new Set([
329
346
  "admin",
@@ -466,8 +483,9 @@ var docsPath = () => `/docs`;
466
483
  var projectPath = () => `/project`;
467
484
  var notifyPath = () => `/notify`;
468
485
  var notifySettingsPath = () => `/notify/settings`;
469
- // Annotate the CommonJS export names for ESM import in node:
470
- 0 && (module.exports = {
486
+ export {
487
+ ALLOWED_FRAME_ORIGINS,
488
+ APP_DISPLAY_NAME,
471
489
  HANDLE_ALLOWED_CHARS,
472
490
  HANDLE_ERROR,
473
491
  HANDLE_INPUT_PATTERN,
@@ -503,6 +521,7 @@ var notifySettingsPath = () => `/notify/settings`;
503
521
  bugsPath,
504
522
  buildPath,
505
523
  buildPublicUrl,
524
+ buildPublicUrlAbsolute,
506
525
  buildServiceUrlMap,
507
526
  bumpPath,
508
527
  chatConversationPath,
@@ -539,12 +558,16 @@ var notifySettingsPath = () => `/notify/settings`;
539
558
  eventsCheckoutSuccessPath,
540
559
  eventsCreatePath,
541
560
  eventsDashboardPath,
561
+ getClientIP,
562
+ getForestScopeConfig,
563
+ getNodeSelf,
542
564
  getPort,
543
565
  getPublicUrl,
544
566
  getService,
545
567
  getServiceUrl,
546
568
  getSessionCookieName,
547
569
  getSessionCookieOptions,
570
+ hasRegistryServiceUrl,
548
571
  healthPath,
549
572
  isAllowedOrigin,
550
573
  isReservedHandle,
@@ -582,12 +605,18 @@ var notifySettingsPath = () => `/notify/settings`;
582
605
  profileRegisterPath,
583
606
  profileUrl,
584
607
  projectPath,
608
+ rateLimit,
585
609
  registryDocsPath,
586
610
  registryPath,
611
+ registryServiceUrl,
587
612
  servicesByTier,
588
613
  servicesByVisibility,
614
+ standaloneDashboardMiddleware,
589
615
  subscribePath,
616
+ tier1Headers,
617
+ tier2Headers,
618
+ tier3Headers,
590
619
  validateOrigin,
591
620
  whitepaperPath,
592
621
  withCors
593
- });
622
+ };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Security-header helpers for next.config.js (CommonJS bridge).
5
+ *
6
+ * The frame-ancestors value is derived from ALLOWED_FRAME_ORIGINS in src/cors.ts
7
+ * and the localhost dev behaviour mirrors isAllowedOrigin() in the same file.
8
+ * If the allowed-origin set changes, update src/cors.ts (ORIGIN_PATTERN +
9
+ * ALLOWED_FRAME_ORIGINS) and the constant below together.
10
+ */
11
+
12
+ // Mirrors ALLOWED_FRAME_ORIGINS in src/cors.ts
13
+ const ALLOWED_FRAME_ORIGINS = 'https://*.imajin.ai https://imajin.ai';
14
+
15
+ /**
16
+ * Build the frame-ancestors directive value.
17
+ * Adds localhost:* in non-prod, mirroring isAllowedOrigin().
18
+ */
19
+ function buildFrameAncestors() {
20
+ const local = process.env.NODE_ENV !== 'production' ? ' http://localhost:*' : '';
21
+ return `frame-ancestors 'self' ${ALLOWED_FRAME_ORIGINS}${local}`;
22
+ }
23
+
24
+ /**
25
+ * Tier 1 — hard deny framing.
26
+ * Use on credential and auth-sensitive pages that must never be embedded.
27
+ * @returns {Array<{key: string, value: string}>}
28
+ */
29
+ function tier1Headers() {
30
+ return [
31
+ { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
32
+ { key: 'X-Frame-Options', value: 'DENY' },
33
+ ];
34
+ }
35
+
36
+ /**
37
+ * Tier 2 — allow framing by Imajin origins only.
38
+ * Use on routes embedded by the auth hub (service dashboards, document viewer, surveys).
39
+ * Intentionally omits X-Frame-Options — it cannot express an allow-list.
40
+ * @returns {Array<{key: string, value: string}>}
41
+ */
42
+ function tier2Headers() {
43
+ return [{ key: 'Content-Security-Policy', value: buildFrameAncestors() }];
44
+ }
45
+
46
+ /**
47
+ * Tier 3 — baseline hardening. Safe to apply on every route in every app.
48
+ * @returns {Array<{key: string, value: string}>}
49
+ */
50
+ function tier3Headers() {
51
+ return [
52
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
53
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
54
+ ];
55
+ }
56
+
57
+ module.exports = { tier1Headers, tier2Headers, tier3Headers };
package/package.json CHANGED
@@ -1,24 +1,31 @@
1
1
  {
2
2
  "name": "@ima-jin/config",
3
- "version": "0.7.0",
3
+ "version": "0.8.13",
4
+ "description": "Shared configuration for Imajin services — CORS, service routing, session config, handle validation, and route helpers",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ima-jin/imajin-ai.git",
9
+ "directory": "packages/config"
10
+ },
11
+ "type": "module",
4
12
  "main": "./dist/index.js",
5
13
  "types": "./dist/index.d.ts",
6
14
  "exports": {
7
15
  ".": {
8
- "import": "./dist/index.mjs",
9
- "require": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
11
- }
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ },
20
+ "./next-headers": "./next-headers.cjs"
12
21
  },
13
22
  "files": [
14
23
  "dist/",
15
- "src/"
24
+ "src/",
25
+ "next-headers.cjs"
16
26
  ],
17
- "dependencies": {
18
- "@ima-jin/tokens": "^0.6.1"
19
- },
20
27
  "peerDependencies": {
21
- "next": ">=14"
28
+ "next": ">=15.5.24"
22
29
  },
23
30
  "publishConfig": {
24
31
  "access": "public"
package/src/base-path.ts CHANGED
@@ -4,20 +4,20 @@
4
4
  * Next.js `basePath` auto-prefixes <Link> and router.push() but NOT:
5
5
  * - fetch() calls
6
6
  * - <a href=""> tags
7
- * - window.location assignments
7
+ * - globalThis.location assignments
8
8
  *
9
9
  * These utilities read NEXT_PUBLIC_BASE_PATH (set automatically by Next.js
10
10
  * when basePath is configured) and prepend it where needed.
11
11
  *
12
12
  * Usage:
13
- * import { apiFetch, apiUrl } from '@imajin/config';
13
+ * import { apiFetch, apiUrl } from '@ima-jin/config';
14
14
  *
15
15
  * // Instead of: fetch('/api/listings')
16
16
  * apiFetch('/api/listings')
17
17
  *
18
- * // For <a> tags or window.location:
18
+ * // For <a> tags or globalThis.location:
19
19
  * <a href={apiUrl('/dashboard')}>Dashboard</a>
20
- * window.location.href = apiUrl('/dashboard');
20
+ * globalThis.location.href = apiUrl('/dashboard');
21
21
  */
22
22
 
23
23
  /**
@@ -52,17 +52,17 @@ export function apiUrl(path: string): string {
52
52
  *
53
53
  * Absolute URLs (http://, https://) are passed through unchanged.
54
54
  *
55
- * TODO(WO2/correlation): For server-side service-to-service calls, callers should
56
- * forward the X-Correlation-Id header so the full request chain shares one ID.
57
- * apiFetch is browser-side too, so we can't read headers here automatically —
58
- * instead, pass it explicitly via init.headers when calling from a withLogger handler:
55
+ * For server-side service-to-service calls, forward the X-Correlation-Id header
56
+ * so the full request chain shares one ID. apiFetch is browser-side too, so it
57
+ * can't read headers here automatically — pass it explicitly via init.headers
58
+ * when calling from a withLogger handler:
59
59
  *
60
60
  * apiFetch('/api/something', {
61
61
  * headers: { 'x-correlation-id': correlationId },
62
62
  * });
63
63
  *
64
- * When WO3 instruments services, each withLogger handler receives correlationId in
65
- * ctx and should forward it on any outbound apiFetch/fetch calls.
64
+ * see: automatic correlationId forwarding on outbound apiFetch/fetch calls from
65
+ * withLogger handlers is not yet wired up.
66
66
  */
67
67
  export function apiFetch(path: string, init?: RequestInit): Promise<Response> {
68
68
  // Don't prefix absolute URLs
package/src/brand.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Human-facing product display name.
3
+ *
4
+ * This is the ONLY place the "(α)" (alpha) stage marker lives — flip it here
5
+ * (e.g. to "(β)" or drop it for GA) and every consumer (BuildInfo, nav bars,
6
+ * page titles/metadata, auth screens, email templates, etc.) picks it up.
7
+ *
8
+ * Do NOT use this for DIDs, URLs, package names, event names, localStorage
9
+ * keys, env vars, or any other code identifier — those stay as literal
10
+ * 'imajin' strings. This constant is display-text only.
11
+ */
12
+ export const APP_DISPLAY_NAME = "imajin (α)";