@happyvertical/smrt-app-runtime 0.43.9
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 +36 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +92 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +936 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +15 -0
- package/dist/smrt-knowledge.json +57 -0
- package/package.json +64 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @happyvertical/smrt-app-runtime
|
|
2
|
+
|
|
3
|
+
Application infrastructure composition for the validated runtime profiles in
|
|
4
|
+
`@happyvertical/smrt-config`.
|
|
5
|
+
|
|
6
|
+
## Local profile
|
|
7
|
+
|
|
8
|
+
- `initializeLocalApplicationRuntime()` owns user data paths, SQLite tuning,
|
|
9
|
+
local application-secret creation, and the single-use owner bootstrap flow.
|
|
10
|
+
- SQLite is acquired through `@happyvertical/sql`'s explicit
|
|
11
|
+
`{ driver: 'node:sqlite', custody: 'trusted-parent' }` boundary after the
|
|
12
|
+
runtime establishes its user-owned mode-0700 data root.
|
|
13
|
+
- Application migrations are explicit through `prepareDatabase`; runtime never
|
|
14
|
+
creates application model tables implicitly.
|
|
15
|
+
- Owner bootstrap creates normal `Person`, `User`, `Tenant`, owner `Role` /
|
|
16
|
+
`Membership`, and `Session` records in one transaction.
|
|
17
|
+
- Bootstrap is loopback-only. Only an HMAC of the short-lived token is stored.
|
|
18
|
+
- Background jobs and application-defined paid capabilities are default-off.
|
|
19
|
+
- The embedded runner reuses `TaskRunner`; it is not a second job contract.
|
|
20
|
+
|
|
21
|
+
## Invariants
|
|
22
|
+
|
|
23
|
+
- Never expose application-secret bytes or bootstrap token hashes in diagnostics.
|
|
24
|
+
- Keep local data, assets, database, and secrets outside the source tree.
|
|
25
|
+
- Do not reach into collection/database private fields. Add an upstream public
|
|
26
|
+
API if composition cannot be expressed through exported surfaces.
|
|
27
|
+
- Keep profile selection out of domain models, generated APIs, MCP/WebMCP
|
|
28
|
+
surfaces, effects, approvals, and authorization records.
|
|
29
|
+
|
|
30
|
+
## Validation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm --filter @happyvertical/smrt-app-runtime test
|
|
34
|
+
pnpm --filter @happyvertical/smrt-app-runtime typecheck
|
|
35
|
+
pnpm --filter @happyvertical/smrt-app-runtime build
|
|
36
|
+
```
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @happyvertical/smrt-app-runtime
|
|
2
|
+
|
|
3
|
+
Reusable infrastructure composition for s-m-r-t applications. The package
|
|
4
|
+
implements the validated profiles from `@happyvertical/smrt-config` without
|
|
5
|
+
forking domain objects, generated surfaces, effects, approvals, or job calls.
|
|
6
|
+
|
|
7
|
+
## Private local applications
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
initializeLocalApplicationRuntime,
|
|
12
|
+
} from '@happyvertical/smrt-app-runtime';
|
|
13
|
+
|
|
14
|
+
const { runtime, bootstrap, diagnostics } =
|
|
15
|
+
await initializeLocalApplicationRuntime({
|
|
16
|
+
appId: 'my-app',
|
|
17
|
+
sourceRoot: process.cwd(),
|
|
18
|
+
prepareDatabase: runApplicationMigrations,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Show bootstrap.token only in the loopback onboarding URL. The plaintext is
|
|
22
|
+
// returned once and only its HMAC is persisted.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The path resolver selects the current user's OS application-data directory
|
|
26
|
+
(`~/Library/Application Support` on macOS, `%LOCALAPPDATA%` on Windows, or
|
|
27
|
+
`$XDG_DATA_HOME` / `~/.local/share` on Linux). Secure initialization currently
|
|
28
|
+
requires Node to expose nonzero `O_NOFOLLOW` and `O_DIRECTORY` filesystem
|
|
29
|
+
flags; it fails closed on platforms without that custody support and therefore
|
|
30
|
+
does not yet claim Windows runtime support. The data root contains a mode-0600
|
|
31
|
+
SQLite database, a user-owned asset directory, and generated mode-0600
|
|
32
|
+
application secret material. The root must be a dedicated application
|
|
33
|
+
directory: placing it inside the source checkout or choosing an ancestor is
|
|
34
|
+
refused, as is choosing the user home itself or the filesystem root. An
|
|
35
|
+
explicitly configured root that already exists must already be
|
|
36
|
+
owned by the current user with mode `0700`; initialization rejects it without
|
|
37
|
+
changing permissions or creating artifacts when that custody proof fails.
|
|
38
|
+
An empty root is claimed with an app-specific, empty mode-0600 marker. A
|
|
39
|
+
populated root is accepted only with that valid marker, so selecting an
|
|
40
|
+
unrelated private directory fails without changing its contents. A pending
|
|
41
|
+
marker makes the claim crash-recoverable while the database is first acquired;
|
|
42
|
+
it is promoted atomically after the released SQL custody boundary verifies the
|
|
43
|
+
complete ancestor/root chain, including macOS ACLs. Failed custody removes the
|
|
44
|
+
pending claim and every directory only when they were created by that attempt;
|
|
45
|
+
an inherited pending claim and its database remain authoritative for retry.
|
|
46
|
+
Initializers for the same data root are serialized across processes by an
|
|
47
|
+
exclusive transaction in a dedicated SQLite lock database under a private
|
|
48
|
+
per-user, root-keyed custody directory. The released `@happyvertical/sql`
|
|
49
|
+
trusted-parent boundary validates that directory, its ancestors, and the lock
|
|
50
|
+
leaf for ownership, write permissions, static links, and macOS ACLs before it
|
|
51
|
+
opens the lock database. Atomic SQLite locking elects one owner without deleting
|
|
52
|
+
or replacing a pathname, so there is no stale-file ABA window and no PID or
|
|
53
|
+
clock lease. Process or worker death releases the kernel lock automatically. A
|
|
54
|
+
read-only path, custody, and ownership-marker preflight rejects an obviously
|
|
55
|
+
invalid application root before creating its lock-registry entry; the complete
|
|
56
|
+
checks run again under the lease before application-root mutation. The lease
|
|
57
|
+
covers storage acquisition, secret publication, SQLite tuning,
|
|
58
|
+
application migrations, and bootstrap construction. Contenders wait up to two
|
|
59
|
+
minutes for that complete sequence, so normal migrations can finish without
|
|
60
|
+
overlap. Marker or temporary-secret cleanup therefore cannot race another
|
|
61
|
+
active initializer.
|
|
62
|
+
Every existing path component is opened without following symbolic links and
|
|
63
|
+
checked against its canonical path before descendants or secret bytes are
|
|
64
|
+
written. The runtime then acquires SQLite through `@happyvertical/sql`'s
|
|
65
|
+
explicit `node:sqlite` trusted-parent custody boundary, rooted at the mode-0700
|
|
66
|
+
application data directory. Unsupported runtimes or platforms and unsafe
|
|
67
|
+
ownership, permissions, ACLs, or path components fail closed.
|
|
68
|
+
|
|
69
|
+
The application secret is published by atomically linking a fully written,
|
|
70
|
+
synced mode-0600 temporary file into place. Concurrent installers validate and
|
|
71
|
+
reuse the one winning value; an incomplete or malformed existing secret is
|
|
72
|
+
rejected rather than overwritten, and stale interrupted temporary files are
|
|
73
|
+
removed after a complete value is durably available.
|
|
74
|
+
|
|
75
|
+
The custody boundary prevents static link traversal and mutation by other OS
|
|
76
|
+
principals while the application retains control of that directory. Hostile
|
|
77
|
+
code already running as the same user is outside this boundary; isolating
|
|
78
|
+
same-account processes requires an OS sandbox and a descriptor-relative SQLite
|
|
79
|
+
VFS.
|
|
80
|
+
|
|
81
|
+
Owner onboarding binds to `127.0.0.1` by default and accepts only loopback IP
|
|
82
|
+
literals (`127.0.0.0/8` or `::1`), avoiding hostname-resolution ambiguity.
|
|
83
|
+
The first valid claim creates a real global `Person`, `User`, default `Tenant`,
|
|
84
|
+
owner `Role` / `Membership`, and server-side `Session` atomically. Startup and
|
|
85
|
+
setup are idempotent; replayed, expired, or concurrent claims fail closed.
|
|
86
|
+
Authenticated session TTLs are configured in whole seconds with a minimum of
|
|
87
|
+
one second, and invalid values are rejected before filesystem mutation.
|
|
88
|
+
|
|
89
|
+
Background jobs and application-defined paid capabilities remain disabled until
|
|
90
|
+
explicitly enabled. With embedded job topology, `createEmbeddedJobRunner()`
|
|
91
|
+
returns the normal s-m-r-t `TaskRunner`, so the application keeps one enqueue
|
|
92
|
+
and execution contract without requiring a separate worker service.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { ResolvedApplicationRuntime } from '@happyvertical/smrt-config';
|
|
3
|
+
import { RuntimeProviderOverrides } from '@happyvertical/smrt-config';
|
|
4
|
+
import { SessionService } from '@happyvertical/smrt-users';
|
|
5
|
+
import { TaskRunner } from '@happyvertical/smrt-jobs';
|
|
6
|
+
import { TaskRunnerConfig } from '@happyvertical/smrt-jobs';
|
|
7
|
+
|
|
8
|
+
export declare interface ClaimLocalOwnerInput {
|
|
9
|
+
token: string;
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
tenantName?: string;
|
|
13
|
+
userAgent?: string;
|
|
14
|
+
ipAddress?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize or reopen a local application runtime.
|
|
19
|
+
*
|
|
20
|
+
* A plaintext bootstrap token is returned only when this call creates a new
|
|
21
|
+
* invitation. Existing unexpired invitations remain valid but cannot be read
|
|
22
|
+
* back because only their HMAC is stored.
|
|
23
|
+
*/
|
|
24
|
+
export declare function initializeLocalApplicationRuntime(options: LocalApplicationRuntimeOptions): Promise<LocalRuntimeInitialization>;
|
|
25
|
+
|
|
26
|
+
/** Initialized local runtime used by server startup and onboarding handlers. */
|
|
27
|
+
export declare interface LocalApplicationRuntime {
|
|
28
|
+
readonly db: DatabaseInterface;
|
|
29
|
+
readonly paths: LocalRuntimePaths;
|
|
30
|
+
readonly bindHost: string;
|
|
31
|
+
readonly resolvedRuntime: ResolvedApplicationRuntime;
|
|
32
|
+
readonly backgroundJobsEnabled: boolean;
|
|
33
|
+
readonly paidCapabilitiesEnabled: boolean;
|
|
34
|
+
ensureBootstrapInvitation(): Promise<LocalOwnerBootstrapInvitation | null>;
|
|
35
|
+
rotateBootstrapInvitation(): Promise<LocalOwnerBootstrapInvitation>;
|
|
36
|
+
claimOwner(input: ClaimLocalOwnerInput): Promise<LocalOwnerClaimResult>;
|
|
37
|
+
restoreSession(sessionId: string): ReturnType<SessionService['loadSessionContext']>;
|
|
38
|
+
createEmbeddedJobRunner(config?: TaskRunnerConfig): Promise<TaskRunner>;
|
|
39
|
+
diagnostics(): Promise<LocalRuntimeDiagnostics>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export declare interface LocalApplicationRuntimeOptions extends ResolveLocalRuntimePathsOptions {
|
|
43
|
+
/** Actual HTTP bind host. Owner bootstrap refuses non-loopback values. */
|
|
44
|
+
bindHost?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Explicit application migration/schema hook, invoked before identity access.
|
|
47
|
+
* It must be idempotent; runtime code never synthesizes application tables.
|
|
48
|
+
*/
|
|
49
|
+
prepareDatabase?: (db: DatabaseInterface) => Promise<void>;
|
|
50
|
+
/** Local profile provider overrides (only validated local combinations survive). */
|
|
51
|
+
providers?: RuntimeProviderOverrides;
|
|
52
|
+
/** Short-lived bootstrap token lifetime. Range: 1..900 seconds. */
|
|
53
|
+
bootstrapTtlSeconds?: number;
|
|
54
|
+
/** Normal authenticated session lifetime in whole seconds. Minimum 1. */
|
|
55
|
+
sessionTtlSeconds?: number;
|
|
56
|
+
/** Explicit opt-in for persisted background work. Default false. */
|
|
57
|
+
backgroundJobs?: boolean;
|
|
58
|
+
/** Explicit opt-in for application-defined paid capabilities. Default false. */
|
|
59
|
+
paidCapabilities?: boolean;
|
|
60
|
+
/** Testable clock. */
|
|
61
|
+
now?: () => Date;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export declare type LocalBootstrapStatus = 'available' | 'expired' | 'claimed' | 'unavailable';
|
|
65
|
+
|
|
66
|
+
export declare interface LocalOwnerBootstrapInvitation {
|
|
67
|
+
/** Plaintext is returned exactly when a new claim is issued; it is never persisted. */
|
|
68
|
+
readonly token: string;
|
|
69
|
+
readonly expiresAt: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export declare interface LocalOwnerClaimResult {
|
|
73
|
+
readonly profileId: string;
|
|
74
|
+
readonly userId: string;
|
|
75
|
+
readonly tenantId: string;
|
|
76
|
+
readonly membershipId: string;
|
|
77
|
+
readonly sessionId: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Secret-free, machine-readable local runtime state. */
|
|
81
|
+
export declare interface LocalRuntimeDiagnostics {
|
|
82
|
+
readonly schemaVersion: 1;
|
|
83
|
+
readonly runtime: ResolvedApplicationRuntime;
|
|
84
|
+
readonly bind: {
|
|
85
|
+
readonly host: string;
|
|
86
|
+
readonly loopback: true;
|
|
87
|
+
};
|
|
88
|
+
readonly paths: LocalRuntimePaths;
|
|
89
|
+
readonly storage: {
|
|
90
|
+
readonly database: 'sqlite-file';
|
|
91
|
+
readonly assets: 'user-owned-directory';
|
|
92
|
+
readonly secrets: 'mode-0600-local-file';
|
|
93
|
+
};
|
|
94
|
+
readonly bootstrap: {
|
|
95
|
+
readonly status: LocalBootstrapStatus;
|
|
96
|
+
readonly expiresAt: string | null;
|
|
97
|
+
readonly tokenValuesIncluded: false;
|
|
98
|
+
readonly tokenHashesIncluded: false;
|
|
99
|
+
};
|
|
100
|
+
readonly tenancy: {
|
|
101
|
+
readonly mode: 'real-default-tenant';
|
|
102
|
+
readonly ui: 'hidden';
|
|
103
|
+
};
|
|
104
|
+
readonly jobs: {
|
|
105
|
+
readonly topology: 'inline' | 'embedded';
|
|
106
|
+
readonly backgroundEnabled: boolean;
|
|
107
|
+
};
|
|
108
|
+
readonly paidCapabilitiesEnabled: boolean;
|
|
109
|
+
readonly secretValuesIncluded: false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Stable, secret-free local-runtime failure. */
|
|
113
|
+
export declare class LocalRuntimeError extends Error {
|
|
114
|
+
readonly code: LocalRuntimeErrorCode;
|
|
115
|
+
constructor(code: LocalRuntimeErrorCode, message: string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export declare type LocalRuntimeErrorCode = 'bootstrap_claimed' | 'bootstrap_expired' | 'bootstrap_invalid' | 'bootstrap_unavailable' | 'capability_disabled' | 'invalid_configuration' | 'unsafe_public_exposure';
|
|
119
|
+
|
|
120
|
+
export declare interface LocalRuntimeInitialization {
|
|
121
|
+
readonly runtime: LocalApplicationRuntime;
|
|
122
|
+
readonly bootstrap: LocalOwnerBootstrapInvitation | null;
|
|
123
|
+
readonly diagnostics: LocalRuntimeDiagnostics;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Paths owned by the local user, outside the source tree by default. */
|
|
127
|
+
export declare interface LocalRuntimePaths {
|
|
128
|
+
readonly root: string;
|
|
129
|
+
readonly database: string;
|
|
130
|
+
readonly assets: string;
|
|
131
|
+
readonly secrets: string;
|
|
132
|
+
readonly applicationSecret: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve deterministic user-owned paths and reject source-tree placement.
|
|
137
|
+
*/
|
|
138
|
+
export declare function resolveLocalRuntimePaths(options: ResolveLocalRuntimePathsOptions): LocalRuntimePaths;
|
|
139
|
+
|
|
140
|
+
export declare interface ResolveLocalRuntimePathsOptions {
|
|
141
|
+
/** Stable application identifier, e.g. `lolaus`. */
|
|
142
|
+
appId: string;
|
|
143
|
+
/** Explicit user-owned data directory. Defaults to the OS application-data directory. */
|
|
144
|
+
dataDirectory?: string;
|
|
145
|
+
/** Source checkout to protect from accidental data placement. Defaults to cwd. */
|
|
146
|
+
sourceRoot?: string;
|
|
147
|
+
/** Testable default-root and filesystem case-sensitivity override. */
|
|
148
|
+
platform?: NodeJS.Platform;
|
|
149
|
+
/** Testable home-directory override. */
|
|
150
|
+
homeDirectory?: string;
|
|
151
|
+
/** Testable environment override. */
|
|
152
|
+
env?: NodeJS.ProcessEnv;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { }
|