@ima-jin/config 1.0.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/README.md +30 -0
- package/dist/index.js +593 -0
- package/dist/index.mjs +446 -0
- package/package.json +26 -0
- package/src/base-path.ts +73 -0
- package/src/cors.ts +43 -0
- package/src/handles.ts +51 -0
- package/src/index.ts +150 -0
- package/src/routes.ts +151 -0
- package/src/services.ts +196 -0
- package/src/session.ts +50 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// src/cors.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
var ORIGIN_PATTERN = /^https:\/\/(dev-)?[a-z-]+\.imajin\.ai$/;
|
|
4
|
+
var LOCAL_PATTERN = /^http:\/\/localhost:\d+$/;
|
|
5
|
+
function isAllowedOrigin(origin) {
|
|
6
|
+
if (!origin) return false;
|
|
7
|
+
if (ORIGIN_PATTERN.test(origin)) return true;
|
|
8
|
+
if (process.env.NODE_ENV !== "production" && LOCAL_PATTERN.test(origin)) return true;
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
function corsHeaders(request) {
|
|
12
|
+
const origin = request.headers.get("origin") || "";
|
|
13
|
+
const allowed = isAllowedOrigin(origin);
|
|
14
|
+
return {
|
|
15
|
+
"Access-Control-Allow-Origin": allowed ? origin : "",
|
|
16
|
+
"Access-Control-Allow-Credentials": "true",
|
|
17
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
18
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Caller-DID",
|
|
19
|
+
"Vary": "Origin"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function corsOptions(request) {
|
|
23
|
+
return new NextResponse(null, { status: 204, headers: corsHeaders(request) });
|
|
24
|
+
}
|
|
25
|
+
function withCors(response, request) {
|
|
26
|
+
const headers = corsHeaders(request);
|
|
27
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
28
|
+
response.headers.set(key, value);
|
|
29
|
+
});
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
function validateOrigin(request) {
|
|
33
|
+
const origin = request.headers.get("origin");
|
|
34
|
+
if (!origin) return true;
|
|
35
|
+
return isAllowedOrigin(origin);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/services.ts
|
|
39
|
+
var SERVICES = [
|
|
40
|
+
// Kernel services — individually visible, all run on the kernel process (port 3000/7000)
|
|
41
|
+
{ name: "kernel", label: "Home", icon: "\u{1F3E0}", description: "Network home \u2014 launcher, articles, stats", devPort: 3e3, prodPort: 7e3, schema: null, tier: "core", visibility: "public", category: "kernel" },
|
|
42
|
+
{ name: "auth", label: "Identity", icon: "\u{1F511}", description: "Authentication, keys, and identity management", devPort: 3e3, prodPort: 7e3, schema: "auth", tier: "core", visibility: "authenticated", category: "kernel" },
|
|
43
|
+
{ name: "profile", label: "Profile", icon: "\u{1F464}", description: "Your profile, settings, and display preferences", devPort: 3e3, prodPort: 7e3, schema: "profile", tier: "core", visibility: "authenticated", category: "kernel" },
|
|
44
|
+
{ name: "connections", label: "Connections", icon: "\u{1F91D}", description: "Your network \u2014 people you know and trust", devPort: 3e3, prodPort: 7e3, schema: "connections", tier: "core", visibility: "authenticated", category: "kernel" },
|
|
45
|
+
{ name: "chat", label: "Chat", icon: "\u{1F4AC}", description: "Conversations and group messaging", devPort: 3e3, prodPort: 7e3, schema: "chat", tier: "core", visibility: "authenticated", category: "kernel" },
|
|
46
|
+
{ name: "pay", label: "Wallet", icon: "\u{1F4B0}", description: "Payments, settlements, and MJN balance", devPort: 3e3, prodPort: 7e3, schema: "pay", tier: "core", visibility: "authenticated", category: "kernel" },
|
|
47
|
+
{ 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" },
|
|
48
|
+
{ 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" },
|
|
49
|
+
{ name: "notify", label: "Notify", icon: "\u{1F514}", description: "Notifications and preferences", devPort: 3e3, prodPort: 7e3, schema: "notify", tier: "core", visibility: "internal", category: "kernel" },
|
|
50
|
+
// Core apps — separate processes
|
|
51
|
+
{ 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" },
|
|
52
|
+
// Imajin apps
|
|
53
|
+
{ name: "coffee", label: "Coffee", icon: "\u2615", description: "Tipping and creator support pages", devPort: 3100, prodPort: 7100, schema: "coffee", tier: "imajin", visibility: "creator", category: "creator" },
|
|
54
|
+
{ name: "dykil", label: "Surveys", icon: "\u{1F4CB}", description: "Surveys and do-you-know-if-I-like polls", devPort: 3101, prodPort: 7101, schema: "dykil", tier: "imajin", visibility: "creator", category: "creator" },
|
|
55
|
+
{ name: "links", label: "Links", icon: "\u{1F517}", description: "Link-in-bio pages and click tracking", devPort: 3102, prodPort: 7102, schema: "links", tier: "imajin", visibility: "creator", category: "creator" },
|
|
56
|
+
{ name: "learn", label: "Learn", icon: "\u{1F4DA}", description: "Courses, lessons, and learning progress", devPort: 3103, prodPort: 7103, schema: "learn", tier: "imajin", visibility: "public", category: "core" },
|
|
57
|
+
{ name: "market", label: "Market", icon: "\u{1F3EA}", description: "Local commerce \u2014 buy and sell with trust", devPort: 3104, prodPort: 7104, schema: "market", tier: "imajin", visibility: "public", category: "core" },
|
|
58
|
+
// Meta — project info and external resources surfaced in the launcher
|
|
59
|
+
{ 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" },
|
|
60
|
+
{ 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" },
|
|
61
|
+
{ 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" }
|
|
62
|
+
// Connected apps (separate repos) will use the plugin architecture (#249).
|
|
63
|
+
// Not included here — they authenticate via delegated sessions, not the monorepo manifest.
|
|
64
|
+
];
|
|
65
|
+
function getService(name) {
|
|
66
|
+
return SERVICES.find((s) => s.name === name);
|
|
67
|
+
}
|
|
68
|
+
function getPort(name, env = "dev") {
|
|
69
|
+
const svc = getService(name);
|
|
70
|
+
return svc ? env === "prod" ? svc.prodPort : svc.devPort : void 0;
|
|
71
|
+
}
|
|
72
|
+
function getServiceUrl(name, env = "dev") {
|
|
73
|
+
const port = getPort(name, env);
|
|
74
|
+
return port ? `http://localhost:${port}` : void 0;
|
|
75
|
+
}
|
|
76
|
+
function getPublicUrl(name, options) {
|
|
77
|
+
const svc = SERVICES.find((s) => s.name === name);
|
|
78
|
+
if (svc == null ? void 0 : svc.externalUrl) return svc.externalUrl;
|
|
79
|
+
const domain = (options == null ? void 0 : options.domain) || "imajin.ai";
|
|
80
|
+
const prefix = options == null ? void 0 : options.prefix;
|
|
81
|
+
if (svc == null ? void 0 : svc.wwwPath) {
|
|
82
|
+
const wwwUrl = getPublicUrl("www", options);
|
|
83
|
+
return `${wwwUrl}${svc.wwwPath}`;
|
|
84
|
+
}
|
|
85
|
+
if (domain.includes("localhost") || (prefix == null ? void 0 : prefix.includes("localhost"))) {
|
|
86
|
+
const port = getPort(name, "dev");
|
|
87
|
+
return port ? `http://localhost:${port}` : `http://localhost:3000`;
|
|
88
|
+
}
|
|
89
|
+
if (prefix && prefix.includes(".")) {
|
|
90
|
+
const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
91
|
+
if (name === "www" || name === "kernel") return `https://${base}`;
|
|
92
|
+
return `https://${base}/${name}`;
|
|
93
|
+
}
|
|
94
|
+
const subdomain = prefix ? `${prefix}-${name}` : name;
|
|
95
|
+
return `https://${subdomain}.${domain}`;
|
|
96
|
+
}
|
|
97
|
+
function buildPublicUrl(name, servicePrefix, domain) {
|
|
98
|
+
if (!servicePrefix && !domain && typeof process !== "undefined") {
|
|
99
|
+
const envKey = `NEXT_PUBLIC_${name.toUpperCase()}_URL`;
|
|
100
|
+
const explicit = process.env[envKey];
|
|
101
|
+
if (explicit) return explicit;
|
|
102
|
+
}
|
|
103
|
+
const p = servicePrefix ?? (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_SERVICE_PREFIX : void 0) ?? "https://";
|
|
104
|
+
const d = domain ?? (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_DOMAIN : void 0) ?? "imajin.ai";
|
|
105
|
+
if (p.includes("localhost") || d.includes("localhost")) {
|
|
106
|
+
const port = getPort(name, "dev");
|
|
107
|
+
return port ? `http://localhost:${port}` : `http://localhost:3000`;
|
|
108
|
+
}
|
|
109
|
+
if (!servicePrefix && !domain) {
|
|
110
|
+
return name === "kernel" ? "" : `/${name}`;
|
|
111
|
+
}
|
|
112
|
+
const match = p.replace(/^https?:\/\//, "").replace(/-$/, "") || void 0;
|
|
113
|
+
return getPublicUrl(name, { prefix: match, domain: d });
|
|
114
|
+
}
|
|
115
|
+
var SERVICE_NAMES = SERVICES.map((s) => s.name);
|
|
116
|
+
function servicesByTier(tier) {
|
|
117
|
+
return SERVICES.filter((s) => s.tier === tier);
|
|
118
|
+
}
|
|
119
|
+
function servicesByVisibility(visibility) {
|
|
120
|
+
return SERVICES.filter((s) => s.visibility === visibility);
|
|
121
|
+
}
|
|
122
|
+
function buildServiceUrlMap(env, mode = "dev") {
|
|
123
|
+
const map = {};
|
|
124
|
+
for (const svc of SERVICES) {
|
|
125
|
+
const envKey = `${svc.name.toUpperCase()}_SERVICE_URL`;
|
|
126
|
+
const port = mode === "prod" ? svc.prodPort : svc.devPort;
|
|
127
|
+
map[svc.name] = env[envKey] || `http://localhost:${port}`;
|
|
128
|
+
}
|
|
129
|
+
return map;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/session.ts
|
|
133
|
+
function getSessionCookieName(env) {
|
|
134
|
+
const resolved = env ?? (typeof process !== "undefined" && (process.env.IMAJIN_ENV === "dev" || process.env.NODE_ENV === "development") ? "dev" : "prod");
|
|
135
|
+
return resolved === "dev" ? "imajin_session_dev" : "imajin_session";
|
|
136
|
+
}
|
|
137
|
+
var SESSION_COOKIE_NAME = getSessionCookieName();
|
|
138
|
+
function isLocalhost() {
|
|
139
|
+
if (typeof process === "undefined") return false;
|
|
140
|
+
const prefix = process.env.NEXT_PUBLIC_SERVICE_PREFIX ?? "";
|
|
141
|
+
return prefix.includes("localhost");
|
|
142
|
+
}
|
|
143
|
+
function getSessionCookieOptions(env) {
|
|
144
|
+
const local = isLocalhost();
|
|
145
|
+
return {
|
|
146
|
+
name: getSessionCookieName(env),
|
|
147
|
+
options: {
|
|
148
|
+
httpOnly: true,
|
|
149
|
+
secure: !local,
|
|
150
|
+
sameSite: local ? "lax" : "none",
|
|
151
|
+
path: "/",
|
|
152
|
+
...local ? {} : { domain: ".imajin.ai" },
|
|
153
|
+
maxAge: 60 * 60 * 24
|
|
154
|
+
// 24 hours
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/base-path.ts
|
|
160
|
+
function getBasePath() {
|
|
161
|
+
return process.env.NEXT_PUBLIC_BASE_PATH || "";
|
|
162
|
+
}
|
|
163
|
+
function apiUrl(path) {
|
|
164
|
+
const base = getBasePath();
|
|
165
|
+
if (!base) return path;
|
|
166
|
+
if (path.startsWith(base)) return path;
|
|
167
|
+
return `${base}${path}`;
|
|
168
|
+
}
|
|
169
|
+
function apiFetch(path, init) {
|
|
170
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
171
|
+
return fetch(path, init);
|
|
172
|
+
}
|
|
173
|
+
return fetch(apiUrl(path), init);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/handles.ts
|
|
177
|
+
var HANDLE_PATTERN = /^[a-z0-9._-]{3,30}$/;
|
|
178
|
+
var HANDLE_EDGE = /^[.\-]|[.\-]$/;
|
|
179
|
+
var HANDLE_CONSECUTIVE = /[.\-]{2}/;
|
|
180
|
+
var HANDLE_INPUT_PATTERN = "[a-z0-9._\\-]{3,30}";
|
|
181
|
+
var HANDLE_ALLOWED_CHARS = /[^a-z0-9._-]/g;
|
|
182
|
+
var RESERVED_HANDLES = /* @__PURE__ */ new Set([
|
|
183
|
+
"admin",
|
|
184
|
+
"api",
|
|
185
|
+
"app",
|
|
186
|
+
"auth",
|
|
187
|
+
"blog",
|
|
188
|
+
"coffee",
|
|
189
|
+
"connect",
|
|
190
|
+
"dashboard",
|
|
191
|
+
"docs",
|
|
192
|
+
"edit",
|
|
193
|
+
"events",
|
|
194
|
+
"help",
|
|
195
|
+
"home",
|
|
196
|
+
"imajin",
|
|
197
|
+
"inbox",
|
|
198
|
+
"links",
|
|
199
|
+
"login",
|
|
200
|
+
"logout",
|
|
201
|
+
"mail",
|
|
202
|
+
"news",
|
|
203
|
+
"pay",
|
|
204
|
+
"profile",
|
|
205
|
+
"register",
|
|
206
|
+
"search",
|
|
207
|
+
"settings",
|
|
208
|
+
"signup",
|
|
209
|
+
"status",
|
|
210
|
+
"support",
|
|
211
|
+
"team",
|
|
212
|
+
"www"
|
|
213
|
+
]);
|
|
214
|
+
function isValidHandle(handle) {
|
|
215
|
+
if (!HANDLE_PATTERN.test(handle)) return false;
|
|
216
|
+
if (HANDLE_EDGE.test(handle)) return false;
|
|
217
|
+
if (HANDLE_CONSECUTIVE.test(handle)) return false;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
function isReservedHandle(handle) {
|
|
221
|
+
return RESERVED_HANDLES.has(handle);
|
|
222
|
+
}
|
|
223
|
+
function normalizeHandleInput(value) {
|
|
224
|
+
return value.toLowerCase().replace(HANDLE_ALLOWED_CHARS, "");
|
|
225
|
+
}
|
|
226
|
+
var HANDLE_ERROR = "Handle must be 3-30 characters: lowercase letters, numbers, dots, hyphens, underscores";
|
|
227
|
+
|
|
228
|
+
// src/routes.ts
|
|
229
|
+
var eventPath = (eventId) => `/e/${eventId}`;
|
|
230
|
+
var eventEditPath = (eventId) => `/e/${eventId}/edit`;
|
|
231
|
+
var eventRegisterPath = (eventId, ticketId) => `/e/${eventId}/register/${ticketId}`;
|
|
232
|
+
var eventMyTicketsPath = (eventId) => `/e/${eventId}/my-tickets`;
|
|
233
|
+
var eventCheckoutSuccessPath = (eventId) => `/e/${eventId}#tickets`;
|
|
234
|
+
var eventAdminPath = (eventId) => `/admin/${eventId}`;
|
|
235
|
+
var eventsDashboardPath = () => `/dashboard`;
|
|
236
|
+
var eventsCreatePath = () => `/create`;
|
|
237
|
+
var eventsCheckoutSuccessPath = () => `/checkout/success`;
|
|
238
|
+
var eventsAdminListPath = () => `/admin`;
|
|
239
|
+
var eventUrl = (baseUrl, eventId) => `${baseUrl}/e/${eventId}`;
|
|
240
|
+
var eventEditUrl = (baseUrl, eventId) => `${baseUrl}/e/${eventId}/edit`;
|
|
241
|
+
var eventRegisterUrl = (baseUrl, eventId, ticketId) => `${baseUrl}/e/${eventId}/register/${ticketId}`;
|
|
242
|
+
var eventMyTicketsUrl = (baseUrl, eventId) => `${baseUrl}/e/${eventId}/my-tickets`;
|
|
243
|
+
var profilePath = (handle) => `/p/${handle}`;
|
|
244
|
+
var profileEditPath = () => `/p/edit`;
|
|
245
|
+
var profileLoginPath = () => `/profile/login`;
|
|
246
|
+
var profileRegisterPath = () => `/profile/register`;
|
|
247
|
+
var profileUrl = (baseUrl, handle) => `${baseUrl}/p/${handle}`;
|
|
248
|
+
var authLoginPath = () => `/auth/login`;
|
|
249
|
+
var authRegisterPath = () => `/auth/register`;
|
|
250
|
+
var authOnboardPath = () => `/auth/onboard`;
|
|
251
|
+
var authSettingsPath = () => `/auth/settings`;
|
|
252
|
+
var authSecurityPath = () => `/auth/settings/security`;
|
|
253
|
+
var authGroupSettingsPath = (groupDid) => `/auth/groups/${groupDid}/settings`;
|
|
254
|
+
var authGroupsPath = () => `/auth/groups`;
|
|
255
|
+
var authNewGroupPath = () => `/auth/groups/new`;
|
|
256
|
+
var authAgentsPath = () => `/auth/agents`;
|
|
257
|
+
var authAppsPath = () => `/auth/apps`;
|
|
258
|
+
var authAttestationsPath = () => `/auth/attestations`;
|
|
259
|
+
var authAuthorizePath = () => `/auth/authorize`;
|
|
260
|
+
var authDeveloperAppsPath = () => `/auth/developer/apps`;
|
|
261
|
+
var authDeveloperAppPath = (appId) => `/auth/developer/apps/${appId}`;
|
|
262
|
+
var authMembersPath = () => `/auth/members`;
|
|
263
|
+
var authNotificationsPath = () => `/auth/notifications`;
|
|
264
|
+
var authStubsPath = (did) => `/auth/stubs/${did}`;
|
|
265
|
+
var authNewStubPath = () => `/auth/stubs/new`;
|
|
266
|
+
var chatConversationsPath = () => `/chat/conversations`;
|
|
267
|
+
var chatConversationPath = (type, slug) => `/chat/conversations/${type}/${slug}`;
|
|
268
|
+
var chatPath = () => `/chat`;
|
|
269
|
+
var connectionsPath = () => `/connections`;
|
|
270
|
+
var connectionsInvitePath = (did, code) => `/connections/invite/${did}/${code}`;
|
|
271
|
+
var connectionsPodPath = (id) => `/connections/pods/${id}`;
|
|
272
|
+
var payPath = () => `/pay`;
|
|
273
|
+
var payHistoryPath = () => `/pay/history`;
|
|
274
|
+
var payPayoutsPath = () => `/pay/payouts`;
|
|
275
|
+
var payTopupPath = () => `/pay/topup`;
|
|
276
|
+
var payTopupSuccessPath = () => `/pay/topup/success`;
|
|
277
|
+
var mediaPath = () => `/media`;
|
|
278
|
+
var learnCoursePath = (slug) => `/course/${slug}`;
|
|
279
|
+
var learnCourseLessonPath = (slug, lessonId) => `/course/${slug}/${lessonId}`;
|
|
280
|
+
var learnCoursePresentPath = (slug) => `/course/${slug}/present`;
|
|
281
|
+
var learnDashboardPath = () => `/dashboard`;
|
|
282
|
+
var learnCourseDashboardPath = (slug) => `/dashboard/${slug}`;
|
|
283
|
+
var learnCourseStudentsPath = (slug) => `/dashboard/${slug}/students`;
|
|
284
|
+
var learnHandlePath = (handle) => `/${handle}`;
|
|
285
|
+
var coffeeHandlePath = (handle) => `/${handle}`;
|
|
286
|
+
var coffeeDashboardPath = () => `/dashboard`;
|
|
287
|
+
var coffeeEditPath = () => `/edit`;
|
|
288
|
+
var coffeeSuccessPath = () => `/success`;
|
|
289
|
+
var dykilHandlePath = (handle) => `/${handle}`;
|
|
290
|
+
var dykilSurveyPath = (handle, surveyId) => `/${handle}/${surveyId}`;
|
|
291
|
+
var dykilCreatePath = () => `/create`;
|
|
292
|
+
var dykilDashboardPath = () => `/dashboard`;
|
|
293
|
+
var dykilResultsPath = (id) => `/survey/${id}/results`;
|
|
294
|
+
var dykilEmbedPath = (surveyId) => `/embed/${surveyId}`;
|
|
295
|
+
var linksHandlePath = (handle) => `/${handle}`;
|
|
296
|
+
var linksDashboardPath = () => `/dashboard`;
|
|
297
|
+
var linksEditPath = () => `/edit`;
|
|
298
|
+
var marketListingPath = (id) => `/listings/${id}`;
|
|
299
|
+
var marketListingEditPath = (id) => `/listings/${id}/edit`;
|
|
300
|
+
var marketNewListingPath = () => `/listings/new`;
|
|
301
|
+
var marketSellerPath = (handle) => `/seller/${handle}`;
|
|
302
|
+
var marketCheckoutSuccessPath = () => `/checkout/success`;
|
|
303
|
+
var marketDashboardPath = () => `/dashboard`;
|
|
304
|
+
var marketSettingsPath = () => `/settings`;
|
|
305
|
+
var articlesPath = () => `/articles`;
|
|
306
|
+
var articleAuthorPath = (handle) => `/articles/${handle}`;
|
|
307
|
+
var articlePath = (handle, slug) => `/articles/${handle}/${slug}`;
|
|
308
|
+
var registryPath = () => `/registry`;
|
|
309
|
+
var registryDocsPath = () => `/registry/docs`;
|
|
310
|
+
var buildPath = () => `/build`;
|
|
311
|
+
var bumpPath = () => `/bump`;
|
|
312
|
+
var bugsPath = () => `/bugs`;
|
|
313
|
+
var bugsAdminPath = () => `/bugs/admin`;
|
|
314
|
+
var healthPath = () => `/health`;
|
|
315
|
+
var privacyPath = () => `/privacy`;
|
|
316
|
+
var whitepaperPath = () => `/whitepaper`;
|
|
317
|
+
var subscribePath = () => `/subscribe`;
|
|
318
|
+
var developerGuidePath = () => `/developer-guide`;
|
|
319
|
+
var docsPath = () => `/docs`;
|
|
320
|
+
var projectPath = () => `/project`;
|
|
321
|
+
var notifyPath = () => `/notify`;
|
|
322
|
+
var notifySettingsPath = () => `/notify/settings`;
|
|
323
|
+
export {
|
|
324
|
+
HANDLE_ALLOWED_CHARS,
|
|
325
|
+
HANDLE_ERROR,
|
|
326
|
+
HANDLE_INPUT_PATTERN,
|
|
327
|
+
HANDLE_PATTERN,
|
|
328
|
+
RESERVED_HANDLES,
|
|
329
|
+
SERVICES,
|
|
330
|
+
SERVICE_NAMES,
|
|
331
|
+
SESSION_COOKIE_NAME,
|
|
332
|
+
apiFetch,
|
|
333
|
+
apiUrl,
|
|
334
|
+
articleAuthorPath,
|
|
335
|
+
articlePath,
|
|
336
|
+
articlesPath,
|
|
337
|
+
authAgentsPath,
|
|
338
|
+
authAppsPath,
|
|
339
|
+
authAttestationsPath,
|
|
340
|
+
authAuthorizePath,
|
|
341
|
+
authDeveloperAppPath,
|
|
342
|
+
authDeveloperAppsPath,
|
|
343
|
+
authGroupSettingsPath,
|
|
344
|
+
authGroupsPath,
|
|
345
|
+
authLoginPath,
|
|
346
|
+
authMembersPath,
|
|
347
|
+
authNewGroupPath,
|
|
348
|
+
authNewStubPath,
|
|
349
|
+
authNotificationsPath,
|
|
350
|
+
authOnboardPath,
|
|
351
|
+
authRegisterPath,
|
|
352
|
+
authSecurityPath,
|
|
353
|
+
authSettingsPath,
|
|
354
|
+
authStubsPath,
|
|
355
|
+
bugsAdminPath,
|
|
356
|
+
bugsPath,
|
|
357
|
+
buildPath,
|
|
358
|
+
buildPublicUrl,
|
|
359
|
+
buildServiceUrlMap,
|
|
360
|
+
bumpPath,
|
|
361
|
+
chatConversationPath,
|
|
362
|
+
chatConversationsPath,
|
|
363
|
+
chatPath,
|
|
364
|
+
coffeeDashboardPath,
|
|
365
|
+
coffeeEditPath,
|
|
366
|
+
coffeeHandlePath,
|
|
367
|
+
coffeeSuccessPath,
|
|
368
|
+
connectionsInvitePath,
|
|
369
|
+
connectionsPath,
|
|
370
|
+
connectionsPodPath,
|
|
371
|
+
corsHeaders,
|
|
372
|
+
corsOptions,
|
|
373
|
+
developerGuidePath,
|
|
374
|
+
docsPath,
|
|
375
|
+
dykilCreatePath,
|
|
376
|
+
dykilDashboardPath,
|
|
377
|
+
dykilEmbedPath,
|
|
378
|
+
dykilHandlePath,
|
|
379
|
+
dykilResultsPath,
|
|
380
|
+
dykilSurveyPath,
|
|
381
|
+
eventAdminPath,
|
|
382
|
+
eventCheckoutSuccessPath,
|
|
383
|
+
eventEditPath,
|
|
384
|
+
eventEditUrl,
|
|
385
|
+
eventMyTicketsPath,
|
|
386
|
+
eventMyTicketsUrl,
|
|
387
|
+
eventPath,
|
|
388
|
+
eventRegisterPath,
|
|
389
|
+
eventRegisterUrl,
|
|
390
|
+
eventUrl,
|
|
391
|
+
eventsAdminListPath,
|
|
392
|
+
eventsCheckoutSuccessPath,
|
|
393
|
+
eventsCreatePath,
|
|
394
|
+
eventsDashboardPath,
|
|
395
|
+
getPort,
|
|
396
|
+
getPublicUrl,
|
|
397
|
+
getService,
|
|
398
|
+
getServiceUrl,
|
|
399
|
+
getSessionCookieName,
|
|
400
|
+
getSessionCookieOptions,
|
|
401
|
+
healthPath,
|
|
402
|
+
isAllowedOrigin,
|
|
403
|
+
isReservedHandle,
|
|
404
|
+
isValidHandle,
|
|
405
|
+
learnCourseDashboardPath,
|
|
406
|
+
learnCourseLessonPath,
|
|
407
|
+
learnCoursePath,
|
|
408
|
+
learnCoursePresentPath,
|
|
409
|
+
learnCourseStudentsPath,
|
|
410
|
+
learnDashboardPath,
|
|
411
|
+
learnHandlePath,
|
|
412
|
+
linksDashboardPath,
|
|
413
|
+
linksEditPath,
|
|
414
|
+
linksHandlePath,
|
|
415
|
+
marketCheckoutSuccessPath,
|
|
416
|
+
marketDashboardPath,
|
|
417
|
+
marketListingEditPath,
|
|
418
|
+
marketListingPath,
|
|
419
|
+
marketNewListingPath,
|
|
420
|
+
marketSellerPath,
|
|
421
|
+
marketSettingsPath,
|
|
422
|
+
mediaPath,
|
|
423
|
+
normalizeHandleInput,
|
|
424
|
+
notifyPath,
|
|
425
|
+
notifySettingsPath,
|
|
426
|
+
payHistoryPath,
|
|
427
|
+
payPath,
|
|
428
|
+
payPayoutsPath,
|
|
429
|
+
payTopupPath,
|
|
430
|
+
payTopupSuccessPath,
|
|
431
|
+
privacyPath,
|
|
432
|
+
profileEditPath,
|
|
433
|
+
profileLoginPath,
|
|
434
|
+
profilePath,
|
|
435
|
+
profileRegisterPath,
|
|
436
|
+
profileUrl,
|
|
437
|
+
projectPath,
|
|
438
|
+
registryDocsPath,
|
|
439
|
+
registryPath,
|
|
440
|
+
servicesByTier,
|
|
441
|
+
servicesByVisibility,
|
|
442
|
+
subscribePath,
|
|
443
|
+
validateOrigin,
|
|
444
|
+
whitepaperPath,
|
|
445
|
+
withCors
|
|
446
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ima-jin/config",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.mjs",
|
|
9
|
+
"require": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/",
|
|
15
|
+
"src/"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@ima-jin/tokens": "^1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"next": ">=14"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/base-path.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* basePath-aware utilities for userspace apps running behind Next.js basePath.
|
|
3
|
+
*
|
|
4
|
+
* Next.js `basePath` auto-prefixes <Link> and router.push() but NOT:
|
|
5
|
+
* - fetch() calls
|
|
6
|
+
* - <a href=""> tags
|
|
7
|
+
* - window.location assignments
|
|
8
|
+
*
|
|
9
|
+
* These utilities read NEXT_PUBLIC_BASE_PATH (set automatically by Next.js
|
|
10
|
+
* when basePath is configured) and prepend it where needed.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import { apiFetch, apiUrl } from '@imajin/config';
|
|
14
|
+
*
|
|
15
|
+
* // Instead of: fetch('/api/listings')
|
|
16
|
+
* apiFetch('/api/listings')
|
|
17
|
+
*
|
|
18
|
+
* // For <a> tags or window.location:
|
|
19
|
+
* <a href={apiUrl('/dashboard')}>Dashboard</a>
|
|
20
|
+
* window.location.href = apiUrl('/dashboard');
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the basePath prefix. Works in both browser and server contexts.
|
|
25
|
+
* Returns empty string when no basePath is configured (standalone mode).
|
|
26
|
+
*/
|
|
27
|
+
function getBasePath(): string {
|
|
28
|
+
return process.env.NEXT_PUBLIC_BASE_PATH || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prepend basePath to any root-relative path.
|
|
33
|
+
*
|
|
34
|
+
* apiUrl('/api/listings') → '/market/api/listings' (with basePath: '/market')
|
|
35
|
+
* apiUrl('/dashboard') → '/market/dashboard'
|
|
36
|
+
* apiUrl('/api/listings') → '/api/listings' (standalone, no basePath)
|
|
37
|
+
*/
|
|
38
|
+
export function apiUrl(path: string): string {
|
|
39
|
+
const base = getBasePath();
|
|
40
|
+
if (!base) return path;
|
|
41
|
+
// Avoid double-prefixing
|
|
42
|
+
if (path.startsWith(base)) return path;
|
|
43
|
+
return `${base}${path}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* basePath-aware fetch wrapper. Drop-in replacement for fetch() with
|
|
48
|
+
* root-relative paths.
|
|
49
|
+
*
|
|
50
|
+
* apiFetch('/api/listings') → fetch('/market/api/listings')
|
|
51
|
+
* apiFetch('/api/listings', { ... }) → fetch('/market/api/listings', { ... })
|
|
52
|
+
*
|
|
53
|
+
* Absolute URLs (http://, https://) are passed through unchanged.
|
|
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:
|
|
59
|
+
*
|
|
60
|
+
* apiFetch('/api/something', {
|
|
61
|
+
* headers: { 'x-correlation-id': correlationId },
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* When WO3 instruments services, each withLogger handler receives correlationId in
|
|
65
|
+
* ctx and should forward it on any outbound apiFetch/fetch calls.
|
|
66
|
+
*/
|
|
67
|
+
export function apiFetch(path: string, init?: RequestInit): Promise<Response> {
|
|
68
|
+
// Don't prefix absolute URLs
|
|
69
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
70
|
+
return fetch(path, init);
|
|
71
|
+
}
|
|
72
|
+
return fetch(apiUrl(path), init);
|
|
73
|
+
}
|
package/src/cors.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
// Match *.imajin.ai, dev-*.imajin.ai, and localhost for dev
|
|
4
|
+
const ORIGIN_PATTERN = /^https:\/\/(dev-)?[a-z-]+\.imajin\.ai$/;
|
|
5
|
+
const LOCAL_PATTERN = /^http:\/\/localhost:\d+$/;
|
|
6
|
+
|
|
7
|
+
export function isAllowedOrigin(origin: string | null): boolean {
|
|
8
|
+
if (!origin) return false;
|
|
9
|
+
if (ORIGIN_PATTERN.test(origin)) return true;
|
|
10
|
+
if (process.env.NODE_ENV !== "production" && LOCAL_PATTERN.test(origin)) return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function corsHeaders(request: NextRequest): Record<string, string> {
|
|
15
|
+
const origin = request.headers.get("origin") || "";
|
|
16
|
+
const allowed = isAllowedOrigin(origin);
|
|
17
|
+
return {
|
|
18
|
+
"Access-Control-Allow-Origin": allowed ? origin : "",
|
|
19
|
+
"Access-Control-Allow-Credentials": "true",
|
|
20
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
21
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Caller-DID",
|
|
22
|
+
"Vary": "Origin",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function corsOptions(request: NextRequest): NextResponse {
|
|
27
|
+
return new NextResponse(null, { status: 204, headers: corsHeaders(request) });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function withCors(response: NextResponse, request: NextRequest): NextResponse {
|
|
31
|
+
const headers = corsHeaders(request);
|
|
32
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
33
|
+
response.headers.set(key, value);
|
|
34
|
+
});
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateOrigin(request: NextRequest): boolean {
|
|
39
|
+
const origin = request.headers.get("origin");
|
|
40
|
+
// Allow server-side calls (no origin header)
|
|
41
|
+
if (!origin) return true;
|
|
42
|
+
return isAllowedOrigin(origin);
|
|
43
|
+
}
|
package/src/handles.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle validation and normalization.
|
|
3
|
+
*
|
|
4
|
+
* Handles are node-scoped today, globally unique via DFOS chain in the future.
|
|
5
|
+
* Pattern: lowercase alphanumeric + dots, hyphens, underscores. 3-30 chars.
|
|
6
|
+
* No leading/trailing dots or hyphens. No consecutive dots or hyphens.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Regex: the full allowed character set, length enforced */
|
|
10
|
+
export const HANDLE_PATTERN = /^[a-z0-9._-]{3,30}$/;
|
|
11
|
+
|
|
12
|
+
/** Regex: disallowed leading/trailing characters */
|
|
13
|
+
const HANDLE_EDGE = /^[.\-]|[.\-]$/;
|
|
14
|
+
|
|
15
|
+
/** Regex: consecutive dots or hyphens */
|
|
16
|
+
const HANDLE_CONSECUTIVE = /[.\-]{2}/;
|
|
17
|
+
|
|
18
|
+
/** HTML input pattern attribute (no anchors, no flags) */
|
|
19
|
+
export const HANDLE_INPUT_PATTERN = '[a-z0-9._\\-]{3,30}';
|
|
20
|
+
|
|
21
|
+
/** Characters allowed in handles — use in onChange filters */
|
|
22
|
+
export const HANDLE_ALLOWED_CHARS = /[^a-z0-9._-]/g;
|
|
23
|
+
|
|
24
|
+
/** Reserved handles that cannot be claimed */
|
|
25
|
+
export const RESERVED_HANDLES = new Set([
|
|
26
|
+
'admin', 'api', 'app', 'auth', 'blog', 'coffee', 'connect', 'dashboard',
|
|
27
|
+
'docs', 'edit', 'events', 'help', 'home', 'imajin', 'inbox', 'links', 'login',
|
|
28
|
+
'logout', 'mail', 'news', 'pay', 'profile', 'register', 'search', 'settings',
|
|
29
|
+
'signup', 'status', 'support', 'team', 'www',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** Validate handle format (does NOT check reserved or uniqueness) */
|
|
33
|
+
export function isValidHandle(handle: string): boolean {
|
|
34
|
+
if (!HANDLE_PATTERN.test(handle)) return false;
|
|
35
|
+
if (HANDLE_EDGE.test(handle)) return false;
|
|
36
|
+
if (HANDLE_CONSECUTIVE.test(handle)) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Check if handle is reserved */
|
|
41
|
+
export function isReservedHandle(handle: string): boolean {
|
|
42
|
+
return RESERVED_HANDLES.has(handle);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Normalize input: lowercase, strip disallowed chars */
|
|
46
|
+
export function normalizeHandleInput(value: string): string {
|
|
47
|
+
return value.toLowerCase().replace(HANDLE_ALLOWED_CHARS, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validation error message */
|
|
51
|
+
export const HANDLE_ERROR = 'Handle must be 3-30 characters: lowercase letters, numbers, dots, hyphens, underscores';
|