@jini-ai/platform 0.3.0 → 0.5.0
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 +146 -0
- package/README.md +121 -2
- package/dist/asset-cache.d.ts +0 -22
- package/dist/asset-cache.js +3 -138
- package/dist/download.js +2 -2
- package/dist/fs/atomic-write.d.ts +103 -0
- package/dist/fs/atomic-write.js +201 -0
- package/dist/fs/durable-json.d.ts +89 -0
- package/dist/fs/durable-json.js +166 -0
- package/dist/fs/env-text.d.ts +11 -0
- package/dist/fs/env-text.js +22 -0
- package/dist/fs/file-lock.d.ts +126 -0
- package/dist/fs/file-lock.js +306 -0
- package/dist/fs/guarded-reader.d.ts +60 -0
- package/dist/fs/guarded-reader.js +310 -0
- package/dist/fs/index.d.ts +8 -0
- package/dist/fs/index.js +9 -0
- package/dist/fs/strict-containment.d.ts +5 -0
- package/dist/fs/strict-containment.js +13 -0
- package/dist/fs.js +2 -3
- package/dist/http/guarded/client.d.ts +13 -0
- package/dist/http/guarded/client.js +240 -0
- package/dist/http/guarded/errors.d.ts +21 -0
- package/dist/http/guarded/errors.js +20 -0
- package/dist/http/guarded/fetch-adapter.d.ts +23 -0
- package/dist/http/guarded/fetch-adapter.js +30 -0
- package/dist/http/guarded/index.d.ts +12 -0
- package/dist/http/guarded/index.js +7 -0
- package/dist/http/guarded/node-ports.d.ts +8 -0
- package/dist/http/guarded/node-ports.js +11 -0
- package/dist/http/guarded/ports.d.ts +51 -0
- package/dist/http/guarded/ports.js +2 -0
- package/dist/http/guarded/transport.fetch.d.ts +11 -0
- package/dist/http/guarded/transport.fetch.js +109 -0
- package/dist/http/guarded/types.d.ts +8 -0
- package/dist/http/guarded/types.js +2 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +61 -1
- package/dist/mail/adapter-module.d.ts +20 -0
- package/dist/mail/adapter-module.js +2 -0
- package/dist/mail/delivery-availability.d.ts +7 -0
- package/dist/mail/delivery-availability.js +6 -0
- package/dist/mail/index.d.ts +7 -0
- package/dist/mail/index.js +3 -0
- package/dist/mail/ports.d.ts +43 -0
- package/dist/mail/ports.js +2 -0
- package/dist/mail/purpose-scoped-mailer.d.ts +18 -0
- package/dist/mail/purpose-scoped-mailer.js +50 -0
- package/dist/mail/smtp.d.ts +79 -0
- package/dist/mail/smtp.js +102 -0
- package/dist/mail/types.d.ts +83 -0
- package/dist/mail/types.js +2 -0
- package/dist/messages.d.ts +22 -0
- package/dist/messages.js +15 -0
- package/dist/net/address.d.ts +38 -0
- package/dist/net/address.js +241 -0
- package/dist/net/index.d.ts +2 -0
- package/dist/net/index.js +2 -0
- package/dist/process.d.ts +2 -1
- package/dist/process.js +2 -1
- package/dist/proxy-env.js +1 -1
- package/dist/resource-paths.js +1 -1
- package/dist/sandbox-env.d.ts +1 -1
- package/dist/sandbox-env.js +1 -1
- package/dist/secrets/aad.d.ts +7 -0
- package/dist/secrets/aad.js +5 -0
- package/dist/secrets/credential-sets/aad.d.ts +7 -0
- package/dist/secrets/credential-sets/aad.js +7 -0
- package/dist/secrets/credential-sets/index.d.ts +4 -0
- package/dist/secrets/credential-sets/index.js +3 -0
- package/dist/secrets/credential-sets/repo.memory.d.ts +36 -0
- package/dist/secrets/credential-sets/repo.memory.js +72 -0
- package/dist/secrets/credential-sets/types.d.ts +67 -0
- package/dist/secrets/credential-sets/types.js +6 -0
- package/dist/secrets/index.d.ts +9 -0
- package/dist/secrets/index.js +5 -0
- package/dist/secrets/keyring.env.d.ts +176 -0
- package/dist/secrets/keyring.env.js +271 -0
- package/dist/secrets/keyring.memory.d.ts +10 -0
- package/dist/secrets/keyring.memory.js +9 -0
- package/dist/secrets/ports.d.ts +33 -0
- package/dist/secrets/ports.js +2 -0
- package/dist/secrets/secret-sealer.aesgcm.d.ts +22 -0
- package/dist/secrets/secret-sealer.aesgcm.js +64 -0
- package/dist/secrets/testing.d.ts +4 -0
- package/dist/secrets/testing.js +4 -0
- package/dist/secrets/types.d.ts +10 -0
- package/dist/secrets/types.js +2 -0
- package/dist/shell.d.ts +10 -6
- package/dist/shell.js +10 -6
- package/dist/terminal.d.ts +2 -2
- package/dist/terminal.js +9 -5
- package/package.json +79 -4
- package/dist/asset-cache.d.ts.map +0 -1
- package/dist/asset-cache.js.map +0 -1
- package/dist/aws-sigv4.d.ts.map +0 -1
- package/dist/aws-sigv4.js.map +0 -1
- package/dist/blob-storage.d.ts.map +0 -1
- package/dist/blob-storage.js.map +0 -1
- package/dist/command.d.ts.map +0 -1
- package/dist/command.js.map +0 -1
- package/dist/download.d.ts.map +0 -1
- package/dist/download.js.map +0 -1
- package/dist/fetch-with-timeout.d.ts.map +0 -1
- package/dist/fetch-with-timeout.js.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/home-expansion.d.ts.map +0 -1
- package/dist/home-expansion.js.map +0 -1
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/process.d.ts.map +0 -1
- package/dist/process.js.map +0 -1
- package/dist/proxy-env.d.ts.map +0 -1
- package/dist/proxy-env.js.map +0 -1
- package/dist/resource-paths.d.ts.map +0 -1
- package/dist/resource-paths.js.map +0 -1
- package/dist/sandbox-env.d.ts.map +0 -1
- package/dist/sandbox-env.js.map +0 -1
- package/dist/shell.d.ts.map +0 -1
- package/dist/shell.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/toolchain.d.ts.map +0 -1
- package/dist/toolchain.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @jini-ai/platform
|
|
2
|
+
|
|
3
|
+
## 0.5.0 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
### BREAKING
|
|
6
|
+
|
|
7
|
+
- Use canonical core ports and platform ./net and ./fs/file-lock. Analytics and trash moved to their domain packages; credential scope and durable filesystem contracts changed.
|
|
8
|
+
- Distribution includes runtime output, release documentation and required assets only. Process records and per-job neutrality checks are no longer part of the package surface.
|
|
9
|
+
|
|
10
|
+
## Unreleased
|
|
11
|
+
|
|
12
|
+
### Compilation fixes
|
|
13
|
+
|
|
14
|
+
- Omit an absent guarded HTTP abort signal to satisfy the canonical request contract.
|
|
15
|
+
- Import the mail adapter's HTTP port from core primitives and type the key-rotation test
|
|
16
|
+
fake against `KeyringPort`, preserving its assertions and behavior.
|
|
17
|
+
|
|
18
|
+
### Breaking domain extraction
|
|
19
|
+
|
|
20
|
+
- Remove `./analytics` and the analytics root namespace; import the standalone `@jini-ai/analytics` package.
|
|
21
|
+
- Remove `./trash` and the trash root namespace; import `@jini-ai/cms/trash`.
|
|
22
|
+
- Move both capabilities' behavior tests with their source; platform retains only generic OS/transport capabilities.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Shared platform primitives
|
|
26
|
+
|
|
27
|
+
- BREAKING: HTTP primitive types now come from `@jini-ai/core/primitives`; guarded HTTP exports
|
|
28
|
+
`GuardedClock` with `nowMs()` and `timeoutSignal({ timeoutMs })`. Mail uses the kernel clock.
|
|
29
|
+
Analytics primitive declarations also come from core. Add core as a regular dependency.
|
|
30
|
+
- BREAKING: address classifiers move from asset-cache.ts to `./net`; `isPrivateAddress` and
|
|
31
|
+
`expandIpv6` take `{ address }`. The root IP-classifier export points to the canonical module.
|
|
32
|
+
- Add `./fs/file-lock` async/sync token-and-inode-owned locks sharing one stale decision.
|
|
33
|
+
Durable JSON retains its lock budgets, user copy, read/quarantine behavior and JSON bytes.
|
|
34
|
+
- BREAKING: the synchronous atomic filesystem seam owns descriptors and exposes open/sync/close/
|
|
35
|
+
lstat as well as existing mode/rename/remove effects. Add file and directory fsync and owned-temp
|
|
36
|
+
failure cleanup; move `tempName` into argument two as an optional token factory with the native pid.UUID default. Add async file/JSON
|
|
37
|
+
writers, secret-mode verification, optional symlink refusal, and sidecar newline/parent support.
|
|
38
|
+
Durable JSON delegates to the canonical writer, retains its temp layout/new-file mode, and gains
|
|
39
|
+
exclusive temp creation, cleanup on failure, and directory fsync.
|
|
40
|
+
- Add exported `defaultPlatformMessages` with host replacement seams. Egress refusal and invalid
|
|
41
|
+
root-key recovery warnings use neutral defaults. Keyring source-permission flags and derivation
|
|
42
|
+
formats are unchanged. Tests and builds are deferred by the owner.
|
|
43
|
+
|
|
44
|
+
- Security defaults: purpose-gated mail validates runtime modes at composition; only explicit local mode bypasses readiness. Root-key adapters require an injected environment reader and explicit fallback/generation booleans. Fresh inspection/reveal also require that reader. Existing wire derivation is unchanged.
|
|
45
|
+
|
|
46
|
+
- Guarded HTTP restores per-socket inactivity timeouts. Requests accept explicit
|
|
47
|
+
`idleTimeoutMs`, with `timeoutMs` retained as its legacy alias and the historical
|
|
48
|
+
`connectTimeoutMs` policy ceiling applied per socket. Optional `totalDeadlineMs`
|
|
49
|
+
bounds DNS, all redirects and decoded body reads independently; it is off by default.
|
|
50
|
+
Idle failures retain `Error("request timed out after …ms")`; explicit deadline failures
|
|
51
|
+
use `FetchTimeoutError`. Slow but progressing transfers are no longer cut off by
|
|
52
|
+
an implicit total deadline. Verification is deferred by owner directive.
|
|
53
|
+
|
|
54
|
+
### Integration finish pass
|
|
55
|
+
|
|
56
|
+
- Complete Node runtime metadata for every export, the filesystem barrel, and additive root
|
|
57
|
+
namespaces for `./http/guarded`, `./mail`, `./mail/smtp`, `./fs`, `./fs/guarded-reader`,
|
|
58
|
+
`./fs/durable-json`, `./analytics`, `./trash`, `./secrets`, `./secrets/credential-sets`,
|
|
59
|
+
and `./secrets/testing`. No version change or dependency addition.
|
|
60
|
+
- BREAKING (new extraction APIs): native factories and transport/memory constructors take `{}`;
|
|
61
|
+
keyring `activeKey`, mailer `capabilities`, clock/random methods and durable JSON `read` take
|
|
62
|
+
`{}`. SMTP transport `sendMail` takes `{ mail }`; the native nodemailer payload remains
|
|
63
|
+
behind `NativeSmtpTransport`. Analytics/trash policy errors take `{ message }` and optional
|
|
64
|
+
`{ cause }`. Existing legacy root functions retain their compatibility signatures.
|
|
65
|
+
- Reconcile the export-map contract test and remove application names from analytics fixtures.
|
|
66
|
+
Verification of this finish pass is deferred by owner directive.
|
|
67
|
+
|
|
68
|
+
- Add privacy analytics ingest with required HKDF context and privacy policy, injectable hash/hook seams, and a recent-hit memory sink.
|
|
69
|
+
- Add trash write/sweep orchestration with required retention/entity policy, reentrant transaction ports, host scheduler, record-store adapter, and optional follow-ups in arg 2.
|
|
70
|
+
- Add `./fs` with mode-preserving atomic file/JSON writes, narrow env-text transforms, and strict lexical containment; keep all existing filesystem exports unchanged.
|
|
71
|
+
|
|
72
|
+
- Add guarded outbound HTTP with injected DNS, pinned transport, clock, policy, and required user agent. Reuse existing address validation and timeout errors. Preserve redirect refusal and response-byte contracts.
|
|
73
|
+
- Add mail contracts, a purpose/readiness gate, delivery capability checks, and an optional SMTP subpath over a host-supplied transport/module; no new dependency.
|
|
74
|
+
- Add contained and bounded file readers with caller-owned deny rules/limits, and durable JSON file operations with injectable filesystem, time, liveness, sleep, randomness, and notices.
|
|
75
|
+
- Convert the unpublished secrets surface to required-object/optional-object signatures; preserve pinned derivation, AAD, ciphertext, and envelope bytes.
|
|
76
|
+
|
|
77
|
+
## 0.4.0
|
|
78
|
+
|
|
79
|
+
### Minor Changes
|
|
80
|
+
|
|
81
|
+
- Add `./secrets` with AES-256-GCM sealing, root-key ports, explicit env/file and fixed-key
|
|
82
|
+
adapters, HKDF helpers, and root-key validation, status, reveal, and exclusive generation.
|
|
83
|
+
Env names, key paths, and HKDF salts are caller-supplied, with no defaults. New seals require
|
|
84
|
+
AAD; opening historical ciphertext without AAD remains supported. The ciphertext envelope
|
|
85
|
+
and derivation labels are preserved and covered by pinned vectors.
|
|
86
|
+
- Add `./secrets/credential-sets` with generic vendor credential records, repository contracts,
|
|
87
|
+
the stable v1 AAD builder, and a memory repository. Add `./secrets/testing` with ephemeral and
|
|
88
|
+
fixed root-key adapters and the memory repository. Existing exports and dependencies are unchanged.
|
|
89
|
+
|
|
90
|
+
## 0.1.2
|
|
91
|
+
|
|
92
|
+
### Patch Changes
|
|
93
|
+
|
|
94
|
+
- Add top-level `main`/`types` fields alongside the existing `exports` map. A consumer on
|
|
95
|
+
TypeScript's classic `moduleResolution: "node"` (node10) — which ignores `package.json#exports`
|
|
96
|
+
entirely — could not resolve this package's types at all (`TS2307: Cannot find module`) even
|
|
97
|
+
after the previous exports-map fix restored `require()` at runtime; type resolution and runtime
|
|
98
|
+
resolution are separate algorithms. Verified against a real external consumer (whose
|
|
99
|
+
tsconfig uses this legacy resolution mode): adding these two fields, with its tsconfig completely
|
|
100
|
+
unchanged, made the error disappear. Also fixes absolute-path `require()` (distinct from a bare
|
|
101
|
+
specifier, which already worked) for the same reason — `main` was previously absent.
|
|
102
|
+
|
|
103
|
+
Purely additive: every modern resolver (Node's own runtime `exports` resolution, TypeScript's
|
|
104
|
+
`bundler`/`node16`/`nodenext`) prefers `exports` over `main`/`types` when both are present, so
|
|
105
|
+
this changes nothing for a consumer already on a modern resolver.
|
|
106
|
+
|
|
107
|
+
## 0.1.1
|
|
108
|
+
|
|
109
|
+
### Patch Changes
|
|
110
|
+
|
|
111
|
+
- Add a `"default"` export condition to every published package's `exports` map — every one of
|
|
112
|
+
them lacked it, which meant `require()` failed with `ERR_PACKAGE_PATH_NOT_EXPORTED` for any
|
|
113
|
+
CommonJS consumer (found via a real external integration attempt; Node needs `require(esm)`
|
|
114
|
+
support, i.e. Node >=22.12, for this to resolve).
|
|
115
|
+
|
|
116
|
+
`@jini-ai/agent-runtime`:
|
|
117
|
+
|
|
118
|
+
- **New**: `RuntimeBuildOptions.permissionMode` (`'bypass' | 'restricted'`) lets a caller opt a
|
|
119
|
+
run OUT of the auto-approve-every-permission-prompt flag every def with one
|
|
120
|
+
(`bypassPermissions` / `--yolo` / `--dangerously-skip-permissions`) previously pushed
|
|
121
|
+
unconditionally, with no way to turn it off. Omitting it keeps today's default (bypass)
|
|
122
|
+
behavior unchanged.
|
|
123
|
+
- **New**: `ClaudeStreamEvent`, `CopilotStreamEvent`, and `QoderEvent` are now real exported
|
|
124
|
+
discriminated unions instead of `Record<string, unknown>` — a real external consumer guessed a
|
|
125
|
+
nonexistent field name (`event.text` instead of the actual `event.delta`) against the old
|
|
126
|
+
untyped sink and silently lost every streamed token with no compile or runtime error.
|
|
127
|
+
- Fixed a doc/implementation mismatch in `claude-stream.ts`: the module doc claimed `tool_result`
|
|
128
|
+
events carry `{ tool_use_id, content, is_error }`; the actual emitted shape is
|
|
129
|
+
`{ toolUseId, content, isError }`.
|
|
130
|
+
|
|
131
|
+
`@jini-ai/daemon`: `AgentExecutorRunInput.permissionMode` forwards the new
|
|
132
|
+
`RuntimeBuildOptions.permissionMode` through to `buildArgs`, so a host can actually reach the new
|
|
133
|
+
opt-out from the daemon's real run-input surface, not just from `@jini-ai/agent-runtime` in
|
|
134
|
+
isolation.
|
|
135
|
+
|
|
136
|
+
`@jini-ai/agentic`: `setAtPointer` no longer throws on a malformed (e.g. missing leading `/`)
|
|
137
|
+
`updateDataModel` path — degrades to a no-op like its sibling `getAtPointer`, matching this
|
|
138
|
+
package's own "a bad binding must not crash the renderer" contract. That path is agent-authored
|
|
139
|
+
wire data with no error boundary above it in any host, so the uncaught throw could unmount an
|
|
140
|
+
entire chat UI from ~40 bytes of malformed input.
|
|
141
|
+
|
|
142
|
+
`@jini-ai/chat-react`: a local (client-resolved) A2UI button action is no longer a silent no-op —
|
|
143
|
+
`A2uiSurfaceCard` now surfaces the resolved value. New `ExtEventErrorBoundary` confines a
|
|
144
|
+
`kind: 'ext'` event group's renderer to its own card instead of letting a render/effect-phase
|
|
145
|
+
throw from agent-controlled content unmount the whole chat root (there was no error boundary
|
|
146
|
+
anywhere in this package or its hosts before this).
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This is the Node-only substrate a daemon host needs to spawn and supervise child
|
|
|
6
6
|
for a port to come up, resolve a user's installed toolchains, manage terminal (PTY) sessions, and
|
|
7
7
|
store blobs — all without depending on `@jini-ai/daemon` or any product code. Ported mostly
|
|
8
8
|
verbatim from Open Design's own `packages/platform` plus several generic daemon-support files
|
|
9
|
-
that were split out of OD's monolithic daemon (see
|
|
9
|
+
that were split out of OD's monolithic daemon (see the archived provenance ledger for the full per-file trail).
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -85,4 +85,123 @@ ESM only — ships `"type": "module"` with no CommonJS `require` build.
|
|
|
85
85
|
|
|
86
86
|
## Provenance
|
|
87
87
|
|
|
88
|
-
See
|
|
88
|
+
See the archived provenance ledger for per-file provenance and scope decisions. Apache-2.0.
|
|
89
|
+
# Additional subpaths
|
|
90
|
+
|
|
91
|
+
The package also exposes `./http/guarded`, `./mail`, `./mail/smtp`,
|
|
92
|
+
`./fs/guarded-reader`, `./fs/durable-json`, and `./secrets`. New APIs take a required
|
|
93
|
+
object followed by an optional object. Application identity and policy values belong to the caller.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createHttpClient, createNodeGuardedHttpPorts, guardedFetch } from "@jini-ai/platform/http/guarded";
|
|
97
|
+
|
|
98
|
+
const client = createHttpClient({
|
|
99
|
+
...createNodeGuardedHttpPorts({}),
|
|
100
|
+
userAgent: "MyApplication/1.0",
|
|
101
|
+
policy: {
|
|
102
|
+
allowedSchemes: ["https"], denyPrivateAddresses: true, devHostAllowlist: [],
|
|
103
|
+
maxRedirects: 3, connectTimeoutMs: 5000,
|
|
104
|
+
maxResponseBytes: 1_000_000, maxDecompressedBytes: 1_000_000,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const response = await guardedFetch(
|
|
108
|
+
{ client, url: "https://provider.example/metadata", timeoutMs: 5000 },
|
|
109
|
+
{ redirect: "error" },
|
|
110
|
+
);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`guardedFetch` returns a native Response, refuses truncated bodies, and defaults to redirect
|
|
114
|
+
refusal. SDKs with positional fetch callbacks can adapt them at the host boundary. The HTTP
|
|
115
|
+
transport pins the vetted IP while preserving authority and TLS SNI; every followed GET redirect
|
|
116
|
+
is checked again. The `rateLimit` policy field is reserved metadata and does not implement pacing.
|
|
117
|
+
|
|
118
|
+
Mail sends put message, idempotency key, workspace, and source context in argument one, with
|
|
119
|
+
lane/purpose/timeout in argument two. `wrapMailerWithPurposeGate` requires an inner mailer,
|
|
120
|
+
runtime mode, and a readiness port. Unknown or absent lanes require a durable path in production.
|
|
121
|
+
The SMTP subpath accepts a transport and clock; its nodemailer factory accepts the host's module
|
|
122
|
+
as a structural port. Core loads no mail vendor and adds no mail dependency.
|
|
123
|
+
|
|
124
|
+
The guarded reader requires root, deny rules, all read/traversal limits, and a filesystem port.
|
|
125
|
+
The durable JSON factory requires file path, filesystem/time/sleep/liveness/random/reporting ports,
|
|
126
|
+
and host notices. It preserves atomic replacement and corrupt-file states; parent-directory
|
|
127
|
+
persistence and the inherited simultaneous stale-lock-break race remain filesystem/locking limits.
|
|
128
|
+
|
|
129
|
+
See [API.md](./API.md) for the source/export mapping and host wiring,
|
|
130
|
+
and [the secrets guide](./src/secrets/README.md) for ciphertext-compatible object signatures.
|
|
131
|
+
|
|
132
|
+
## Integrated extraction APIs
|
|
133
|
+
|
|
134
|
+
All exports declare a Node runtime. The root exposes `filesystem`, `guardedHttp`, `mail`,
|
|
135
|
+
`smtp`, `secrets`, and `credentialSets` namespaces; the dedicated subpaths
|
|
136
|
+
remain available. `./fs` now also exports guarded readers and durable JSON helpers. Legacy root
|
|
137
|
+
functions keep their existing signatures for consumers already using them.
|
|
138
|
+
|
|
139
|
+
New factories/functions/methods accept a required object and, where needed, a second optional
|
|
140
|
+
object. No-input operations take `{}`: `createNodeGuardedHttpPorts({})`,
|
|
141
|
+
`createNodeGuardedReaderFilesystem({})`, `createNodeDurableJsonPorts({})`,
|
|
142
|
+
`new FetchHttpTransportAdapter({})`, `new InMemoryTrashRepo({})`,
|
|
143
|
+
`new InMemoryVendorCredentialSetRepo({})`, `keyring.activeKey({})`,
|
|
144
|
+
`mailer.capabilities({})`, `clock.nowMs({})`, `clock.nowIso({})`, `random.token({})`,
|
|
145
|
+
and `file.read({})`. SMTP ports receive `sendMail({ mail })`; `NodemailerPort` exposes the
|
|
146
|
+
vendor-shaped `NativeSmtpTransport` so the factory adapts the payload once. Policy errors use
|
|
147
|
+
`new AnalyticsPiiRejectedError({ message }, { cause })` and
|
|
148
|
+
`new TrashAdapterMissingError({ message }, { cause })`.
|
|
149
|
+
|
|
150
|
+
| Subpath | API and required host ports/policy |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `@jini-ai/analytics` (separate package) | `ingestHit({ input, deps }, { hooks, hash })`, `normalizeIngestContext({ input, dailySalt }, { hash })`, `deriveDailySalt({ rootKeySeed, workspaceId, utcDate, saltContext })`. The host supplies sink/config, host resolution, HKDF context, and privacy bounds. `LocalBufferSink({}, { initialHits })` is process-local. |
|
|
153
|
+
| `@jini-ai/cms/trash` (separate package) | `createTrashService({ repo, adapters, transaction, idGen, entityPolicy }, { retentionDays, ...optional })`, `createTrashSweep({ repo, adapters, transaction, entityPolicy })`, and `startTrashSweeper({ sweep, clock, scheduler, leaseOwner }, optional)`. The host owns authorization, transactions, retention, scheduling, and adapters; follow-up hooks are in argument two. |
|
|
154
|
+
| `./fs` | `writeFileAtomic({ filePath, content, fs, tempName })`, `writeJsonFileAtomic({ filePath, data, fs, tempName })`, `upsertEnvLine({ source, key, value })`, `readEnvLine({ source, key })`, `resolvePathWithin({ root, segment })`. Atomic writes preserve existing modes and require filesystem/temp-name ports. |
|
|
155
|
+
|
|
156
|
+
The extraction mappings, caller counts, remaining ownership needs, and serial verification
|
|
157
|
+
commands are recorded in [the integration handoff](./integration-platform-report.md).
|
|
158
|
+
|
|
159
|
+
## Design decisions
|
|
160
|
+
|
|
161
|
+
- [Unknown mail purposes take the restrictive delivery lane](docs/decisions/DR-001-purpose-scoped-mail.md).
|
|
162
|
+
- [Outbound HTTP inherits guards from one injected client](docs/decisions/DR-002-guarded-egress.md).
|
|
163
|
+
|
|
164
|
+
## Shared kernel and filesystem/network primitives
|
|
165
|
+
|
|
166
|
+
HTTP request/response/redirect/client contracts and `Clock`, `UUID`, `ISODateTime`, and JSON
|
|
167
|
+
contracts come from `@jini-ai/core/primitives`. Guarded construction uses `GuardedClock`, which
|
|
168
|
+
adds `timeoutSignal({ timeoutMs })` to `nowMs()`. SMTP acceptance time uses `nowIso({ clock })`.
|
|
169
|
+
Mail adapter kits use that same core `HttpClientPort`; guarded requests omit `signal` when
|
|
170
|
+
neither caller cancellation nor a total deadline supplies one.
|
|
171
|
+
Socket idle budgets and optional total deadlines retain their existing behavior.
|
|
172
|
+
|
|
173
|
+
`@jini-ai/platform/net` publishes `isPrivateAddress({ address })`, `expandIpv6({ address })`,
|
|
174
|
+
`isLoopbackApiHost({ hostname })`, `isBlockedExternalApiHostname({ hostname })`, and
|
|
175
|
+
`isLoopbackHostname({ hostname })`. Resolved-peer SSRF classification fails closed and blocks
|
|
176
|
+
loopback, including compatible/mapped IPv6. Provider hostname policy deliberately allows loopback
|
|
177
|
+
and requires DNS/connection-time checks separately. The daemon hostname predicate retains its
|
|
178
|
+
narrower policy. The IP classifier no longer lives in asset-cache.ts.
|
|
179
|
+
|
|
180
|
+
`@jini-ai/platform/fs` publishes `writeFileAtomic` / `writeJsonFileAtomic` and their `Async`
|
|
181
|
+
counterparts. Synchronous calls take `{ filePath, content, fs }` (JSON uses `data`),
|
|
182
|
+
where `fs` comes from `createNodeAtomicFilesystem({})`. Async calls take `{ filePath, content }` and accept a native `fs/promises` seam in the optional object. Both preserve existing
|
|
183
|
+
permissions, default new files to 0600, create a sibling exclusively, flush file bytes before rename,
|
|
184
|
+
flush the directory after rename, and clean owned temps on failure. The optional object
|
|
185
|
+
accepts `tempName`; omitting it uses
|
|
186
|
+
`${process.pid}.${randomUUID()}`. Options include `mode`, `defaultMode`, `refuseSymlink`,
|
|
187
|
+
`verifyOwnerOnly`, and `platform`; async calls also support `createParent`, and async JSON supports
|
|
188
|
+
`trailingNewline` for runtime-state files. Secret stores pass `{ mode: 0o600, verifyOwnerOnly: true }`;
|
|
189
|
+
POSIX permission verification fails closed. Directory fsync failures surface after replacement.
|
|
190
|
+
|
|
191
|
+
`@jini-ai/platform/fs/file-lock` publishes `withFileLock({ lockPath, run }, options)` and
|
|
192
|
+
`withFileLockSync(...)`. Defaults are 15 seconds to wait, 10 seconds stale, and 10ms polling;
|
|
193
|
+
options accept `timeoutMs`, `staleMs`, `pollMs`, and the kernel `clock`, plus filesystem/liveness/
|
|
194
|
+
sleep seams. Run receives an ownership handle with `assertHeld({})`. Age or dead local-owner
|
|
195
|
+
liveness permits stealing only after inode/device, contents and mtime remain unchanged. Unknown or
|
|
196
|
+
foreign-owner pids do not authorize death-based stealing. `staleMs: Infinity` disables age stealing.
|
|
197
|
+
The documented final check-to-unlink race remains; use a stale budget longer than the operation.
|
|
198
|
+
Durable JSON passes its existing 5s/30s/10ms values, legacy token/temp format, and timeout copy.
|
|
199
|
+
|
|
200
|
+
`defaultPlatformMessages` is exported from the root, guarded HTTP, and secrets entries. Hosts can
|
|
201
|
+
supply a replacement `PlatformMessages` object to `EgressRefusedError`, `EnvOrFileKeyring`, or the
|
|
202
|
+
lock/atomic-writer options. Explicit `callerSafeMessage` still takes precedence for an egress refusal.
|
|
203
|
+
Credential records retain the required `workspaceId` field: it binds existing repository rows and
|
|
204
|
+
AES-GCM AAD. Renaming that persisted field requires a host adapter migration as well as a type edit.
|
|
205
|
+
Analytics is available from the standalone `@jini-ai/analytics` package; soft-delete and purge
|
|
206
|
+
services are available from `@jini-ai/cms/trash`. Their platform subpaths and root namespaces
|
|
207
|
+
have been removed.
|
package/dist/asset-cache.d.ts
CHANGED
|
@@ -16,28 +16,6 @@ export declare class AssetCacheError extends Error {
|
|
|
16
16
|
readonly status: number;
|
|
17
17
|
constructor(status: number, message: string);
|
|
18
18
|
}
|
|
19
|
-
/** Expand an IPv6 literal (with `::` compression and/or an embedded dotted
|
|
20
|
-
* IPv4 tail) into its eight 16-bit groups, or null if it does not parse.
|
|
21
|
-
* Node's URL parser normalizes mapped literals to hex (`::ffff:127.0.0.1` →
|
|
22
|
-
* `::ffff:7f00:1`), so a string regex can't reliably spot the mapped range —
|
|
23
|
-
* we have to canonicalize.
|
|
24
|
-
*
|
|
25
|
-
* Exported (previously module-private) so its parse-failure guards are
|
|
26
|
-
* directly unit-testable: `isPrivateAddress`'s only real call site gates
|
|
27
|
-
* every call behind `net.isIP(addr) === 6`, and Node's `isIP` already fully
|
|
28
|
-
* validates IPv6 syntax (including embedded-IPv4-tail octet ranges) before
|
|
29
|
-
* returning 6 — empirically re-verified this session by fuzzing ~10k
|
|
30
|
-
* `isIP`-accepted v6 literals through this function's guards with zero
|
|
31
|
-
* hits — so none of these guards are reachable through that path. They stay
|
|
32
|
-
* as real defense-in-depth (fail closed with `null` rather than trust a
|
|
33
|
-
* platform primitive never changing behavior), matching this file's
|
|
34
|
-
* "exported for direct unit testing" precedent (see `readBodyCapped`'s
|
|
35
|
-
* abort parameter). */
|
|
36
|
-
export declare function expandIpv6(addr: string): number[] | null;
|
|
37
|
-
/** True for any address an external CDN must never resolve to: loopback,
|
|
38
|
-
* link-local, private, CGNAT, ULA, multicast, or unspecified. An unparseable
|
|
39
|
-
* input is treated as unsafe. */
|
|
40
|
-
export declare function isPrivateAddress(addr: string): boolean;
|
|
41
19
|
/** Cheap up-front rejection: enforce http(s), no embedded credentials, no
|
|
42
20
|
* localhost, and no *literal* private IP. This is NOT the DNS-rebinding guard
|
|
43
21
|
* (that is `createValidatingLookup`, applied at connection time) — it only
|
package/dist/asset-cache.js
CHANGED
|
@@ -25,6 +25,7 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
|
25
25
|
import { isIP } from 'node:net';
|
|
26
26
|
import path from 'node:path';
|
|
27
27
|
import { Agent } from 'undici';
|
|
28
|
+
import { isPrivateAddress } from './net/address.js';
|
|
28
29
|
/** Media extensions we are willing to cache + proxy. Anything else (HTML,
|
|
29
30
|
* CSS, JS, fonts) is intentionally left alone so this surface never becomes a
|
|
30
31
|
* general-purpose open proxy. */
|
|
@@ -75,142 +76,6 @@ export class AssetCacheError extends Error {
|
|
|
75
76
|
this.status = status;
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
|
-
/** Expand an IPv6 literal (with `::` compression and/or an embedded dotted
|
|
79
|
-
* IPv4 tail) into its eight 16-bit groups, or null if it does not parse.
|
|
80
|
-
* Node's URL parser normalizes mapped literals to hex (`::ffff:127.0.0.1` →
|
|
81
|
-
* `::ffff:7f00:1`), so a string regex can't reliably spot the mapped range —
|
|
82
|
-
* we have to canonicalize.
|
|
83
|
-
*
|
|
84
|
-
* Exported (previously module-private) so its parse-failure guards are
|
|
85
|
-
* directly unit-testable: `isPrivateAddress`'s only real call site gates
|
|
86
|
-
* every call behind `net.isIP(addr) === 6`, and Node's `isIP` already fully
|
|
87
|
-
* validates IPv6 syntax (including embedded-IPv4-tail octet ranges) before
|
|
88
|
-
* returning 6 — empirically re-verified this session by fuzzing ~10k
|
|
89
|
-
* `isIP`-accepted v6 literals through this function's guards with zero
|
|
90
|
-
* hits — so none of these guards are reachable through that path. They stay
|
|
91
|
-
* as real defense-in-depth (fail closed with `null` rather than trust a
|
|
92
|
-
* platform primitive never changing behavior), matching this file's
|
|
93
|
-
* "exported for direct unit testing" precedent (see `readBodyCapped`'s
|
|
94
|
-
* abort parameter). */
|
|
95
|
-
export function expandIpv6(addr) {
|
|
96
|
-
// `String#split` always returns a non-empty array, so index 0 is never
|
|
97
|
-
// `undefined` — the `!` narrows a `noUncheckedIndexedAccess` false
|
|
98
|
-
// positive, not a real fallback path.
|
|
99
|
-
let s = addr.toLowerCase().split('%')[0];
|
|
100
|
-
if (!s.includes(':'))
|
|
101
|
-
return null;
|
|
102
|
-
// Fold a trailing dotted IPv4 (`…:1.2.3.4`) into two hex groups.
|
|
103
|
-
const v4 = /^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(s);
|
|
104
|
-
if (v4) {
|
|
105
|
-
// `v4[2]` is the regex's second (mandatory, non-optional) capture group
|
|
106
|
-
// of a successful match — always defined.
|
|
107
|
-
const o = v4[2].split('.').map((n) => Number(n));
|
|
108
|
-
if (o.length !== 4 || o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
109
|
-
return null;
|
|
110
|
-
// `o` is now a validated 4-element array — indices 0-3 always defined.
|
|
111
|
-
const hi = (((o[0] << 8) | o[1])).toString(16);
|
|
112
|
-
const lo = (((o[2] << 8) | o[3])).toString(16);
|
|
113
|
-
s = `${v4[1]}${hi}:${lo}`;
|
|
114
|
-
}
|
|
115
|
-
const halves = s.split('::');
|
|
116
|
-
if (halves.length > 2)
|
|
117
|
-
return null;
|
|
118
|
-
const parse = (p) => p === '' ? [] : p.split(':').map((g) => (/^[0-9a-f]{1,4}$/.test(g) ? parseInt(g, 16) : Number.NaN));
|
|
119
|
-
// `halves` always has at least one element (split guarantee).
|
|
120
|
-
const left = parse(halves[0]);
|
|
121
|
-
let groups;
|
|
122
|
-
if (halves.length === 1) {
|
|
123
|
-
groups = left;
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
// `halves.length === 2` here (the `> 2` case already returned above).
|
|
127
|
-
const right = parse(halves[1]);
|
|
128
|
-
const fill = 8 - left.length - right.length;
|
|
129
|
-
if (fill < 1)
|
|
130
|
-
return null;
|
|
131
|
-
groups = [...left, ...new Array(fill).fill(0), ...right];
|
|
132
|
-
}
|
|
133
|
-
if (groups.length !== 8 || groups.some((g) => Number.isNaN(g) || g < 0 || g > 0xffff))
|
|
134
|
-
return null;
|
|
135
|
-
return groups;
|
|
136
|
-
}
|
|
137
|
-
/** True for any address an external CDN must never resolve to: loopback,
|
|
138
|
-
* link-local, private, CGNAT, ULA, multicast, or unspecified. An unparseable
|
|
139
|
-
* input is treated as unsafe. */
|
|
140
|
-
export function isPrivateAddress(addr) {
|
|
141
|
-
const fam = isIP(addr);
|
|
142
|
-
if (fam === 4) {
|
|
143
|
-
// `net.isIP(addr) === 4` already guarantees `addr` is a syntactically
|
|
144
|
-
// valid dotted-quad with four 0-255 decimal octets (no leading-zero
|
|
145
|
-
// octal ambiguity, no out-of-range component) — empirically re-verified
|
|
146
|
-
// this session by fuzzing 500k+ candidate strings through `isIP` and
|
|
147
|
-
// this same re-derivation with zero divergences. `o` is therefore always
|
|
148
|
-
// a validated 4-element array.
|
|
149
|
-
const o = addr.split('.').map((n) => Number(n));
|
|
150
|
-
const a = o[0];
|
|
151
|
-
const b = o[1];
|
|
152
|
-
if (a === 0)
|
|
153
|
-
return true; // "this" network / unspecified
|
|
154
|
-
if (a === 10)
|
|
155
|
-
return true; // private
|
|
156
|
-
if (a === 127)
|
|
157
|
-
return true; // loopback
|
|
158
|
-
if (a === 169 && b === 254)
|
|
159
|
-
return true; // link-local
|
|
160
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
161
|
-
return true; // private
|
|
162
|
-
if (a === 192 && b === 168)
|
|
163
|
-
return true; // private
|
|
164
|
-
if (a === 100 && b >= 64 && b <= 127)
|
|
165
|
-
return true; // CGNAT
|
|
166
|
-
if (a >= 224)
|
|
167
|
-
return true; // multicast + reserved
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
if (fam === 6) {
|
|
171
|
-
const groups = expandIpv6(addr);
|
|
172
|
-
// `net.isIP(addr) === 6` already guarantees `addr` is syntactically valid
|
|
173
|
-
// IPv6 (Node validates this before classifying the family), and
|
|
174
|
-
// `expandIpv6` implements the same canonicalization Node's own parser
|
|
175
|
-
// does, so it never returns `null` for an `isIP`-accepted literal —
|
|
176
|
-
// empirically re-verified this session by fuzzing ~10k `isIP`-accepted
|
|
177
|
-
// v6 literals through `expandIpv6` with zero `null` results (see
|
|
178
|
-
// `expandIpv6`'s doc comment and `source-map.md`'s 2026-07-22 entry).
|
|
179
|
-
// Kept as a real fail-closed guard (defense-in-depth against a future
|
|
180
|
-
// change to either parser) rather than asserted away, so it stays
|
|
181
|
-
// provably-unreachable-but-intentional instead of a forced test.
|
|
182
|
-
if (!groups)
|
|
183
|
-
return true; // can't canonicalize a "valid" v6 → refuse
|
|
184
|
-
// IPv4-mapped (`::ffff:0:0/96`) and the deprecated IPv4-compatible
|
|
185
|
-
// (`::0:0/96`, excluding :: / ::1) carry an embedded IPv4 — classify it by
|
|
186
|
-
// its real v4 range so `::ffff:7f00:1` (== 127.0.0.1) is caught.
|
|
187
|
-
const topZero = groups.slice(0, 5).every((g) => g === 0);
|
|
188
|
-
if (topZero && (groups[5] === 0xffff || groups[5] === 0)) {
|
|
189
|
-
// `groups` is a validated 8-element array once non-null — indices 6-7
|
|
190
|
-
// always defined.
|
|
191
|
-
const a = groups[6] >> 8;
|
|
192
|
-
const b = groups[6] & 0xff;
|
|
193
|
-
const c = groups[7] >> 8;
|
|
194
|
-
const d = groups[7] & 0xff;
|
|
195
|
-
if (groups[5] === 0xffff || a !== 0 || b !== 0) {
|
|
196
|
-
return isPrivateAddress(`${a}.${b}.${c}.${d}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (groups.every((g) => g === 0))
|
|
200
|
-
return true; // :: (unspecified)
|
|
201
|
-
if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1)
|
|
202
|
-
return true; // ::1 loopback
|
|
203
|
-
const first = groups[0];
|
|
204
|
-
if ((first & 0xffc0) === 0xfe80)
|
|
205
|
-
return true; // link-local fe80::/10 (fe80–febf)
|
|
206
|
-
if ((first & 0xfe00) === 0xfc00)
|
|
207
|
-
return true; // unique-local fc00::/7
|
|
208
|
-
if ((first & 0xff00) === 0xff00)
|
|
209
|
-
return true; // multicast ff00::/8
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
return true; // not a literal IP — caller must resolve before trusting it
|
|
213
|
-
}
|
|
214
79
|
/** Cheap up-front rejection: enforce http(s), no embedded credentials, no
|
|
215
80
|
* localhost, and no *literal* private IP. This is NOT the DNS-rebinding guard
|
|
216
81
|
* (that is `createValidatingLookup`, applied at connection time) — it only
|
|
@@ -234,7 +99,7 @@ export function assertSafePublicUrl(raw) {
|
|
|
234
99
|
if (lowerHost === 'localhost' || lowerHost.endsWith('.localhost')) {
|
|
235
100
|
throw new AssetCacheError(400, 'localhost is not allowed');
|
|
236
101
|
}
|
|
237
|
-
if (isIP(host) && isPrivateAddress(host)) {
|
|
102
|
+
if (isIP(host) && isPrivateAddress({ address: host })) {
|
|
238
103
|
throw new AssetCacheError(400, 'url points at a private address');
|
|
239
104
|
}
|
|
240
105
|
return url;
|
|
@@ -254,7 +119,7 @@ export function createValidatingLookup(lookupImpl = dnsLookupCb) {
|
|
|
254
119
|
const list = Array.isArray(address) ? address : [{ address, family }];
|
|
255
120
|
for (const entry of list) {
|
|
256
121
|
const addr = typeof entry === 'string' ? entry : entry.address;
|
|
257
|
-
if (isPrivateAddress(addr)) {
|
|
122
|
+
if (isPrivateAddress({ address: addr })) {
|
|
258
123
|
return cb(new AssetCacheError(400, 'host resolves to a private address'));
|
|
259
124
|
}
|
|
260
125
|
}
|
package/dist/download.js
CHANGED
|
@@ -130,7 +130,7 @@ function targetFromOptions(options) {
|
|
|
130
130
|
// `path.resolve` never actually walked upward. Empirically verified this
|
|
131
131
|
// session: a 300k-segment fuzz found ~0.2% of `normalizeSegment`-valid
|
|
132
132
|
// segments (every one starting with literal "..") trip this exact guard —
|
|
133
|
-
// see
|
|
133
|
+
// see archived provenance ledger's 2026-07-22 entry and this file's own test for a
|
|
134
134
|
// concrete example (bucket "..evil").
|
|
135
135
|
for (const path of [finalPath, manifestPath, partialPath, lockPath]) {
|
|
136
136
|
if (!pathContains(basePath, path)) {
|
|
@@ -473,7 +473,7 @@ async function acquireLock(target) {
|
|
|
473
473
|
// no trailing fallback throw is needed. Every path out of this loop body is
|
|
474
474
|
// a `return` or a `throw`; V8 still instruments the loop's closing brace as
|
|
475
475
|
// a branch (whether execution "falls off" the loop normally), but that
|
|
476
|
-
// outcome is structurally impossible here — see
|
|
476
|
+
// outcome is structurally impossible here — see archived provenance ledger's
|
|
477
477
|
// 2026-07-22 entry.
|
|
478
478
|
for (let attempt = 0;; attempt += 1) {
|
|
479
479
|
try {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
3
|
+
import { type PlatformMessages } from "../messages.js";
|
|
4
|
+
/** Synchronous seam preserves the original fail-before-rename and permission contract.
|
|
5
|
+
* Descriptor ownership separates failed exclusive creates from failures after creating a temp. */
|
|
6
|
+
export interface AtomicFilesystemPort {
|
|
7
|
+
stat(required: {
|
|
8
|
+
path: string;
|
|
9
|
+
}): {
|
|
10
|
+
mode: number;
|
|
11
|
+
};
|
|
12
|
+
lstat(required: {
|
|
13
|
+
path: string;
|
|
14
|
+
}): {
|
|
15
|
+
mode: number;
|
|
16
|
+
isSymbolicLink(): boolean;
|
|
17
|
+
};
|
|
18
|
+
open(required: {
|
|
19
|
+
path: string;
|
|
20
|
+
flags: "wx" | "r";
|
|
21
|
+
mode?: number;
|
|
22
|
+
}): number;
|
|
23
|
+
write(required: {
|
|
24
|
+
descriptor: number;
|
|
25
|
+
content: string;
|
|
26
|
+
}): void;
|
|
27
|
+
sync(required: {
|
|
28
|
+
descriptor: number;
|
|
29
|
+
}): void;
|
|
30
|
+
close(required: {
|
|
31
|
+
descriptor: number;
|
|
32
|
+
}): void;
|
|
33
|
+
chmod(required: {
|
|
34
|
+
path: string;
|
|
35
|
+
mode: number;
|
|
36
|
+
}): void;
|
|
37
|
+
rename(required: {
|
|
38
|
+
from: string;
|
|
39
|
+
to: string;
|
|
40
|
+
}): void;
|
|
41
|
+
remove(required: {
|
|
42
|
+
path: string;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|
|
45
|
+
export type AtomicNativeFilesystem = Pick<typeof fs, "statSync" | "lstatSync" | "openSync" | "writeFileSync" | "fsyncSync" | "closeSync" | "chmodSync" | "renameSync" | "rmSync">;
|
|
46
|
+
export type AtomicAsyncFilesystem = Pick<typeof fsPromises, "stat" | "lstat" | "open" | "chmod" | "rename" | "rm" | "mkdir">;
|
|
47
|
+
export interface AtomicWriteRequired {
|
|
48
|
+
filePath: string;
|
|
49
|
+
content: string;
|
|
50
|
+
fs: AtomicFilesystemPort;
|
|
51
|
+
}
|
|
52
|
+
export interface AtomicWriteOptions {
|
|
53
|
+
messages?: PlatformMessages;
|
|
54
|
+
/** Optional collision token; native pid + UUID restores the original default. */
|
|
55
|
+
tempName?: (required: {
|
|
56
|
+
filePath: string;
|
|
57
|
+
}) => string;
|
|
58
|
+
/** Explicit mode overrides preservation, e.g. secret-bearing files must use 0600. */
|
|
59
|
+
mode?: number;
|
|
60
|
+
/** Default for a missing destination; shared durable stores retain their former 0666 create mode. */
|
|
61
|
+
defaultMode?: number;
|
|
62
|
+
refuseSymlink?: boolean;
|
|
63
|
+
/** Verify owner-only permissions before rename on POSIX, as secret stores require. */
|
|
64
|
+
verifyOwnerOnly?: boolean;
|
|
65
|
+
platform?: NodeJS.Platform;
|
|
66
|
+
/** Same-directory legacy temp layout, for stores that expose their temp path publicly. */
|
|
67
|
+
tempPath?: (required: {
|
|
68
|
+
filePath: string;
|
|
69
|
+
}) => string;
|
|
70
|
+
}
|
|
71
|
+
/** Optional adapter wiring: no layout, env key, or application name is selected here. */
|
|
72
|
+
export declare function createNodeAtomicFilesystem(_required: Record<string, never>, { filesystem }?: {
|
|
73
|
+
filesystem?: AtomicNativeFilesystem;
|
|
74
|
+
}): AtomicFilesystemPort;
|
|
75
|
+
/** Same-directory rename; preserve existing mode exactly, default new files to 0600.
|
|
76
|
+
* Only ENOENT means absent. Stat/write/chmod failures propagate before any rename.
|
|
77
|
+
* Flush file bytes before rename so metadata cannot precede content; then flush the parent dir.
|
|
78
|
+
* A directory-sync failure is reported after replacement, because rename cannot be rolled back safely.
|
|
79
|
+
*/
|
|
80
|
+
export declare function writeFileAtomic(required: AtomicWriteRequired, options?: AtomicWriteOptions): void;
|
|
81
|
+
/** Pretty JSON delegates to the same mode-preserving atomic primitive. */
|
|
82
|
+
export declare function writeJsonFileAtomic(required: Omit<AtomicWriteRequired, "content"> & {
|
|
83
|
+
data: unknown;
|
|
84
|
+
}, options?: AtomicWriteOptions): void;
|
|
85
|
+
/** Async counterpart with the same ownership, permission and durability guarantees.
|
|
86
|
+
* Parent creation supports runtime-state and secret stores without another writer implementation. */
|
|
87
|
+
export declare function writeFileAtomicAsync(required: {
|
|
88
|
+
filePath: string;
|
|
89
|
+
content: string;
|
|
90
|
+
}, options?: AtomicWriteOptions & {
|
|
91
|
+
fs?: AtomicAsyncFilesystem;
|
|
92
|
+
createParent?: boolean;
|
|
93
|
+
}): Promise<void>;
|
|
94
|
+
/** Pretty JSON can retain a state-store's trailing newline without changing its serialization. */
|
|
95
|
+
export declare function writeJsonFileAtomicAsync(required: {
|
|
96
|
+
filePath: string;
|
|
97
|
+
data: unknown;
|
|
98
|
+
}, options?: AtomicWriteOptions & {
|
|
99
|
+
fs?: AtomicAsyncFilesystem;
|
|
100
|
+
createParent?: boolean;
|
|
101
|
+
trailingNewline?: boolean;
|
|
102
|
+
}): Promise<void>;
|
|
103
|
+
//# sourceMappingURL=atomic-write.d.ts.map
|