@o3co/create-auth-policy-verifier 0.3.1 → 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/README.ja.md +27 -7
- package/README.md +27 -7
- package/dist/cli.mjs +0 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +110 -12
- package/package.json +40 -30
- package/templates/standalone/Dockerfile +75 -11
- package/templates/standalone/README.ja.md +245 -12
- package/templates/standalone/README.md +248 -12
- package/templates/standalone/config/application.conf +307 -4
- package/templates/standalone/config/development.conf +8 -0
- package/templates/standalone/config/production.conf +9 -0
- package/templates/standalone/docker-compose.yml +7 -0
- package/templates/standalone/package.json +11 -9
- package/templates/standalone/src/__tests__/configPath.test.mts +37 -0
- package/templates/standalone/src/__tests__/loadConfig.test.mts +415 -0
- package/templates/standalone/src/__tests__/logger.test.mts +56 -0
- package/templates/standalone/src/__tests__/smoke.test.mts +254 -48
- package/templates/standalone/src/configPath.ts +21 -0
- package/templates/standalone/src/loadConfig.ts +22 -0
- package/templates/standalone/src/logger.ts +33 -0
- package/templates/standalone/src/main.mts +11 -14
- package/templates/standalone/tsconfig.json +2 -1
- package/templates/standalone/tsconfig.typecheck.json +8 -0
- package/templates/versions.json +3 -3
- package/dist/__tests__/index.test.d.mts +0 -2
- package/dist/__tests__/index.test.d.mts.map +0 -1
- package/dist/__tests__/index.test.mjs +0 -213
package/README.ja.md
CHANGED
|
@@ -5,7 +5,7 @@ auth.policy-verifier 用の CLI スキャフォルダーです。組み込みテ
|
|
|
5
5
|
## 使い方
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx @o3co/create-auth-policy-verifier <project-name> [--dir <dir-name>]
|
|
8
|
+
npx @o3co/create-auth-policy-verifier <project-name> [--dir <dir-name>] [--no-lockfile]
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
`<project-name>` はスコープ付き npm 名 (`@scope/pkg`) とスコープなしの名前 (`pkg`) のどちらでも指定できます。
|
|
@@ -15,8 +15,8 @@ npx @o3co/create-auth-policy-verifier <project-name> [--dir <dir-name>]
|
|
|
15
15
|
```sh
|
|
16
16
|
npx @o3co/create-auth-policy-verifier my-verifier
|
|
17
17
|
cd my-verifier
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
pnpm install
|
|
19
|
+
pnpm run debug
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
スコープ付きの例(ディレクトリ名はパッケージ部分がデフォルト):
|
|
@@ -24,8 +24,8 @@ npm run debug
|
|
|
24
24
|
```sh
|
|
25
25
|
npx @o3co/create-auth-policy-verifier @my-org/auth.policy-verifier
|
|
26
26
|
cd auth.policy-verifier
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
pnpm install
|
|
28
|
+
pnpm run debug
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
`--dir` でディレクトリ名を明示指定:
|
|
@@ -42,7 +42,25 @@ cd verifier
|
|
|
42
42
|
3. 生成先ディレクトリがすでに存在する場合はエラーを出力して終了する。
|
|
43
43
|
4. `templates/standalone/` を生成先ディレクトリにコピーする(`node_modules/` と `dist/` は除外)。
|
|
44
44
|
5. `package.json` を書き換える: `name` を `<project-name>` をそのまま設定し(スコープを保持)、`private` を削除し、`workspace:*` のバージョン参照を `templates/versions.json` の公開 semver バージョンに置き換える。
|
|
45
|
-
6.
|
|
45
|
+
6. その依存セットを `pnpm-lock.yaml` に解決する(`pnpm install --lockfile-only --ignore-workspace`)。`--no-lockfile` 指定時は省略。
|
|
46
|
+
7. 次のステップの手順を表示する。
|
|
47
|
+
|
|
48
|
+
### 生成される `pnpm-lock.yaml`
|
|
49
|
+
|
|
50
|
+
テンプレートの `Dockerfile` は `pnpm install --frozen-lockfile` でインストール
|
|
51
|
+
するため、生成されたプロジェクトは lockfile が無いとそもそもビルドできません。
|
|
52
|
+
これはテンプレートに同梱できません: 手順 5 が `workspace:*` を公開バージョンに
|
|
53
|
+
置き換えるまで、lockfile が固定すべき依存セットは存在しないからです。そのため
|
|
54
|
+
書き換え後の `package.json` に対してここで一度だけ解決します。**コミットして
|
|
55
|
+
ください** — `docker build` を再現可能にしているのはこれです。
|
|
56
|
+
|
|
57
|
+
手順 6 には `pnpm`(または `corepack`)と到達可能なレジストリが必要ですが、
|
|
58
|
+
スキャフォルダーを実行するマシンにそれらがある保証はありません。したがって
|
|
59
|
+
best-effort です: 失敗しても scaffold 自体は成功し、対処方法を表示します。
|
|
60
|
+
lockfile が無くても生成されたプロジェクトは問題なく使えます — 一度
|
|
61
|
+
`pnpm install` を実行するまでイメージにビルドできないだけです。手順そのものを
|
|
62
|
+
省略するには `--no-lockfile` を指定します(オフラインでの scaffold や、
|
|
63
|
+
後段でどのみちインストールするパイプライン向け)。
|
|
46
64
|
|
|
47
65
|
## バリデーションルール
|
|
48
66
|
|
|
@@ -72,6 +90,7 @@ cd verifier
|
|
|
72
90
|
├── docker-compose.yml
|
|
73
91
|
├── docker-compose.test.yml
|
|
74
92
|
├── package.json
|
|
93
|
+
├── pnpm-lock.yaml # scaffold 時に生成 — コミットすること
|
|
75
94
|
└── tsconfig.json
|
|
76
95
|
```
|
|
77
96
|
|
|
@@ -86,7 +105,8 @@ import { scaffold, main } from "@o3co/create-auth-policy-verifier";
|
|
|
86
105
|
| エクスポート | シグネチャ | 説明 |
|
|
87
106
|
|---|---|---|
|
|
88
107
|
| `scaffold` | `(targetDir: string, projectName: string): void` | テンプレートをコピーして `package.json` を書き換える |
|
|
89
|
-
| `
|
|
108
|
+
| `generateLockfile` | `(targetDir: string): LockfileResult` | scaffold 済みディレクトリで `pnpm-lock.yaml` を解決する。失敗は throw せず結果として返す |
|
|
109
|
+
| `main` | `(): void` | CLI エントリポイント — `process.argv` を解析し `scaffold` → `generateLockfile` を呼び出す |
|
|
90
110
|
|
|
91
111
|
## 関連
|
|
92
112
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ CLI scaffolder for auth.policy-verifier. Generates a new standalone server proje
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx @o3co/create-auth-policy-verifier <project-name> [--dir <dir-name>]
|
|
8
|
+
npx @o3co/create-auth-policy-verifier <project-name> [--dir <dir-name>] [--no-lockfile]
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
`<project-name>` may be either a scoped npm name (`@scope/pkg`) or an unscoped name (`pkg`).
|
|
@@ -15,8 +15,8 @@ Unscoped example:
|
|
|
15
15
|
```sh
|
|
16
16
|
npx @o3co/create-auth-policy-verifier my-verifier
|
|
17
17
|
cd my-verifier
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
pnpm install
|
|
19
|
+
pnpm run debug
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Scoped example (directory defaults to the package portion):
|
|
@@ -24,8 +24,8 @@ Scoped example (directory defaults to the package portion):
|
|
|
24
24
|
```sh
|
|
25
25
|
npx @o3co/create-auth-policy-verifier @my-org/auth.policy-verifier
|
|
26
26
|
cd auth.policy-verifier
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
pnpm install
|
|
28
|
+
pnpm run debug
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Override the directory name with `--dir`:
|
|
@@ -42,7 +42,25 @@ cd verifier
|
|
|
42
42
|
3. Aborts with an error if the target directory already exists.
|
|
43
43
|
4. Copies `templates/standalone/` to the target directory, excluding `node_modules/` and `dist/`.
|
|
44
44
|
5. Rewrites `package.json`: sets `name` to `<project-name>` verbatim (scope-preserving), removes `private`, and replaces `workspace:*` dependency versions with published semver versions from `templates/versions.json`.
|
|
45
|
-
6.
|
|
45
|
+
6. Resolves that dependency set into `pnpm-lock.yaml` (`pnpm install --lockfile-only --ignore-workspace`), unless `--no-lockfile` was passed.
|
|
46
|
+
7. Prints next-step instructions.
|
|
47
|
+
|
|
48
|
+
### The generated `pnpm-lock.yaml`
|
|
49
|
+
|
|
50
|
+
The template's `Dockerfile` installs with `pnpm install --frozen-lockfile`, so
|
|
51
|
+
the generated project needs a lockfile to build at all. It cannot ship with the
|
|
52
|
+
template: until step 5 has replaced every `workspace:*` with a published
|
|
53
|
+
version, the dependency set the lockfile would have to pin does not exist. So
|
|
54
|
+
it is resolved once, here, against the rewritten `package.json`. **Commit it** —
|
|
55
|
+
it is what makes `docker build` reproducible.
|
|
56
|
+
|
|
57
|
+
Step 6 needs `pnpm` (or `corepack`) and a reachable registry, and neither is
|
|
58
|
+
guaranteed on the machine running the scaffolder. It is therefore best-effort:
|
|
59
|
+
on failure the scaffold still succeeds and prints what to do, because the
|
|
60
|
+
generated project is perfectly usable without a lockfile — it just cannot be
|
|
61
|
+
built into an image until `pnpm install` has been run once. Pass
|
|
62
|
+
`--no-lockfile` to skip the step outright (offline scaffolding, or a pipeline
|
|
63
|
+
that installs later anyway).
|
|
46
64
|
|
|
47
65
|
## Validation Rules
|
|
48
66
|
|
|
@@ -72,6 +90,7 @@ The bundled template's `README.md` / `README.ja.md` still carry the upstream tit
|
|
|
72
90
|
├── docker-compose.yml
|
|
73
91
|
├── docker-compose.test.yml
|
|
74
92
|
├── package.json
|
|
93
|
+
├── pnpm-lock.yaml # Generated at scaffold time — commit it
|
|
75
94
|
└── tsconfig.json
|
|
76
95
|
```
|
|
77
96
|
|
|
@@ -86,7 +105,8 @@ import { scaffold, main } from "@o3co/create-auth-policy-verifier";
|
|
|
86
105
|
| Export | Signature | Description |
|
|
87
106
|
|---|---|---|
|
|
88
107
|
| `scaffold` | `(targetDir: string, projectName: string): void` | Copies the template and rewrites `package.json` |
|
|
89
|
-
| `
|
|
108
|
+
| `generateLockfile` | `(targetDir: string): LockfileResult` | Resolves `pnpm-lock.yaml` in an already-scaffolded directory; reports failure rather than throwing |
|
|
109
|
+
| `main` | `(): void` | CLI entry point — parses `process.argv`, calls `scaffold`, then `generateLockfile` |
|
|
90
110
|
|
|
91
111
|
## See Also
|
|
92
112
|
|
package/dist/cli.mjs
CHANGED
|
File without changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether one entry of the template tree is copied into a new project.
|
|
3
|
+
*
|
|
4
|
+
* The exclusion is judged on the path RELATIVE to the template root, which is
|
|
5
|
+
* the whole point: the scaffolder ships inside `node_modules` whenever it is
|
|
6
|
+
* run the documented way (`npm create`, `npx`), so every absolute source path
|
|
7
|
+
* it copies from contains a `node_modules` segment. Matching against the
|
|
8
|
+
* absolute path therefore rejected the entire template and produced an empty
|
|
9
|
+
* project directory — the scaffolder only ever worked from a source checkout.
|
|
10
|
+
*/
|
|
11
|
+
export declare const isTemplateEntryIncluded: (templatesDir: string, source: string) => boolean;
|
|
1
12
|
export declare const isValidProjectName: (name: string) => boolean;
|
|
2
13
|
export declare const isValidDirName: (name: string) => boolean;
|
|
3
14
|
/**
|
|
@@ -9,6 +20,29 @@ export declare const isValidDirName: (name: string) => boolean;
|
|
|
9
20
|
* workspace dependency is not pinned in `versions.json`.
|
|
10
21
|
*/
|
|
11
22
|
export declare const scaffold: (targetDir: string, projectName: string) => void;
|
|
23
|
+
/** Outcome of the scaffold-time lockfile generation. */
|
|
24
|
+
export type LockfileResult = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly command: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Generates `pnpm-lock.yaml` inside an already-scaffolded `targetDir`.
|
|
33
|
+
*
|
|
34
|
+
* This is what makes the template's `pnpm install --frozen-lockfile` build
|
|
35
|
+
* possible: the lockfile cannot be shipped with the template, because the
|
|
36
|
+
* template's dependency set does not exist until `scaffold` has replaced every
|
|
37
|
+
* `workspace:*` with a published version. So it is resolved here, once, against
|
|
38
|
+
* the rewritten `package.json`, and the project commits the result.
|
|
39
|
+
*
|
|
40
|
+
* Best-effort by design: it needs a package manager and a reachable registry,
|
|
41
|
+
* and neither is guaranteed on the machine running the scaffolder. Failure is
|
|
42
|
+
* reported, never thrown — the generated project is perfectly usable without
|
|
43
|
+
* it, the operator just has to run `pnpm install` before `docker build`.
|
|
44
|
+
*/
|
|
45
|
+
export declare const generateLockfile: (targetDir: string) => LockfileResult;
|
|
12
46
|
/**
|
|
13
47
|
* CLI entry point: parses argv, validates project name and optional --dir flag,
|
|
14
48
|
* ensures the target directory does not exist, and scaffolds the template.
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OAIjD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,OAI7C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,aAAa,MAAM,KAAG,IA0CjE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AA2BA;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GAAI,cAAc,MAAM,EAAE,QAAQ,MAAM,KAAG,OAI9E,CAAC;AAMF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OAIjD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,OAI7C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,aAAa,MAAM,KAAG,IA0CjE,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,cAAc,GACvB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyBnD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,KAAG,cA6BpD,CAAC;AAyDF;;;;GAIG;AACH,eAAO,MAAM,IAAI,QAAO,IAgEvB,CAAC"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, resolve, sep } from "node:path";
|
|
5
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const TEMPLATES_DIR = resolve(__dirname, "../templates/standalone");
|
|
@@ -17,6 +18,24 @@ const getPackageVersions = () => {
|
|
|
17
18
|
}
|
|
18
19
|
return {};
|
|
19
20
|
};
|
|
21
|
+
/** Directories that are build output inside the template, never scaffolded. */
|
|
22
|
+
const EXCLUDED_DIRS = new Set(["node_modules", "dist"]);
|
|
23
|
+
/**
|
|
24
|
+
* Decides whether one entry of the template tree is copied into a new project.
|
|
25
|
+
*
|
|
26
|
+
* The exclusion is judged on the path RELATIVE to the template root, which is
|
|
27
|
+
* the whole point: the scaffolder ships inside `node_modules` whenever it is
|
|
28
|
+
* run the documented way (`npm create`, `npx`), so every absolute source path
|
|
29
|
+
* it copies from contains a `node_modules` segment. Matching against the
|
|
30
|
+
* absolute path therefore rejected the entire template and produced an empty
|
|
31
|
+
* project directory — the scaffolder only ever worked from a source checkout.
|
|
32
|
+
*/
|
|
33
|
+
export const isTemplateEntryIncluded = (templatesDir, source) => {
|
|
34
|
+
const rel = relative(templatesDir, source);
|
|
35
|
+
if (rel === "")
|
|
36
|
+
return true;
|
|
37
|
+
return !rel.split(sep).some((segment) => EXCLUDED_DIRS.has(segment));
|
|
38
|
+
};
|
|
20
39
|
const UNSCOPED_NAME_RE = /^[a-z0-9][a-z0-9-._~]*$/;
|
|
21
40
|
const SCOPED_NAME_RE = /^@[a-z0-9][a-z0-9-._~]*\/[a-z0-9][a-z0-9-._~]*$/;
|
|
22
41
|
const MAX_NAME_LEN = 214;
|
|
@@ -47,19 +66,19 @@ export const scaffold = (targetDir, projectName) => {
|
|
|
47
66
|
throw new Error(`Template directory not found at ${TEMPLATES_DIR}. If developing locally, run the prebuild script first.`);
|
|
48
67
|
}
|
|
49
68
|
// Copy template to target
|
|
50
|
-
const EXCLUDED_DIRS = new Set(["node_modules", "dist"]);
|
|
51
69
|
cpSync(TEMPLATES_DIR, targetDir, {
|
|
52
70
|
recursive: true,
|
|
53
|
-
filter: (source) =>
|
|
54
|
-
const segments = source.split(sep);
|
|
55
|
-
return !segments.some((s) => EXCLUDED_DIRS.has(s));
|
|
56
|
-
},
|
|
71
|
+
filter: (source) => isTemplateEntryIncluded(TEMPLATES_DIR, source),
|
|
57
72
|
});
|
|
58
73
|
// Rewrite package.json
|
|
59
74
|
const pkgPath = resolve(targetDir, "package.json");
|
|
60
75
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
61
76
|
pkg.name = projectName;
|
|
62
|
-
|
|
77
|
+
// `"private": true` is deliberately KEPT (#126 item 4). The scaffold is an
|
|
78
|
+
// authorization service — policy code and config — and deleting the field
|
|
79
|
+
// made an accidental `npm publish` succeed by default. Publishing a
|
|
80
|
+
// scaffolded service is the rare intent; the operator who has it states it
|
|
81
|
+
// by removing the field.
|
|
63
82
|
// Replace all workspace:* references with per-package published versions
|
|
64
83
|
const versions = getPackageVersions();
|
|
65
84
|
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
@@ -78,10 +97,73 @@ export const scaffold = (targetDir, projectName) => {
|
|
|
78
97
|
}
|
|
79
98
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
80
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Arguments that make pnpm resolve the dependency graph and write
|
|
102
|
+
* `pnpm-lock.yaml` without linking `node_modules` or running any lifecycle
|
|
103
|
+
* script. `--ignore-workspace` keeps the new project's lockfile its own even
|
|
104
|
+
* when the target directory happens to sit inside another pnpm workspace.
|
|
105
|
+
*/
|
|
106
|
+
const LOCKFILE_ARGS = ["install", "--lockfile-only", "--ignore-workspace"];
|
|
107
|
+
/**
|
|
108
|
+
* Launchers tried in order. `pnpm` on PATH is the common case; `corepack pnpm`
|
|
109
|
+
* covers a machine that only has Node, and honours the `packageManager` field
|
|
110
|
+
* the template ships.
|
|
111
|
+
*
|
|
112
|
+
* The fallback is narrow on purpose: it applies to `ENOENT` and nothing else,
|
|
113
|
+
* because `ENOENT` is the one failure that means "this binary is not here" and
|
|
114
|
+
* is therefore the only one a different launcher can answer. Any other launch
|
|
115
|
+
* error (`EACCES`, `EPERM`, …) says the binary IS there and could not be run,
|
|
116
|
+
* and a package manager that ran and exited non-zero has already reported that
|
|
117
|
+
* resolution failed — retrying either would hide the real cause behind
|
|
118
|
+
* "package manager missing" and hand the operator the wrong instruction.
|
|
119
|
+
*/
|
|
120
|
+
const LOCKFILE_LAUNCHERS = [["pnpm"], ["corepack", "pnpm"]];
|
|
121
|
+
/**
|
|
122
|
+
* Generates `pnpm-lock.yaml` inside an already-scaffolded `targetDir`.
|
|
123
|
+
*
|
|
124
|
+
* This is what makes the template's `pnpm install --frozen-lockfile` build
|
|
125
|
+
* possible: the lockfile cannot be shipped with the template, because the
|
|
126
|
+
* template's dependency set does not exist until `scaffold` has replaced every
|
|
127
|
+
* `workspace:*` with a published version. So it is resolved here, once, against
|
|
128
|
+
* the rewritten `package.json`, and the project commits the result.
|
|
129
|
+
*
|
|
130
|
+
* Best-effort by design: it needs a package manager and a reachable registry,
|
|
131
|
+
* and neither is guaranteed on the machine running the scaffolder. Failure is
|
|
132
|
+
* reported, never thrown — the generated project is perfectly usable without
|
|
133
|
+
* it, the operator just has to run `pnpm install` before `docker build`.
|
|
134
|
+
*/
|
|
135
|
+
export const generateLockfile = (targetDir) => {
|
|
136
|
+
const attempts = [];
|
|
137
|
+
for (const launcher of LOCKFILE_LAUNCHERS) {
|
|
138
|
+
const [bin, ...prefix] = launcher;
|
|
139
|
+
const args = [...prefix, ...LOCKFILE_ARGS];
|
|
140
|
+
const printable = [bin, ...args].join(" ");
|
|
141
|
+
const result = spawnSync(bin, args, {
|
|
142
|
+
cwd: targetDir,
|
|
143
|
+
stdio: "inherit",
|
|
144
|
+
shell: false,
|
|
145
|
+
env: { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
|
|
146
|
+
});
|
|
147
|
+
if (result.error) {
|
|
148
|
+
attempts.push(`${printable}: ${result.error.message}`);
|
|
149
|
+
// Only "not on PATH" is worth asking a different launcher about.
|
|
150
|
+
if (result.error.code === "ENOENT")
|
|
151
|
+
continue;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
if (result.status === 0)
|
|
155
|
+
return { ok: true, command: printable };
|
|
156
|
+
const how = result.status === null ? `killed by signal ${result.signal}` : `exit code ${result.status}`;
|
|
157
|
+
attempts.push(`${printable}: ${how}`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
return { ok: false, reason: attempts.join("; ") };
|
|
161
|
+
};
|
|
81
162
|
const parseArgs = (args) => {
|
|
82
163
|
const positionals = [];
|
|
83
164
|
let dir;
|
|
84
165
|
let dirSeen = false;
|
|
166
|
+
let lockfile = true;
|
|
85
167
|
for (let i = 0; i < args.length; i++) {
|
|
86
168
|
const a = args[i];
|
|
87
169
|
if (a === "--dir") {
|
|
@@ -99,6 +181,9 @@ const parseArgs = (args) => {
|
|
|
99
181
|
dir = a.slice("--dir=".length);
|
|
100
182
|
dirSeen = true;
|
|
101
183
|
}
|
|
184
|
+
else if (a === "--no-lockfile") {
|
|
185
|
+
lockfile = false;
|
|
186
|
+
}
|
|
102
187
|
else if (a.startsWith("-")) {
|
|
103
188
|
// Treats `--` and any --unknown as an unknown flag.
|
|
104
189
|
throw new Error(`unknown flag: ${a}`);
|
|
@@ -111,7 +196,7 @@ const parseArgs = (args) => {
|
|
|
111
196
|
throw new Error("missing <project-name>");
|
|
112
197
|
if (positionals.length > 1)
|
|
113
198
|
throw new Error("too many positional arguments");
|
|
114
|
-
return { projectName: positionals[0], dir };
|
|
199
|
+
return { projectName: positionals[0], dir, lockfile };
|
|
115
200
|
};
|
|
116
201
|
const deriveDirName = (projectName, dir) => {
|
|
117
202
|
if (dir !== undefined)
|
|
@@ -141,10 +226,10 @@ export const main = () => {
|
|
|
141
226
|
}
|
|
142
227
|
catch (e) {
|
|
143
228
|
console.error(`Error: ${e.message}`);
|
|
144
|
-
console.error("Usage: @o3co/create-auth-policy-verifier <project-name> [--dir <dir-name>]");
|
|
229
|
+
console.error("Usage: @o3co/create-auth-policy-verifier <project-name> [--dir <dir-name>] [--no-lockfile]");
|
|
145
230
|
process.exit(1);
|
|
146
231
|
}
|
|
147
|
-
const { projectName, dir } = parsed;
|
|
232
|
+
const { projectName, dir, lockfile } = parsed;
|
|
148
233
|
if (!isValidProjectName(projectName)) {
|
|
149
234
|
console.error("Error: <project-name> must be a valid npm package name (scoped like @scope/pkg, or unscoped; max 214 chars; no backslashes; no extra '/' beyond the single scope separator).");
|
|
150
235
|
process.exit(1);
|
|
@@ -161,9 +246,22 @@ export const main = () => {
|
|
|
161
246
|
}
|
|
162
247
|
console.log(`Creating ${projectName}...`);
|
|
163
248
|
scaffold(targetDir, projectName);
|
|
249
|
+
let lockfileGenerated = false;
|
|
250
|
+
if (lockfile) {
|
|
251
|
+
console.log("\nResolving dependencies into pnpm-lock.yaml...");
|
|
252
|
+
const result = generateLockfile(targetDir);
|
|
253
|
+
lockfileGenerated = result.ok;
|
|
254
|
+
if (!result.ok) {
|
|
255
|
+
console.error(`\nWarning: could not generate pnpm-lock.yaml (${result.reason}).`);
|
|
256
|
+
console.error(`Run 'pnpm install' in ${dirName} and commit pnpm-lock.yaml: the Dockerfile installs with --frozen-lockfile and will not build without it.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
164
259
|
console.log(`\nDone! Created ${projectName} at ${targetDir}`);
|
|
260
|
+
if (lockfileGenerated) {
|
|
261
|
+
console.log("\nCommit pnpm-lock.yaml along with the rest: it is what makes 'docker build' reproducible.");
|
|
262
|
+
}
|
|
165
263
|
console.log(`\nNext steps:`);
|
|
166
264
|
console.log(` cd ${dirName}`);
|
|
167
|
-
console.log("
|
|
168
|
-
console.log("
|
|
265
|
+
console.log(" pnpm install");
|
|
266
|
+
console.log(" pnpm run debug");
|
|
169
267
|
};
|
package/package.json
CHANGED
|
@@ -1,31 +1,41 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
"name": "@o3co/create-auth-policy-verifier",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Scaffold a new o3co/auth.policy-verifier project from the standalone template",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/o3co/auth.policy-verifier.git",
|
|
10
|
+
"directory": "create-app"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22.0.0"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.mjs",
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"import": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"create-auth-policy-verifier": "./dist/cli.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"templates"
|
|
29
|
+
],
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.4.0",
|
|
32
|
+
"vitest": "^4.1.11"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
37
|
+
"prebuild": "node scripts/copy-templates.mjs",
|
|
38
|
+
"pretest": "node scripts/copy-templates.mjs",
|
|
39
|
+
"test": "vitest run"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
# The base image is pinned by digest, not just by tag: `node:24-alpine` is a
|
|
2
|
+
# moving pointer, so the same source produced a different image on every
|
|
3
|
+
# rebuild. The digest is the multi-arch index, so amd64 and arm64 both resolve
|
|
4
|
+
# from it. Dependabot's `docker` ecosystem keeps the pair in step — bump the
|
|
5
|
+
# tag and the digest together, never the tag alone.
|
|
6
|
+
FROM node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS node-base
|
|
2
7
|
|
|
3
8
|
ENV HOME=/home/node
|
|
4
9
|
|
|
10
|
+
# corepack is pinned too. `npm install -g corepack --force` resolved whatever
|
|
11
|
+
# was latest at build time, which is a mutable dependency of every build. The
|
|
12
|
+
# pnpm version itself comes from the `packageManager` field in package.json,
|
|
13
|
+
# which corepack reads.
|
|
5
14
|
RUN apk add --no-cache tini \
|
|
6
|
-
&& npm install -g corepack --force \
|
|
15
|
+
&& npm install -g corepack@0.35.0 --force \
|
|
7
16
|
&& corepack enable
|
|
8
17
|
|
|
9
18
|
WORKDIR /home/node
|
|
@@ -11,34 +20,89 @@ WORKDIR /home/node
|
|
|
11
20
|
#############################################
|
|
12
21
|
FROM node-base AS deps
|
|
13
22
|
|
|
14
|
-
|
|
23
|
+
# pnpm-lock.yaml is a required build input: `create-auth-policy-verifier`
|
|
24
|
+
# generates it at scaffold time and the project commits it. Without it this
|
|
25
|
+
# COPY fails, which is the intended outcome — a build that silently re-resolves
|
|
26
|
+
# every dependency range is the thing this file stopped doing.
|
|
27
|
+
COPY --chown=node:node package.json pnpm-lock.yaml ./
|
|
15
28
|
|
|
16
|
-
|
|
29
|
+
USER node
|
|
30
|
+
|
|
31
|
+
# --frozen-lockfile: install exactly what the lockfile pins, and fail if
|
|
32
|
+
# package.json and the lockfile disagree, rather than quietly rewriting it.
|
|
33
|
+
RUN pnpm install --frozen-lockfile
|
|
17
34
|
|
|
18
35
|
#############################################
|
|
19
36
|
FROM deps AS builder
|
|
20
37
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
# USER node inherited from deps stage; repeated here to make the builder
|
|
39
|
+
# hardening invariant obvious in Dockerfile reviews.
|
|
40
|
+
USER node
|
|
41
|
+
|
|
42
|
+
# tsconfig*.json, not tsconfig.json: the template also ships
|
|
43
|
+
# tsconfig.typecheck.json, which is what `pnpm run typecheck` reads. Copying the
|
|
44
|
+
# whole set keeps the test and develop stages able to run it.
|
|
45
|
+
COPY --chown=node:node tsconfig*.json ./
|
|
46
|
+
COPY --chown=node:node config/ ./config/
|
|
47
|
+
COPY --chown=node:node src/ ./src/
|
|
24
48
|
|
|
25
49
|
RUN pnpm run build
|
|
26
50
|
|
|
51
|
+
#############################################
|
|
52
|
+
# Dependencies are resolved once, in `deps`. This stage drops the dev half of
|
|
53
|
+
# that same tree instead of running a second `pnpm install --prod` in the
|
|
54
|
+
# runtime stage: a second install is a second resolution, and therefore a
|
|
55
|
+
# second chance for the runtime image to hold something the build never saw.
|
|
56
|
+
FROM deps AS prod-deps
|
|
57
|
+
|
|
58
|
+
USER node
|
|
59
|
+
|
|
60
|
+
RUN pnpm prune --prod
|
|
61
|
+
|
|
27
62
|
#############################################
|
|
28
63
|
FROM node-base AS runtime
|
|
29
64
|
|
|
30
65
|
ENV NODE_ENV=production
|
|
31
66
|
|
|
32
67
|
COPY --from=deps /home/node/package.json /home/node/pnpm-lock.yaml ./
|
|
33
|
-
|
|
34
|
-
RUN pnpm install --prod \
|
|
35
|
-
&& pnpm store prune
|
|
36
|
-
|
|
68
|
+
COPY --from=prod-deps /home/node/node_modules/ ./node_modules/
|
|
37
69
|
COPY --from=builder /home/node/dist/ ./dist/
|
|
38
70
|
COPY --from=builder /home/node/config/ ./config/
|
|
39
71
|
|
|
40
72
|
USER node
|
|
41
73
|
|
|
74
|
+
ENV HTTP_PORT=3000
|
|
75
|
+
EXPOSE ${HTTP_PORT}
|
|
76
|
+
|
|
77
|
+
# Probes the container's own routable address, NOT 127.0.0.1 — deliberately.
|
|
78
|
+
#
|
|
79
|
+
# `application.conf` binds loopback by default (#108), because /verify answers
|
|
80
|
+
# with an authorization decision and a reachable port is a decision oracle. A
|
|
81
|
+
# container that keeps that default is reachable from nothing: the published
|
|
82
|
+
# port never connects. A probe against 127.0.0.1 passes anyway — inside the
|
|
83
|
+
# container loopback is exactly where the server is listening — so it would
|
|
84
|
+
# report "healthy" for a container no caller can reach. That is the one failure
|
|
85
|
+
# this check exists to catch, so it asks the same question a caller does:
|
|
86
|
+
# "does the port answer at the address the container is reachable at?"
|
|
87
|
+
#
|
|
88
|
+
# `hostname -i` yields the container's IP; a v6 literal is bracketed for the
|
|
89
|
+
# URL. `${HTTP_PATH_PREFIX}` keeps the probe on the mount point the server
|
|
90
|
+
# actually used (`http.pathPrefix`), and `${HTTP_PORT}` keeps it on the
|
|
91
|
+
# operator's port — the config reads `${?HTTP_PORT}`, so app.listen() and the
|
|
92
|
+
# probe stay in sync. Shell form, so both expand at probe time.
|
|
93
|
+
#
|
|
94
|
+
# `GET /healthcheck` is never gated by `http.callerAuth`, so setting
|
|
95
|
+
# HTTP_CALLER_AUTH_TOKEN does not require a credential here.
|
|
96
|
+
#
|
|
97
|
+
# The deployment where loopback is genuinely correct — a sidecar sharing a
|
|
98
|
+
# network namespace with its caller — is the one shape this check is wrong for.
|
|
99
|
+
# That deployment overrides `healthcheck:` per service, which keeps the
|
|
100
|
+
# restart-driving signal honest for everyone else.
|
|
101
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
102
|
+
CMD addr="$(hostname -i | cut -d' ' -f1)"; \
|
|
103
|
+
case "$addr" in *:*) addr="[$addr]" ;; esac; \
|
|
104
|
+
wget -q -O /dev/null "http://${addr}:${HTTP_PORT}${HTTP_PATH_PREFIX%/}/healthcheck" || exit 1
|
|
105
|
+
|
|
42
106
|
ENTRYPOINT ["tini", "--"]
|
|
43
107
|
CMD ["node", "dist/main.mjs"]
|
|
44
108
|
|