@kernhq/module-quire 0.2.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.
Files changed (72) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +71 -0
  3. package/dist/client/rank.d.ts +42 -0
  4. package/dist/client/rank.d.ts.map +1 -0
  5. package/dist/client/rank.js +97 -0
  6. package/dist/client/rank.js.map +1 -0
  7. package/dist/contract/capabilities.d.ts +35 -0
  8. package/dist/contract/capabilities.d.ts.map +1 -0
  9. package/dist/contract/capabilities.js +35 -0
  10. package/dist/contract/capabilities.js.map +1 -0
  11. package/dist/contract/events.d.ts +58 -0
  12. package/dist/contract/events.d.ts.map +1 -0
  13. package/dist/contract/events.js +18 -0
  14. package/dist/contract/events.js.map +1 -0
  15. package/dist/contract/index.d.ts +6 -0
  16. package/dist/contract/index.d.ts.map +1 -0
  17. package/dist/contract/index.js +6 -0
  18. package/dist/contract/index.js.map +1 -0
  19. package/dist/contract/models.d.ts +95 -0
  20. package/dist/contract/models.d.ts.map +1 -0
  21. package/dist/contract/models.js +82 -0
  22. package/dist/contract/models.js.map +1 -0
  23. package/dist/contract/permissions.d.ts +51 -0
  24. package/dist/contract/permissions.d.ts.map +1 -0
  25. package/dist/contract/permissions.js +59 -0
  26. package/dist/contract/permissions.js.map +1 -0
  27. package/dist/contract/router.d.ts +713 -0
  28. package/dist/contract/router.d.ts.map +1 -0
  29. package/dist/contract/router.js +109 -0
  30. package/dist/contract/router.js.map +1 -0
  31. package/dist/server/_impl.d.ts +815 -0
  32. package/dist/server/_impl.d.ts.map +1 -0
  33. package/dist/server/_impl.js +189 -0
  34. package/dist/server/_impl.js.map +1 -0
  35. package/dist/server/index.d.ts +3 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +87 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/schema.d.ts +540 -0
  40. package/dist/server/schema.d.ts.map +1 -0
  41. package/dist/server/schema.js +86 -0
  42. package/dist/server/schema.js.map +1 -0
  43. package/dist/server/services/access.d.ts +78 -0
  44. package/dist/server/services/access.d.ts.map +1 -0
  45. package/dist/server/services/access.js +96 -0
  46. package/dist/server/services/access.js.map +1 -0
  47. package/dist/server/services/index.d.ts +15 -0
  48. package/dist/server/services/index.d.ts.map +1 -0
  49. package/dist/server/services/index.js +22 -0
  50. package/dist/server/services/index.js.map +1 -0
  51. package/dist/server/services/pages.d.ts +190 -0
  52. package/dist/server/services/pages.d.ts.map +1 -0
  53. package/dist/server/services/pages.js +242 -0
  54. package/dist/server/services/pages.js.map +1 -0
  55. package/dist/server/services/spaces.d.ts +95 -0
  56. package/dist/server/services/spaces.d.ts.map +1 -0
  57. package/dist/server/services/spaces.js +100 -0
  58. package/dist/server/services/spaces.js.map +1 -0
  59. package/migrations/0000_init.sql +43 -0
  60. package/migrations/0001_rls.sql +17 -0
  61. package/migrations/meta/0000_snapshot.json +358 -0
  62. package/migrations/meta/_journal.json +20 -0
  63. package/package.json +78 -0
  64. package/src/client/index.ts +67 -0
  65. package/src/client/rank.test.ts +92 -0
  66. package/src/client/rank.ts +100 -0
  67. package/src/contract/capabilities.ts +36 -0
  68. package/src/contract/events.ts +22 -0
  69. package/src/contract/index.ts +5 -0
  70. package/src/contract/models.ts +93 -0
  71. package/src/contract/permissions.ts +59 -0
  72. package/src/contract/router.ts +121 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Fractional indexing for `Page.position`.
