@elytracms/studio 0.0.79 → 0.0.81
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/convex/contract-parity.test.ts +1 -1
- package/convex/functions.ts +13 -3
- package/convex/http-routes.test.ts +143 -0
- package/convex/http.ts +19 -9
- package/convex/schema.ts +0 -19
- package/convex/users.ts +1 -1
- package/dist/components/app-views/default-modules.d.ts +141 -44
- package/dist/components/app-views/default-modules.js +229 -65
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +38 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +5 -2
- package/dist/components/app-views/sidebar.js +16 -5
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +27 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +7 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/reuse-context.js +7 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +94 -16
- package/dist/components/builder/reuse.js +201 -19
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +3 -1
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.js +6 -2
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +40 -13
- package/dist/components/cms/cms-store.js +50 -22
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +2 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/date-input-core.d.ts +21 -0
- package/dist/components/cms/date-input-core.js +20 -0
- package/dist/components/cms/date-input-core.js.map +1 -1
- package/dist/components/cms/date-input.d.ts +9 -1
- package/dist/components/cms/date-input.js +2 -2
- package/dist/components/cms/date-input.js.map +1 -1
- package/dist/components/cms/detail-editor.js +11 -5
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/editable-cell.js +2 -1
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +4 -2
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.js +8 -1
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/import-export.js +2 -1
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/issues-pane.d.ts +4 -2
- package/dist/components/cms/issues-pane.js +6 -4
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.js +2 -1
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +10 -12
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/project-list.d.ts +9 -3
- package/dist/components/project-list.js +11 -5
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +3 -1
- package/dist/components/project-switcher.js +4 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/redirects/redirects-store.d.ts +21 -0
- package/dist/components/redirects/redirects-store.js +17 -4
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/section-nav.js +102 -4
- package/dist/components/section-nav.js.map +1 -1
- package/dist/customization.d.ts +7 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +6 -1
- package/dist/lib/data/collection-content.js +50 -12
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +132 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +103 -0
- package/dist/lib/i18n/messages.en.js +146 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/binding-fallback.d.ts +8 -1
- package/dist/lib/locale/binding-fallback.js +8 -1
- package/dist/lib/locale/binding-fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +41 -9
- package/dist/lib/locale/locale-store.js +34 -15
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/locale/locale-switcher.d.ts +3 -1
- package/dist/lib/locale/locale-switcher.js +4 -2
- package/dist/lib/locale/locale-switcher.js.map +1 -1
- package/dist/lib/locale/use-studio-locale.d.ts +9 -4
- package/dist/lib/locale/use-studio-locale.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +0 -55
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +42 -1
- package/dist/lib/persona/labels.js +57 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +9 -0
- package/dist/lib/routes/route-issues.js +117 -0
- package/dist/lib/routes/route-issues.js.map +1 -0
- package/dist/lib/workspace/config-issues.d.ts +54 -0
- package/dist/lib/workspace/config-issues.js +70 -0
- package/dist/lib/workspace/config-issues.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +9 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +20 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/package.json +2 -2
- package/convex/cliTokens.ts +0 -63
- package/convex/sync.ts +0 -297
package/convex/sync.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { makeFunctionReference } from 'convex/server'
|
|
2
|
-
import type { GenericActionCtx, GenericDataModel } from 'convex/server'
|
|
3
|
-
import { v } from 'convex/values'
|
|
4
|
-
import { verifyCliToken } from '@elytracms/core/operations'
|
|
5
|
-
import type { VerifiedCliToken } from '@elytracms/core/operations'
|
|
6
|
-
import { roleGrantsCapability } from '@elytracms/core/persistence'
|
|
7
|
-
import type {
|
|
8
|
-
CliTokenRepository,
|
|
9
|
-
OperationCapabilityId,
|
|
10
|
-
PersistenceAdapter,
|
|
11
|
-
} from '@elytracms/core/persistence'
|
|
12
|
-
import { httpAction, internalQuery } from './functions'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* EC-157 — the builder-sync HTTP endpoint. Serves the contract fixed in
|
|
16
|
-
* `@elytracms/core/export-sync`'s `registrySyncEndpoints` from this deployment's
|
|
17
|
-
* `.convex.site` origin, so a host repo's `builder check` / `typegen` run against
|
|
18
|
-
* the real builder instead of a `fetchImpl` double:
|
|
19
|
-
*
|
|
20
|
-
* - `GET /api/projects/:projectId/registry-sync/snapshot?mode=&environment=` — the
|
|
21
|
-
* typegen/check snapshot: locale config only. Collection schemas, routes/redirects,
|
|
22
|
-
* AND the project graph are config-owned (EC-183/EC-207/EC-287, AD-11) — the CLI
|
|
23
|
-
* reads them from `elytra.config.ts`, not this snapshot.
|
|
24
|
-
*
|
|
25
|
-
* ## Package-shipped, deployment-agnostic (EC-223/EC-252)
|
|
26
|
-
*
|
|
27
|
-
* This module ships inside `@elytracms/studio`; the consumer's `convex/sync.ts` is a
|
|
28
|
-
* one-line `export *` shim. It therefore NEVER imports the per-deployment
|
|
29
|
-
* `./_generated/api` — it addresses sibling functions by STRING ref via
|
|
30
|
-
* `makeFunctionReference` (resolved against the consumer's deployment), exactly like
|
|
31
|
-
* `publishing.ts`. That decoupling is also what makes it typecheck under a cross-repo
|
|
32
|
-
* `link:` loop: there is no `ApiFromModules` inference to unify across two `convex`
|
|
33
|
-
* instances (the bug EC-252 fixes — a copied `sync.ts` collapsed `api.cliTokens.byHash`
|
|
34
|
-
* to `{}`).
|
|
35
|
-
*
|
|
36
|
-
* ## Auth (EC-150 CLI tokens)
|
|
37
|
-
*
|
|
38
|
-
* The endpoint requires `Authorization: Bearer <ect_…>`. Tokens are verified by
|
|
39
|
-
* `verifyCliToken` (`@elytracms/core/operations`): the presented plaintext is SHA-256-hashed
|
|
40
|
-
* and looked up via `cliTokens.byHash`; unknown and revoked tokens are rejected with
|
|
41
|
-
* a 401. Single-tenant (EC-182): a deployment hosts one project, so a token carries
|
|
42
|
-
* no per-project scope — it just needs at least the `viewer` capability the snapshot
|
|
43
|
-
* read requires.
|
|
44
|
-
*
|
|
45
|
-
* ## Error contract
|
|
46
|
-
*
|
|
47
|
-
* Failures are structured JSON bodies — `{ error: { code, message, issues? } }` —
|
|
48
|
-
* matching `RegistrySyncErrorBody` in `@elytracms/core/export-sync`; even unexpected
|
|
49
|
-
* exceptions return a structured body, never an opaque crash page. The CLI surfaces
|
|
50
|
-
* them as `transport` issues.
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
type ActionCtx = GenericActionCtx<GenericDataModel>
|
|
54
|
-
|
|
55
|
-
// Sibling functions are addressed by STRING ref (resolved against the consumer's
|
|
56
|
-
// deployment), never `./_generated/api` — see the module note. The explicit arg +
|
|
57
|
-
// return generics keep the call sites typed without `ApiFromModules` inference.
|
|
58
|
-
type CliTokenRow = {
|
|
59
|
-
tokenId: string
|
|
60
|
-
label: string
|
|
61
|
-
tokenHash: string
|
|
62
|
-
createdAt: string
|
|
63
|
-
revokedAt?: string
|
|
64
|
-
}
|
|
65
|
-
const cliTokensByHash = makeFunctionReference<
|
|
66
|
-
'query',
|
|
67
|
-
{ tokenHash: string },
|
|
68
|
-
CliTokenRow | null
|
|
69
|
-
>('cliTokens:byHash')
|
|
70
|
-
const syncExportSnapshot = makeFunctionReference<
|
|
71
|
-
'query',
|
|
72
|
-
{ projectId: string; mode: SnapshotMode; environment: string },
|
|
73
|
-
BuilderSnapshot
|
|
74
|
-
>('sync:exportSnapshot')
|
|
75
|
-
|
|
76
|
-
// --- Structured responses ----------------------------------------------------
|
|
77
|
-
|
|
78
|
-
interface SyncErrorIssue {
|
|
79
|
-
code: string
|
|
80
|
-
message: string
|
|
81
|
-
componentId?: string
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function jsonResponse(value: unknown, status = 200): Response {
|
|
85
|
-
return new Response(JSON.stringify(value), {
|
|
86
|
-
status,
|
|
87
|
-
headers: { 'content-type': 'application/json' },
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function errorResponse(
|
|
92
|
-
status: number,
|
|
93
|
-
code: string,
|
|
94
|
-
message: string,
|
|
95
|
-
issues?: SyncErrorIssue[],
|
|
96
|
-
): Response {
|
|
97
|
-
return jsonResponse(
|
|
98
|
-
{ error: { code, message, ...(issues && issues.length > 0 ? { issues } : {}) } },
|
|
99
|
-
status,
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// --- Routing -----------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
const PROJECTS_PREFIX = '/api/projects/'
|
|
106
|
-
|
|
107
|
-
type SyncEndpoint = 'snapshot'
|
|
108
|
-
|
|
109
|
-
interface SyncRoute {
|
|
110
|
-
projectId: string
|
|
111
|
-
endpoint: SyncEndpoint
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Parse `/api/projects/:projectId/registry-sync/<rest>` (Convex routes by prefix only). */
|
|
115
|
-
function parseSyncRoute(method: string, pathname: string): SyncRoute | undefined {
|
|
116
|
-
if (!pathname.startsWith(PROJECTS_PREFIX)) return undefined
|
|
117
|
-
const segments = pathname
|
|
118
|
-
.slice(PROJECTS_PREFIX.length)
|
|
119
|
-
.split('/')
|
|
120
|
-
.map((segment) => decodeURIComponent(segment))
|
|
121
|
-
const [projectId, scope, ...rest] = segments
|
|
122
|
-
if (!projectId || scope !== 'registry-sync') return undefined
|
|
123
|
-
const tail = rest.join('/')
|
|
124
|
-
if (tail === 'snapshot' && method === 'GET') return { projectId, endpoint: 'snapshot' }
|
|
125
|
-
return undefined
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// --- Token auth (EC-150) -------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The capability each endpoint requires; the snapshot read needs `content.read`
|
|
132
|
-
* (EC-240c). Capability grants come from the ONE canonical role policy
|
|
133
|
-
* (`roleGrantsCapability`), so a token's role is checked the same way handlers
|
|
134
|
-
* check members — no role literal at the boundary.
|
|
135
|
-
*/
|
|
136
|
-
const REQUIRED_CAPABILITY: Record<SyncEndpoint, OperationCapabilityId> = {
|
|
137
|
-
snapshot: 'content.read',
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* A {@link CliTokenRepository} over this deployment's `cliTokens` table, narrowed to
|
|
142
|
-
* the verification path. `verifyCliToken` only ever calls `findByHash`; the write
|
|
143
|
-
* methods live behind the admin-only operations layer and are not reachable here.
|
|
144
|
-
*/
|
|
145
|
-
function cliTokenRepository(ctx: ActionCtx): CliTokenRepository {
|
|
146
|
-
const unsupported = (method: string) =>
|
|
147
|
-
Promise.reject(new Error(`cli-token ${method} is not available on the sync endpoint`))
|
|
148
|
-
return {
|
|
149
|
-
createToken: () => unsupported('createToken'),
|
|
150
|
-
listTokens: () => unsupported('listTokens'),
|
|
151
|
-
revokeToken: () => unsupported('revokeToken'),
|
|
152
|
-
async findByHash(tokenHash) {
|
|
153
|
-
const doc = await ctx.runQuery(cliTokensByHash, { tokenHash })
|
|
154
|
-
if (!doc) return undefined
|
|
155
|
-
return {
|
|
156
|
-
id: doc.tokenId,
|
|
157
|
-
label: doc.label,
|
|
158
|
-
tokenHash: doc.tokenHash,
|
|
159
|
-
createdAt: doc.createdAt,
|
|
160
|
-
...(doc.revokedAt ? { revokedAt: doc.revokedAt } : {}),
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
type AuthResult = { ok: true; verified: VerifiedCliToken } | { ok: false; response: Response }
|
|
167
|
-
|
|
168
|
-
/** Resolve and verify the Bearer CLI token, then check it grants the required capability. */
|
|
169
|
-
async function authorize(
|
|
170
|
-
ctx: ActionCtx,
|
|
171
|
-
request: Request,
|
|
172
|
-
route: SyncRoute,
|
|
173
|
-
): Promise<AuthResult> {
|
|
174
|
-
const header = request.headers.get('authorization')
|
|
175
|
-
if (!header || !header.toLowerCase().startsWith('bearer ')) {
|
|
176
|
-
return {
|
|
177
|
-
ok: false,
|
|
178
|
-
response: errorResponse(
|
|
179
|
-
401,
|
|
180
|
-
'unauthorized',
|
|
181
|
-
'Missing CLI token — send "Authorization: Bearer <token>" (issue one with tokens.issue).',
|
|
182
|
-
),
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const token = header.slice('bearer '.length).trim()
|
|
186
|
-
// verifyCliToken only reads `adapter.cliTokens`; the rest of the adapter surface
|
|
187
|
-
// is deliberately absent here (the assertion documents exactly that contract).
|
|
188
|
-
const adapter = { cliTokens: cliTokenRepository(ctx) } as PersistenceAdapter
|
|
189
|
-
const verified = await verifyCliToken(adapter, token)
|
|
190
|
-
if (verified === null) {
|
|
191
|
-
return {
|
|
192
|
-
ok: false,
|
|
193
|
-
response: errorResponse(401, 'unauthorized', 'Unknown, malformed, or revoked CLI token.'),
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (!roleGrantsCapability(verified.role, REQUIRED_CAPABILITY[route.endpoint])) {
|
|
197
|
-
return {
|
|
198
|
-
ok: false,
|
|
199
|
-
response: errorResponse(
|
|
200
|
-
403,
|
|
201
|
-
'forbidden',
|
|
202
|
-
`This endpoint requires the "${REQUIRED_CAPABILITY[route.endpoint]}" capability (token role "${verified.role}" does not grant it).`,
|
|
203
|
-
),
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return { ok: true, verified }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// --- The HTTP action ------------------------------------------------------------
|
|
210
|
-
|
|
211
|
-
export const registrySync = httpAction(async (ctx, request) => {
|
|
212
|
-
const pathname = new URL(request.url).pathname
|
|
213
|
-
try {
|
|
214
|
-
const route = parseSyncRoute(request.method, pathname)
|
|
215
|
-
if (!route) {
|
|
216
|
-
return errorResponse(
|
|
217
|
-
404,
|
|
218
|
-
'not-found',
|
|
219
|
-
`No registry-sync endpoint matches ${request.method} ${pathname}.`,
|
|
220
|
-
)
|
|
221
|
-
}
|
|
222
|
-
const auth = await authorize(ctx, request, route)
|
|
223
|
-
if (!auth.ok) return auth.response
|
|
224
|
-
const { projectId } = route
|
|
225
|
-
|
|
226
|
-
// snapshot
|
|
227
|
-
const params = new URL(request.url).searchParams
|
|
228
|
-
const mode = params.get('mode') ?? 'published'
|
|
229
|
-
if (mode !== 'published' && mode !== 'draft') {
|
|
230
|
-
return errorResponse(
|
|
231
|
-
400,
|
|
232
|
-
'invalid-request',
|
|
233
|
-
`Invalid mode "${mode}" (expected published|draft).`,
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
const environment = params.get('environment') ?? 'production'
|
|
237
|
-
const snapshot = await ctx.runQuery(syncExportSnapshot, {
|
|
238
|
-
projectId,
|
|
239
|
-
mode,
|
|
240
|
-
environment,
|
|
241
|
-
})
|
|
242
|
-
return jsonResponse(snapshot)
|
|
243
|
-
} catch (cause) {
|
|
244
|
-
// Even unexpected failures keep the structured contract — never an opaque crash.
|
|
245
|
-
return errorResponse(
|
|
246
|
-
500,
|
|
247
|
-
'internal',
|
|
248
|
-
`Registry sync failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
// --- Typegen/check snapshot -----------------------------------------------------
|
|
254
|
-
|
|
255
|
-
type SnapshotMode = 'published' | 'draft'
|
|
256
|
-
|
|
257
|
-
interface BuilderSnapshot {
|
|
258
|
-
projectId: string
|
|
259
|
-
mode: SnapshotMode
|
|
260
|
-
environment: string
|
|
261
|
-
project: null
|
|
262
|
-
localeConfig: { default: string; locales: string[] }
|
|
263
|
-
collections: never[]
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Everything `builder typegen`/`builder check` need from live builder state, in the
|
|
268
|
-
* `BuilderSnapshot` wire shape of `@elytracms/core/export-sync`'s `createHttpBuilderSource`.
|
|
269
|
-
* EC-287: the project graph is retired (structure is code), so the snapshot no
|
|
270
|
-
* longer carries a graph — like the CMS schema and routes, it is config-owned and
|
|
271
|
-
* read from `elytra.config.ts` in the consuming repo.
|
|
272
|
-
*/
|
|
273
|
-
export const exportSnapshot = internalQuery({
|
|
274
|
-
args: {
|
|
275
|
-
projectId: v.string(),
|
|
276
|
-
mode: v.union(v.literal('published'), v.literal('draft')),
|
|
277
|
-
environment: v.string(),
|
|
278
|
-
},
|
|
279
|
-
handler: async (ctx, { projectId, mode, environment }): Promise<BuilderSnapshot> => {
|
|
280
|
-
// `environment` is a vestigial wire-compat field on the CLI snapshot contract
|
|
281
|
-
// (EC-179 §2): a deployment IS one environment.
|
|
282
|
-
// AD-11 (EC-182/EC-183/EC-207): project identity, locale config, CMS
|
|
283
|
-
// collection schemas, AND routes/redirects are all config-owned —
|
|
284
|
-
// `builder typegen`/`check` run in the consuming repo and read them from
|
|
285
|
-
// `elytra.config.ts`, not the wire. `project`/`localeConfig`/`collections`
|
|
286
|
-
// stay in the wire shape as config-sourced placeholders until the CLI reads
|
|
287
|
-
// config directly (C1).
|
|
288
|
-
return {
|
|
289
|
-
projectId,
|
|
290
|
-
mode,
|
|
291
|
-
environment,
|
|
292
|
-
project: null,
|
|
293
|
-
localeConfig: { default: 'en', locales: ['en'] },
|
|
294
|
-
collections: [],
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
})
|