@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/CHANGELOG.md +50 -0
- package/LICENSE +18 -122
- package/README.md +6 -4
- package/dist/index.d.mts +75 -26
- package/dist/index.mjs +1 -1
- package/package.json +6 -6
- package/src/audit.test.ts +0 -25
- package/src/audit.ts +0 -25
- package/src/check.test.ts +0 -77
- package/src/check.ts +0 -202
- package/src/domain.test.ts +0 -45
- package/src/domain.ts +0 -75
- package/src/env.test.ts +0 -146
- package/src/env.ts +0 -261
- package/src/index.test.ts +0 -117
- package/src/index.ts +0 -206
- package/src/install.ts +0 -62
- package/src/paths.test.ts +0 -49
- package/src/paths.ts +0 -74
- package/src/runtime.test.ts +0 -21
- package/src/runtime.ts +0 -31
- package/src/state.test.ts +0 -54
- package/src/state.ts +0 -129
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
|
-
}
|
package/src/runtime.test.ts
DELETED
|
@@ -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
|
-
}
|