3
+ *
4
+ * Copied from the tracker's `rank.ts` rather than imported: modules do not depend on one another,
5
+ * and a pure algorithm is the one thing worth duplicating. Keep them in step if either changes.
6
+ *
7
+ * A page tree must survive concurrent edits: two people dragging different pages at the same time
8
+ * should not have to renumber anything. Each page therefore carries a short string key, and moving
9
+ * one between two siblings only mints a new key strictly between its neighbours — no other row is
10
+ * touched, and the result sorts with a plain string comparison.
11
+ *
12
+ * The alphabet is ordered by code point (digits, uppercase, lowercase) so lexicographic order and
13
+ * numeric order agree, which is what `ORDER BY position` in Postgres relies on.
14
+ */
15
+
16
+ const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
17
+ const BASE = DIGITS.length
18
+
19
+ const indexOfDigit = (ch: string) => {
20
+ const i = DIGITS.indexOf(ch)
21
+ if (i < 0) throw new Error(`invalid rank character: ${JSON.stringify(ch)}`)
22
+ return i
23
+ }
24
+
25
+ /** True when `rank` only uses the rank alphabet and has no trailing zero (the canonical form). */
26
+ export function isValidRank(rank: string): boolean {
27
+ if (rank.length === 0) return false
28
+ for (const ch of rank) if (!DIGITS.includes(ch)) return false
29
+ return !rank.endsWith(DIGITS[0] as string)
30
+ }
31
+
32
+ /**
33
+ * A key strictly between `before` and `after`. Pass `null` for an open end: `rankBetween(null, first)`
34
+ * puts an item at the top of the list, `rankBetween(last, null)` at the bottom.
35
+ */
36
+ export function rankBetween(before: string | null, after: string | null): string {
37
+ const lo = before ?? ''
38
+ const hi = after ?? ''
39
+ if (lo && hi && lo >= hi) {
40
+ throw new Error(`rankBetween expects before < after, got ${JSON.stringify(lo)} >= ${JSON.stringify(hi)}`)
41
+ }
42
+
43
+ let prefix = ''
44
+ // once the digit we keep is strictly below the upper bound's digit, the rest of `hi` cannot
45
+ // constrain us any more and we are free to use the whole alphabet
46
+ let free = hi === ''
47
+
48
+ for (let i = 0; ; i++) {
49
+ const da = i < lo.length ? indexOfDigit(lo[i] as string) : -1
50
+ const db = free ? BASE : i < hi.length ? indexOfDigit(hi[i] as string) : 0
51
+ /**
52
+ * An absent lower bound is digit 0, not "below 0".
53
+ *
54
+ * Digit 0 can never be the last digit of a key: nothing sorts strictly between `''` and `'0'`,
55
+ * so a key ending in 0 leaves no room to insert in front of it. Treating the absent bound as -1
56
+ * makes the midpoint of (nothing, '2') come out as '0', and the next insertion at the front then
57
+ * looks for a key below '0', finds none, and appends a digit for ever — a loop inside a request
58
+ * handler that allocates until the process dies. Reaching it takes five insertions at the top of
59
+ * the same list.
60
+ */
61
+ const low = da >= 0 ? da : 0
62
+ if (db - low > 1) return prefix + DIGITS[Math.floor((low + db) / 2)]
63
+ prefix += DIGITS[low]
64
+ if (!free && low < db) free = true
65
+ }
66
+ }
67
+
68
+ /** First rank in an empty list. */
69
+ export function initialRank(): string {
70
+ return rankBetween(null, null)
71
+ }
72
+
73
+ /**
74
+ * `count` evenly spread ranks, for seeding a list. Cheaper and tidier than calling `rankBetween`
75
+ * repeatedly, which would bias every new key towards the end.
76
+ */
77
+ export function rankSequence(count: number): string[] {
78
+ const out: string[] = []
79
+ let prev: string | null = null
80
+ for (let i = 0; i < count; i++) {
81
+ prev = rankBetween(prev, null)
82
+ out.push(prev)
83
+ }
84
+ return out
85
+ }
86
+
87
+ /** Ascending comparator for anything carrying a `rank`. */
88
+ export function byRank(a: { rank: string }, b: { rank: string }): number {
89
+ return a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0
90
+ }
91
+
92
+ /**
93
+ * The rank an item needs to land at `targetIndex` of `ordered` (the list it is being dropped into,
94
+ * already excluding the dragged item).
95
+ */
96
+ export function rankForIndex(ordered: Array<{ rank: string }>, targetIndex: number): string {
97
+ const before = targetIndex > 0 ? (ordered[targetIndex - 1]?.rank ?? null) : null
98
+ const after = targetIndex < ordered.length ? (ordered[targetIndex]?.rank ?? null) : null
99
+ return rankBetween(before, after)
100
+ }
@@ -0,0 +1,36 @@
1
+ import { defineCapabilities } from '@kernhq/contracts'
2
+
3
+ /**
4
+ * Sub-features a workspace can switch off inside this module.
5
+ *
6
+ * Only the foundation exists today, and it is `required` — always on, never offered as a switch — so
7
+ * that the optional ones arriving later have something to depend on. A capability is not a second
8
+ * permission system: a permission asks whether this *person* may do something, a capability asks
9
+ * whether this *workspace* has the feature at all, and a procedure behind a disabled one answers
10
+ * `notFound` rather than `forbidden`, because a surface the workspace never enabled is not being
11
+ * withheld — it is not there.
12
+ *
13
+ * Planned, each of which is a real "different customers want different amounts of this": databases,
14
+ * public publishing, blogs and page analytics. They are declared when the feature lands, not before:
15
+ * a switch that does nothing is worse than no switch.
16
+ */
17
+ export const quireCapabilities = defineCapabilities([
18
+ {
19
+ id: 'pages',
20
+ label: 'Spaces and pages',
21
+ description: 'The page tree itself',
22
+ required: true,
23
+ },
24
+ ])
25
+
26
+ /**
27
+ * Which procedures belong to which capability, as data.
28
+ *
29
+ * Declared rather than inferred, because a missing `requiresCapability` is invisible: the procedure
30
+ * type-checks, the tests pass, and the only symptom is that a workspace which switched the feature
31
+ * off can still call it. `module.test.ts` reads this and fails when a procedure named here is not
32
+ * carrying the extra middleware.
33
+ *
34
+ * Everything Quire offers today belongs to the module as a whole, so this is empty.
35
+ */
36
+ export const quireCapabilityProcedures: Record<string, readonly string[]> = {}
@@ -0,0 +1,22 @@
1
+ import { defineEvent, Id, WorkspaceId } from '@kernhq/contracts'
2
+ import { z } from 'zod'
3
+
4
+ const page = z.object({ pageId: Id, spaceId: Id, workspaceId: WorkspaceId })
5
+
6
+ /** `<module>.<entity>.<action>`. Anything that emits one declares it here. */
7
+ export const quireEvents = {
8
+ spaceCreated: defineEvent('quire.space.created', z.object({ spaceId: Id, workspaceId: WorkspaceId })),
9
+ spaceUpdated: defineEvent('quire.space.updated', z.object({ spaceId: Id, workspaceId: WorkspaceId })),
10
+ spaceArchived: defineEvent(
11
+ 'quire.space.archived',
12
+ z.object({ spaceId: Id, workspaceId: WorkspaceId, archived: z.boolean() }),
13
+ ),
14
+ pageCreated: defineEvent('quire.page.created', page),
15
+ pageUpdated: defineEvent('quire.page.updated', page),
16
+ pageMoved: defineEvent('quire.page.moved', page.extend({ parentId: Id.nullable() })),
17
+ pageArchived: defineEvent('quire.page.archived', page.extend({ archived: z.boolean() })),
18
+ pageTrashed: defineEvent('quire.page.trashed', page.extend({ count: z.number().int().nonnegative() })),
19
+ pageRestored: defineEvent('quire.page.restored', page),
20
+ /** The page and its descendants are gone; anything holding a reference should drop it. */
21
+ pageDeleted: defineEvent('quire.page.deleted', page.extend({ pageIds: z.array(Id) })),
22
+ } as const
@@ -0,0 +1,5 @@
1
+ export * from './capabilities.js'
2
+ export * from './events.js'
3
+ export * from './models.js'
4
+ export * from './permissions.js'
5
+ export * from './router.js'
@@ -0,0 +1,93 @@
1
+ import { Id, Timestamp, UserId, WorkspaceId } from '@kernhq/contracts'
2
+ import { z } from 'zod'
3
+
4
+ /** Lowercase, 2-32 characters. Names the API prefix, the Postgres schema `mod_quire` and every event. */
5
+ export const MODULE_ID = 'quire'
6
+
7
+ /**
8
+ * How a space decides who may see it before any binding is consulted.
9
+ *
10
+ * `open` — every member of the workspace may read it.
11
+ * `restricted` — members may find it and see its name, and need a binding to read a page.
12
+ * `private` — only people with a binding know it exists at all.
13
+ */
14
+ export const SpaceVisibility = z.enum(['open', 'restricted', 'private'])
15
+ export type SpaceVisibility = z.infer<typeof SpaceVisibility>
16
+
17
+ export const Space = z.object({
18
+ id: Id,
19
+ workspaceId: WorkspaceId,
20
+ /** unique per workspace; it is what appears in the URL */
21
+ key: z
22
+ .string()
23
+ .min(2)
24
+ .max(48)
25
+ .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'lowercase letters, digits and dashes'),
26
+ name: z.string().min(1).max(120),
27
+ description: z.string().max(2000),
28
+ /** a Lucide icon name, or an emoji */
29
+ icon: z.string().max(64).nullable(),
30
+ visibility: SpaceVisibility,
31
+ /** the page shown when somebody opens the space; null until one is set */
32
+ homepageId: Id.nullable(),
33
+ createdBy: UserId.nullable(),
34
+ createdAt: Timestamp,
35
+ updatedAt: Timestamp,
36
+ archivedAt: Timestamp.nullable(),
37
+ })
38
+ export type Space = z.infer<typeof Space>
39
+
40
+ /**
41
+ * What a page *is*, which decides what a reader sees.
42
+ *
43
+ * `page` — has a published version and a draft. Everyone editing shares one live document; a reader
44
+ * without edit rights, and every public URL, is served the last published version instead. This is
45
+ * what a documentation site is made of.
46
+ * `live` — always live, like a shared note. There is no draft and no unpublished-changes state;
47
+ * versions still accumulate so history and restore work the same way.
48
+ * `database` — the page *is* a database. Its own body is the description above the view.
49
+ */
50
+ export const PageKind = z.enum(['page', 'live', 'database'])
51
+ export type PageKind = z.infer<typeof PageKind>
52
+
53
+ export const Page = z.object({
54
+ id: Id,
55
+ workspaceId: WorkspaceId,
56
+ spaceId: Id,
57
+ parentId: Id.nullable(),
58
+ /**
59
+ * A fractional index, not an integer. Moving one page between two others must not renumber its
60
+ * siblings: two people reordering at once would then write different numbers for the same rows.
61
+ */
62
+ position: z.string().min(1).max(256),
63
+ kind: PageKind,
64
+ title: z.string().max(300),
65
+ icon: z.string().max(64).nullable(),
66
+ coverUrl: z.string().max(2048).nullable(),
67
+ /** the version a reader without edit rights sees; null while a `page` has never been published */
68
+ publishedVersionId: Id.nullable(),
69
+ /** whether the live document has changed since `publishedVersionId` was written */
70
+ hasUnpublishedChanges: z.boolean(),
71
+ createdBy: UserId.nullable(),
72
+ updatedBy: UserId.nullable(),
73
+ createdAt: Timestamp,
74
+ updatedAt: Timestamp,
75
+ archivedAt: Timestamp.nullable(),
76
+ deletedAt: Timestamp.nullable(),
77
+ })
78
+ export type Page = z.infer<typeof Page>
79
+
80
+ /** A page in the sidebar tree: enough to draw a row, and nothing that costs a join. */
81
+ export const PageNode = z.object({
82
+ id: Id,
83
+ parentId: Id.nullable(),
84
+ position: z.string(),
85
+ kind: PageKind,
86
+ title: z.string(),
87
+ icon: z.string().nullable(),
88
+ hasChildren: z.boolean(),
89
+ archivedAt: Timestamp.nullable(),
90
+ })
91
+ export type PageNode = z.infer<typeof PageNode>
92
+
93
+ export const Ok = z.object({ ok: z.literal(true) })
@@ -0,0 +1,59 @@
1
+ import { definePermissions } from '@kernhq/contracts'
2
+
3
+ /**
4
+ * `<module>.<resource>.<action>`, each at the narrowest scope that works.
5
+ *
6
+ * Almost everything here is bound at **space** scope rather than workspace scope. That scope kind has
7
+ * existed in the permission model since before there was anything to use it, and this is what it was
8
+ * for: "everyone may read the Handbook, the design team may write it, and the contractor may read one
9
+ * page of it". Bindings resolve nearest-first, so a binding on a page beats one on its space, which
10
+ * beats one on the workspace — and a deny beats an allow at the same level.
11
+ */
12
+ export const quirePermissions = definePermissions([
13
+ {
14
+ key: 'quire.space.view',
15
+ label: 'See a space',
16
+ description: 'Find the space and read its name, whatever its pages allow',
17
+ scope: 'space',
18
+ defaultRoles: ['owner', 'admin', 'member', 'guest'],
19
+ dangerous: false,
20
+ },
21
+ {
22
+ key: 'quire.space.manage',
23
+ label: 'Create and configure spaces',
24
+ description: 'Rename, set the home page, change who may read it, archive it',
25
+ scope: 'space',
26
+ defaultRoles: ['owner', 'admin'],
27
+ dangerous: false,
28
+ },
29
+ {
30
+ key: 'quire.page.view',
31
+ label: 'Read pages',
32
+ scope: 'space',
33
+ defaultRoles: ['owner', 'admin', 'member', 'guest'],
34
+ dangerous: false,
35
+ },
36
+ {
37
+ key: 'quire.page.create',
38
+ label: 'Create pages',
39
+ scope: 'space',
40
+ defaultRoles: ['owner', 'admin', 'member'],
41
+ dangerous: false,
42
+ },
43
+ {
44
+ key: 'quire.page.edit',
45
+ label: 'Edit pages',
46
+ description: 'Write in a page, rename it, and move it in the tree',
47
+ scope: 'space',
48
+ defaultRoles: ['owner', 'admin', 'member'],
49
+ dangerous: false,
50
+ },
51
+ {
52
+ key: 'quire.page.delete',
53
+ label: 'Delete pages permanently',
54
+ description: 'Empty the trash. A purged page and its history cannot be recovered.',
55
+ scope: 'space',
56
+ defaultRoles: ['owner', 'admin'],
57
+ dangerous: true,
58
+ },
59
+ ])
@@ -0,0 +1,121 @@
1
+ import { baseContract, Id, PageInput, page, WorkspaceId } from '@kernhq/contracts'
2
+ import { z } from 'zod'
3
+ import { Ok, Page, PageKind, PageNode, Space, SpaceVisibility } from './models.js'
4
+
5
+ const ws = z.object({ workspaceId: WorkspaceId })
6
+ const t = (...tags: string[]) => ({ tags })
7
+
8
+ export const quireContract = {
9
+ spaces: {
10
+ list: baseContract
11
+ .route({ method: 'GET', path: '/spaces', ...t('spaces') })
12
+ .input(ws.extend({ includeArchived: z.boolean().default(false) }))
13
+ .output(z.array(Space)),
14
+ get: baseContract
15
+ .route({ method: 'GET', path: '/spaces/{spaceId}', ...t('spaces') })
16
+ .input(ws.extend({ spaceId: Id }))
17
+ .output(Space),
18
+ create: baseContract
19
+ .route({ method: 'POST', path: '/spaces', ...t('spaces') })
20
+ .input(
21
+ ws.extend({
22
+ key: Space.shape.key,
23
+ name: Space.shape.name,
24
+ description: z.string().max(2000).default(''),
25
+ icon: z.string().max(64).nullable().default(null),
26
+ visibility: SpaceVisibility.default('open'),
27
+ }),
28
+ )
29
+ .output(Space),
30
+ update: baseContract
31
+ .route({ method: 'PATCH', path: '/spaces/{spaceId}', ...t('spaces') })
32
+ .input(
33
+ ws.extend({
34
+ spaceId: Id,
35
+ name: Space.shape.name.optional(),
36
+ description: z.string().max(2000).optional(),
37
+ icon: z.string().max(64).nullable().optional(),
38
+ visibility: SpaceVisibility.optional(),
39
+ homepageId: Id.nullable().optional(),
40
+ }),
41
+ )
42
+ .output(Space),
43
+ archive: baseContract
44
+ .route({ method: 'POST', path: '/spaces/{spaceId}/archive', ...t('spaces') })
45
+ .input(ws.extend({ spaceId: Id, archived: z.boolean().default(true) }))
46
+ .output(Space),
47
+ },
48
+
49
+ pages: {
50
+ /**
51
+ * The whole tree of one space in one call. A wiki sidebar shows every level at once, and asking
52
+ * per level turns opening a space into a request per expanded node.
53
+ */
54
+ tree: baseContract
55
+ .route({ method: 'GET', path: '/spaces/{spaceId}/tree', ...t('pages') })
56
+ .input(ws.extend({ spaceId: Id, includeArchived: z.boolean().default(false) }))
57
+ .output(z.array(PageNode)),
58
+ get: baseContract
59
+ .route({ method: 'GET', path: '/pages/{pageId}', ...t('pages') })
60
+ .input(ws.extend({ pageId: Id }))
61
+ .output(Page),
62
+ /** Everything in the space's trash, newest first. */
63
+ trash: baseContract
64
+ .route({ method: 'GET', path: '/spaces/{spaceId}/trash', ...t('pages') })
65
+ .input(ws.extend({ spaceId: Id }).extend(PageInput.shape))
66
+ .output(page(Page)),
67
+ create: baseContract
68
+ .route({ method: 'POST', path: '/pages', ...t('pages') })
69
+ .input(
70
+ ws.extend({
71
+ spaceId: Id,
72
+ parentId: Id.nullable().default(null),
73
+ title: z.string().max(300).default(''),
74
+ kind: PageKind.default('page'),
75
+ icon: z.string().max(64).nullable().default(null),
76
+ /** place it after this sibling; null means first */
77
+ afterId: Id.nullable().default(null),
78
+ }),
79
+ )
80
+ .output(Page),
81
+ update: baseContract
82
+ .route({ method: 'PATCH', path: '/pages/{pageId}', ...t('pages') })
83
+ .input(
84
+ ws.extend({
85
+ pageId: Id,
86
+ title: z.string().max(300).optional(),
87
+ icon: z.string().max(64).nullable().optional(),
88
+ coverUrl: z.string().max(2048).nullable().optional(),
89
+ kind: PageKind.optional(),
90
+ }),
91
+ )
92
+ .output(Page),
93
+ /** Reparent, reorder, or both. `afterId` is the sibling to land behind; null means first. */
94
+ move: baseContract
95
+ .route({ method: 'POST', path: '/pages/{pageId}/move', ...t('pages') })
96
+ .input(ws.extend({ pageId: Id, parentId: Id.nullable(), afterId: Id.nullable().default(null) }))
97
+ .output(Page),
98
+ /** Out of the tree but not gone: still searchable, still restorable, no longer in the sidebar. */
99
+ archive: baseContract
100
+ .route({ method: 'POST', path: '/pages/{pageId}/archive', ...t('pages') })
101
+ .input(ws.extend({ pageId: Id, archived: z.boolean().default(true) }))
102
+ .output(Page),
103
+ /** Into the trash, with every descendant. Reversible until `purge`. */
104
+ trashPage: baseContract
105
+ .route({ method: 'POST', path: '/pages/{pageId}/trash', ...t('pages') })
106
+ .input(ws.extend({ pageId: Id }))
107
+ .output(z.object({ ok: z.literal(true), count: z.number().int().nonnegative() })),
108
+ restore: baseContract
109
+ .route({ method: 'POST', path: '/pages/{pageId}/restore', ...t('pages') })
110
+ .input(ws.extend({ pageId: Id }))
111
+ .output(Page),
112
+ /** Gone, with its collaborative document and every descendant. */
113
+ purge: baseContract
114
+ .route({ method: 'DELETE', path: '/pages/{pageId}', ...t('pages') })
115
+ .input(ws.extend({ pageId: Id }))
116
+ .output(z.object({ ok: z.literal(true), count: z.number().int().nonnegative() })),
117
+ },
118
+ } as const
119
+ export type QuireContract = typeof quireContract
120
+
121
+ export { Ok }