@drhomet/mithaq 0.0.0-stage → 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 (152) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/openstatus-time-picker.txt +21 -0
  3. package/LICENSES/payload.txt +22 -0
  4. package/LICENSES/ponytail.txt +21 -0
  5. package/README.md +160 -2
  6. package/dist/asas/actor.d.ts +96 -0
  7. package/dist/asas/actor.js +114 -0
  8. package/dist/asas/build.d.ts +239 -0
  9. package/dist/asas/build.js +834 -0
  10. package/dist/asas/cli/mithaq.d.ts +2 -0
  11. package/dist/asas/cli/mithaq.js +177 -0
  12. package/dist/asas/cli/upgrade.d.ts +33 -0
  13. package/dist/asas/cli/upgrade.js +256 -0
  14. package/dist/asas/http/fetch.d.ts +41 -0
  15. package/dist/asas/http/fetch.js +178 -0
  16. package/dist/asas/http/orpc.d.ts +45 -0
  17. package/dist/asas/http/orpc.js +153 -0
  18. package/dist/asas/runner/bulk.d.ts +20 -0
  19. package/dist/asas/runner/bulk.js +196 -0
  20. package/dist/asas/runner/errors.d.ts +63 -0
  21. package/dist/asas/runner/errors.js +75 -0
  22. package/dist/asas/runner/read.d.ts +72 -0
  23. package/dist/asas/runner/read.js +601 -0
  24. package/dist/asas/runner/runtime.d.ts +21 -0
  25. package/dist/asas/runner/runtime.js +8 -0
  26. package/dist/asas/runner/steps.d.ts +418 -0
  27. package/dist/asas/runner/steps.js +277 -0
  28. package/dist/asas/runner/store.d.ts +4 -0
  29. package/dist/asas/runner/store.js +21 -0
  30. package/dist/asas/runner/tenant.d.ts +54 -0
  31. package/dist/asas/runner/tenant.js +275 -0
  32. package/dist/asas/runner/write.d.ts +606 -0
  33. package/dist/asas/runner/write.js +1019 -0
  34. package/dist/auth/better-auth/access.d.ts +29 -0
  35. package/dist/auth/better-auth/access.js +103 -0
  36. package/dist/auth/better-auth/generate.d.ts +31 -0
  37. package/dist/auth/better-auth/generate.js +328 -0
  38. package/dist/auth/better-auth/index.d.ts +94 -0
  39. package/dist/auth/better-auth/index.js +81 -0
  40. package/dist/auth/better-auth/tenant.d.ts +26 -0
  41. package/dist/auth/better-auth/tenant.js +34 -0
  42. package/dist/auth/local/index.d.ts +285 -0
  43. package/dist/auth/local/index.js +738 -0
  44. package/dist/base/decimal.d.ts +9 -0
  45. package/dist/base/decimal.js +16 -0
  46. package/dist/base/fields.d.ts +124 -0
  47. package/dist/base/fields.js +52 -0
  48. package/dist/base/index.d.ts +47 -0
  49. package/dist/base/index.js +5 -0
  50. package/dist/base/json-schema.d.ts +7 -0
  51. package/dist/base/json-schema.js +35 -0
  52. package/dist/base/kinds.d.ts +75 -0
  53. package/dist/base/kinds.js +123 -0
  54. package/dist/base/rules.d.ts +73 -0
  55. package/dist/base/rules.js +81 -0
  56. package/dist/drizzle/index.d.ts +308 -0
  57. package/dist/drizzle/index.js +951 -0
  58. package/dist/drizzle/kinds.d.ts +28 -0
  59. package/dist/drizzle/kinds.js +43 -0
  60. package/dist/drizzle/migrate.d.ts +159 -0
  61. package/dist/drizzle/migrate.js +1328 -0
  62. package/dist/drizzle/proxy/index.d.ts +1 -0
  63. package/dist/drizzle/proxy/index.js +1 -0
  64. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  65. package/dist/drizzle/proxy/node-postgres.js +1 -0
  66. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  67. package/dist/drizzle/proxy/pg-core.js +1 -0
  68. package/dist/drizzle/rules.d.ts +336 -0
  69. package/dist/drizzle/rules.js +44 -0
  70. package/dist/index.d.ts +35 -0
  71. package/dist/index.js +26 -0
  72. package/dist/mithaq/define.d.ts +335 -0
  73. package/dist/mithaq/define.js +57 -0
  74. package/dist/mithaq/generate-messages.d.ts +38 -0
  75. package/dist/mithaq/generate-messages.js +235 -0
  76. package/dist/mithaq/language.d.ts +374 -0
  77. package/dist/mithaq/language.js +76 -0
  78. package/dist/mithaq/lock.d.ts +71 -0
  79. package/dist/mithaq/lock.js +329 -0
  80. package/dist/mithaq/message-file.d.ts +13 -0
  81. package/dist/mithaq/message-file.js +29 -0
  82. package/dist/mithaq/messages.d.ts +55 -0
  83. package/dist/mithaq/messages.js +111 -0
  84. package/dist/mithaq/refusal.d.ts +6 -0
  85. package/dist/mithaq/refusal.js +6 -0
  86. package/dist/mithaq/resolve.d.ts +10 -0
  87. package/dist/mithaq/resolve.js +1014 -0
  88. package/dist/mithaq/rows.d.ts +87 -0
  89. package/dist/mithaq/rows.js +174 -0
  90. package/dist/mithaq/rukn.d.ts +72 -0
  91. package/dist/mithaq/rukn.js +15 -0
  92. package/dist/mithaq/standard-schema.d.ts +38 -0
  93. package/dist/mithaq/standard-schema.js +2 -0
  94. package/dist/mithaq/types.d.ts +386 -0
  95. package/dist/mithaq/types.js +15 -0
  96. package/dist/mithaq/user.d.ts +19 -0
  97. package/dist/mithaq/user.js +16 -0
  98. package/dist/mithaq/where.d.ts +32 -0
  99. package/dist/mithaq/where.js +87 -0
  100. package/dist/rbac/index.d.ts +105 -0
  101. package/dist/rbac/index.js +182 -0
  102. package/dist/rbac/steps.d.ts +22 -0
  103. package/dist/rbac/steps.js +161 -0
  104. package/dist/rich-text/index.d.ts +28 -0
  105. package/dist/rich-text/index.js +59 -0
  106. package/dist/rich-text/rich-text.d.ts +106 -0
  107. package/dist/rich-text/rich-text.js +249 -0
  108. package/dist/rich-text/value.d.ts +55 -0
  109. package/dist/rich-text/value.js +126 -0
  110. package/dist/screen/client.d.ts +156 -0
  111. package/dist/screen/client.js +71 -0
  112. package/dist/screen/generate.d.ts +14 -0
  113. package/dist/screen/generate.js +268 -0
  114. package/dist/screen/index.d.ts +496 -0
  115. package/dist/screen/index.js +663 -0
  116. package/dist/screen/react.d.ts +97 -0
  117. package/dist/screen/react.js +58 -0
  118. package/dist/screen/studio.d.ts +24 -0
  119. package/dist/screen/studio.js +68 -0
  120. package/dist/service/index.d.ts +63 -0
  121. package/dist/service/index.js +15 -0
  122. package/dist/storage/allowed-type.d.ts +5 -0
  123. package/dist/storage/allowed-type.js +7 -0
  124. package/dist/storage/index.d.ts +42 -0
  125. package/dist/storage/index.js +43 -0
  126. package/dist/storage/storage.d.ts +386 -0
  127. package/dist/storage/storage.js +412 -0
  128. package/dist/targets.d.ts +14 -0
  129. package/dist/targets.js +16 -0
  130. package/dist/versions/index.d.ts +37 -0
  131. package/dist/versions/index.js +8 -0
  132. package/dist/versions/versions.d.ts +104 -0
  133. package/dist/versions/versions.js +160 -0
  134. package/dist/vite.d.ts +7 -0
  135. package/dist/vite.js +81 -0
  136. package/dist/zod/index.d.ts +193 -0
  137. package/dist/zod/index.js +122 -0
  138. package/dist/zod/kinds.d.ts +36 -0
  139. package/dist/zod/kinds.js +58 -0
  140. package/dist/zod/rules.d.ts +117 -0
  141. package/dist/zod/rules.js +67 -0
  142. package/docs/packages/mithaq/README.md +962 -0
  143. package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
  144. package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
  145. package/docs/packages/mithaq/src/drizzle/README.md +39 -0
  146. package/docs/packages/mithaq-admin/README.md +312 -0
  147. package/docs/templates/asas/README.md +1056 -0
  148. package/package.json +92 -4
  149. package/skills/add-field/SKILL.md +27 -0
  150. package/skills/add-kayan/SKILL.md +26 -0
  151. package/skills/eject-admin-piece/SKILL.md +28 -0
  152. package/skills/upgrade-mithaq/SKILL.md +35 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 drhomet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OpenStatus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018-2026 Payload CMS, LLC <info@payloadcms.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DietrichGebert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,161 @@
