@dokebi/cli 0.4.0 → 0.4.2

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/dist/verify.js CHANGED
@@ -61,10 +61,31 @@ export async function npmScript(root, script, timeoutMs) {
61
61
  export const VERIFY_TOKEN_ENV = 'DOKEBI_VERIFY_TOKEN';
62
62
  /** The response header @dokebi/web answers with when that variable is set. */
63
63
  export const VERIFY_HEADER = 'x-dokebi-verify';
64
- /** GET with a Host header (fetch cannot set one). */
64
+ /**
65
+ * GET with a Host header (fetch cannot set one). Settles once: with the answer when the body
66
+ * ended, or with an error when the request fails, the response is aborted or its connection
67
+ * closes before the end (ISSUE-0006 A09), or nothing complete came within `timeoutMs` in all
68
+ * (the socket timeout alone never fires for a server that keeps trickling bytes).
69
+ */
65
70
  function get(url, host, timeoutMs = 5000) {
66
71
  return new Promise((resolve, reject) => {
67
72
  const target = new URL(url);
73
+ let settled = false;
74
+ const finish = (error, answer) => {
75
+ if (settled)
76
+ return;
77
+ settled = true;
78
+ clearTimeout(deadline);
79
+ if (error) {
80
+ // Frees the socket of a response that will never complete.
81
+ if (!req.destroyed)
82
+ req.destroy();
83
+ reject(error);
84
+ }
85
+ else
86
+ resolve(answer);
87
+ };
88
+ const failed = (error) => finish(error);
68
89
  const req = request({
69
90
  host: target.hostname.replace(/^\[|\]$/g, ''),
70
91
  port: target.port,
@@ -73,14 +94,23 @@ function get(url, host, timeoutMs = 5000) {
73
94
  timeout: timeoutMs,
74
95
  }, (response) => {
75
96
  response.resume();
76
- response.once('end', () => resolve({
97
+ response.once('end', () => finish(undefined, {
77
98
  status: response.statusCode ?? 0,
78
99
  type: String(response.headers['content-type'] ?? ''),
79
100
  instance: response.headers[VERIFY_HEADER],
80
101
  }));
102
+ response.once('aborted', () => finish(new Error('The response was aborted')));
103
+ // Error listeners stay after settling: a late error (the destroy below) is then ignored
104
+ // by `finish` instead of being an unhandled 'error' event.
105
+ response.on('error', failed);
106
+ response.once('close', () => {
107
+ if (!response.complete)
108
+ finish(new Error('The connection closed before the response ended'));
109
+ });
81
110
  });
111
+ const deadline = setTimeout(() => finish(new Error(`No complete answer within ${timeoutMs} ms`)), timeoutMs);
82
112
  req.once('timeout', () => req.destroy(new Error(`No answer within ${timeoutMs} ms`)));
83
- req.once('error', reject);
113
+ req.on('error', failed);
84
114
  req.end();
85
115
  });
86
116
  }
@@ -123,18 +153,30 @@ async function settings(root) {
123
153
  * The HTTP check of verify: GET the readiness path, then `/` of each app domain, at `base`. An
124
154
  * answer counts only when it carries `token` in the x-dokebi-verify header (the instance verify
125
155
  * started); anything else is retried like a failed request and then reported FOREIGN_SERVER.
156
+ * With neither a readiness path (`healthRoutes: false`) nor an app domain, one identity probe
157
+ * runs instead (ISSUE-0006 A08: no request at all used to pass): `GET /`, which passes with any
158
+ * status below 500 that carries the token, a 404 included (the app may have no route at `/`).
159
+ * Each request must complete within `timeoutMs` (ISSUE-0006 A09).
126
160
  */
127
- export async function httpChecks(base, paths, token, delayMs = 250) {
161
+ export async function httpChecks(base, paths, token, delayMs = 250, timeoutMs = 5000) {
128
162
  const requests = [];
129
- const probe = async (path, host, attempts) => {
163
+ const probe = async (path, host, attempts, identity = false) => {
130
164
  const url = `${base}${path}`;
165
+ const kind = identity ? { identity: true } : {};
131
166
  for (let attempt = 1;; attempt++) {
132
167
  try {
133
- const answer = await get(url, host);
168
+ const answer = await get(url, host, timeoutMs);
134
169
  const instance = answer.instance === token ? 'ours' : 'other';
135
- const ok = answer.status >= 200 && answer.status < 400 && instance === 'ours';
170
+ const ok = answer.status >= 200 && answer.status < (identity ? 500 : 400) && instance === 'ours';
136
171
  if (ok || attempt >= attempts) {
137
- requests.push({ url, ...(host ? { host } : {}), status: answer.status, ok, instance });
172
+ requests.push({
173
+ url,
174
+ ...(host ? { host } : {}),
175
+ status: answer.status,
176
+ ok,
177
+ instance,
178
+ ...kind,
179
+ });
138
180
  return;
139
181
  }
140
182
  if (instance === 'other')
@@ -151,6 +193,7 @@ export async function httpChecks(base, paths, token, delayMs = 250) {
151
193
  status: 0,
152
194
  ok: false,
153
195
  error: error instanceof Error ? error.message : String(error),
196
+ ...kind,
154
197
  });
155
198
  return;
156
199
  }
@@ -163,6 +206,10 @@ export async function httpChecks(base, paths, token, delayMs = 250) {
163
206
  await probe(paths.ready, undefined, 20);
164
207
  for (const domain of paths.domains)
165
208
  await probe('/', domain, 4);
209
+ if (!paths.ready && !paths.domains.length) {
210
+ debug('verify: no readiness route and no app domain; probing GET / for this run', { base });
211
+ await probe('/', undefined, 20, true);
212
+ }
166
213
  const failed = requests.filter((item) => !item.ok);
167
214
  const foreign = failed.filter((item) => item.instance === 'other');
168
215
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dokebi/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Project scaffolding, plugin catalog, recipes, install and verification CLI for Dokebi.",
5
5
  "keywords": [
6
6
  "dokebi",