@bluelamp/cli-token 1.4.0-rc.6 → 1.4.0-rc.8

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/LICENSE ADDED
@@ -0,0 +1,45 @@
1
+ BlueLamp ソフトウェア利用許諾
2
+
3
+ Copyright (c) 2024-2026 株式会社命(MIKOTO, K.K.). All rights reserved.
4
+
5
+ 1. このソフトウェア(プログラム・難読化されたコード・付属の文書と画像を含みます。以下「本ソフトウェア」)の著作権その他の権利は、株式会社命(以下「当社」)に帰属します。本ソフトウェアはオープンソースではありません。
6
+
7
+ 2. 当社は、当社の BlueLamp 利用規約(https://bluelamp.ai/terms 。以下「利用規約」)に同意した方に限り、利用規約に従って本ソフトウェアを自分の機器に入れて使うことを許します。この許しは譲渡・再許諾できません。
8
+
9
+ 3. 次のことは、当社が書面で許した場合を除き、禁止します。
10
+ (1) 本ソフトウェアの全部または一部を複製すること(上記 2 のために必要な範囲で入れて使う場合を除きます)
11
+ (2) 逆コンパイル・逆アセンブル・難読化の解除その他の方法で、本ソフトウェアを解析すること
12
+ (3) 本ソフトウェアを改変し、またはこれを元に別のものを作ること
13
+ (4) 本ソフトウェアを第三者へ配布・再配布・販売・貸与・公開・再許諾すること
14
+
15
+ 4. 本ソフトウェアに含まれる第三者のソフトウェアには、それぞれの利用条件が適用されます。この許諾はそれらの条件を変えるものではありません。
16
+
17
+ 5. 本ソフトウェアは現状のまま提供します。当社の責任は利用規約の定めるところによります。
18
+
19
+ 6. この許諾と利用規約に定めのないことは、利用規約に従います。この許諾と利用規約が食い違うときは、利用規約を優先します。
20
+
21
+ 7. この許諾は、この版から適用します。この版より前に MIT ライセンスの表示を付けて配布された版については、その表示はその版に限って残ります。
22
+
23
+ ---
24
+
25
+ BlueLamp Software License (English translation; the Japanese text above prevails)
26
+
27
+ Copyright (c) 2024-2026 MIKOTO, K.K. All rights reserved.
28
+
29
+ 1. All rights in this software, including its programs, obfuscated code, and accompanying documents and images (the "Software"), belong to MIKOTO, K.K. ("we"). The Software is not open source.
30
+
31
+ 2. We permit only those who have agreed to the BlueLamp Terms of Service (https://bluelamp.ai/terms, the "Terms") to install and use the Software on their own devices in accordance with the Terms. This permission is non-transferable and non-sublicensable.
32
+
33
+ 3. Except with our written permission, you may not:
34
+ (1) copy all or any part of the Software, except as necessary to install and use it under item 2;
35
+ (2) analyze the Software by decompiling, disassembling, deobfuscating, or any other means;
36
+ (3) modify the Software or create derivative works based on it;
37
+ (4) distribute, redistribute, sell, lend, publish, or sublicense the Software to any third party.
38
+
39
+ 4. Third-party software included in the Software is subject to its own license terms, which this license does not change.
40
+
41
+ 5. The Software is provided "as is". Our liability is as set out in the Terms.
42
+
43
+ 6. Matters not covered by this license are governed by the Terms. If this license conflicts with the Terms, the Terms prevail.
44
+
45
+ 7. This license applies from this version onward. For versions distributed before this version with an MIT License notice, that notice remains only for those versions.
package/README.md CHANGED
@@ -31,11 +31,30 @@ const maybe = tryReadCliToken();
31
31
  | 書き手の門の残る穴(1.4.0-rc.5・n=4591) | 古い版の読み手を後から入れた機体では、次の書き(札の延長・最長 1 時間)まで v2 の置き場を読めない=その間その読み手のログインが落ちうる(記録だけ・直さない) |
32
32
  | shell(I/O あり) | `getTokenFilePath` / `readCliTokenSync` / `tryReadCliToken` / `loadPortalToken` / `savePortalToken` / `deletePortalToken` |
33
33
  | 比べて消す(1.4.0-rc.6・n=4640) | `deletePortalTokenIfMatches(token)`(無効と判定した札が今も置き場に在るときだけ消す。入れ直された・読めない・延ばしのロック中は消さない。人の明示ログアウトは今までどおり `deletePortalToken`) |
34
+ | 断られた札の印(1.4.0-rc.7・n=4755) | `noteCliTokenResponse` / `markCliTokenRejected` / `cliTokenRejection` / `cliTokenRejectionOf` / `cliTokenFingerprint` / `parseCliTokenRejection` / `isCliTokenRejectionEffective` / `getCliTokenRejectionPath` / `CLI_TOKEN_REJECTION_TTL_MS` / `CliTokenRejectedError`(印の付いた札は `readCliTokenSync`・`tryReadCliToken`・`readFreshCliToken`・`ensureFreshCliToken` が渡さない。`loadPortalToken` は門を通らない) |
35
+ | 置き場ごとの名義(1.4.0-rc.8・n=5011) | `resolveIdentity` / `musuhiDirFor` / `configureIdentityCwd` / `getIdentityTablePath` / `readIdentityTable` / `parseIdentityTable` / `matchIdentityRow` / `identityMusuhiDir` / `defaultMusuhiDir`(下の節) |
34
36
  | 艦隊の資格の門(1.1.0・n=2607) | `checkFleetGate` / `isAllowedForScope` / `getFleetEntitlementFilePath` / `getFleetGateLogPath` / `formatFleetGateLogLine` / `fleetGateRefusalMessage` / `parseFleetEntitlementCache` / `isFleetEntitlementFresh` |
35
37
  | 交換済み JWT の機体共有キャッシュ(1.2.0・n=2774) | `getExchangedJwt` / `evictExchangedJwt` / `exchangedJwtCacheKey` / `getExchangedJwtCacheDir` / `parseCachedExchangedJwt` / `isCachedExchangedJwtUsable` |
36
38
  | 機体の鍵・登録・札の延長(1.3.0・n=3918) | `startDeviceRegistration` / `pollDeviceRegistration` / `awaitDeviceApproval` / `saveApprovedDevice` / `loadOrCreateDeviceKey` / `readDeviceKey` / `signWithDeviceKey` / `devicePollMessage` / `tokenRefreshMessage` / `readDeviceId` / `saveDeviceId` / `ensureFreshCliToken` / `readFreshCliToken` / `needsRefresh` / `parseExpiresAt` |
37
39
  | 発火の前の CLI の更新(1.4.0・n=4082・`@bluelamp/cli-token/cli-update`) | `updateCliToMin` / `fetchMinCliVersion` / `runNpmBesideNode` / `readPackageVersion` / `describeCliUpdate` / `isOlderThan` / `selfUpdatePackage` / `selfUpdateExecArgs` / `parseSelfUpdateLine` |
38
40
 
41
+ ### 置き場ごとの名義(1.4.0-rc.8・n=5011)
42
+
43
+ 1 台の機体で 2 つ以上の事業を持つとき、置き場(フォルダ)ごとに札の置き場を分ける。対応表は `~/.bluelamp/identities.json` の 1 枚:
44
+
45
+ ```json
46
+ { "version": 1, "rows": [
47
+ { "path": "/Users/me/work/company-a", "identity": "company-a" },
48
+ { "path": "/Users/me/work/company-b", "identity": "company-b" }
49
+ ] }
50
+ ```
51
+
52
+ - 席が立つ置き場(cwd)に一番長く一致した行の名義の札の置き場 `~/.musuhi/identities/<名義>` を `musuhiDir()` が返す。札・pin の宣言・機体の鍵・機体の id・艦隊の資格のキャッシュが全部その置き場になる
53
+ - 表に書くのは置き場と名義の名前だけ(札もテナントも書かない)。テナントはその置き場でログインした札が決める(名義の置き場を作るのは、その置き場で `bluelamp1` を起動してログインするだけ)
54
+ - 表が無い機体・表に当たらない置き場は今どおり既定の `~/.musuhi`。`BLUELAMP_MUSUHI_DIR` は表より強い
55
+ - 表が壊れていたら throw(黙って既定の名義へ落ちない)。名義の名前は `[A-Za-z0-9][A-Za-z0-9._-]{0,63}`
56
+ - cwd が作業の置き場と違うプロセスは `configureIdentityCwd(dir)` で判定の置き場を明示する(プロセスの中だけ・子へは継がない)
57
+
39
58
  ### 発火の前の CLI の更新(`@bluelamp/cli-token/cli-update`・1.4.0)
40
59
 
41
60
  定期便(routine・scheduler)が席を立てる前に、席が使う BlueLamp CLI を最低版(`GET {bluelamp}/cli/min-version`)へ上げる**単一実装**(箱 n=4076 → n=4082)。
@@ -79,6 +98,17 @@ CLI の札を機体に結び、短い期限(1 時間)を裏で延ばす(
79
98
  - `ensureFreshCliToken()`: 残り 15 分を切った札だけ、`~/.musuhi/token-refresh.lock`(O_EXCL・2 分より古いロックは払う)を取れた 1 手が `POST {base}/cli/token/refresh {device_id, ts, nonce, signature}`(`X-CLI-Token`=今の札)を撃つ。署名の文は `bluelamp-token-refresh\n{device_id}\n{ts}\n{nonce}`(ts は epoch 秒・署名は base64)。他の手は最大 5 秒待って置き場を読む。throw しない。断られても置き場を消さない
80
99
  - 読み手は 401 を受けたら `readFreshCliToken()` で読み直して 1 回だけ撃ち直す(普段の要求は今までどおり札だけ・1 回ごとに署名しない)
81
100
 
101
+ ### サーバに断られた札の印(`noteCliTokenResponse`・1.4.0-rc.7)
102
+
103
+ 失効した札・機体の承認が要る札を持つ常駐の手が、断られても撃ち続けないための印(箱 n=4755)。
104
+
105
+ - 常駐の手は応答を受けたら `noteCliTokenResponse(札, status, body)` を呼ぶ。401 の `code: device_registration_required`・`code: invalid_token`(code が無く `message: "Invalid or expired CLI token"` も同じ)なら `~/.musuhi/cli-token-rejected.json`(0600)に `{ fp, reason, at }` を書き、理由を返す。返りが null でなければ**その手の中で撃つのを止める**(撃ち直しの輪を作らない)。`superseded_by_other_device`・403・429・5xx・網の不調には印を付けない
106
+ - `fp` は札の sha256 の先頭 16 桁。⛔ 札そのものは書かない
107
+ - 読み手の門: 印の付いた札は `tryReadCliToken()`=null/`readCliTokenSync()`=`CliTokenRejectedError`(`CliTokenNotFoundError` の子=今の catch がそのまま止まる・文は番号の承認へ誘う)/`readFreshCliToken()`=null/`ensureFreshCliToken()`=網に出ず `{ status: 'rejected', detail: 'marked <reason>' }`。延ばしの口が上の 401 を返したときも印が付く
108
+ - 置き場の札が替われば(番号の承認・`--register-device`・延ばし)指紋が違うので効かない。印は 24 時間で効かなくなる(1 本だけ撃って確かめ、また断られればまた付く=1 日 1 本)
109
+ - ⛔ `loadPortalToken()` は門を通らない(CLI の起動時の確かめ→番号の承認を塞がない)
110
+ - 今の印を見るのは `cliTokenRejection()`(`{ reason, at }` か null)。判定は純関数 `cliTokenRejectionOf`・`isCliTokenRejectionEffective`
111
+
82
112
  ## 制約
83
113
 
84
114
  - **node 標準モジュール以外に依存しない**(VSCode 拡張が webpack で bundle するため)。
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ /**
3
+ * 箱 n=5011: 1 台の機体で、置き場(フォルダ)ごとに BlueLamp の名義を切り替える。
4
+ *
5
+ * 機体に対応表 1 枚(`~/.bluelamp/identities.json`)を置くと、席が立つ置き場(cwd)で名義が決まる。
6
+ * 名義=札の置き場(`~/.musuhi/identities/<名義>`)。テナントはその置き場の札と pin の宣言が決める
7
+ * (表にテナントも札も書かない=表は置き場→名義の名前だけ)。
8
+ *
9
+ * 線:
10
+ * - 表が無い機体・表に当たらない置き場は今どおり(既定の `~/.musuhi`)=会員の今の使い方を変えない
11
+ * - 表が壊れていたら throw(黙って既定の名義へ落ちると、それがこの仕組みが塞ぐ「混ざる」そのもの)
12
+ * - 一番長く一致した行を採る(入れ子の置き場は内側が勝つ)
13
+ * - node 標準モジュール以外に依存しない(index.ts と同じ制約)
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.getIdentityTablePath = getIdentityTablePath;
50
+ exports.defaultMusuhiDir = defaultMusuhiDir;
51
+ exports.identityMusuhiDir = identityMusuhiDir;
52
+ exports.parseIdentityTable = parseIdentityTable;
53
+ exports.matchIdentityRow = matchIdentityRow;
54
+ exports.readIdentityTable = readIdentityTable;
55
+ exports.configureIdentityCwd = configureIdentityCwd;
56
+ exports.resolveIdentity = resolveIdentity;
57
+ exports.musuhiDirFor = musuhiDirFor;
58
+ const fs = __importStar(require("node:fs"));
59
+ const os = __importStar(require("node:os"));
60
+ const path = __importStar(require("node:path"));
61
+ /** 名義の名前に許す字(パスを作るので区切り・`..` を通さない) */
62
+ const IDENTITY_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
63
+ function homeDir() {
64
+ const home = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
65
+ return home || os.homedir();
66
+ }
67
+ /** 対応表のパス。HOME の下に固定(札の置き場に依らない=循環しない) */
68
+ function getIdentityTablePath() {
69
+ return path.join(homeDir(), '.bluelamp', 'identities.json');
70
+ }
71
+ /** 既定の札の置き場(表を見ない) */
72
+ function defaultMusuhiDir() {
73
+ return path.join(homeDir(), '.musuhi');
74
+ }
75
+ /** 名義の札の置き場 */
76
+ function identityMusuhiDir(identity) {
77
+ return path.join(defaultMusuhiDir(), 'identities', identity);
78
+ }
79
+ /** 在る祖先まで realpath で解く(/var → /private/var 等のずれで一致を外さない) */
80
+ function canonical(p) {
81
+ let cur = path.resolve(p);
82
+ const rest = [];
83
+ for (;;) {
84
+ try {
85
+ return path.join(fs.realpathSync.native(cur), ...rest.reverse());
86
+ }
87
+ catch {
88
+ const parent = path.dirname(cur);
89
+ if (parent === cur)
90
+ return path.resolve(p);
91
+ rest.push(path.basename(cur));
92
+ cur = parent;
93
+ }
94
+ }
95
+ }
96
+ function sameOrInside(child, parent) {
97
+ const rel = path.relative(parent, child);
98
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
99
+ }
100
+ /**
101
+ * 表の中身を検める(純関数)。形が違えば throw=黙って既定へ落ちない。
102
+ * 形: `{"version":1,"rows":[{"path":"/絶対パス","identity":"名義"}]}`
103
+ */
104
+ function parseIdentityTable(raw, tablePath = getIdentityTablePath()) {
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(raw);
108
+ }
109
+ catch (e) {
110
+ throw new Error(`${tablePath} is not valid JSON: ${e.message}`);
111
+ }
112
+ const obj = parsed;
113
+ if (!obj || typeof obj !== 'object' || obj.version !== 1 || !Array.isArray(obj.rows)) {
114
+ throw new Error(`${tablePath} must be {"version":1,"rows":[{"path":"<absolute path>","identity":"<name>"}]}`);
115
+ }
116
+ const seen = new Set();
117
+ return obj.rows.map((r, i) => {
118
+ const row = r;
119
+ const p = row?.path;
120
+ const id = row?.identity;
121
+ if (typeof p !== 'string' || !path.isAbsolute(p)) {
122
+ throw new Error(`${tablePath} rows[${i}].path must be an absolute path`);
123
+ }
124
+ if (typeof id !== 'string' || !IDENTITY_NAME.test(id)) {
125
+ throw new Error(`${tablePath} rows[${i}].identity must match ${IDENTITY_NAME.source}`);
126
+ }
127
+ const key = canonical(p);
128
+ if (seen.has(key))
129
+ throw new Error(`${tablePath} rows[${i}].path is listed twice`);
130
+ seen.add(key);
131
+ return { path: p, identity: id };
132
+ });
133
+ }
134
+ /** 一番長く一致した行(純関数・cwd と行の path は正規化済みを渡す) */
135
+ function matchIdentityRow(rows, cwd) {
136
+ let best = null;
137
+ for (const row of rows) {
138
+ if (sameOrInside(cwd, row.path) && (!best || row.path.length > best.path.length))
139
+ best = row;
140
+ }
141
+ return best;
142
+ }
143
+ let cache = null;
144
+ /** 表を読む。無ければ null。壊れていれば throw。mtime と size が同じ間は読み直さない */
145
+ function readIdentityTable(tablePath = getIdentityTablePath()) {
146
+ let st;
147
+ try {
148
+ st = fs.statSync(tablePath);
149
+ }
150
+ catch (e) {
151
+ if (e.code === 'ENOENT') {
152
+ cache = null;
153
+ return null;
154
+ }
155
+ throw e;
156
+ }
157
+ if (cache && cache.tablePath === tablePath && cache.mtimeMs === st.mtimeMs && cache.size === st.size) {
158
+ return cache.rows;
159
+ }
160
+ const rows = parseIdentityTable(fs.readFileSync(tablePath, 'utf8'), tablePath).map((r) => ({
161
+ path: canonical(r.path),
162
+ identity: r.identity,
163
+ }));
164
+ cache = { tablePath, mtimeMs: st.mtimeMs, size: st.size, rows };
165
+ return rows;
166
+ }
167
+ let configuredCwd = null;
168
+ /**
169
+ * 判定に使う置き場を明示する(プロセスの中だけ・子へは継がない)。
170
+ * cwd が置き場と一致しないプロセス(VS Code の拡張ホスト等)が、自分の作業フォルダで名義を決めるための口。
171
+ * null で解除(process.cwd() に戻る)。
172
+ */
173
+ function configureIdentityCwd(dir) {
174
+ configuredCwd = dir ? path.resolve(dir) : null;
175
+ }
176
+ function currentCwd() {
177
+ if (configuredCwd)
178
+ return configuredCwd;
179
+ try {
180
+ return process.cwd();
181
+ }
182
+ catch {
183
+ return null; // cwd が消えた(ENOENT)=表は引けない
184
+ }
185
+ }
186
+ /** この置き場の名義を決める(判定の唯一の実装)。cwd を省けば configureIdentityCwd → process.cwd() */
187
+ function resolveIdentity(cwd) {
188
+ const tablePath = getIdentityTablePath();
189
+ const raw = cwd ?? currentCwd();
190
+ const at = raw ? canonical(raw) : null;
191
+ if (process.env.BLUELAMP_MUSUHI_DIR) {
192
+ return { source: 'env', identity: null, rowPath: null, cwd: at, tablePath };
193
+ }
194
+ const rows = readIdentityTable(tablePath);
195
+ const row = rows && at ? matchIdentityRow(rows, at) : null;
196
+ if (!row)
197
+ return { source: 'default', identity: null, rowPath: null, cwd: at, tablePath };
198
+ return { source: 'table', identity: row.identity, rowPath: row.path, cwd: at, tablePath };
199
+ }
200
+ /** 決めた名義の札の置き場 */
201
+ function musuhiDirFor(resolved) {
202
+ if (resolved.source === 'env')
203
+ return path.resolve(process.env.BLUELAMP_MUSUHI_DIR);
204
+ if (resolved.source === 'table')
205
+ return identityMusuhiDir(resolved.identity);
206
+ return defaultMusuhiDir();
207
+ }
208
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"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"}
package/dist/cjs/index.js CHANGED
@@ -46,7 +46,7 @@ var __importStar = (this && this.__importStar) || (function () {
46
46
  };
47
47
  })();
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
- exports.TOKEN_REFRESH_LOCK_WAIT_MS = exports.TOKEN_REFRESH_LOCK_STALE_MS = exports.CLI_TOKEN_REFRESH_BEFORE_MS = exports.DeviceKeyError = exports.EXCHANGED_JWT_LOCK_STALE_MS = exports.EXCHANGED_JWT_LOCK_WAIT_MS = exports.EXCHANGED_JWT_REFRESH_BUFFER_MS = exports.FLEET_GATE_LOG_MAX_BYTES = exports.FLEET_ENTITLEMENT_MAX_TTL_SEC = exports.MEMBER_SCOPE_ALLOWED_REASONS = exports.V2_MIGRATE_CHECK_INTERVAL_MS = exports.CliTokenNotFoundError = exports.V2_READER_MIN = exports.versionAtLeast = exports.tokenReaderRoots = exports.scanTokenReaders = void 0;
49
+ exports.TOKEN_REFRESH_LOCK_WAIT_MS = exports.TOKEN_REFRESH_LOCK_STALE_MS = exports.CLI_TOKEN_REFRESH_BEFORE_MS = exports.DeviceKeyError = exports.EXCHANGED_JWT_LOCK_STALE_MS = exports.EXCHANGED_JWT_LOCK_WAIT_MS = exports.EXCHANGED_JWT_REFRESH_BUFFER_MS = exports.FLEET_GATE_LOG_MAX_BYTES = exports.FLEET_ENTITLEMENT_MAX_TTL_SEC = exports.MEMBER_SCOPE_ALLOWED_REASONS = exports.V2_MIGRATE_CHECK_INTERVAL_MS = exports.CLI_TOKEN_REJECTION_TTL_MS = exports.CliTokenRejectedError = exports.CliTokenNotFoundError = exports.V2_READER_MIN = exports.versionAtLeast = exports.tokenReaderRoots = exports.scanTokenReaders = exports.resolveIdentity = exports.readIdentityTable = exports.parseIdentityTable = exports.musuhiDirFor = exports.matchIdentityRow = exports.identityMusuhiDir = exports.getIdentityTablePath = exports.defaultMusuhiDir = exports.configureIdentityCwd = void 0;
50
50
  exports.musuhiDir = musuhiDir;
