@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.mjs → index.cjs} +356 -19
- package/dist/index.d.cts +485 -0
- package/dist/index.d.ts +485 -0
- package/dist/index.js +195 -166
- package/next-headers.cjs +57 -0
- package/package.json +17 -10
- package/src/base-path.ts +10 -10
- package/src/brand.ts +12 -0
- package/src/cors.ts +8 -1
- package/src/dashboard-redirect-middleware.ts +56 -0
- package/src/forest-config.ts +51 -0
- package/src/handles.ts +3 -3
- package/src/index.ts +16 -1
- package/src/node-self.ts +45 -0
- package/src/rate-limit.ts +54 -0
- package/src/registry-service-url.ts +54 -0
- package/src/security-headers.ts +45 -0
- package/src/services.ts +65 -6
- package/src/session.ts +28 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
225
|
-
const domain =
|
|
226
|
-
const prefix = options
|
|
227
|
-
if (svc
|
|
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") ||
|
|
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
|
|
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
|
|
250
|
-
const d = domain ?? (typeof process
|
|
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
|
-
...
|
|
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 = /[
|
|
326
|
-
var HANDLE_INPUT_PATTERN =
|
|
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
|
-
|
|
470
|
-
|
|
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
|
+
};
|
package/next-headers.cjs
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
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": ">=
|
|
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
|
-
* -
|
|
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 '@
|
|
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
|
|
18
|
+
* // For <a> tags or globalThis.location:
|
|
19
19
|
* <a href={apiUrl('/dashboard')}>Dashboard</a>
|
|
20
|
-
*
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
*
|
|
65
|
-
*
|
|
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 (α)";
|