@auspex-ai/next 0.0.0-stage → 1.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.
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { promises } from "node:fs";
4
+ import path from "node:path";
5
+ //#region ../build-names/src/element-ref.ts
6
+ /** sha256(ビルド鍵)の先頭 16 文字(16 進)。秘密ではなく(先頭 64 bit から鍵は戻せない)、キャッシュやオプションに残ってよい */
7
+ function keyIdOf(key) {
8
+ return createHash("sha256").update(key, "utf8").digest("hex").slice(0, 16);
9
+ }
10
+ //#endregion
11
+ //#region ../build-names/src/host.ts
12
+ function isValidHost(host) {
13
+ let u;
14
+ try {
15
+ u = new URL(host);
16
+ } catch {
17
+ return false;
18
+ }
19
+ if (u.username || u.password) return false;
20
+ if (u.protocol === "https:") return true;
21
+ return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1");
22
+ }
23
+ /** セルフホスト版で /api/names を受ける internal の面の URL。あれば通常の host より優先する(ビルド時だけ読む) */
24
+ const INTERNAL_HOST_ENV = "AUSPEX_INTERNAL_HOST";
25
+ /**
26
+ * 名前の登録の送り先。環境変数 AUSPEX_INTERNAL_HOST が空でなければそれ(不正な値でも通常の host に
27
+ * 落とさない。落とすと 404 になる面へビルド鍵を送るため、呼び出し側の isValidHost で弾く)。無ければ通常の host
28
+ */
29
+ function registrationHost(host, internal = process.env[INTERNAL_HOST_ENV]) {
30
+ return internal?.trim() ? internal.trim() : host;
31
+ }
32
+ //#endregion
33
+ //#region ../build-names/src/iap-token.ts
34
+ const IAP_SERVICE_ACCOUNT_ENV = "AUSPEX_IAP_SERVICE_ACCOUNT";
35
+ const TOKEN_LIFETIME_SECONDS = 3600;
36
+ /** IAP が検証する aud。host の origin 全体(`<origin>/*`) */
37
+ function iapAudience(host) {
38
+ return `${new URL(host).origin}/*`;
39
+ }
40
+ function iapPayload(serviceAccount, host, nowMs = Date.now()) {
41
+ const iat = Math.floor(nowMs / 1e3);
42
+ return JSON.stringify({
43
+ iss: serviceAccount,
44
+ sub: serviceAccount,
45
+ aud: iapAudience(host),
46
+ iat,
47
+ exp: iat + TOKEN_LIFETIME_SECONDS
48
+ });
49
+ }
50
+ /** 既定の署名: google-auth-library(任意の依存)で ADC を読み、IAM Credentials の signJwt を呼ぶ */
51
+ const signJwtWithAdc = async (serviceAccount, payload) => {
52
+ let lib;
53
+ try {
54
+ lib = await import("google-auth-library");
55
+ } catch {
56
+ throw new Error(`${IAP_SERVICE_ACCOUNT_ENV} が設定されているが google-auth-library が入っていない。ビルドの環境に google-auth-library を入れる(例: pnpm add -D google-auth-library)`);
57
+ }
58
+ const res = await new lib.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }).request({
59
+ url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(serviceAccount)}:signJwt`,
60
+ method: "POST",
61
+ data: { payload },
62
+ timeout: 2e4
63
+ });
64
+ if (!res.data.signedJwt) throw new Error("signJwt の応答に signedJwt が無い");
65
+ return res.data.signedJwt;
66
+ };
67
+ /** `Proxy-Authorization` の値(`Bearer <JWT>`)。失敗は投げる(呼び出し側が警告にする) */
68
+ async function iapProxyAuthorization(host, serviceAccount, sign = signJwtWithAdc) {
69
+ return `Bearer ${await sign(serviceAccount, iapPayload(serviceAccount, host))}`;
70
+ }
71
+ //#endregion
72
+ //#region ../build-names/src/log.ts
73
+ const PREFIX = "[auspex]";
74
+ const consoleLogger = {
75
+ info: (m) => console.log(`${PREFIX} ${m}`),
76
+ warn: (m) => console.warn(`${PREFIX} ${m}`)
77
+ };
78
+ const MAX_BUILD_ID = 128;
79
+ const MAX_RETRIES = 3;
80
+ /** ディレクトリのプロジェクトからの相対パス(ローダーが this.rootContext に対して解決する)。POSIX の区切りで書く */
81
+ const fragmentsSubdir = (keyId) => `cache/auspex/names/${keyId}`;
82
+ const KIND_ORDER = {
83
+ element: 0,
84
+ icon: 1,
85
+ label: 2
86
+ };
87
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
88
+ /**
89
+ * 断片を読んで要素名・アイコン名の集合(それぞれ辞書順)にする。壊れた断片は警告して飛ばす。
90
+ * `icons` の無い段階 1 の断片も読める
91
+ */
92
+ async function readFragments(dir, logger) {
93
+ let files;
94
+ try {
95
+ files = (await promises.readdir(dir)).filter((f) => f.endsWith(".json"));
96
+ } catch {
97
+ return {
98
+ names: [],
99
+ icons: [],
100
+ labels: []
101
+ };
102
+ }
103
+ const names = /* @__PURE__ */ new Set();
104
+ const icons = /* @__PURE__ */ new Set();
105
+ const labels = /* @__PURE__ */ new Set();
106
+ for (const f of files) try {
107
+ const parsed = JSON.parse(await promises.readFile(path.join(dir, f), "utf8"));
108
+ if (!Array.isArray(parsed.names)) throw new Error("names が配列ではない");
109
+ if (parsed.icons !== void 0 && !Array.isArray(parsed.icons)) throw new Error("icons が配列ではない");
110
+ if (parsed.labels !== void 0 && !Array.isArray(parsed.labels)) throw new Error("labels が配列ではない");
111
+ for (const n of parsed.names) if (typeof n === "string") names.add(n);
112
+ for (const n of parsed.icons ?? []) if (typeof n === "string") icons.add(n);
113
+ for (const n of parsed.labels ?? []) if (typeof n === "string") labels.add(n);
114
+ } catch (e) {
115
+ logger.warn(`壊れた断片を飛ばした: ${path.join(dir, f)}(${e.message})`);
116
+ }
117
+ return {
118
+ names: [...names].sort(),
119
+ icons: [...icons].sort(),
120
+ labels: [...labels].sort()
121
+ };
122
+ }
123
+ /** build_id: `<distDir>/BUILD_ID`(か --build-id)。無い・128 文字超は自前の id(1 回の実行で 1 度だけ作る) */
124
+ async function resolveBuildId(distDirAbs, override, logger, readFile = true) {
125
+ let raw = override;
126
+ if (raw === void 0 && readFile) try {
127
+ raw = (await promises.readFile(path.join(distDirAbs, "BUILD_ID"), "utf8")).trim();
128
+ } catch {}
129
+ if (raw && raw.length <= MAX_BUILD_ID) return raw;
130
+ const own = `local-${Date.now()}-${randomBytes(4).toString("hex")}`;
131
+ logger.info(raw ? `build_id が長すぎる(${raw.length} 文字。上限 ${MAX_BUILD_ID})ので、自前の id を使う: ${own}` : `build_id(${path.join(distDirAbs, "BUILD_ID")})が無いので、自前の id を使う: ${own}`);
132
+ return own;
133
+ }
134
+ async function post(o, buildId, names, icons, labels) {
135
+ const url = `${o.host.replace(/\/+$/, "")}/api/names`;
136
+ let reason = "";
137
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
138
+ if (attempt > 0) await o.sleep(500 * 2 ** (attempt - 1));
139
+ try {
140
+ const res = await o.fetch(url, {
141
+ method: "POST",
142
+ headers: {
143
+ "content-type": "application/json",
144
+ authorization: `Bearer ${o.key}`,
145
+ ...o.proxyAuthorization ? { "proxy-authorization": o.proxyAuthorization } : {}
146
+ },
147
+ body: JSON.stringify({
148
+ build_id: buildId,
149
+ names,
150
+ ...icons.length > 0 ? { icons } : {},
151
+ ...labels.length > 0 ? { labels } : {}
152
+ }),
153
+ signal: AbortSignal.timeout(2e4),
154
+ redirect: "error"
155
+ });
156
+ if (res.status === 401) return { kind: "unauthorized" };
157
+ if (res.status >= 500) {
158
+ reason = `HTTP ${res.status}`;
159
+ continue;
160
+ }
161
+ if (res.status !== 200) return {
162
+ kind: "rejected",
163
+ status: res.status
164
+ };
165
+ const body = await res.json();
166
+ if (!Array.isArray(body.results)) return {
167
+ kind: "rejected",
168
+ status: res.status
169
+ };
170
+ return {
171
+ kind: "ok",
172
+ results: body.results
173
+ };
174
+ } catch (e) {
175
+ reason = e.message;
176
+ }
177
+ }
178
+ return {
179
+ kind: "failed",
180
+ reason
181
+ };
182
+ }
183
+ /**
184
+ * 断片の名前を登録する。例外は投げない(ネットワーク・5xx・401・400・413 のどれでもビルドを落とさず警告する)
185
+ */
186
+ async function registerNames(options) {
187
+ const logger = options.logger ?? consoleLogger;
188
+ const summary = {
189
+ buildId: null,
190
+ names: 0,
191
+ icons: 0,
192
+ labels: 0,
193
+ registered: 0,
194
+ already: 0,
195
+ dropped: [],
196
+ collisions: [],
197
+ unregistered: []
198
+ };
199
+ try {
200
+ let names;
201
+ let icons;
202
+ let labels;
203
+ let buildId;
204
+ if ("names" in options) {
205
+ names = [...new Set(options.names)].sort();
206
+ icons = [...new Set(options.icons)].sort();
207
+ summary.names = names.length;
208
+ labels = [...new Set(options.labels ?? [])].sort();
209
+ summary.icons = icons.length;
210
+ summary.labels = labels.length;
211
+ if (names.length + icons.length + labels.length === 0) logger.info("登録する名前が無い");
212
+ buildId = await resolveBuildId("", options.buildId, logger, false);
213
+ } else {
214
+ const distDirAbs = path.resolve(options.projectDir ?? process.cwd(), options.distDir);
215
+ const dir = path.join(distDirAbs, fragmentsSubdir(options.keyId));
216
+ ({names, icons, labels} = await readFragments(dir, logger));
217
+ summary.names = names.length;
218
+ summary.icons = icons.length;
219
+ summary.labels = labels.length;
220
+ if (names.length + icons.length + labels.length === 0) logger.info(`登録する名前が無い(断片: ${dir})`);
221
+ buildId = await resolveBuildId(distDirAbs, options.buildId, logger, options.readBuildIdFile ?? true);
222
+ }
223
+ summary.buildId = buildId;
224
+ const iapServiceAccount = options.iapServiceAccount ?? process.env["AUSPEX_IAP_SERVICE_ACCOUNT"];
225
+ const proxyAuthorization = iapServiceAccount ? await iapProxyAuthorization(options.host, iapServiceAccount, options.iapSigner).catch((e) => {
226
+ throw new Error(`IAP 用のトークンを作れなかった: ${e.message}`);
227
+ }) : void 0;
228
+ const o = {
229
+ key: options.key,
230
+ host: options.host,
231
+ proxyAuthorization,
232
+ fetch: options.fetch ?? fetch,
233
+ sleep: options.sleep ?? defaultSleep
234
+ };
235
+ const items = (ns, is, ls) => [
236
+ ...ns.map((name) => ({
237
+ kind: "element",
238
+ name
239
+ })),
240
+ ...is.map((name) => ({
241
+ kind: "icon",
242
+ name
243
+ })),
244
+ ...ls.map((name) => ({
245
+ kind: "label",
246
+ name
247
+ }))
248
+ ];
249
+ const chunks = (list) => {
250
+ const out = [];
251
+ for (let i = 0; i < list.length; i += 100) out.push(list.slice(i, i + 100));
252
+ return out;
253
+ };
254
+ const retry = [];
255
+ const unregistered = /* @__PURE__ */ new Map();
256
+ const markUnregistered = (item, reason) => {
257
+ unregistered.set(`${item.kind}\0${item.name}`, {
258
+ item,
259
+ reason
260
+ });
261
+ };
262
+ const label = (kind) => kind === "icon" ? "アイコン名" : kind === "label" ? "ラベル" : "名前";
263
+ const send = async (list, second) => {
264
+ const all = list.length === 0 ? [[]] : chunks(list);
265
+ for (const [ci, chunk] of all.entries()) {
266
+ const out = await post(o, buildId, chunk.filter((i) => i.kind === "element").map((i) => i.name), chunk.filter((i) => i.kind === "icon").map((i) => i.name), chunk.filter((i) => i.kind === "label").map((i) => i.name));
267
+ if (out.kind === "unauthorized") {
268
+ logger.warn("ビルド鍵が受け付けられなかった(401)。鍵が違うか、再発行された可能性がある。登録せずに続ける(名前なしのクリックとして入る)");
269
+ for (const n of all.slice(ci).flat()) markUnregistered(n, "401");
270
+ return "stop";
271
+ }
272
+ if (out.kind === "rejected") {
273
+ logger.warn(`登録が拒否された(HTTP ${out.status}、${chunk.length} 件のチャンク)。再送しない`);
274
+ for (const n of chunk) markUnregistered(n, `HTTP ${out.status}`);
275
+ continue;
276
+ }
277
+ if (out.kind === "failed") {
278
+ logger.warn(`登録を送れなかった(${MAX_RETRIES} 回送り直した。${out.reason})。ビルドは続ける。次のビルドで再登録される`);
279
+ for (const n of chunk) markUnregistered(n, out.reason);
280
+ continue;
281
+ }
282
+ for (const r of out.results) {
283
+ const kind = r.kind === "icon" ? "icon" : r.kind === "label" ? "label" : "element";
284
+ if (r.status === "registered") summary.registered++;
285
+ else if (r.status === "already") summary.already++;
286
+ else if (r.status === "collision") {
287
+ summary.collisions.push({
288
+ kind,
289
+ name: r.name,
290
+ winner: r.winner
291
+ });
292
+ logger.warn(`${label(kind)} "${r.name}" は登録済みの "${r.winner}" と同じ参照になった。"${r.name}" の${kind === "icon" ? "アイコン" : kind === "label" ? "ラベル" : "要素"}は "${r.winner}" の名前で記録される`);
293
+ } else if (r.status === "retry") {
294
+ if (second) markUnregistered({
295
+ kind,
296
+ name: r.name
297
+ }, "retry");
298
+ else retry.push({
299
+ kind,
300
+ name: r.name
301
+ });
302
+ } else {
303
+ summary.dropped.push({
304
+ kind,
305
+ name: r.name,
306
+ status: r.status
307
+ });
308
+ logger.warn(`${label(kind)} "${r.name}" は登録されなかった(${r.status})`);
309
+ }
310
+ }
311
+ }
312
+ return "go";
313
+ };
314
+ if (await send(items(names, icons, labels), false) === "go" && retry.length > 0) await send(retry.sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind] || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)), true);
315
+ for (const { item, reason } of unregistered.values()) {
316
+ summary.unregistered.push(item);
317
+ if (reason !== "401") logger.warn(`${label(item.kind)} "${item.name}" は未登録(${reason})。次のビルドで再登録される`);
318
+ }
319
+ logger.info(`名前の登録: 要素名 ${summary.names} 件・アイコン名 ${summary.icons} 件・ラベル ${summary.labels} 件中 新規 ${summary.registered}・登録済み ${summary.already}・捨てた ${summary.dropped.length}・衝突 ${summary.collisions.length}・未登録 ${summary.unregistered.length}(build_id ${buildId})`);
320
+ } catch (e) {
321
+ logger.warn(`名前の登録に失敗した(ビルドは続ける): ${e.message}`);
322
+ }
323
+ return summary;
324
+ }
325
+ //#endregion
326
+ //#region src/register-names-cli.ts
327
+ const USAGE = `auspex-register-names [--host <https://...>] [--dist-dir <dir>] [--build-id <id>]
328
+ AUSPEX_BUILD_KEY ビルド鍵(必須)
329
+ AUSPEX_IAP_SERVICE_ACCOUNT IAP の背後の受信口(セルフホスト)に入るサービスアカウント(任意。google-auth-library と Google の認証情報が要る)
330
+ AUSPEX_INTERNAL_HOST セルフホストの internal の面の URL(任意。あれば --host・NEXT_PUBLIC_AUSPEX_HOST より優先。https)
331
+ --host 登録の受け口(既定は NEXT_PUBLIC_AUSPEX_HOST)
332
+ --dist-dir Next の distDir(既定 .next)
333
+ --build-id build_id(既定は <distDir>/BUILD_ID)`;
334
+ function parseArgs(argv) {
335
+ const out = {
336
+ host: void 0,
337
+ distDir: void 0,
338
+ buildId: void 0
339
+ };
340
+ const names = {
341
+ "--host": "host",
342
+ "--dist-dir": "distDir",
343
+ "--build-id": "buildId"
344
+ };
345
+ for (let i = 0; i < argv.length; i++) {
346
+ const [flag, inline] = (argv[i] ?? "").split(/=(.*)/s, 2);
347
+ const field = flag ? names[flag] : void 0;
348
+ if (!field) return null;
349
+ const value = inline ?? argv[++i];
350
+ if (value === void 0) return null;
351
+ out[field] = value;
352
+ }
353
+ return out;
354
+ }
355
+ async function main() {
356
+ const argv = process.argv.slice(2);
357
+ if (argv.includes("--help") || argv.includes("-h")) {
358
+ console.log(USAGE);
359
+ return 0;
360
+ }
361
+ const args = parseArgs(argv);
362
+ if (!args) {
363
+ console.error(USAGE);
364
+ return 2;
365
+ }
366
+ const key = process.env.AUSPEX_BUILD_KEY;
367
+ const host = registrationHost(args.host ?? process.env.NEXT_PUBLIC_AUSPEX_HOST);
368
+ if (!key) {
369
+ consoleLogger.warn("AUSPEX_BUILD_KEY が無いので、要素名を登録しない(CI の秘密に置く。NEXT_PUBLIC_ は付けない)");
370
+ return 0;
371
+ }
372
+ if (!host || !isValidHost(host)) {
373
+ consoleLogger.warn(`登録の受け口 host が無い、または https ではないので、要素名を登録しない(--host か NEXT_PUBLIC_AUSPEX_HOST か AUSPEX_INTERNAL_HOST。${host ?? "未設定"})`);
374
+ return 0;
375
+ }
376
+ await registerNames({
377
+ key,
378
+ host,
379
+ keyId: keyIdOf(key),
380
+ distDir: args.distDir ?? ".next",
381
+ ...args.buildId === void 0 ? {} : { buildId: args.buildId }
382
+ });
383
+ return 0;
384
+ }
385
+ main().then((code) => process.exit(code), (e) => {
386
+ consoleLogger.warn(`要素名の登録に失敗した: ${e.message}`);
387
+ process.exit(0);
388
+ });
389
+ //#endregion
390
+ export {};
@@ -0,0 +1,28 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ Object.defineProperty(exports, "__toESM", {
24
+ enumerable: true,
25
+ get: function() {
26
+ return __toESM;
27
+ }
28
+ });
@@ -0,0 +1,18 @@
1
+ const require_transform = require("./transform-Dc5diC_P.cjs");
2
+ let node_path = require("node:path");
3
+ node_path = require_transform.__toESM(node_path, 1);
4
+ //#region src/icon-name/svg-loader.ts
5
+ function svgNameLoader(source) {
6
+ const options = this.getOptions?.() ?? {};
7
+ const dev = (this.mode ?? process.env.NODE_ENV) === "development";
8
+ const refOf = options.iconNames === false || dev ? null : require_transform.refOfFromEnv(options);
9
+ const out = require_transform.transformSvgFile(source, this.resourcePath, refOf ? (name) => refOf(name, "icon") : void 0);
10
+ if (!out) return source;
11
+ if (out.name && options.fragmentsDir) require_transform.writeFragment(node_path.default.resolve(this.rootContext ?? process.cwd(), options.fragmentsDir), this.resourcePath, {
12
+ names: [],
13
+ icons: [out.name]
14
+ });
15
+ return out.code;
16
+ }
17
+ //#endregion
18
+ module.exports = svgNameLoader;