51
51
  exports.assertMusuhiWritable = assertMusuhiWritable;
52
52
  exports.getTokenFilePath = getTokenFilePath;
@@ -59,6 +59,14 @@ exports.extractToken = extractToken;
59
59
  exports.validateTokenFormat = validateTokenFormat;
60
60
  exports.readCliTokenSync = readCliTokenSync;
61
61
  exports.tryReadCliToken = tryReadCliToken;
62
+ exports.getCliTokenRejectionPath = getCliTokenRejectionPath;
63
+ exports.cliTokenFingerprint = cliTokenFingerprint;
64
+ exports.cliTokenRejectionOf = cliTokenRejectionOf;
65
+ exports.parseCliTokenRejection = parseCliTokenRejection;
66
+ exports.isCliTokenRejectionEffective = isCliTokenRejectionEffective;
67
+ exports.markCliTokenRejected = markCliTokenRejected;
68
+ exports.noteCliTokenResponse = noteCliTokenResponse;
69
+ exports.cliTokenRejection = cliTokenRejection;
62
70
  exports.loadPortalToken = loadPortalToken;
63
71
  exports.savePortalToken = savePortalToken;
64
72
  exports.configureTokenReaderScan = configureTokenReaderScan;
@@ -107,6 +115,17 @@ const fs = __importStar(require("node:fs"));
107
115
  const os = __importStar(require("node:os"));
