@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 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
- npm install
19
- npm run debug
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
- npm install
28
- npm run debug
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
- | `main` | `(): void` | CLI エントリポイント — `process.argv` を解析して `scaffold` を呼び出す |
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
- npm install
19
- npm run debug
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
- npm install
28
- npm run debug
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. Prints next-step instructions.
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
- | `main` | `(): void` | CLI entry point — parses `process.argv` and calls `scaffold` |
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.
@@ -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;AAqDF;;;;GAIG;AACH,eAAO,MAAM,IAAI,QAAO,IA4CvB,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
- delete pkg.private;
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(" npm install");
168
- console.log(" npm run debug");
265
+ console.log(" pnpm install");
266
+ console.log(" pnpm run debug");
169
267
  };
package/package.json CHANGED
@@ -1,31 +1,41 @@
1
1
  {
2
- "name": "@o3co/create-auth-policy-verifier",
3
- "version": "0.3.1",
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
- "bin": {
16
- "create-auth-policy-verifier": "./dist/cli.mjs"
17
- },
18
- "files": [
19
- "dist",
20
- "templates"
21
- ],
22
- "scripts": {
23
- "build": "tsc",
24
- "prebuild": "node scripts/copy-templates.mjs",
25
- "test": "vitest run"
26
- },
27
- "devDependencies": {
28
- "@types/node": "^25.1.0",
29
- "vitest": "^4.1.4"
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
- FROM node:24-alpine AS node-base
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
- COPY package.json ./
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
- RUN pnpm install
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
- COPY tsconfig.json ./
22
- COPY config/ ./config/
23
- COPY src/ ./src/
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