@jchat/runtime 1.0.0-alpha.1 → 1.0.0-alpha.10

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/src/check.ts DELETED
@@ -1,202 +0,0 @@
1
- import crypto from "node:crypto"
2
- import { recordAudit } from "./audit"
3
- import { normalizeDomain } from "./domain"
4
- import { parsePayload, verifyEntitlement, type RuntimeOptions } from "./index"
5
- import { setRuntimeStatus } from "./runtime"
6
-
7
- const DEFAULT_REGISTRY_ENDPOINT = Buffer.from("aHR0cDovL2xvY2FsaG9zdDo0MDAw", "base64").toString(
8
- "utf8"
9
- )
10
-
11
- export type CheckResult =
12
- | { status: "valid"; token: string }
13
- | { status: "unreachable-fallback"; token: string; error: string }
14
- | { status: "invalid"; error: string; reference: string }
15
-
16
- export type RegistryProvider = "envato" | "jchat"
17
- export type RegistryRoutingMode = "path" | "subdomain"
18
-
19
- export type ActivationInput = {
20
- purchaseCode: string
21
- domain: string
22
- siteSlug?: string
23
- domainScope?: "current-host" | "parent-domain"
24
- provider: RegistryProvider
25
- routingMode: RegistryRoutingMode
26
- installationId: string
27
- installationPublicKey: string
28
- machineFingerprint: string
29
- registryEndpoint?: string
30
- fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
31
- }
32
-
33
- export type ActivationResult =
34
- | { success: true; buyer: string; token: string; domain: string }
35
- | { success: false; message: string }
36
-
37
- export async function activateRegistry(input: ActivationInput): Promise<ActivationResult> {
38
- try {
39
- const response = await (input.fetchImpl ?? fetch)(
40
- `${input.registryEndpoint ?? DEFAULT_REGISTRY_ENDPOINT}/api/activate`,
41
- {
42
- method: "POST",
43
- headers: { "Content-Type": "application/json" },
44
- body: JSON.stringify({
45
- purchaseCode: input.purchaseCode,
46
- domain: normalizeDomain(input.domain).host,
47
- siteSlug: input.siteSlug,
48
- domainScope: input.domainScope,
49
- provider: input.provider,
50
- routingMode: input.routingMode,
51
- installationId: input.installationId,
52
- installationPublicKey: input.installationPublicKey,
53
- machineFingerprint: input.machineFingerprint
54
- })
55
- }
56
- )
57
- const body = (await response.json()) as {
58
- success?: boolean
59
- buyer?: string
60
- token?: string
61
- domain?: string
62
- message?: string
63
- }
64
- if (response.ok === false || !body.success || !body.token) {
65
- return {
66
- success: false,
67
- message:
68
- body.message ??
69
- "This license is already active on another domain. Please deactivate it first."
70
- }
71
- }
72
- return {
73
- success: true,
74
- buyer: body.buyer ?? "Customer",
75
- token: body.token,
76
- domain: body.domain ?? normalizeDomain(input.domain).host
77
- }
78
- } catch {
79
- return {
80
- success: false,
81
- message: "Failed to connect to the central registry. Please try again later."
82
- }
83
- }
84
- }
85
-
86
- export type CheckInput = {
87
- token: string
88
- domain: string
89
- routingMode: RegistryRoutingMode
90
- installationId: string
91
- machineFingerprint: string
92
- registryEndpoint?: string
93
- runtimeOptions?: RuntimeOptions
94
- auditPath?: string
95
- fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
96
- localTokenValidator?: (
97
- token: string,
98
- domain: string,
99
- installationId: string,
100
- machineFingerprint: string
101
- ) => boolean
102
- }
103
-
104
- export async function checkRegistry(input: CheckInput): Promise<CheckResult> {
105
- const fetchImpl = input.fetchImpl ?? fetch
106
- const localTokenValidator =
107
- input.localTokenValidator ??
108
- ((token, domain, installationId, machineFingerprint) =>
109
- input.runtimeOptions
110
- ? verifyEntitlement(token, domain, installationId, machineFingerprint, input.runtimeOptions)
111
- : false)
112
-
113
- const reject = (domain: string, error: string): CheckResult => {
114
- const reference = crypto.randomUUID()
115
- setRuntimeStatus("invalid", reference)
116
- void recordAudit("check_rejected", { domain, reference }, input.auditPath)
117
- return { status: "invalid", error, reference }
118
- }
119
-
120
- const useLocalFallback = (error: string): CheckResult => {
121
- const normalizedDomain = normalizeDomain(input.domain).host
122
- if (
123
- !localTokenValidator(
124
- input.token,
125
- normalizedDomain,
126
- input.installationId,
127
- input.machineFingerprint
128
- )
129
- ) {
130
- return reject(normalizedDomain, "Local license validation failed.")
131
- }
132
- setRuntimeStatus("unreachable-fallback")
133
- void recordAudit("check_unreachable", { domain: normalizedDomain }, input.auditPath)
134
- return { status: "unreachable-fallback", token: input.token, error }
135
- }
136
-
137
- try {
138
- const normalizedDomain = normalizeDomain(input.domain).host
139
- const registryEndpoint = input.registryEndpoint ?? DEFAULT_REGISTRY_ENDPOINT
140
- const response = await fetchImpl(`${registryEndpoint}/api/check`, {
141
- method: "POST",
142
- headers: { "Content-Type": "application/json" },
143
- body: JSON.stringify({
144
- token: input.token,
145
- domain: normalizedDomain,
146
- routingMode: input.routingMode,
147
- installationId: input.installationId,
148
- machineFingerprint: input.machineFingerprint
149
- })
150
- })
151
-
152
- if (!response.ok) {
153
- if (response.status >= 500) return useLocalFallback(`HTTP ${response.status}`)
154
- return reject(normalizedDomain, "License rejected by jchat service.")
155
- }
156
-
157
- const body = (await response.json()) as { success?: boolean; token?: string }
158
- if (!body.success || !body.token) {
159
- return reject(normalizedDomain, "License rejected by jchat service.")
160
- }
161
-
162
- const isValid = input.runtimeOptions
163
- ? verifyEntitlement(
164
- body.token,
165
- normalizedDomain,
166
- input.installationId,
167
- input.machineFingerprint,
168
- input.runtimeOptions
169
- )
170
- : false
171
- if (!isValid && !input.localTokenValidator) {
172
- return reject(normalizedDomain, "License rejected by jchat service.")
173
- }
174
- if (
175
- input.localTokenValidator &&
176
- !input.localTokenValidator(
177
- body.token,
178
- normalizedDomain,
179
- input.installationId,
180
- input.machineFingerprint
181
- )
182
- ) {
183
- return reject(normalizedDomain, "License rejected by jchat service.")
184
- }
185
-
186
- const payload = parsePayload(body.token)
187
- if (
188
- !payload ||
189
- payload.installationId !== input.installationId ||
190
- payload.machineFingerprint !== input.machineFingerprint ||
191
- payload.routingMode !== input.routingMode
192
- ) {
193
- return reject(normalizedDomain, "License binding rejected by jchat service.")
194
- }
195
-
196
- setRuntimeStatus("valid")
197
- void recordAudit("check_success", { domain: normalizedDomain }, input.auditPath)
198
- return { status: "valid", token: body.token }
199
- } catch {
200
- return useLocalFallback("License service unreachable.")
201
- }
202
- }
@@ -1,45 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import {
3
- isAllowedConfiguredDomain,
4
- isPublicDomain,
5
- normalizeDomain,
6
- resolveActivationDomain
7
- } from "./domain"
8
-
9
- describe("instance domain", () => {
10
- test("normalizes and classifies public, local, and configured domains", () => {
11
- expect(normalizeDomain("HTTPS://WWW.Example.com.:8080")).toEqual({
12
- host: "example.com",
13
- isLocalhost: false
14
- })
15
- expect(normalizeDomain("localhost:3000")).toEqual({
16
- host: "localhost",
17
- isLocalhost: true
18
- })
19
- expect(isPublicDomain("example.com")).toBe(true)
20
- expect(isPublicDomain("localhost")).toBe(false)
21
- expect(isPublicDomain("192.0.2.10")).toBe(false)
22
- expect(isPublicDomain("chat.local")).toBe(false)
23
- expect(isPublicDomain("chat.test")).toBe(false)
24
- expect(isAllowedConfiguredDomain("chat.example.com")).toBe(true)
25
- expect(isAllowedConfiguredDomain("localhost:3000")).toBe(true)
26
- expect(isAllowedConfiguredDomain("chat.test")).toBe(false)
27
- expect(isAllowedConfiguredDomain("")).toBe(false)
28
- })
29
-
30
- test("resolves current-host and parent-domain activation scopes", () => {
31
- expect(resolveActivationDomain("demo.example.com", "extended", "path")).toBe("demo.example.com")
32
- expect(resolveActivationDomain("chat.example.com", "standard", "subdomain")).toBe(
33
- "chat.example.com"
34
- )
35
- expect(resolveActivationDomain("chat.example.com", "extended", "subdomain")).toBe(
36
- "chat.example.com"
37
- )
38
- expect(
39
- resolveActivationDomain("chat.example.com", "extended", "subdomain", "parent-domain")
40
- ).toBe("example.com")
41
- expect(
42
- resolveActivationDomain("chat.example.co.uk", "extended", "subdomain", "parent-domain")
43
- ).toBe("example.co.uk")
44
- })
45
- })
package/src/domain.ts DELETED
@@ -1,75 +0,0 @@
1
- import net from "node:net"
2
-
3
- export type Plan = "standard" | "extended"
4
- export type RoutingMode = "path" | "subdomain"
5
- export type DomainScope = "current-host" | "parent-domain"
6
-
7
- const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1"])
8
- const RESERVED_SUFFIXES = [".local", ".test", ".invalid", ".example"]
9
-
10
- export type NormalizedDomain = {
11
- host: string
12
- isLocalhost: boolean
13
- }
14
-
15
- /** Normalize the host used for instance binding; ports and www are not significant. */
16
- export function normalizeDomain(value: string): NormalizedDomain {
17
- const raw = value.trim()
18
- if (!raw) throw new Error("Instance domain is required")
19
-
20
- const url = new URL(raw.includes("://") ? raw : `https://${raw}`)
21
- const hostname = url.hostname.toLowerCase().replace(/\.$/, "")
22
- const host = hostname === "www.localhost" ? "localhost" : hostname.replace(/^www\./, "")
23
- if (!host || host.includes("@") || /\s/.test(host)) {
24
- throw new Error("Invalid instance domain")
25
- }
26
- return { host, isLocalhost: LOCAL_HOSTS.has(host) }
27
- }
28
-
29
- export function isPublicDomain(value: string): boolean {
30
- const { host, isLocalhost } = normalizeDomain(value)
31
- return (
32
- !isLocalhost &&
33
- net.isIP(host) === 0 &&
34
- host.includes(".") &&
35
- !RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix))
36
- )
37
- }
38
-
39
- export function isAllowedConfiguredDomain(value: string): boolean {
40
- try {
41
- const normalized = normalizeDomain(value)
42
- return normalized.isLocalhost || isPublicDomain(value)
43
- } catch {
44
- return false
45
- }
46
- }
47
-
48
- function getRootDomain(value: string): string | null {
49
- const { host, isLocalhost } = normalizeDomain(value)
50
- if (isLocalhost || net.isIP(host) !== 0) return null
51
-
52
- const parts = host.split(".")
53
- if (parts.length <= 2) return host
54
-
55
- const secondLevelTlds = new Set(["co", "com", "org", "net", "gov", "edu", "ac", "sch"])
56
- const topLevel = parts.at(-1) ?? ""
57
- const secondLevel = parts.at(-2) ?? ""
58
- if (secondLevelTlds.has(secondLevel) && topLevel.length === 2 && parts.length >= 3) {
59
- return parts.slice(-3).join(".")
60
- }
61
- return parts.slice(-2).join(".")
62
- }
63
-
64
- export function resolveActivationDomain(
65
- host: string,
66
- plan: Plan,
67
- routingMode: RoutingMode,
68
- domainScope: DomainScope = "current-host"
69
- ): string {
70
- const normalizedHost = normalizeDomain(host).host
71
- if (routingMode === "subdomain" && plan === "extended" && domainScope === "parent-domain") {
72
- return getRootDomain(normalizedHost) ?? normalizedHost
73
- }
74
- return normalizedHost
75
- }
package/src/env.test.ts DELETED
@@ -1,146 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { ServerConfigSchema } from "./env"
3
-
4
- const BASE = {
5
- JCHAT_PORT: "3000",
6
- JCHAT_COOKIE_SECRET: "secret",
7
- JCHAT_DATABASE_URL: "postgresql://user:pass@localhost:5432/jchat",
8
- JCHAT_REDIS_DATABASE_URL: "redis://default:pass@localhost:6379",
9
- JCHAT_STORAGE_TYPE: "LOCAL",
10
- JCHAT_BOT_NAME: "Bot",
11
- JCHAT_BOT_EMAIL: "bot@jchat.com",
12
- JCHAT_ADDON_ENCRYPTION_KEY: "1234567890123456",
13
- JCHAT_DOMAIN: "localhost"
14
- }
15
-
16
- describe("ServerConfigSchema", () => {
17
- it("parses the base environment and coerces the port", () => {
18
- const result = ServerConfigSchema.safeParse(BASE)
19
-
20
- expect(result.success).toBe(true)
21
- if (result.success) expect(result.data.JCHAT_PORT).toBe(3000)
22
- })
23
-
24
- it("defaults the omitted port to 3000", () => {
25
- const { JCHAT_PORT: _, ...environment } = BASE
26
- const result = ServerConfigSchema.safeParse(environment)
27
-
28
- expect(result.success).toBe(true)
29
- if (result.success) expect(result.data.JCHAT_PORT).toBe(3000)
30
- })
31
-
32
- it("rejects malformed, out-of-range, and reserved ports", () => {
33
- for (const port of ["abc", "0", "65536", "80", "443"]) {
34
- expect(ServerConfigSchema.safeParse({ ...BASE, JCHAT_PORT: port }).success).toBe(false)
35
- }
36
- })
37
-
38
- it("validates PostgreSQL and Redis connection URLs", () => {
39
- const cases: Array<[Record<string, string>, boolean]> = [
40
- [{ JCHAT_DATABASE_URL: "mysql://user:pass@localhost:3306/db" }, false],
41
- [{ JCHAT_DATABASE_URL: "postgresql://no-port" }, false],
42
- [{ JCHAT_REDIS_DATABASE_URL: "rediss://default:pass@localhost:6380" }, true],
43
- [{ JCHAT_REDIS_DATABASE_URL: "http://localhost:6379" }, false],
44
- [{ JCHAT_REDIS_DATABASE_URL: "redis://default:pass@localhost" }, false]
45
- ]
46
-
47
- for (const [override, expected] of cases) {
48
- expect(ServerConfigSchema.safeParse({ ...BASE, ...override }).success).toBe(expected)
49
- }
50
- })
51
-
52
- it("accepts only a full main-domain public site origin", () => {
53
- const cases: Array<[string, boolean]> = [
54
- ["https://example.com", true],
55
- ["example.com", false],
56
- ["https://www.example.com", false]
57
- ]
58
-
59
- for (const [siteUrl, expected] of cases) {
60
- expect(ServerConfigSchema.safeParse({ ...BASE, JCHAT_SITE_URL: siteUrl }).success).toBe(
61
- expected
62
- )
63
- }
64
- })
65
-
66
- it("allows a missing domain during initial installation", () => {
67
- const { JCHAT_DOMAIN: _, ...environment } = BASE
68
- const result = ServerConfigSchema.safeParse(environment)
69
-
70
- expect(result.success).toBe(true)
71
- if (result.success) expect(result.data.JCHAT_DOMAIN).toBeUndefined()
72
- })
73
-
74
- it("accepts root and nested domains in supported routing modes", () => {
75
- const cases = [
76
- { JCHAT_DOMAIN: "example.com" },
77
- { JCHAT_DOMAIN: "chat.example.com", JCHAT_ROUTING_MODE: "path" },
78
- { JCHAT_DOMAIN: "chat.example.com", JCHAT_ROUTING_MODE: "subdomain" }
79
- ]
80
-
81
- for (const override of cases) {
82
- expect(ServerConfigSchema.safeParse({ ...BASE, ...override }).success).toBe(true)
83
- }
84
- })
85
-
86
- it("requires provider-specific storage settings", () => {
87
- const providers = [
88
- {
89
- type: "MINIO",
90
- config: {
91
- JCHAT_MINIO_ENDPOINT: "http://localhost:9000",
92
- JCHAT_MINIO_BUCKET_NAME: "bucket",
93
- JCHAT_MINIO_ACCESS_KEY_ID: "key",
94
- JCHAT_MINIO_SECRET_ACCESS_KEY: "secret",
95
- JCHAT_MINIO_PUBLIC_URL: "https://files.example.com"
96
- }
97
- },
98
- {
99
- type: "AWS_S3",
100
- config: {
101
- JCHAT_AWS_REGION: "us-east-1",
102
- JCHAT_AWS_BUCKET_NAME: "bucket",
103
- JCHAT_AWS_ACCESS_KEY_ID: "key",
104
- JCHAT_AWS_SECRET_ACCESS_KEY: "secret"
105
- }
106
- },
107
- {
108
- type: "CLOUDFLARE_R2",
109
- config: {
110
- JCHAT_CLOUDFLARE_ACCOUNT_ID: "account",
111
- JCHAT_CLOUDFLARE_BUCKET_NAME: "bucket",
112
- JCHAT_CLOUDFLARE_ACCESS_KEY_ID: "key",
113
- JCHAT_CLOUDFLARE_SECRET_ACCESS_KEY: "secret",
114
- JCHAT_CLOUDFLARE_PUBLIC_URL: "https://cdn.example.com"
115
- }
116
- },
117
- {
118
- type: "GCS",
119
- config: {
120
- JCHAT_GCS_PROJECT_ID: "project",
121
- JCHAT_GCS_BUCKET_NAME: "bucket",
122
- JCHAT_GCS_CLIENT_EMAIL: "service@example.iam.gserviceaccount.com",
123
- JCHAT_GCS_PRIVATE_KEY: "-----BEGIN RSA PRIVATE KEY-----"
124
- }
125
- }
126
- ] as const
127
-
128
- for (const provider of providers) {
129
- expect(
130
- ServerConfigSchema.safeParse({
131
- ...BASE,
132
- JCHAT_STORAGE_TYPE: provider.type
133
- }).success
134
- ).toBe(false)
135
- expect(
136
- ServerConfigSchema.safeParse({
137
- ...BASE,
138
- JCHAT_STORAGE_TYPE: provider.type,
139
- ...provider.config
140
- }).success
141
- ).toBe(true)
142
- }
143
-
144
- expect(ServerConfigSchema.safeParse({ ...BASE, JCHAT_STORAGE_TYPE: "FTP" }).success).toBe(false)
145
- })
146
- })