@hollware/server 0.0.1 → 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 CHANGED
@@ -8,9 +8,9 @@ for the agent era, native to Cloudflare Workers.
8
8
 
9
9
  ## What it will be
10
10
 
11
- The Hollware Worker: the operation registry behind MCP, Workers RPC and HTTP, authentication,
12
- and the D1 migrations that shape the database. The migrations ship inside the package, in
13
- `migrations/`.
11
+ The Hollware Worker: the handlers of the operations that `@hollware/core` specifies, served
12
+ over MCP, Workers RPC and HTTP, authentication, and the D1 migrations that shape the
13
+ database. The migrations ship inside the package, in `migrations/`.
14
14
 
15
15
  ## Links
16
16
 
package/dist/index.d.mts CHANGED
@@ -1,3 +1,309 @@
1
+ import { AcknowledgedWarning, EntryDocument, EntryLookup, EntryRef, HollwareConfig, OperationByName, OperationName, ResolvedSite, Scope, Verdict, describeSite, getEntry, getTranslations, listEntries } from "@hollware/core";
2
+ import * as z from "zod";
3
+ import { WorkerEntrypoint } from "cloudflare:workers";
4
+ //#region src/auth/types.d.ts
5
+ /**
6
+ * Who calls an operation. The gateway turns each request into a principal before any handler
7
+ * runs, and the handlers check their scopes against it at call time (ADR-019).
8
+ */
9
+ type Principal = {
10
+ /** A personal token. */
11
+ readonly kind: 'token';
12
+ readonly userId: string;
13
+ readonly tokenId: string;
14
+ /** The site the token belongs to; it never reaches another one. */
15
+ readonly site: string;
16
+ /** The token's own scopes intersected with those of its user's role, as of now. */
17
+ readonly scopes: readonly Scope[];
18
+ } | {
19
+ /** A Worker of the same account, over the RPC binding: it reads published content. */
20
+ readonly kind: 'binding';
21
+ readonly scopes: readonly ['content:read'];
22
+ };
23
+ //#endregion
24
+ //#region src/ports.d.ts
25
+ /**
26
+ * The ports of the operation handlers: what they need from the world, as interfaces. The D1
27
+ * store implements `ContentStore` (`db/`); the handlers (`ops/`) only see this file, so their
28
+ * tests run on fakes. Every method takes the site first and never sees another site's rows.
29
+ */
30
+ /** Which revision a read returns: the latest one (MCP) or the published one (RPC). */
31
+ type View = 'head' | 'published';
32
+ /** Who wrote a revision or a publication. */
33
+ interface Actor {
34
+ readonly kind: 'token' | 'user' | 'oauth_client';
35
+ readonly userId: string;
36
+ readonly tokenId: string | null;
37
+ /** What the client said about itself (the MCP client and, when it says, the model). */
38
+ readonly meta: Readonly<Record<string, unknown>> | null;
39
+ }
40
+ /** An entry as the store holds it. */
41
+ interface StoredEntry {
42
+ readonly id: string;
43
+ readonly collection: string;
44
+ readonly locale: string;
45
+ readonly slug: string;
46
+ readonly translationGroup: string | null;
47
+ readonly status: 'draft' | 'scheduled' | 'published';
48
+ /** Moves on every change of the entry; a write against an older one is a conflict. */
49
+ readonly version: number;
50
+ readonly headRevision: string;
51
+ readonly publishedRevision: string | null;
52
+ /** When the entry was first published; republishing keeps it. */
53
+ readonly publishedAt: string | null;
54
+ readonly updatedAt: string;
55
+ }
56
+ /** A revision as the store holds it. */
57
+ interface StoredRevision {
58
+ readonly id: string;
59
+ readonly entryId: string;
60
+ /** The whole file, frontmatter included: the source of truth (ADR-001). */
61
+ readonly markdown: string;
62
+ readonly frontmatter: Readonly<Record<string, unknown>>;
63
+ readonly document: EntryDocument;
64
+ readonly comarkVersion: string;
65
+ readonly pipelineVersion: string;
66
+ readonly wordCount: number;
67
+ /** The verdict of the `write` phase when the revision was written. */
68
+ readonly verdict: Verdict;
69
+ readonly tags: readonly string[];
70
+ readonly actor: Actor;
71
+ readonly createdAt: string;
72
+ }
73
+ /** An entry with the revision of the view that was asked for. */
74
+ interface EntryView {
75
+ readonly entry: StoredEntry;
76
+ readonly revision: StoredRevision;
77
+ }
78
+ /**
79
+ * What a listing needs of a revision: its frontmatter and tags, never the Markdown or the
80
+ * document, which can weigh megabytes on a page of a hundred entries.
81
+ */
82
+ interface RevisionSummary {
83
+ readonly id: string;
84
+ readonly frontmatter: Readonly<Record<string, unknown>>;
85
+ readonly tags: readonly string[];
86
+ }
87
+ /** An entry with the summary of the revision of the view that was asked for. */
88
+ interface EntrySummaryView {
89
+ readonly entry: StoredEntry;
90
+ readonly revision: RevisionSummary;
91
+ }
92
+ /** A publish or an unpublish, as `publications` keeps it (ADR-021). */
93
+ interface StoredPublication {
94
+ readonly id: string;
95
+ readonly entryId: string;
96
+ readonly action: 'publish' | 'unpublish';
97
+ readonly revisionId: string | null;
98
+ readonly verdict: Verdict | null;
99
+ readonly acknowledged: readonly AcknowledgedWarning[];
100
+ readonly actor: Actor;
101
+ readonly createdAt: string;
102
+ }
103
+ /** A revision to append. The store adds its id and time. */
104
+ interface NewRevision {
105
+ readonly markdown: string;
106
+ readonly frontmatter: Readonly<Record<string, unknown>>;
107
+ readonly document: EntryDocument;
108
+ readonly comarkVersion: string;
109
+ readonly pipelineVersion: string;
110
+ readonly wordCount: number;
111
+ readonly verdict: Verdict;
112
+ readonly tags: readonly string[];
113
+ readonly actor: Actor;
114
+ }
115
+ interface ListQuery {
116
+ readonly collection: string;
117
+ readonly locale?: string;
118
+ readonly tag?: string;
119
+ readonly status?: StoredEntry['status'];
120
+ readonly view: View;
121
+ readonly limit: number;
122
+ /** The `nextCursor` of the previous page. */
123
+ readonly cursor?: string;
124
+ }
125
+ /** The answer to a write that the store could not make. */
126
+ type WriteRefusal = {
127
+ readonly outcome: 'conflict';
128
+ readonly currentVersion: number;
129
+ readonly headRevision: string;
130
+ } | {
131
+ readonly outcome: 'not_found';
132
+ } |
133
+ /** Another entry already has this slug, or this translation, in the collection and locale. */
134
+ {
135
+ readonly outcome: 'taken';
136
+ readonly field: 'slug' | 'translationGroup';
137
+ readonly by: string;
138
+ } |
139
+ /** The row would be over `MAX_ROW_BYTES` (`limits.ts`); nothing was written. */
140
+ {
141
+ readonly outcome: 'too_large';
142
+ readonly bytes: number;
143
+ readonly limit: number;
144
+ };
145
+ interface ContentStore {
146
+ /** The entry with that collection, locale and slug, in the given view, or `null`. */
147
+ findEntry(site: string, ref: EntryRef, view: View): Promise<EntryView | null>;
148
+ /** The entry with that id, in the given view, or `null`. */
149
+ findEntryById(site: string, id: string, view: View): Promise<EntryView | null>;
150
+ /**
151
+ * A page of entries. The `head` view is ordered by the last change, the `published` view by
152
+ * the published sort key, newest first; ties go by id. The cursor is opaque.
153
+ */
154
+ listEntries(site: string, query: ListQuery): Promise<{
155
+ readonly entries: readonly EntrySummaryView[];
156
+ readonly nextCursor: string | null;
157
+ }>;
158
+ /** The entries of a translation group, one per locale, in the given view. */
159
+ listTranslations(site: string, collection: string, translationGroup: string, view: View): Promise<readonly EntrySummaryView[]>;
160
+ /** What cross-entry contracts read, scoped to the site. */
161
+ lookup(site: string): EntryLookup;
162
+ /** The latest publish or unpublish of an entry, or `null`. */
163
+ lastPublication(site: string, entryId: string): Promise<StoredPublication | null>;
164
+ /** Creates an entry at version 1 with its first revision, as a draft. */
165
+ createEntry(site: string, entry: EntryRef & {
166
+ readonly translationGroup: string | null;
167
+ readonly revision: NewRevision;
168
+ }): Promise<{
169
+ readonly outcome: 'saved';
170
+ readonly view: EntryView;
171
+ } | WriteRefusal>;
172
+ /**
173
+ * Appends a revision and makes it the head, if the entry is still at `expectedVersion`. In
174
+ * one batch: the revision, its tags (only where the revision was inserted) and the entry.
175
+ */
176
+ appendRevision(site: string, write: {
177
+ readonly entryId: string;
178
+ readonly expectedVersion: number;
179
+ readonly translationGroup: string | null;
180
+ readonly revision: NewRevision;
181
+ }): Promise<{
182
+ readonly outcome: 'saved';
183
+ readonly view: EntryView;
184
+ } | WriteRefusal>;
185
+ /**
186
+ * Publishes the head revision, if the entry is still at `expectedVersion`, and appends the
187
+ * publication with its verdict and acknowledged warnings.
188
+ */
189
+ publish(site: string, write: {
190
+ readonly entryId: string;
191
+ readonly expectedVersion: number;
192
+ readonly verdict: Verdict;
193
+ readonly acknowledged: readonly AcknowledgedWarning[];
194
+ readonly actor: Actor;
195
+ /** What orders the entry in published listings. */
196
+ readonly sortKey: string;
197
+ }): Promise<{
198
+ readonly outcome: 'published';
199
+ readonly view: EntryView;
200
+ } | WriteRefusal>;
201
+ /** Takes the entry off the public site and appends the unpublish. */
202
+ unpublish(site: string, write: {
203
+ readonly entryId: string;
204
+ readonly actor: Actor;
205
+ }): Promise<{
206
+ readonly outcome: 'unpublished';
207
+ readonly view: EntryView;
208
+ } | WriteRefusal>;
209
+ }
210
+ /** The time, so that tests can fix it. */
211
+ interface Clock {
212
+ now(): Date;
213
+ }
214
+ //#endregion
215
+ //#region src/ops/types.d.ts
216
+ /**
217
+ * The handlers of the operations (ADR-017): one per spec of `@hollware/core`, paired by type,
218
+ * so that a missing handler or a handler for an unknown operation does not compile.
219
+ */
220
+ /** What a handler gets besides its input. Adapters build it; handlers never see a request. */
221
+ interface OperationContext {
222
+ readonly site: ResolvedSite;
223
+ readonly principal: Principal;
224
+ /** `head` over MCP, `published` over RPC. */
225
+ readonly view: View;
226
+ readonly store: ContentStore;
227
+ readonly clock: Clock;
228
+ /** What the client said about itself; writes record it as the actor's meta. */
229
+ readonly client: Readonly<Record<string, unknown>> | null;
230
+ readonly requestId: string;
231
+ }
232
+ /** The handler of one operation: its parsed input in, its output out. */
233
+ type OperationHandler<Name extends OperationName> = (input: z.output<OperationByName<Name>['input']>, context: OperationContext) => Promise<z.output<OperationByName<Name>['output']>>;
234
+ /** One handler for every operation. */
235
+ type Handlers = { readonly [Name in OperationName]: OperationHandler<Name>; };
236
+ /** Declares the handlers; the type checks that they pair with the specs one to one. */
237
+ export declare function defineHandlers(handlers: Handlers): Handlers;
238
+ //#endregion
239
+ //#region src/limits.d.ts
240
+ /**
241
+ * The limits every operation runs under, pinned for the adapters and the store (the v0.1 plan).
242
+ */
243
+ /**
244
+ * The largest answer of an operation, in UTF-8 bytes of its JSON. A larger one is the error
245
+ * `output_too_large`, with a fix that says how to ask for less; content is never truncated.
246
+ */
247
+ export declare const OUTPUT_CAP_BYTES: number;
248
+ /** How long an operation may take before it answers the error `timeout`. */
249
+ export declare const OPERATION_TIMEOUT_MS = 10000;
250
+ /**
251
+ * The largest row the store writes, counted in UTF-8 bytes across all its columns. D1 refuses
252
+ * rows over 2 MB in production, and Miniflare does not enforce it, so the store checks first.
253
+ */
254
+ export declare const MAX_ROW_BYTES = 1900000;
255
+ /** The most values bound in one statement: D1 allows 100, and lookups go in chunks of this. */
256
+ export declare const MAX_BOUND_PARAMETERS = 90;
257
+ //#endregion
1
258
  //#region src/index.d.ts
