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

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/env.ts DELETED
@@ -1,261 +0,0 @@
1
- import { StorageType, isMainDomain } from "@jchat/shared"
2
- import { replaceKeys } from "string-ts"
3
- import * as z from "zod"
4
-
5
- export function makeTypedEnv<T>(schema: { parse: (data: unknown) => T }) {
6
- return (args: Record<string, unknown>) => schema.parse(replaceKeys(args, "VITE_", ""))
7
- }
8
-
9
- export const ServerConfigSchema = z
10
- .object({
11
- JCHAT_PORT: z
12
- .string()
13
- .regex(/^\d+$/)
14
- .transform(Number)
15
- .refine((port) => port >= 1 && port <= 65535, {
16
- message: "Port must be between 1 and 65535"
17
- })
18
- .refine((port) => port !== 80 && port !== 443, {
19
- message: "Avoid ports 80/443 (reserved for HTTP/HTTPS)"
20
- })
21
- .default(3000),
22
-
23
- JCHAT_COOKIE_SECRET: z.string().min(1),
24
-
25
- //DATABASE CONFIG
26
- JCHAT_DATABASE_URL: z
27
- .url()
28
- .startsWith("postgresql://")
29
- .regex(/^postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)(\?.*)?$/, {
30
- message: "Must be in format: postgresql://username:password@host:port/database?schema"
31
- }),
32
- JCHAT_REDIS_DATABASE_URL: z
33
- .url()
34
- .refine((url) => url.startsWith("rediss://") || url.startsWith("redis://"), {
35
- message: "Must use redis:// or rediss:// protocol"
36
- })
37
- .refine(
38
- (url) => {
39
- const pattern = /^(rediss?):\/\/(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/
40
- return pattern.test(url)
41
- },
42
- {
43
- message: "Must be in format: rediss://default:<password>@<host>:<port>"
44
- }
45
- ),
46
-
47
- JCHAT_STORAGE_TYPE: z.enum(StorageType),
48
-
49
- JCHAT_MINIO_ENDPOINT: z.string().optional(),
50
- JCHAT_MINIO_BUCKET_NAME: z.string().optional(),
51
- JCHAT_MINIO_ACCESS_KEY_ID: z.string().optional(),
52
- JCHAT_MINIO_SECRET_ACCESS_KEY: z.string().optional(),
53
- JCHAT_MINIO_PUBLIC_URL: z.string().optional(),
54
-
55
- JCHAT_AWS_REGION: z.string().optional(),
56
- JCHAT_AWS_BUCKET_NAME: z.string().optional(),
57
- JCHAT_AWS_ACCESS_KEY_ID: z.string().optional(),
58
- JCHAT_AWS_SECRET_ACCESS_KEY: z.string().optional(),
59
-
60
- JCHAT_CLOUDFLARE_ACCOUNT_ID: z.string().optional(),
61
- JCHAT_CLOUDFLARE_BUCKET_NAME: z.string().optional(),
62
- JCHAT_CLOUDFLARE_ACCESS_KEY_ID: z.string().optional(),
63
- JCHAT_CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(),
64
- JCHAT_CLOUDFLARE_PUBLIC_URL: z.string().optional(),
65
-
66
- JCHAT_GCS_BUCKET_NAME: z.string().optional(),
67
- JCHAT_GCS_PROJECT_ID: z.string().optional(),
68
- JCHAT_GCS_CLIENT_EMAIL: z.string().optional(),
69
- JCHAT_GCS_PRIVATE_KEY: z.string().optional(),
70
- JCHAT_GCS_KEY_FILE: z.string().optional(),
71
-
72
- JCHAT_BOT_NAME: z.string().trim().min(3),
73
- JCHAT_BOT_EMAIL: z.email(),
74
-
75
- JCHAT_DOMAIN: z.string().optional(),
76
- JCHAT_SITE_URL: z.url().optional(),
77
- JCHAT_ROUTING_MODE: z.enum(["path", "subdomain"]).default("subdomain"),
78
-
79
- JCHAT_ADDON_ENCRYPTION_KEY: z.string().min(16),
80
-
81
- NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
82
-
83
- SYSTEM_BOOTSTRAP_COMPLETE: z.boolean().optional()
84
- })
85
- .superRefine((env, ctx) => {
86
- if (env.JCHAT_SITE_URL) {
87
- let siteHostname: string | undefined
88
- try {
89
- siteHostname = new URL(env.JCHAT_SITE_URL).hostname
90
- } catch {}
91
-
92
- if (siteHostname && !isMainDomain(siteHostname)) {
93
- ctx.addIssue({
94
- path: ["JCHAT_SITE_URL"],
95
- code: "custom",
96
- message: "JCHAT_SITE_URL must use the client main/root domain"
97
- })
98
- }
99
- }
100
-
101
- if (
102
- env.JCHAT_STORAGE_TYPE === "MINIO" &&
103
- !(
104
- env.JCHAT_MINIO_ENDPOINT &&
105
- env.JCHAT_MINIO_BUCKET_NAME &&
106
- env.JCHAT_MINIO_ACCESS_KEY_ID &&
107
- env.JCHAT_MINIO_SECRET_ACCESS_KEY &&
108
- env.JCHAT_MINIO_PUBLIC_URL
109
- )
110
- ) {
111
- if (!env.JCHAT_MINIO_ENDPOINT)
112
- ctx.addIssue({
113
- path: ["JCHAT_MINIO_ENDPOINT"],
114
- code: "custom",
115
- message: "JCHAT_MINIO_ENDPOINT is required when JCHAT_STORAGE_TYPE is MINIO"
116
- })
117
- if (!env.JCHAT_MINIO_BUCKET_NAME)
118
- ctx.addIssue({
119
- path: ["JCHAT_MINIO_BUCKET_NAME"],
120
- code: "custom",
121
- message: "JCHAT_MINIO_BUCKET_NAME is required when JCHAT_STORAGE_TYPE is MINIO"
122
- })
123
- if (!env.JCHAT_MINIO_ACCESS_KEY_ID)
124
- ctx.addIssue({
125
- path: ["JCHAT_MINIO_ACCESS_KEY_ID"],
126
- code: "custom",
127
- message: "JCHAT_MINIO_ACCESS_KEY_ID is required when JCHAT_STORAGE_TYPE is MINIO"
128
- })
129
- if (!env.JCHAT_MINIO_SECRET_ACCESS_KEY)
130
- ctx.addIssue({
131
- path: ["JCHAT_MINIO_SECRET_ACCESS_KEY"],
132
- code: "custom",
133
- message: "JCHAT_MINIO_SECRET_ACCESS_KEY is required when JCHAT_STORAGE_TYPE is MINIO"
134
- })
135
- if (!env.JCHAT_MINIO_PUBLIC_URL)
136
- ctx.addIssue({
137
- path: ["JCHAT_MINIO_PUBLIC_URL"],
138
- code: "custom",
139
- message: "JCHAT_MINIO_PUBLIC_URL is required when JCHAT_STORAGE_TYPE is MINIO"
140
- })
141
- }
142
-
143
- if (
144
- env.JCHAT_STORAGE_TYPE === "AWS_S3" &&
145
- !(
146
- env.JCHAT_AWS_REGION &&
147
- env.JCHAT_AWS_ACCESS_KEY_ID &&
148
- env.JCHAT_AWS_SECRET_ACCESS_KEY &&
149
- env.JCHAT_AWS_BUCKET_NAME
150
- )
151
- ) {
152
- if (!env.JCHAT_AWS_REGION)
153
- ctx.addIssue({
154
- path: ["JCHAT_AWS_REGION"],
155
- code: "custom",
156
- message: "JCHAT_AWS_REGION is required when JCHAT_STORAGE_TYPE is AWS_S3"
157
- })
158
- if (!env.JCHAT_AWS_ACCESS_KEY_ID)
159
- ctx.addIssue({
160
- path: ["JCHAT_AWS_ACCESS_KEY_ID"],
161
- code: "custom",
162
- message: "JCHAT_AWS_ACCESS_KEY_ID is required when JCHAT_STORAGE_TYPE is AWS_S3"
163
- })
164
- if (!env.JCHAT_AWS_SECRET_ACCESS_KEY)
165
- ctx.addIssue({
166
- path: ["JCHAT_AWS_SECRET_ACCESS_KEY"],
167
- code: "custom",
168
- message: "JCHAT_AWS_SECRET_ACCESS_KEY is required when JCHAT_STORAGE_TYPE is AWS_S3"
169
- })
170
- if (!env.JCHAT_AWS_BUCKET_NAME)
171
- ctx.addIssue({
172
- path: ["JCHAT_AWS_BUCKET_NAME"],
173
- code: "custom",
174
- message: "JCHAT_AWS_BUCKET_NAME is required when JCHAT_STORAGE_TYPE is AWS_S3"
175
- })
176
- }
177
-
178
- if (
179
- env.JCHAT_STORAGE_TYPE === "CLOUDFLARE_R2" &&
180
- !(
181
- env.JCHAT_CLOUDFLARE_ACCOUNT_ID &&
182
- env.JCHAT_CLOUDFLARE_ACCESS_KEY_ID &&
183
- env.JCHAT_CLOUDFLARE_SECRET_ACCESS_KEY &&
184
- env.JCHAT_CLOUDFLARE_BUCKET_NAME &&
185
- env.JCHAT_CLOUDFLARE_PUBLIC_URL
186
- )
187
- ) {
188
- if (!env.JCHAT_CLOUDFLARE_ACCOUNT_ID)
189
- ctx.addIssue({
190
- path: ["JCHAT_CLOUDFLARE_ACCOUNT_ID"],
191
- code: "custom",
192
- message:
193
- "JCHAT_CLOUDFLARE_ACCOUNT_ID is required when JCHAT_STORAGE_TYPE is CLOUDFLARE_R2"
194
- })
195
- if (!env.JCHAT_CLOUDFLARE_ACCESS_KEY_ID)
196
- ctx.addIssue({
197
- path: ["JCHAT_CLOUDFLARE_ACCESS_KEY_ID"],
198
- code: "custom",
199
- message:
200
- "JCHAT_CLOUDFLARE_ACCESS_KEY_ID is required when JCHAT_STORAGE_TYPE is CLOUDFLARE_R2"
201
- })
202
- if (!env.JCHAT_CLOUDFLARE_SECRET_ACCESS_KEY)
203
- ctx.addIssue({
204
- path: ["JCHAT_CLOUDFLARE_SECRET_ACCESS_KEY"],
205
- code: "custom",
206
- message:
207
- "JCHAT_CLOUDFLARE_SECRET_ACCESS_KEY is required when JCHAT_STORAGE_TYPE is CLOUDFLARE_R2"
208
- })
209
- if (!env.JCHAT_CLOUDFLARE_BUCKET_NAME)
210
- ctx.addIssue({
211
- path: ["JCHAT_CLOUDFLARE_BUCKET_NAME"],
212
- code: "custom",
213
- message:
214
- "JCHAT_CLOUDFLARE_BUCKET_NAME is required when JCHAT_STORAGE_TYPE is CLOUDFLARE_R2"
215
- })
216
- if (!env.JCHAT_CLOUDFLARE_PUBLIC_URL)
217
- ctx.addIssue({
218
- path: ["JCHAT_CLOUDFLARE_PUBLIC_URL"],
219
- code: "custom",
220
- message:
221
- "JCHAT_CLOUDFLARE_PUBLIC_URL is required when JCHAT_STORAGE_TYPE is CLOUDFLARE_R2"
222
- })
223
- }
224
-
225
- if (
226
- env.JCHAT_STORAGE_TYPE === "GCS" &&
227
- !(
228
- env.JCHAT_GCS_PROJECT_ID &&
229
- env.JCHAT_GCS_BUCKET_NAME &&
230
- env.JCHAT_GCS_CLIENT_EMAIL &&
231
- env.JCHAT_GCS_PRIVATE_KEY
232
- )
233
- ) {
234
- if (!env.JCHAT_GCS_PROJECT_ID)
235
- ctx.addIssue({
236
- path: ["JCHAT_GCS_PROJECT_ID"],
237
- code: "custom",
238
- message: "JCHAT_GCS_PROJECT_ID is required when JCHAT_STORAGE_TYPE is GCS"
239
- })
240
- if (!env.JCHAT_GCS_BUCKET_NAME)
241
- ctx.addIssue({
242
- path: ["JCHAT_GCS_BUCKET_NAME"],
243
- code: "custom",
244
- message: "JCHAT_GCS_BUCKET_NAME is required when JCHAT_STORAGE_TYPE is GCS"
245
- })
246
- if (!env.JCHAT_GCS_CLIENT_EMAIL)
247
- ctx.addIssue({
248
- path: ["JCHAT_GCS_CLIENT_EMAIL"],
249
- code: "custom",
250
- message: "JCHAT_GCS_CLIENT_EMAIL is required when JCHAT_STORAGE_TYPE is GCS"
251
- })
252
- if (!env.JCHAT_GCS_PRIVATE_KEY)
253
- ctx.addIssue({
254
- path: ["JCHAT_GCS_PRIVATE_KEY"],
255
- code: "custom",
256
- message: "JCHAT_GCS_PRIVATE_KEY is required when JCHAT_STORAGE_TYPE is GCS"
257
- })
258
- }
259
- })
260
-
261
- export type ServerConfig = z.infer<typeof ServerConfigSchema>
package/src/index.test.ts DELETED
@@ -1,117 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import crypto from "node:crypto"
3
- import { hasEntitlement, parsePayload, verifyEntitlement, verifySignature } from "./index"
4
-
5
- const keyPair = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 })
6
- const publicKeyPem = keyPair.publicKey.export({ type: "spki", format: "pem" }).toString()
7
-
8
- function createToken(
9
- overrides: Record<string, unknown> = {},
10
- headerOverrides: Record<string, unknown> = {}
11
- ) {
12
- const header = Buffer.from(
13
- JSON.stringify({ alg: "RS256", typ: "JWT", ...headerOverrides })
14
- ).toString("base64url")
15
- const payload = Buffer.from(
16
- JSON.stringify({
17
- iss: "https://license.jchat.local",
18
- aud: "jchat-chat",
19
- licenseId: "license-1",
20
- domain: "www.example.com:8080",
21
- buyer: "Customer",
22
- installationId: "install-1",
23
- machineFingerprint: "machine-1",
24
- plan: "extended",
25
- entitlements: ["site.create.multi"],
26
- purchaseProvider: "envato",
27
- environment: "production",
28
- routingMode: "path",
29
- iat: 1,
30
- ...overrides
31
- })
32
- ).toString("base64url")
33
- const signingInput = `${header}.${payload}`
34
- const signature = crypto
35
- .sign("sha256", Buffer.from(signingInput), keyPair.privateKey)
36
- .toString("base64url")
37
- return `${signingInput}.${signature}`
38
- }
39
-
40
- const options = { publicKeyPem }
41
-
42
- describe("license runtime", () => {
43
- test("parses and verifies a normalized perpetual public-domain license", () => {
44
- const token = createToken()
45
- const payload = parsePayload(token)
46
- expect(payload?.licenseId).toBe("license-1")
47
- expect(payload).not.toHaveProperty("exp")
48
- expect(verifySignature(token, "example.com", options)).toBe(true)
49
- expect(verifyEntitlement(token, "example.com:3000", "install-1", "machine-1", options)).toBe(
50
- true
51
- )
52
- expect(hasEntitlement(token, "www.example.com", "site.create.multi", options)).toBe(true)
53
- })
54
-
55
- test("rejects forged signatures and binding mismatches", () => {
56
- const token = createToken()
57
- expect(verifySignature(`${token}x`, "example.com", options)).toBe(false)
58
- expect(verifySignature(token, "other.example.com", options)).toBe(false)
59
- expect(verifyEntitlement(token, "example.com", "other-install", "machine-1", options)).toBe(
60
- false
61
- )
62
- expect(verifyEntitlement(token, "example.com", "install-1", "other-machine", options)).toBe(
63
- false
64
- )
65
- })
66
-
67
- test("rejects issuer, audience, and algorithm mismatches", () => {
68
- expect(
69
- verifySignature(createToken({ iss: "https://other.example" }), "example.com", options)
70
- ).toBe(false)
71
- expect(verifySignature(createToken({ aud: "other-app" }), "example.com", options)).toBe(false)
72
- expect(verifySignature(createToken({}, { alg: "HS256" }), "example.com", options)).toBe(false)
73
- })
74
-
75
- test("rejects malformed and incomplete claims", () => {
76
- for (const overrides of [
77
- { licenseId: undefined },
78
- { domain: 42 },
79
- { buyer: "" },
80
- { installationId: undefined },
81
- { machineFingerprint: undefined },
82
- { plan: "multi" },
83
- { entitlements: "site.create.multi" },
84
- { purchaseProvider: "unknown" },
85
- { environment: "staging" },
86
- { routingMode: "host" },
87
- { iat: "now" }
88
- ]) {
89
- expect(parsePayload(createToken(overrides))).toBeNull()
90
- }
91
- expect(parsePayload("header.invalid.signature")).toBeNull()
92
- })
93
-
94
- test("enforces standard and extended entitlement consistency", () => {
95
- const standard = createToken({ plan: "standard", entitlements: [] })
96
- expect(verifyEntitlement(standard, "example.com", "install-1", "machine-1", options)).toBe(true)
97
- expect(hasEntitlement(standard, "example.com", "site.create.multi", options)).toBe(false)
98
-
99
- expect(parsePayload(createToken({ plan: "standard" }))).toBeNull()
100
- expect(parsePayload(createToken({ plan: "extended", entitlements: [] }))).toBeNull()
101
- })
102
-
103
- test("allows only vendor-issued development claims on localhost", () => {
104
- const development = createToken({
105
- domain: "localhost",
106
- plan: "standard",
107
- entitlements: [],
108
- environment: "development"
109
- })
110
- expect(
111
- verifyEntitlement(development, "www.localhost:3000", "install-1", "machine-1", options)
112
- ).toBe(true)
113
-
114
- expect(parsePayload(createToken({ domain: "localhost" }))).toBeNull()
115
- expect(parsePayload(createToken({ environment: "development" }))).toBeNull()
116
- })
117
- })
package/src/index.ts DELETED
@@ -1,206 +0,0 @@
1
- import crypto from "node:crypto"
2
- import { isPublicDomain, normalizeDomain, type Plan, type RoutingMode } from "./domain"
3
-
4
- export * from "./audit"
5
- export * from "./check"
6
- export * from "./domain"
7
- export * from "./state"
8
- export * from "./runtime"
9
- export * from "./install"
10
- export * from "./paths"
11
- export * from "./env"
12
-
13
- export type PurchaseProvider = "envato" | "jchat"
14
- export type RuntimeEnvironment = "production" | "development"
15
-
16
- export type ConfigPayload = {
17
- iss: string
18
- aud: string
19
- licenseId: string
20
- domain: string
21
- buyer: string
22
- installationId: string
23
- machineFingerprint: string
24
- plan: Plan
25
- entitlements: string[]
26
- purchaseProvider: PurchaseProvider
27
- environment: RuntimeEnvironment
28
- routingMode: RoutingMode
29
- iat: number
30
- }
31
-
32
- export type RuntimeOptions = {
33
- publicKeyPem: string
34
- issuer?: string
35
- audience?: string
36
- }
37
-
38
- export type RuntimeStatus = "valid" | "unreachable-fallback" | "invalid" | "unavailable"
39
-
40
- type JwtHeader = {
41
- alg: "RS256"
42
- typ: "JWT"
43
- }
44
-
45
- function isRecord(value: unknown): value is Record<string, unknown> {
46
- return typeof value === "object" && value !== null && !Array.isArray(value)
47
- }
48
-
49
- function isNonEmptyString(value: unknown): value is string {
50
- return typeof value === "string" && value.trim().length > 0
51
- }
52
-
53
- function decodeJsonSegment(value: string): unknown {
54
- if (!value) return null
55
- try {
56
- return JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as unknown
57
- } catch {
58
- return null
59
- }
60
- }
61
-
62
- function parseHeader(token: string): JwtHeader | null {
63
- const parts = token.split(".")
64
- if (parts.length !== 3) return null
65
- const header = decodeJsonSegment(parts[0])
66
- if (!isRecord(header) || header.alg !== "RS256" || header.typ !== "JWT") return null
67
- return { alg: "RS256", typ: "JWT" }
68
- }
69
-
70
- function validatePayload(value: unknown): ConfigPayload | null {
71
- if (!isRecord(value)) return null
72
-
73
- const {
74
- iss,
75
- aud,
76
- licenseId,
77
- domain,
78
- buyer,
79
- installationId,
80
- machineFingerprint,
81
- plan,
82
- entitlements,
83
- purchaseProvider,
84
- environment,
85
- routingMode,
86
- iat
87
- } = value
88
-
89
- if (
90
- !isNonEmptyString(iss) ||
91
- !isNonEmptyString(aud) ||
92
- !isNonEmptyString(licenseId) ||
93
- !isNonEmptyString(domain) ||
94
- !isNonEmptyString(buyer) ||
95
- !isNonEmptyString(installationId) ||
96
- !isNonEmptyString(machineFingerprint) ||
97
- (plan !== "standard" && plan !== "extended") ||
98
- !Array.isArray(entitlements) ||
99
- !entitlements.every(isNonEmptyString) ||
100
- (purchaseProvider !== "envato" && purchaseProvider !== "jchat") ||
101
- (environment !== "production" && environment !== "development") ||
102
- (routingMode !== "path" && routingMode !== "subdomain") ||
103
- typeof iat !== "number" ||
104
- !Number.isSafeInteger(iat) ||
105
- iat < 0
106
- ) {
107
- return null
108
- }
109
-
110
- let normalizedDomain
111
- try {
112
- normalizedDomain = normalizeDomain(domain)
113
- } catch {
114
- return null
115
- }
116
-
117
- if (
118
- (environment === "development" && !normalizedDomain.isLocalhost) ||
119
- (environment === "production" && !isPublicDomain(domain))
120
- ) {
121
- return null
122
- }
123
-
124
- const hasMultiSiteEntitlement = entitlements.includes("site.create.multi")
125
- if (
126
- (plan === "extended" && !hasMultiSiteEntitlement) ||
127
- (plan === "standard" && hasMultiSiteEntitlement)
128
- ) {
129
- return null
130
- }
131
-
132
- return {
133
- iss,
134
- aud,
135
- licenseId,
136
- domain,
137
- buyer,
138
- installationId,
139
- machineFingerprint,
140
- plan,
141
- entitlements,
142
- purchaseProvider,
143
- environment,
144
- routingMode,
145
- iat
146
- }
147
- }
148
-
149
- export function parsePayload(token: string): ConfigPayload | null {
150
- const parts = token.split(".")
151
- if (parts.length !== 3 || !parts.every(Boolean) || !parseHeader(token)) return null
152
- return validatePayload(decodeJsonSegment(parts[1]))
153
- }
154
-
155
- export function verifySignature(
156
- token: string,
157
- currentDomain: string,
158
- options: RuntimeOptions
159
- ): boolean {
160
- if (!token || token === "YOUR_LICENSE_KEY_PLACEHOLDER") return false
161
-
162
- try {
163
- const parts = token.split(".")
164
- if (parts.length !== 3 || !parseHeader(token)) return false
165
-
166
- const payload = parsePayload(token)
167
- if (!payload) return false
168
- if (payload.iss !== (options.issuer ?? "https://license.jchat.local")) return false
169
- if (payload.aud !== (options.audience ?? "jchat-chat")) return false
170
- if (normalizeDomain(payload.domain).host !== normalizeDomain(currentDomain).host) {
171
- return false
172
- }
173
-
174
- const dataToVerify = Buffer.from(`${parts[0]}.${parts[1]}`)
175
- const signature = Buffer.from(parts[2].replace(/-/g, "+").replace(/_/g, "/"), "base64")
176
- return crypto.verify("sha256", dataToVerify, options.publicKeyPem, signature)
177
- } catch {
178
- return false
179
- }
180
- }
181
-
182
- export function verifyEntitlement(
183
- token: string,
184
- currentDomain: string,
185
- installationId: string,
186
- machineFingerprint: string,
187
- options: RuntimeOptions
188
- ): boolean {
189
- if (!verifySignature(token, currentDomain, options)) return false
190
-
191
- const payload = parsePayload(token)
192
- if (!payload) return false
193
- return (
194
- payload.installationId === installationId && payload.machineFingerprint === machineFingerprint
195
- )
196
- }
197
-
198
- export function hasEntitlement(
199
- token: string,
200
- currentDomain: string,
201
- entitlement: string,
202
- options: RuntimeOptions
203
- ): boolean {
204
- if (!verifySignature(token, currentDomain, options)) return false
205
- return parsePayload(token)?.entitlements?.includes(entitlement) ?? false
206
- }
package/src/install.ts DELETED
@@ -1,62 +0,0 @@
1
- import type { DomainScope, RoutingMode } from "./domain"
2
- import type { PurchaseProvider } from "./index"
3
- import { activateRegistry } from "./check"
4
- import { createIdentity, type LocalState } from "./state"
5
-
6
- export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
7
-
8
- export type LicenseVerificationResult =
9
- | {
10
- valid: true
11
- buyer: string
12
- token: string
13
- domain: string
14
- state: Omit<LocalState, "instanceToken" | "version">
15
- }
16
- | { valid: false; error: string }
17
-
18
- export async function installAndActivate(
19
- purchaseCode: string,
20
- domain: string,
21
- siteSlug: string | undefined,
22
- domainScope: DomainScope | undefined,
23
- routingMode: RoutingMode,
24
- provider: PurchaseProvider = "envato",
25
- fetchImpl: FetchLike = fetch
26
- ): Promise<LicenseVerificationResult> {
27
- const installationIdentity = createIdentity()
28
- try {
29
- const licenseData = await activateRegistry({
30
- purchaseCode,
31
- domain,
32
- siteSlug,
33
- domainScope,
34
- routingMode,
35
- provider,
36
- installationId: installationIdentity.installationId,
37
- installationPublicKey: installationIdentity.publicKeyPem,
38
- machineFingerprint: installationIdentity.machineFingerprint,
39
- fetchImpl
40
- })
41
-
42
- if (!licenseData.success) {
43
- return {
44
- valid: false,
45
- error: licenseData.message
46
- }
47
- }
48
-
49
- return {
50
- valid: true,
51
- buyer: licenseData.buyer,
52
- token: licenseData.token,
53
- domain: licenseData.domain,
54
- state: installationIdentity
55
- }
56
- } catch {
57
- return {
58
- valid: false,
59
- error: "Failed to verify license. Check your internet connection."
60
- }
61
- }
62
- }
package/src/paths.test.ts DELETED
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import path from "node:path"
3
- import { resolveRuntimePaths } from "./paths"
4
-
5
- describe("resolveRuntimePaths", () => {
6
- it("prefers explicit env paths", () => {
7
- const paths = resolveRuntimePaths({
8
- cwd: "D:/runtime",
9
- env: {
10
- JCHAT_UPLOADS_DIR: "/data/uploads",
11
- JCHAT_ADDONS_DIR: "/data/addons",
12
- JCHAT_MIGRATIONS_DIR: "/data/migrations"
13
- }
14
- })
15
-
16
- expect(paths.uploadsDir).toBe(path.resolve("/data/uploads"))
17
- expect(paths.addonBundlesDir).toBe(path.resolve("/data/addons"))
18
- expect(paths.addonSourceDir).toBe(path.join(path.resolve("D:/runtime"), "addons-dev"))
19
- expect(paths.migrationsDir).toBe(path.resolve("/data/migrations"))
20
- })
21
-
22
- it("uses repo-root fallbacks when running inside the source tree", () => {
23
- const repoRoot = path.resolve(import.meta.dir, "..", "..", "..")
24
- const paths = resolveRuntimePaths({
25
- cwd: path.join(repoRoot, "apps", "web"),
26
- env: {}
27
- })
28
-
29
- expect(paths.repoRoot).toBe(repoRoot)
30
- expect(paths.uploadsDir).toBe(path.join(repoRoot, "uploads"))
31
- expect(paths.addonBundlesDir).toBe(path.join(repoRoot, "addons"))
32
- expect(paths.addonSourceDir).toBe(path.join(repoRoot, "addons-dev"))
33
- expect(paths.migrationsDir).toBe(path.join(repoRoot, "packages", "db", "migrations"))
34
- })
35
-
36
- it("falls back to runtime-relative directories outside the repo", () => {
37
- const runtimeRoot = path.resolve("D:/isolated-runtime")
38
- const paths = resolveRuntimePaths({
39
- cwd: runtimeRoot,
40
- env: {}
41
- })
42
-
43
- expect(paths.repoRoot).toBeNull()
44
- expect(paths.uploadsDir).toBe(path.join(runtimeRoot, "uploads"))
45
- expect(paths.addonBundlesDir).toBe(path.join(runtimeRoot, "addons"))
46
- expect(paths.addonSourceDir).toBe(path.join(runtimeRoot, "addons-dev"))
47
- expect(paths.migrationsDir).toBe(path.join(runtimeRoot, "migrations"))
48
- })
49
- })