@almadar/shell-node 0.0.0-stage → 1.0.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.
@@ -0,0 +1,183 @@
1
+ import { readFile, mkdir, writeFile, rename } from 'fs/promises';
2
+ import { join, dirname } from 'path';
3
+ import { EntityRowSchema, runRowWrite } from '@almadar/core';
4
+ import { AsyncLocalStorage } from 'async_hooks';
5
+ import { installCredentialScopeStorage, CREDENTIAL_MASTER_KEY_ENV, CREDENTIAL_PREVIOUS_MASTER_KEY_ENV, CredentialVault, installCredentialStore } from '@almadar/shell-runtime';
6
+ import { lookup } from 'dns/promises';
7
+ import { request } from 'https';
8
+ import { isIP } from 'net';
9
+
10
+ // src/credential-rows.ts
11
+ var CREDENTIALS_FILE_ENV = "ALMADAR_CREDENTIALS_FILE";
12
+ var DEFAULT_CREDENTIALS_FILE = ".almadar/dev-credentials.json";
13
+ var CREDENTIAL_ROWS_ENTITY = "AlmadarIntegrationCredential";
14
+ function credentialsFilePath(env = process.env, cwd = process.cwd()) {
15
+ const named = env[CREDENTIALS_FILE_ENV];
16
+ return named !== void 0 && named !== "" ? named : join(cwd, DEFAULT_CREDENTIALS_FILE);
17
+ }
18
+ var gates = /* @__PURE__ */ new Map();
19
+ function gateOf(path) {
20
+ const held = gates.get(path);
21
+ if (held !== void 0) return held;
22
+ const gate = { tail: Promise.resolve() };
23
+ gates.set(path, gate);
24
+ return gate;
25
+ }
26
+ function isMissing(error) {
27
+ return "code" in error && error.code === "ENOENT";
28
+ }
29
+ function fileCredentialRows(path) {
30
+ const gate = gateOf(path);
31
+ const read = async () => {
32
+ let text;
33
+ try {
34
+ text = await readFile(path, "utf8");
35
+ } catch (error) {
36
+ if (error instanceof Error && isMissing(error)) return [];
37
+ throw error;
38
+ }
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(text);
42
+ } catch (error) {
43
+ throw new Error(`credential file ${path} is not JSON: ${error instanceof Error ? error.message : String(error)}`);
44
+ }
45
+ const rows = EntityRowSchema.array().safeParse(parsed);
46
+ if (!rows.success) throw new Error(`credential file ${path} is not an array of rows: ${rows.error.message}`);
47
+ const at = rows.data.findIndex((row) => typeof row.id !== "string");
48
+ if (at >= 0) throw new Error(`credential file ${path}: row ${at} carries no id`);
49
+ return rows.data;
50
+ };
51
+ const write = async (rows) => {
52
+ await mkdir(dirname(path), { recursive: true });
53
+ const temporary = `${path}.tmp`;
54
+ await writeFile(temporary, JSON.stringify(rows, null, 2));
55
+ await rename(temporary, path);
56
+ };
57
+ const change = (work) => {
58
+ const next = gate.tail.then(async () => {
59
+ const { rows, result, changed } = work(await read());
60
+ if (changed) await write(rows);
61
+ return result;
62
+ });
63
+ gate.tail = next.then(
64
+ () => void 0,
65
+ () => void 0
66
+ );
67
+ return next;
68
+ };
69
+ return {
70
+ async list() {
71
+ await gate.tail;
72
+ return read();
73
+ },
74
+ create: (row) => change((rows) => ({ rows: [...rows, row], result: void 0, changed: true })),
75
+ update: (id, data) => change((rows) => ({
76
+ rows: rows.map((row) => row.id === id ? { ...row, ...data, id } : row),
77
+ result: void 0,
78
+ changed: rows.some((row) => row.id === id)
79
+ })),
80
+ delete: (id) => change((rows) => {
81
+ const written = runRowWrite(rows, { kind: "delete", entityType: CREDENTIAL_ROWS_ENTITY, id });
82
+ const removed = written.outcome.kind === "deleted";
83
+ return { rows: written.rows, result: removed, changed: removed };
84
+ })
85
+ };
86
+ }
87
+ function nodeCredentialScope() {
88
+ return new AsyncLocalStorage();
89
+ }
90
+ var installed = false;
91
+ function installNodeHostPorts() {
92
+ if (installed) return;
93
+ installed = true;
94
+ installCredentialScopeStorage(nodeCredentialScope());
95
+ }
96
+ async function installNodeCredentialVault(options) {
97
+ const env = options.env ?? process.env;
98
+ const master = env[CREDENTIAL_MASTER_KEY_ENV];
99
+ if (master === void 0) return null;
100
+ const previous = env[CREDENTIAL_PREVIOUS_MASTER_KEY_ENV];
101
+ const vault = await CredentialVault.open(
102
+ new options.kernel.CredentialVault(master, previous === void 0 || previous === "" ? void 0 : previous),
103
+ fileCredentialRows(credentialsFilePath(env, options.cwd))
104
+ );
105
+ installCredentialStore(vault);
106
+ return vault;
107
+ }
108
+ var IO_TIMEOUT_MS = 5e3;
109
+ async function systemResolve(host) {
110
+ return lookup(host, { all: true });
111
+ }
112
+ var TokenPostRefusal = class extends Error {
113
+ constructor(message) {
114
+ super(message);
115
+ this.name = "TokenPostRefusal";
116
+ }
117
+ };
118
+ function urlOf(endpoint) {
119
+ try {
120
+ return new URL(endpoint);
121
+ } catch {
122
+ throw new TokenPostRefusal("the token endpoint is not a url");
123
+ }
124
+ }
125
+ function createNodeTokenPost(options) {
126
+ const send = options.request ?? request;
127
+ const resolve = options.resolve ?? systemResolve;
128
+ return async ({ endpoint, headers, body }) => {
129
+ const url = urlOf(endpoint);
130
+ if (url.protocol !== "https:") throw new TokenPostRefusal("the token endpoint must be https");
131
+ if (!options.egressAllows(endpoint)) throw new TokenPostRefusal(`${url.host} is not an origin the program declares in egress`);
132
+ const host = url.hostname.replace(/^\[/, "").replace(/\]$/, "");
133
+ const addresses = isIP(host) !== 0 ? [{ address: host, family: isIP(host) }] : await resolve(host);
134
+ if (addresses.length === 0) throw new TokenPostRefusal(`${host} has no addresses`);
135
+ const private_ = addresses.find((entry) => !options.isPublicAddress(entry.address));
136
+ if (private_ !== void 0) throw new TokenPostRefusal(`${host} resolves to ${private_.address}, not a public address`);
137
+ const checked = addresses[0];
138
+ const payload = Buffer.from(body, "utf8");
139
+ const requestHeaders = {};
140
+ for (const [name, value] of headers) requestHeaders[name] = value;
141
+ requestHeaders["Content-Length"] = payload.length;
142
+ requestHeaders["Connection"] = "close";
143
+ return new Promise((settle, reject) => {
144
+ const req = send(
145
+ {
146
+ method: "POST",
147
+ protocol: "https:",
148
+ host,
149
+ port: url.port === "" ? 443 : Number(url.port),
150
+ path: `${url.pathname}${url.search}`,
151
+ headers: requestHeaders,
152
+ timeout: IO_TIMEOUT_MS,
153
+ servername: isIP(host) === 0 ? host : void 0,
154
+ lookup: (_name, lookupOptions, callback) => {
155
+ if (lookupOptions.all === true) callback(null, [{ address: checked.address, family: checked.family }]);
156
+ else callback(null, checked.address, checked.family);
157
+ }
158
+ },
159
+ (response) => {
160
+ const status = response.statusCode ?? 0;
161
+ if (status >= 300 && status < 400) {
162
+ response.resume();
163
+ reject(new TokenPostRefusal(`the token endpoint answered a redirect (${status}); redirects are not followed`));
164
+ req.destroy();
165
+ return;
166
+ }
167
+ const chunks = [];
168
+ response.on("data", (chunk) => chunks.push(chunk));
169
+ response.on("end", () => settle({ status, body: Buffer.concat(chunks).toString("utf8") }));
170
+ response.on("error", () => reject(new TokenPostRefusal("the token endpoint response failed")));
171
+ }
172
+ );
173
+ req.on("timeout", () => {
174
+ reject(new TokenPostRefusal("the token endpoint timed out"));
175
+ req.destroy();
176
+ });
177
+ req.on("error", (error) => reject(new TokenPostRefusal(`could not reach the token endpoint${error.code !== void 0 ? ` (${error.code})` : ""}`)));
178
+ req.end(payload);
179
+ });
180
+ };
181
+ }
182
+
183
+ export { DEFAULT_CREDENTIALS_FILE, TokenPostRefusal, createNodeTokenPost, credentialsFilePath, fileCredentialRows, installNodeCredentialVault, installNodeHostPorts };