@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/paths.ts DELETED
@@ -1,74 +0,0 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
-
4
- interface RuntimePathOptions {
5
- cwd?: string
6
- env?: Record<string, string | undefined>
7
- }
8
-
9
- export function resolveRuntimePaths(options: RuntimePathOptions = {}) {
10
- const runtimeRoot = path.resolve(options.cwd ?? process.cwd())
11
- const env = options.env ?? process.env
12
- const repoRoot = findRepoRoot(runtimeRoot)
13
-
14
- return {
15
- repoRoot,
16
- runtimeRoot,
17
- uploadsDir: resolveConfiguredPath(
18
- env.JCHAT_UPLOADS_DIR,
19
- runtimeRoot,
20
- repoRoot,
21
- "uploads",
22
- "uploads"
23
- ),
24
- addonBundlesDir: resolveConfiguredPath(
25
- env.JCHAT_ADDONS_DIR,
26
- runtimeRoot,
27
- repoRoot,
28
- "addons",
29
- "addons"
30
- ),
31
- addonSourceDir: repoRoot
32
- ? path.join(repoRoot, "addons-dev")
33
- : path.join(runtimeRoot, "addons-dev"),
34
- migrationsDir: resolveConfiguredPath(
35
- env.JCHAT_MIGRATIONS_DIR,
36
- runtimeRoot,
37
- repoRoot,
38
- path.join("packages", "db", "migrations"),
39
- "migrations"
40
- )
41
- }
42
- }
43
-
44
- function hasRepoMarkers(dir: string) {
45
- return (
46
- fs.existsSync(path.join(dir, "package.json")) &&
47
- fs.existsSync(path.join(dir, "apps")) &&
48
- fs.existsSync(path.join(dir, "packages"))
49
- )
50
- }
51
-
52
- function findRepoRoot(startDir: string) {
53
- let current = path.resolve(startDir)
54
-
55
- while (true) {
56
- if (hasRepoMarkers(current)) return current
57
-
58
- const parent = path.dirname(current)
59
- if (parent === current) return null
60
- current = parent
61
- }
62
- }
63
-
64
- function resolveConfiguredPath(
65
- configuredPath: string | undefined,
66
- runtimeRoot: string,
67
- repoRoot: string | null,
68
- repoRelativePath: string,
69
- runtimeRelativePath: string
70
- ) {
71
- if (configuredPath) return path.resolve(configuredPath)
72
- if (repoRoot) return path.join(repoRoot, repoRelativePath)
73
- return path.join(runtimeRoot, runtimeRelativePath)
74
- }
@@ -1,21 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import {
3
- getRuntimeStatus,
4
- isApplicationDisabled,
5
- registerInvalidationHandler,
6
- setRuntimeStatus
7
- } from "./runtime"
8
-
9
- describe("license runtime status", () => {
10
- test("disables the application and invokes the invalidation handler", () => {
11
- let shutdowns = 0
12
- registerInvalidationHandler(() => {
13
- shutdowns += 1
14
- })
15
- setRuntimeStatus("invalid", "support-1")
16
- expect(isApplicationDisabled()).toBe(true)
17
- expect(getRuntimeStatus()).toEqual({ status: "invalid", reference: "support-1" })
18
- expect(shutdowns).toBe(1)
19
- setRuntimeStatus("valid", "support-2")
20
- })
21
- })
package/src/runtime.ts DELETED
@@ -1,31 +0,0 @@
1
- import crypto from "node:crypto"
2
-
3
- export type RuntimeStatus = "valid" | "unreachable-fallback" | "invalid" | "unavailable"
4
-
5
- let runtimeStatus: RuntimeStatus = "unavailable"
6
- let supportReference = "startup"
7
- let shutdownHandler: (() => void) | undefined
8
-
9
- export function registerInvalidationHandler(handler: () => void): void {
10
- shutdownHandler = handler
11
- }
12
-
13
- export function setRuntimeStatus(
14
- status: RuntimeStatus,
15
- reference: string = crypto.randomUUID()
16
- ): void {
17
- runtimeStatus = status
18
- supportReference = reference
19
- if (status === "invalid") shutdownHandler?.()
20
- }
21
-
22
- export function getRuntimeStatus(): {
23
- status: RuntimeStatus
24
- reference: string
25
- } {
26
- return { status: runtimeStatus, reference: supportReference }
27
- }
28
-
29
- export function isApplicationDisabled(): boolean {
30
- return runtimeStatus === "invalid"
31
- }
package/src/state.test.ts DELETED
@@ -1,54 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test"
2
- import fs from "node:fs/promises"
3
- import os from "node:os"
4
- import path from "node:path"
5
- import { acquireProcessLock, createIdentity, readState, readTokenSync, writeState } from "./state"
6
-
7
- const temporaryDirectories: string[] = []
8
-
9
- async function createTemporaryDirectory(prefix: string): Promise<string> {
10
- const directory = await fs.mkdtemp(path.join(os.tmpdir(), prefix))
11
- temporaryDirectories.push(directory)
12
- return directory
13
- }
14
-
15
- describe("license state", () => {
16
- afterEach(async () => {
17
- await Promise.all(
18
- temporaryDirectories
19
- .splice(0)
20
- .map((directory) => fs.rm(directory, { recursive: true, force: true }))
21
- )
22
- })
23
-
24
- test("persists and reads a complete state atomically", async () => {
25
- const directory = await createTemporaryDirectory("jchat-runtime-")
26
- const filePath = path.join(directory, "license.json")
27
- const identity = createIdentity("machine-a")
28
- const state = { version: 1 as const, ...identity, instanceToken: "signed-license" }
29
- await writeState(state, filePath)
30
- expect(await readState(filePath)).toEqual(state)
31
- expect(readTokenSync(filePath)).toBe("signed-license")
32
- })
33
-
34
- test("rejects incomplete local state", async () => {
35
- const directory = await createTemporaryDirectory("jchat-runtime-invalid-")
36
- const filePath = path.join(directory, "license.json")
37
- await Bun.write(
38
- filePath,
39
- JSON.stringify({ version: 1, installationId: "installation-1", instanceToken: "token" })
40
- )
41
-
42
- expect(await readState(filePath)).toBeNull()
43
- })
44
-
45
- test("prevents two processes from acquiring the same local lock", async () => {
46
- const directory = await createTemporaryDirectory("jchat-runtime-lock-")
47
- const filePath = path.join(directory, "license.json")
48
- const release = await acquireProcessLock(filePath)
49
- await expect(acquireProcessLock(filePath)).rejects.toBeTruthy()
50
- await release()
51
- const secondRelease = await acquireProcessLock(filePath)
52
- await secondRelease()
53
- })
54
- })
package/src/state.ts DELETED
@@ -1,129 +0,0 @@
1
- import crypto from "node:crypto"
2
- import fsSync from "node:fs"
3
- import fs from "node:fs/promises"
4
- import os from "node:os"
5
- import path from "node:path"
6
-
7
- export type LocalState = {
8
- version: 1
9
- installationId: string
10
- privateKeyPem: string
11
- publicKeyPem: string
12
- machineFingerprint: string
13
- instanceToken: string
14
- lastSuccessfulCheckAt?: string
15
- }
16
-
17
- export const DEFAULT_STATE_PATH = path.resolve(process.cwd(), "data/instance.json")
18
-
19
- export function createMachineFingerprint(): string {
20
- return crypto
21
- .createHash("sha256")
22
- .update([os.hostname(), os.platform(), os.arch()].join("|"))
23
- .digest("hex")
24
- }
25
-
26
- export function createIdentity(machineFingerprint = createMachineFingerprint()) {
27
- const keyPair = crypto.generateKeyPairSync("ed25519")
28
- return {
29
- installationId: crypto.randomUUID(),
30
- machineFingerprint,
31
- privateKeyPem: keyPair.privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
32
- publicKeyPem: keyPair.publicKey.export({ type: "spki", format: "pem" }).toString()
33
- }
34
- }
35
-
36
- export function readTokenSync(filePath = DEFAULT_STATE_PATH): string {
37
- try {
38
- if (!fsSync.existsSync(filePath)) return ""
39
- const content = fsSync.readFileSync(filePath, "utf8")
40
- const parsed = JSON.parse(content) as LocalState
41
- return parsed?.instanceToken ?? ""
42
- } catch {
43
- return ""
44
- }
45
- }
46
-
47
- export async function readState(filePath = DEFAULT_STATE_PATH): Promise<LocalState | null> {
48
- try {
49
- const parsed: unknown = JSON.parse(await Bun.file(filePath).text())
50
- if (!parsed || typeof parsed !== "object") return null
51
- const state = parsed as Partial<LocalState>
52
- if (
53
- state.version !== 1 ||
54
- typeof state.installationId !== "string" ||
55
- !state.installationId ||
56
- typeof state.privateKeyPem !== "string" ||
57
- !state.privateKeyPem ||
58
- typeof state.publicKeyPem !== "string" ||
59
- !state.publicKeyPem ||
60
- typeof state.machineFingerprint !== "string" ||
61
- !state.machineFingerprint ||
62
- typeof state.instanceToken !== "string" ||
63
- !state.instanceToken ||
64
- (state.lastSuccessfulCheckAt !== undefined &&
65
- (typeof state.lastSuccessfulCheckAt !== "string" || !state.lastSuccessfulCheckAt))
66
- ) {
67
- return null
68
- }
69
- return state as LocalState
70
- } catch {
71
- return null
72
- }
73
- }
74
-
75
- export async function writeState(state: LocalState, filePath = DEFAULT_STATE_PATH): Promise<void> {
76
- const directory = path.dirname(filePath)
77
- await fs.mkdir(directory, { recursive: true })
78
- const temporaryPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`
79
- await Bun.write(temporaryPath, `${JSON.stringify(state, null, 2)}\n`)
80
- await fs.rename(temporaryPath, filePath)
81
- }
82
-
83
- export async function updateInstanceToken(
84
- instanceToken: string,
85
- statePath = DEFAULT_STATE_PATH
86
- ): Promise<void> {
87
- const localState = await readState(statePath)
88
- if (!localState) throw new Error("Local license state is missing")
89
- await writeState(
90
- { ...localState, instanceToken, lastSuccessfulCheckAt: new Date().toISOString() },
91
- statePath
92
- )
93
- }
94
-
95
- export async function acquireProcessLock(
96
- statePath = DEFAULT_STATE_PATH
97
- ): Promise<() => Promise<void>> {
98
- const lockPath = `${statePath}.lock`
99
- await fs.mkdir(path.dirname(lockPath), { recursive: true })
100
- let handle
101
- try {
102
- handle = await fs.open(lockPath, "wx")
103
- } catch (error) {
104
- if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") throw error
105
- const ownerPid = Number.parseInt(
106
- await Bun.file(lockPath)
107
- .text()
108
- .catch(() => ""),
109
- 10
110
- )
111
- let ownerAlive = false
112
- if (Number.isInteger(ownerPid) && ownerPid > 0) {
113
- try {
114
- process.kill(ownerPid, 0)
115
- ownerAlive = true
116
- } catch {
117
- ownerAlive = false
118
- }
119
- }
120
- if (ownerAlive) throw new Error("Another JChat process already owns the license lock")
121
- await fs.rm(lockPath, { force: true })
122
- handle = await fs.open(lockPath, "wx")
123
- }
124
- await handle.writeFile(`${process.pid}\n`)
125
- return async () => {
126
- await handle.close()
127
- await fs.rm(lockPath, { force: true })
128
- }
129
- }