108
116
  const path = __importStar(require("node:path"));
109
117
  const token_readers_js_1 = require("./token-readers.js");
118
+ const identity_js_1 = require("./identity.js");
119
+ var identity_js_2 = require("./identity.js");
120
+ Object.defineProperty(exports, "configureIdentityCwd", { enumerable: true, get: function () { return identity_js_2.configureIdentityCwd; } });
121
+ Object.defineProperty(exports, "defaultMusuhiDir", { enumerable: true, get: function () { return identity_js_2.defaultMusuhiDir; } });
122
+ Object.defineProperty(exports, "getIdentityTablePath", { enumerable: true, get: function () { return identity_js_2.getIdentityTablePath; } });
123
+ Object.defineProperty(exports, "identityMusuhiDir", { enumerable: true, get: function () { return identity_js_2.identityMusuhiDir; } });
124
+ Object.defineProperty(exports, "matchIdentityRow", { enumerable: true, get: function () { return identity_js_2.matchIdentityRow; } });
125
+ Object.defineProperty(exports, "musuhiDirFor", { enumerable: true, get: function () { return identity_js_2.musuhiDirFor; } });
126
+ Object.defineProperty(exports, "parseIdentityTable", { enumerable: true, get: function () { return identity_js_2.parseIdentityTable; } });
127
+ Object.defineProperty(exports, "readIdentityTable", { enumerable: true, get: function () { return identity_js_2.readIdentityTable; } });
128
+ Object.defineProperty(exports, "resolveIdentity", { enumerable: true, get: function () { return identity_js_2.resolveIdentity; } });
110
129
  var token_readers_js_2 = require("./token-readers.js");
