@getstrata/core 0.5.63 → 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 CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## 0.5.63
4
9
 
5
10
  - Flash cookies honor `FLASH_COOKIE_NAME` (default `workhub_flash`) so sibling apps do not inherit WorkHub’s cookie name.
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.64 && git push origin v0.5.64`
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,16 +1 @@
1
- import { Job } from "../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 };
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 = "workhub:queue:default";
5
- declare const QUEUE_HIGH_KEY = "workhub:queue:high";
6
- declare const QUEUE_LOW_KEY = "workhub:queue:low";
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 {
@@ -0,0 +1,3 @@
1
+ declare function appKeyPrefix(): string;
2
+ declare function namespacedRedisKey(kind: string): string;
3
+ export { appKeyPrefix, namespacedRedisKey };
@@ -1,8 +1,22 @@
1
1
  // @bun
2
2
  // ../../src/core/cache/redisCacheStore.ts
3
3
  var {RedisClient } = globalThis.Bun;
4
- var KEY_PREFIX = "workhub:cache:";
5
- var TAG_PREFIX = "workhub:cache:tag:";
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(`${KEY_PREFIX}*`);
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(KEY_PREFIX.length);
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(`${KEY_PREFIX}*`);
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(`${TAG_PREFIX}*`);
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(`${KEY_PREFIX}*`);
131
+ const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
118
132
  return keys.length;
119
133
  }
120
134
  storageKey(key) {
121
- return `${KEY_PREFIX}${key}`;
135
+ return `${cacheKeyPrefix()}${key}`;
122
136
  }
123
137
  tagKey(tag) {
124
- return `${TAG_PREFIX}${tag}`;
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(`${KEY_PREFIX}*`);
151
+ const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
138
152
  if (keys.length <= this.maxEntries) {
139
153
  return;
140
154
  }
@@ -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 ?? "workhub:login-throttle:";
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 ?? "workhub:memory-throttle:";
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 = `workhub:scim-throttle:${identity}`;
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 ?? "workhub:throttle:";
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/core/jobs/dispatchWebhookJob.ts
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 db2`
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 db2`
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 db2`
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
- var QUEUE_LIST_KEY = "workhub:queue:default";
157
- var QUEUE_HIGH_KEY = "workhub:queue:high";
158
- var QUEUE_LOW_KEY = "workhub:queue:low";
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 QUEUE_HIGH_KEY;
182
+ return queueHighKey();
164
183
  case "low":
165
- return QUEUE_LOW_KEY;
184
+ return queueLowKey();
166
185
  default:
167
- return QUEUE_LIST_KEY;
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
- var QUEUE_LIST_KEY = "workhub:queue:default";
157
- var QUEUE_HIGH_KEY = "workhub:queue:high";
158
- var QUEUE_LOW_KEY = "workhub:queue:low";
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 QUEUE_HIGH_KEY;
182
+ return queueHighKey();
164
183
  case "low":
165
- return QUEUE_LOW_KEY;
184
+ return queueLowKey();
166
185
  default:
167
- return QUEUE_LIST_KEY;
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
- var QUEUE_LIST_KEY = "workhub:queue:default";
160
- var QUEUE_HIGH_KEY = "workhub:queue:high";
161
- var QUEUE_LOW_KEY = "workhub:queue:low";
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 QUEUE_HIGH_KEY;
185
+ return queueHighKey();
167
186
  case "low":
168
- return QUEUE_LOW_KEY;
187
+ return queueLowKey();
169
188
  default:
170
- return QUEUE_LIST_KEY;
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
- var QUEUE_LIST_KEY = "workhub:queue:default";
95
- var QUEUE_HIGH_KEY = "workhub:queue:high";
96
- var QUEUE_LOW_KEY = "workhub:queue:low";
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 QUEUE_HIGH_KEY;
118
+ return queueHighKey();
102
119
  case "low":
103
- return QUEUE_LOW_KEY;
120
+ return queueLowKey();
104
121
  default:
105
- return QUEUE_LIST_KEY;
122
+ return queueListKey();
106
123
  }
107
124
  }
