@cartanova/qgrid-cli 2.6.7 → 2.6.9

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.
Files changed (87) hide show
  1. package/bundle/dist/application/monit/monit.frame.js +2 -2
  2. package/bundle/dist/application/qgrid/expired-token-reminder.js +45 -15
  3. package/bundle/dist/application/qgrid/oauth.js +1 -1
  4. package/bundle/dist/application/qgrid/qgrid.frame.js +3 -3
  5. package/bundle/dist/application/qgrid/slack-user-map.js +5 -14
  6. package/bundle/dist/application/qgrid/token-death.js +3 -2
  7. package/bundle/dist/application/qgrid/tool-emulation.js +1 -1
  8. package/bundle/dist/application/queries.generated.js +8 -2
  9. package/bundle/dist/application/request-log/request-log.model.js +2 -2
  10. package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
  11. package/bundle/dist/application/setting/setting.constant.js +226 -0
  12. package/bundle/dist/application/setting/setting.model.js +254 -0
  13. package/bundle/dist/application/setting/setting.store.js +66 -0
  14. package/bundle/dist/application/setting/setting.types.js +51 -0
  15. package/bundle/dist/application/sonamu.generated.js +32 -2
  16. package/bundle/dist/application/sonamu.generated.sso.js +12 -2
  17. package/bundle/dist/application/token/token.model.js +1 -1
  18. package/bundle/dist/i18n/en.js +3 -1
  19. package/bundle/dist/i18n/ko.js +3 -1
  20. package/bundle/dist/i18n/sd.generated.js +9 -1
  21. package/bundle/dist/migrations/20260808194708_create__settings.js +24 -0
  22. package/bundle/dist/sonamu.config.js +8 -2
  23. package/bundle/dist/utils/providers/openai/openai-worker-pool-config.js +21 -6
  24. package/bundle/dist/utils/quiet-hours.js +47 -0
  25. package/bundle/dist/utils/slack-notify.js +16 -5
  26. package/bundle/src/application/qgrid/expired-token-reminder.test.ts +147 -2
  27. package/bundle/src/application/qgrid/expired-token-reminder.ts +66 -16
  28. package/bundle/src/application/qgrid/slack-user-map.test.ts +23 -0
  29. package/bundle/src/application/qgrid/slack-user-map.ts +4 -14
  30. package/bundle/src/application/qgrid/token-death.ts +2 -0
  31. package/bundle/src/application/queries.generated.ts +21 -1
  32. package/bundle/src/application/setting/setting.constant.test.ts +109 -0
  33. package/bundle/src/application/setting/setting.constant.ts +250 -0
  34. package/bundle/src/application/setting/setting.entity.json +59 -0
  35. package/bundle/src/application/setting/setting.model.ts +277 -0
  36. package/bundle/src/application/setting/setting.store.test.ts +87 -0
  37. package/bundle/src/application/setting/setting.store.ts +85 -0
  38. package/bundle/src/application/setting/setting.types.ts +53 -0
  39. package/bundle/src/application/sonamu.generated.http +54 -0
  40. package/bundle/src/application/sonamu.generated.sso.ts +21 -0
  41. package/bundle/src/application/sonamu.generated.ts +51 -0
  42. package/bundle/src/i18n/en.ts +2 -0
  43. package/bundle/src/i18n/ko.ts +2 -0
  44. package/bundle/src/i18n/sd.generated.ts +8 -0
  45. package/bundle/src/migrations/20260808194708_create__settings.ts +19 -0
  46. package/bundle/src/sonamu.config.ts +12 -0
  47. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +12 -0
  48. package/bundle/src/utils/providers/openai/openai-worker-pool-config.ts +43 -5
  49. package/bundle/src/utils/quiet-hours.ts +79 -0
  50. package/bundle/src/utils/slack-notify.test.ts +171 -20
  51. package/bundle/src/utils/slack-notify.ts +31 -4
  52. package/bundle/web-dist/client/assets/index-DnnliAzf.js +78 -0
  53. package/bundle/web-dist/client/assets/index-rdbxJinT.css +1 -0
  54. package/bundle/web-dist/client/assets/{logs-CUO9Q0UT.js → logs-5gACO5tJ.js} +1 -1
  55. package/bundle/web-dist/client/assets/{monit-Dy7UtqC2.js → monit-DRCc50GU.js} +1 -1
  56. package/bundle/web-dist/client/assets/plus-p4vtg6LN.js +1 -0
  57. package/bundle/web-dist/client/assets/{routes-B6gfQrph.js → routes-DBdI9Ba1.js} +2 -2
  58. package/bundle/web-dist/client/assets/{sd.generated-DYdwSB2I.js → sd.generated-Bwa7rses.js} +1 -1
  59. package/bundle/web-dist/client/assets/services.generated-BzwTuZRP.js +1 -0
  60. package/bundle/web-dist/client/assets/settings-B62_r1i4.js +1 -0
  61. package/bundle/web-dist/client/assets/{show-Bi0QYUVm.js → show-BCRrSc8-.js} +1 -1
  62. package/bundle/web-dist/client/assets/tokens-CDiXKYGf.js +1 -0
  63. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cx6BlvXM.js → use-oauth-login-flow-Qqg4koSg.js} +1 -1
  64. package/bundle/web-dist/client/index.html +3 -3
  65. package/bundle/web-dist/server/assets/{lazyRouteComponent-BaZaveNc.js → lazyRouteComponent-Ws7I6fL-.js} +2 -1
  66. package/bundle/web-dist/server/assets/{logs-DMOw3SjD.js → logs-C1rxLh8H.js} +4 -4
  67. package/bundle/web-dist/server/assets/{monit-UmcqHroX.js → monit-C8gx9Sar.js} +3 -2
  68. package/bundle/web-dist/server/assets/plus-BISXNqRU.js +19 -0
  69. package/bundle/web-dist/server/assets/react-dom-D7duWOwf.js +462 -0
  70. package/bundle/web-dist/server/assets/{routes-BfoQmYud.js → routes-ZW_Ih5Em.js} +11 -7
  71. package/bundle/web-dist/server/assets/{sd.generated-B0xqnHEo.js → sd.generated-LO-h1_16.js} +13 -461
  72. package/bundle/web-dist/server/assets/{services.generated-DqAKskAS.js → services.generated-CZIChgHV.js} +119 -2
  73. package/bundle/web-dist/server/assets/settings--aoatERH.js +382 -0
  74. package/bundle/web-dist/server/assets/{show-C-oSdzHX.js → show-BY27w9oi.js} +4 -4
  75. package/bundle/web-dist/server/assets/{tokens-Dkd_uXEk.js → tokens-DIObtTuZ.js} +17 -72
  76. package/bundle/web-dist/server/assets/{use-oauth-login-flow-YM1nsd-H.js → use-oauth-login-flow-V3k8PSqQ.js} +2 -2
  77. package/bundle/web-dist/server/entry-server.generated.js +54 -18
  78. package/package.json +1 -1
  79. package/skills/qgrid/references/cli-env-and-server-boot.md +17 -2
  80. package/skills/qgrid/references/openai-codex-runtime.md +1 -1
  81. package/skills/qgrid/references/sonamu-api-web-flow.md +2 -0
  82. package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
  83. package/bundle/web-dist/client/assets/index-B4GlN1IX.css +0 -1
  84. package/bundle/web-dist/client/assets/index-CV_rp3zE.js +0 -78
  85. package/bundle/web-dist/client/assets/services.generated-BsAvotjG.js +0 -1
  86. package/bundle/web-dist/client/assets/tokens-BAuYgBl6.js +0 -1
  87. /package/bundle/web-dist/server/assets/{cost-CSH_t17I.js → cost-DzjfVtUr.js} +0 -0