111
130
  Object.defineProperty(exports, "scanTokenReaders", { enumerable: true, get: function () { return token_readers_js_2.scanTokenReaders; } });
112
131
  Object.defineProperty(exports, "tokenReaderRoots", { enumerable: true, get: function () { return token_readers_js_2.tokenReaderRoots; } });
@@ -121,16 +140,14 @@ const KEY_LENGTH = 32;
121
140
  const SECRET = 'musuhi-vscode-portal-token-key-2025';
122
141
  /**
123
142
  * 箱 n=4109: `~/.musuhi` の置き場はここ 1 本から出す(札・pin の宣言・機体の鍵・機体の id・延ばしのロック)。
124
- * 順=env `BLUELAMP_MUSUHI_DIR` → env の HOME(Windows は USERPROFILE)の下 → `os.homedir()` の下。
143
+ * 順=env `BLUELAMP_MUSUHI_DIR` → 箱 n=5011 の対応表(`~/.bluelamp/identities.json`)で置き場に当たった名義の
144
+ * `~/.musuhi/identities/<名義>` → env の HOME(Windows は USERPROFILE)の下 → `os.homedir()` の下。
125
145
  * 🔑 env を JS から読む=jest が写しの env に HOME を入れても効く(`os.homedir()` は本物の環境を読むので、
126
146
  * jest の中で HOME を替えても本物の ~/.musuhi に書いた・2026-10-30 13:58)。呼び出し時に解決する。
147
+ * 🔑 表が無い機体・表に当たらない置き場は今どおり(既定)。表が壊れていれば throw(黙って既定へ落ちない)。
127
148
  */
