@ima-jin/config 0.7.0 → 0.8.0
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} +252 -16
- package/dist/index.d.cts +343 -0
- package/dist/index.d.ts +343 -0
- package/dist/index.js +97 -163
- package/next-headers.cjs +57 -0
- package/package.json +17 -7
- package/src/base-path.ts +4 -4
- package/src/cors.ts +7 -0
- package/src/handles.ts +2 -2
- package/src/index.ts +5 -1
- package/src/rate-limit.ts +54 -0
- package/src/security-headers.ts +45 -0
- package/src/services.ts +47 -4
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;
|
|
@@ -166,7 +21,7 @@ function corsHeaders(request) {
|
|
|
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,27 @@ function validateOrigin(request) {
|
|
|
181
36
|
return isAllowedOrigin(origin);
|
|
182
37
|
}
|
|
183
38
|
|
|
39
|
+
// src/security-headers.ts
|
|
40
|
+
function buildFrameAncestors() {
|
|
41
|
+
const local = process.env.NODE_ENV !== "production" ? " http://localhost:*" : "";
|
|
42
|
+
return `frame-ancestors 'self' ${ALLOWED_FRAME_ORIGINS}${local}`;
|
|
43
|
+
}
|
|
44
|
+
function tier1Headers() {
|
|
45
|
+
return [
|
|
46
|
+
{ key: "Content-Security-Policy", value: "frame-ancestors 'none'" },
|
|
47
|
+
{ key: "X-Frame-Options", value: "DENY" }
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function tier2Headers() {
|
|
51
|
+
return [{ key: "Content-Security-Policy", value: buildFrameAncestors() }];
|
|
52
|
+
}
|
|
53
|
+
function tier3Headers() {
|
|
54
|
+
return [
|
|
55
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
56
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
184
60
|
// src/services.ts
|
|
185
61
|
var SERVICES = [
|
|
186
62
|
// Kernel services — individually visible, all run on the kernel process (port 3000/7000)
|
|
@@ -204,7 +80,9 @@ var SERVICES = [
|
|
|
204
80
|
// Meta — project info and external resources surfaced in the launcher
|
|
205
81
|
{ 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
82
|
{ 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" }
|
|
83
|
+
{ 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" },
|
|
84
|
+
// Daemon processes — not web apps, no HTTP port exposed
|
|
85
|
+
{ 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" }
|
|
208
86
|
// Connected apps (separate repos) will use the plugin architecture (#249).
|
|
209
87
|
// Not included here — they authenticate via delegated sessions, not the monorepo manifest.
|
|
210
88
|
];
|
|
@@ -213,7 +91,8 @@ function getService(name) {
|
|
|
213
91
|
}
|
|
214
92
|
function getPort(name, env = "dev") {
|
|
215
93
|
const svc = getService(name);
|
|
216
|
-
|
|
94
|
+
if (!svc) return void 0;
|
|
95
|
+
return env === "prod" ? svc.prodPort : svc.devPort;
|
|
217
96
|
}
|
|
218
97
|
function getServiceUrl(name, env = "dev") {
|
|
219
98
|
const port = getPort(name, env);
|
|
@@ -221,18 +100,18 @@ function getServiceUrl(name, env = "dev") {
|
|
|
221
100
|
}
|
|
222
101
|
function getPublicUrl(name, options) {
|
|
223
102
|
const svc = SERVICES.find((s) => s.name === name);
|
|
224
|
-
if (svc
|
|
225
|
-
const domain =
|
|
226
|
-
const prefix = options
|
|
227
|
-
if (svc
|
|
103
|
+
if (svc?.externalUrl) return svc.externalUrl;
|
|
104
|
+
const domain = options?.domain || "imajin.ai";
|
|
105
|
+
const prefix = options?.prefix;
|
|
106
|
+
if (svc?.wwwPath) {
|
|
228
107
|
const wwwUrl = getPublicUrl("www", options);
|
|
229
108
|
return `${wwwUrl}${svc.wwwPath}`;
|
|
230
109
|
}
|
|
231
|
-
if (domain.includes("localhost") ||
|
|
110
|
+
if (domain.includes("localhost") || prefix?.includes("localhost")) {
|
|
232
111
|
const port = getPort(name, "dev");
|
|
233
112
|
return port ? `http://localhost:${port}` : `http://localhost:3000`;
|
|
234
113
|
}
|
|
235
|
-
if (prefix
|
|
114
|
+
if (prefix?.includes(".")) {
|
|
236
115
|
const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
237
116
|
if (name === "www" || name === "kernel") return `https://${base}`;
|
|
238
117
|
return `https://${base}/${name}`;
|
|
@@ -246,8 +125,8 @@ function buildPublicUrl(name, servicePrefix, domain) {
|
|
|
246
125
|
const explicit = process.env[envKey];
|
|
247
126
|
if (explicit) return explicit;
|
|
248
127
|
}
|
|
249
|
-
const p = servicePrefix ?? (typeof process
|
|
250
|
-
const d = domain ?? (typeof process
|
|
128
|
+
const p = servicePrefix ?? (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_SERVICE_PREFIX) ?? "https://";
|
|
129
|
+
const d = domain ?? (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_DOMAIN) ?? "imajin.ai";
|
|
251
130
|
if (p.includes("localhost") || d.includes("localhost")) {
|
|
252
131
|
const port = getPort(name, "dev");
|
|
253
132
|
return port ? `http://localhost:${port}` : `http://localhost:3000`;
|
|
@@ -258,6 +137,20 @@ function buildPublicUrl(name, servicePrefix, domain) {
|
|
|
258
137
|
const match = p.replace(/^https?:\/\//, "").replace(/-$/, "") || void 0;
|
|
259
138
|
return getPublicUrl(name, { prefix: match, domain: d });
|
|
260
139
|
}
|
|
140
|
+
function buildPublicUrlAbsolute(name) {
|
|
141
|
+
if (typeof process !== "undefined") {
|
|
142
|
+
const explicit = process.env[`NEXT_PUBLIC_${name.toUpperCase()}_URL`];
|
|
143
|
+
if (explicit && /^https?:\/\//.test(explicit)) return explicit;
|
|
144
|
+
}
|
|
145
|
+
const prefix = (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_SERVICE_PREFIX) ?? "https://";
|
|
146
|
+
const domain = (typeof process === "undefined" ? void 0 : process.env.NEXT_PUBLIC_DOMAIN) ?? "imajin.ai";
|
|
147
|
+
if (prefix.includes("localhost") || domain.includes("localhost")) {
|
|
148
|
+
const port = getPort(name, "dev");
|
|
149
|
+
return port ? `http://localhost:${port}` : "http://localhost:3000";
|
|
150
|
+
}
|
|
151
|
+
const match = prefix.replace(/^https?:\/\//, "").replace(/-$/, "") || void 0;
|
|
152
|
+
return getPublicUrl(name, { prefix: match, domain });
|
|
153
|
+
}
|
|
261
154
|
var SERVICE_NAMES = SERVICES.map((s) => s.name);
|
|
262
155
|
function servicesByTier(tier) {
|
|
263
156
|
return SERVICES.filter((s) => s.tier === tier);
|
|
@@ -319,10 +212,45 @@ function apiFetch(path, init) {
|
|
|
319
212
|
return fetch(apiUrl(path), init);
|
|
320
213
|
}
|
|
321
214
|
|
|
215
|
+
// src/rate-limit.ts
|
|
216
|
+
var store = /* @__PURE__ */ new Map();
|
|
217
|
+
var lastGlobalCleanup = Date.now();
|
|
218
|
+
function pruneStore(now) {
|
|
219
|
+
if (now - lastGlobalCleanup < 6e4) return;
|
|
220
|
+
store.forEach((entry, key) => {
|
|
221
|
+
if (now - entry.lastSeen > 12e4) {
|
|
222
|
+
store.delete(key);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
lastGlobalCleanup = now;
|
|
226
|
+
}
|
|
227
|
+
function rateLimit(key, limit, windowMs) {
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
pruneStore(now);
|
|
230
|
+
const windowStart = now - windowMs;
|
|
231
|
+
const entry = store.get(key) ?? { timestamps: [], lastSeen: now };
|
|
232
|
+
entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
|
|
233
|
+
entry.lastSeen = now;
|
|
234
|
+
if (entry.timestamps.length >= limit) {
|
|
235
|
+
const oldest = entry.timestamps[0];
|
|
236
|
+
const retryAfter = Math.ceil((oldest + windowMs - now) / 1e3);
|
|
237
|
+
store.set(key, entry);
|
|
238
|
+
return { limited: true, retryAfter: Math.max(1, retryAfter) };
|
|
239
|
+
}
|
|
240
|
+
entry.timestamps.push(now);
|
|
241
|
+
store.set(key, entry);
|
|
242
|
+
return { limited: false, retryAfter: 0 };
|
|
243
|
+
}
|
|
244
|
+
function getClientIP(request) {
|
|
245
|
+
const forwarded = request.headers.get("x-forwarded-for");
|
|
246
|
+
if (forwarded) return forwarded.split(",")[0].trim();
|
|
247
|
+
return "127.0.0.1";
|
|
248
|
+
}
|
|
249
|
+
|
|
322
250
|
// src/handles.ts
|
|
323
251
|
var HANDLE_PATTERN = /^[a-z0-9._-]{3,30}$/;
|
|
324
|
-
var HANDLE_EDGE = /^[
|
|
325
|
-
var HANDLE_CONSECUTIVE = /[
|
|
252
|
+
var HANDLE_EDGE = /^[.-]|[.-]$/;
|
|
253
|
+
var HANDLE_CONSECUTIVE = /[.-]{2}/;
|
|
326
254
|
var HANDLE_INPUT_PATTERN = "[a-z0-9._\\-]{3,30}";
|
|
327
255
|
var HANDLE_ALLOWED_CHARS = /[^a-z0-9._-]/g;
|
|
328
256
|
var RESERVED_HANDLES = /* @__PURE__ */ new Set([
|
|
@@ -466,8 +394,8 @@ var docsPath = () => `/docs`;
|
|
|
466
394
|
var projectPath = () => `/project`;
|
|
467
395
|
var notifyPath = () => `/notify`;
|
|
468
396
|
var notifySettingsPath = () => `/notify/settings`;
|
|
469
|
-
|
|
470
|
-
|
|
397
|
+
export {
|
|
398
|
+
ALLOWED_FRAME_ORIGINS,
|
|
471
399
|
HANDLE_ALLOWED_CHARS,
|
|
472
400
|
HANDLE_ERROR,
|
|
473
401
|
HANDLE_INPUT_PATTERN,
|
|
@@ -503,6 +431,7 @@ var notifySettingsPath = () => `/notify/settings`;
|
|
|
503
431
|
bugsPath,
|
|
504
432
|
buildPath,
|
|
505
433
|
buildPublicUrl,
|
|
434
|
+
buildPublicUrlAbsolute,
|
|
506
435
|
buildServiceUrlMap,
|
|
507
436
|
bumpPath,
|
|
508
437
|
chatConversationPath,
|
|
@@ -539,6 +468,7 @@ var notifySettingsPath = () => `/notify/settings`;
|
|
|
539
468
|
eventsCheckoutSuccessPath,
|
|
540
469
|
eventsCreatePath,
|
|
541
470
|
eventsDashboardPath,
|
|
471
|
+
getClientIP,
|
|
542
472
|
getPort,
|
|
543
473
|
getPublicUrl,
|
|
544
474
|
getService,
|
|
@@ -582,12 +512,16 @@ var notifySettingsPath = () => `/notify/settings`;
|
|
|
582
512
|
profileRegisterPath,
|
|
583
513
|
profileUrl,
|
|
584
514
|
projectPath,
|
|
515
|
+
rateLimit,
|
|
585
516
|
registryDocsPath,
|
|
586
517
|
registryPath,
|
|
587
518
|
servicesByTier,
|
|
588
519
|
servicesByVisibility,
|
|
589
520
|
subscribePath,
|
|
521
|
+
tier1Headers,
|
|
522
|
+
tier2Headers,
|
|
523
|
+
tier3Headers,
|
|
590
524
|
validateOrigin,
|
|
591
525
|
whitepaperPath,
|
|
592
526
|
withCors
|
|
593
|
-
}
|
|
527
|
+
};
|
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,21 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ima-jin/config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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
27
|
"dependencies": {
|
|
18
|
-
"@ima-jin/tokens": "^0.
|
|
28
|
+
"@ima-jin/tokens": "^0.8.0"
|
|
19
29
|
},
|
|
20
30
|
"peerDependencies": {
|
|
21
31
|
"next": ">=14"
|
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
|
/**
|
package/src/cors.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
4
4
|
const ORIGIN_PATTERN = /^https:\/\/(dev-)?[a-z-]+\.imajin\.ai$/;
|
|
5
5
|
const LOCAL_PATTERN = /^http:\/\/localhost:\d+$/;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* CSP host-source list corresponding to ORIGIN_PATTERN (prod).
|
|
9
|
+
* Used by security-headers.ts to build frame-ancestors directives.
|
|
10
|
+
* If the allowed-origin set changes, update ORIGIN_PATTERN and this constant together.
|
|
11
|
+
*/
|
|
12
|
+
export const ALLOWED_FRAME_ORIGINS = 'https://*.imajin.ai https://imajin.ai';
|
|
13
|
+
|
|
7
14
|
export function isAllowedOrigin(origin: string | null): boolean {
|
|
8
15
|
if (!origin) return false;
|
|
9
16
|
if (ORIGIN_PATTERN.test(origin)) return true;
|
package/src/handles.ts
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
export const HANDLE_PATTERN = /^[a-z0-9._-]{3,30}$/;
|
|
11
11
|
|
|
12
12
|
/** Regex: disallowed leading/trailing characters */
|
|
13
|
-
const HANDLE_EDGE = /^[
|
|
13
|
+
const HANDLE_EDGE = /^[.-]|[.-]$/;
|
|
14
14
|
|
|
15
15
|
/** Regex: consecutive dots or hyphens */
|
|
16
|
-
const HANDLE_CONSECUTIVE = /[
|
|
16
|
+
const HANDLE_CONSECUTIVE = /[.-]{2}/;
|
|
17
17
|
|
|
18
18
|
/** HTML input pattern attribute (no anchors, no flags) */
|
|
19
19
|
export const HANDLE_INPUT_PATTERN = '[a-z0-9._\\-]{3,30}';
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { isAllowedOrigin, corsHeaders, corsOptions, withCors, validateOrigin } from "./cors";
|
|
1
|
+
export { isAllowedOrigin, corsHeaders, corsOptions, withCors, validateOrigin, ALLOWED_FRAME_ORIGINS } from "./cors";
|
|
2
|
+
export { tier1Headers, tier2Headers, tier3Headers } from './security-headers';
|
|
2
3
|
|
|
3
4
|
export {
|
|
4
5
|
SERVICES,
|
|
@@ -8,6 +9,7 @@ export {
|
|
|
8
9
|
getServiceUrl,
|
|
9
10
|
getPublicUrl,
|
|
10
11
|
buildPublicUrl,
|
|
12
|
+
buildPublicUrlAbsolute,
|
|
11
13
|
servicesByTier,
|
|
12
14
|
servicesByVisibility,
|
|
13
15
|
buildServiceUrlMap,
|
|
@@ -27,6 +29,8 @@ export {
|
|
|
27
29
|
|
|
28
30
|
export { apiFetch, apiUrl } from "./base-path";
|
|
29
31
|
|
|
32
|
+
export { rateLimit, getClientIP } from "./rate-limit";
|
|
33
|
+
|
|
30
34
|
export {
|
|
31
35
|
HANDLE_PATTERN,
|
|
32
36
|
HANDLE_INPUT_PATTERN,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple sliding-window in-memory rate limiter.
|
|
3
|
+
* Safe for single-process Next.js. Per-worker in multi-worker setups (acceptable).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface RateLimitEntry {
|
|
7
|
+
timestamps: number[];
|
|
8
|
+
lastSeen: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const store = new Map<string, RateLimitEntry>();
|
|
12
|
+
let lastGlobalCleanup = Date.now();
|
|
13
|
+
|
|
14
|
+
function pruneStore(now: number) {
|
|
15
|
+
if (now - lastGlobalCleanup < 60_000) return;
|
|
16
|
+
store.forEach((entry, key) => {
|
|
17
|
+
if (now - entry.lastSeen > 120_000) {
|
|
18
|
+
store.delete(key);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
lastGlobalCleanup = now;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function rateLimit(
|
|
25
|
+
key: string,
|
|
26
|
+
limit: number,
|
|
27
|
+
windowMs: number
|
|
28
|
+
): { limited: boolean; retryAfter: number } {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
pruneStore(now);
|
|
31
|
+
|
|
32
|
+
const windowStart = now - windowMs;
|
|
33
|
+
const entry = store.get(key) ?? { timestamps: [], lastSeen: now };
|
|
34
|
+
|
|
35
|
+
entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
|
|
36
|
+
entry.lastSeen = now;
|
|
37
|
+
|
|
38
|
+
if (entry.timestamps.length >= limit) {
|
|
39
|
+
const oldest = entry.timestamps[0];
|
|
40
|
+
const retryAfter = Math.ceil((oldest + windowMs - now) / 1000);
|
|
41
|
+
store.set(key, entry);
|
|
42
|
+
return { limited: true, retryAfter: Math.max(1, retryAfter) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
entry.timestamps.push(now);
|
|
46
|
+
store.set(key, entry);
|
|
47
|
+
return { limited: false, retryAfter: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getClientIP(request: Request): string {
|
|
51
|
+
const forwarded = request.headers.get('x-forwarded-for');
|
|
52
|
+
if (forwarded) return forwarded.split(',')[0].trim();
|
|
53
|
+
return '127.0.0.1';
|
|
54
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ALLOWED_FRAME_ORIGINS } from './cors';
|
|
2
|
+
|
|
3
|
+
type HeaderEntry = { key: string; value: string };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the frame-ancestors directive value.
|
|
7
|
+
* Mirrors isAllowedOrigin(): includes localhost:* in non-prod so local
|
|
8
|
+
* hub-iframe development works without disabling frame protection.
|
|
9
|
+
*/
|
|
10
|
+
function buildFrameAncestors(): string {
|
|
11
|
+
const local = process.env.NODE_ENV !== 'production' ? ' http://localhost:*' : '';
|
|
12
|
+
return `frame-ancestors 'self' ${ALLOWED_FRAME_ORIGINS}${local}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tier 1 — hard deny framing.
|
|
17
|
+
* Use on credential and auth-sensitive pages that must never be embedded.
|
|
18
|
+
*/
|
|
19
|
+
export function tier1Headers(): HeaderEntry[] {
|
|
20
|
+
return [
|
|
21
|
+
{ key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
|
|
22
|
+
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tier 2 — allow framing by Imajin origins only.
|
|
28
|
+
* Use on routes embedded by the auth hub (service dashboards, document viewer, surveys).
|
|
29
|
+
* Does not include X-Frame-Options — it cannot express an allow-list, and SAMEORIGIN
|
|
30
|
+
* would break cross-subdomain framing. CSP frame-ancestors does the work here.
|
|
31
|
+
*/
|
|
32
|
+
export function tier2Headers(): HeaderEntry[] {
|
|
33
|
+
return [{ key: 'Content-Security-Policy', value: buildFrameAncestors() }];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Tier 3 — baseline hardening.
|
|
38
|
+
* Safe to apply on every route in every app.
|
|
39
|
+
*/
|
|
40
|
+
export function tier3Headers(): HeaderEntry[] {
|
|
41
|
+
return [
|
|
42
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
43
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
44
|
+
];
|
|
45
|
+
}
|