108
125
  function parseQueueJobEnvelope(rawPayload) {
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
- var KEY_PREFIX = "workhub:cache:";
970
- var TAG_PREFIX = "workhub:cache:tag:";
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(`${KEY_PREFIX}*`);
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(KEY_PREFIX.length);
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(`${KEY_PREFIX}*`);
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(`${TAG_PREFIX}*`);
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(`${KEY_PREFIX}*`);
1096
+ const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
1083
1097
  return keys.length;
1084
1098
  }
1085
1099
  storageKey(key) {
1086
- return `${KEY_PREFIX}${key}`;
1100
+ return `${cacheKeyPrefix()}${key}`;
1087
1101
  }
1088
1102
  tagKey(tag) {
1089
- return `${TAG_PREFIX}${tag}`;
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(`${KEY_PREFIX}*`);
1116
+ const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
1103
1117
  if (keys.length <= this.maxEntries) {
1104
1118
  return;
1105
1119
  }
@@ -5401,7 +5415,7 @@ async function resolveLoginEmail(request) {
5401
5415
  }
5402
5416
  function createLoginThrottleMiddleware(options) {
5403
5417
  const client = new RedisClient2(options.redisUrl);
5404
- const prefix = options.keyPrefix ?? "workhub:login-throttle:";
5418
+ const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
5405
5419
  return async (request, next) => {
5406
5420
  const identity = resolveLoginIdentity(request);
5407
5421
  const email = await resolveLoginEmail(request);
@@ -5424,7 +5438,7 @@ function createLoginThrottleMiddleware(options) {
5424
5438
  // ../../src/core/http/memoryThrottleMiddleware.ts
5425
5439
  var throttleBucketRegistries = new Set;
5426
5440
  function createMemoryThrottleMiddleware(options) {
5427
- const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
5441
+ const prefix = options.keyPrefix ?? namespacedRedisKey("memory-throttle:");
5428
5442
  const buckets = new Map;
5429
5443
  throttleBucketRegistries.add(buckets);
5430
5444
  return async (request, next) => {
@@ -5626,7 +5640,7 @@ function createScimThrottleMiddleware(options) {
5626
5640
  const client = options.redisUrl ? new RedisClient3(options.redisUrl) : null;
5627
5641
  return async (request, next) => {
5628
5642
  const identity = resolveScimIdentity(request);
5629
- const key = `workhub:scim-throttle:${identity}`;
5643
+ const key = `${namespacedRedisKey("scim-throttle:")}${identity}`;
5630
5644
  if (client) {
5631
5645
  const attempts = Number(await client.incr(key));
5632
5646
  if (attempts === 1) {
@@ -5782,7 +5796,7 @@ function resolveThrottleIdentity(request) {
5782
5796
  }
5783
5797
  function createThrottleMiddleware(options) {
5784
5798
  const client = new RedisClient4(options.redisUrl);
5785
- const prefix = options.keyPrefix ?? "workhub:throttle:";
5799
+ const prefix = options.keyPrefix ?? namespacedRedisKey("throttle:");
5786
5800
  return async (request, next) => {
5787
5801
  const identity = resolveThrottleIdentity(request);
5788
5802
  const path = new URL(request.url).pathname;
@@ -6263,18 +6277,27 @@ async function runQueueJob(envelope, failedJobs) {
6263
6277
 
6264
6278
  // ../../src/core/queue/redisQueue.ts
6265
6279
  var {RedisClient: RedisClient5 } = globalThis.Bun;
6266
- var QUEUE_LIST_KEY = "workhub:queue:default";
6267
- var QUEUE_HIGH_KEY = "workhub:queue:high";
6268
- var QUEUE_LOW_KEY = "workhub:queue:low";
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();
6269
6292
  var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
6270
6293
  function queueKeyForPriority(priority = "default") {
6271
6294
  switch (priority) {
6272
6295
  case "high":
6273
- return QUEUE_HIGH_KEY;
6296
+ return queueHighKey();
6274
6297
  case "low":
6275
- return QUEUE_LOW_KEY;
6298
+ return queueLowKey();
6276
6299
  default:
6277
- return QUEUE_LIST_KEY;
6300
+ return queueListKey();
6278
6301
  }
6279
6302
  }
6280
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.63",
3
+ "version": "0.5.64",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",