@407dev/cms-astro 0.1.0
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/README.md +69 -0
- package/dist/chunk-2TKIQIOE.js +118 -0
- package/dist/chunk-2TKIQIOE.js.map +1 -0
- package/dist/chunk-53OQ5RXJ.js +912 -0
- package/dist/chunk-53OQ5RXJ.js.map +1 -0
- package/dist/chunk-56OMNQLH.js +1013 -0
- package/dist/chunk-56OMNQLH.js.map +1 -0
- package/dist/chunk-5AOOMQ7K.js +1031 -0
- package/dist/chunk-5AOOMQ7K.js.map +1 -0
- package/dist/chunk-5XRUCFDC.js +743 -0
- package/dist/chunk-5XRUCFDC.js.map +1 -0
- package/dist/chunk-62ZHCOLZ.js +25 -0
- package/dist/chunk-62ZHCOLZ.js.map +1 -0
- package/dist/chunk-6AZLRSYA.js +126 -0
- package/dist/chunk-6AZLRSYA.js.map +1 -0
- package/dist/chunk-6L3ZTCFB.js +14 -0
- package/dist/chunk-6L3ZTCFB.js.map +1 -0
- package/dist/chunk-6O7ZNNDT.js +697 -0
- package/dist/chunk-6O7ZNNDT.js.map +1 -0
- package/dist/chunk-6PQIXTMK.js +697 -0
- package/dist/chunk-6PQIXTMK.js.map +1 -0
- package/dist/chunk-7AW6M7QW.js +1013 -0
- package/dist/chunk-7AW6M7QW.js.map +1 -0
- package/dist/chunk-7E3XJLKX.js +73 -0
- package/dist/chunk-7E3XJLKX.js.map +1 -0
- package/dist/chunk-7LM54OTA.js +1031 -0
- package/dist/chunk-7LM54OTA.js.map +1 -0
- package/dist/chunk-7LMUFCWX.js +126 -0
- package/dist/chunk-7LMUFCWX.js.map +1 -0
- package/dist/chunk-A3OGL4OX.js +706 -0
- package/dist/chunk-A3OGL4OX.js.map +1 -0
- package/dist/chunk-AO4UAWOV.js +1030 -0
- package/dist/chunk-AO4UAWOV.js.map +1 -0
- package/dist/chunk-B4YYLS7Y.js +14 -0
- package/dist/chunk-B4YYLS7Y.js.map +1 -0
- package/dist/chunk-BDFVGXM2.js +697 -0
- package/dist/chunk-BDFVGXM2.js.map +1 -0
- package/dist/chunk-CXM7NZVX.js +1031 -0
- package/dist/chunk-CXM7NZVX.js.map +1 -0
- package/dist/chunk-DMLOBKYE.js +96 -0
- package/dist/chunk-DMLOBKYE.js.map +1 -0
- package/dist/chunk-DXJSYGS6.js +743 -0
- package/dist/chunk-DXJSYGS6.js.map +1 -0
- package/dist/chunk-EFUSAQZM.js +72 -0
- package/dist/chunk-EFUSAQZM.js.map +1 -0
- package/dist/chunk-F7KDGPBN.js +1013 -0
- package/dist/chunk-F7KDGPBN.js.map +1 -0
- package/dist/chunk-FBSQT45X.js +126 -0
- package/dist/chunk-FBSQT45X.js.map +1 -0
- package/dist/chunk-FEPDOMVH.js +697 -0
- package/dist/chunk-FEPDOMVH.js.map +1 -0
- package/dist/chunk-FQDOL2QW.js +25 -0
- package/dist/chunk-FQDOL2QW.js.map +1 -0
- package/dist/chunk-GB3LGQJM.js +109 -0
- package/dist/chunk-GB3LGQJM.js.map +1 -0
- package/dist/chunk-HCNP6R3P.js +736 -0
- package/dist/chunk-HCNP6R3P.js.map +1 -0
- package/dist/chunk-J6WEJDRI.js +743 -0
- package/dist/chunk-J6WEJDRI.js.map +1 -0
- package/dist/chunk-JHEAPNJ2.js +117 -0
- package/dist/chunk-JHEAPNJ2.js.map +1 -0
- package/dist/chunk-JOJDJ6GW.js +25 -0
- package/dist/chunk-JOJDJ6GW.js.map +1 -0
- package/dist/chunk-KTDV6WYB.js +202 -0
- package/dist/chunk-KTDV6WYB.js.map +1 -0
- package/dist/chunk-KZWJBKPC.js +734 -0
- package/dist/chunk-KZWJBKPC.js.map +1 -0
- package/dist/chunk-LEAQBGL6.js +109 -0
- package/dist/chunk-LEAQBGL6.js.map +1 -0
- package/dist/chunk-LGZJL2Q7.js +697 -0
- package/dist/chunk-LGZJL2Q7.js.map +1 -0
- package/dist/chunk-LQPBU46R.js +109 -0
- package/dist/chunk-LQPBU46R.js.map +1 -0
- package/dist/chunk-M55NNUTY.js +1013 -0
- package/dist/chunk-M55NNUTY.js.map +1 -0
- package/dist/chunk-M6NCYCC3.js +1013 -0
- package/dist/chunk-M6NCYCC3.js.map +1 -0
- package/dist/chunk-MGN7OZAU.js +109 -0
- package/dist/chunk-MGN7OZAU.js.map +1 -0
- package/dist/chunk-MGYZOX4K.js +1013 -0
- package/dist/chunk-MGYZOX4K.js.map +1 -0
- package/dist/chunk-MY4IEKJS.js +1031 -0
- package/dist/chunk-MY4IEKJS.js.map +1 -0
- package/dist/chunk-NCQRGAB4.js +705 -0
- package/dist/chunk-NCQRGAB4.js.map +1 -0
- package/dist/chunk-NI4NHXI4.js +697 -0
- package/dist/chunk-NI4NHXI4.js.map +1 -0
- package/dist/chunk-NPI5N7AU.js +14 -0
- package/dist/chunk-NPI5N7AU.js.map +1 -0
- package/dist/chunk-QN4CPMJF.js +126 -0
- package/dist/chunk-QN4CPMJF.js.map +1 -0
- package/dist/chunk-QYUVAKMC.js +1013 -0
- package/dist/chunk-QYUVAKMC.js.map +1 -0
- package/dist/chunk-RAAT6PDT.js +25 -0
- package/dist/chunk-RAAT6PDT.js.map +1 -0
- package/dist/chunk-RDPO5VPS.js +1013 -0
- package/dist/chunk-RDPO5VPS.js.map +1 -0
- package/dist/chunk-RQHIW2PF.js +25 -0
- package/dist/chunk-RQHIW2PF.js.map +1 -0
- package/dist/chunk-RREVHA52.js +722 -0
- package/dist/chunk-RREVHA52.js.map +1 -0
- package/dist/chunk-S7PT4PZN.js +25 -0
- package/dist/chunk-S7PT4PZN.js.map +1 -0
- package/dist/chunk-SUXV5M32.js +109 -0
- package/dist/chunk-SUXV5M32.js.map +1 -0
- package/dist/chunk-SZEORUY6.js +696 -0
- package/dist/chunk-SZEORUY6.js.map +1 -0
- package/dist/chunk-T7NGGWGY.js +25 -0
- package/dist/chunk-T7NGGWGY.js.map +1 -0
- package/dist/chunk-UEVWP5VE.js +696 -0
- package/dist/chunk-UEVWP5VE.js.map +1 -0
- package/dist/chunk-UJNKNP7U.js +748 -0
- package/dist/chunk-UJNKNP7U.js.map +1 -0
- package/dist/chunk-V4XVFWFI.js +1013 -0
- package/dist/chunk-V4XVFWFI.js.map +1 -0
- package/dist/chunk-VJU2CIKZ.js +729 -0
- package/dist/chunk-VJU2CIKZ.js.map +1 -0
- package/dist/chunk-VRQUP5L3.js +202 -0
- package/dist/chunk-VRQUP5L3.js.map +1 -0
- package/dist/chunk-WBJ5E7CL.js +742 -0
- package/dist/chunk-WBJ5E7CL.js.map +1 -0
- package/dist/chunk-XJPVYMZM.js +24 -0
- package/dist/chunk-XJPVYMZM.js.map +1 -0
- package/dist/chunk-Y3Q65C7R.js +14 -0
- package/dist/chunk-Y3Q65C7R.js.map +1 -0
- package/dist/chunk-YHEGFPTR.js +1030 -0
- package/dist/chunk-YHEGFPTR.js.map +1 -0
- package/dist/chunk-ZDLBN46C.js +697 -0
- package/dist/chunk-ZDLBN46C.js.map +1 -0
- package/dist/dev/index.d.ts +122 -0
- package/dist/dev/index.js +662 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/fields/collections.d.ts +18 -0
- package/dist/fields/collections.js +37 -0
- package/dist/fields/collections.js.map +1 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +956 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +8 -0
- package/dist/middleware/index.js +13 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/preview-BEkLMD6N.d.ts +23 -0
- package/dist/preview-D9yewK8x.d.ts +24 -0
- package/dist/preview-DJEA4fj7.d.ts +23 -0
- package/dist/preview-DeD1Jn7U.d.ts +23 -0
- package/dist/toolbar/app.d.ts +28 -0
- package/dist/toolbar/app.js +1176 -0
- package/dist/toolbar/app.js.map +1 -0
- package/dist/types-15jfCQKb.d.ts +141 -0
- package/dist/types-BREdL9Gx.d.ts +140 -0
- package/dist/types-C6sUZNa2.d.ts +104 -0
- package/dist/types-CEYS5Fzc.d.ts +112 -0
- package/dist/types-DpeHpobR.d.ts +104 -0
- package/dist/types-NaKJe6Pg.d.ts +140 -0
- package/package.json +90 -0
- package/src/CmsHead.astro +25 -0
- package/src/CmsImage.astro +62 -0
- package/src/CmsVideo.astro +185 -0
- package/src/RichText.astro +32 -0
- package/src/client.test.ts +227 -0
- package/src/client.ts +164 -0
- package/src/dev/config.test.ts +258 -0
- package/src/dev/config.ts +319 -0
- package/src/dev/index.ts +5 -0
- package/src/dev/preview-token.ts +39 -0
- package/src/dev/routes.test.ts +354 -0
- package/src/dev/routes.ts +245 -0
- package/src/dev/token.test.ts +280 -0
- package/src/dev/token.ts +201 -0
- package/src/dev/verifier.test.ts +63 -0
- package/src/dev/verifier.ts +161 -0
- package/src/fields/attrs.ts +24 -0
- package/src/fields/collection.ts +72 -0
- package/src/fields/collections.ts +35 -0
- package/src/fields/entryScope.test.ts +62 -0
- package/src/fields/entryScope.ts +240 -0
- package/src/fields/group.ts +51 -0
- package/src/fields/helpers.test.ts +51 -0
- package/src/fields/helpers.ts +214 -0
- package/src/fields/index.ts +46 -0
- package/src/fields/scope.ts +21 -0
- package/src/fields/types.ts +119 -0
- package/src/head.test.ts +74 -0
- package/src/head.ts +123 -0
- package/src/image.test.ts +42 -0
- package/src/image.ts +64 -0
- package/src/index.test.ts +76 -0
- package/src/index.ts +13 -0
- package/src/integration.ts +218 -0
- package/src/jwt.ts +131 -0
- package/src/middleware/index.ts +4 -0
- package/src/middleware/preview.test.ts +184 -0
- package/src/middleware/preview.ts +206 -0
- package/src/preview/bridge-script.test.ts +683 -0
- package/src/preview/bridge-script.ts +763 -0
- package/src/preview/origins.ts +29 -0
- package/src/preview.ts +49 -0
- package/src/richtext/RichTextNode.astro +130 -0
- package/src/richtext/render.ts +248 -0
- package/src/richtext.test.ts +139 -0
- package/src/runtime/bundle.ts +28 -0
- package/src/runtime/store.ts +130 -0
- package/src/siteFiles.test.ts +93 -0
- package/src/siteFiles.ts +111 -0
- package/src/toolbar/app.test.ts +191 -0
- package/src/toolbar/app.ts +1091 -0
- package/src/toolbar/icon.ts +5 -0
- package/src/toolbar/widgets.test.ts +153 -0
- package/src/toolbar/widgets.ts +365 -0
- package/src/types.test.ts +154 -0
- package/src/types.ts +163 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { signDevToken, validateDevToken } from './token';
|
|
3
|
+
|
|
4
|
+
function createMockJwt(payload: Record<string, unknown>): string {
|
|
5
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
6
|
+
const encode = (obj: unknown) =>
|
|
7
|
+
Buffer.from(JSON.stringify(obj))
|
|
8
|
+
.toString('base64')
|
|
9
|
+
.replace(/\+/g, '-')
|
|
10
|
+
.replace(/\//g, '_')
|
|
11
|
+
.replace(/=+$/, '');
|
|
12
|
+
return `${encode(header)}.${encode(payload)}.mocksignature`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('validateDevToken credential gate', () => {
|
|
16
|
+
it('refuses empty, null, or non-string tokens', () => {
|
|
17
|
+
expect(validateDevToken('')).toEqual({
|
|
18
|
+
ok: false,
|
|
19
|
+
reason: 'CMS_DEV_TOKEN is missing or empty',
|
|
20
|
+
});
|
|
21
|
+
expect(validateDevToken(null)).toEqual({
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: 'CMS_DEV_TOKEN is missing or empty',
|
|
24
|
+
});
|
|
25
|
+
expect(validateDevToken(undefined)).toEqual({
|
|
26
|
+
ok: false,
|
|
27
|
+
reason: 'CMS_DEV_TOKEN is missing or empty',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('refuses invalid JWT formats', () => {
|
|
32
|
+
expect(validateDevToken('not-a-jwt')).toEqual({
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: 'CMS_DEV_TOKEN is not a valid 3-part JWT',
|
|
35
|
+
});
|
|
36
|
+
expect(validateDevToken('header.payload')).toEqual({
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: 'CMS_DEV_TOKEN is not a valid 3-part JWT',
|
|
39
|
+
});
|
|
40
|
+
expect(validateDevToken('header.%%%badbase64%%%.sig')).toEqual({
|
|
41
|
+
ok: false,
|
|
42
|
+
reason: 'CMS_DEV_TOKEN payload is not decodable',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('refuses service_role tokens', () => {
|
|
47
|
+
const serviceToken = createMockJwt({
|
|
48
|
+
role: 'service_role',
|
|
49
|
+
sub: 'service-key-id',
|
|
50
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
51
|
+
});
|
|
52
|
+
const result = validateDevToken(serviceToken);
|
|
53
|
+
expect(result.ok).toBe(false);
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
expect(result.reason).toContain('service_role');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('refuses anonymous or publishable tokens', () => {
|
|
60
|
+
const anonToken = createMockJwt({
|
|
61
|
+
role: 'anon',
|
|
62
|
+
sub: 'anon-key-id',
|
|
63
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
64
|
+
});
|
|
65
|
+
const result = validateDevToken(anonToken);
|
|
66
|
+
expect(result.ok).toBe(false);
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
expect(result.reason).toContain("invalid role 'anon'");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('refuses expired authenticated tokens', () => {
|
|
73
|
+
const expiredToken = createMockJwt({
|
|
74
|
+
role: 'authenticated',
|
|
75
|
+
sub: 'user-expired',
|
|
76
|
+
exp: Math.floor(Date.now() / 1000) - 600,
|
|
77
|
+
});
|
|
78
|
+
const result = validateDevToken(expiredToken);
|
|
79
|
+
expect(result.ok).toBe(false);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
expect(result.reason).toContain('expired');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('accepts valid authenticated JWT and extracts identity', () => {
|
|
86
|
+
const validExp = Math.floor(Date.now() / 1000) + 7200;
|
|
87
|
+
const validToken = createMockJwt({
|
|
88
|
+
role: 'authenticated',
|
|
89
|
+
sub: 'usr-12345',
|
|
90
|
+
email: 'builder@example.com',
|
|
91
|
+
exp: validExp,
|
|
92
|
+
app_metadata: {
|
|
93
|
+
workspace_id: 'ws-1',
|
|
94
|
+
role: 'member',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = validateDevToken(validToken);
|
|
99
|
+
expect(result.ok).toBe(true);
|
|
100
|
+
if (result.ok) {
|
|
101
|
+
expect(result.identity.sub).toBe('usr-12345');
|
|
102
|
+
expect(result.identity.email).toBe('builder@example.com');
|
|
103
|
+
expect(result.identity.exp).toBe(validExp);
|
|
104
|
+
expect(result.identity.workspaceRole).toBe('member');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('refuses tokens with workspace owner scope without allowElevated', () => {
|
|
109
|
+
const ownerToken = createMockJwt({
|
|
110
|
+
role: 'authenticated',
|
|
111
|
+
sub: 'usr-owner',
|
|
112
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
113
|
+
app_metadata: {
|
|
114
|
+
workspace_id: 'ws-1',
|
|
115
|
+
role: 'owner',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = validateDevToken(ownerToken);
|
|
120
|
+
expect(result.ok).toBe(false);
|
|
121
|
+
if (!result.ok) {
|
|
122
|
+
expect(result.reason).toContain("workspace 'owner' scope");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const explicitFalseResult = validateDevToken(ownerToken, { allowElevated: false });
|
|
126
|
+
expect(explicitFalseResult.ok).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('accepts tokens with workspace owner scope when allowElevated is true', () => {
|
|
130
|
+
const ownerToken = createMockJwt({
|
|
131
|
+
role: 'authenticated',
|
|
132
|
+
sub: 'usr-owner',
|
|
133
|
+
email: 'owner@example.com',
|
|
134
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
135
|
+
app_metadata: {
|
|
136
|
+
workspace_id: 'ws-1',
|
|
137
|
+
role: 'owner',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = validateDevToken(ownerToken, { allowElevated: true });
|
|
142
|
+
expect(result.ok).toBe(true);
|
|
143
|
+
if (result.ok) {
|
|
144
|
+
expect(result.identity.sub).toBe('usr-owner');
|
|
145
|
+
expect(result.identity.email).toBe('owner@example.com');
|
|
146
|
+
expect(result.identity.workspaceRole).toBe('owner');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Also supports boolean parameter
|
|
150
|
+
const boolResult = validateDevToken(ownerToken, true);
|
|
151
|
+
expect(boolResult.ok).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('refuses tokens with workspace editor scope in the per-workspace claim map without allowElevated', () => {
|
|
155
|
+
const editorToken = createMockJwt({
|
|
156
|
+
role: 'authenticated',
|
|
157
|
+
sub: 'usr-editor',
|
|
158
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
159
|
+
app_metadata: {
|
|
160
|
+
workspaces: {
|
|
161
|
+
'ws-1': { role: 'viewer' },
|
|
162
|
+
'ws-2': { role: 'editor' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = validateDevToken(editorToken);
|
|
168
|
+
expect(result.ok).toBe(false);
|
|
169
|
+
if (!result.ok) {
|
|
170
|
+
expect(result.reason).toContain("workspace 'editor' scope");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('accepts tokens with workspace editor scope when allowElevated is true', () => {
|
|
175
|
+
const editorToken = createMockJwt({
|
|
176
|
+
role: 'authenticated',
|
|
177
|
+
sub: 'usr-editor',
|
|
178
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
179
|
+
app_metadata: {
|
|
180
|
+
workspaces: {
|
|
181
|
+
'ws-1': { role: 'editor' },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const result = validateDevToken(editorToken, { allowElevated: true });
|
|
187
|
+
expect(result.ok).toBe(true);
|
|
188
|
+
if (result.ok) {
|
|
189
|
+
expect(result.identity.workspaceRole).toBe('editor');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('does not treat retired admin or developer roles as elevated', () => {
|
|
194
|
+
const retiredAdminToken = createMockJwt({
|
|
195
|
+
role: 'authenticated',
|
|
196
|
+
sub: 'usr-admin',
|
|
197
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
198
|
+
app_metadata: {
|
|
199
|
+
workspace_id: 'ws-1',
|
|
200
|
+
role: 'admin',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const resultAdmin = validateDevToken(retiredAdminToken);
|
|
205
|
+
expect(resultAdmin.ok).toBe(true);
|
|
206
|
+
|
|
207
|
+
const retiredDevToken = createMockJwt({
|
|
208
|
+
role: 'authenticated',
|
|
209
|
+
sub: 'usr-dev',
|
|
210
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
211
|
+
app_metadata: {
|
|
212
|
+
workspaces: {
|
|
213
|
+
'ws-1': { role: 'developer' },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const resultDev = validateDevToken(retiredDevToken);
|
|
219
|
+
expect(resultDev.ok).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('accepts a workspace-viewer-scoped token via the per-workspace claim map', () => {
|
|
223
|
+
const viewerToken = createMockJwt({
|
|
224
|
+
role: 'authenticated',
|
|
225
|
+
sub: 'usr-viewer',
|
|
226
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
227
|
+
app_metadata: {
|
|
228
|
+
workspaces: {
|
|
229
|
+
'ws-1': { role: 'viewer' },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const result = validateDevToken(viewerToken);
|
|
235
|
+
expect(result.ok).toBe(true);
|
|
236
|
+
if (result.ok) {
|
|
237
|
+
expect(result.identity.workspaceRole).toBe('viewer');
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('accepts a workspace-member-scoped token via the per-workspace claim map', () => {
|
|
242
|
+
const memberToken = createMockJwt({
|
|
243
|
+
role: 'authenticated',
|
|
244
|
+
sub: 'usr-member',
|
|
245
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
246
|
+
app_metadata: {
|
|
247
|
+
workspaces: {
|
|
248
|
+
'ws-1': { role: 'member' },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const result = validateDevToken(memberToken);
|
|
254
|
+
expect(result.ok).toBe(true);
|
|
255
|
+
if (result.ok) {
|
|
256
|
+
expect(result.identity.workspaceRole).toBe('member');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('signDevToken', () => {
|
|
261
|
+
it('generates a valid, signed 3-part JWT acceptable by validateDevToken', () => {
|
|
262
|
+
const secret = 'super-secret-jwt-token-with-at-least-32-characters-long';
|
|
263
|
+
const token = signDevToken(secret, {
|
|
264
|
+
siteId: 'site-custom',
|
|
265
|
+
workspaceId: 'ws-custom',
|
|
266
|
+
userId: 'usr-custom',
|
|
267
|
+
});
|
|
268
|
+
expect(typeof token).toBe('string');
|
|
269
|
+
expect(token.split('.').length).toBe(3);
|
|
270
|
+
|
|
271
|
+
const result = validateDevToken(token);
|
|
272
|
+
expect(result.ok).toBe(true);
|
|
273
|
+
if (result.ok) {
|
|
274
|
+
expect(result.identity.sub).toBe('usr-custom');
|
|
275
|
+
expect(result.identity.email).toBe('dev@example.com');
|
|
276
|
+
expect(result.identity.workspaceRole).toBe('viewer');
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
package/src/dev/token.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export interface DevTokenIdentity {
|
|
4
|
+
sub: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
exp?: number;
|
|
7
|
+
workspaceRole?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ValidateDevTokenResult =
|
|
11
|
+
| { ok: true; identity: DevTokenIdentity }
|
|
12
|
+
| { ok: false; reason: string };
|
|
13
|
+
|
|
14
|
+
// Matches is_workspace_admin_or_dev()'s RLS bypass roles (owner | editor); CMS_DEV_TOKEN must not carry them.
|
|
15
|
+
const WORKSPACE_ELEVATED_ROLES = new Set(['owner', 'editor']);
|
|
16
|
+
|
|
17
|
+
// Reads both claim shapes RLS accepts: the flat single-workspace shortcut (app_metadata.workspace_id + app_metadata.role) and the per-workspace map (app_metadata.workspaces[id].role).
|
|
18
|
+
function collectWorkspaceRoles(payload: Record<string, unknown>): string[] {
|
|
19
|
+
const appMetadata = payload.app_metadata as Record<string, unknown> | undefined;
|
|
20
|
+
if (!appMetadata || typeof appMetadata !== 'object') return [];
|
|
21
|
+
|
|
22
|
+
const roles: string[] = [];
|
|
23
|
+
|
|
24
|
+
if (typeof appMetadata.workspace_id === 'string' && typeof appMetadata.role === 'string') {
|
|
25
|
+
roles.push(appMetadata.role);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const workspaces = appMetadata.workspaces;
|
|
29
|
+
if (workspaces && typeof workspaces === 'object') {
|
|
30
|
+
for (const entry of Object.values(workspaces as Record<string, unknown>)) {
|
|
31
|
+
const role = (entry as Record<string, unknown> | null)?.role;
|
|
32
|
+
if (typeof role === 'string') roles.push(role);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return roles;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function base64UrlDecode(str: string): string {
|
|
40
|
+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
41
|
+
while (base64.length % 4 !== 0) {
|
|
42
|
+
base64 += '=';
|
|
43
|
+
}
|
|
44
|
+
return Buffer.from(base64, 'base64').toString('utf-8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ValidateDevTokenOptions {
|
|
48
|
+
allowElevated?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decodes and validates a developer user JWT for the dev toolbar.
|
|
53
|
+
* Rejects non-JWTs, service-role keys, anon keys, and expired tokens.
|
|
54
|
+
*/
|
|
55
|
+
export function validateDevToken(
|
|
56
|
+
token: string | null | undefined,
|
|
57
|
+
options?: ValidateDevTokenOptions | boolean,
|
|
58
|
+
): ValidateDevTokenResult {
|
|
59
|
+
const allowElevated = typeof options === 'boolean' ? options : Boolean(options?.allowElevated);
|
|
60
|
+
|
|
61
|
+
if (!token || typeof token !== 'string' || !token.trim()) {
|
|
62
|
+
return { ok: false, reason: 'CMS_DEV_TOKEN is missing or empty' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parts = token.trim().split('.');
|
|
66
|
+
if (parts.length !== 3) {
|
|
67
|
+
return { ok: false, reason: 'CMS_DEV_TOKEN is not a valid 3-part JWT' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let payload: Record<string, unknown>;
|
|
71
|
+
try {
|
|
72
|
+
const decoded = base64UrlDecode(parts[1]);
|
|
73
|
+
payload = JSON.parse(decoded);
|
|
74
|
+
if (!payload || typeof payload !== 'object') {
|
|
75
|
+
return { ok: false, reason: 'CMS_DEV_TOKEN payload is not a valid JSON object' };
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
return { ok: false, reason: 'CMS_DEV_TOKEN payload is not decodable' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 1. Refuse service_role
|
|
82
|
+
if (payload.role === 'service_role') {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: 'CMS_DEV_TOKEN must not be a service_role key; authenticated user JWT required',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Refuse non-authenticated (e.g. anon / publishable)
|
|
90
|
+
if (payload.role !== 'authenticated') {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
reason: `CMS_DEV_TOKEN has invalid role '${String(payload.role || 'none')}'; expected 'authenticated'`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Refuse expired
|
|
98
|
+
if (typeof payload.exp === 'number' && payload.exp * 1000 <= Date.now()) {
|
|
99
|
+
return { ok: false, reason: 'CMS_DEV_TOKEN has expired' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 4. Refuse workspace scope above 'viewer'/'member' unless explicitly allowed (e.g. CLI session)
|
|
103
|
+
const workspaceRoles = collectWorkspaceRoles(payload);
|
|
104
|
+
if (!allowElevated) {
|
|
105
|
+
const elevatedRole = workspaceRoles.find((role) => WORKSPACE_ELEVATED_ROLES.has(role));
|
|
106
|
+
if (elevatedRole) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: `CMS_DEV_TOKEN grants workspace '${elevatedRole}' scope; only workspace-member tokens are permitted`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sub = typeof payload.sub === 'string' ? payload.sub : '';
|
|
115
|
+
if (!sub) {
|
|
116
|
+
return { ok: false, reason: 'CMS_DEV_TOKEN is missing sub claim' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const email =
|
|
120
|
+
typeof payload.email === 'string'
|
|
121
|
+
? payload.email
|
|
122
|
+
: typeof (payload.user_metadata as Record<string, unknown> | undefined)?.email === 'string'
|
|
123
|
+
? ((payload.user_metadata as Record<string, unknown>).email as string)
|
|
124
|
+
: undefined;
|
|
125
|
+
|
|
126
|
+
const exp = typeof payload.exp === 'number' ? payload.exp : undefined;
|
|
127
|
+
|
|
128
|
+
const workspaceRole = workspaceRoles[0];
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
identity: {
|
|
133
|
+
sub,
|
|
134
|
+
email,
|
|
135
|
+
exp,
|
|
136
|
+
workspaceRole,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SignDevTokenOptions {
|
|
142
|
+
siteId?: string;
|
|
143
|
+
workspaceId?: string;
|
|
144
|
+
userId?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Signs a developer user JWT (with workspace viewer & site editor roles) for local development against local Supabase/CMS API.
|
|
149
|
+
*/
|
|
150
|
+
export function signDevToken(
|
|
151
|
+
secret: string,
|
|
152
|
+
siteIdOrOptions?: string | SignDevTokenOptions,
|
|
153
|
+
workspaceIdParam?: string,
|
|
154
|
+
userIdParam?: string,
|
|
155
|
+
): string {
|
|
156
|
+
let siteId = '00000000-0000-0000-0000-000000000002';
|
|
157
|
+
let workspaceId = '00000000-0000-0000-0000-000000000001';
|
|
158
|
+
let userId = '00000000-0000-0000-0000-000000000003';
|
|
159
|
+
|
|
160
|
+
if (typeof siteIdOrOptions === 'object' && siteIdOrOptions !== null) {
|
|
161
|
+
if (siteIdOrOptions.siteId) siteId = siteIdOrOptions.siteId;
|
|
162
|
+
if (siteIdOrOptions.workspaceId) workspaceId = siteIdOrOptions.workspaceId;
|
|
163
|
+
if (siteIdOrOptions.userId) userId = siteIdOrOptions.userId;
|
|
164
|
+
} else {
|
|
165
|
+
if (typeof siteIdOrOptions === 'string') siteId = siteIdOrOptions;
|
|
166
|
+
if (typeof workspaceIdParam === 'string') workspaceId = workspaceIdParam;
|
|
167
|
+
if (typeof userIdParam === 'string') userId = userIdParam;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
171
|
+
const payload = {
|
|
172
|
+
aud: 'authenticated',
|
|
173
|
+
role: 'authenticated',
|
|
174
|
+
sub: userId,
|
|
175
|
+
email: 'dev@example.com',
|
|
176
|
+
app_metadata: {
|
|
177
|
+
workspace_id: workspaceId,
|
|
178
|
+
site_id: siteId,
|
|
179
|
+
role: 'viewer',
|
|
180
|
+
workspaces: {
|
|
181
|
+
[workspaceId]: { role: 'viewer' },
|
|
182
|
+
},
|
|
183
|
+
sites: {
|
|
184
|
+
[siteId]: { role: 'editor', workspace_id: workspaceId },
|
|
185
|
+
},
|
|
186
|
+
site_roles: {
|
|
187
|
+
[siteId]: 'editor',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
user_metadata: { role: 'viewer' },
|
|
191
|
+
iat: Math.floor(Date.now() / 1000),
|
|
192
|
+
exp: Math.floor(Date.now() / 1000) + 3600 * 24 * 365,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
196
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
197
|
+
const data = `${encodedHeader}.${encodedPayload}`;
|
|
198
|
+
const signature = crypto.createHmac('sha256', secret).update(data).digest('base64url');
|
|
199
|
+
|
|
200
|
+
return `${data}.${signature}`;
|
|
201
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as cliModule from '@407dev/cli';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { createVerifier } from './verifier';
|
|
4
|
+
|
|
5
|
+
describe('createVerifier service', () => {
|
|
6
|
+
it('clears error cache when a subsequent run produces zero diagnostics', async () => {
|
|
7
|
+
let mockDiagnostics: cliModule.ExtractionDiagnostic[] = [
|
|
8
|
+
{
|
|
9
|
+
type: 'error',
|
|
10
|
+
message: 'Dynamic template key not extractable',
|
|
11
|
+
file: 'src/pages/index.astro',
|
|
12
|
+
line: 10,
|
|
13
|
+
column: 5,
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const extractSpy = vi.spyOn(cliModule, 'extractProject').mockImplementation(async () => {
|
|
18
|
+
const errors = mockDiagnostics.filter((d) => d.type === 'error');
|
|
19
|
+
const warnings = mockDiagnostics.filter((d) => d.type === 'warning');
|
|
20
|
+
return {
|
|
21
|
+
manifest: {
|
|
22
|
+
manifestVersion: 1,
|
|
23
|
+
fields: {},
|
|
24
|
+
collections: {},
|
|
25
|
+
routes: [],
|
|
26
|
+
},
|
|
27
|
+
diagnostics: mockDiagnostics,
|
|
28
|
+
errors,
|
|
29
|
+
warnings,
|
|
30
|
+
merged: {
|
|
31
|
+
fields: {},
|
|
32
|
+
scopes: {},
|
|
33
|
+
collections: {},
|
|
34
|
+
routes: [],
|
|
35
|
+
diagnostics: [],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const verifier = await createVerifier({
|
|
41
|
+
siteDir: '/mock/site',
|
|
42
|
+
siteId: 'site-123',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 1. Initial run with errors
|
|
46
|
+
const state1 = await verifier.run();
|
|
47
|
+
expect(state1.errors.length).toBe(1);
|
|
48
|
+
expect(state1.errors[0].message).toBe('Dynamic template key not extractable');
|
|
49
|
+
|
|
50
|
+
// 2. User fixes the error in template; next run has 0 diagnostics
|
|
51
|
+
mockDiagnostics = [];
|
|
52
|
+
const state2 = await verifier.run();
|
|
53
|
+
|
|
54
|
+
// Prior error cache must be completely replaced
|
|
55
|
+
expect(state2.errors.length).toBe(0);
|
|
56
|
+
expect(state2.warnings.length).toBe(0);
|
|
57
|
+
|
|
58
|
+
const currentCached = await verifier.getDiagnostics();
|
|
59
|
+
expect(currentCached.errors.length).toBe(0);
|
|
60
|
+
|
|
61
|
+
extractSpy.mockRestore();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { hashSchema } from '@407dev/blocks';
|
|
3
|
+
import type { ViteDevServer } from 'vite';
|
|
4
|
+
|
|
5
|
+
export interface VerifierDiagnostic {
|
|
6
|
+
type: 'error' | 'warning';
|
|
7
|
+
message: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
line?: number;
|
|
10
|
+
column?: number;
|
|
11
|
+
code?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface VerifierState {
|
|
15
|
+
available: boolean;
|
|
16
|
+
errors: VerifierDiagnostic[];
|
|
17
|
+
warnings: VerifierDiagnostic[];
|
|
18
|
+
localHash: string | null;
|
|
19
|
+
pushedHash: string | null;
|
|
20
|
+
fresh: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VerifierInstance {
|
|
24
|
+
getDiagnostics: () => Promise<VerifierState>;
|
|
25
|
+
run: () => Promise<VerifierState>;
|
|
26
|
+
attachServer: (server: ViteDevServer) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CreateVerifierOptions {
|
|
30
|
+
siteDir: string;
|
|
31
|
+
siteId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a diagnostics verifier that extracts AST diagnostics from templates via @407dev/cli,
|
|
36
|
+
* tracks manifest freshness against .cms/state.json, and streams updates via Vite HMR.
|
|
37
|
+
*/
|
|
38
|
+
export async function createVerifier(options: CreateVerifierOptions): Promise<VerifierInstance> {
|
|
39
|
+
const { siteDir, siteId } = options;
|
|
40
|
+
|
|
41
|
+
let cliModule: typeof import('@407dev/cli') | null = null;
|
|
42
|
+
try {
|
|
43
|
+
const cliPkg = '@407dev/cli';
|
|
44
|
+
cliModule = await import(/* @vite-ignore */ cliPkg);
|
|
45
|
+
} catch {
|
|
46
|
+
cliModule = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let cachedState: VerifierState = {
|
|
50
|
+
available: Boolean(cliModule),
|
|
51
|
+
errors: [],
|
|
52
|
+
warnings: [],
|
|
53
|
+
localHash: null,
|
|
54
|
+
pushedHash: null,
|
|
55
|
+
fresh: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let activeServer: ViteDevServer | null = null;
|
|
59
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
60
|
+
|
|
61
|
+
async function run(): Promise<VerifierState> {
|
|
62
|
+
if (!cliModule) {
|
|
63
|
+
cachedState = {
|
|
64
|
+
available: false,
|
|
65
|
+
errors: [],
|
|
66
|
+
warnings: [],
|
|
67
|
+
localHash: null,
|
|
68
|
+
pushedHash: null,
|
|
69
|
+
fresh: true,
|
|
70
|
+
};
|
|
71
|
+
return cachedState;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const extractResult = await cliModule.extractProject(siteDir, { noSync: true });
|
|
76
|
+
const localHash = hashSchema(extractResult.manifest);
|
|
77
|
+
|
|
78
|
+
const stateStorePath = path.join(siteDir, '.cms', 'state.json');
|
|
79
|
+
const store = new cliModule.StateStore(stateStorePath);
|
|
80
|
+
const pushedHash = store.getSiteManifestHash(siteId);
|
|
81
|
+
|
|
82
|
+
const fresh = Boolean(localHash && pushedHash && localHash === pushedHash);
|
|
83
|
+
|
|
84
|
+
const errors: VerifierDiagnostic[] = (extractResult.errors || []).map((e) => ({
|
|
85
|
+
type: 'error',
|
|
86
|
+
message: e.message,
|
|
87
|
+
file: e.file,
|
|
88
|
+
line: e.line,
|
|
89
|
+
column: e.column,
|
|
90
|
+
code: e.code,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const warnings: VerifierDiagnostic[] = (extractResult.warnings || []).map((w) => ({
|
|
94
|
+
type: 'warning',
|
|
95
|
+
message: w.message,
|
|
96
|
+
file: w.file,
|
|
97
|
+
line: w.line,
|
|
98
|
+
column: w.column,
|
|
99
|
+
code: w.code,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// A run replaces the cache, ensuring warnings clear on fix
|
|
103
|
+
cachedState = {
|
|
104
|
+
available: true,
|
|
105
|
+
errors,
|
|
106
|
+
warnings,
|
|
107
|
+
localHash,
|
|
108
|
+
pushedHash,
|
|
109
|
+
fresh,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (activeServer?.hot) {
|
|
113
|
+
activeServer.hot.send({
|
|
114
|
+
type: 'custom',
|
|
115
|
+
event: 'cms:diagnostics',
|
|
116
|
+
data: cachedState,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return cachedState;
|
|
121
|
+
} catch (_err) {
|
|
122
|
+
return cachedState;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleFileEvent(filePath: string) {
|
|
127
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
128
|
+
if (
|
|
129
|
+
normalized.includes('/src/') &&
|
|
130
|
+
/\.(astro|ts|tsx|js|mjs)$/.test(normalized) &&
|
|
131
|
+
!normalized.endsWith('.d.ts')
|
|
132
|
+
) {
|
|
133
|
+
if (debounceTimer) {
|
|
134
|
+
clearTimeout(debounceTimer);
|
|
135
|
+
}
|
|
136
|
+
debounceTimer = setTimeout(() => {
|
|
137
|
+
debounceTimer = null;
|
|
138
|
+
run();
|
|
139
|
+
}, 300);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function attachServer(server: ViteDevServer) {
|
|
144
|
+
activeServer = server;
|
|
145
|
+
|
|
146
|
+
server.watcher.on('change', handleFileEvent);
|
|
147
|
+
server.watcher.on('add', handleFileEvent);
|
|
148
|
+
server.watcher.on('unlink', handleFileEvent);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Initial execution
|
|
152
|
+
if (cliModule) {
|
|
153
|
+
await run();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
getDiagnostics: async () => cachedState,
|
|
158
|
+
run,
|
|
159
|
+
attachServer,
|
|
160
|
+
};
|
|
161
|
+
}
|