@takagaki/cortex-decisions-viewer 0.12.181 → 0.12.184

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/dist/build.js CHANGED
@@ -88,6 +88,7 @@ const member_origin_1 = require("./member-origin");
88
88
  const home_project_1 = require("./home-project");
89
89
  const supersedes_1 = require("./supersedes");
90
90
  const minify_1 = require("./minify");
91
+ const google_confirm_page_1 = require("./google-confirm-page");
91
92
  const render_1 = require("./render");
92
93
  const ledger_dir_1 = require("./ledger-dir");
93
94
  const view_dir_1 = require("./view-dir");
@@ -2741,6 +2742,15 @@ async function buildSite(opts, views) {
2741
2742
  // 型が通ってしまう形にすると、本番の出力が黙ってインラインに戻る
2742
2743
  const html = (0, render_1.renderSiteWithAssets)(site, assets);
2743
2744
  await node_fs_1.promises.writeFile(path.join(outAbs, "index.html"), html, "utf8");
2745
+ // Drive・Google Chat の追加の確認ページ(cortex-tools #1357)。**操作タブ(intake)がある出力にだけ**置く。
2746
+ // 本体とは別のページ・別の JS にして、ポータルの配信側がこのパスにだけ専用の CSP(Google の画面を載せる)を当てる。
2747
+ // 置いても、栓が切の間は本体のどこからも開かれない
2748
+ if (site.intake) {
2749
+ const gcJs = (0, minify_1.minifyClientJs)((0, google_confirm_page_1.googleConfirmPageJs)());
2750
+ const gcJsName = (0, google_confirm_page_1.googleConfirmAssetName)(gcJs);
2751
+ await node_fs_1.promises.writeFile(path.join(outAbs, gcJsName), gcJs, "utf8");
2752
+ await node_fs_1.promises.writeFile(path.join(outAbs, google_confirm_page_1.GOOGLE_CONFIRM_PAGE_FILE), (0, google_confirm_page_1.renderGoogleConfirmPage)({ css: assets.css, js: gcJsName }), "utf8");
2753
+ }
2744
2754
  // SPA的フォールバック(?id= はクエリなので不要だが、サブパス直叩き対策として404も用意)
2745
2755
  await node_fs_1.promises.writeFile(path.join(outAbs, "404.html"), html, "utf8");
2746
2756
  const sectionCount = sections.reduce((n, s) => n + s.records.length, 0);
@@ -0,0 +1,369 @@
1
+ "use strict";
2
+ /**
3
+ * Drive・Google Chat の追加の確認ページ(cortex-tools #1357・設計 v3 §4)。
4
+ *
5
+ * 追加欄(ビューア本体)は、このページをポップアップで開く。Google のトークンは**このページの中だけ**で扱い、
6
+ * intake の呼び出しもこのページが行って、ビューア本体には結果(成功か・画面に出す文言)だけを返す。
7
+ * ビューア全体の CSP は緩めない——このページだけが Google の画面(Picker)を載せるので、ポータルの配信側が
8
+ * このページのパスにだけ専用の CSP を当てる(classmethod-cortex-portal の `ViewerGoogleConfirm…` ポリシー)。
9
+ *
10
+ * ## 流れ
11
+ *
12
+ * 1. ビューア本体が `window.open` で開く → このページが `ready` を返す → 本体が指示(`start`)を返す
13
+ * (指示には intake の宛先・名乗り券・種別・追加か確認か・Chat の URL か確認したい行の ID が入る)
14
+ * 2. [Google で確認する](利用者の操作)→ 承認用のウィンドウを**先に**開く(ポップアップを止められないため)
15
+ * 3. intake に nonce をもらう(`google-nonce`)→ 承認用のウィンドウを Google の承認画面へ送る
16
+ * (`response_type=id_token token`・`nonce`・`prompt=select_account`)
17
+ * 4. 承認が終わると、ポータルの戻り先の頁(`/google-confirm-callback.html`)が結果を同じオリジンへ返す
18
+ * (BroadcastChannel と opener の両方。`state` が合うものだけ受ける)
19
+ * 5. Drive は Picker でフォルダを選ぶ。選んだ ID(Chat は URL)とトークン 2 本と nonce を、op に **1 回で**送る
20
+ * 6. 結果を本体へ返し、成功なら閉じる
21
+ *
22
+ * 🔴 トークン・nonce は変数に持つだけ。localStorage・URL・DOM・ログに書かない。戻り先の頁は受け取ったらすぐ URL の
23
+ * フラグメントを消す。postMessage は送り元・送り先のオリジンを確かめる(同じオリジンだけ)。
24
+ *
25
+ * 文言(画面に出る言葉)の正本は intake の `GOOGLE_CONFIRM_MESSAGES`。ここにあるのは、サーバに届く前の失敗の言葉だけ。
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.GOOGLE_CONFIRM_SCOPES = exports.GOOGLE_CONFIRM_CALLBACK_TYPE = exports.GOOGLE_CONFIRM_CHANNEL = exports.GOOGLE_CONFIRM_CALLBACK_PATH = exports.GOOGLE_CONFIRM_POST_TYPES = exports.GOOGLE_CONFIRM_PAGE_FILE = void 0;
29
+ exports.googleConfirmAssetName = googleConfirmAssetName;
30
+ exports.googleConfirmPageJs = googleConfirmPageJs;
31
+ exports.renderGoogleConfirmPage = renderGoogleConfirmPage;
32
+ const node_crypto_1 = require("node:crypto");
33
+ /** ページのファイル名(ビューアの出力の直下。本体からの相対参照で開く) */
34
+ exports.GOOGLE_CONFIRM_PAGE_FILE = "google-confirm.html";
35
+ /** 本体とページの間の postMessage の型(本体の埋め込み JS にも同じ綴りを流し込む) */
36
+ exports.GOOGLE_CONFIRM_POST_TYPES = {
37
+ ready: "cortex-google-confirm-ready",
38
+ start: "cortex-google-confirm-start",
39
+ result: "cortex-google-confirm-result",
40
+ };
41
+ /** ポータルの戻り先の頁と、同じオリジンの中で結果を運ぶ BroadcastChannel の名前 */
42
+ exports.GOOGLE_CONFIRM_CALLBACK_PATH = "/google-confirm-callback.html";
43
+ exports.GOOGLE_CONFIRM_CHANNEL = "cortex-google-confirm";
44
+ exports.GOOGLE_CONFIRM_CALLBACK_TYPE = "cortex-google-callback";
45
+ /** 種別ごとの承認のスコープ(`openid` は共通)。intake の `GOOGLE_SCOPES` と同じ綴り(テストが突き合わせる) */
46
+ exports.GOOGLE_CONFIRM_SCOPES = {
47
+ drive: "https://www.googleapis.com/auth/drive.file",
48
+ "google-chat": "https://www.googleapis.com/auth/chat.spaces.readonly",
49
+ };
50
+ /** 外部ファイル名(内容ハッシュ。同じ中身なら同じ名前=全案件で同じファイル名になる) */
51
+ function googleConfirmAssetName(js) {
52
+ return `assets/google-confirm.${(0, node_crypto_1.createHash)("sha256").update(js, "utf8").digest("hex").slice(0, 8)}.js`;
53
+ }
54
+ /** ページの JS(ブラウザで動く素の JS・ES2017)。**バッククォートと ${ を書かない**(この文字列が閉じる) */
55
+ function googleConfirmPageJs() {
56
+ return `(function () {
57
+ "use strict";
58
+ var MSG = ${JSON.stringify(exports.GOOGLE_CONFIRM_POST_TYPES)};
59
+ var CALLBACK_PATH = ${JSON.stringify(exports.GOOGLE_CONFIRM_CALLBACK_PATH)};
60
+ var CHANNEL = ${JSON.stringify(exports.GOOGLE_CONFIRM_CHANNEL)};
61
+ var CALLBACK_TYPE = ${JSON.stringify(exports.GOOGLE_CONFIRM_CALLBACK_TYPE)};
62
+ var SCOPES = ${JSON.stringify(exports.GOOGLE_CONFIRM_SCOPES)};
63
+ var ORIGIN = location.origin;
64
+ var UNFINISHED = "Google での確認が終わっていません。もう一度押してください";
65
+ var CLOSED_GRACE_MS = 6000;
66
+ var ADMIN_NOTE = "相手の管理者がこのアプリを許可する必要があります";
67
+
68
+ var $ = function (id) { return document.getElementById(id); };
69
+ var titleEl = $("gc-title");
70
+ var leadEl = $("gc-lead");
71
+ var statusEl = $("gc-status");
72
+ var goBtn = $("gc-go");
73
+ var closeBtn = $("gc-close");
74
+
75
+ var ctx = null;
76
+ var busy = false;
77
+ var finished = false;
78
+
79
+ function setStatus(text, cls) {
80
+ statusEl.textContent = text || "";
81
+ statusEl.className = "gc-status" + (cls ? " " + cls : "");
82
+ }
83
+ function randomText(bytes) {
84
+ var a = new Uint8Array(bytes);
85
+ crypto.getRandomValues(a);
86
+ var out = "";
87
+ for (var i = 0; i < a.length; i++) out += (a[i] < 16 ? "0" : "") + a[i].toString(16);
88
+ return out;
89
+ }
90
+ /** 本体へ結果だけを返す(トークンは載せない)。同じオリジンの opener にだけ */
91
+ function reportToOpener(result) {
92
+ try {
93
+ if (window.opener && !window.opener.closed) {
94
+ window.opener.postMessage(Object.assign({ type: MSG.result }, result), ORIGIN);
95
+ }
96
+ } catch (e) { /* 本体が閉じていても、このページの表示は残す */ }
97
+ }
98
+ /** 成功: 閉じる。本体へ結果を返す */
99
+ function succeed(message, extra) {
100
+ finished = true;
101
+ busy = false;
102
+ goBtn.hidden = true;
103
+ closeBtn.hidden = false;
104
+ setStatus("✓ " + message, "is-ok");
105
+ reportToOpener(Object.assign({ ok: true, message: message }, extra || {}));
106
+ setTimeout(function () { try { window.close(); } catch (e) { /* 閉じられなくても表示は残る */ } }, 1200);
107
+ }
108
+ /** 失敗: 理由を出し、押し直せるように戻す(別のアカウントでやり直す人のため)。本体にも理由を渡す */
109
+ function retryable(message, extra) {
110
+ busy = false;
111
+ goBtn.disabled = false;
112
+ goBtn.textContent = "もう一度 Google で確認する";
113
+ setStatus("✗ " + message, "is-error");
114
+ reportToOpener(Object.assign({ ok: false, message: message }, extra || {}));
115
+ }
116
+
117
+ /** intake を 1 回呼ぶ。戻り値は { status, body }(body は JSON か、文字列のとき { message } に直す) */
118
+ function callIntake(body) {
119
+ var intake = ctx.intake;
120
+ var url = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&") + "op=config&t=" + encodeURIComponent(intake.token);
121
+ var payload = Object.assign({}, body, ctx.ticket ? { actor: ctx.ticket } : {});
122
+ return fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) })
123
+ .then(function (res) {
124
+ return res.text().then(function (text) {
125
+ var parsed;
126
+ try { parsed = JSON.parse(text); } catch (e) { parsed = text; }
127
+ var b = parsed && typeof parsed === "object" ? parsed : { message: String(parsed || "") };
128
+ return { status: res.status, body: b };
129
+ });
130
+ });
131
+ }
132
+ function loadInfo() {
133
+ var intake = ctx.intake;
134
+ var url = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&") + "op=google-confirm-info&t=" + encodeURIComponent(intake.token);
135
+ return fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(ctx.ticket ? { actor: ctx.ticket } : {}) })
136
+ .then(function (res) { return res.json().catch(function () { return {}; }); });
137
+ }
138
+
139
+ /** 承認の結果を待つ。BroadcastChannel と opener の両方を聞き、state が合うものだけを受ける */
140
+ function waitForCallback(state, authWin) {
141
+ return new Promise(function (resolve) {
142
+ var done = false;
143
+ var bc = null;
144
+ var timer = null;
145
+ function settle(value) {
146
+ if (done) return;
147
+ done = true;
148
+ clearInterval(timer);
149
+ window.removeEventListener("message", onMessage);
150
+ try { if (bc) bc.close(); } catch (e) { /* 閉じられなくても害は無い */ }
151
+ resolve(value);
152
+ }
153
+ function accept(data) {
154
+ if (!data || data.type !== CALLBACK_TYPE || typeof data.hash !== "string") return;
155
+ var params = new URLSearchParams(data.hash.replace(/^#/, ""));
156
+ // 別の流れの結果(古い承認・別のタブ)は受けない
157
+ if (params.get("state") !== state) return;
158
+ settle({ params: params });
159
+ }
160
+ function onMessage(ev) {
161
+ // 🔴 送り元のオリジンと、開いた承認用のウィンドウであることを確かめる
162
+ if (ev.origin !== ORIGIN || ev.source !== authWin) return;
163
+ accept(ev.data);
164
+ }
165
+ window.addEventListener("message", onMessage);
166
+ try {
167
+ bc = new BroadcastChannel(CHANNEL);
168
+ bc.onmessage = function (ev) { accept(ev.data); };
169
+ } catch (e) { bc = null; }
170
+ // 承認のウィンドウが(結果を返さずに)閉じられた
171
+ // ブラウザによっては、承認の途中で Google 側の設定によりウィンドウの所属が切り替わり、こちらから見て closed が真になる。
172
+ // すぐ失敗にせず、結果(BroadcastChannel)を少し待つ
173
+ var closedAt = 0;
174
+ timer = setInterval(function () {
175
+ if (!authWin.closed) { closedAt = 0; return; }
176
+ if (!closedAt) closedAt = Date.now();
177
+ if (Date.now() - closedAt >= CLOSED_GRACE_MS) settle({ closed: true });
178
+ }, 500);
179
+ });
180
+ }
181
+
182
+ function authUrl(info, nonce, state) {
183
+ var q = [
184
+ ["client_id", info.clientId],
185
+ ["redirect_uri", ORIGIN + CALLBACK_PATH],
186
+ ["response_type", "id_token token"],
187
+ ["scope", "openid " + SCOPES[ctx.kind]],
188
+ ["nonce", nonce],
189
+ ["state", state],
190
+ ["prompt", "select_account"],
191
+ ["include_granted_scopes", "false"]
192
+ ];
193
+ return "https://accounts.google.com/o/oauth2/v2/auth?" + q.map(function (kv) { return kv[0] + "=" + encodeURIComponent(kv[1]); }).join("&");
194
+ }
195
+
196
+ function loadScript(src) {
197
+ return new Promise(function (resolve, reject) {
198
+ var s = document.createElement("script");
199
+ s.src = src;
200
+ s.onload = function () { resolve(); };
201
+ s.onerror = function () { reject(new Error("script")); };
202
+ document.head.appendChild(s);
203
+ });
204
+ }
205
+ /** Picker でフォルダを選ぶ。選ばずに閉じたら null */
206
+ function pickFolder(info, accessToken) {
207
+ return loadScript("https://apis.google.com/js/api.js")
208
+ .then(function () { return new Promise(function (resolve) { window.gapi.load("picker", { callback: resolve }); }); })
209
+ .then(function () {
210
+ return new Promise(function (resolve) {
211
+ var g = window.google.picker;
212
+ var mine = new g.DocsView(g.ViewId.FOLDERS).setIncludeFolders(true).setSelectFolderEnabled(true).setMimeTypes("application/vnd.google-apps.folder");
213
+ var shared = new g.DocsView(g.ViewId.FOLDERS).setIncludeFolders(true).setSelectFolderEnabled(true).setEnableDrives(true).setMimeTypes("application/vnd.google-apps.folder");
214
+ new g.PickerBuilder()
215
+ .setAppId(info.projectNumber)
216
+ .setDeveloperKey(info.apiKey)
217
+ .setOAuthToken(accessToken)
218
+ .enableFeature(g.Feature.SUPPORT_DRIVES)
219
+ .addView(mine)
220
+ .addView(shared)
221
+ .setCallback(function (data) {
222
+ if (data.action === g.Action.PICKED && data.docs && data.docs[0]) resolve(data.docs[0]);
223
+ else if (data.action === g.Action.CANCEL) resolve(null);
224
+ })
225
+ .build()
226
+ .setVisible(true);
227
+ });
228
+ });
229
+ }
230
+
231
+ function authErrorMessage(code) {
232
+ if (code === "access_denied") return UNFINISHED;
233
+ if (code === "admin_policy_enforced" || code === "org_internal" || code === "disallowed_useragent") {
234
+ return "あなたの Google アカウントでは、このアプリを使えませんでした。" + ADMIN_NOTE;
235
+ }
236
+ return UNFINISHED;
237
+ }
238
+
239
+ /** [Google で確認する]を押したあとの全部 */
240
+ function run() {
241
+ if (busy || finished || !ctx) return;
242
+ busy = true;
243
+ goBtn.disabled = true;
244
+ setStatus("Google の画面で確認してください…", "");
245
+ // 🔴 承認用のウィンドウは、押した直後(同期)に開く。後ろへ回すとポップアップとして止められる
246
+ var authWin = window.open("about:blank", "cortex-google-auth", "popup,width=500,height=680");
247
+ if (!authWin) { retryable("ポップアップが開けませんでした。ブラウザのポップアップを許可して、もう一度押してください"); return; }
248
+ var info = null;
249
+ var nonce = null;
250
+ var tokens = null;
251
+ loadInfo()
252
+ .then(function (i) {
253
+ if (!i || i.enabled !== true || i.ready !== true) throw { message: "今は確認できません。しばらくしてからもう一度" };
254
+ info = i;
255
+ var body = { op: "google-nonce", kind: ctx.kind };
256
+ if (ctx.kind === "google-chat") body.url = ctx.url || ctx.id;
257
+ else if (ctx.id) body.folderId = ctx.id;
258
+ return callIntake(body);
259
+ })
260
+ .then(function (r) {
261
+ if (r.status !== 200 || !r.body.nonce) throw { message: r.body.message || "今は確認できません。しばらくしてからもう一度" };
262
+ nonce = r.body.nonce;
263
+ var state = randomText(16);
264
+ var waiting = waitForCallback(state, authWin);
265
+ authWin.location.href = authUrl(info, nonce, state);
266
+ return waiting;
267
+ })
268
+ .then(function (cb) {
269
+ try { authWin.close(); } catch (e) { /* もう閉じている */ }
270
+ if (cb.closed) throw { message: UNFINISHED };
271
+ var p = cb.params;
272
+ if (p.get("error")) throw { message: authErrorMessage(p.get("error")) };
273
+ var access = p.get("access_token");
274
+ var idToken = p.get("id_token");
275
+ if (!access || !idToken) throw { message: UNFINISHED };
276
+ tokens = { googleToken: access, googleIdToken: idToken, googleNonce: nonce };
277
+ // Drive の追加と確認は、承認のあとに本人が Picker で選ぶ(drive.file はここで選んだものだけが読める)
278
+ if (ctx.kind === "drive") return pickFolder(info, access);
279
+ return null;
280
+ })
281
+ .then(function (picked) {
282
+ var body;
283
+ if (ctx.kind === "drive") {
284
+ if (!picked) throw { message: "フォルダを選ばずに閉じられました。もう一度押してください" };
285
+ body = ctx.mode === "confirm"
286
+ ? { op: "confirm-drive-folder", id: ctx.id, folderId: picked.id }
287
+ : { op: "add-drive-folder", folderId: picked.id };
288
+ } else {
289
+ body = ctx.mode === "confirm"
290
+ ? { op: "confirm-google-chat-space", ref: ctx.id }
291
+ : { op: "add-google-chat-space", url: ctx.url };
292
+ }
293
+ return callIntake(Object.assign(body, tokens));
294
+ })
295
+ .then(function (r) {
296
+ tokens = null;
297
+ if (r.status === 200) {
298
+ succeed(r.body.message || "確認しました", { changed: r.body.changed !== false, status: 200 });
299
+ } else {
300
+ retryable(r.body.message || "追加できませんでした", { status: r.status, code: r.body.code || null });
301
+ }
302
+ })
303
+ .catch(function (e) {
304
+ tokens = null;
305
+ try { authWin.close(); } catch (e2) { /* もう閉じている */ }
306
+ // 通信そのものの失敗(Error)の英語の文面は画面に出さない
307
+ var message = Object.prototype.toString.call(e) === "[object Error]" ? "今は確認できません。しばらくしてからもう一度" : e && e.message ? e.message : UNFINISHED;
308
+ retryable(message);
309
+ });
310
+ }
311
+
312
+ function onStart(ev) {
313
+ // 🔴 同じオリジンの、このページを開いた本体からの指示だけを受ける
314
+ if (ev.origin !== ORIGIN || ev.source !== window.opener) return;
315
+ var d = ev.data;
316
+ if (!d || d.type !== MSG.start || ctx) return;
317
+ if (!d.intake || typeof d.intake.url !== "string" || typeof d.intake.token !== "string") return;
318
+ if (d.kind !== "drive" && d.kind !== "google-chat") return;
319
+ ctx = { intake: d.intake, ticket: typeof d.ticket === "string" ? d.ticket : null, kind: d.kind, mode: d.mode === "confirm" ? "confirm" : "add", url: typeof d.url === "string" ? d.url : "", id: typeof d.id === "string" ? d.id : "" };
320
+ var isDrive = ctx.kind === "drive";
321
+ var what = isDrive ? "Drive のフォルダ" : "Google Chat のスペース";
322
+ titleEl.textContent = (ctx.mode === "confirm" ? what + "を確認する" : what + "を追加する");
323
+ leadEl.textContent = isDrive
324
+ ? "あなたの Google アカウントで、追加するフォルダを選びます。選んだフォルダだけを Cortex が確認に使います。「このアプリは Google で確認されていません」と出たら、「詳細」から進めます。"
325
+ : "あなたの Google アカウントで、このスペースを開けるかを確認します。メッセージは読みません。";
326
+ goBtn.textContent = "Google で確認する";
327
+ goBtn.hidden = false;
328
+ setStatus("", "");
329
+ }
330
+
331
+ goBtn.addEventListener("click", run);
332
+ closeBtn.addEventListener("click", function () { try { window.close(); } catch (e) { /* 閉じられなければそのまま */ } });
333
+ window.addEventListener("message", onStart);
334
+ // このページは本体から開かれたときだけ使える
335
+ if (window.opener) {
336
+ try { window.opener.postMessage({ type: MSG.ready }, ORIGIN); } catch (e) { /* 本体が閉じた */ }
337
+ } else {
338
+ setStatus("このページは、追加欄の[Google で選ぶ]などから開きます。", "");
339
+ }
340
+ })();
341
+ `;
342
+ }
343
+ /** ページの HTML。CSS はビューア本体と同じ外部ファイル(`assets/viewer.<hash>.css`)・JS は専用の外部ファイル */
344
+ function renderGoogleConfirmPage(assets) {
345
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
346
+ return `<!DOCTYPE html>
347
+ <html lang="ja">
348
+ <head>
349
+ <meta charset="utf-8" />
350
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
351
+ <meta name="robots" content="noindex, nofollow" />
352
+ <meta name="referrer" content="strict-origin-when-cross-origin" />
353
+ <title>Google で確認</title>
354
+ <link rel="stylesheet" href="${esc(assets.css)}" />
355
+ </head>
356
+ <body class="gc-body">
357
+ <main class="gc-page">
358
+ <h1 id="gc-title" class="gc-title">Google で確認</h1>
359
+ <p id="gc-lead" class="gc-lead"></p>
360
+ <p id="gc-status" class="gc-status" role="status" aria-live="polite"></p>
361
+ <div class="gc-actions">
362
+ <button id="gc-go" class="ds-toggle-btn" type="button" hidden>Google で確認する</button>
363
+ <button id="gc-close" class="ds-toggle-btn" type="button" hidden>閉じる</button>
364
+ </div>
365
+ </main>
366
+ <script src="${esc(assets.js)}"></script>
367
+ </body>
368
+ </html>`;
369
+ }
@@ -347,7 +347,8 @@ function pendingOpOf(payload, ctx) {
347
347
  case "remove-figma-file":
348
348
  return make("figma", "remove", pendingFigmaKey(payload.url) || payload.key);
349
349
  case "add-drive-folder": {
350
- const id = pendingDriveFolderId(payload.url);
350
+ // 選択画面(Picker)から来た ID は、URL ではなく folderId で届く(#1357。サーバの planConfigChange と同じ読み分け)
351
+ const id = payload.folderId !== undefined ? pendingDriveFolderId(String(payload.folderId)) : pendingDriveFolderId(payload.url);
351
352
  return make("drive", "add", id, { name: pendingLabel(payload.name) });
352
353
  }
353
354
  case "remove-drive-folder":
package/dist/render.js CHANGED
@@ -100,6 +100,8 @@ const confirmed_1 = require("./confirmed");
100
100
  const external_id_1 = require("./external-id");
101
101
  // 操作タブの[権限](#841)。生成物で、正本は infra/lambda/shared/permissions.mjs
102
102
  const permissions_data_1 = require("./permissions-data");
103
+ // Drive・Google Chat の追加の確認ページ(#1357)。本体からの開き方と、メッセージの型の綴りだけ引く
104
+ const google_confirm_page_1 = require("./google-confirm-page");
103
105
  // 操作タブの仮置き(#388)。判定の実体はあちら——ここは流し込みと配線だけ
104
106
  const pending_ops_1 = require("./pending-ops");
105
107
  /**
@@ -5590,6 +5592,17 @@ a.ds-tile:hover { text-decoration: none; }
5590
5592
  .ds-toggle-msg { font-size: 12px; color: var(--muted); }
5591
5593
  .ds-toggle-msg.is-ok { color: #1a7f37; }
5592
5594
  .ds-toggle-msg.is-error { color: #b42318; }
5595
+ /* Drive・Google Chat の追加の確認(cortex-tools #1357)。確認ページ(別ページ)と、行の「未確認」の印 */
5596
+ .gc-body { margin: 0; background: var(--bg, #fff); color: var(--fg, #1a1a1a); font: 14px/1.6 system-ui, sans-serif; }
5597
+ .gc-page { max-width: 460px; margin: 0 auto; padding: 24px 16px; }
5598
+ .gc-title { font-size: 18px; margin: 0 0 8px; }
5599
+ .gc-lead { margin: 0 0 16px; color: var(--muted); }
5600
+ .gc-status { min-height: 1.6em; margin: 0 0 16px; font-size: 13px; color: var(--muted); }
5601
+ .gc-status.is-ok { color: #1a7f37; }
5602
+ .gc-status.is-error { color: #b42318; }
5603
+ .gc-actions { display: flex; gap: 8px; }
5604
+ .gc-unverified { font-size: 12px; color: #9a6700; border: 1px solid #d4a72c; border-radius: 4px; padding: 0 6px; }
5605
+ .gc-verify-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
5593
5606
  /* Backlog の同期経路のスイッチ(#516)。role=switch の button に、トラックとつまみを描く。
5594
5607
  語(.ds-switch-label)は押している間も差し替えない(状態は下の 1 行が言う) */
5595
5608
  .ds-route { margin-top: 10px; }
@@ -6225,6 +6238,156 @@ const CLIENT_JS = `
6225
6238
  });
6226
6239
  }
6227
6240
 
6241
+ // ---- Drive・Google Chat の追加の確認(cortex-tools #1357)----
6242
+ //
6243
+ // 栓が入っている間だけ働く(intake の google-confirm-info が enabled のとき)。切のあいだは info が { enabled: false } を返し、
6244
+ // 追加欄も行も今のまま。Google のトークンはここでは一切扱わない——確認ページ(別ページ)がポップアップで扱い、ここへは結果だけが戻る。
6245
+ // 注: ここは埋め込みJSのテンプレートリテラル内。コメントでもバッククォートを使わない
6246
+ var GC_PAGE = ${JSON.stringify(google_confirm_page_1.GOOGLE_CONFIRM_PAGE_FILE)};
6247
+ var GC_MSG = ${JSON.stringify(google_confirm_page_1.GOOGLE_CONFIRM_POST_TYPES)};
6248
+ var GC_UNFINISHED = "Google での確認が終わっていません。もう一度押してください";
6249
+ var GC_UNAVAILABLE = "今は確認できません。しばらくしてからもう一度";
6250
+ // サーバの op=state が返す googleConfirm(栓が切なら無い=null=今の追加欄のまま)。取れていない間も null
6251
+ var googleInfo = null;
6252
+ // 栓が入った(または切れた)ときに作り直す追加欄の口
6253
+ var googleSlotRefills = [];
6254
+ function googleInfoApply(next) {
6255
+ var info = next && typeof next === "object" && next.enabled === true ? next : null;
6256
+ if (googleInfoSigOf(info) === googleInfoSigOf(googleInfo)) return;
6257
+ googleInfo = info;
6258
+ googleSlotRefills = googleSlotRefills.filter(function (refill) { return refill(); });
6259
+ repaintOpenLists();
6260
+ }
6261
+ function googleInfoSigOf(i) {
6262
+ if (!i) return "";
6263
+ return JSON.stringify([i.enabled === true, i.ready === true, i.confirmed || null]);
6264
+ }
6265
+ function googleInfoSig() { return googleInfoSigOf(googleInfo); }
6266
+ function googleConfirmOn() { return !!googleInfo && googleInfo.enabled === true; }
6267
+ function googleConfirmReady() { return googleConfirmOn() && googleInfo.ready === true; }
6268
+ function googleConfirmedHas(kind, id) {
6269
+ var c = googleInfo && googleInfo.confirmed;
6270
+ return !!c && Array.isArray(c[kind]) && c[kind].indexOf(id) >= 0;
6271
+ }
6272
+ /**
6273
+ * 確認ページをポップアップで開き、結果を待つ。**ここで受け取るのは結果だけ**(トークンは来ない)。
6274
+ * 🔴 メッセージは同じオリジンの、いま開いたウィンドウからのものだけを受ける。閉じられたら、最後の失敗の理由(無ければ「終わっていません」)
6275
+ */
6276
+ function openGoogleConfirm(opts) {
6277
+ return new Promise(function (resolve) {
6278
+ var intake = SITE.intake;
6279
+ var win = window.open(new URL(GC_PAGE, location.href).href, "cortex-google-confirm", "popup,width=520,height=640");
6280
+ if (!win) { resolve({ ok: false, message: "ポップアップが開けませんでした。ブラウザのポップアップを許可して、もう一度押してください" }); return; }
6281
+ var done = false;
6282
+ var last = null;
6283
+ var timer = null;
6284
+ function end(r) {
6285
+ if (done) return;
6286
+ done = true;
6287
+ window.removeEventListener("message", onMsg);
6288
+ clearInterval(timer);
6289
+ resolve(r);
6290
+ }
6291
+ function onMsg(ev) {
6292
+ if (ev.origin !== location.origin || ev.source !== win || !ev.data) return;
6293
+ if (ev.data.type === GC_MSG.ready) {
6294
+ win.postMessage({
6295
+ type: GC_MSG.start,
6296
+ intake: { url: intake.url, token: intake.token },
6297
+ ticket: actorTicket(),
6298
+ kind: opts.kind,
6299
+ mode: opts.mode,
6300
+ url: opts.url || "",
6301
+ id: opts.id || "",
6302
+ }, location.origin);
6303
+ } else if (ev.data.type === GC_MSG.result) {
6304
+ if (ev.data.ok) end({ ok: true, message: ev.data.message, changed: ev.data.changed });
6305
+ else last = ev.data;
6306
+ }
6307
+ }
6308
+ window.addEventListener("message", onMsg);
6309
+ timer = setInterval(function () {
6310
+ if (win.closed) end({ ok: false, message: last && last.message ? last.message : GC_UNFINISHED });
6311
+ }, 500);
6312
+ });
6313
+ }
6314
+ /** 確認が済んだあとの後始末: 登録の一覧と確認済みの一覧(どちらも op=state)を取り直し、開いている一覧を描き直す */
6315
+ function afterGoogleConfirm() {
6316
+ loadLiveFleet(true);
6317
+ }
6318
+ /** 栓が入っている案件では追加欄を作り替える。切(または取れない)なら今の追加欄 */
6319
+ function googleAwareSlot(buildLegacy, buildPicker) {
6320
+ var slot = el("div", {});
6321
+ function fill() {
6322
+ while (slot.firstChild) slot.removeChild(slot.firstChild);
6323
+ slot.appendChild(googleConfirmOn() ? buildPicker(googleInfo) : buildLegacy());
6324
+ }
6325
+ fill();
6326
+ // 栓の状態が後から届いたとき(最初の op=state の応答)に作り直す。画面から外れたら外す
6327
+ googleSlotRefills.push(function () {
6328
+ if (!slot.isConnected) return false;
6329
+ fill();
6330
+ return true;
6331
+ });
6332
+ return slot;
6333
+ }
6334
+ /** Drive の追加欄(栓が入っているとき)。URL の貼り付けは無く、[Google で選ぶ]だけ */
6335
+ function googleDriveAddBox(info) {
6336
+ var msg = el("div", { class: "ds-toggle-msg" });
6337
+ var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "Google で選ぶ" });
6338
+ btn.addEventListener("click", async function () {
6339
+ if (info.ready !== true) { msg.className = "ds-toggle-msg is-error"; msg.textContent = "✗ " + GC_UNAVAILABLE; return; }
6340
+ btn.disabled = true;
6341
+ msg.className = "ds-toggle-msg";
6342
+ msg.textContent = "Google の画面で選んでください…";
6343
+ var r = await openGoogleConfirm({ kind: "drive", mode: "add" });
6344
+ btn.disabled = false;
6345
+ msg.className = "ds-toggle-msg" + (r.ok ? " is-ok" : " is-error");
6346
+ msg.textContent = (r.ok ? "✓ " : "✗ ") + r.message;
6347
+ if (r.ok) afterGoogleConfirm();
6348
+ });
6349
+ return el("div", { class: "ds-addform", "data-adder": "google-drive" }, [
6350
+ el("div", { class: "ds-addform-title", text: "+ Google Drive のフォルダを追加" }),
6351
+ el("div", { class: "ds-toggle" }, [btn, msg]),
6352
+ ]);
6353
+ }
6354
+ /** Chat の[追加]を押したとき、栓が入っていれば確認ページを開く。扱ったら true(呼び手は通常の送信をしない) */
6355
+ function googleChatAddIntercept(url, msg, btn, onOk) {
6356
+ if (!googleConfirmOn()) return false;
6357
+ if (!googleConfirmReady()) { msg.className = "ds-toggle-msg is-error"; msg.textContent = "✗ " + GC_UNAVAILABLE; return true; }
6358
+ btn.disabled = true;
6359
+ msg.className = "ds-toggle-msg";
6360
+ msg.textContent = "Google の画面で確認してください…";
6361
+ openGoogleConfirm({ kind: "google-chat", mode: "add", url: url }).then(function (r) {
6362
+ btn.disabled = false;
6363
+ msg.className = "ds-toggle-msg" + (r.ok ? " is-ok" : " is-error");
6364
+ msg.textContent = (r.ok ? "✓ " : "✗ ") + r.message;
6365
+ if (r.ok) { onOk(); afterGoogleConfirm(); }
6366
+ });
6367
+ return true;
6368
+ }
6369
+ /** 今ある行の「未確認」の印と[確認する](確認済みの行には何も足さない・期限で外さない) */
6370
+ function appendGoogleVerifyUi(row, kind, id) {
6371
+ if (!googleConfirmReady() || googleConfirmedHas(kind, id)) return;
6372
+ var msg = el("span", { class: "ds-toggle-msg" });
6373
+ var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "確認する" });
6374
+ btn.addEventListener("click", async function () {
6375
+ btn.disabled = true;
6376
+ msg.className = "ds-toggle-msg";
6377
+ msg.textContent = "Google の画面で確認してください…";
6378
+ var r = await openGoogleConfirm({ kind: kind, mode: "confirm", id: id, url: id });
6379
+ btn.disabled = false;
6380
+ msg.className = "ds-toggle-msg" + (r.ok ? " is-ok" : " is-error");
6381
+ msg.textContent = (r.ok ? "✓ " : "✗ ") + r.message;
6382
+ if (r.ok) afterGoogleConfirm();
6383
+ });
6384
+ row.appendChild(el("div", { class: "gc-verify-row" }, [
6385
+ el("span", { class: "gc-unverified", title: "追加した人が、自分の Google で開けるかを確かめた記録がありません。押さなくても取り込みは続きます", text: "未確認" }),
6386
+ btn,
6387
+ msg,
6388
+ ]));
6389
+ }
6390
+
6228
6391
  // ---- ビルド時TSと共有する判定(唯一の正本は上の関数定義) ----
6229
6392
  //
6230
6393
  // **写経しない。** 接続マップ(ビルド時にSVGを描く)と画面(ブラウザで一覧を描く)が
@@ -9820,16 +9983,18 @@ ${story_1.splitByNameLinks.toString()}
9820
9983
  var meta = owner ? githubOwnerMeta(owner) : null;
9821
9984
  if (meta && meta.source === "unknown") return null;
9822
9985
  // 🔴 リンクの宛先は、その組織について窓口が答えたアプリ(案件のテナントのアプリ・#1328 T-6)。
9823
- // 正本(op=state)が答えている画面で slug が無い組織は「分からない」回なので、**宛先を出さず文だけ**にする
9986
+ // 正本(op=state)が答えている画面で、自分の行にも同じ案件の他の行にも slug が無いときは、**宛先を出さず文だけ**にする
9824
9987
  // (当社のアプリへ落とすと、会社ごとのアプリの会社の画面で別のアプリを入れさせる)。
9825
9988
  // 当社のアプリの定数へ落とすのは、正本の答えがまだ無い画面(まとめ表だけ)のときだけ
9826
- var slug = githubAppSlugOf(meta);
9989
+ // その組織の行に slug が無いときは、同じ案件の正本の他の行から借りる(入れる App は組織でなく案件のテナントで決まる・#1359)
9990
+ var slug = githubAppSlugOf(meta) || githubTenantAppSlug();
9827
9991
  var linkable = !!slug || !LIVE_GH_OWNERS;
9828
9992
  var pick = row.gate === "unreachable" && meta && meta.appInstalled === true;
9829
9993
  var say = pick ? "GitHub App の設定で、このリポジトリを選んでください。" : "この組織に Cortex の GitHub App を入れてください。";
9830
9994
  if (!linkable) return pick ? [say] : [say, { br: true }, GITHUB_APP_RECHECK_TEXT];
9831
- // 部品には行の生の値を渡す(宛先の検めは githubAppLinkUrl が持つ。ここの slug はリンクを出すかの判断だけ)
9832
- var raw = meta && meta.appSlug;
9995
+ // 🔴 部品には**解決後の slug**を渡す(組織の行の値が無いのに元の値を渡すと、githubAppLinkUrl が当社の定数へ落ちる)。
9996
+ // 正本の答えが無い画面(まとめ表だけ)では slug が空のまま渡り、従来どおり定数へ落ちる
9997
+ var raw = slug || (meta && meta.appSlug);
9833
9998
  // 入れた後の一手は同じ段落の 1 行に添える(行の補足は最大 1 段)。選び直す方(pick)は入れ直しではないので添えない
9834
9999
  return pick
9835
10000
  ? [say, { link: "インストール設定を開く →", kind: "settings", slug: raw }]
@@ -11148,6 +11313,24 @@ ${story_1.splitByNameLinks.toString()}
11148
11313
  var url = s ? "https://github.com/apps/" + encodeURIComponent(s) + "/installations/new" : GITHUB_APP_INSTALL_URL;
11149
11314
  return ({ install: url, settings: url })[kind] || url;
11150
11315
  }
11316
+ /**
11317
+ * 同じ案件のテナントのアプリの slug。**案件単位の値(LIVE_GH_APP_SLUG・窓口が案件の会社で答えた値)を先に使う**——組織が 1 つだけで
11318
+ * 照会が断られた案件でも宛先が分かる(#1359)。無ければ、同じ案件の正本(LIVE_GH_OWNERS の行だけ。まとめ表の古い行は混ぜない)の
11319
+ * 有効な slug が 1 つ以上あり、**全部が同じ**ときだけ返す(食い違えば「分からない」=null=文だけ)。
11320
+ * 🔴 当社の定数には落とさない(#1328 T-6)。⚠ 埋め込みJSなので、正規表現は逆斜線なしで書く
11321
+ */
11322
+ function githubTenantAppSlug() {
11323
+ if (LIVE_GH_APP_SLUG) return LIVE_GH_APP_SLUG;
11324
+ var found = null;
11325
+ var list = LIVE_GH_OWNERS || [];
11326
+ for (var i = 0; i < list.length; i++) {
11327
+ var s = githubAppSlugOf(list[i]);
11328
+ if (!s) continue;
11329
+ if (found && found !== s) return null;
11330
+ found = s;
11331
+ }
11332
+ return found;
11333
+ }
11151
11334
  /** 行の slug(形が合わなければ null)。⚠ 埋め込みJSなので、正規表現は逆斜線なしで書く */
11152
11335
  function githubAppSlugOf(meta) {
11153
11336
  var v = meta && meta.appSlug;
@@ -11386,6 +11569,8 @@ ${story_1.splitByNameLinks.toString()}
11386
11569
  * (unsettled/pending)に組織の行が紛れ込む。
11387
11570
  */
11388
11571
  var LIVE_GH_OWNERS = null;
11572
+ /** 案件の会社の GitHub App の slug(op=state の registeredNow.githubAppSlug・組織ごとの行に slug が無い回の宛先・#1359) */
11573
+ var LIVE_GH_APP_SLUG = null;
11389
11574
  /** マージ済みの3キー(作り直しは setLive* だけが行う) */
11390
11575
  var MERGED = null;
11391
11576
  /** マージの対象になるキー。これ以外は素の値をそのまま返す */
@@ -12350,7 +12535,29 @@ ${story_1.splitByNameLinks.toString()}
12350
12535
  } else if (cap === "課題管理" && (!adapter || adapter === "backlog")) {
12351
12536
  // 🔴 **Backlog の説明文(c-07 の 2 本目・新設)。** 追加の欄の説明から外した 3 つを、ここに 1 回だけ置く:
12352
12537
  // 何が同期されるか・起票先は変わらないこと(2026-08-20 に追加の確認をやめたとき追加の欄へ移した文)・止め方
12353
- m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいる Backlog プロジェクトの課題・ドキュメント・Wiki を毎晩同期します。接続先を足しても、ビューアからの起票先は変わりません。やめるには「この接続先を外す」を押してください。" }));
12538
+ var backlogDesc = el("p", { class: "modal-desc", text: "ここに並んでいる Backlog プロジェクトの課題・ドキュメント・Wiki を毎晩同期します。接続先を足しても、ビューアからの起票先は変わりません。やめるには「この接続先を外す」を押してください。" });
12539
+ m.modal.appendChild(backlogDesc);
12540
+ // 🔴 **つなぎ方の動画(#1353)はモーダルの見出しのすぐ下(説明文より前)に置く。** 鍵の欄の「発行のしかた」の中に置くと、
12541
+ // 接続先が 0 件のあいだは鍵の欄ごと出ないので、いちばん要る最初のときに見えない。
12542
+ // 接続先が 0 件のときは開いて出し、つないだあとは畳む(鍵の欄の中には置かない=同じ動画を 2 か所に出さない)
12543
+ // 説明文は先に追加済みなので、説明文の前へ差し込む
12544
+ var backlogForm = SECRET_FORMS["backlog"];
12545
+ var backlogCarousel = backlogForm && backlogForm.guideInModal && backlogTargetsEditable()
12546
+ ? buildGuideCarousel("backlog", backlogForm.guide) : null;
12547
+ if (backlogCarousel) {
12548
+ var guideFold = el("details", { class: "sec-fold mt-guide-fold", "data-guide-kind": "backlog" }, [
12549
+ el("summary", { text: "つなぎ方を動画で見る" }),
12550
+ backlogCarousel,
12551
+ ]);
12552
+ guideFold.open = keyTargetCount("課題管理") === 0;
12553
+ // 閉じても <video> は DOM に残るので、再生中なら止める(音が鳴り続けない)
12554
+ guideFold.addEventListener("toggle", function () {
12555
+ if (guideFold.open) return;
12556
+ var playing = guideFold.querySelector("video");
12557
+ if (playing && typeof playing.pause === "function") playing.pause();
12558
+ });
12559
+ m.modal.insertBefore(guideFold, backlogDesc);
12560
+ }
12354
12561
  } else if (cap === "開発") {
12355
12562
  // 🔴 **開発の開示文は「記録に使う・決定の抽出には使わない」(#660)。** 2026-09-18 の
12356
12563
  // オーナー決定でソースの性質は種類で固定になり、GitHub は「作業の記録」に固定された。
@@ -12839,7 +13046,7 @@ ${story_1.splitByNameLinks.toString()}
12839
13046
  // 作れない。判断③)。4件以上でも畳まない——畳みは汎用経路の仕様で、ここは通らない
12840
13047
  if (liveListWanted("google-chat")) pushCap("チャット", liveList(
12841
13048
  "チャット",
12842
- function () { return externalsSig("google-chat", null); },
13049
+ function () { return externalsSig("google-chat", googleInfoSig()); },
12843
13050
  function (box) {
12844
13051
  var items = liveExternals("google-chat");
12845
13052
  // 注記は**行より前**に置く(0件なら一覧の代わり・行があれば添え書き)
@@ -12847,7 +13054,12 @@ ${story_1.splitByNameLinks.toString()}
12847
13054
  if (noteGchat) box.appendChild(noteGchat);
12848
13055
  if (!items.length) return;
12849
13056
  var meta = DS_TYPE["google-chat"] || { icon: "💬", label: "Google Chat" };
12850
- dimUnconfirmedRows(items.map(function (s) { return extSourceCard(s, meta); }))
13057
+ dimUnconfirmedRows(items.map(function (s) {
13058
+ var card = extSourceCard(s, meta);
13059
+ // 追加した人が自分の Google で開けると確かめた記録が無い行(栓が入っているときだけ・#1357)
13060
+ appendGoogleVerifyUi(card, "google-chat", s.ref);
13061
+ return card;
13062
+ }))
12851
13063
  .forEach(function (n) { box.appendChild(n); });
12852
13064
  },
12853
13065
  ), "google-chat");
@@ -15911,7 +16123,7 @@ ${story_1.splitByNameLinks.toString()}
15911
16123
  // 「確かめられたか」も混ぜる(#409。混ぜないと注記の要否が変わっても作り直されない)
15912
16124
  // 「届くか」も混ぜる(#523 S1。混ぜないと、ストアの reachable が届いても札が付かない)
15913
16125
  return JSON.stringify([d.driveFolderIds || [], d.driveFolderNames || {}, d.driveState || "", d.drivePending || {}, registrationUnreadable(), registryUnconfirmed(),
15914
- reachSigOf("materials", d.driveFolderIds || [])]);
16126
+ reachSigOf("materials", d.driveFolderIds || []), googleInfoSig()]);
15915
16127
  }, function (listBox) {
15916
16128
  var src = driveNow();
15917
16129
  var folders = src.driveFolderIds || [];
@@ -15943,6 +16155,8 @@ ${story_1.splitByNameLinks.toString()}
15943
16155
  // **これまで画面に出す場所が1つも無かった**(共有が外れたことを、取り込みが
15944
16156
  // 失敗するまで言えなかった)
15945
16157
  appendReachableChip(row, "materials", id);
16158
+ // 追加した人が自分の Google で開けると確かめた記録が無い行(栓が入っているときだけ・#1357)
16159
+ appendGoogleVerifyUi(row, "drive", id);
15946
16160
  // **名前を出したときはIDも併記する。** 同名のフォルダを見分けられなくなるのと、
15947
16161
  // 「このIDを外す」という確認文と画面の表示が食い違うのを防ぐ
15948
16162
  if (fname) row.appendChild(el("span", { class: "ds-sync", text: id }));
@@ -15986,7 +16200,7 @@ ${story_1.splitByNameLinks.toString()}
15986
16200
  listBox.appendChild(el("p", { class: "ds-empty", text: "取り込み対象のフォルダはまだ登録されていません。" }));
15987
16201
  }
15988
16202
  }));
15989
- box.appendChild(addForm({
16203
+ box.appendChild(googleAwareSlot(function () { return addForm({
15990
16204
  title: "+ Google Drive のフォルダを追加",
15991
16205
  placeholder: "https://drive.google.com/drive/folders/…",
15992
16206
  namePlaceholder: "メモ(任意)",
@@ -15996,7 +16210,7 @@ ${story_1.splitByNameLinks.toString()}
15996
16210
  // (DS_GATE_CAP.materials.unreachable: 「このフォルダを〈取り込み用アカウント〉に閲覧者として共有してください。[コピー]」)。
15997
16211
  // 「中身が 共有資料 にコピーされ Markdown 化される・止め方」はモーダル上部の説明文が 1 回だけ持つ。
15998
16212
  build: function (url, name) { return { op: "add-drive-folder", url: url, name: name }; },
15999
- }));
16213
+ }); }, googleDriveAddBox));
16000
16214
  used = true;
16001
16215
  }
16002
16216
  }
@@ -16286,6 +16500,8 @@ ${story_1.splitByNameLinks.toString()}
16286
16500
  build: function (url) {
16287
16501
  return { op: "add-google-chat-space", url: url };
16288
16502
  },
16503
+ // 栓が入っているときは、[追加]で確認ページ(Google の承認)を開く(#1357)。切のときは今の送信
16504
+ googleChat: true,
16289
16505
  adder: "google-chat",
16290
16506
  }));
16291
16507
 
@@ -16940,6 +17156,8 @@ ${story_1.splitByNameLinks.toString()}
16940
17156
  }
16941
17157
  var added = main.value.trim();
16942
17158
  var addedName = name ? name.value.trim() : "";
17159
+ // Google Chat は、栓が入っていれば本人の Google で開けるかを確かめてから追加する(確認ページが intake を呼ぶ・#1357)
17160
+ if (opts.googleChat && googleChatAddIntercept(added, msg, btn, function () { main.value = ""; })) return;
16943
17161
  var payload = opts.build(added, addedName, wsSelect ? wsSelect.value : "");
16944
17162
  sendConfig(payload, msg, btn, function (j, info) {
16945
17163
  // 追加できたら入力欄を空にする(同じものを二度押ししないように)
@@ -17754,6 +17972,8 @@ ${story_1.splitByNameLinks.toString()}
17754
17972
  // 🔴 **registeredNow も fleet より先に読む。** まとめ表をまだ持たない案件
17755
17973
  // (=立ち上げ直後=この仕組みが一番効く案件)は下で throw するので、
17756
17974
  // ここより後に置くと**その案件だけ素通り**する
17975
+ // 追加の確認の栓と確認済みの一覧(#1357)。fleet より先に読む(立ち上げ直後の案件でも追加欄を作り替える)
17976
+ googleInfoApply(j && j.googleConfirm);
17757
17977
  LIVE_REGISTERED = sanitizeRegisteredNow(j && j.registeredNow);
17758
17978
  LIVE_REGISTERED_AT = startedAt;
17759
17979
  LIVE_STATE = sanitizeStateNow(j && j.stateNow);
@@ -17769,6 +17989,7 @@ ${story_1.splitByNameLinks.toString()}
17769
17989
  LIVE_SECRETS = sanitizeSecretsNow(j && j.secretsNow);
17770
17990
  LIVE_CAPS = sanitizeCapabilitiesNow(j && j.capabilitiesNow);
17771
17991
  LIVE_GH_OWNERS = sanitizeGithubOwnersNow(j && j.registeredNow && j.registeredNow.githubOwners);
17992
+ LIVE_GH_APP_SLUG = githubAppSlugOf({ appSlug: j && j.registeredNow && j.registeredNow.githubAppSlug });
17772
17993
  MERGED = null;
17773
17994
  if (!j || !j.fleet) throw new Error("no fleet");
17774
17995
  LIVE_FLEET = normalizeFleet(j.fleet);
@@ -18289,6 +18510,8 @@ ${story_1.splitByNameLinks.toString()}
18289
18510
  // **手順の動画+画像(#1353)。** 動画の実 URL は guide に置かない(SITE.guideAssets.backlog.video・
18290
18511
  // build.ts の GUIDE_VIDEO_SOURCES が注入する)。動画が無い配信先では画像5枚だけで成立する。
18291
18512
  // 画像の並びは動画の流れ(URL をコピー → キーを発行 → 貼る → つながる)に揃える。
18513
+ // 動画+画像は鍵の欄ではなく、モーダルの上(見出しの下・説明文の前)に出す(openCapabilityModal の Backlog の項)
18514
+ guideInModal: true,
18292
18515
  guide: {
18293
18516
  video: {
18294
18517
  alt: "Backlog を Cortex につなぐ手順の動画(約1分30秒)。Backlog の URL のコピー、API キーの発行、操作タブへの貼り付け、接続OK の確認までを画面で見せます。",
@@ -18543,7 +18766,9 @@ ${story_1.splitByNameLinks.toString()}
18543
18766
  // **手順の動画+画像(#547・#1353)。** 文章(guidance)の下に足す。
18544
18767
  // meta.guide を持つ種別(Figma)はカルーセル1つに乗せる。meta.guide も meta.shots も
18545
18768
  // 持たない種別(Backlog)ではこの節は素通りする
18546
- if (meta.guide) {
18769
+ if (meta.guide && meta.guideInModal) {
18770
+ // モーダルの上に出している種別(Backlog)は、ここには重ねない
18771
+ } else if (meta.guide) {
18547
18772
  var carousel = buildGuideCarousel(kind, meta.guide);
18548
18773
  if (carousel) guidanceBox.appendChild(carousel);
18549
18774
  } else if (meta.shots) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.181",
3
+ "version": "0.12.184",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,