@@ -1,21 +1,32 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
- import { notifySlack, SLACK_COLOR } from "./slack-notify";
3
+ import { isQuietHours } from "./quiet-hours";
4
+ import { notifySlack, SLACK_COLOR, type SlackNotification } from "./slack-notify";
4
5
 
5
- describe("notifySlack", () => {
6
- const savedToken = process.env.SLACK_BOT_TOKEN;
7
- const savedChannel = process.env.SLACK_CHANNEL_ID;
6
+ const settingMock = vi.fn();
7
+
8
+ const notify = (notification: SlackNotification) => notifySlack(notification, settingMock);
9
+
10
+ /** 한국 시간을 UTC 로 표현한다 — dev0 가 UTC 라 이 변환이 실제 운영 조건이다. */
11
+ function seoul(iso: string): Date {
12
+ return new Date(`${iso}+09:00`);
13
+ }
14
+
15
+ /** 저장값도 env 도 없는 상태 — 조용 시간이 코드 기본값(20~8시, 주말 차단)으로 동작한다. */
16
+ const noSettings = () => undefined;
8
17
 
18
+ /** 평일 업무 시간. 조용 시간에 막히지 않도록 전송 케이스의 기준 시각으로 쓴다. */
19
+ const WORKING_HOURS = seoul("2026-08-05T14:00:00");
20
+
21
+ describe("notifySlack", () => {
9
22
  beforeEach(() => {
10
- process.env.SLACK_BOT_TOKEN = "xoxb-test";
11
- process.env.SLACK_CHANNEL_ID = "C123";
23
+ settingMock.mockImplementation((key: string) =>
24
+ key === "slack.botToken" ? "xoxb-test" : key === "slack.channelId" ? "C123" : undefined,
25
+ );
12
26
  });
13
27
 
14
28
  afterEach(() => {
15
- if (savedToken === undefined) delete process.env.SLACK_BOT_TOKEN;
16
- else process.env.SLACK_BOT_TOKEN = savedToken;
17
- if (savedChannel === undefined) delete process.env.SLACK_CHANNEL_ID;
18
- else process.env.SLACK_CHANNEL_ID = savedChannel;
29
+ settingMock.mockReset();
19
30
  vi.restoreAllMocks();
20
31
  });
21
32
 
@@ -24,11 +35,12 @@ describe("notifySlack", () => {
24
35
  .spyOn(globalThis, "fetch")
25
36
  .mockResolvedValue(new Response(JSON.stringify({ ok: true })));
26
37
 
27
- await notifySlack({
38
+ await notify({
28
39
  title: "토큰 추가",
29
40
  subject: "anthropic/test-token",
30
41
  context: "anthropic · 요청 처리에 사용됩니다",
31
42
  color: SLACK_COLOR.good,
43
+ now: WORKING_HOURS,
32
44
  });
33
45
 
34
46
  expect(fetchMock).toHaveBeenCalledTimes(1);
@@ -62,11 +74,12 @@ describe("notifySlack", () => {
62
74
  .spyOn(globalThis, "fetch")
63
75
  .mockResolvedValue(new Response(JSON.stringify({ ok: true })));
64
76
 
65
- await notifySlack({ title: "제목만" });
77
+ await notify({ title: "제목만", now: WORKING_HOURS });
66
78
 
67
- const body = JSON.parse(
68
- (fetchMock.mock.calls[0]![1] as RequestInit).body as string,
69
- ) as { text?: string; attachments: [{ fallback: string; color?: string; blocks: unknown[] }] };
79
+ const body = JSON.parse((fetchMock.mock.calls[0]![1] as RequestInit).body as string) as {
80
+ text?: string;
81
+ attachments: [{ fallback: string; color?: string; blocks: unknown[] }];
82
+ };
70
83
  // 최상위 text 를 보내지 않아야 blocks 렌더 화면에서 제목이 중복되지 않는다.
71
84
  expect(body.text).toBeUndefined();
72
85
  expect(body.attachments[0].fallback).toBe("제목만");
@@ -74,11 +87,11 @@ describe("notifySlack", () => {
74
87
  expect(body.attachments[0].blocks).toHaveLength(1);
75
88
  });
76
89
 
77
- it("skips the request entirely when env is not configured", async () => {
78
- delete process.env.SLACK_BOT_TOKEN;
90
+ it("skips the request entirely when Slack is not configured", async () => {
91
+ settingMock.mockReturnValue(undefined);
79
92
  const fetchMock = vi.spyOn(globalThis, "fetch");
80
93
 
81
- await expect(notifySlack({ title: "hello" })).resolves.toBeUndefined();
94
+ await expect(notify({ title: "hello", now: WORKING_HOURS })).resolves.toBeUndefined();
82
95
 
83
96
  expect(fetchMock).not.toHaveBeenCalled();
84
97
  });
@@ -88,12 +101,150 @@ describe("notifySlack", () => {
88
101
  new Response(JSON.stringify({ ok: false, error: "not_in_channel" })),
89
102
  );
90
103
 
91
- await expect(notifySlack({ title: "hello" })).resolves.toBeUndefined();
104
+ await expect(notify({ title: "hello", now: WORKING_HOURS })).resolves.toBeUndefined();
92
105
  });
93
106
 
94
107
  it("swallows network and timeout failures", async () => {
95
108
  vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("timed out"));
96
109
 
97
- await expect(notifySlack({ title: "hello" })).resolves.toBeUndefined();
110
+ await expect(notify({ title: "hello", now: WORKING_HOURS })).resolves.toBeUndefined();
111
+ });
112
+
113
+ it("조용 시간에는 보통 알림을 보내지 않는다", async () => {
114
+ const fetchMock = vi.spyOn(globalThis, "fetch");
115
+
116
+ await notify({ title: "세션 만료", now: seoul("2026-08-05T22:00:00") });
117
+
118
+ expect(fetchMock).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("urgent 알림은 조용 시간에도 보낸다", async () => {
122
+ // provider 전체가 죽은 상황은 다음 근무일까지 미룰 수 없다.
123
+ const fetchMock = vi
124
+ .spyOn(globalThis, "fetch")
125
+ .mockResolvedValue(new Response(JSON.stringify({ ok: true })));
126
+
127
+ await notify({
128
+ title: "마지막 토큰 사망",
129
+ urgent: true,
130
+ now: seoul("2026-08-05T22:00:00"),
131
+ });
132
+
133
+ expect(fetchMock).toHaveBeenCalledTimes(1);
134
+ });
135
+
136
+ it("마스터 스위치를 내리면 보통 알림을 보내지 않는다", async () => {
137
+ // 연휴처럼 규칙으로 잡을 수 없는 기간에 관리자가 직접 내리는 스위치.
138
+ settingMock.mockImplementation((key: string) =>
139
+ key === "slack.botToken"
140
+ ? "xoxb-test"
141
+ : key === "slack.channelId"
142
+ ? "C123"
143
+ : key === "slack.enabled"
144
+ ? "false"
145
+ : undefined,
146
+ );
147
+ const fetchMock = vi.spyOn(globalThis, "fetch");
148
+
149
+ await notify({ title: "토큰 추가", now: WORKING_HOURS });
150
+
151
+ expect(fetchMock).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it("마스터 스위치를 내려도 urgent 는 통과한다", async () => {
155
+ // 끈 채로 provider 가 전부 죽으면 연휴 내내 서비스 정지를 아무도 모른다.
156
+ settingMock.mockImplementation((key: string) =>
157
+ key === "slack.botToken"
158
+ ? "xoxb-test"
159
+ : key === "slack.channelId"
160
+ ? "C123"
161
+ : key === "slack.enabled"
162
+ ? "false"
163
+ : undefined,
164
+ );
165
+ const fetchMock = vi
166
+ .spyOn(globalThis, "fetch")
167
+ .mockResolvedValue(new Response(JSON.stringify({ ok: true })));
168
+
169
+ await notify({ title: "마지막 토큰 사망", urgent: true, now: WORKING_HOURS });
170
+
171
+ expect(fetchMock).toHaveBeenCalledTimes(1);
172
+ });
173
+ });
174
+
175
+ describe("isQuietHours 설정", () => {
176
+
177
+ /** 설정 조회를 고정해 실행 환경의 env 에 기대지 않게 한다. */
178
+ const withSettings = (values: Record<string, string>) => (key: string) => values[key];
179
+
180
+ it("조용 시간 구간을 설정으로 바꾼다", () => {
181
+ const read = withSettings({ "slack.quietFromHour": "22", "slack.quietUntilHour": "6" });
182
+ // 21시는 기본값(20시 시작)이면 조용하지만 22시 시작으로 바꾸면 알린다.
183
+ expect(isQuietHours(seoul("2026-08-05T21:00:00"), read)).toBe(false);
184
+ expect(isQuietHours(seoul("2026-08-05T22:00:00"), read)).toBe(true);
185
+ expect(isQuietHours(seoul("2026-08-06T05:59:00"), read)).toBe(true);
186
+ expect(isQuietHours(seoul("2026-08-06T06:00:00"), read)).toBe(false);
187
+ });
188
+
189
+ it("주말 알림을 켜면 주말에도 시간 규칙만 본다", () => {
190
+ const read = withSettings({ "slack.notifyOnWeekends": "true" });
191
+ // 2026-08-08 토요일
192
+ expect(isQuietHours(seoul("2026-08-08T10:00:00"), read)).toBe(false);
193
+ expect(isQuietHours(seoul("2026-08-08T22:00:00"), read)).toBe(true);
194
+ });
195
+
196
+ it("시작과 종료가 같으면 조용 시간을 두지 않는다", () => {
197
+ const read = withSettings({ "slack.quietFromHour": "8", "slack.quietUntilHour": "8" });
198
+ expect(isQuietHours(seoul("2026-08-05T03:00:00"), read)).toBe(false);
199
+ expect(isQuietHours(seoul("2026-08-05T23:00:00"), read)).toBe(false);
200
+ });
201
+
202
+ it("시작이 종료보다 빠르면 같은 날 구간으로 읽는다", () => {
203
+ // 야간 근무처럼 낮에 조용하고 싶은 경우를 막지 않는다.
204
+ const read = withSettings({ "slack.quietFromHour": "9", "slack.quietUntilHour": "18" });
205
+ expect(isQuietHours(seoul("2026-08-05T12:00:00"), read)).toBe(true);
206
+ expect(isQuietHours(seoul("2026-08-05T20:00:00"), read)).toBe(false);
207
+ });
208
+
209
+ it("범위를 벗어난 값은 기본 구간으로 떨어진다", () => {
210
+ // 저장을 막고 있지만 env 로도 들어올 수 있어 런타임에서 한 번 더 지킨다.
211
+ const read = withSettings({ "slack.quietFromHour": "99", "slack.quietUntilHour": "-1" });
212
+ expect(isQuietHours(seoul("2026-08-05T21:00:00"), read)).toBe(true);
213
+ expect(isQuietHours(seoul("2026-08-05T14:00:00"), read)).toBe(false);
214
+ });
215
+ });
216
+
217
+ describe("isQuietHours", () => {
218
+ it("평일 업무 시간에는 알린다", () => {
219
+ // 2026-08-05 는 수요일
220
+ expect(isQuietHours(seoul("2026-08-05T09:00:00"), noSettings)).toBe(false);
221
+ expect(isQuietHours(seoul("2026-08-05T19:59:00"), noSettings)).toBe(false);
222
+ });
223
+
224
+ it("평일 20시부터 다음날 8시까지 조용하다", () => {
225
+ expect(isQuietHours(seoul("2026-08-05T20:00:00"), noSettings)).toBe(true);
226
+ expect(isQuietHours(seoul("2026-08-06T00:00:00"), noSettings)).toBe(true);
227
+ expect(isQuietHours(seoul("2026-08-06T07:59:00"), noSettings)).toBe(true);
228
+ expect(isQuietHours(seoul("2026-08-06T08:00:00"), noSettings)).toBe(false);
229
+ });
230
+
231
+ it("주말은 시간과 무관하게 조용하다", () => {
232
+ // 2026-08-08 토요일, 08-09 일요일
233
+ expect(isQuietHours(seoul("2026-08-08T10:00:00"), noSettings)).toBe(true);
234
+ expect(isQuietHours(seoul("2026-08-09T12:00:00"), noSettings)).toBe(true);
235
+ });
236
+
237
+ it("월요일 8시에 다시 알리기 시작한다", () => {
238
+ expect(isQuietHours(seoul("2026-08-10T07:59:00"), noSettings)).toBe(true);
239
+ expect(isQuietHours(seoul("2026-08-10T08:00:00"), noSettings)).toBe(false);
240
+ });
241
+
242
+ it("서버가 UTC 여도 한국 시간으로 판정한다", () => {
243
+ // UTC 23시 = 한국 다음날 08시 → 업무 시간
244
+ expect(isQuietHours(new Date("2026-08-04T23:00:00Z"), noSettings)).toBe(false);
245
+ // UTC 12시 = 한국 21시 → 조용 시간
246
+ expect(isQuietHours(new Date("2026-08-05T12:00:00Z"), noSettings)).toBe(true);
247
+ // UTC 금요일 22시 = 한국 토요일 07시 → 주말
248
+ expect(isQuietHours(new Date("2026-08-07T22:00:00Z"), noSettings)).toBe(true);
98
249
  });
99
250
  });
@@ -2,10 +2,14 @@
2
2
  * Slack 알림 (chat.postMessage).
3
3
  *
4
4
  * fail-open: env 미설정이면 조용히 no-op 이고, 전송 실패는 warn 로그로만 남긴다.
5
+ * 조용 시간(주말·20~8시)에는 `urgent` 가 아닌 알림을 보내지 않는다.
5
6
  * 알림 경로가 토큰 라우팅이나 오류 전파를 지연·차단해서는 안 된다.
6
7
  */
7
8
  import { getLogger } from "@logtape/logtape";
8
9
 
10
+ import { getSetting } from "../application/setting/setting.store";
11
+ import { isQuietHours } from "./quiet-hours";
12
+
9
13
  const logger = getLogger(["qgrid", "slack"]);
10
14
 
11
15
  const SLACK_POST_MESSAGE_URL = "https://slack.com/api/chat.postMessage";
@@ -25,17 +29,40 @@ export type SlackNotification = {
25
29
  /** 제목 아래 작은 글씨로 내려가는 부가 정보 — 읽지 않아도 되는 것들. */
26
30
  context?: string;
27
31
  color?: (typeof SLACK_COLOR)[keyof typeof SLACK_COLOR];
32
+ /**
33
+ * 조용 시간(주말·20~8시)에도 보낸다. 서비스가 멈춘 상태처럼 지금 알지 않으면 손해가
34
+ * 커지는 사건에만 쓴다 — 남용하면 조용 시간 자체가 무의미해진다.
35
+ */
36
+ urgent?: boolean;
37
+ /** 조용 시간 판정 기준 시각. 테스트에서 시계를 고정할 때만 넘긴다. */
38
+ now?: Date;
28
39
  };
29
40
 
30
- export async function notifySlack(notification: SlackNotification): Promise<void> {
31
- const botToken = process.env.SLACK_BOT_TOKEN;
32
- const channel = process.env.SLACK_CHANNEL_ID;
33
- const { title, subject, context, color } = notification;
41
+ export async function notifySlack(
42
+ notification: SlackNotification,
43
+ readSetting: typeof getSetting = getSetting,
44
+ ): Promise<void> {
45
+ const botToken = readSetting("slack.botToken", "SLACK_BOT_TOKEN");
46
+ const channel = readSetting("slack.channelId", "SLACK_CHANNEL_ID");
47
+ const { title, subject, context, color, urgent, now } = notification;
34
48
  if (!botToken || !channel) {
35
49
  logger.debug(`slack not configured, skipping notification: ${title} ${subject ?? ""}`);
36
50
  return;
37
51
  }
38
52
 
53
+ // 연휴처럼 규칙으로 잡을 수 없는 기간에 관리자가 내리는 스위치. urgent 는 통과시킨다 —
54
+ // 끈 상태로 provider 가 전부 죽으면 연휴 내내 서비스가 멈춘 줄 아무도 모른다.
55
+ if (!urgent && readSetting("slack.enabled", "SLACK_ENABLED") === "false") {
56
+ logger.debug(`slack disabled, skipping notification: ${title} ${subject ?? ""}`);
57
+ return;
58
+ }
59
+
60
+ // 만료 알림은 주기적으로 반복되므로 여기서 버려도 다음 근무 시간 첫 주기에 다시 온다.
61
+ if (!urgent && isQuietHours(now ?? new Date(), readSetting)) {
62
+ logger.debug(`quiet hours, skipping notification: ${title} ${subject ?? ""}`);
63
+ return;
64
+ }
65
+
39
66
  const blocks: unknown[] = [
40
67
  {
41
68
  type: "section",