alepha 0.20.6 → 0.20.7
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/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +370 -355
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +1 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +179 -170
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +1 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +7 -0
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +271 -262
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +21 -3
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +198 -192
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +1 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +246 -245
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/organizations/index.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +323 -320
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/payments/index.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +863 -847
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/verifications/index.d.ts +126 -125
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/bucket/index.d.ts +3 -2
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +114 -4
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +20 -19
- package/dist/cache/database/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cli/core/index.d.ts +113 -129
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +75 -7
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +105 -6
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/command/index.d.ts +5 -4
- package/dist/command/index.d.ts.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/email/core/index.d.ts +3 -2
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/smtp/index.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/lock/core/index.d.ts +5 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/react/form/index.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +6 -4
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/router/index.d.ts +206 -205
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/ui/index.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +3 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/security/index.browser.js +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/core/index.d.ts +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts +16 -15
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/links/index.d.ts +51 -50
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/package.json +16 -32
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/jobs/__tests__/$job.spec.ts +92 -40
- package/src/api/jobs/entities/jobExecutionEntity.ts +1 -0
- package/src/api/jobs/providers/JobProvider.ts +20 -5
- package/src/api/jobs/schemas/jobConfigAtom.ts +5 -0
- package/src/api/keys/entities/apiKeyEntity.ts +1 -0
- package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +133 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +347 -21
- package/src/cli/core/tasks/BuildCloudflareTask.ts +16 -0
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -848
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1185
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- package/src/websocket/services/WebSocketTopicService.ts +0 -108
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { $atom, t } from "alepha";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Atom storing the active tenant for the current request.
|
|
5
|
+
*
|
|
6
|
+
* Transport-agnostic — works with HTTP, MCP, pipelines, jobs, and any context
|
|
7
|
+
* that sets the atom before calling tenant-scoped logic.
|
|
8
|
+
*
|
|
9
|
+
* Typically set by an app-level middleware that resolves the tenant from the
|
|
10
|
+
* request `Host` header (or another signal) and writes the resolved id to the
|
|
11
|
+
* store. Framework code that reads this atom:
|
|
12
|
+
*
|
|
13
|
+
* - Repository scoping: `withOrganization` / `stampOrganization` prefer this
|
|
14
|
+
* value over `currentUserAtom.organization` so cross-tenant users (admins,
|
|
15
|
+
* agency operators) are scoped to the tenant they are currently acting in
|
|
16
|
+
* rather than the one they belong to.
|
|
17
|
+
* - Session creation: the value is persisted into the JWT as a `tenant` claim,
|
|
18
|
+
* and the issuer resolver rejects tokens whose claim does not match the
|
|
19
|
+
* tenant resolved from the current request.
|
|
20
|
+
*
|
|
21
|
+
* `id` is a free-form string so the framework stays neutral on tenant identity
|
|
22
|
+
* (slug, UUID, composite). Pick whatever matches the column marked with
|
|
23
|
+
* `PG_ORGANIZATION` in your entities.
|
|
24
|
+
*/
|
|
25
|
+
export const currentTenantAtom = $atom({
|
|
26
|
+
name: "alepha.security.tenant",
|
|
27
|
+
schema: t.optional(
|
|
28
|
+
t.object({
|
|
29
|
+
id: t.text({
|
|
30
|
+
description: "Tenant identifier (slug, UUID, or composite).",
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
});
|
|
@@ -2,6 +2,7 @@ import { $module } from "alepha";
|
|
|
2
2
|
|
|
3
3
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
+
export * from "./atoms/currentTenantAtom.ts";
|
|
5
6
|
export * from "./atoms/currentUserAtom.ts";
|
|
6
7
|
export * from "./errors/InvalidCredentialsError.ts";
|
|
7
8
|
export * from "./errors/InvalidPermissionError.ts";
|
package/src/security/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { $module } from "alepha";
|
|
2
2
|
import type { FetchOptions } from "alepha/server";
|
|
3
|
+
import { currentTenantAtom } from "./atoms/currentTenantAtom.ts";
|
|
3
4
|
import { currentUserAtom } from "./atoms/currentUserAtom.ts";
|
|
4
5
|
import type { UserAccountToken } from "./interfaces/UserAccountToken.ts";
|
|
5
6
|
import { $issuer } from "./primitives/$issuer.ts";
|
|
@@ -13,6 +14,7 @@ import type { UserAccount } from "./schemas/userAccountInfoSchema.ts";
|
|
|
13
14
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
14
15
|
|
|
15
16
|
export * from "alepha/crypto";
|
|
17
|
+
export * from "./atoms/currentTenantAtom.ts";
|
|
16
18
|
export * from "./atoms/currentUserAtom.ts";
|
|
17
19
|
export * from "./errors/InvalidCredentialsError.ts";
|
|
18
20
|
export * from "./errors/InvalidPermissionError.ts";
|
|
@@ -55,6 +57,15 @@ declare module "alepha" {
|
|
|
55
57
|
* The current authenticated user.
|
|
56
58
|
*/
|
|
57
59
|
"alepha.security.user"?: UserAccount;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The tenant the current request is acting in.
|
|
63
|
+
*
|
|
64
|
+
* Typically set by an app-level middleware from the request `Host`. When
|
|
65
|
+
* present, `Repository` scoping and session creation prefer this value
|
|
66
|
+
* over `currentUserAtom.organization`.
|
|
67
|
+
*/
|
|
68
|
+
"alepha.security.tenant"?: { id: string };
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
|
|
@@ -100,6 +111,6 @@ declare module "alepha/server" {
|
|
|
100
111
|
export const AlephaSecurity = $module({
|
|
101
112
|
name: "alepha.security",
|
|
102
113
|
primitives: [$issuer, $role, $permission],
|
|
103
|
-
atoms: [currentUserAtom],
|
|
114
|
+
atoms: [currentUserAtom, currentTenantAtom],
|
|
104
115
|
services: [SecurityProvider, JwtProvider, ServerSecurityProvider],
|
|
105
116
|
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
$inject,
|
|
3
|
+
Alepha,
|
|
4
|
+
AlephaError,
|
|
5
|
+
createPrimitive,
|
|
6
|
+
KIND,
|
|
7
|
+
Primitive,
|
|
8
|
+
} from "alepha";
|
|
2
9
|
import {
|
|
3
10
|
DateTimeProvider,
|
|
4
11
|
type Duration,
|
|
@@ -7,6 +14,7 @@ import {
|
|
|
7
14
|
import { $logger } from "alepha/logger";
|
|
8
15
|
import type { ServerRequest } from "alepha/server";
|
|
9
16
|
import type { JSONWebKeySet, JWTPayload } from "jose";
|
|
17
|
+
import { currentTenantAtom } from "../atoms/currentTenantAtom.ts";
|
|
10
18
|
import { SecurityError } from "../errors/SecurityError.ts";
|
|
11
19
|
import type { IssuerResolver } from "../interfaces/IssuerResolver.ts";
|
|
12
20
|
import { JwtProvider } from "../providers/JwtProvider.ts";
|
|
@@ -119,6 +127,7 @@ export interface IssuerExternal {
|
|
|
119
127
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
120
128
|
|
|
121
129
|
export class IssuerPrimitive extends Primitive<IssuerPrimitiveOptions> {
|
|
130
|
+
protected readonly alepha = $inject(Alepha);
|
|
122
131
|
protected readonly securityProvider = $inject(SecurityProvider);
|
|
123
132
|
protected readonly dateTimeProvider = $inject(DateTimeProvider);
|
|
124
133
|
protected readonly jwt = $inject(JwtProvider);
|
|
@@ -302,6 +311,12 @@ export class IssuerPrimitive extends Primitive<IssuerPrimitiveOptions> {
|
|
|
302
311
|
aud: this.name,
|
|
303
312
|
});
|
|
304
313
|
|
|
314
|
+
// Bind the token to the tenant the request is acting in. The default JWT
|
|
315
|
+
// resolver compares this claim against `currentTenantAtom` on every
|
|
316
|
+
// request, so a token minted on `b14.club.alepha.dev` is rejected on
|
|
317
|
+
// `viska.club.alepha.dev` even when the user belongs to both.
|
|
318
|
+
const tenant = this.alepha.store.get(currentTenantAtom)?.id;
|
|
319
|
+
|
|
305
320
|
const access_token = await this.jwt.create(
|
|
306
321
|
{
|
|
307
322
|
// jwt
|
|
@@ -318,6 +333,7 @@ export class IssuerPrimitive extends Primitive<IssuerPrimitiveOptions> {
|
|
|
318
333
|
// our claims
|
|
319
334
|
organization: user.organization,
|
|
320
335
|
roles: user.roles,
|
|
336
|
+
tenant,
|
|
321
337
|
},
|
|
322
338
|
this.name,
|
|
323
339
|
);
|
|
@@ -10,6 +10,7 @@ import { $logger } from "alepha/logger";
|
|
|
10
10
|
import { ForbiddenError } from "alepha/server";
|
|
11
11
|
import type { JSONWebKeySet, JWTPayload } from "jose";
|
|
12
12
|
import type { JWTVerifyOptions } from "jose/jwt/verify";
|
|
13
|
+
import { currentTenantAtom } from "../atoms/currentTenantAtom.ts";
|
|
13
14
|
import { currentUserAtom } from "../atoms/currentUserAtom.ts";
|
|
14
15
|
import { InvalidPermissionError } from "../errors/InvalidPermissionError.ts";
|
|
15
16
|
import { InvalidTokenError } from "../errors/InvalidTokenError.ts";
|
|
@@ -110,6 +111,23 @@ export class SecurityProvider {
|
|
|
110
111
|
// Parse and validate JWT
|
|
111
112
|
const { result } = await this.jwt.parse(token, realmName);
|
|
112
113
|
|
|
114
|
+
// Reject tokens whose tenant claim doesn't match the tenant resolved
|
|
115
|
+
// for the current request. Prevents a token minted on tenant A from
|
|
116
|
+
// being replayed on tenant B (subdomain spoofing, leaked bearer
|
|
117
|
+
// tokens). Tokens minted without a tenant claim (no active tenant at
|
|
118
|
+
// session creation) pass through — single-tenant apps are unaffected.
|
|
119
|
+
const claimTenant = this.getTenantFromPayload(result.payload);
|
|
120
|
+
if (claimTenant) {
|
|
121
|
+
const activeTenant = this.alepha.store.get(currentTenantAtom)?.id;
|
|
122
|
+
if (activeTenant && activeTenant !== claimTenant) {
|
|
123
|
+
this.log.warn("JWT tenant claim does not match active tenant", {
|
|
124
|
+
claim: claimTenant,
|
|
125
|
+
active: activeTenant,
|
|
126
|
+
});
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
// Extract user info from JWT payload
|
|
114
132
|
return this.createUserFromPayload(result.payload, realmName);
|
|
115
133
|
},
|
|
@@ -927,6 +945,25 @@ export class SecurityProvider {
|
|
|
927
945
|
return payload.organization;
|
|
928
946
|
}
|
|
929
947
|
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Extracts the tenant id from the JWT payload, when present.
|
|
951
|
+
*
|
|
952
|
+
* Tokens minted with no active tenant (single-tenant apps, server-to-server
|
|
953
|
+
* calls before any request-scoped middleware runs) omit the claim, in which
|
|
954
|
+
* case the resolver does not enforce a tenant match.
|
|
955
|
+
*/
|
|
956
|
+
public getTenantFromPayload(
|
|
957
|
+
payload: Record<string, any>,
|
|
958
|
+
): string | undefined {
|
|
959
|
+
if (!payload) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (typeof payload.tenant === "string") {
|
|
964
|
+
return payload.tenant;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
930
967
|
}
|
|
931
968
|
|
|
932
969
|
// =====================================================================================================================
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { AlephaServerAuth } from "../index.ts";
|
|
4
|
+
import { ServerAuthProvider } from "../providers/ServerAuthProvider.ts";
|
|
5
|
+
|
|
6
|
+
class ProviderProbe extends ServerAuthProvider {
|
|
7
|
+
public probeValidate(uri: string) {
|
|
8
|
+
return this.validateRedirectUri(uri);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const setup = async (env: Record<string, string> = {}) => {
|
|
13
|
+
const alepha = Alepha.create({ env });
|
|
14
|
+
alepha.with(AlephaServerAuth);
|
|
15
|
+
alepha.with(ProviderProbe);
|
|
16
|
+
await alepha.start();
|
|
17
|
+
return alepha.inject(ProviderProbe);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("validateRedirectUri", () => {
|
|
21
|
+
describe("without COOKIE_PARENT_DOMAIN (legacy behaviour)", () => {
|
|
22
|
+
it("accepts relative paths", async ({ expect }) => {
|
|
23
|
+
const p = await setup({});
|
|
24
|
+
expect(p.probeValidate("/admin")).toBe("/admin");
|
|
25
|
+
});
|
|
26
|
+
it("rejects protocol-relative URLs", async ({ expect }) => {
|
|
27
|
+
const p = await setup({});
|
|
28
|
+
expect(p.probeValidate("//evil.com")).toBe("/");
|
|
29
|
+
});
|
|
30
|
+
it("rejects absolute URLs", async ({ expect }) => {
|
|
31
|
+
const p = await setup({});
|
|
32
|
+
expect(p.probeValidate("https://evil.com/x")).toBe("/");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("with COOKIE_PARENT_DOMAIN=.club.alepha.dev", () => {
|
|
37
|
+
const env = { COOKIE_PARENT_DOMAIN: ".club.alepha.dev" };
|
|
38
|
+
it("still accepts relative paths", async ({ expect }) => {
|
|
39
|
+
const p = await setup(env);
|
|
40
|
+
expect(p.probeValidate("/admin")).toBe("/admin");
|
|
41
|
+
});
|
|
42
|
+
it("still rejects protocol-relative URLs", async ({ expect }) => {
|
|
43
|
+
const p = await setup(env);
|
|
44
|
+
expect(p.probeValidate("//evil.com")).toBe("/");
|
|
45
|
+
});
|
|
46
|
+
it("rejects unrelated absolute URLs", async ({ expect }) => {
|
|
47
|
+
const p = await setup(env);
|
|
48
|
+
expect(p.probeValidate("https://evil.com/x")).toBe("/");
|
|
49
|
+
});
|
|
50
|
+
it("rejects http (must be https)", async ({ expect }) => {
|
|
51
|
+
const p = await setup(env);
|
|
52
|
+
expect(p.probeValidate("http://viscapadel.club.alepha.dev/admin")).toBe(
|
|
53
|
+
"/",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
it("accepts subdomain under the parent domain", async ({ expect }) => {
|
|
57
|
+
const p = await setup(env);
|
|
58
|
+
expect(p.probeValidate("https://viscapadel.club.alepha.dev/admin")).toBe(
|
|
59
|
+
"https://viscapadel.club.alepha.dev/admin",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
it("accepts the bare parent domain", async ({ expect }) => {
|
|
63
|
+
const p = await setup(env);
|
|
64
|
+
expect(p.probeValidate("https://club.alepha.dev/dashboard")).toBe(
|
|
65
|
+
"https://club.alepha.dev/dashboard",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
it("rejects look-alike domains (suffix match without dot boundary)", async ({
|
|
69
|
+
expect,
|
|
70
|
+
}) => {
|
|
71
|
+
const p = await setup(env);
|
|
72
|
+
expect(p.probeValidate("https://notclub.alepha.dev/x")).toBe("/");
|
|
73
|
+
expect(
|
|
74
|
+
p.probeValidate("https://evil-club.alepha.dev.attacker.com/x"),
|
|
75
|
+
).toBe("/");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -41,14 +41,30 @@ export class ServerAuthProvider {
|
|
|
41
41
|
protected readonly serverLinksProvider = $inject(ServerLinksProvider);
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Validates that a redirect URI is a safe relative path
|
|
45
|
-
*
|
|
44
|
+
* Validates that a redirect URI is a safe relative path, or — when
|
|
45
|
+
* COOKIE_PARENT_DOMAIN is configured — an https URL whose host is the
|
|
46
|
+
* parent domain or a subdomain of it. Used by SaaS deployments where the
|
|
47
|
+
* OAuth callback dispatches users back to their tenant subdomain.
|
|
48
|
+
*
|
|
49
|
+
* Prevents open redirect attacks by rejecting any other absolute URL.
|
|
46
50
|
*/
|
|
47
51
|
protected validateRedirectUri(uri: string): string {
|
|
48
|
-
if (
|
|
49
|
-
return
|
|
52
|
+
if (uri.startsWith("/") && !uri.startsWith("//")) {
|
|
53
|
+
return uri;
|
|
50
54
|
}
|
|
51
|
-
|
|
55
|
+
const parent = this.alepha.env.COOKIE_PARENT_DOMAIN;
|
|
56
|
+
if (typeof parent === "string" && parent) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(uri);
|
|
59
|
+
const parentHost = parent.startsWith(".") ? parent.slice(1) : parent;
|
|
60
|
+
if (parsed.protocol !== "https:") return "/";
|
|
61
|
+
if (parsed.host === parentHost) return uri;
|
|
62
|
+
if (parsed.host.endsWith(`.${parentHost}`)) return uri;
|
|
63
|
+
} catch {
|
|
64
|
+
// fall through
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return "/";
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
public get identities(): Array<AuthPrimitive> {
|
package/tsconfig.base.json
CHANGED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { ChannelPrimitive, TWSObject } from "alepha/websocket";
|
|
2
|
-
import { Static } from "alepha";
|
|
3
|
-
|
|
4
|
-
//#region ../../src/react/websocket/hooks/useRoom.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* UseRoom options
|
|
7
|
-
*/
|
|
8
|
-
interface UseRoomOptions<TClient extends TWSObject, TServer extends TWSObject> {
|
|
9
|
-
/**
|
|
10
|
-
* Room ID to connect to
|
|
11
|
-
*/
|
|
12
|
-
roomId: string;
|
|
13
|
-
/**
|
|
14
|
-
* Channel primitive defining the schemas
|
|
15
|
-
*/
|
|
16
|
-
channel: ChannelPrimitive<TClient, TServer>;
|
|
17
|
-
/**
|
|
18
|
-
* Handler for incoming messages from the server
|
|
19
|
-
*/
|
|
20
|
-
handler: (message: Static<TClient>) => void;
|
|
21
|
-
/**
|
|
22
|
-
* Optional WebSocket URL override
|
|
23
|
-
* Defaults to auto-detected URL based on window.location
|
|
24
|
-
*/
|
|
25
|
-
url?: string;
|
|
26
|
-
/**
|
|
27
|
-
* Enable automatic reconnection on disconnect
|
|
28
|
-
* @default true
|
|
29
|
-
*/
|
|
30
|
-
autoReconnect?: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Reconnection interval in milliseconds
|
|
33
|
-
* @default 3000
|
|
34
|
-
*/
|
|
35
|
-
reconnectInterval?: number;
|
|
36
|
-
/**
|
|
37
|
-
* Maximum reconnection attempts (-1 for infinite)
|
|
38
|
-
* @default 10
|
|
39
|
-
*/
|
|
40
|
-
maxReconnectAttempts?: number;
|
|
41
|
-
/**
|
|
42
|
-
* Called when connection is established
|
|
43
|
-
*/
|
|
44
|
-
onConnect?: () => void;
|
|
45
|
-
/**
|
|
46
|
-
* Called when connection is closed
|
|
47
|
-
*/
|
|
48
|
-
onDisconnect?: () => void;
|
|
49
|
-
/**
|
|
50
|
-
* Called on connection error
|
|
51
|
-
*/
|
|
52
|
-
onError?: (error: Error) => void;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* UseRoom return value
|
|
56
|
-
*/
|
|
57
|
-
interface UseRoomReturn<TServer extends TWSObject> {
|
|
58
|
-
/**
|
|
59
|
-
* Send a message to the server
|
|
60
|
-
*/
|
|
61
|
-
send: (message: Static<TServer>) => Promise<void>;
|
|
62
|
-
/**
|
|
63
|
-
* Whether the connection is established
|
|
64
|
-
*/
|
|
65
|
-
isConnected: boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Whether the connection is in progress
|
|
68
|
-
*/
|
|
69
|
-
isConnecting: boolean;
|
|
70
|
-
/**
|
|
71
|
-
* Whether there was an error
|
|
72
|
-
*/
|
|
73
|
-
isError: boolean;
|
|
74
|
-
/**
|
|
75
|
-
* The error object if any
|
|
76
|
-
*/
|
|
77
|
-
error?: Error;
|
|
78
|
-
/**
|
|
79
|
-
* Manually reconnect
|
|
80
|
-
*/
|
|
81
|
-
reconnect: () => void;
|
|
82
|
-
/**
|
|
83
|
-
* Manually disconnect
|
|
84
|
-
*/
|
|
85
|
-
disconnect: () => void;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* React hook for WebSocket room communication
|
|
89
|
-
*
|
|
90
|
-
* Provides automatic connection management, reconnection, and type-safe messaging
|
|
91
|
-
* for WebSocket rooms using the injected WebSocketClient service.
|
|
92
|
-
*
|
|
93
|
-
* Multiple useRoom hooks on the same channel will share a single WebSocket connection.
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* ```tsx
|
|
97
|
-
* const chat = useRoom({
|
|
98
|
-
* roomId: "room-123",
|
|
99
|
-
* channel: chatChannel,
|
|
100
|
-
* handler: (message) => {
|
|
101
|
-
* if (message.type === "append") {
|
|
102
|
-
* setMessages(prev => [...prev, message]);
|
|
103
|
-
* }
|
|
104
|
-
* }
|
|
105
|
-
* }, [roomId]);
|
|
106
|
-
*
|
|
107
|
-
* const sendMessage = async () => {
|
|
108
|
-
* await chat.send({
|
|
109
|
-
* content: "Hello, world!"
|
|
110
|
-
* });
|
|
111
|
-
* };
|
|
112
|
-
* ```
|
|
113
|
-
*/
|
|
114
|
-
declare const useRoom: <TClient extends TWSObject, TServer extends TWSObject>(options: UseRoomOptions<TClient, TServer>, deps: unknown[]) => UseRoomReturn<TServer>;
|
|
115
|
-
//#endregion
|
|
116
|
-
export { UseRoomOptions, UseRoomReturn, useRoom };
|
|
117
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/react/websocket/hooks/useRoom.tsx"],"mappings":";;;;;;AASA;UAAiB,cAAA,iBACC,SAAA,kBACA,SAAA;EAFa;;;EAO7B,MAAA;EAKmC;;;EAAnC,OAAA,EAAS,gBAAA,CAAiB,OAAA,EAAS,OAAA;EA4CjB;;;EAvClB,OAAA,GAAU,OAAA,EAAS,MAAA,CAAO,OAAA;EAhBV;;;;EAsBhB,GAAA;EAXS;;;;EAiBT,aAAA;EAZ0B;;;;EAkB1B,iBAAA;EAMA;;;;EAAA,oBAAA;EAeW;;;EAVX,SAAA;EAgB4B;;;EAX5B,YAAA;EAegB;;;EAVhB,OAAA,IAAW,KAAA,EAAO,KAAA;AAAA;;;;UAMH,aAAA,iBAA8B,SAAA;EAItB;;;EAAvB,IAAA,GAAO,OAAA,EAAS,MAAA,CAAO,OAAA,MAAa,OAAA;EAUpC;;;EALA,WAAA;EAoBA;;;EAfA,YAAA;EAkDW;;;EA7CX,OAAA;EA6CiE;;;EAxCjE,KAAA,GAAQ,KAAA;EA2CO;;;EAtCf,SAAA;EAmCsB;;;EA9BtB,UAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BW,OAAA,mBAA2B,SAAA,kBAA2B,SAAA,EACjE,OAAA,EAAS,cAAA,CAAe,OAAA,EAAS,OAAA,GACjC,IAAA,gBACC,aAAA,CAAc,OAAA"}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { useAlepha, useInject } from "alepha/react";
|
|
2
|
-
import { WebSocketClient } from "alepha/websocket";
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
//#region ../../src/react/websocket/hooks/useRoom.tsx
|
|
5
|
-
/**
|
|
6
|
-
* React hook for WebSocket room communication
|
|
7
|
-
*
|
|
8
|
-
* Provides automatic connection management, reconnection, and type-safe messaging
|
|
9
|
-
* for WebSocket rooms using the injected WebSocketClient service.
|
|
10
|
-
*
|
|
11
|
-
* Multiple useRoom hooks on the same channel will share a single WebSocket connection.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```tsx
|
|
15
|
-
* const chat = useRoom({
|
|
16
|
-
* roomId: "room-123",
|
|
17
|
-
* channel: chatChannel,
|
|
18
|
-
* handler: (message) => {
|
|
19
|
-
* if (message.type === "append") {
|
|
20
|
-
* setMessages(prev => [...prev, message]);
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
* }, [roomId]);
|
|
24
|
-
*
|
|
25
|
-
* const sendMessage = async () => {
|
|
26
|
-
* await chat.send({
|
|
27
|
-
* content: "Hello, world!"
|
|
28
|
-
* });
|
|
29
|
-
* };
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
const useRoom = (options, deps) => {
|
|
33
|
-
const webSocketClient = useInject(WebSocketClient);
|
|
34
|
-
const unsubscribeRef = useRef(null);
|
|
35
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
36
|
-
const [isConnecting, setIsConnecting] = useState(false);
|
|
37
|
-
const [isError, setIsError] = useState(false);
|
|
38
|
-
const [error, setError] = useState(void 0);
|
|
39
|
-
const { roomId, channel, handler, url, autoReconnect, reconnectInterval, maxReconnectAttempts, onConnect, onDisconnect, onError } = options;
|
|
40
|
-
const handlerRef = useRef(handler);
|
|
41
|
-
handlerRef.current = handler;
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
const unsubscribe = webSocketClient.subscribe(roomId, channel, (msg) => handlerRef.current(msg), {
|
|
44
|
-
url,
|
|
45
|
-
autoReconnect,
|
|
46
|
-
reconnectInterval,
|
|
47
|
-
maxReconnectAttempts,
|
|
48
|
-
onConnect: () => {
|
|
49
|
-
setIsConnected(true);
|
|
50
|
-
setIsConnecting(false);
|
|
51
|
-
setIsError(false);
|
|
52
|
-
setError(void 0);
|
|
53
|
-
onConnect?.();
|
|
54
|
-
},
|
|
55
|
-
onDisconnect: () => {
|
|
56
|
-
setIsConnected(false);
|
|
57
|
-
setIsConnecting(false);
|
|
58
|
-
onDisconnect?.();
|
|
59
|
-
},
|
|
60
|
-
onError: (err) => {
|
|
61
|
-
setIsError(true);
|
|
62
|
-
setError(err);
|
|
63
|
-
setIsConnecting(false);
|
|
64
|
-
onError?.(err);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
unsubscribeRef.current = unsubscribe;
|
|
68
|
-
const connection = webSocketClient.getConnection(channel);
|
|
69
|
-
if (connection) {
|
|
70
|
-
setIsConnected(connection.isConnected);
|
|
71
|
-
setIsConnecting(connection.isConnecting);
|
|
72
|
-
setIsError(connection.isError);
|
|
73
|
-
setError(connection.error);
|
|
74
|
-
}
|
|
75
|
-
return () => {
|
|
76
|
-
unsubscribe();
|
|
77
|
-
unsubscribeRef.current = null;
|
|
78
|
-
};
|
|
79
|
-
}, deps);
|
|
80
|
-
if (!useAlepha().isBrowser()) return {
|
|
81
|
-
send: async (_message) => {},
|
|
82
|
-
isConnected: false,
|
|
83
|
-
isConnecting: false,
|
|
84
|
-
isError: false,
|
|
85
|
-
error: void 0,
|
|
86
|
-
reconnect: () => {},
|
|
87
|
-
disconnect: () => {}
|
|
88
|
-
};
|
|
89
|
-
return {
|
|
90
|
-
send: async (message) => {
|
|
91
|
-
await webSocketClient.send(roomId, channel, message);
|
|
92
|
-
},
|
|
93
|
-
isConnected,
|
|
94
|
-
isConnecting,
|
|
95
|
-
isError,
|
|
96
|
-
error,
|
|
97
|
-
reconnect: () => {
|
|
98
|
-
webSocketClient.getConnection(channel)?.reconnect();
|
|
99
|
-
},
|
|
100
|
-
disconnect: () => {
|
|
101
|
-
unsubscribeRef.current?.();
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
};
|
|
105
|
-
//#endregion
|
|
106
|
-
export { useRoom };
|
|
107
|
-
|
|
108
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/websocket/hooks/useRoom.tsx"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { useAlepha, useInject } from \"alepha/react\";\nimport type { ChannelPrimitive, TWSObject } from \"alepha/websocket\";\nimport { WebSocketClient } from \"alepha/websocket\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * UseRoom options\n */\nexport interface UseRoomOptions<\n TClient extends TWSObject,\n TServer extends TWSObject,\n> {\n /**\n * Room ID to connect to\n */\n roomId: string;\n\n /**\n * Channel primitive defining the schemas\n */\n channel: ChannelPrimitive<TClient, TServer>;\n\n /**\n * Handler for incoming messages from the server\n */\n handler: (message: Static<TClient>) => void;\n\n /**\n * Optional WebSocket URL override\n * Defaults to auto-detected URL based on window.location\n */\n url?: string;\n\n /**\n * Enable automatic reconnection on disconnect\n * @default true\n */\n autoReconnect?: boolean;\n\n /**\n * Reconnection interval in milliseconds\n * @default 3000\n */\n reconnectInterval?: number;\n\n /**\n * Maximum reconnection attempts (-1 for infinite)\n * @default 10\n */\n maxReconnectAttempts?: number;\n\n /**\n * Called when connection is established\n */\n onConnect?: () => void;\n\n /**\n * Called when connection is closed\n */\n onDisconnect?: () => void;\n\n /**\n * Called on connection error\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * UseRoom return value\n */\nexport interface UseRoomReturn<TServer extends TWSObject> {\n /**\n * Send a message to the server\n */\n send: (message: Static<TServer>) => Promise<void>;\n\n /**\n * Whether the connection is established\n */\n isConnected: boolean;\n\n /**\n * Whether the connection is in progress\n */\n isConnecting: boolean;\n\n /**\n * Whether there was an error\n */\n isError: boolean;\n\n /**\n * The error object if any\n */\n error?: Error;\n\n /**\n * Manually reconnect\n */\n reconnect: () => void;\n\n /**\n * Manually disconnect\n */\n disconnect: () => void;\n}\n\n/**\n * React hook for WebSocket room communication\n *\n * Provides automatic connection management, reconnection, and type-safe messaging\n * for WebSocket rooms using the injected WebSocketClient service.\n *\n * Multiple useRoom hooks on the same channel will share a single WebSocket connection.\n *\n * @example\n * ```tsx\n * const chat = useRoom({\n * roomId: \"room-123\",\n * channel: chatChannel,\n * handler: (message) => {\n * if (message.type === \"append\") {\n * setMessages(prev => [...prev, message]);\n * }\n * }\n * }, [roomId]);\n *\n * const sendMessage = async () => {\n * await chat.send({\n * content: \"Hello, world!\"\n * });\n * };\n * ```\n */\nexport const useRoom = <TClient extends TWSObject, TServer extends TWSObject>(\n options: UseRoomOptions<TClient, TServer>,\n deps: unknown[],\n): UseRoomReturn<TServer> => {\n const webSocketClient = useInject(WebSocketClient);\n const unsubscribeRef = useRef<(() => void) | null>(null);\n\n const [isConnected, setIsConnected] = useState(false);\n const [isConnecting, setIsConnecting] = useState(false);\n const [isError, setIsError] = useState(false);\n const [error, setError] = useState<Error | undefined>(undefined);\n\n const {\n roomId,\n channel,\n handler,\n url,\n autoReconnect,\n reconnectInterval,\n maxReconnectAttempts,\n onConnect,\n onDisconnect,\n onError,\n } = options;\n\n // Keep handler ref stable to avoid stale closures\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n\n useEffect(() => {\n // Subscribe to room — use ref so handler is always current\n const unsubscribe = webSocketClient.subscribe(\n roomId,\n channel,\n (msg) => handlerRef.current(msg),\n {\n url,\n autoReconnect,\n reconnectInterval,\n maxReconnectAttempts,\n onConnect: () => {\n setIsConnected(true);\n setIsConnecting(false);\n setIsError(false);\n setError(undefined);\n onConnect?.();\n },\n onDisconnect: () => {\n setIsConnected(false);\n setIsConnecting(false);\n onDisconnect?.();\n },\n onError: (err) => {\n setIsError(true);\n setError(err);\n setIsConnecting(false);\n onError?.(err);\n },\n },\n );\n\n unsubscribeRef.current = unsubscribe;\n\n // Get initial state from connection\n const connection = webSocketClient.getConnection(channel);\n if (connection) {\n setIsConnected(connection.isConnected);\n setIsConnecting(connection.isConnecting);\n setIsError(connection.isError);\n setError(connection.error);\n }\n\n // Cleanup on unmount or deps change\n return () => {\n unsubscribe();\n unsubscribeRef.current = null;\n };\n }, deps);\n\n const alepha = useAlepha();\n\n if (!alepha.isBrowser()) {\n return {\n send: async (_message: Static<TServer>) => {\n // No-op on server\n },\n isConnected: false,\n isConnecting: false,\n isError: false,\n error: undefined,\n reconnect: () => {\n // No-op on server\n },\n disconnect: () => {\n // No-op on server\n },\n };\n }\n\n return {\n send: async (message: Static<TServer>) => {\n await webSocketClient.send(roomId, channel, message);\n },\n isConnected,\n isConnecting,\n isError,\n error,\n reconnect: () => {\n const connection = webSocketClient.getConnection(channel);\n connection?.reconnect();\n },\n disconnect: () => {\n unsubscribeRef.current?.();\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuIA,MAAa,WACX,SACA,SAC2B;CAC3B,MAAM,kBAAkB,UAAU,gBAAgB;CAClD,MAAM,iBAAiB,OAA4B,KAAK;CAExD,MAAM,CAAC,aAAa,kBAAkB,SAAS,MAAM;CACrD,MAAM,CAAC,cAAc,mBAAmB,SAAS,MAAM;CACvD,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,CAAC,OAAO,YAAY,SAA4B,KAAA,EAAU;CAEhE,MAAM,EACJ,QACA,SACA,SACA,KACA,eACA,mBACA,sBACA,WACA,cACA,YACE;CAGJ,MAAM,aAAa,OAAO,QAAQ;CAClC,WAAW,UAAU;CAErB,gBAAgB;EAEd,MAAM,cAAc,gBAAgB,UAClC,QACA,UACC,QAAQ,WAAW,QAAQ,IAAI,EAChC;GACE;GACA;GACA;GACA;GACA,iBAAiB;IACf,eAAe,KAAK;IACpB,gBAAgB,MAAM;IACtB,WAAW,MAAM;IACjB,SAAS,KAAA,EAAU;IACnB,aAAa;;GAEf,oBAAoB;IAClB,eAAe,MAAM;IACrB,gBAAgB,MAAM;IACtB,gBAAgB;;GAElB,UAAU,QAAQ;IAChB,WAAW,KAAK;IAChB,SAAS,IAAI;IACb,gBAAgB,MAAM;IACtB,UAAU,IAAI;;GAEjB,CACF;EAED,eAAe,UAAU;EAGzB,MAAM,aAAa,gBAAgB,cAAc,QAAQ;EACzD,IAAI,YAAY;GACd,eAAe,WAAW,YAAY;GACtC,gBAAgB,WAAW,aAAa;GACxC,WAAW,WAAW,QAAQ;GAC9B,SAAS,WAAW,MAAM;;EAI5B,aAAa;GACX,aAAa;GACb,eAAe,UAAU;;IAE1B,KAAK;CAIR,IAAI,CAFW,WAEJ,CAAC,WAAW,EACrB,OAAO;EACL,MAAM,OAAO,aAA8B;EAG3C,aAAa;EACb,cAAc;EACd,SAAS;EACT,OAAO,KAAA;EACP,iBAAiB;EAGjB,kBAAkB;EAGnB;CAGH,OAAO;EACL,MAAM,OAAO,YAA6B;GACxC,MAAM,gBAAgB,KAAK,QAAQ,SAAS,QAAQ;;EAEtD;EACA;EACA;EACA;EACA,iBAAiB;GAEf,gBADmC,cAAc,QACvC,EAAE,WAAW;;EAEzB,kBAAkB;GAChB,eAAe,WAAW;;EAE7B"}
|