@rebasepro/server 0.9.1-canary.09aaf62 → 0.9.1-canary.0fce67c
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/cron/cron-store.d.ts +0 -18
- package/dist/env.d.ts +0 -3
- package/dist/index.es.js +14 -366
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +0 -21
- package/dist/storage/LocalStorageController.d.ts +2 -19
- package/dist/storage/routes.d.ts +1 -8
- package/dist/storage/tus-handler.d.ts +1 -21
- package/dist/storage/types.d.ts +0 -33
- package/package.json +5 -5
|
@@ -28,23 +28,5 @@ export interface CronStore {
|
|
|
28
28
|
totalFailures: number;
|
|
29
29
|
lastRunAt?: string;
|
|
30
30
|
}>>;
|
|
31
|
-
/**
|
|
32
|
-
* Atomically claim a scheduled run slot for a job.
|
|
33
|
-
*
|
|
34
|
-
* `slot` is the *scheduled* fire time (ISO string) derived from the cron
|
|
35
|
-
* expression — deterministic across instances regardless of timer drift,
|
|
36
|
-
* so all instances contend on the same (jobId, slot) key. Exactly one
|
|
37
|
-
* caller wins the insert against the unique constraint and executes;
|
|
38
|
-
* the rest skip.
|
|
39
|
-
*
|
|
40
|
-
* Fails open (returns true) on unexpected store errors, so a broken
|
|
41
|
-
* claims table degrades to uncoordinated execution rather than silently
|
|
42
|
-
* never running jobs.
|
|
43
|
-
*
|
|
44
|
-
* Optional so custom stores written against the pre-claims interface
|
|
45
|
-
* keep working — the scheduler treats a missing implementation as
|
|
46
|
-
* uncoordinated (always run).
|
|
47
|
-
*/
|
|
48
|
-
tryClaimRun?(jobId: string, slot: string): Promise<boolean>;
|
|
49
31
|
}
|
|
50
32
|
export declare function createCronStore(driver: DataDriver): CronStore | undefined;
|
package/dist/env.d.ts
CHANGED
|
@@ -55,9 +55,6 @@ declare const rebaseEnvSchema: z.ZodObject<{
|
|
|
55
55
|
true: "true";
|
|
56
56
|
false: "false";
|
|
57
57
|
}>>, z.ZodTransform<boolean, "" | "true" | "false" | undefined>>;
|
|
58
|
-
GCS_BUCKET: z.ZodOptional<z.ZodString>;
|
|
59
|
-
GCS_PROJECT_ID: z.ZodOptional<z.ZodString>;
|
|
60
|
-
GCS_KEY_FILENAME: z.ZodOptional<z.ZodString>;
|
|
61
58
|
}, z.core.$strip>;
|
|
62
59
|
/** Inferred type of the validated environment. */
|
|
63
60
|
export type RebaseEnv = z.infer<typeof rebaseEnvSchema>;
|
package/dist/index.es.js
CHANGED
|
@@ -11623,21 +11623,11 @@ var LocalStorageController = class {
|
|
|
11623
11623
|
}
|
|
11624
11624
|
}
|
|
11625
11625
|
/**
|
|
11626
|
-
* Get the full filesystem path for a storage path
|
|
11627
|
-
*
|
|
11628
|
-
*
|
|
11629
|
-
* Defaults the bucket the way `putObject` does.
|
|
11630
|
-
*
|
|
11631
|
-
* `putObject` has always written into `default` when given no bucket, while
|
|
11632
|
-
* the read side resolved a bare key against the storage root — where
|
|
11633
|
-
* nothing is. The two disagreed silently: `getObject` returned null (reads
|
|
11634
|
-
* as "file missing"), `deleteObject` deleted nothing (404s are swallowed by
|
|
11635
|
-
* design), and `listObjects` returned an empty page. So the obvious
|
|
11636
|
-
* `putObject({ key })` → `getObject(key)` did not round-trip and nothing
|
|
11637
|
-
* said why. One default, applied everywhere, removes the whole class.
|
|
11626
|
+
* Get the full filesystem path for a storage path.
|
|
11627
|
+
* Includes a path traversal guard to prevent escaping the base directory.
|
|
11638
11628
|
*/
|
|
11639
11629
|
getFullPath(storagePath, bucket) {
|
|
11640
|
-
const bucketPath = path$1.join(this.basePath, bucket
|
|
11630
|
+
const bucketPath = bucket ? path$1.join(this.basePath, bucket) : this.basePath;
|
|
11641
11631
|
const resolved = path$1.resolve(path$1.join(bucketPath, storagePath));
|
|
11642
11632
|
if (!resolved.startsWith(bucketPath + path$1.sep) && resolved !== bucketPath) throw new Error("Path traversal detected: resolved storage path is outside the bucket directory.");
|
|
11643
11633
|
return resolved;
|
|
@@ -11798,10 +11788,8 @@ var LocalStorageController = class {
|
|
|
11798
11788
|
let count = 0;
|
|
11799
11789
|
const maxResults = options?.maxResults ?? 1e3;
|
|
11800
11790
|
const startIndex = options?.pageToken ? parseInt(options.pageToken, 10) : 0;
|
|
11801
|
-
let scanned = startIndex;
|
|
11802
11791
|
for (let i = startIndex; i < entries.length && count < maxResults; i++) {
|
|
11803
11792
|
const entry = entries[i];
|
|
11804
|
-
scanned = i + 1;
|
|
11805
11793
|
if (entry.name.endsWith(".metadata.json")) continue;
|
|
11806
11794
|
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
11807
11795
|
const bucket = options?.bucket ?? "default";
|
|
@@ -11820,7 +11808,7 @@ var LocalStorageController = class {
|
|
|
11820
11808
|
return {
|
|
11821
11809
|
items,
|
|
11822
11810
|
prefixes,
|
|
11823
|
-
nextPageToken:
|
|
11811
|
+
nextPageToken: startIndex + count < entries.length ? String(startIndex + count) : void 0
|
|
11824
11812
|
};
|
|
11825
11813
|
} catch (error) {
|
|
11826
11814
|
const code = error?.code;
|
|
@@ -13771,14 +13759,12 @@ var UPLOAD_EXPIRY_MS = 1440 * 60 * 1e3;
|
|
|
13771
13759
|
var TusHandler = class {
|
|
13772
13760
|
storageController;
|
|
13773
13761
|
storageRegistry;
|
|
13774
|
-
authorizeUpload;
|
|
13775
13762
|
uploads = /* @__PURE__ */ new Map();
|
|
13776
13763
|
tusDir;
|
|
13777
13764
|
cleanupTimer;
|
|
13778
|
-
constructor(storageBaseDir, storageController, storageRegistry
|
|
13765
|
+
constructor(storageBaseDir, storageController, storageRegistry) {
|
|
13779
13766
|
this.storageController = storageController;
|
|
13780
13767
|
this.storageRegistry = storageRegistry;
|
|
13781
|
-
this.authorizeUpload = authorizeUpload;
|
|
13782
13768
|
this.tusDir = join(storageBaseDir, ".tus-uploads");
|
|
13783
13769
|
}
|
|
13784
13770
|
/** Ensure the temp directory exists. */
|
|
@@ -13842,10 +13828,6 @@ var TusHandler = class {
|
|
|
13842
13828
|
if (Number.isNaN(uploadLength) || uploadLength <= 0) throw ApiError.badRequest("Invalid Upload-Length");
|
|
13843
13829
|
if (uploadLength > MAX_UPLOAD_SIZE) throw new ApiError(413, "PAYLOAD_TOO_LARGE", `Upload-Length exceeds maximum of ${MAX_UPLOAD_SIZE} bytes`);
|
|
13844
13830
|
const metadata = this.parseMetadata(c.req.header("Upload-Metadata") || "");
|
|
13845
|
-
if (this.authorizeUpload) {
|
|
13846
|
-
const key = metadata.key || metadata.filename || "";
|
|
13847
|
-
await this.authorizeUpload(c, key, metadata.bucket || "default");
|
|
13848
|
-
}
|
|
13849
13831
|
const id = randomUUID$1();
|
|
13850
13832
|
const filePath = join(this.tusDir, id);
|
|
13851
13833
|
await writeFile(filePath, Buffer.alloc(0));
|
|
@@ -14053,34 +14035,7 @@ function buildAdapterAuthMiddleware(adapter, requireAuth, publicRead) {
|
|
|
14053
14035
|
function createStorageRoutes(config) {
|
|
14054
14036
|
const router = new Hono();
|
|
14055
14037
|
router.onError(errorHandler);
|
|
14056
|
-
const { controller, registry, sources: declaredSources, requireAuth: requireAuth$1 = true, publicRead = false, authAdapter
|
|
14057
|
-
/**
|
|
14058
|
-
* Run the per-object authorization hook, if one is configured.
|
|
14059
|
-
*
|
|
14060
|
-
* Denials are 403 rather than 404: the route already established that the
|
|
14061
|
-
* caller is authenticated, so hiding existence buys nothing, and a
|
|
14062
|
-
* distinguishable status is what makes a misconfigured policy debuggable.
|
|
14063
|
-
* A hook that throws denies too — an ownership lookup that fails must not
|
|
14064
|
-
* fall open.
|
|
14065
|
-
*/
|
|
14066
|
-
const checkAuthorized = async (c, operation, key, bucket, storageId) => {
|
|
14067
|
-
if (!authorize) return;
|
|
14068
|
-
const user = c.get("user") ?? null;
|
|
14069
|
-
if (user?.userId === "download-token" || user?.userId === "public") return;
|
|
14070
|
-
let allowed;
|
|
14071
|
-
try {
|
|
14072
|
-
allowed = await authorize({
|
|
14073
|
-
key,
|
|
14074
|
-
bucket,
|
|
14075
|
-
operation,
|
|
14076
|
-
user,
|
|
14077
|
-
storageId: storageId ?? void 0
|
|
14078
|
-
});
|
|
14079
|
-
} catch {
|
|
14080
|
-
allowed = false;
|
|
14081
|
-
}
|
|
14082
|
-
if (!allowed) throw ApiError.forbidden("Not authorized for this object");
|
|
14083
|
-
};
|
|
14038
|
+
const { controller, registry, sources: declaredSources, requireAuth: requireAuth$1 = true, publicRead = false, authAdapter } = config;
|
|
14084
14039
|
/**
|
|
14085
14040
|
* Resolve the storage controller for a request.
|
|
14086
14041
|
* Looks up by `storageId` in the registry, falls back to the single
|
|
@@ -14130,7 +14085,6 @@ function createStorageRoutes(config) {
|
|
|
14130
14085
|
const finalKey = sanitizeStorageKey(key || uploadedFile.name || "unnamed");
|
|
14131
14086
|
const metadata = {};
|
|
14132
14087
|
for (const [k, value] of Object.entries(body)) if (k.startsWith("metadata_")) metadata[k.replace("metadata_", "")] = value;
|
|
14133
|
-
await checkAuthorized(c, "write", finalKey, bucket ?? "default", storageId);
|
|
14134
14088
|
const result = await resolveController(storageId).putObject({
|
|
14135
14089
|
file: uploadedFile,
|
|
14136
14090
|
key: finalKey,
|
|
@@ -14151,12 +14105,7 @@ function createStorageRoutes(config) {
|
|
|
14151
14105
|
const rawPath = extractWildcardPath(c);
|
|
14152
14106
|
if (!rawPath) throw ApiError.notFound("File not found");
|
|
14153
14107
|
const filePath = decodeURIComponent(rawPath);
|
|
14154
|
-
const
|
|
14155
|
-
const resolved = resolveController(storageId);
|
|
14156
|
-
{
|
|
14157
|
-
const { bucket, resolvedPath } = parseBucketAndPath(filePath);
|
|
14158
|
-
await checkAuthorized(c, "read", resolvedPath, bucket, storageId);
|
|
14159
|
-
}
|
|
14108
|
+
const resolved = resolveController(c.req.query("storageId"));
|
|
14160
14109
|
const transformOpts = parseTransformOptions(c.req.query());
|
|
14161
14110
|
if (resolved.getType() === "local") {
|
|
14162
14111
|
const localController = resolved;
|
|
@@ -14219,10 +14168,8 @@ function createStorageRoutes(config) {
|
|
|
14219
14168
|
fileNotFound: true
|
|
14220
14169
|
}, 404);
|
|
14221
14170
|
const filePath = decodeURIComponent(rawPath);
|
|
14222
|
-
const
|
|
14223
|
-
const resolved = resolveController(storageId);
|
|
14171
|
+
const resolved = resolveController(c.req.query("storageId"));
|
|
14224
14172
|
const { bucket, resolvedPath } = parseBucketAndPath(filePath);
|
|
14225
|
-
await checkAuthorized(c, "read", resolvedPath, bucket, storageId);
|
|
14226
14173
|
const downloadConfig = await resolved.getSignedUrl(resolvedPath, bucket);
|
|
14227
14174
|
if (downloadConfig.fileNotFound) throw ApiError.notFound("File not found");
|
|
14228
14175
|
if (downloadConfig.metadata) {
|
|
@@ -14248,10 +14195,8 @@ function createStorageRoutes(config) {
|
|
|
14248
14195
|
message: "No file to delete"
|
|
14249
14196
|
});
|
|
14250
14197
|
const filePath = decodeURIComponent(rawPath);
|
|
14251
|
-
const
|
|
14252
|
-
const resolved = resolveController(storageId);
|
|
14198
|
+
const resolved = resolveController(c.req.query("storageId"));
|
|
14253
14199
|
const { bucket, resolvedPath } = parseBucketAndPath(filePath);
|
|
14254
|
-
await checkAuthorized(c, "delete", resolvedPath, bucket, storageId);
|
|
14255
14200
|
await resolved.deleteObject(resolvedPath, bucket);
|
|
14256
14201
|
return c.json({
|
|
14257
14202
|
success: true,
|
|
@@ -14266,9 +14211,7 @@ function createStorageRoutes(config) {
|
|
|
14266
14211
|
const bucket = c.req.query("bucket");
|
|
14267
14212
|
const maxResults = c.req.query("maxResults");
|
|
14268
14213
|
const pageToken = c.req.query("pageToken");
|
|
14269
|
-
const
|
|
14270
|
-
const resolved = resolveController(storageId);
|
|
14271
|
-
await checkAuthorized(c, "list", storagePrefix, bucket ?? "default", storageId);
|
|
14214
|
+
const resolved = resolveController(c.req.query("storageId"));
|
|
14272
14215
|
const result = await resolved.listObjects(storagePrefix, {
|
|
14273
14216
|
bucket: bucket ?? (resolved.getType() === "local" ? "default" : void 0),
|
|
14274
14217
|
maxResults: maxResults ? parseInt(maxResults, 10) : void 0,
|
|
@@ -14291,7 +14234,6 @@ function createStorageRoutes(config) {
|
|
|
14291
14234
|
const resolved = resolveController(storageId);
|
|
14292
14235
|
const { bucket, resolvedPath } = parseBucketAndPath(folderPath);
|
|
14293
14236
|
if (!resolvedPath || resolvedPath.trim() === "") throw ApiError.badRequest("Invalid folder path");
|
|
14294
|
-
await checkAuthorized(c, "write", resolvedPath, bucket, storageId);
|
|
14295
14237
|
if (resolved.getType() === "local") {
|
|
14296
14238
|
const absolutePath = resolved.getAbsolutePath(resolvedPath, bucket);
|
|
14297
14239
|
fs$1.mkdirSync(absolutePath, { recursive: true });
|
|
@@ -14309,9 +14251,7 @@ function createStorageRoutes(config) {
|
|
|
14309
14251
|
}, 201);
|
|
14310
14252
|
});
|
|
14311
14253
|
const defaultCtrl = getDefaultController();
|
|
14312
|
-
const tusHandler = new TusHandler(defaultCtrl.getType() === "local" ? defaultCtrl.getBasePath() : process.env.STORAGE_PATH || "./uploads", defaultCtrl, registry
|
|
14313
|
-
await checkAuthorized(c, "write", sanitizeStorageKey(key), bucket, c.req.query("storageId"));
|
|
14314
|
-
} : void 0);
|
|
14254
|
+
const tusHandler = new TusHandler(defaultCtrl.getType() === "local" ? defaultCtrl.getBasePath() : process.env.STORAGE_PATH || "./uploads", defaultCtrl, registry);
|
|
14315
14255
|
tusHandler.startCleanup();
|
|
14316
14256
|
router.options("/tus", (_c) => tusHandler.options());
|
|
14317
14257
|
router.post("/tus", writeAuthMiddleware, async (c) => tusHandler.create(c));
|
|
@@ -14452,7 +14392,7 @@ async function initializeStorage(storageConfig, isProduction) {
|
|
|
14452
14392
|
const toController = async (entry, label) => {
|
|
14453
14393
|
if (typeof entry.putObject === "function") return entry;
|
|
14454
14394
|
const conf = entry;
|
|
14455
|
-
if (isProduction && conf.type === "local"
|
|
14395
|
+
if (isProduction && conf.type === "local") logger.warn(`Storage backend "${label}" uses local filesystem in production. Files will be lost on container restart. Configure S3-compatible storage or a custom StorageController.`);
|
|
14456
14396
|
return await createStorageController(conf);
|
|
14457
14397
|
};
|
|
14458
14398
|
if (typeof storageConfig === "object" && ("type" in storageConfig || typeof storageConfig.putObject === "function")) controllers[DEFAULT_STORAGE_ID] = await toController(storageConfig, DEFAULT_STORAGE_ID);
|
|
@@ -16015,23 +15955,6 @@ var RebaseWebSocketClient = class {
|
|
|
16015
15955
|
getAuthToken;
|
|
16016
15956
|
subscriptions = /* @__PURE__ */ new Map();
|
|
16017
15957
|
listeners = /* @__PURE__ */ new Map();
|
|
16018
|
-
/** Channel-name → handlers, for broadcast and presence frames. */
|
|
16019
|
-
channelHandlers = /* @__PURE__ */ new Map();
|
|
16020
|
-
/** Subscribe to broadcast/presence frames for one channel. */
|
|
16021
|
-
onChannelMessage(channel, handler) {
|
|
16022
|
-
if (!this.channelHandlers.has(channel)) this.channelHandlers.set(channel, /* @__PURE__ */ new Set());
|
|
16023
|
-
this.channelHandlers.get(channel).add(handler);
|
|
16024
|
-
return () => {
|
|
16025
|
-
const handlers = this.channelHandlers.get(channel);
|
|
16026
|
-
if (!handlers) return;
|
|
16027
|
-
handlers.delete(handler);
|
|
16028
|
-
if (handlers.size === 0) this.channelHandlers.delete(channel);
|
|
16029
|
-
};
|
|
16030
|
-
}
|
|
16031
|
-
/** Notified after the socket comes back, so channels can re-join. */
|
|
16032
|
-
onReconnect(handler) {
|
|
16033
|
-
return this.on("reconnect", handler);
|
|
16034
|
-
}
|
|
16035
15958
|
on(event, cb) {
|
|
16036
15959
|
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
16037
15960
|
this.listeners.get(event).add(cb);
|
|
@@ -16298,15 +16221,6 @@ var RebaseWebSocketClient = class {
|
|
|
16298
16221
|
}
|
|
16299
16222
|
return;
|
|
16300
16223
|
}
|
|
16301
|
-
if (typeof message.channel === "string" && (type === "broadcast" || type === "presence_state" || type === "presence_diff")) {
|
|
16302
|
-
const handlers = this.channelHandlers.get(message.channel);
|
|
16303
|
-
if (handlers) for (const handler of [...handlers]) try {
|
|
16304
|
-
handler(message);
|
|
16305
|
-
} catch (error) {
|
|
16306
|
-
console.error("Error in channel handler:", error);
|
|
16307
|
-
}
|
|
16308
|
-
return;
|
|
16309
|
-
}
|
|
16310
16224
|
if (subscriptionId && type === "collection_update") {
|
|
16311
16225
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
16312
16226
|
if (subscriptionKey) {
|
|
@@ -16495,10 +16409,6 @@ var RebaseWebSocketClient = class {
|
|
|
16495
16409
|
throw error;
|
|
16496
16410
|
}
|
|
16497
16411
|
}
|
|
16498
|
-
/**
|
|
16499
|
-
* Public because `RebaseRealtimeChannel` sends channel frames through it.
|
|
16500
|
-
* Not part of the stable surface — prefer `client.realtime.channel(name)`.
|
|
16501
|
-
*/
|
|
16502
16412
|
sendMessage(message) {
|
|
16503
16413
|
const queuedMsg = message;
|
|
16504
16414
|
if (queuedMsg._queuedResolve && queuedMsg._queuedReject) return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
|
|
@@ -17079,188 +16989,6 @@ var RebaseWebSocketClient = class {
|
|
|
17079
16989
|
}
|
|
17080
16990
|
};
|
|
17081
16991
|
/**
|
|
17082
|
-
* Re-send presence comfortably inside the server's 30s expiry.
|
|
17083
|
-
*
|
|
17084
|
-
* Two-thirds of the window: one lost heartbeat still leaves time for the next
|
|
17085
|
-
* before the entry is reaped, so a single dropped frame is not a disappearance.
|
|
17086
|
-
*/
|
|
17087
|
-
var PRESENCE_HEARTBEAT_MS = 2e4;
|
|
17088
|
-
var RebaseRealtimeChannel = class {
|
|
17089
|
-
name;
|
|
17090
|
-
transport;
|
|
17091
|
-
presenceHandlers = /* @__PURE__ */ new Set();
|
|
17092
|
-
broadcastHandlers = /* @__PURE__ */ new Set();
|
|
17093
|
-
unsubscribers = [];
|
|
17094
|
-
/** Last known roster, kept so handlers always get a full picture. */
|
|
17095
|
-
presences = {};
|
|
17096
|
-
/** What this client last tracked, replayed on reconnect and heartbeat. */
|
|
17097
|
-
trackedState = null;
|
|
17098
|
-
heartbeat = null;
|
|
17099
|
-
joined = false;
|
|
17100
|
-
constructor(name, transport) {
|
|
17101
|
-
this.name = name;
|
|
17102
|
-
this.transport = transport;
|
|
17103
|
-
}
|
|
17104
|
-
/**
|
|
17105
|
-
* Join the channel and ask for the current roster.
|
|
17106
|
-
*
|
|
17107
|
-
* Called automatically by `track`, `broadcast`, `onPresence` and
|
|
17108
|
-
* `onBroadcast`; calling it directly is only needed to start receiving
|
|
17109
|
-
* before there is anything to send.
|
|
17110
|
-
*/
|
|
17111
|
-
async join() {
|
|
17112
|
-
if (this.joined) return;
|
|
17113
|
-
this.joined = true;
|
|
17114
|
-
this.unsubscribers.push(this.transport.onChannelMessage(this.name, (message) => this.handle(message)));
|
|
17115
|
-
this.unsubscribers.push(this.transport.onReconnect(() => {
|
|
17116
|
-
this.rejoin();
|
|
17117
|
-
}));
|
|
17118
|
-
await this.transport.sendMessage({
|
|
17119
|
-
type: "join_channel",
|
|
17120
|
-
channel: this.name
|
|
17121
|
-
});
|
|
17122
|
-
await this.transport.sendMessage({
|
|
17123
|
-
type: "presence_state",
|
|
17124
|
-
channel: this.name
|
|
17125
|
-
});
|
|
17126
|
-
}
|
|
17127
|
-
async rejoin() {
|
|
17128
|
-
try {
|
|
17129
|
-
await this.transport.sendMessage({
|
|
17130
|
-
type: "join_channel",
|
|
17131
|
-
channel: this.name
|
|
17132
|
-
});
|
|
17133
|
-
await this.transport.sendMessage({
|
|
17134
|
-
type: "presence_state",
|
|
17135
|
-
channel: this.name
|
|
17136
|
-
});
|
|
17137
|
-
if (this.trackedState) await this.transport.sendMessage({
|
|
17138
|
-
type: "presence_track",
|
|
17139
|
-
channel: this.name,
|
|
17140
|
-
state: this.trackedState
|
|
17141
|
-
});
|
|
17142
|
-
} catch {}
|
|
17143
|
-
}
|
|
17144
|
-
/**
|
|
17145
|
-
* Publish this client's presence state, and keep publishing it.
|
|
17146
|
-
*
|
|
17147
|
-
* Calling `track` again replaces the state (and restarts the heartbeat),
|
|
17148
|
-
* which is how you update e.g. a cursor position.
|
|
17149
|
-
*/
|
|
17150
|
-
async track(state) {
|
|
17151
|
-
await this.join();
|
|
17152
|
-
this.trackedState = state;
|
|
17153
|
-
await this.transport.sendMessage({
|
|
17154
|
-
type: "presence_track",
|
|
17155
|
-
channel: this.name,
|
|
17156
|
-
state
|
|
17157
|
-
});
|
|
17158
|
-
if (!this.heartbeat) {
|
|
17159
|
-
this.heartbeat = setInterval(() => {
|
|
17160
|
-
if (!this.trackedState) return;
|
|
17161
|
-
this.transport.sendMessage({
|
|
17162
|
-
type: "presence_track",
|
|
17163
|
-
channel: this.name,
|
|
17164
|
-
state: this.trackedState
|
|
17165
|
-
}).catch(() => {});
|
|
17166
|
-
}, PRESENCE_HEARTBEAT_MS);
|
|
17167
|
-
this.heartbeat.unref?.();
|
|
17168
|
-
}
|
|
17169
|
-
}
|
|
17170
|
-
/** Stop publishing presence, without leaving the channel. */
|
|
17171
|
-
async untrack() {
|
|
17172
|
-
this.stopHeartbeat();
|
|
17173
|
-
this.trackedState = null;
|
|
17174
|
-
if (this.joined) await this.transport.sendMessage({
|
|
17175
|
-
type: "presence_untrack",
|
|
17176
|
-
channel: this.name
|
|
17177
|
-
});
|
|
17178
|
-
}
|
|
17179
|
-
/**
|
|
17180
|
-
* Observe the roster. The handler fires immediately with what is already
|
|
17181
|
-
* known, then on every change.
|
|
17182
|
-
*/
|
|
17183
|
-
onPresence(handler) {
|
|
17184
|
-
this.presenceHandlers.add(handler);
|
|
17185
|
-
this.join();
|
|
17186
|
-
if (Object.keys(this.presences).length > 0) handler({ ...this.presences });
|
|
17187
|
-
return () => this.presenceHandlers.delete(handler);
|
|
17188
|
-
}
|
|
17189
|
-
/** Send a broadcast. The sender does not receive its own message. */
|
|
17190
|
-
async broadcast(event, payload) {
|
|
17191
|
-
await this.join();
|
|
17192
|
-
await this.transport.sendMessage({
|
|
17193
|
-
type: "broadcast",
|
|
17194
|
-
channel: this.name,
|
|
17195
|
-
event,
|
|
17196
|
-
payload
|
|
17197
|
-
});
|
|
17198
|
-
}
|
|
17199
|
-
onBroadcast(eventOrHandler, maybeHandler) {
|
|
17200
|
-
const wrapped = typeof eventOrHandler === "string" ? (e) => {
|
|
17201
|
-
if (e.event === eventOrHandler) maybeHandler(e.payload);
|
|
17202
|
-
} : eventOrHandler;
|
|
17203
|
-
this.broadcastHandlers.add(wrapped);
|
|
17204
|
-
this.join();
|
|
17205
|
-
return () => this.broadcastHandlers.delete(wrapped);
|
|
17206
|
-
}
|
|
17207
|
-
/** Leave the channel and release every listener and timer. */
|
|
17208
|
-
async leave() {
|
|
17209
|
-
this.stopHeartbeat();
|
|
17210
|
-
this.trackedState = null;
|
|
17211
|
-
this.presences = {};
|
|
17212
|
-
this.presenceHandlers.clear();
|
|
17213
|
-
this.broadcastHandlers.clear();
|
|
17214
|
-
for (const off of this.unsubscribers) off();
|
|
17215
|
-
this.unsubscribers = [];
|
|
17216
|
-
if (this.joined) {
|
|
17217
|
-
this.joined = false;
|
|
17218
|
-
await this.transport.sendMessage({
|
|
17219
|
-
type: "leave_channel",
|
|
17220
|
-
channel: this.name
|
|
17221
|
-
});
|
|
17222
|
-
}
|
|
17223
|
-
}
|
|
17224
|
-
stopHeartbeat() {
|
|
17225
|
-
if (this.heartbeat) {
|
|
17226
|
-
clearInterval(this.heartbeat);
|
|
17227
|
-
this.heartbeat = null;
|
|
17228
|
-
}
|
|
17229
|
-
}
|
|
17230
|
-
/** Fold an incoming frame into the roster and fan it out. */
|
|
17231
|
-
handle(message) {
|
|
17232
|
-
switch (message.type) {
|
|
17233
|
-
case "presence_state":
|
|
17234
|
-
this.presences = message.presences ?? {};
|
|
17235
|
-
this.emitPresence();
|
|
17236
|
-
break;
|
|
17237
|
-
case "presence_diff": {
|
|
17238
|
-
const joins = message.joins ?? {};
|
|
17239
|
-
const leaves = message.leaves ?? {};
|
|
17240
|
-
for (const [id, state] of Object.entries(joins)) this.presences[id] = state;
|
|
17241
|
-
for (const id of Object.keys(leaves)) delete this.presences[id];
|
|
17242
|
-
this.emitPresence({
|
|
17243
|
-
joins,
|
|
17244
|
-
leaves
|
|
17245
|
-
});
|
|
17246
|
-
break;
|
|
17247
|
-
}
|
|
17248
|
-
case "broadcast": {
|
|
17249
|
-
const event = {
|
|
17250
|
-
event: message.event,
|
|
17251
|
-
payload: message.payload
|
|
17252
|
-
};
|
|
17253
|
-
for (const handler of this.broadcastHandlers) handler(event);
|
|
17254
|
-
break;
|
|
17255
|
-
}
|
|
17256
|
-
}
|
|
17257
|
-
}
|
|
17258
|
-
emitPresence(diff) {
|
|
17259
|
-
const snapshot = { ...this.presences };
|
|
17260
|
-
for (const handler of this.presenceHandlers) handler(snapshot, diff);
|
|
17261
|
-
}
|
|
17262
|
-
};
|
|
17263
|
-
/**
|
|
17264
16992
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
17265
16993
|
* `http://` → `ws://`, `https://` → `wss://`.
|
|
17266
16994
|
*/
|
|
@@ -17309,8 +17037,6 @@ function createRebaseClient(options) {
|
|
|
17309
17037
|
};
|
|
17310
17038
|
const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
17311
17039
|
let ws;
|
|
17312
|
-
/** One channel object per name — see `realtime.channel`. */
|
|
17313
|
-
const realtimeChannels = /* @__PURE__ */ new Map();
|
|
17314
17040
|
if (resolvedWsUrl) {
|
|
17315
17041
|
ws = new RebaseWebSocketClient({
|
|
17316
17042
|
websocketUrl: resolvedWsUrl,
|
|
@@ -17421,24 +17147,6 @@ function createRebaseClient(options) {
|
|
|
17421
17147
|
createStorageSource,
|
|
17422
17148
|
fetchStorageSources,
|
|
17423
17149
|
ws,
|
|
17424
|
-
realtime: {
|
|
17425
|
-
/**
|
|
17426
|
-
* Join a broadcast/presence channel.
|
|
17427
|
-
*
|
|
17428
|
-
* Repeated calls with the same name return the same channel, so
|
|
17429
|
-
* separate components can attach handlers without each opening its
|
|
17430
|
-
* own membership — and `leave()` from one would otherwise silently
|
|
17431
|
-
* cut off the others.
|
|
17432
|
-
*/
|
|
17433
|
-
channel: (name) => {
|
|
17434
|
-
if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
17435
|
-
let existing = realtimeChannels.get(name);
|
|
17436
|
-
if (!existing) {
|
|
17437
|
-
existing = new RebaseRealtimeChannel(name, ws);
|
|
17438
|
-
realtimeChannels.set(name, existing);
|
|
17439
|
-
}
|
|
17440
|
-
return existing;
|
|
17441
|
-
} },
|
|
17442
17150
|
/**
|
|
17443
17151
|
* Release the realtime socket and its reconnect timer.
|
|
17444
17152
|
*
|
|
@@ -17447,8 +17155,6 @@ channel: (name) => {
|
|
|
17447
17155
|
* was never started, and safe to call twice.
|
|
17448
17156
|
*/
|
|
17449
17157
|
close: () => {
|
|
17450
|
-
for (const channel of realtimeChannels.values()) channel.leave();
|
|
17451
|
-
realtimeChannels.clear();
|
|
17452
17158
|
ws?.disconnect();
|
|
17453
17159
|
},
|
|
17454
17160
|
setToken: transport.setToken,
|
|
@@ -18090,8 +17796,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
18090
17796
|
registry: storageRegistry,
|
|
18091
17797
|
sources: config.storageSources,
|
|
18092
17798
|
requireAuth: resolveRequireAuth(config.auth),
|
|
18093
|
-
authAdapter
|
|
18094
|
-
authorize: config.storageAuthorize
|
|
17799
|
+
authAdapter
|
|
18095
17800
|
});
|
|
18096
17801
|
const storageRouter = new Hono();
|
|
18097
17802
|
if (apiKeyPreAuth) storageRouter.use("/*", apiKeyPreAuth, createStorageApiKeyGuard());
|
|
@@ -18763,7 +18468,6 @@ var CronScheduler = class {
|
|
|
18763
18468
|
logger.warn("[cron] Failed to seed job stats from database", { error: err });
|
|
18764
18469
|
});
|
|
18765
18470
|
for (const [id, job] of this.jobs) if (job.enabled) this.scheduleNext(id);
|
|
18766
|
-
if (!this.store) logger.warn("[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job");
|
|
18767
18471
|
logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);
|
|
18768
18472
|
}
|
|
18769
18473
|
/**
|
|
@@ -18882,19 +18586,6 @@ var CronScheduler = class {
|
|
|
18882
18586
|
this.scheduleNext(id);
|
|
18883
18587
|
return;
|
|
18884
18588
|
}
|
|
18885
|
-
if (this.store?.tryClaimRun) {
|
|
18886
|
-
let claimed = true;
|
|
18887
|
-
try {
|
|
18888
|
-
claimed = await this.store.tryClaimRun(id, nextRun.toISOString());
|
|
18889
|
-
} catch (err) {
|
|
18890
|
-
logger.warn(`[cron] Claim check threw for "${id}" — running uncoordinated`, { error: err });
|
|
18891
|
-
}
|
|
18892
|
-
if (!claimed) {
|
|
18893
|
-
logger.info(`[cron] Slot ${nextRun.toISOString()} for "${id}" claimed by another instance — skipping`);
|
|
18894
|
-
if (this.started && job.enabled) this.scheduleNext(id);
|
|
18895
|
-
return;
|
|
18896
|
-
}
|
|
18897
|
-
}
|
|
18898
18589
|
await this.executeJob(job, false);
|
|
18899
18590
|
if (this.started && job.enabled) this.scheduleNext(id);
|
|
18900
18591
|
}, Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS));
|
|
@@ -19063,25 +18754,6 @@ function createCronRoutes(scheduler) {
|
|
|
19063
18754
|
//#region src/cron/cron-store.ts
|
|
19064
18755
|
var cron_store_exports = /* @__PURE__ */ __exportAll({ createCronStore: () => createCronStore });
|
|
19065
18756
|
var TABLE = "rebase.cron_logs";
|
|
19066
|
-
var CLAIMS_TABLE = "rebase.cron_claims";
|
|
19067
|
-
/** Claims older than this are garbage-collected on startup. */
|
|
19068
|
-
var CLAIM_RETENTION_DAYS = 7;
|
|
19069
|
-
/**
|
|
19070
|
-
* Detect a unique-constraint violation anywhere in an error's cause chain.
|
|
19071
|
-
* Drizzle wraps the PG error — match the SQLSTATE code, never message text.
|
|
19072
|
-
* Also covers SQLite ("UNIQUE constraint failed") and MySQL (ER_DUP_ENTRY 1062)
|
|
19073
|
-
* for future SQL drivers.
|
|
19074
|
-
*/
|
|
19075
|
-
function isUniqueViolation(err) {
|
|
19076
|
-
let current = err;
|
|
19077
|
-
for (let depth = 0; depth < 10 && current; depth++) if (typeof current === "object") {
|
|
19078
|
-
const e = current;
|
|
19079
|
-
if (e.code === "23505" || e.errno === 1062) return true;
|
|
19080
|
-
if (typeof e.message === "string" && e.message.includes("UNIQUE constraint failed")) return true;
|
|
19081
|
-
current = e.cause;
|
|
19082
|
-
} else break;
|
|
19083
|
-
return false;
|
|
19084
|
-
}
|
|
19085
18757
|
function createCronStore(driver) {
|
|
19086
18758
|
const admin = driver.admin;
|
|
19087
18759
|
if (!isSQLAdmin(admin)) {
|
|
@@ -19111,15 +18783,6 @@ function createCronStore(driver) {
|
|
|
19111
18783
|
CREATE INDEX IF NOT EXISTS idx_cron_logs_job
|
|
19112
18784
|
ON ${TABLE}(job_id, started_at DESC)
|
|
19113
18785
|
`);
|
|
19114
|
-
await exec(`
|
|
19115
|
-
CREATE TABLE IF NOT EXISTS ${CLAIMS_TABLE} (
|
|
19116
|
-
job_id TEXT NOT NULL,
|
|
19117
|
-
slot TIMESTAMPTZ NOT NULL,
|
|
19118
|
-
claimed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
19119
|
-
PRIMARY KEY (job_id, slot)
|
|
19120
|
-
)
|
|
19121
|
-
`);
|
|
19122
|
-
await exec(`DELETE FROM ${CLAIMS_TABLE} WHERE claimed_at < now() - make_interval(days => $1)`, { params: [CLAIM_RETENTION_DAYS] });
|
|
19123
18786
|
logger.info("✅ Cron logs table ready");
|
|
19124
18787
|
} catch (err) {
|
|
19125
18788
|
logger.error("❌ Failed to create cron logs table", { error: err });
|
|
@@ -19179,18 +18842,6 @@ function createCronStore(driver) {
|
|
|
19179
18842
|
logger.error("[cron-store] Failed to fetch job stats", { error: err });
|
|
19180
18843
|
}
|
|
19181
18844
|
return stats;
|
|
19182
|
-
},
|
|
19183
|
-
async tryClaimRun(jobId, slot) {
|
|
19184
|
-
try {
|
|
19185
|
-
return (await exec(`INSERT INTO ${CLAIMS_TABLE} (job_id, slot)
|
|
19186
|
-
VALUES ($1, $2)
|
|
19187
|
-
ON CONFLICT (job_id, slot) DO NOTHING
|
|
19188
|
-
RETURNING job_id`, { params: [jobId, slot] })).length > 0;
|
|
19189
|
-
} catch (err) {
|
|
19190
|
-
if (isUniqueViolation(err)) return false;
|
|
19191
|
-
logger.warn(`[cron-store] Claim check failed for "${jobId}" — running uncoordinated`, { error: err });
|
|
19192
|
-
return true;
|
|
19193
|
-
}
|
|
19194
18845
|
}
|
|
19195
18846
|
};
|
|
19196
18847
|
}
|
|
@@ -19490,10 +19141,7 @@ var rebaseEnvSchema = object({
|
|
|
19490
19141
|
S3_ACCESS_KEY_ID: string().optional(),
|
|
19491
19142
|
S3_SECRET_ACCESS_KEY: string().optional(),
|
|
19492
19143
|
S3_ENDPOINT: string().url().optional(),
|
|
19493
|
-
S3_FORCE_PATH_STYLE: optionalBoolString
|
|
19494
|
-
GCS_BUCKET: string().optional(),
|
|
19495
|
-
GCS_PROJECT_ID: string().optional(),
|
|
19496
|
-
GCS_KEY_FILENAME: string().optional()
|
|
19144
|
+
S3_FORCE_PATH_STYLE: optionalBoolString
|
|
19497
19145
|
});
|
|
19498
19146
|
function loadEnv(options) {
|
|
19499
19147
|
const isProduction = process.env.NODE_ENV === "production";
|