128
149
  function musuhiDir() {
129
- const override = process.env.BLUELAMP_MUSUHI_DIR;
130
- if (override)
131
- return path.resolve(override);
132
- const home = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
133
- return path.join(home || os.homedir(), '.musuhi');
150
+ return (0, identity_js_1.musuhiDirFor)((0, identity_js_1.resolveIdentity)());
134
151
  }
135
152
  /** 試験の走り手の中か(jest・vitest・node --test・Playwright・NODE_ENV=test) */
136
153
  function inTestRunner() {
@@ -211,6 +228,26 @@ class CliTokenNotFoundError extends Error {
211
228
  }
212
229
  }
213
230
  exports.CliTokenNotFoundError = CliTokenNotFoundError;
231
+ /**
232
+ * 箱 n=4755: 置き場の札にサーバに断られた印が付いている。CliTokenNotFoundError の子=今の catch がそのまま
233
+ * 「札が無い」として止まる。文は番号の承認へ誘う。
234
+ * 🚨 親と同じく**ブランド名と CLI コマンド名を持たせない**(白ラベル変種が同じ実体を読む)。`--register-device` は
235
+ * どの変種の CLI にも在る引数なので、コマンド名を付けずに書く。
236
+ */
237
+ class CliTokenRejectedError extends CliTokenNotFoundError {
238
+ reason;
239
+ rejectedAt;
240
+ constructor(reason, rejectedAt) {
241
+ super();
242
+ this.message =
243
+ `この機体の札はサーバに断られました(${reason})。` +
244
+ 'ご利用の CLI を起動して表示される番号を Web で承認してください(CLI に `--register-device` を付けて起動しても同じです)';
245
+ this.name = 'CliTokenRejectedError';
246
+ this.reason = reason;
247
+ this.rejectedAt = rejectedAt;
248
+ }
249
+ }
250
+ exports.CliTokenRejectedError = CliTokenRejectedError;
214
251
  // 入力が定数なので 1 回だけ導く(scrypt は 1 回数十 ms。札を読むたびに払わない)