2
259
  export declare const name = "@hollware/server";
3
- //#endregion
260
+ /** The bindings and variables of the Worker, as its `wrangler.jsonc` declares them. */
261
+ export interface HollwareEnv {
262
+ /** The D1 database, with the migrations of this package applied. */
263
+ readonly DB: D1Database;
264
+ /**
265
+ * The public URL of the Worker, such as `https://cms.example.com`. It is the only Host and
266
+ * Origin the Worker answers; without it, every request fails.
267
+ */
268
+ readonly HOLLWARE_PUBLIC_URL: string;
269
+ }
270
+ type Output<Spec extends {
271
+ output: z.ZodType;
272
+ }> = z.output<Spec['output']>;
273
+ /**
274
+ * The read-only RPC surface that sites of the same account call over a service binding. It
275
+ * returns published content only; nothing in it writes.
276
+ */
277
+ export interface HollwareRpc {
278
+ getEntry(site: string, collection: string, slug: string, locale: string, options?: {
279
+ readonly formats?: readonly ('markdown' | 'document' | 'html')[];
280
+ }): Promise<Output<typeof getEntry>>;
281
+ listEntries(site: string, collection: string, options?: {
282
+ readonly locale?: string;
283
+ readonly tag?: string;
284
+ readonly limit?: number;
285
+ readonly cursor?: string;
286
+ }): Promise<Output<typeof listEntries>>;
287
+ getTranslations(site: string, collection: string, translationGroup: string): Promise<Output<typeof getTranslations>>;
288
+ describeSite(site: string): Promise<Output<typeof describeSite>>;
289
+ }
290
+ /**
291
+ * What `createHollware(config)` returns. The user's Worker is
292
+ * `export default { fetch }` and `export class Hollware extends hollware.Hollware {}`.
293
+ */
294
+ export interface HollwareWorker {
295
+ /** MCP at `/mcp`, and the other routes of the gateway. */
296
+ readonly fetch: (request: Request, env: HollwareEnv, ctx: ExecutionContext) => Promise<Response>;
297
+ /** The RPC entrypoint, for the user's Worker to export under the name `Hollware`. */
298
+ readonly Hollware: new (ctx: ExecutionContext, env: HollwareEnv) => WorkerEntrypoint<HollwareEnv> & HollwareRpc;
299
+ }
300
+ /** Builds the Worker from the config. The config is resolved once, when the Worker starts. */
301
+ export type CreateHollware = (config: HollwareConfig) => HollwareWorker;
302
+ /**
303
+ * Builds the Worker from the config: `const hollware = createHollware(config)`, then
304
+ * `export default { fetch: hollware.fetch }` and `export class Hollware extends hollware.Hollware {}`.
305
+ * The config is resolved once, here; a broken one throws its `ConfigError`.
306
+ */
307
+ export declare const createHollware: CreateHollware;
308
+ //#endregion
309
+ export type { Actor, Clock, ContentStore, EntrySummaryView, EntryView, Handlers, ListQuery, NewRevision, OperationContext, OperationHandler, Principal, RevisionSummary, StoredEntry, StoredPublication, StoredRevision, View, WriteRefusal };