@takagaki/cortex-decisions-viewer 0.12.181 → 0.12.185
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/backlog-guide-images.js +26 -16
- package/dist/build.js +10 -0
- package/dist/google-confirm-page.js +369 -0
- package/dist/pending-ops.js +2 -1
- package/dist/render.js +290 -32
- package/package.json +2 -2
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
+
}
|
package/dist/pending-ops.js
CHANGED
|
@@ -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
|
-
|
|
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":
|