@luckystack/core 0.1.9 → 0.2.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/CHANGELOG.md +25 -25
- package/CLAUDE.md +243 -209
- package/LICENSE +21 -21
- package/README.md +93 -93
- package/dist/chunk-OZTB2RL2.js +699 -0
- package/dist/chunk-OZTB2RL2.js.map +1 -0
- package/dist/chunk-USQA6QTX.js +28 -0
- package/dist/chunk-USQA6QTX.js.map +1 -0
- package/dist/chunk-V2WKGTZH.js +357 -0
- package/dist/chunk-V2WKGTZH.js.map +1 -0
- package/dist/chunk-W62GGT2P.js +241 -0
- package/dist/chunk-W62GGT2P.js.map +1 -0
- package/dist/client.d.ts +57 -4
- package/dist/client.js +251 -705
- package/dist/client.js.map +1 -1
- package/dist/eslint/index.js +4 -3
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.d.ts +671 -22
- package/dist/index.js +808 -1156
- package/dist/index.js.map +1 -1
- package/dist/{pageRouteValidation-CxZJVwZh.d.ts → pageRouteValidation-BoZ-rQWi.d.ts} +336 -7
- package/dist/projectConfig-JX563K43.js +16 -0
- package/dist/projectConfig-JX563K43.js.map +1 -0
- package/dist/sentrySetup-Q4BKUAQ3.js +16 -0
- package/dist/sentrySetup-Q4BKUAQ3.js.map +1 -0
- package/docs/app-bootstrap.md +418 -418
- package/docs/config-registry.md +350 -350
- package/docs/csrf-config.md +88 -88
- package/docs/error-tracker-registry.md +174 -174
- package/docs/hooks.md +298 -298
- package/docs/rate-limit-strategy.md +170 -170
- package/docs/redis-adapter.md +111 -111
- package/docs/session-types.md +180 -180
- package/docs/socket-bootstrap.md +366 -366
- package/package.json +3 -2
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getProjectConfig
|
|
3
|
+
} from "./chunk-W62GGT2P.js";
|
|
4
|
+
import {
|
|
5
|
+
captureException,
|
|
6
|
+
getLogger
|
|
7
|
+
} from "./chunk-V2WKGTZH.js";
|
|
8
|
+
import {
|
|
9
|
+
createRegistry
|
|
10
|
+
} from "./chunk-USQA6QTX.js";
|
|
11
|
+
|
|
12
|
+
// src/apiMethodMapRegistry.ts
|
|
13
|
+
var registry = createRegistry(null);
|
|
14
|
+
var registerApiMethodMap = (map) => {
|
|
15
|
+
registry.register(map);
|
|
16
|
+
};
|
|
17
|
+
var getRegisteredApiMethod = (pagePath, apiName, version) => {
|
|
18
|
+
const activeMap = registry.get();
|
|
19
|
+
if (!activeMap) return void 0;
|
|
20
|
+
return activeMap[pagePath]?.[apiName]?.[version];
|
|
21
|
+
};
|
|
22
|
+
var isApiMethodMapRegistered = () => registry.isRegistered();
|
|
23
|
+
|
|
24
|
+
// src/httpApiUtils.ts
|
|
25
|
+
var isGetMethodName = (methodNameLower) => methodNameLower.startsWith("get") || methodNameLower.startsWith("fetch") || methodNameLower.startsWith("list");
|
|
26
|
+
var inferHttpMethod = (apiName) => {
|
|
27
|
+
const nameParts = apiName.split("/");
|
|
28
|
+
const lastPart = nameParts.at(-1)?.toLowerCase() ?? "";
|
|
29
|
+
const methodName = /^v\d+$/.test(lastPart) ? (nameParts.at(-2) ?? "").toLowerCase() : lastPart;
|
|
30
|
+
if (isGetMethodName(methodName)) {
|
|
31
|
+
return "GET";
|
|
32
|
+
}
|
|
33
|
+
if (methodName.startsWith("delete") || methodName.startsWith("remove")) {
|
|
34
|
+
return "DELETE";
|
|
35
|
+
}
|
|
36
|
+
if (methodName.startsWith("update") || methodName.startsWith("edit") || methodName.startsWith("patch")) {
|
|
37
|
+
return "PUT";
|
|
38
|
+
}
|
|
39
|
+
return "POST";
|
|
40
|
+
};
|
|
41
|
+
var getEffectiveHttpMethod = (apiConfig, apiName) => {
|
|
42
|
+
return apiConfig.httpMethod ?? inferHttpMethod(apiName);
|
|
43
|
+
};
|
|
44
|
+
var isMethodAllowed = (requestMethod, allowedMethod) => {
|
|
45
|
+
return requestMethod === allowedMethod;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/socketState.ts
|
|
49
|
+
var socket = null;
|
|
50
|
+
var waiters = [];
|
|
51
|
+
var WAIT_FOR_SOCKET_TIMEOUT_MS = 5e3;
|
|
52
|
+
var setSocket = (next) => {
|
|
53
|
+
socket = next;
|
|
54
|
+
if (next) {
|
|
55
|
+
const pending = waiters;
|
|
56
|
+
waiters = [];
|
|
57
|
+
for (const resolve of pending) {
|
|
58
|
+
resolve(next);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var responseIndex = 0;
|
|
63
|
+
var incrementResponseIndex = () => {
|
|
64
|
+
responseIndex = responseIndex + 1;
|
|
65
|
+
return responseIndex;
|
|
66
|
+
};
|
|
67
|
+
var waitForSocket = () => {
|
|
68
|
+
if (socket) return Promise.resolve(socket);
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
waiters = waiters.filter((w) => w !== onSocket);
|
|
72
|
+
resolve(null);
|
|
73
|
+
}, WAIT_FOR_SOCKET_TIMEOUT_MS);
|
|
74
|
+
const onSocket = (s) => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
resolve(s);
|
|
77
|
+
};
|
|
78
|
+
waiters.push(onSocket);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/notifier.ts
|
|
83
|
+
var noopNotifier = {
|
|
84
|
+
success: () => {
|
|
85
|
+
},
|
|
86
|
+
error: () => {
|
|
87
|
+
},
|
|
88
|
+
info: () => {
|
|
89
|
+
},
|
|
90
|
+
warning: () => {
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var registry2 = createRegistry(noopNotifier);
|
|
94
|
+
var registerNotifier = (notifier) => {
|
|
95
|
+
registry2.register(notifier);
|
|
96
|
+
};
|
|
97
|
+
var getNotifier = () => registry2.get();
|
|
98
|
+
var notify = {
|
|
99
|
+
success: (input) => {
|
|
100
|
+
registry2.get().success(input);
|
|
101
|
+
},
|
|
102
|
+
error: (input) => {
|
|
103
|
+
registry2.get().error(input);
|
|
104
|
+
},
|
|
105
|
+
info: (input) => {
|
|
106
|
+
registry2.get().info(input);
|
|
107
|
+
},
|
|
108
|
+
warning: (input) => {
|
|
109
|
+
registry2.get().warning(input);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/tryCatchSync.ts
|
|
114
|
+
function tryCatchSync(func, params) {
|
|
115
|
+
try {
|
|
116
|
+
const response = func(params);
|
|
117
|
+
return [null, response];
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return [error, null];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/clientHookBus.ts
|
|
124
|
+
var handlerSets = {};
|
|
125
|
+
function registerClientHook(name, handler) {
|
|
126
|
+
const existing = handlerSets[name];
|
|
127
|
+
const set = existing ?? /* @__PURE__ */ new Set();
|
|
128
|
+
if (!existing) handlerSets[name] = set;
|
|
129
|
+
const wrapped = handler;
|
|
130
|
+
set.add(wrapped);
|
|
131
|
+
return () => {
|
|
132
|
+
set.delete(wrapped);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function dispatchClientHook(name, payload) {
|
|
136
|
+
const set = handlerSets[name];
|
|
137
|
+
if (!set || set.size === 0) return;
|
|
138
|
+
const handlers = [...set];
|
|
139
|
+
for (const handler of handlers) {
|
|
140
|
+
try {
|
|
141
|
+
const result = handler(payload);
|
|
142
|
+
if (result && typeof result.catch === "function") {
|
|
143
|
+
result.catch((error) => {
|
|
144
|
+
getLogger().error(`[clientHook:${name}] async handler rejected`, error);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
getLogger().error(`[clientHook:${name}] handler threw`, error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function dispatchVetoableClientHook(name, payload) {
|
|
153
|
+
const set = handlerSets[name];
|
|
154
|
+
if (!set || set.size === 0) return { stopped: false };
|
|
155
|
+
const handlers = [...set];
|
|
156
|
+
for (const handler of handlers) {
|
|
157
|
+
let result;
|
|
158
|
+
try {
|
|
159
|
+
result = await handler(payload);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
getLogger().error(`[clientHook:${name}] vetoable handler threw`, error);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (result?.stop) {
|
|
165
|
+
return { stopped: true, signal: result };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { stopped: false };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/offlineQueue.ts
|
|
172
|
+
var apiQueue = [];
|
|
173
|
+
var syncQueue = [];
|
|
174
|
+
var isFlushingApi = false;
|
|
175
|
+
var isFlushingSync = false;
|
|
176
|
+
var isOnline = () => {
|
|
177
|
+
if (typeof navigator === "undefined") return true;
|
|
178
|
+
return navigator.onLine;
|
|
179
|
+
};
|
|
180
|
+
var notifyDropped = (item, reason) => {
|
|
181
|
+
if (!item.onDrop) return;
|
|
182
|
+
tryCatchSync(() => {
|
|
183
|
+
item.onDrop?.(reason);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
var safeDispatchDropHook = (queue, key, reason, dropPolicy) => {
|
|
187
|
+
tryCatchSync(() => {
|
|
188
|
+
dispatchClientHook("queueItemDropped", { queue, key, reason, dropPolicy });
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
var evictExpired = (queue, label) => {
|
|
192
|
+
const { maxAgeMs, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;
|
|
193
|
+
if (maxAgeMs <= 0) return;
|
|
194
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
195
|
+
for (let i = queue.length - 1; i >= 0; i -= 1) {
|
|
196
|
+
const item = queue[i];
|
|
197
|
+
if (item && item.createdAt < cutoff) {
|
|
198
|
+
queue.splice(i, 1);
|
|
199
|
+
notifyDropped(item, "expired");
|
|
200
|
+
safeDispatchDropHook(label, item.key, "expired", item.dropPolicy ?? globalPolicy);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var enqueueWithPolicy = (queue, item, label) => {
|
|
205
|
+
evictExpired(queue, label);
|
|
206
|
+
const { maxSize, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;
|
|
207
|
+
const effectivePolicy = item.dropPolicy ?? globalPolicy;
|
|
208
|
+
if (queue.length < maxSize) {
|
|
209
|
+
queue.push(item);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (effectivePolicy === "drop-oldest") {
|
|
213
|
+
const dropped = queue.shift();
|
|
214
|
+
queue.push(item);
|
|
215
|
+
getLogger().debug(`offlineQueue:${label} full \u2014 dropped oldest item (policy=${effectivePolicy})`);
|
|
216
|
+
if (dropped) {
|
|
217
|
+
notifyDropped(dropped, "queue-full");
|
|
218
|
+
safeDispatchDropHook(label, dropped.key, "queue-full", effectivePolicy);
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (effectivePolicy === "drop-newest") {
|
|
223
|
+
getLogger().debug(`offlineQueue:${label} full \u2014 dropped newest item (policy=${effectivePolicy})`);
|
|
224
|
+
notifyDropped(item, "queue-full");
|
|
225
|
+
safeDispatchDropHook(label, item.key, "queue-full", effectivePolicy);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
getLogger().warn(`offlineQueue:${label} full \u2014 rejecting enqueue (policy=${effectivePolicy})`);
|
|
229
|
+
notifyDropped(item, "queue-full");
|
|
230
|
+
safeDispatchDropHook(label, item.key, "queue-full", effectivePolicy);
|
|
231
|
+
return false;
|
|
232
|
+
};
|
|
233
|
+
var enqueueApiRequest = (item) => enqueueWithPolicy(apiQueue, item, "api");
|
|
234
|
+
var enqueueSyncRequest = (item) => enqueueWithPolicy(syncQueue, item, "sync");
|
|
235
|
+
var removeApiQueueItem = (id) => {
|
|
236
|
+
const index = apiQueue.findIndex((item) => item.id === id);
|
|
237
|
+
if (index !== -1) {
|
|
238
|
+
apiQueue.splice(index, 1);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var removeSyncQueueItem = (id) => {
|
|
242
|
+
const index = syncQueue.findIndex((item) => item.id === id);
|
|
243
|
+
if (index !== -1) {
|
|
244
|
+
syncQueue.splice(index, 1);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var removeApiQueueItemsByKey = (key) => {
|
|
248
|
+
for (let i = apiQueue.length - 1; i >= 0; i -= 1) {
|
|
249
|
+
if (apiQueue[i]?.key === key) {
|
|
250
|
+
apiQueue.splice(i, 1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var runQueueItem = (queue, item, socketInstance, label) => {
|
|
255
|
+
const [error] = tryCatchSync(() => {
|
|
256
|
+
item.run(socketInstance);
|
|
257
|
+
});
|
|
258
|
+
if (error) {
|
|
259
|
+
queue.unshift(item);
|
|
260
|
+
getLogger().warn(`offlineQueue:${label} run threw \u2014 requeued item, pausing flush`, { error });
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
};
|
|
265
|
+
var flushApiQueue = (canRun, socketInstance) => {
|
|
266
|
+
if (isFlushingApi) return;
|
|
267
|
+
evictExpired(apiQueue, "api");
|
|
268
|
+
if (apiQueue.length === 0) return;
|
|
269
|
+
if (!canRun()) return;
|
|
270
|
+
isFlushingApi = true;
|
|
271
|
+
while (apiQueue.length > 0) {
|
|
272
|
+
if (!canRun()) break;
|
|
273
|
+
const item = apiQueue.shift();
|
|
274
|
+
if (item && !runQueueItem(apiQueue, item, socketInstance, "api")) break;
|
|
275
|
+
}
|
|
276
|
+
isFlushingApi = false;
|
|
277
|
+
};
|
|
278
|
+
var flushSyncQueue = (canRun, socketInstance) => {
|
|
279
|
+
if (isFlushingSync) return;
|
|
280
|
+
evictExpired(syncQueue, "sync");
|
|
281
|
+
if (syncQueue.length === 0) return;
|
|
282
|
+
if (!canRun()) return;
|
|
283
|
+
isFlushingSync = true;
|
|
284
|
+
while (syncQueue.length > 0) {
|
|
285
|
+
if (!canRun()) break;
|
|
286
|
+
const item = syncQueue.shift();
|
|
287
|
+
if (item && !runQueueItem(syncQueue, item, socketInstance, "sync")) break;
|
|
288
|
+
}
|
|
289
|
+
isFlushingSync = false;
|
|
290
|
+
};
|
|
291
|
+
var getApiQueueSize = () => apiQueue.length;
|
|
292
|
+
var getSyncQueueSize = () => syncQueue.length;
|
|
293
|
+
|
|
294
|
+
// src/responseNormalizer.ts
|
|
295
|
+
var INVALID_ERROR_RESPONSE_CODE = "error.invalidResponse";
|
|
296
|
+
var isErrorParamArray = (value) => {
|
|
297
|
+
return Array.isArray(value) && value.every((item) => {
|
|
298
|
+
return !!item && typeof item === "object" && typeof item.key === "string" && ["string", "number", "boolean"].includes(typeof item.value);
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
var normalizeErrorResponseCore = ({
|
|
302
|
+
response,
|
|
303
|
+
fallbackHttpStatus,
|
|
304
|
+
fallbackErrorCode,
|
|
305
|
+
resolveMessage
|
|
306
|
+
}) => {
|
|
307
|
+
const normalizedParams = isErrorParamArray(response.errorParams) ? response.errorParams : void 0;
|
|
308
|
+
const errorCodeValue = typeof response.errorCode === "string" ? response.errorCode.trim() : "";
|
|
309
|
+
const hasErrorCode = errorCodeValue.length > 0;
|
|
310
|
+
const finalErrorCode = hasErrorCode ? errorCodeValue : fallbackErrorCode ?? INVALID_ERROR_RESPONSE_CODE;
|
|
311
|
+
const providedHttpStatus = typeof response.httpStatus === "number" ? response.httpStatus : void 0;
|
|
312
|
+
const message = resolveMessage ? resolveMessage({ errorCode: finalErrorCode, errorParams: normalizedParams }) : finalErrorCode;
|
|
313
|
+
return {
|
|
314
|
+
status: "error",
|
|
315
|
+
message,
|
|
316
|
+
errorCode: finalErrorCode,
|
|
317
|
+
errorParams: normalizedParams,
|
|
318
|
+
httpStatus: providedHttpStatus ?? fallbackHttpStatus
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
var defaultHttpStatusForResponse = ({
|
|
322
|
+
status,
|
|
323
|
+
explicitHttpStatus,
|
|
324
|
+
fallbackErrorStatus = 400
|
|
325
|
+
}) => {
|
|
326
|
+
if (typeof explicitHttpStatus === "number") return explicitHttpStatus;
|
|
327
|
+
return status === "success" ? 200 : fallbackErrorStatus;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/serviceRoute.ts
|
|
331
|
+
var VERSION_SEGMENT_REGEX = /^v\d+$/;
|
|
332
|
+
var normalizeRawRoute = (value) => {
|
|
333
|
+
return value.trim().replaceAll("\\", "/").replaceAll(/^\/+/g, "").replaceAll(/\/+$/g, "");
|
|
334
|
+
};
|
|
335
|
+
var hasEmptySegments = (value) => {
|
|
336
|
+
return value.split("/").some((segment) => segment.length === 0);
|
|
337
|
+
};
|
|
338
|
+
var parseServiceRouteName = (value) => {
|
|
339
|
+
const normalized = normalizeRawRoute(value);
|
|
340
|
+
if (!normalized) {
|
|
341
|
+
return { status: "error", reason: "Route name cannot be empty." };
|
|
342
|
+
}
|
|
343
|
+
if (hasEmptySegments(normalized)) {
|
|
344
|
+
return { status: "error", reason: "Route name cannot contain empty segments." };
|
|
345
|
+
}
|
|
346
|
+
const segments = normalized.split("/");
|
|
347
|
+
if (segments.length < 2) {
|
|
348
|
+
return { status: "error", reason: "Route name must include service and route segments." };
|
|
349
|
+
}
|
|
350
|
+
const [service, ...routeSegments] = segments;
|
|
351
|
+
const routeName = routeSegments.join("/");
|
|
352
|
+
if (!service || !routeName) {
|
|
353
|
+
return { status: "error", reason: "Route name must include service and route segments." };
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
status: "success",
|
|
357
|
+
normalizedRouteName: `${service}/${routeName}`,
|
|
358
|
+
service,
|
|
359
|
+
routeName
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
var parseTransportRouteName = ({
|
|
363
|
+
value,
|
|
364
|
+
prefix
|
|
365
|
+
}) => {
|
|
366
|
+
const normalized = normalizeRawRoute(value);
|
|
367
|
+
if (!normalized) {
|
|
368
|
+
return { status: "error", reason: "Transport route cannot be empty." };
|
|
369
|
+
}
|
|
370
|
+
if (hasEmptySegments(normalized)) {
|
|
371
|
+
return { status: "error", reason: "Transport route cannot contain empty segments." };
|
|
372
|
+
}
|
|
373
|
+
const segments = normalized.split("/");
|
|
374
|
+
const routeSegments = segments[0] === prefix ? segments.slice(1) : segments;
|
|
375
|
+
if (routeSegments.length < 3) {
|
|
376
|
+
return {
|
|
377
|
+
status: "error",
|
|
378
|
+
reason: `Transport route must use ${prefix}/{service}/{name}/{version} format.`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const version = routeSegments.at(-1);
|
|
382
|
+
if (!version || !VERSION_SEGMENT_REGEX.test(version)) {
|
|
383
|
+
return { status: "error", reason: "Route version segment must match v{number}." };
|
|
384
|
+
}
|
|
385
|
+
const serviceRouteToken = routeSegments.slice(0, -1).join("/");
|
|
386
|
+
const serviceRoute = parseServiceRouteName(serviceRouteToken);
|
|
387
|
+
if (serviceRoute.status === "error") {
|
|
388
|
+
return {
|
|
389
|
+
status: "error",
|
|
390
|
+
reason: serviceRoute.reason
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
status: "success",
|
|
395
|
+
normalizedFullName: `${prefix}/${serviceRoute.normalizedRouteName}/${version}`,
|
|
396
|
+
version,
|
|
397
|
+
serviceRoute
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// src/socketEvents.ts
|
|
402
|
+
var withIndex = (prefix, responseIndex2) => {
|
|
403
|
+
return `${prefix}${String(responseIndex2)}`;
|
|
404
|
+
};
|
|
405
|
+
var socketEventNames = {
|
|
406
|
+
apiRequest: "apiRequest",
|
|
407
|
+
apiCancel: "apiCancel",
|
|
408
|
+
sync: "sync",
|
|
409
|
+
syncCancel: "syncCancel",
|
|
410
|
+
joinRoom: "joinRoom",
|
|
411
|
+
leaveRoom: "leaveRoom",
|
|
412
|
+
getJoinedRooms: "getJoinedRooms",
|
|
413
|
+
updateLocation: "updateLocation",
|
|
414
|
+
updateSession: "updateSession",
|
|
415
|
+
sessionReplaced: "sessionReplaced",
|
|
416
|
+
logout: "logout",
|
|
417
|
+
intentionalDisconnect: "intentionalDisconnect",
|
|
418
|
+
intentionalReconnect: "intentionalReconnect",
|
|
419
|
+
//? Client -> server activity heartbeat (mouse/keyboard/touch), throttled.
|
|
420
|
+
//? Drives the server-side activity sampler that fires registered activity
|
|
421
|
+
//? events (e.g. the built-in AFK detector).
|
|
422
|
+
activity: "activity",
|
|
423
|
+
userAfk: "userAfk",
|
|
424
|
+
userBack: "userBack",
|
|
425
|
+
//? Emitted to a room's remaining members when a peer leaves — hard disconnect
|
|
426
|
+
//? (socket close) or grace-expiry (presence MIS-003). Lets clients prune a
|
|
427
|
+
//? departed peer from their roster without polling. The presence package owns
|
|
428
|
+
//? the dispatch; core owns the wire-event name so client + server can't drift.
|
|
429
|
+
userLeft: "userLeft",
|
|
430
|
+
connect: "connect",
|
|
431
|
+
disconnect: "disconnect",
|
|
432
|
+
reconnectAttempt: "reconnect_attempt",
|
|
433
|
+
connectError: "connect_error",
|
|
434
|
+
apiResponsePrefix: "apiResponse-",
|
|
435
|
+
apiStreamPrefix: "apiStream-",
|
|
436
|
+
syncResponsePrefix: "sync-",
|
|
437
|
+
syncProgressPrefix: "sync-progress-",
|
|
438
|
+
joinRoomResponsePrefix: "joinRoom-",
|
|
439
|
+
leaveRoomResponsePrefix: "leaveRoom-",
|
|
440
|
+
getJoinedRoomsResponsePrefix: "getJoinedRooms-"
|
|
441
|
+
};
|
|
442
|
+
var buildApiResponseEventName = (responseIndex2) => {
|
|
443
|
+
return withIndex(socketEventNames.apiResponsePrefix, responseIndex2);
|
|
444
|
+
};
|
|
445
|
+
var buildApiStreamEventName = (responseIndex2) => {
|
|
446
|
+
return withIndex(socketEventNames.apiStreamPrefix, responseIndex2);
|
|
447
|
+
};
|
|
448
|
+
var buildSyncResponseEventName = (responseIndex2) => {
|
|
449
|
+
return withIndex(socketEventNames.syncResponsePrefix, responseIndex2);
|
|
450
|
+
};
|
|
451
|
+
var buildSyncProgressEventName = (responseIndex2) => {
|
|
452
|
+
return withIndex(socketEventNames.syncProgressPrefix, responseIndex2);
|
|
453
|
+
};
|
|
454
|
+
var buildJoinRoomResponseEventName = (responseIndex2) => {
|
|
455
|
+
return withIndex(socketEventNames.joinRoomResponsePrefix, responseIndex2);
|
|
456
|
+
};
|
|
457
|
+
var buildLeaveRoomResponseEventName = (responseIndex2) => {
|
|
458
|
+
return withIndex(socketEventNames.leaveRoomResponsePrefix, responseIndex2);
|
|
459
|
+
};
|
|
460
|
+
var buildGetJoinedRoomsResponseEventName = (responseIndex2) => {
|
|
461
|
+
return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex2);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/sleep.ts
|
|
465
|
+
function sleep(ms) {
|
|
466
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/csrfConfig.ts
|
|
470
|
+
var DEFAULT_CSRF_CONFIG = {
|
|
471
|
+
cookieName: "csrf-token",
|
|
472
|
+
headerName: "x-csrf-token",
|
|
473
|
+
tokenLength: 32,
|
|
474
|
+
cookieOptions: {
|
|
475
|
+
sameSite: "lax",
|
|
476
|
+
secure: true,
|
|
477
|
+
httpOnly: false,
|
|
478
|
+
path: "/",
|
|
479
|
+
maxAgeMs: 24 * 60 * 60 * 1e3
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var registry3 = createRegistry(DEFAULT_CSRF_CONFIG, {
|
|
483
|
+
//? Merge shallowly with the CURRENT config; `cookieOptions` is deep-merged
|
|
484
|
+
//? so a partial override of (say) `sameSite` does not clobber `path` /
|
|
485
|
+
//? `maxAgeMs`. Last-write-wins, and successive calls accumulate.
|
|
486
|
+
transform: (input, current) => ({
|
|
487
|
+
...current,
|
|
488
|
+
...input,
|
|
489
|
+
cookieOptions: { ...current.cookieOptions, ...input.cookieOptions }
|
|
490
|
+
})
|
|
491
|
+
});
|
|
492
|
+
var registerCsrfConfig = (input) => {
|
|
493
|
+
registry3.register(input);
|
|
494
|
+
};
|
|
495
|
+
var getCsrfConfig = () => registry3.get();
|
|
496
|
+
var resetCsrfConfigForTests = () => {
|
|
497
|
+
registry3.reset();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/tryCatch.ts
|
|
501
|
+
async function tryCatch(func, params, context) {
|
|
502
|
+
try {
|
|
503
|
+
const response = await func(params);
|
|
504
|
+
return [null, response];
|
|
505
|
+
} catch (error) {
|
|
506
|
+
captureException(error, context);
|
|
507
|
+
return [error, null];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/csrf.ts
|
|
512
|
+
var cachedToken = null;
|
|
513
|
+
var inflightFetch = null;
|
|
514
|
+
var resolveBackendUrl = () => {
|
|
515
|
+
const socketUri = socket?.io.opts.hostname ? `${socket.io.opts.secure ? "https" : "http"}://${socket.io.opts.hostname}${socket.io.opts.port ? `:${socket.io.opts.port}` : ""}` : "";
|
|
516
|
+
if (socketUri) return socketUri;
|
|
517
|
+
if (globalThis.window !== void 0 && globalThis.location?.origin) {
|
|
518
|
+
return globalThis.location.origin;
|
|
519
|
+
}
|
|
520
|
+
return "";
|
|
521
|
+
};
|
|
522
|
+
var fetchCsrfToken = async () => {
|
|
523
|
+
const base = resolveBackendUrl();
|
|
524
|
+
const [error, token] = await tryCatch(async () => {
|
|
525
|
+
const response = await fetch(`${base}/auth/csrf`, {
|
|
526
|
+
method: "GET",
|
|
527
|
+
credentials: "include"
|
|
528
|
+
});
|
|
529
|
+
if (!response.ok) return null;
|
|
530
|
+
const body = await response.json();
|
|
531
|
+
return body.csrfToken ?? null;
|
|
532
|
+
});
|
|
533
|
+
if (error) return null;
|
|
534
|
+
return token ?? null;
|
|
535
|
+
};
|
|
536
|
+
var getCsrfToken = async () => {
|
|
537
|
+
if (getProjectConfig().session.basedToken) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
if (cachedToken) return cachedToken;
|
|
541
|
+
if (inflightFetch) return inflightFetch;
|
|
542
|
+
inflightFetch = fetchCsrfToken().then((token) => {
|
|
543
|
+
cachedToken = token;
|
|
544
|
+
inflightFetch = null;
|
|
545
|
+
return token;
|
|
546
|
+
});
|
|
547
|
+
return inflightFetch;
|
|
548
|
+
};
|
|
549
|
+
var clearCsrfToken = () => {
|
|
550
|
+
cachedToken = null;
|
|
551
|
+
inflightFetch = null;
|
|
552
|
+
};
|
|
553
|
+
var STATE_CHANGING = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
554
|
+
var isCsrfMismatchResponse = async (response) => {
|
|
555
|
+
if (response.status !== 403) return false;
|
|
556
|
+
const [error, isMismatch] = await tryCatch(async () => {
|
|
557
|
+
const cloned = response.clone();
|
|
558
|
+
const body = await cloned.json();
|
|
559
|
+
return body.errorCode === "auth.csrfMismatch";
|
|
560
|
+
});
|
|
561
|
+
if (error) return false;
|
|
562
|
+
return isMismatch ?? false;
|
|
563
|
+
};
|
|
564
|
+
var httpFetch = async (input, init = {}) => {
|
|
565
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
566
|
+
const headers = new Headers(init.headers);
|
|
567
|
+
if (!headers.has("Content-Type") && init.body && typeof init.body === "string") {
|
|
568
|
+
headers.set("Content-Type", "application/json");
|
|
569
|
+
}
|
|
570
|
+
const csrfHeaderName = getCsrfConfig().headerName;
|
|
571
|
+
const send = async (csrfToken) => {
|
|
572
|
+
const finalHeaders = new Headers(headers);
|
|
573
|
+
if (csrfToken && STATE_CHANGING.has(method) && !finalHeaders.has(csrfHeaderName)) {
|
|
574
|
+
finalHeaders.set(csrfHeaderName, csrfToken);
|
|
575
|
+
}
|
|
576
|
+
return fetch(input, {
|
|
577
|
+
...init,
|
|
578
|
+
method,
|
|
579
|
+
headers: finalHeaders,
|
|
580
|
+
credentials: init.credentials ?? "include"
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
if (!STATE_CHANGING.has(method) || getProjectConfig().session.basedToken) {
|
|
584
|
+
return send(null);
|
|
585
|
+
}
|
|
586
|
+
const token = await getCsrfToken();
|
|
587
|
+
let response = await send(token);
|
|
588
|
+
if (await isCsrfMismatchResponse(response)) {
|
|
589
|
+
clearCsrfToken();
|
|
590
|
+
const refreshed = await getCsrfToken();
|
|
591
|
+
if (refreshed) {
|
|
592
|
+
response = await send(refreshed);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return response;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/pageRouteValidation.ts
|
|
599
|
+
var DEFAULT_PAGE_ROUTE_RULES = {
|
|
600
|
+
privateFolderPrefix: "_",
|
|
601
|
+
scaffoldIgnoredFolders: [
|
|
602
|
+
"_api",
|
|
603
|
+
"_sync",
|
|
604
|
+
"_function",
|
|
605
|
+
"_functions",
|
|
606
|
+
"_component",
|
|
607
|
+
"_components",
|
|
608
|
+
"_provider",
|
|
609
|
+
"_providers",
|
|
610
|
+
"_locale",
|
|
611
|
+
"_locales",
|
|
612
|
+
"_socket",
|
|
613
|
+
"_sockets",
|
|
614
|
+
"_shared",
|
|
615
|
+
"_server"
|
|
616
|
+
]
|
|
617
|
+
};
|
|
618
|
+
var validatePagePath = (srcRelativePath, rules = DEFAULT_PAGE_ROUTE_RULES) => {
|
|
619
|
+
const normalized = srcRelativePath.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
620
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0);
|
|
621
|
+
if (segments.length === 0) {
|
|
622
|
+
return { valid: false, reason: "empty path" };
|
|
623
|
+
}
|
|
624
|
+
const filename = segments.at(-1);
|
|
625
|
+
if (filename !== "page.tsx" && filename !== "page.jsx") {
|
|
626
|
+
return { valid: false, reason: "not a page file (expected page.tsx)" };
|
|
627
|
+
}
|
|
628
|
+
const folderSegments = segments.slice(0, -1);
|
|
629
|
+
const prefix = rules.privateFolderPrefix;
|
|
630
|
+
const reserved = rules.scaffoldIgnoredFolders;
|
|
631
|
+
for (const reservedName of reserved) {
|
|
632
|
+
if (folderSegments.includes(reservedName)) {
|
|
633
|
+
return { valid: false, reason: `page.tsx cannot live inside reserved folder ${reservedName}` };
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const visibleSegments = folderSegments.filter((segment) => !segment.startsWith(prefix));
|
|
637
|
+
if (folderSegments.length > 0 && visibleSegments.length === 0) {
|
|
638
|
+
return { valid: false, reason: "no URL segment left after stripping underscore folders" };
|
|
639
|
+
}
|
|
640
|
+
const route = visibleSegments.length === 0 ? "/" : `/${visibleSegments.join("/")}`;
|
|
641
|
+
return { valid: true, route };
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
export {
|
|
645
|
+
registerApiMethodMap,
|
|
646
|
+
getRegisteredApiMethod,
|
|
647
|
+
isApiMethodMapRegistered,
|
|
648
|
+
isGetMethodName,
|
|
649
|
+
inferHttpMethod,
|
|
650
|
+
getEffectiveHttpMethod,
|
|
651
|
+
isMethodAllowed,
|
|
652
|
+
socket,
|
|
653
|
+
setSocket,
|
|
654
|
+
incrementResponseIndex,
|
|
655
|
+
waitForSocket,
|
|
656
|
+
registerNotifier,
|
|
657
|
+
getNotifier,
|
|
658
|
+
notify,
|
|
659
|
+
tryCatchSync,
|
|
660
|
+
registerClientHook,
|
|
661
|
+
dispatchClientHook,
|
|
662
|
+
dispatchVetoableClientHook,
|
|
663
|
+
isOnline,
|
|
664
|
+
enqueueApiRequest,
|
|
665
|
+
enqueueSyncRequest,
|
|
666
|
+
removeApiQueueItem,
|
|
667
|
+
removeSyncQueueItem,
|
|
668
|
+
removeApiQueueItemsByKey,
|
|
669
|
+
flushApiQueue,
|
|
670
|
+
flushSyncQueue,
|
|
671
|
+
getApiQueueSize,
|
|
672
|
+
getSyncQueueSize,
|
|
673
|
+
INVALID_ERROR_RESPONSE_CODE,
|
|
674
|
+
isErrorParamArray,
|
|
675
|
+
normalizeErrorResponseCore,
|
|
676
|
+
defaultHttpStatusForResponse,
|
|
677
|
+
parseServiceRouteName,
|
|
678
|
+
parseTransportRouteName,
|
|
679
|
+
socketEventNames,
|
|
680
|
+
buildApiResponseEventName,
|
|
681
|
+
buildApiStreamEventName,
|
|
682
|
+
buildSyncResponseEventName,
|
|
683
|
+
buildSyncProgressEventName,
|
|
684
|
+
buildJoinRoomResponseEventName,
|
|
685
|
+
buildLeaveRoomResponseEventName,
|
|
686
|
+
buildGetJoinedRoomsResponseEventName,
|
|
687
|
+
sleep,
|
|
688
|
+
DEFAULT_CSRF_CONFIG,
|
|
689
|
+
registerCsrfConfig,
|
|
690
|
+
getCsrfConfig,
|
|
691
|
+
resetCsrfConfigForTests,
|
|
692
|
+
tryCatch,
|
|
693
|
+
getCsrfToken,
|
|
694
|
+
clearCsrfToken,
|
|
695
|
+
httpFetch,
|
|
696
|
+
DEFAULT_PAGE_ROUTE_RULES,
|
|
697
|
+
validatePagePath
|
|
698
|
+
};
|
|
699
|
+
//# sourceMappingURL=chunk-OZTB2RL2.js.map
|