@getstrata/cli 2.2.0 → 2.2.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/CHANGELOG.md +11 -0
- package/README.md +2 -2
- package/package.json +7 -3
- package/src/initialAdmin.ts +99 -0
- package/src/queueFailed.ts +27 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @getstrata/cli changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.2
|
|
4
|
+
|
|
5
|
+
- orm improvments
|
|
6
|
+
|
|
7
|
+
## 2.2.1
|
|
8
|
+
|
|
9
|
+
F02 — complete queue durability. Queues have leased reservations, recovery and fenced acknowledgement, but still use Redis lists. Persisted retry schedules, execution deadlines, cancellation and the planned Streams migration remain unfinished. Retries currently wait inside the running worker.
|
|
10
|
+
F10 — production public-route tenancy. Separate anonymous access from development tenant headers, support a trusted-host resolver, reject unknown hosts, and accept forwarded hosts only through configured trusted proxies. The shop still lacks the approved-domain mapping.
|
|
11
|
+
F12 — initial administrator provisioning and migration guidance. Automatic demo seeding is fixed. Secure initial-admin provisioning and a consolidated upgrade/operations guide remain.
|
|
12
|
+
Distributed qualification. Cache generations/fill leases, scheduler ownership, OpenTelemetry, body limits and lifecycle controls now exist. Their implementation should no longer be described as entirely pending, but broader failure and capacity qualification is still required.
|
|
13
|
+
|
|
3
14
|
## 2.2.0
|
|
4
15
|
|
|
5
16
|
Consistent model scopes.
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ strata help
|
|
|
19
19
|
|
|
20
20
|
`strata start` does not migrate when `APP_ENV=production`. Run `strata migrate` as an explicit deploy step.
|
|
21
21
|
|
|
22
|
-
App commands from `src/cli/register.ts` merge with this list. Generated apps add `queue:work`, failed-job commands, `make:*`, `openapi:*`, and `schedule:run`.
|
|
22
|
+
App commands from `src/cli/register.ts` merge with this list. Generated apps add `queue:work`, failed-job commands, `make:*`, `openapi:*`, and `schedule:run`. User-auth starters also add explicit `auth:provision-admin`; its `initialAdmin` subpath reads bounded password input from a pipe and redacts provisioning errors. The app owns its transactional one-time claim and user schema. See [initial-admin provisioning](https://github.com/EyK-26/strata/blob/main/docs/INITIAL-ADMIN.md) before adoption.
|
|
23
23
|
|
|
24
24
|
## Running it
|
|
25
25
|
|
|
@@ -51,7 +51,7 @@ From the current working directory, `strata` loads `strata.config.ts` if present
|
|
|
51
51
|
| `migrate` | `src/db/migrate.ts` |
|
|
52
52
|
| `fresh` | `src/db/fresh.ts` |
|
|
53
53
|
|
|
54
|
-
`src/cli/register.ts` can export `commands` or `registerCommands()` to add your own commands. Generated apps ship `queue:work`, which boots the app (`bootstrapApp` / `createApp`) rather than the monorepo provider stack, plus `make:*`, failed-job commands, `openapi:*`, and `schedule:run`.
|
|
54
|
+
`src/cli/register.ts` can export `commands` or `registerCommands()` to add your own commands. Generated apps ship `queue:work`, which boots the app (`bootstrapApp` / `createApp`) rather than the monorepo provider stack, plus `make:*`, failed-job commands, `openapi:*`, and `schedule:run`. User-auth starters also add explicit `auth:provision-admin`; its `initialAdmin` subpath reads bounded password input from a pipe and redacts provisioning errors. The app owns its transactional one-time claim and user schema. See [initial-admin provisioning](https://github.com/EyK-26/strata/blob/main/docs/INITIAL-ADMIN.md) before adoption.
|
|
55
55
|
|
|
56
56
|
Helpers live on package subpaths: `@getstrata/cli/queueWorker`, `@getstrata/cli/queueFailed`, `@getstrata/cli/scaffold`, `@getstrata/cli/openapi`, and `@getstrata/cli/schedule`. `queue:work` takes `{ boot, close }` and does not call `assertProductionSecrets()` (that belongs in app boot). Scaffold commands resolve paths from the app working directory (`src/modules`, `src/db/migrations`, `src/jobs`, `views/` or `resources/views`). See [docs/BUILDING-APPS.md](https://github.com/EyK-26/strata/blob/main/docs/BUILDING-APPS.md#extending-the-cli).
|
|
57
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/cli",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Strata CLI: framework commands for Bun apps (dev, start, migrate, run)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"./schedule": {
|
|
37
37
|
"import": "./src/schedule.ts",
|
|
38
38
|
"default": "./src/schedule.ts"
|
|
39
|
+
},
|
|
40
|
+
"./initialAdmin": {
|
|
41
|
+
"import": "./src/initialAdmin.ts",
|
|
42
|
+
"default": "./src/initialAdmin.ts"
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
"files": [
|
|
@@ -46,8 +50,8 @@
|
|
|
46
50
|
],
|
|
47
51
|
"scripts": {},
|
|
48
52
|
"peerDependencies": {
|
|
49
|
-
"@getstrata/bootstrap": "^2.2.
|
|
50
|
-
"@getstrata/core": "^2.2.
|
|
53
|
+
"@getstrata/bootstrap": "^2.2.2",
|
|
54
|
+
"@getstrata/core": "^2.2.2"
|
|
51
55
|
},
|
|
52
56
|
"publishConfig": {
|
|
53
57
|
"access": "public"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { stdin } from "node:process";
|
|
2
|
+
|
|
3
|
+
type InitialAdminInput = {
|
|
4
|
+
email: string;
|
|
5
|
+
name: string;
|
|
6
|
+
password: string;
|
|
7
|
+
tenantId?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type InitialAdminOptions = {
|
|
11
|
+
tenantRequired: boolean;
|
|
12
|
+
provision: (input: InitialAdminInput) => Promise<void>;
|
|
13
|
+
/** Trusted test/embedding input; never accept password arguments or environment defaults. */
|
|
14
|
+
input?: AsyncIterable<Uint8Array>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function runInitialAdminCommand(args: string[], options: InitialAdminOptions): Promise<void> {
|
|
18
|
+
const flags = new Map<string, string>();
|
|
19
|
+
let passwordStdin = false;
|
|
20
|
+
for (let index = 0; index < args.length; index++) {
|
|
21
|
+
const flag = args[index];
|
|
22
|
+
if (flag === "--password-stdin" && !passwordStdin) {
|
|
23
|
+
passwordStdin = true;
|
|
24
|
+
} else if (flag === "--email" || flag === "--name" || flag === "--tenant") {
|
|
25
|
+
const value = args[++index];
|
|
26
|
+
if (!value || value.startsWith("--") || flags.has(flag)) {
|
|
27
|
+
throw new Error("Initial-admin options must have one explicit value each.");
|
|
28
|
+
}
|
|
29
|
+
flags.set(flag, value);
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error("Use --email, --name, --password-stdin and, for tenant apps, --tenant.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const email = (flags.get("--email") ?? "").trim().toLowerCase();
|
|
35
|
+
const name = (flags.get("--name") ?? "").trim();
|
|
36
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || email.length > 254 || /[\p{C}]/u.test(email)) {
|
|
37
|
+
throw new Error("A valid initial-admin email is required.");
|
|
38
|
+
}
|
|
39
|
+
if (!name || name.length > 200 || /[\p{C}]/u.test(name)) {
|
|
40
|
+
throw new Error("A nonempty initial-admin name without control characters is required.");
|
|
41
|
+
}
|
|
42
|
+
const tenantRaw = flags.get("--tenant");
|
|
43
|
+
const tenantId = tenantRaw === undefined ? undefined : Number(tenantRaw);
|
|
44
|
+
if (options.tenantRequired) {
|
|
45
|
+
if (!tenantRaw || !/^[1-9]\d*$/.test(tenantRaw) || !Number.isSafeInteger(tenantId)) {
|
|
46
|
+
throw new Error("An existing positive safe-integer tenant ID is required.");
|
|
47
|
+
}
|
|
48
|
+
} else if (tenantRaw !== undefined) {
|
|
49
|
+
throw new Error("This application does not accept a tenant option.");
|
|
50
|
+
}
|
|
51
|
+
if (!passwordStdin || (!options.input && stdin.isTTY)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Provide --password-stdin through a pipe; interactive terminal input is refused.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const chunks: Uint8Array[] = [];
|
|
57
|
+
let size = 0;
|
|
58
|
+
// Bcrypt accepts at most 72 UTF-8 bytes; allow a single terminal CRLF.
|
|
59
|
+
for await (const chunk of options.input ?? stdin) {
|
|
60
|
+
size += chunk.byteLength;
|
|
61
|
+
if (size > 74) throw new Error("Initial-admin password exceeds the supported byte limit.");
|
|
62
|
+
chunks.push(chunk);
|
|
63
|
+
}
|
|
64
|
+
const bytes = new Uint8Array(size);
|
|
65
|
+
let offset = 0;
|
|
66
|
+
for (const chunk of chunks) {
|
|
67
|
+
bytes.set(chunk, offset);
|
|
68
|
+
offset += chunk.byteLength;
|
|
69
|
+
}
|
|
70
|
+
let password: string;
|
|
71
|
+
try {
|
|
72
|
+
password = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true })
|
|
73
|
+
.decode(bytes)
|
|
74
|
+
.replace(/\r?\n$/, "");
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error("Initial-admin password must be valid UTF-8.");
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
[...password].length < 16 ||
|
|
80
|
+
new TextEncoder().encode(password).byteLength > 72 ||
|
|
81
|
+
/[\p{C}]/u.test(password) ||
|
|
82
|
+
password === "StrataDemo!ChangeMe"
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"Use a unique password of at least 16 characters and at most 72 UTF-8 bytes, without control characters or demo defaults.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await options.provision({ email, name, password, tenantId });
|
|
90
|
+
} catch {
|
|
91
|
+
// Drivers/observers can include bound values in errors. Never forward credential-bearing causes.
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Initial-admin provisioning failed. Check schema, tenant, existing admins/accounts and the one-time claim; no account is promoted or overwritten.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type { InitialAdminInput, InitialAdminOptions };
|
|
99
|
+
export { runInitialAdminCommand };
|
package/src/queueFailed.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createAppQueue,
|
|
3
|
+
createFailedJobService,
|
|
4
|
+
resolveQueueConfig,
|
|
5
|
+
} from "@getstrata/core/queue/createAppQueue";
|
|
2
6
|
import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
|
|
3
|
-
import {
|
|
7
|
+
import { resolveApplicationQueue } from "@getstrata/core/runtime/applicationRegistry";
|
|
4
8
|
import type { StrataCommand } from "./types.ts";
|
|
5
9
|
|
|
6
10
|
type QueueFailedBoot = () => unknown | Promise<unknown>;
|
|
@@ -25,26 +29,30 @@ async function queueRetryCommand(id?: string, boot?: QueueFailedBoot): Promise<v
|
|
|
25
29
|
throw new Error("queue:retry requires a failed job id.");
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
const jobId = Number(id);
|
|
33
|
+
if (!Number.isSafeInteger(jobId) || jobId <= 0)
|
|
34
|
+
throw new Error("queue:retry requires a positive integer failed job id.");
|
|
28
35
|
await boot?.();
|
|
29
36
|
const failedJobs = createFailedJobService();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
// App boot owns its configured queue; standalone CLI dispatch owns and closes its queue.
|
|
38
|
+
const queue = boot
|
|
39
|
+
? resolveApplicationQueue()
|
|
40
|
+
: createAppQueue(resolveQueueConfig().driver, process.env.REDIS_URL, failedJobs);
|
|
41
|
+
try {
|
|
42
|
+
await failedJobs.retry(jobId, async (failedJob) => {
|
|
43
|
+
const job = jobRegistry.create(failedJob.job_name);
|
|
44
|
+
if (!job) throw new Error(`Unknown job "${failedJob.job_name}".`);
|
|
45
|
+
jobRegistry.track(failedJob.job_name, job);
|
|
46
|
+
if (failedJob.job_id !== null && failedJob.job_id !== undefined) {
|
|
47
|
+
if (!queue.replay)
|
|
48
|
+
throw new Error("Configured queue cannot preserve the failed-job identity");
|
|
49
|
+
await queue.replay(job, failedJob.payload, failedJob.job_id);
|
|
50
|
+
} else await queue.dispatch(job, failedJob.payload);
|
|
51
|
+
});
|
|
52
|
+
console.log(`Enqueued failed job #${jobId}.`);
|
|
53
|
+
} finally {
|
|
54
|
+
if (!boot) await queue.close?.();
|
|
36
55
|
}
|
|
37
|
-
|
|
38
|
-
await runQueueJob(
|
|
39
|
-
{
|
|
40
|
-
name: failedJob.job_name,
|
|
41
|
-
payload: failedJob.payload,
|
|
42
|
-
attempts: 0,
|
|
43
|
-
},
|
|
44
|
-
failedJobs,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
console.log(`Retried failed job #${id}.`);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
async function queueFlushFailedCommand(boot?: QueueFailedBoot): Promise<void> {
|