215
252
  let derivedKeyCache = null;
216
253
  function derivedKey() {
@@ -301,8 +338,17 @@ function validateTokenFormat(token) {
301
338
  /**
302
339
  * トークンを読む。読めない場合は CliTokenNotFoundError を throw する。
303
340
  * 失敗の詳細はメッセージに載るが、**復号結果そのものは決して載せない**。
341
+ * 箱 n=4755: 置き場の札にサーバに断られた印が付いていれば CliTokenRejectedError(CliTokenNotFoundError の子)。
304
342
  */
305
343
  function readCliTokenSync() {
344
+ const token = readStoredTokenSync();
345
+ const mark = rejectionFor(token, Date.now());
346
+ if (mark)
347
+ throw new CliTokenRejectedError(mark.reason, mark.at);
348
+ return token;
349
+ }
350
+ /** 置き場の札を読む(印の門を通さない)。読めなければ CliTokenNotFoundError */
351
+ function readStoredTokenSync() {
306
352
  const filePath = getTokenFilePath();
307
353
  if (!fs.existsSync(filePath)) {
308
354
  throw new CliTokenNotFoundError(`${filePath} not found`);
@@ -338,6 +384,110 @@ function tryReadCliToken() {
338
384
  return null;
339
385
  }
340
386
  }
387
+ /** 印が効いている長さ。過ぎたら 1 本だけ撃って確かめる */
388
+ exports.CLI_TOKEN_REJECTION_TTL_MS = 24 * 60 * 60_000;
389
+ /** サーバが失効した札に返す文(code を持たない古い応答) */
390
+ const INVALID_TOKEN_MESSAGE = 'Invalid or expired CLI token';
391
+ /** 印の置き場。テストで HOME・BLUELAMP_MUSUHI_DIR を差し替えられるよう呼び出し時に解決する */
392
+ function getCliTokenRejectionPath() {
393
+ return path.join(musuhiDir(), 'cli-token-rejected.json');
394
+ }
395
+ /** 札の指紋(純関数)。印に書くのはこれだけ */
396
+ function cliTokenFingerprint(token) {
397
+ return (0, node_crypto_1.createHash)('sha256').update(token).digest('hex').slice(0, 16);
398
+ }
399
+ /**
400
+ * 応答が「札を捨てるべき断り」か(純関数)。401 の device_registration_required・invalid_token だけ。
401
+ * それ以外(superseded_by_other_device・ログインを止めた会員・403・429・5xx)は null。
402
+ */
403
+ function cliTokenRejectionOf(status, body) {
404
+ if (status !== 401 || !body || typeof body !== 'object')
405
+ return null;
406
+ const b = body;
407
+ if (b.code === 'device_registration_required')
408
+ return 'device_registration_required';
409
+ if (b.code === 'invalid_token')
410
+ return 'invalid_token';
411
+ if (b.code === undefined || b.code === null) {
412
+ if (b.message === INVALID_TOKEN_MESSAGE)
413
+ return 'invalid_token';
414
+ }
415
+ return null;
416
+ }
417
+ /** 印の中身を検める(純関数)。形が違えば null=印が無いのと同じ */
418
+ function parseCliTokenRejection(raw) {
419
+ let v;
420
+ try {
421
+ v = JSON.parse(raw);
422
+ }
423
+ catch {
424
+ return null;
425
+ }
426
+ const o = v;
427
+ if (!o || typeof o.fp !== 'string' || !/^[0-9a-f]{16}$/.test(o.fp))
428
+ return null;
429
+ if (o.reason !== 'device_registration_required' && o.reason !== 'invalid_token')
430
+ return null;
431
+ if (typeof o.at !== 'number' || !Number.isFinite(o.at))
432
+ return null;
433
+ return { fp: o.fp, reason: o.reason, at: o.at };
434
+ }
435
+ /**
436
+ * 印がこの札に今も効いているか(純関数)。指紋が同じで、付けてから 24 時間未満のときだけ。
437
+ * 未来の時刻(時計の巻き戻り)は効かないと読む(1 本撃って、断られればまた付く)。
438
+ */
439
+ function isCliTokenRejectionEffective(mark, token, nowMs) {
440
+ if (mark.fp !== cliTokenFingerprint(token))
441
+ return false;
442
+ const age = nowMs - mark.at;
443
+ return age >= 0 && age < exports.CLI_TOKEN_REJECTION_TTL_MS;
444
+ }
445
+ function readRejectionMark() {
446
+ try {
447
+ return parseCliTokenRejection(fs.readFileSync(getCliTokenRejectionPath(), 'utf8'));
448
+ }
449
+ catch {
450
+ return null;
451
+ }
452
+ }
453
+ function rejectionFor(token, nowMs) {
454
+ const mark = readRejectionMark();
455
+ return mark && isCliTokenRejectionEffective(mark, token, nowMs) ? mark : null;
456
+ }
457
+ /**
458
+ * 札に「サーバに断られた」印を付ける(指紋・理由・時刻だけ・0600・一時ファイル→rename)。throw しない。
459
+ */
460
+ function markCliTokenRejected(token, reason, options = {}) {
461
+ try {
462
+ const mark = { fp: cliTokenFingerprint(token), reason, at: (options.now ?? Date.now)() };
463
+ writePrivateFileAtomic(getCliTokenRejectionPath(), JSON.stringify(mark) + '\n');
464
+ }
465
+ catch {
466
+ // 印が書けなくても呼び手の本線は落とさない(次の 401 でまた試す)
467
+ }
468
+ }
469
+ /**
470
+ * 常駐の手が応答を受けたら渡す口。断り(cliTokenRejectionOf)なら印を付けて理由を返す。それ以外は null。throw しない。
471
+ * 印が付いたら、その手の中で撃つのを止めること(次の周は読み手の門で止まる)。
472
+ */
473
+ function noteCliTokenResponse(token, status, body, options = {}) {
474
+ const reason = cliTokenRejectionOf(status, body);
475
+ if (reason && token)
476
+ markCliTokenRejected(token, reason, options);
477
+ return reason;
478
+ }
479
+ /** 今の置き場の札に効いている印。無い・札が読めない=null。throw しない */
480
+ function cliTokenRejection(options = {}) {
481
+ let token;
482
+ try {
483
+ token = readStoredTokenSync();
484
+ }
485
+ catch {
486
+ return null;
487
+ }
488
+ const mark = rejectionFor(token, (options.now ?? Date.now)());
489
+ return mark ? { reason: mark.reason, at: mark.at } : null;
490
+ }
341
491
  /**
342
492
  * 保存済みデータを構造ごと読む。ファイル無し・内容不正は null、復号失敗は throw。
343
493
  * 箱 n=4102: 置き場は引数で渡せる(既定は ~/.musuhi)。試験は一時ディレクトリを渡す=HOME の差し替えに頼らない
@@ -660,8 +810,15 @@ exports.FLEET_ENTITLEMENT_MAX_TTL_SEC = 86_400;
660
810
  const FLEET_GATE_TIMEOUT_MS = 5_000;
661
811
  // 3 つの MCP(backend-client / yashiro-client / ledger-auth)と同じ既定=/api/bluelamp まで含む
662
812
  const FLEET_DEFAULT_BASE = 'https://api.motohi.jp/api/bluelamp';
663
- /** キャッシュの置き場。テストで HOME を差し替えられるよう呼び出し時に解決する */
813
+ /**
814
+ * キャッシュの置き場。テストで HOME を差し替えられるよう呼び出し時に解決する。
815
+ * 🔑 箱 n=5011: 答えは口座ごとの物=表で決めた名義は名義の札の置き場に持つ(既定の名義は今どおり ~/.bluelamp)。
816
+ * 1 枚を共有すると、ある名義の active を別の名義が借りる。
817
+ */
664
818
  function getFleetEntitlementFilePath() {
819
+ const id = (0, identity_js_1.resolveIdentity)();
820
+ if (id.source === 'table')
821
+ return path.join((0, identity_js_1.musuhiDirFor)(id), 'fleet-entitlement.json');
665
822
  return path.join(os.homedir(), '.bluelamp', 'fleet-entitlement.json');
666
823
  }
667
824
  /** 網に出た回だけ 1 行ずつ追記するログ(門の呼び出しを数える)。テストで HOME を差し替えられるよう呼び出し時に解決する */
@@ -1178,6 +1335,9 @@ async function postRefresh(url, current, key, nowMs, fetchFn, timeoutMs) {
1178
1335
  }
1179
1336
  if (!res.ok) {
1180
1337
  const code = typeof body?.code === 'string' ? body.code : null;
1338
+ // 箱 n=4755: 失効・機体の承認が要る札は印を付ける(次から延ばしも読み手も網に出ない)
1339
+ // 印の時刻は実時間(読み手の門も実時間で測る。nowMs は期限の判定の時計)
1340
+ noteCliTokenResponse(current.token, res.status, body);
1181
1341
  if (res.status === 409 && code === 'device_over_quota')
1182
1342
  return { status: 'device_over_quota', detail: 'http 409' };
1183
1343
  if (res.status >= 400 && res.status < 500)
@@ -1210,6 +1370,10 @@ async function ensureFreshCliToken(options = {}) {
1210
1370
  const initial = loadPortalTokenQuiet();
1211
1371
  if (!initial || !initial.token)
1212
1372
  return { status: 'no_token' };
1373
+ // 箱 n=4755: 断られた印の付いた札は網に出ない(置き場が替わるか 24 時間経つまで)
1374
+ const marked = rejectionFor(initial.token, Date.now());
1375
+ if (marked)
1376
+ return { status: 'rejected', detail: `marked ${marked.reason}` };
1213
1377
  if (typeof initial.expiresAt !== 'number' || !initial.deviceId) {
1214
1378
  maybeMigrateToV2();
1215
1379
  return { status: 'legacy' };