@getstrata/core 0.5.62 → 0.5.64
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 +9 -0
- package/README.md +1 -1
- package/dist/core/http/flashSession.d.ts +3 -1
- package/dist/core/jobs/dispatchWebhookJob.d.ts +1 -16
- package/dist/core/queue/redisQueue.d.ts +3 -3
- package/dist/core/runtime/appKeyPrefix.d.ts +3 -0
- package/dist/entries/cache/createCacheStore.js +24 -10
- package/dist/entries/http/flashMiddleware.js +6 -3
- package/dist/entries/http/flashSession.js +8 -3
- package/dist/entries/http/loginThrottleMiddleware.js +9 -1
- package/dist/entries/http/memoryThrottleMiddleware.js +9 -1
- package/dist/entries/http/scimThrottleMiddleware.js +9 -1
- package/dist/entries/http/throttleMiddleware.js +9 -1
- package/dist/entries/jobs/dispatchWebhookJob.js +9 -265
- package/dist/entries/queue/createAppQueue.js +25 -6
- package/dist/entries/queue/publicQueue.js +25 -6
- package/dist/entries/queue/queueMetrics.js +25 -6
- package/dist/entries/queue/redisQueue.js +23 -6
- package/dist/entries/view.js +6 -3
- package/dist/index.js +48 -22
- package/dist/modules/webhook/dispatchWebhookJob.d.ts +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.64
|
|
4
|
+
|
|
5
|
+
- `APP_KEY_PREFIX` (default `workhub`) namespaces Redis cache, queue, and throttle keys so sibling apps do not share WorkHub’s keyspace.
|
|
6
|
+
- `DispatchWebhookJob` implementation lives in the WorkHub webhook module. `@getstrata/core/jobs/dispatchWebhookJob` remains a compatibility re-export.
|
|
7
|
+
|
|
8
|
+
## 0.5.63
|
|
9
|
+
|
|
10
|
+
- Flash cookies honor `FLASH_COOKIE_NAME` (default `workhub_flash`) so sibling apps do not inherit WorkHub’s cookie name.
|
|
11
|
+
|
|
3
12
|
## 0.5.62
|
|
4
13
|
|
|
5
14
|
Laravel URL signing, Bun-native markdown mail, and session-auth cleanup.
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ import type { Migration } from "@getstrata/core/database/migrations/types";
|
|
|
82
82
|
Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
|
|
83
83
|
|
|
84
84
|
1. Add `NPM_TOKEN` to GitHub repository secrets.
|
|
85
|
-
2. Tag a release: `git tag v0.5.
|
|
85
|
+
2. Tag a release: `git tag v0.5.66 && git push origin v0.5.66`
|
|
86
86
|
3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
|
|
87
87
|
|
|
88
88
|
Previously published as `@eyk-workhub/framework@0.1.0`, deprecated in favor of this package.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
declare const FLASH_COOKIE = "workhub_flash";
|
|
2
|
+
declare function flashCookieName(): string;
|
|
1
3
|
type FlashLevel = "success" | "error" | "info";
|
|
2
4
|
interface FlashMessage {
|
|
3
5
|
level: FlashLevel;
|
|
@@ -9,4 +11,4 @@ declare function pullFlash(request: Request): FlashMessage | null;
|
|
|
9
11
|
declare function flashResponse(response: Response, message: FlashMessage): Response;
|
|
10
12
|
declare function withFlashClear(response: Response): Response;
|
|
11
13
|
export type { FlashLevel, FlashMessage };
|
|
12
|
-
export { clearFlashCookie, createFlashCookie, flashResponse, pullFlash, withFlashClear };
|
|
14
|
+
export { clearFlashCookie, createFlashCookie, FLASH_COOKIE, flashCookieName, flashResponse, pullFlash, withFlashClear, };
|
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
interface DispatchWebhookPayload {
|
|
3
|
-
webhookId: number;
|
|
4
|
-
tenantId: number;
|
|
5
|
-
event: string;
|
|
6
|
-
payload: Record<string, unknown>;
|
|
7
|
-
}
|
|
8
|
-
declare class DispatchWebhookJob extends Job<DispatchWebhookPayload> {
|
|
9
|
-
readonly maxAttempts = 3;
|
|
10
|
-
readonly backoffMs = 2000;
|
|
11
|
-
handle(payload: DispatchWebhookPayload): Promise<void>;
|
|
12
|
-
private deliver;
|
|
13
|
-
}
|
|
14
|
-
export { DispatchWebhookJob };
|
|
15
|
-
export default DispatchWebhookJob;
|
|
16
|
-
export type { DispatchWebhookPayload };
|
|
1
|
+
export { DispatchWebhookJob, type DispatchWebhookPayload, default, } from "../../modules/webhook/dispatchWebhookJob";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type FailedJobService from "./failedJobService";
|
|
2
2
|
import type { Job, Queue, QueuePriority } from "./index";
|
|
3
3
|
import { type QueueJobEnvelope } from "./jobRunner";
|
|
4
|
-
declare const QUEUE_LIST_KEY
|
|
5
|
-
declare const QUEUE_HIGH_KEY
|
|
6
|
-
declare const QUEUE_LOW_KEY
|
|
4
|
+
declare const QUEUE_LIST_KEY: string;
|
|
5
|
+
declare const QUEUE_HIGH_KEY: string;
|
|
6
|
+
declare const QUEUE_LOW_KEY: string;
|
|
7
7
|
declare function queueKeyForPriority(priority?: QueuePriority): string;
|
|
8
8
|
declare function parseQueueJobEnvelope(rawPayload: string): QueueJobEnvelope | null;
|
|
9
9
|
declare class RedisQueue implements Queue {
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/cache/redisCacheStore.ts
|
|
3
3
|
var {RedisClient } = globalThis.Bun;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
+
function appKeyPrefix() {
|
|
7
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
|
+
}
|
|
9
|
+
function namespacedRedisKey(kind) {
|
|
10
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ../../src/core/cache/redisCacheStore.ts
|
|
14
|
+
function cacheKeyPrefix() {
|
|
15
|
+
return namespacedRedisKey("cache:");
|
|
16
|
+
}
|
|
17
|
+
function cacheTagPrefix() {
|
|
18
|
+
return namespacedRedisKey("cache:tag:");
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
class RedisCacheStore {
|
|
8
22
|
ttlMs;
|
|
@@ -89,10 +103,10 @@ class RedisCacheStore {
|
|
|
89
103
|
return deleted > 0;
|
|
90
104
|
}
|
|
91
105
|
async invalidateByPrefix(prefix) {
|
|
92
|
-
const keys = await this.client.keys(`${
|
|
106
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
93
107
|
let removed = 0;
|
|
94
108
|
for (const storageKey of keys) {
|
|
95
|
-
const key = storageKey.slice(
|
|
109
|
+
const key = storageKey.slice(cacheKeyPrefix().length);
|
|
96
110
|
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
97
111
|
if (await this.invalidate(key)) {
|
|
98
112
|
removed += 1;
|
|
@@ -102,11 +116,11 @@ class RedisCacheStore {
|
|
|
102
116
|
return removed;
|
|
103
117
|
}
|
|
104
118
|
async clear() {
|
|
105
|
-
const keys = await this.client.keys(`${
|
|
119
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
106
120
|
if (keys.length > 0) {
|
|
107
121
|
await this.client.del(...keys);
|
|
108
122
|
}
|
|
109
|
-
const tagKeys = await this.client.keys(`${
|
|
123
|
+
const tagKeys = await this.client.keys(`${cacheTagPrefix()}*`);
|
|
110
124
|
if (tagKeys.length > 0) {
|
|
111
125
|
await this.client.del(...tagKeys);
|
|
112
126
|
}
|
|
@@ -114,14 +128,14 @@ class RedisCacheStore {
|
|
|
114
128
|
this.keyTags.clear();
|
|
115
129
|
}
|
|
116
130
|
async size() {
|
|
117
|
-
const keys = await this.client.keys(`${
|
|
131
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
118
132
|
return keys.length;
|
|
119
133
|
}
|
|
120
134
|
storageKey(key) {
|
|
121
|
-
return `${
|
|
135
|
+
return `${cacheKeyPrefix()}${key}`;
|
|
122
136
|
}
|
|
123
137
|
tagKey(tag) {
|
|
124
|
-
return `${
|
|
138
|
+
return `${cacheTagPrefix()}${tag}`;
|
|
125
139
|
}
|
|
126
140
|
async detachKeyFromTags(key) {
|
|
127
141
|
const tags = this.keyTags.get(key);
|
|
@@ -134,7 +148,7 @@ class RedisCacheStore {
|
|
|
134
148
|
this.keyTags.delete(key);
|
|
135
149
|
}
|
|
136
150
|
async enforceMaxEntries() {
|
|
137
|
-
const keys = await this.client.keys(`${
|
|
151
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
138
152
|
if (keys.length <= this.maxEntries) {
|
|
139
153
|
return;
|
|
140
154
|
}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
4
|
var FLASH_COOKIE = "workhub_flash";
|
|
5
5
|
var FLASH_TTL_MS = 60 * 1000;
|
|
6
|
+
function flashCookieName() {
|
|
7
|
+
return process.env.FLASH_COOKIE_NAME?.trim() || FLASH_COOKIE;
|
|
8
|
+
}
|
|
6
9
|
function resolveFlashSecret() {
|
|
7
10
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
8
11
|
}
|
|
@@ -17,7 +20,7 @@ function readFlashCookie(request) {
|
|
|
17
20
|
}
|
|
18
21
|
for (const part of cookieHeader.split(";")) {
|
|
19
22
|
const [name, ...rest] = part.trim().split("=");
|
|
20
|
-
if (name ===
|
|
23
|
+
if (name === flashCookieName()) {
|
|
21
24
|
return decodeURIComponent(rest.join("="));
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -67,10 +70,10 @@ function createFlashCookie(message) {
|
|
|
67
70
|
const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
|
|
68
71
|
const issuedAt = Date.now();
|
|
69
72
|
const value = signFlashPayload(payload, issuedAt);
|
|
70
|
-
return `${
|
|
73
|
+
return `${flashCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
|
|
71
74
|
}
|
|
72
75
|
function clearFlashCookie() {
|
|
73
|
-
return `${
|
|
76
|
+
return `${flashCookieName()}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
74
77
|
}
|
|
75
78
|
function pullFlash(request) {
|
|
76
79
|
const cookieValue = readFlashCookie(request);
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
4
|
var FLASH_COOKIE = "workhub_flash";
|
|
5
5
|
var FLASH_TTL_MS = 60 * 1000;
|
|
6
|
+
function flashCookieName() {
|
|
7
|
+
return process.env.FLASH_COOKIE_NAME?.trim() || FLASH_COOKIE;
|
|
8
|
+
}
|
|
6
9
|
function resolveFlashSecret() {
|
|
7
10
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
8
11
|
}
|
|
@@ -17,7 +20,7 @@ function readFlashCookie(request) {
|
|
|
17
20
|
}
|
|
18
21
|
for (const part of cookieHeader.split(";")) {
|
|
19
22
|
const [name, ...rest] = part.trim().split("=");
|
|
20
|
-
if (name ===
|
|
23
|
+
if (name === flashCookieName()) {
|
|
21
24
|
return decodeURIComponent(rest.join("="));
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -67,10 +70,10 @@ function createFlashCookie(message) {
|
|
|
67
70
|
const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
|
|
68
71
|
const issuedAt = Date.now();
|
|
69
72
|
const value = signFlashPayload(payload, issuedAt);
|
|
70
|
-
return `${
|
|
73
|
+
return `${flashCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
|
|
71
74
|
}
|
|
72
75
|
function clearFlashCookie() {
|
|
73
|
-
return `${
|
|
76
|
+
return `${flashCookieName()}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
74
77
|
}
|
|
75
78
|
function pullFlash(request) {
|
|
76
79
|
const cookieValue = readFlashCookie(request);
|
|
@@ -98,8 +101,10 @@ function withFlashClear(response) {
|
|
|
98
101
|
});
|
|
99
102
|
}
|
|
100
103
|
export {
|
|
104
|
+
FLASH_COOKIE,
|
|
101
105
|
clearFlashCookie,
|
|
102
106
|
createFlashCookie,
|
|
107
|
+
flashCookieName,
|
|
103
108
|
flashResponse,
|
|
104
109
|
pullFlash,
|
|
105
110
|
withFlashClear
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
3
3
|
var {RedisClient } = globalThis.Bun;
|
|
4
4
|
|
|
5
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
+
function appKeyPrefix() {
|
|
7
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
|
+
}
|
|
9
|
+
function namespacedRedisKey(kind) {
|
|
10
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
// ../../src/core/http/clientIp.ts
|
|
6
14
|
function trustForwardedFor(env = process.env) {
|
|
7
15
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
@@ -37,7 +45,7 @@ async function resolveLoginEmail(request) {
|
|
|
37
45
|
}
|
|
38
46
|
function createLoginThrottleMiddleware(options) {
|
|
39
47
|
const client = new RedisClient(options.redisUrl);
|
|
40
|
-
const prefix = options.keyPrefix ?? "
|
|
48
|
+
const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
|
|
41
49
|
return async (request, next) => {
|
|
42
50
|
const identity = resolveLoginIdentity(request);
|
|
43
51
|
const email = await resolveLoginEmail(request);
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
3
|
+
function appKeyPrefix() {
|
|
4
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
5
|
+
}
|
|
6
|
+
function namespacedRedisKey(kind) {
|
|
7
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
2
10
|
// ../../src/core/http/clientIp.ts
|
|
3
11
|
function trustForwardedFor(env = process.env) {
|
|
4
12
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
@@ -17,7 +25,7 @@ function readClientIp(request, env = process.env) {
|
|
|
17
25
|
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
18
26
|
var throttleBucketRegistries = new Set;
|
|
19
27
|
function createMemoryThrottleMiddleware(options) {
|
|
20
|
-
const prefix = options.keyPrefix ?? "
|
|
28
|
+
const prefix = options.keyPrefix ?? namespacedRedisKey("memory-throttle:");
|
|
21
29
|
const buckets = new Map;
|
|
22
30
|
throttleBucketRegistries.add(buckets);
|
|
23
31
|
return async (request, next) => {
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
3
3
|
var {RedisClient } = globalThis.Bun;
|
|
4
4
|
|
|
5
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
+
function appKeyPrefix() {
|
|
7
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
|
+
}
|
|
9
|
+
function namespacedRedisKey(kind) {
|
|
10
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
// ../../src/core/http/clientIp.ts
|
|
6
14
|
function trustForwardedFor(env = process.env) {
|
|
7
15
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
@@ -39,7 +47,7 @@ function createScimThrottleMiddleware(options) {
|
|
|
39
47
|
const client = options.redisUrl ? new RedisClient(options.redisUrl) : null;
|
|
40
48
|
return async (request, next) => {
|
|
41
49
|
const identity = resolveScimIdentity(request);
|
|
42
|
-
const key =
|
|
50
|
+
const key = `${namespacedRedisKey("scim-throttle:")}${identity}`;
|
|
43
51
|
if (client) {
|
|
44
52
|
const attempts = Number(await client.incr(key));
|
|
45
53
|
if (attempts === 1) {
|
|
@@ -4,6 +4,14 @@ import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
|
4
4
|
import { currentTenant, rateLimitMultiplierForPlan } from "@getstrata/core/tenant/tenantContext";
|
|
5
5
|
var {RedisClient } = globalThis.Bun;
|
|
6
6
|
|
|
7
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
8
|
+
function appKeyPrefix() {
|
|
9
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
10
|
+
}
|
|
11
|
+
function namespacedRedisKey(kind) {
|
|
12
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
// ../../src/core/http/clientIp.ts
|
|
8
16
|
function trustForwardedFor(env = process.env) {
|
|
9
17
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
@@ -32,7 +40,7 @@ function resolveThrottleIdentity(request) {
|
|
|
32
40
|
}
|
|
33
41
|
function createThrottleMiddleware(options) {
|
|
34
42
|
const client = new RedisClient(options.redisUrl);
|
|
35
|
-
const prefix = options.keyPrefix ?? "
|
|
43
|
+
const prefix = options.keyPrefix ?? namespacedRedisKey("throttle:");
|
|
36
44
|
return async (request, next) => {
|
|
37
45
|
const identity = resolveThrottleIdentity(request);
|
|
38
46
|
const path = new URL(request.url).pathname;
|
|
@@ -1,269 +1,13 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../src/
|
|
2
|
+
// ../../src/modules/webhook/dispatchWebhookJob.ts
|
|
3
3
|
import { createHmac } from "crypto";
|
|
4
|
-
import { repositoryConnection as db2 } from "@getstrata/core/database/repositoryConnection";
|
|
5
|
-
|
|
6
|
-
// ../../src/core/queue/index.ts
|
|
7
|
-
class Job {
|
|
8
|
-
maxAttempts;
|
|
9
|
-
backoffMs;
|
|
10
|
-
priority;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
class SyncQueue {
|
|
14
|
-
async dispatch(job, payload) {
|
|
15
|
-
await job.handle(payload);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
class AsyncQueue {
|
|
20
|
-
async dispatch(job, payload) {
|
|
21
|
-
setTimeout(() => {
|
|
22
|
-
job.handle(payload).catch((error) => {
|
|
23
|
-
console.error("[AsyncQueue] Job failed:", error);
|
|
24
|
-
});
|
|
25
|
-
}, 0);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function createQueue(driver) {
|
|
29
|
-
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ../../src/config/app.ts
|
|
33
|
-
var appConfig = {
|
|
34
|
-
name: process.env.APP_NAME?.trim() || "WorkHub",
|
|
35
|
-
env: process.env.APP_ENV ?? "local",
|
|
36
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
37
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
38
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// ../../src/core/security/safeUrl.ts
|
|
42
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
43
|
-
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
44
|
-
var dnsLookup = dnsLookupImpl;
|
|
45
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
46
|
-
"localhost",
|
|
47
|
-
"127.0.0.1",
|
|
48
|
-
"0.0.0.0",
|
|
49
|
-
"::1",
|
|
50
|
-
"metadata.google.internal"
|
|
51
|
-
]);
|
|
52
|
-
function isPrivateIpv4(hostname) {
|
|
53
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
54
|
-
if (!match) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
58
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
const [a = 0, b = 0] = octets;
|
|
62
|
-
if (a === 10) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
if (a === 127) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
if (a === 0) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
if (a === 169 && b === 254) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
if (a === 192 && b === 168) {
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
function isBlockedHostname(hostname) {
|
|
83
|
-
const normalized = hostname.trim().toLowerCase();
|
|
84
|
-
if (normalized.length === 0) {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
if (normalized.includes(":")) {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
return isPrivateIpv4(normalized);
|
|
97
|
-
}
|
|
98
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
99
|
-
let parsed;
|
|
100
|
-
try {
|
|
101
|
-
parsed = new URL(rawUrl);
|
|
102
|
-
} catch {
|
|
103
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
104
|
-
}
|
|
105
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
106
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
107
|
-
}
|
|
108
|
-
if (parsed.username || parsed.password) {
|
|
109
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
110
|
-
}
|
|
111
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
112
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
113
|
-
}
|
|
114
|
-
return parsed;
|
|
115
|
-
}
|
|
116
|
-
function isBlockedIpAddress(address) {
|
|
117
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
118
|
-
}
|
|
119
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
120
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
121
|
-
if (options.resolveDns === false) {
|
|
122
|
-
return parsed;
|
|
123
|
-
}
|
|
124
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
125
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
126
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
127
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
128
|
-
}
|
|
129
|
-
return parsed;
|
|
130
|
-
}
|
|
131
|
-
function setDnsLookupForTests(lookupFn) {
|
|
132
|
-
dnsLookup = lookupFn;
|
|
133
|
-
}
|
|
134
|
-
function resetDnsLookupForTests() {
|
|
135
|
-
dnsLookup = dnsLookupImpl;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ../../src/core/security/safeFetch.ts
|
|
139
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
140
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
141
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
142
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
143
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
144
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
145
|
-
const controller = new AbortController;
|
|
146
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
147
|
-
try {
|
|
148
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
149
|
-
let redirectCount = 0;
|
|
150
|
-
while (true) {
|
|
151
|
-
const response = await fetch(currentUrl, {
|
|
152
|
-
...init,
|
|
153
|
-
signal: controller.signal,
|
|
154
|
-
redirect: "manual"
|
|
155
|
-
});
|
|
156
|
-
if (response.status >= 300 && response.status < 400) {
|
|
157
|
-
const location = response.headers.get("location");
|
|
158
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
159
|
-
return response;
|
|
160
|
-
}
|
|
161
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
162
|
-
redirectCount += 1;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
return response;
|
|
166
|
-
}
|
|
167
|
-
} finally {
|
|
168
|
-
clearTimeout(timeout);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ../../src/core/tenant/resolveTenant.ts
|
|
173
4
|
import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
|
|
5
|
+
import { Job } from "@getstrata/core/queue";
|
|
6
|
+
import { safeFetch } from "@getstrata/core/security/safeFetch";
|
|
7
|
+
import { assertSafeOutboundUrl } from "@getstrata/core/security/safeUrl";
|
|
8
|
+
import { resolveTenant } from "@getstrata/core/tenant/resolveTenant";
|
|
9
|
+
import { runWithTenantDatabase } from "@getstrata/core/tenant/tenantDatabaseScope";
|
|
174
10
|
|
|
175
|
-
// ../../src/core/tenant/tenancyConfig.ts
|
|
176
|
-
function readTenancyDriver(env = process.env) {
|
|
177
|
-
return env.TENANCY_DRIVER === "none" ? "none" : "rls";
|
|
178
|
-
}
|
|
179
|
-
function isTenancyEnabled(env = process.env) {
|
|
180
|
-
return readTenancyDriver(env) !== "none";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ../../src/core/tenant/resolveTenant.ts
|
|
184
|
-
async function resolveTenant(tenantId) {
|
|
185
|
-
if (!isTenancyEnabled()) {
|
|
186
|
-
return {
|
|
187
|
-
id: tenantId,
|
|
188
|
-
slug: "default",
|
|
189
|
-
plan: "free",
|
|
190
|
-
region: "eu"
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
const rows = await db`
|
|
194
|
-
SELECT id, slug, plan, region
|
|
195
|
-
FROM tenant
|
|
196
|
-
WHERE id = ${tenantId}
|
|
197
|
-
LIMIT 1
|
|
198
|
-
`;
|
|
199
|
-
const row = rows[0];
|
|
200
|
-
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
204
|
-
import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
|
|
205
|
-
|
|
206
|
-
// ../../src/core/runtime/asyncContextStore.ts
|
|
207
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
208
|
-
function createAsyncContextStore(key) {
|
|
209
|
-
const symbol = Symbol.for(key);
|
|
210
|
-
const globalRecord = globalThis;
|
|
211
|
-
const existing = globalRecord[symbol];
|
|
212
|
-
if (existing) {
|
|
213
|
-
return existing;
|
|
214
|
-
}
|
|
215
|
-
const store = new AsyncLocalStorage;
|
|
216
|
-
globalRecord[symbol] = store;
|
|
217
|
-
return store;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ../../src/core/database/connectionContext.ts
|
|
221
|
-
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
222
|
-
function runWithDatabaseConnection(connection, callback) {
|
|
223
|
-
return activeConnection.run(connection, callback);
|
|
224
|
-
}
|
|
225
|
-
function getActiveDatabaseConnection(fallback) {
|
|
226
|
-
return activeConnection.getStore() ?? fallback;
|
|
227
|
-
}
|
|
228
|
-
function hasActiveDatabaseConnection() {
|
|
229
|
-
return activeConnection.getStore() !== undefined;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
233
|
-
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
234
|
-
function runWithTenant(tenant, callback) {
|
|
235
|
-
return tenantContext.run(tenant, callback);
|
|
236
|
-
}
|
|
237
|
-
function currentTenant() {
|
|
238
|
-
return tenantContext.getStore() ?? null;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
242
|
-
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
243
|
-
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
244
|
-
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
245
|
-
}
|
|
246
|
-
async function runWithTenantDatabase(tenant, callback) {
|
|
247
|
-
if (!isTenancyEnabled()) {
|
|
248
|
-
return await runWithTenant(tenant, callback);
|
|
249
|
-
}
|
|
250
|
-
if (hasActiveDatabaseConnection()) {
|
|
251
|
-
const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
252
|
-
await applyTenantContextToTransaction(activeConnection2, tenant.id);
|
|
253
|
-
return await runWithTenant(tenant, callback);
|
|
254
|
-
}
|
|
255
|
-
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
256
|
-
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
257
|
-
return await runWithDatabaseConnection(transaction, async () => {
|
|
258
|
-
return await runWithTenant(tenant, callback);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
263
|
-
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
267
11
|
class DispatchWebhookJob extends Job {
|
|
268
12
|
maxAttempts = 3;
|
|
269
13
|
backoffMs = 2000;
|
|
@@ -280,7 +24,7 @@ class DispatchWebhookJob extends Job {
|
|
|
280
24
|
}
|
|
281
25
|
}
|
|
282
26
|
async deliver(payload) {
|
|
283
|
-
const rows = await
|
|
27
|
+
const rows = await db`
|
|
284
28
|
SELECT id, url, secret
|
|
285
29
|
FROM webhook
|
|
286
30
|
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
@@ -312,7 +56,7 @@ class DispatchWebhookJob extends Job {
|
|
|
312
56
|
}
|
|
313
57
|
} catch (error) {
|
|
314
58
|
errorMessage = error instanceof Error ? error.message : String(error);
|
|
315
|
-
await
|
|
59
|
+
await db`
|
|
316
60
|
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
317
61
|
VALUES (
|
|
318
62
|
${webhook.id},
|
|
@@ -324,7 +68,7 @@ class DispatchWebhookJob extends Job {
|
|
|
324
68
|
`;
|
|
325
69
|
return error instanceof Error ? error : new Error(errorMessage);
|
|
326
70
|
}
|
|
327
|
-
await
|
|
71
|
+
await db`
|
|
328
72
|
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
329
73
|
VALUES (
|
|
330
74
|
${webhook.id},
|
|
@@ -153,18 +153,37 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
153
153
|
|
|
154
154
|
// ../../src/core/queue/redisQueue.ts
|
|
155
155
|
var {RedisClient } = globalThis.Bun;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
|
|
157
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
158
|
+
function appKeyPrefix() {
|
|
159
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
160
|
+
}
|
|
161
|
+
function namespacedRedisKey(kind) {
|
|
162
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ../../src/core/queue/redisQueue.ts
|
|
166
|
+
function queueListKey() {
|
|
167
|
+
return namespacedRedisKey("queue:default");
|
|
168
|
+
}
|
|
169
|
+
function queueHighKey() {
|
|
170
|
+
return namespacedRedisKey("queue:high");
|
|
171
|
+
}
|
|
172
|
+
function queueLowKey() {
|
|
173
|
+
return namespacedRedisKey("queue:low");
|
|
174
|
+
}
|
|
175
|
+
var QUEUE_LIST_KEY = queueListKey();
|
|
176
|
+
var QUEUE_HIGH_KEY = queueHighKey();
|
|
177
|
+
var QUEUE_LOW_KEY = queueLowKey();
|
|
159
178
|
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
160
179
|
function queueKeyForPriority(priority = "default") {
|
|
161
180
|
switch (priority) {
|
|
162
181
|
case "high":
|
|
163
|
-
return
|
|
182
|
+
return queueHighKey();
|
|
164
183
|
case "low":
|
|
165
|
-
return
|
|
184
|
+
return queueLowKey();
|
|
166
185
|
default:
|
|
167
|
-
return
|
|
186
|
+
return queueListKey();
|
|
168
187
|
}
|
|
169
188
|
}
|
|
170
189
|
function parseQueueJobEnvelope(rawPayload) {
|
|
@@ -153,18 +153,37 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
153
153
|
|
|
154
154
|
// ../../src/core/queue/redisQueue.ts
|
|
155
155
|
var {RedisClient } = globalThis.Bun;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
|
|
157
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
158
|
+
function appKeyPrefix() {
|
|
159
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
160
|
+
}
|
|
161
|
+
function namespacedRedisKey(kind) {
|
|
162
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ../../src/core/queue/redisQueue.ts
|
|
166
|
+
function queueListKey() {
|
|
167
|
+
return namespacedRedisKey("queue:default");
|
|
168
|
+
}
|
|
169
|
+
function queueHighKey() {
|
|
170
|
+
return namespacedRedisKey("queue:high");
|
|
171
|
+
}
|
|
172
|
+
function queueLowKey() {
|
|
173
|
+
return namespacedRedisKey("queue:low");
|
|
174
|
+
}
|
|
175
|
+
var QUEUE_LIST_KEY = queueListKey();
|
|
176
|
+
var QUEUE_HIGH_KEY = queueHighKey();
|
|
177
|
+
var QUEUE_LOW_KEY = queueLowKey();
|
|
159
178
|
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
160
179
|
function queueKeyForPriority(priority = "default") {
|
|
161
180
|
switch (priority) {
|
|
162
181
|
case "high":
|
|
163
|
-
return
|
|
182
|
+
return queueHighKey();
|
|
164
183
|
case "low":
|
|
165
|
-
return
|
|
184
|
+
return queueLowKey();
|
|
166
185
|
default:
|
|
167
|
-
return
|
|
186
|
+
return queueListKey();
|
|
168
187
|
}
|
|
169
188
|
}
|
|
170
189
|
function parseQueueJobEnvelope(rawPayload) {
|
|
@@ -156,18 +156,37 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
156
156
|
|
|
157
157
|
// ../../src/core/queue/redisQueue.ts
|
|
158
158
|
var {RedisClient } = globalThis.Bun;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
|
|
160
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
161
|
+
function appKeyPrefix() {
|
|
162
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
163
|
+
}
|
|
164
|
+
function namespacedRedisKey(kind) {
|
|
165
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ../../src/core/queue/redisQueue.ts
|
|
169
|
+
function queueListKey() {
|
|
170
|
+
return namespacedRedisKey("queue:default");
|
|
171
|
+
}
|
|
172
|
+
function queueHighKey() {
|
|
173
|
+
return namespacedRedisKey("queue:high");
|
|
174
|
+
}
|
|
175
|
+
function queueLowKey() {
|
|
176
|
+
return namespacedRedisKey("queue:low");
|
|
177
|
+
}
|
|
178
|
+
var QUEUE_LIST_KEY = queueListKey();
|
|
179
|
+
var QUEUE_HIGH_KEY = queueHighKey();
|
|
180
|
+
var QUEUE_LOW_KEY = queueLowKey();
|
|
162
181
|
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
163
182
|
function queueKeyForPriority(priority = "default") {
|
|
164
183
|
switch (priority) {
|
|
165
184
|
case "high":
|
|
166
|
-
return
|
|
185
|
+
return queueHighKey();
|
|
167
186
|
case "low":
|
|
168
|
-
return
|
|
187
|
+
return queueLowKey();
|
|
169
188
|
default:
|
|
170
|
-
return
|
|
189
|
+
return queueListKey();
|
|
171
190
|
}
|
|
172
191
|
}
|
|
173
192
|
function parseQueueJobEnvelope(rawPayload) {
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
// ../../src/core/queue/redisQueue.ts
|
|
3
3
|
var {RedisClient } = globalThis.Bun;
|
|
4
4
|
|
|
5
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
+
function appKeyPrefix() {
|
|
7
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
|
+
}
|
|
9
|
+
function namespacedRedisKey(kind) {
|
|
10
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
// ../../src/core/queue/jobRegistry.ts
|
|
6
14
|
class JobRegistry {
|
|
7
15
|
factories = new Map;
|
|
@@ -91,18 +99,27 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
// ../../src/core/queue/redisQueue.ts
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
function queueListKey() {
|
|
103
|
+
return namespacedRedisKey("queue:default");
|
|
104
|
+
}
|
|
105
|
+
function queueHighKey() {
|
|
106
|
+
return namespacedRedisKey("queue:high");
|
|
107
|
+
}
|
|
108
|
+
function queueLowKey() {
|
|
109
|
+
return namespacedRedisKey("queue:low");
|
|
110
|
+
}
|
|
111
|
+
var QUEUE_LIST_KEY = queueListKey();
|
|
112
|
+
var QUEUE_HIGH_KEY = queueHighKey();
|
|
113
|
+
var QUEUE_LOW_KEY = queueLowKey();
|
|
97
114
|
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
98
115
|
function queueKeyForPriority(priority = "default") {
|
|
99
116
|
switch (priority) {
|
|
100
117
|
case "high":
|
|
101
|
-
return
|
|
118
|
+
return queueHighKey();
|
|
102
119
|
case "low":
|
|
103
|
-
return
|
|
120
|
+
return queueLowKey();
|
|
104
121
|
default:
|
|
105
|
-
return
|
|
122
|
+
return queueListKey();
|
|
106
123
|
}
|
|
107
124
|
}
|
|
108
125
|
function parseQueueJobEnvelope(rawPayload) {
|
package/dist/entries/view.js
CHANGED
|
@@ -524,6 +524,9 @@ function resolveCsrfTokenForRequest(request) {
|
|
|
524
524
|
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
525
525
|
var FLASH_COOKIE = "workhub_flash";
|
|
526
526
|
var FLASH_TTL_MS = 60 * 1000;
|
|
527
|
+
function flashCookieName() {
|
|
528
|
+
return process.env.FLASH_COOKIE_NAME?.trim() || FLASH_COOKIE;
|
|
529
|
+
}
|
|
527
530
|
function resolveFlashSecret() {
|
|
528
531
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
529
532
|
}
|
|
@@ -538,7 +541,7 @@ function readFlashCookie(request) {
|
|
|
538
541
|
}
|
|
539
542
|
for (const part of cookieHeader.split(";")) {
|
|
540
543
|
const [name, ...rest] = part.trim().split("=");
|
|
541
|
-
if (name ===
|
|
544
|
+
if (name === flashCookieName()) {
|
|
542
545
|
return decodeURIComponent(rest.join("="));
|
|
543
546
|
}
|
|
544
547
|
}
|
|
@@ -588,10 +591,10 @@ function createFlashCookie(message) {
|
|
|
588
591
|
const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
|
|
589
592
|
const issuedAt = Date.now();
|
|
590
593
|
const value = signFlashPayload(payload, issuedAt);
|
|
591
|
-
return `${
|
|
594
|
+
return `${flashCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
|
|
592
595
|
}
|
|
593
596
|
function clearFlashCookie() {
|
|
594
|
-
return `${
|
|
597
|
+
return `${flashCookieName()}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
595
598
|
}
|
|
596
599
|
function pullFlash(request) {
|
|
597
600
|
const cookieValue = readFlashCookie(request);
|
package/dist/index.js
CHANGED
|
@@ -966,8 +966,22 @@ function jsonScimError(detail, status) {
|
|
|
966
966
|
}
|
|
967
967
|
// ../../src/core/cache/redisCacheStore.ts
|
|
968
968
|
var {RedisClient } = globalThis.Bun;
|
|
969
|
-
|
|
970
|
-
|
|
969
|
+
|
|
970
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
971
|
+
function appKeyPrefix() {
|
|
972
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
973
|
+
}
|
|
974
|
+
function namespacedRedisKey(kind) {
|
|
975
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ../../src/core/cache/redisCacheStore.ts
|
|
979
|
+
function cacheKeyPrefix() {
|
|
980
|
+
return namespacedRedisKey("cache:");
|
|
981
|
+
}
|
|
982
|
+
function cacheTagPrefix() {
|
|
983
|
+
return namespacedRedisKey("cache:tag:");
|
|
984
|
+
}
|
|
971
985
|
|
|
972
986
|
class RedisCacheStore {
|
|
973
987
|
ttlMs;
|
|
@@ -1054,10 +1068,10 @@ class RedisCacheStore {
|
|
|
1054
1068
|
return deleted > 0;
|
|
1055
1069
|
}
|
|
1056
1070
|
async invalidateByPrefix(prefix) {
|
|
1057
|
-
const keys = await this.client.keys(`${
|
|
1071
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
1058
1072
|
let removed = 0;
|
|
1059
1073
|
for (const storageKey of keys) {
|
|
1060
|
-
const key = storageKey.slice(
|
|
1074
|
+
const key = storageKey.slice(cacheKeyPrefix().length);
|
|
1061
1075
|
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
1062
1076
|
if (await this.invalidate(key)) {
|
|
1063
1077
|
removed += 1;
|
|
@@ -1067,11 +1081,11 @@ class RedisCacheStore {
|
|
|
1067
1081
|
return removed;
|
|
1068
1082
|
}
|
|
1069
1083
|
async clear() {
|
|
1070
|
-
const keys = await this.client.keys(`${
|
|
1084
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
1071
1085
|
if (keys.length > 0) {
|
|
1072
1086
|
await this.client.del(...keys);
|
|
1073
1087
|
}
|
|
1074
|
-
const tagKeys = await this.client.keys(`${
|
|
1088
|
+
const tagKeys = await this.client.keys(`${cacheTagPrefix()}*`);
|
|
1075
1089
|
if (tagKeys.length > 0) {
|
|
1076
1090
|
await this.client.del(...tagKeys);
|
|
1077
1091
|
}
|
|
@@ -1079,14 +1093,14 @@ class RedisCacheStore {
|
|
|
1079
1093
|
this.keyTags.clear();
|
|
1080
1094
|
}
|
|
1081
1095
|
async size() {
|
|
1082
|
-
const keys = await this.client.keys(`${
|
|
1096
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
1083
1097
|
return keys.length;
|
|
1084
1098
|
}
|
|
1085
1099
|
storageKey(key) {
|
|
1086
|
-
return `${
|
|
1100
|
+
return `${cacheKeyPrefix()}${key}`;
|
|
1087
1101
|
}
|
|
1088
1102
|
tagKey(tag) {
|
|
1089
|
-
return `${
|
|
1103
|
+
return `${cacheTagPrefix()}${tag}`;
|
|
1090
1104
|
}
|
|
1091
1105
|
async detachKeyFromTags(key) {
|
|
1092
1106
|
const tags = this.keyTags.get(key);
|
|
@@ -1099,7 +1113,7 @@ class RedisCacheStore {
|
|
|
1099
1113
|
this.keyTags.delete(key);
|
|
1100
1114
|
}
|
|
1101
1115
|
async enforceMaxEntries() {
|
|
1102
|
-
const keys = await this.client.keys(`${
|
|
1116
|
+
const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
|
|
1103
1117
|
if (keys.length <= this.maxEntries) {
|
|
1104
1118
|
return;
|
|
1105
1119
|
}
|
|
@@ -4660,6 +4674,9 @@ function createCsrfProtection(secret, options = {}) {
|
|
|
4660
4674
|
import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4661
4675
|
var FLASH_COOKIE = "workhub_flash";
|
|
4662
4676
|
var FLASH_TTL_MS = 60 * 1000;
|
|
4677
|
+
function flashCookieName() {
|
|
4678
|
+
return process.env.FLASH_COOKIE_NAME?.trim() || FLASH_COOKIE;
|
|
4679
|
+
}
|
|
4663
4680
|
function resolveFlashSecret() {
|
|
4664
4681
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
4665
4682
|
}
|
|
@@ -4674,7 +4691,7 @@ function readFlashCookie(request) {
|
|
|
4674
4691
|
}
|
|
4675
4692
|
for (const part of cookieHeader.split(";")) {
|
|
4676
4693
|
const [name, ...rest] = part.trim().split("=");
|
|
4677
|
-
if (name ===
|
|
4694
|
+
if (name === flashCookieName()) {
|
|
4678
4695
|
return decodeURIComponent(rest.join("="));
|
|
4679
4696
|
}
|
|
4680
4697
|
}
|
|
@@ -4721,7 +4738,7 @@ function parseFlashCookie(cookieValue) {
|
|
|
4721
4738
|
}
|
|
4722
4739
|
}
|
|
4723
4740
|
function clearFlashCookie() {
|
|
4724
|
-
return `${
|
|
4741
|
+
return `${flashCookieName()}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
4725
4742
|
}
|
|
4726
4743
|
function pullFlash(request) {
|
|
4727
4744
|
const cookieValue = readFlashCookie(request);
|
|
@@ -5398,7 +5415,7 @@ async function resolveLoginEmail(request) {
|
|
|
5398
5415
|
}
|
|
5399
5416
|
function createLoginThrottleMiddleware(options) {
|
|
5400
5417
|
const client = new RedisClient2(options.redisUrl);
|
|
5401
|
-
const prefix = options.keyPrefix ?? "
|
|
5418
|
+
const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
|
|
5402
5419
|
return async (request, next) => {
|
|
5403
5420
|
const identity = resolveLoginIdentity(request);
|
|
5404
5421
|
const email = await resolveLoginEmail(request);
|
|
@@ -5421,7 +5438,7 @@ function createLoginThrottleMiddleware(options) {
|
|
|
5421
5438
|
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
5422
5439
|
var throttleBucketRegistries = new Set;
|
|
5423
5440
|
function createMemoryThrottleMiddleware(options) {
|
|
5424
|
-
const prefix = options.keyPrefix ?? "
|
|
5441
|
+
const prefix = options.keyPrefix ?? namespacedRedisKey("memory-throttle:");
|
|
5425
5442
|
const buckets = new Map;
|
|
5426
5443
|
throttleBucketRegistries.add(buckets);
|
|
5427
5444
|
return async (request, next) => {
|
|
@@ -5623,7 +5640,7 @@ function createScimThrottleMiddleware(options) {
|
|
|
5623
5640
|
const client = options.redisUrl ? new RedisClient3(options.redisUrl) : null;
|
|
5624
5641
|
return async (request, next) => {
|
|
5625
5642
|
const identity = resolveScimIdentity(request);
|
|
5626
|
-
const key =
|
|
5643
|
+
const key = `${namespacedRedisKey("scim-throttle:")}${identity}`;
|
|
5627
5644
|
if (client) {
|
|
5628
5645
|
const attempts = Number(await client.incr(key));
|
|
5629
5646
|
if (attempts === 1) {
|
|
@@ -5779,7 +5796,7 @@ function resolveThrottleIdentity(request) {
|
|
|
5779
5796
|
}
|
|
5780
5797
|
function createThrottleMiddleware(options) {
|
|
5781
5798
|
const client = new RedisClient4(options.redisUrl);
|
|
5782
|
-
const prefix = options.keyPrefix ?? "
|
|
5799
|
+
const prefix = options.keyPrefix ?? namespacedRedisKey("throttle:");
|
|
5783
5800
|
return async (request, next) => {
|
|
5784
5801
|
const identity = resolveThrottleIdentity(request);
|
|
5785
5802
|
const path = new URL(request.url).pathname;
|
|
@@ -6260,18 +6277,27 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
6260
6277
|
|
|
6261
6278
|
// ../../src/core/queue/redisQueue.ts
|
|
6262
6279
|
var {RedisClient: RedisClient5 } = globalThis.Bun;
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6280
|
+
function queueListKey() {
|
|
6281
|
+
return namespacedRedisKey("queue:default");
|
|
6282
|
+
}
|
|
6283
|
+
function queueHighKey() {
|
|
6284
|
+
return namespacedRedisKey("queue:high");
|
|
6285
|
+
}
|
|
6286
|
+
function queueLowKey() {
|
|
6287
|
+
return namespacedRedisKey("queue:low");
|
|
6288
|
+
}
|
|
6289
|
+
var QUEUE_LIST_KEY = queueListKey();
|
|
6290
|
+
var QUEUE_HIGH_KEY = queueHighKey();
|
|
6291
|
+
var QUEUE_LOW_KEY = queueLowKey();
|
|
6266
6292
|
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
6267
6293
|
function queueKeyForPriority(priority = "default") {
|
|
6268
6294
|
switch (priority) {
|
|
6269
6295
|
case "high":
|
|
6270
|
-
return
|
|
6296
|
+
return queueHighKey();
|
|
6271
6297
|
case "low":
|
|
6272
|
-
return
|
|
6298
|
+
return queueLowKey();
|
|
6273
6299
|
default:
|
|
6274
|
-
return
|
|
6300
|
+
return queueListKey();
|
|
6275
6301
|
}
|
|
6276
6302
|
}
|
|
6277
6303
|
function parseQueueJobEnvelope(rawPayload) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Job } from "@getstrata/core/queue";
|
|
2
|
+
interface DispatchWebhookPayload {
|
|
3
|
+
webhookId: number;
|
|
4
|
+
tenantId: number;
|
|
5
|
+
event: string;
|
|
6
|
+
payload: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
declare class DispatchWebhookJob extends Job<DispatchWebhookPayload> {
|
|
9
|
+
readonly maxAttempts = 3;
|
|
10
|
+
readonly backoffMs = 2000;
|
|
11
|
+
handle(payload: DispatchWebhookPayload): Promise<void>;
|
|
12
|
+
private deliver;
|
|
13
|
+
}
|
|
14
|
+
export { DispatchWebhookJob };
|
|
15
|
+
export default DispatchWebhookJob;
|
|
16
|
+
export type { DispatchWebhookPayload };
|