1
- # Temporary Holding Version
1
+ # mithaq
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@drhomet/mithaq)](https://www.npmjs.com/package/@drhomet/mithaq)
4
+
5
+ mithaq is a TypeScript language for declaring a backend and its admin once, and building the database, API, validation, access rules and admin screens from that one declaration.
6
+
7
+ The package is `@drhomet/mithaq`. It is pre-1.0, so the API may still change between releases.
8
+
9
+ The name is Arabic for a covenant. The other words in the API are Arabic too, and each is defined where it first appears below.
10
+
11
+ ## Why it exists
12
+
13
+ - **One declaration drives everything.** A kayan's fields, kinds and rules become the Postgres table (through drizzle), the input schema (through zod), the HTTP operations and the admin forms and lists. Nothing is declared twice, so nothing drifts.
14
+ - **The vocabulary is frozen.** The kinds, rules and targets mithaq ships change only by the maintainer's explicit decision. A project can also seal its own language in a signed `mithaq.lock`. `mithaq lock --check`, which the apps run before `dev`, `build` and `preview`, then fails when the vocabulary no longer matches the lock. Signing a change takes a code shown in a popup, never in the command's output, so a script or an agent running `mithaq lock` cannot approve it.
15
+ - **Tenant isolation is enforced in core.** A kayan that places the tenant field belongs to one organization. Core adds the tenant scope to every operation and combines it with the project's own access with AND, so no access rule can widen it. Writes land in the active organization, and `unique` rules and index groups are built per organization.
16
+ - **Defaults are explicit.** The project names every default it uses (`defaultOperations`, `defaultFields`, `defaultAuthorize`, the limits) in its own code. Core does not fall back to a default of its own: a configuration that leaves a required one out is a type error or a refusal from `buildAsas`.
17
+ - **The admin is your code when you want it.** The screens are shadcn components the project imports from `mithaq-admin` and draws with its own primitives; `mithaq eject` puts any one of them into the project to edit like any other file.
18
+
19
+ ## Quick start
20
+
21
+ ```sh
22
+ bun create mithaq my-app --full
23
+ ```
24
+
25
+ This creates a Bun workspace of the template's three steps, the language (`packages/mithaq`), a package per feature (`packages/features/*`) and the asas that builds them (`packages/asas`), and two apps: the tenant app (`apps/asas-tanstack`), which serves the dashboard and the portal, and the backoffice console (`apps/asas-backoffice`), for staff who work across every organization. It writes a `.env` with fresh secrets and runs `bun install`. `--full` takes every add-on, the whole worked example; without it the project is minimal, auth and a `tags` example, and `--with roles,courses` picks add-ons.
26
+
27
+ Then, with PostgreSQL 15 or later serving the database that `DATABASE_URL` names in `packages/asas/.env`:
28
+
29
+ ```sh
30
+ cd my-app
31
+ cd packages/asas
32
+ # push the schema, make the dev admin and fill its organization, development only
33
+ bun run dev:setup --seed
34
+ cd ../..
35
+ bun run --cwd apps/asas-tanstack dev # the tenant app, http://localhost:5173
36
+ bun run --cwd apps/asas-backoffice dev # in a second terminal: the backoffice console, http://localhost:5174
37
+ ```
38
+
39
+ Sign in to both apps with `DEV_ADMIN_EMAIL` and `DEV_ADMIN_PASSWORD` from `packages/asas/.env`: the dev admin owns "Dev admin's workspace", which `--seed` filled, and is the backoffice's `super_admin`.
40
+
41
+ In production, `connect()` does not push the schema; it checks the migration files instead. [Running in production](https://github.com/drhomet/mithaq/blob/main/templates/asas/README.md#running-in-production) explains how to apply them.
42
+
43
+ ## A small example
44
+
45
+ A language comes from `defineMithaq`. Its arkan (singular rukn, "pillar") are the pluggable parts that bring kinds and rules; `base` brings the shipped ones. Its targets are what each declaration is built into.
46
+
47
+ ```ts
48
+ // src/mithaq.ts
49
+ import {
50
+ base, defaultAuthorize, defaultBulkLimit, defaultDepth, defaultJoinLimit, defaultLimit, defaultMaxLimit,
51
+ defineMithaq, drizzleAdapter, drizzleKinds, drizzleRules, drizzleIdKinds, serviceTarget, zodKinds, zodRules, zodIdKinds, zodTarget,
52
+ } from 'mithaq'
53
+
54
+ export const { defineKayan, buildAsas } = defineMithaq({
55
+ locales: ['en', 'ar'] as const,
56
+ arkan: [base],
57
+ targets: {
58
+ drizzle: drizzleAdapter({ connectionString: process.env.DATABASE_URL!, kinds: drizzleKinds, rules: drizzleRules, idKinds: drizzleIdKinds }),
59
+ zod: zodTarget({ kinds: zodKinds, rules: zodRules, idKinds: zodIdKinds }),
60
+ service: serviceTarget({
61
+ authorize: defaultAuthorize,
62
+ depth: defaultDepth,
63
+ defaultLimit,
64
+ maxLimit: defaultMaxLimit,
65
+ joinLimit: defaultJoinLimit,
66
+ bulkLimit: defaultBulkLimit,
67
+ store: 'drizzle',
68
+ schema: 'zod',
69
+ }),
70
+ },
71
+ })
72
+ ```
73
+
74
+ A kayan ("entity") is one data type: its fields, their kinds and rules, its access and its operations. Every label carries one string per locale the language declares, and a missing one is a type error.
75
+
76
+ ```ts
77
+ // src/tags.ts
78
+ import { defaultFields, defaultOperations } from 'mithaq'
79
+ import { defineKayan } from './mithaq.ts'
80
+
81
+ export const tags = defineKayan({
82
+ name: 'tags',
83
+ access: { read: () => true },
84
+ targets: { service: { operations: defaultOperations } },
85
+ fields: {
86
+ ...defaultFields,
87
+ name: {
88
+ kind: 'text',
89
+ label: { en: 'Name', ar: 'الاسم' },
90
+ rules: { required: true, unique: true, trim: true, maxLength: 60 },
91
+ },
92
+ },
93
+ })
94
+ ```
95
+
96
+ `buildAsas` builds the asas ("foundation"), the runtime:
97
+
98
+ ```ts
99
+ // src/asas.ts
100
+ import { defaultMaxBodyBytes, fetchHttp } from 'mithaq'
101
+ import { buildAsas } from './mithaq.ts'
102
+ import { tags } from './tags.ts'
103
+
104
+ export const asas = buildAsas({ kayan: [tags], http: [fetchHttp({ maxBodyBytes: defaultMaxBodyBytes, onError: console.error })] })
105
+ ```
106
+
107
+ `asas.handler(request)` answers a `Request` with a `Response`; mount it under `/api` in any server. With `fetchHttp`, `POST /api/tags/create` with a JSON body creates a tag, and `POST /api/tags/find` lists them. In development, `asas.connect()` pushes the schema to Postgres. A kayan that declares no access answers only the admin the build names, so this one opens reads to everyone.
108
+
109
+ To make a kayan belong to one organization, add the tenant target to the language and place its field:
110
+
111
+ ```ts
112
+ // in defineMithaq's targets:
113
+ tenant: betterAuthTenant({ relationTo: 'organization', onDelete: 'restrict', newRows: 'active', organizations: 'many' }),
114
+
115
+ // in a kayan's fields, with `tenant` taken from the language's `targets`:
116
+ organization: tenant.field,
117
+ ```
118
+
119
+ `unique: true` on that kayan is then unique within each organization, and `indexes: [{ within: 'organization', unique: [['code', 'term']] }]` declares a compound one. [Tenancy](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/runner/README.md) has the full rules.
120
+
121
+ ## What's in the box
122
+
123
+ - **Store:** Postgres through drizzle (`drizzleAdapter`), with a schema push in development and migration files written by `mithaq migrate` for production.
124
+ - **Validation:** zod schemas derived from every kayan (`zodTarget`). A field can carry its own zod shape instead.
125
+ - **Runtime:** the operation runner (`serviceTarget`) with the default operations (create, update, delete, find, findByID, count, updateMany, deleteMany), steps around every write, an audit step, soft delete and bulk limits.
126
+ - **HTTP:** plain fetch routes (`fetchHttp`), or a typed client and OpenAPI through oRPC (`mithaq/orpc`).
127
+ - **Access:** role-based access with permission keys per operation and per field (`rbacTarget`, `betterAuthTarget` to keep roles in Better Auth's own tables, or `memberRoleTarget` for Better Auth's own member roles alone).
128
+ - **Tenancy:** `betterAuthTenant`, over Better Auth organizations.
129
+ - **Auth:** Better Auth (`betterAuth`) or local accounts (`local`), with `mithaq generate auth` writing the Better Auth kayan.
130
+ - **Versions and drafts:** the `versions` rukn keeps a row's past versions, with drafts, autosave and restore.
131
+ - **Files:** the `storage` rukn and the `upload` kind. The template ships adapters for local disk, Cloudflare R2, S3-compatible services and UploadThing as project files.
132
+ - **Rich text:** the `richText` rukn, over Lexical.
133
+ - **Admin screens:** `screenTarget` describes each screen; `mithaq generate screens` writes its routes into the host app, and the React components come from the `mithaq-admin` package, drawn with the app's own shadcn primitives, and a project ejects the ones it wants to own. `mithaq upgrade --check` compares the files it holds with the installed version.
134
+ - **Copy:** `mithaq generate messages` writes the project's labels and messages to one file per kayan, and refuses a label missing a locale.
135
+
136
+ `mithaq --help` lists every command.
137
+
138
+ ## Requirements
139
+
140
+ - npm, pnpm 10.6 or later, Yarn 1 or [Bun](https://bun.sh) 1.3, whichever runs `create mithaq`. Yarn 2 and later install a copy of mithaq for each workspace of a monorepo, which the build refuses, so a monorepo does not take them yet.
141
+ - Node.js 24 or later.
142
+ - PostgreSQL 15 or later.
143
+ - TypeScript, installed by the project.
144
+
145
+ ## Documentation
146
+
147
+ Each guide is the `README.md` of the folder it describes.
148
+
149
+ - [Working in a mithaq project](https://github.com/drhomet/mithaq/blob/main/templates/asas/README.md): getting started, where each file lives, adding a kayan, a feature or a step, and running in production. It ships in every created project.
150
+ - [The mithaq language](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/README.md): the language, kinds, rules, kayan, targets, the runner and access, and adding a rukn, a kind, a target or a configurable feature.
151
+ - [Tenancy](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/runner/README.md): per-organization kayan, what core enforces, and per-organization indexes.
152
+ - [The `mithaq` command](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/asas/cli/README.md): every command, flag and exit code.
153
+ - [Migrations in production](https://github.com/drhomet/mithaq/blob/main/packages/mithaq/src/drizzle/README.md): `pending: 'refuse'` or `'run'`, the advisory lock, and applying migrations by hand.
154
+ - [The admin package](https://github.com/drhomet/mithaq/blob/main/packages/mithaq-admin/README.md): the screen components, ejecting one, `mithaq upgrade --check`, migrating a project that holds copies, and adding a registry item.
155
+ - [Contributing](https://github.com/drhomet/mithaq/blob/main/CONTRIBUTING.md): setting up, the checks, the repository layout, and sending a pull request.
156
+
157
+ ## Status and license
158
+
159
+ mithaq is pre-1.0. The template that `bun create mithaq` creates is the worked example, and every file there is commented.
160
+
161
+ MIT licensed; see [LICENSE](https://github.com/drhomet/mithaq/blob/main/LICENSE). mithaq includes code adapted from Payload and from openstatus's time picker, both MIT licensed; their notices are in [LICENSES/payload.txt](https://github.com/drhomet/mithaq/blob/main/LICENSES/payload.txt) and [LICENSES/openstatus-time-picker.txt](https://github.com/drhomet/mithaq/blob/main/LICENSES/openstatus-time-picker.txt).
@@ -0,0 +1,96 @@
1
+ import type { Assigned } from '../rbac/index.ts';
2
+ import type { ResolvedKayan } from '../mithaq/types.ts';
3
+ import type { OperationEffect } from './runner/steps.ts';
4
+ export type AuthStrategyArgs = {
5
+ headers: Headers;
6
+ canSetHeaders?: boolean;
7
+ isGraphQL?: boolean;
8
+ strategyName?: string;
9
+ };
10
+ export type AuthStrategyResult<Actor = unknown> = {
11
+ responseHeaders?: Headers;
12
+ user: Actor | null;
13
+ };
14
+ /** An auth strategy: `authenticate` resolves a request's headers to an actor, or to `null`. */
15
+ export type AuthStrategy<Actor = unknown> = {
16
+ name: string;
17
+ authenticate(args: AuthStrategyArgs): Promise<AuthStrategyResult<Actor>> | AuthStrategyResult<Actor>;
18
+ /** What the HTTP layer mounts at `/api/auth/<strategy name>/*`. */
19
+ handler?(request: Request): Promise<Response>;
20
+ };
21
+ /**
22
+ * What a kayan's `auth` is: a shipped provider declaration, `local(...)` or `betterAuth(...)`,
23
+ * which `buildAsas` builds, or a strategy the project wrote.
24
+ */
25
+ export type KayanAuth = AuthStrategy | {
26
+ provider: 'better-auth' | 'local';
27
+ };
28
+ export declare const isAuthDeclaration: (auth: KayanAuth) => auth is {
29
+ provider: 'better-auth' | 'local';
30
+ };
31
+ type TrustedActor = {
32
+ activeOrganizationId?: unknown;
33
+ collection: string;
34
+ id?: unknown;
35
+ memberRole?: unknown;
36
+ organizations?: readonly ({
37
+ id?: unknown;
38
+ } & Record<string, unknown>)[];
39
+ role?: unknown;
40
+ permissions?: true | ReadonlySet<string>;
41
+ roles?: readonly string[];
42
+ strategy?: unknown;
43
+ admin?: boolean;
44
+ };
45
+ /** Record the build's admin answer on an actor a strategy returned; an unmarked object holds none. */
46
+ export declare const markAdminAnswer: (actor: unknown, admin: boolean) => boolean;
47
+ /** Whether the build answered this actor as its administrator. */
48
+ export declare const isAdminActor: (actor: unknown) => boolean;
49
+ export declare const markAuthActor: <Actor extends object>(actor: Actor) => Actor;
50
+ /**
51
+ * The permission keys the access target resolved for this actor, `true` for every key,
52
+ * or nothing where no target graded. Read from the build's snapshot, never the actor object.
53
+ */
54
+ export declare const actorPermissions: (actor: unknown) => true | ReadonlySet<string> | undefined;
55
+ /** The role slugs the access target resolved for this actor, read from the build's snapshot. */
56
+ export declare const actorRoles: (actor: unknown) => readonly string[] | undefined;
57
+ export declare const authActor: (actor: unknown) => TrustedActor | null;
58
+ export declare const actor: <Actor>({ strategies, headers, onError, }: {
59
+ strategies: readonly AuthStrategy<Actor>[];
60
+ headers: Headers;
61
+ onError: (error: unknown) => void;
62
+ }) => Promise<Actor | null>;
63
+ /**
64
+ * What the build's access target answers for one actor: the slugs assigned in the tenant
65
+ * and every key they hold, or nothing where the build declares no access target.
66
+ */
67
+ export type AssignedResolver = (args: {
68
+ tenant: unknown;
69
+ member: unknown;
70
+ /** The actor being made, before it is marked. */
71
+ actor: object;
72
+ }) => Promise<Assigned | undefined> | Assigned | undefined;
73
+ export declare const resolveAssigned: ({ resolver, onError, ...args }: Parameters<AssignedResolver>[0] & {
74
+ resolver: AssignedResolver | undefined;
75
+ onError: (error: unknown) => void;
76
+ }) => Promise<Assigned | undefined>;
77
+ export type LocalAuthPermissions = {
78
+ kayan: readonly ResolvedKayan[];
79
+ can(args: {
80
+ kayan: ResolvedKayan;
81
+ effect: OperationEffect;
82
+ actor: unknown;
83
+ }): Promise<{
84
+ allowed: boolean;
85
+ operations: Record<string, boolean>;
86
+ }>;
87
+ canAccessAdmin(actor: unknown): Promise<boolean>;
88
+ screens(actor: unknown): Promise<readonly string[]>;
89
+ };
90
+ export declare const accessReport: (permissions: LocalAuthPermissions, actor: unknown) => Promise<{
91
+ canAccessAdmin: boolean;
92
+ collections: Record<string, unknown>;
93
+ screens: readonly string[];
94
+ permissions?: string | string[] | undefined;
95
+ }>;
96
+ export {};
@@ -0,0 +1,114 @@
1
+ // Asks for `authenticate`, not `provider`: a project's own strategy may carry a
2
+ // `provider` key of its own meaning.
3
+ export const isAuthDeclaration = (auth) => !('authenticate' in auth);
4
+ const trustedActors = new WeakMap();
5
+ /** Record the build's admin answer on an actor a strategy returned; an unmarked object holds none. */
6
+ export const markAdminAnswer = (actor, admin) => {
7
+ const trusted = authActor(actor);
8
+ if (trusted)
9
+ trusted.admin = admin;
10
+ return admin;
11
+ };
12
+ /** Whether the build answered this actor as its administrator. */
13
+ export const isAdminActor = (actor) => authActor(actor)?.admin === true;
14
+ export const markAuthActor = (actor) => {
15
+ const source = actor;
16
+ const claimed = source.permissions;
17
+ trustedActors.set(actor, {
18
+ activeOrganizationId: source.activeOrganizationId,
19
+ collection: source.collection,
20
+ id: source.id,
21
+ memberRole: source.memberRole,
22
+ organizations: source.organizations?.map((organization) => ({ ...organization })),
23
+ role: source.role,
24
+ // Present decides, absent stands aside. Any other shape is a store that answered
25
+ // unreadably and holds no key: reading it as absent would fail open.
26
+ permissions: 'permissions' in source
27
+ ? claimed === '*'
28
+ ? true
29
+ : new Set(Array.isArray(claimed) && claimed.every((key) => typeof key === 'string') ? claimed : [])
30
+ : undefined,
31
+ roles: Array.isArray(source.roles) && source.roles.every((one) => typeof one === 'string')
32
+ ? [...source.roles]
33
+ : undefined,
34
+ strategy: source.strategy,
35
+ });
36
+ return actor;
37
+ };
38
+ /**
39
+ * The permission keys the access target resolved for this actor, `true` for every key,
40
+ * or nothing where no target graded. Read from the build's snapshot, never the actor object.
41
+ */
42
+ export const actorPermissions = (actor) => authActor(actor)?.permissions;
43
+ /** The role slugs the access target resolved for this actor, read from the build's snapshot. */
44
+ export const actorRoles = (actor) => authActor(actor)?.roles;
45
+ export const authActor = (actor) => actor && typeof actor === 'object' ? trustedActors.get(actor) ?? null : null;
46
+ // First non-null actor wins; a strategy that throws is handed to `onError` and skipped.
47
+ export const actor = async ({ strategies, headers, onError, }) => {
48
+ for (const strategy of strategies) {
49
+ try {
50
+ const found = await strategy.authenticate({
51
+ canSetHeaders: true,
52
+ headers,
53
+ strategyName: strategy.name,
54
+ });
55
+ if (found.user)
56
+ return found.user;
57
+ }
58
+ catch (error) {
59
+ onError(new Error(`Auth strategy "${strategy.name}" failed.`, { cause: error }));
60
+ }
61
+ }
62
+ return null;
63
+ };
64
+ // A throw must not leave: the strategy loop would turn a store outage into
65
+ // "not signed in" instead of a 403. A failed resolver holds no key and `onError` is told.
66
+ export const resolveAssigned = async ({ resolver, onError, ...args }) => {
67
+ if (!resolver)
68
+ return undefined;
69
+ try {
70
+ return await resolver(args);
71
+ }
72
+ catch (error) {
73
+ onError(new Error('The access target failed to resolve, so this actor holds no role and no key.', { cause: error }));
74
+ return { permissions: [], roles: [] };
75
+ }
76
+ };
77
+ // Reports and never grants. A kayan the actor may not read is left out, and so is a
78
+ // field none of `read`, `create` and `update` holds: the report discloses what it names.
79
+ export const accessReport = async (permissions, actor) => {
80
+ // First: the build records its answer on the actor, and a field callback below reads it.
81
+ const canAccessAdmin = await permissions.canAccessAdmin(actor);
82
+ const collections = Object.create(null);
83
+ for (const kayan of permissions.kayan) {
84
+ const effects = ['create', 'read', 'update', 'delete'];
85
+ const allowed = {};
86
+ const operations = {};
87
+ for (const effect of effects) {
88
+ const answer = await permissions.can({ kayan, effect, actor });
89
+ allowed[effect] = answer.allowed;
90
+ Object.assign(operations, answer.operations);
91
+ }
92
+ if (!allowed.read)
93
+ continue;
94
+ const fields = {};
95
+ for (const field of Object.values(kayan.fields)) {
96
+ const answers = {};
97
+ for (const effect of ['create', 'read', 'update']) {
98
+ const own = field.access?.[effect];
99
+ answers[effect] = allowed[effect] && (!own || !!(await own({ user: actor })));
100
+ }
101
+ if (answers.create || answers.read || answers.update)
102
+ fields[field.name] = answers;
103
+ }
104
+ collections[kayan.name] = { ...allowed, fields, operations };
105
+ }
106
+ // The actor's own keys, so a screen can answer a provider action such as `user.ban` without trying it.
107
+ const held = actorPermissions(actor);
108
+ return {
109
+ canAccessAdmin,
110
+ collections,
111
+ screens: await permissions.screens(actor),
112
+ ...(held === undefined ? {} : { permissions: held === true ? '*' : [...held] }),
113
+ };
114
+ };