@pathmx/core 0.5.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/LICENSE.md +172 -0
- package/README.md +111 -0
- package/actions/actions.ts +307 -0
- package/actions/draft.ts +105 -0
- package/actions/transaction.ts +164 -0
- package/assets.ts +149 -0
- package/authority/access.ts +820 -0
- package/authority/actors.ts +271 -0
- package/authority/agents.ts +95 -0
- package/authority/credentials.ts +144 -0
- package/authority/model.ts +71 -0
- package/authority/sharing.ts +89 -0
- package/browser-bundle.ts +61 -0
- package/canonical.ts +41 -0
- package/cli/dev.ts +134 -0
- package/cli/export.ts +74 -0
- package/cli/index.ts +52 -0
- package/cli/lint.ts +164 -0
- package/cli/mv.ts +30 -0
- package/cli/perms.ts +182 -0
- package/cli/plugin-packages.ts +159 -0
- package/cli/plugins.ts +95 -0
- package/cli/query.ts +225 -0
- package/cli/readme.md +95 -0
- package/cli/release.ts +75 -0
- package/cli/rm.ts +22 -0
- package/cli/serve.ts +100 -0
- package/cli/tree.ts +131 -0
- package/cli/update.ts +78 -0
- package/cli/utils.ts +53 -0
- package/core.ts +96 -0
- package/database.ts +189 -0
- package/dependencies.ts +60 -0
- package/dist/pathmx.js +1224 -0
- package/document.ts +194 -0
- package/environment.ts +110 -0
- package/environments/bun-plugins.ts +68 -0
- package/environments/bun-publish.ts +194 -0
- package/environments/bun.ts +223 -0
- package/globals.d.ts +14 -0
- package/graph.ts +153 -0
- package/host/compiler.ts +314 -0
- package/host/create.ts +66 -0
- package/host/engine.ts +630 -0
- package/host/plugin-host.ts +487 -0
- package/host/render.ts +102 -0
- package/host/response.ts +20 -0
- package/host/shell.html +15 -0
- package/host/source-sync.ts +254 -0
- package/html-attributes.ts +15 -0
- package/html.ts +48 -0
- package/icons/LICENSE +43 -0
- package/icons/lucide.ts +54 -0
- package/index.ts +75 -0
- package/invalidation.ts +85 -0
- package/log.ts +84 -0
- package/ops/href.ts +87 -0
- package/ops/index.ts +9 -0
- package/ops/move-source.ts +114 -0
- package/ops/plan.ts +47 -0
- package/ops/remove-source.ts +70 -0
- package/package.json +46 -0
- package/path.ts +75 -0
- package/pattern.ts +314 -0
- package/perf.ts +88 -0
- package/plugins/actor-path.ts +43 -0
- package/plugins/actor-profile.ts +55 -0
- package/plugins/agent-browsing.ts +183 -0
- package/plugins/app-component.ts +23 -0
- package/plugins/block-layout/index.ts +78 -0
- package/plugins/block-layout/theme.css +90 -0
- package/plugins/callout/index.ts +236 -0
- package/plugins/callout/theme.css +138 -0
- package/plugins/canonical-links.ts +144 -0
- package/plugins/code-highlight/client.ts +89 -0
- package/plugins/code-highlight/index.ts +147 -0
- package/plugins/code-highlight/theme.css +89 -0
- package/plugins/context.ts +131 -0
- package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
- package/plugins/datatype/OFL.txt +96 -0
- package/plugins/datatype/index.ts +287 -0
- package/plugins/definition-list.ts +236 -0
- package/plugins/directive-definitions.ts +13 -0
- package/plugins/embedding/client.ts +88 -0
- package/plugins/embedding/index.ts +43 -0
- package/plugins/image/index.ts +68 -0
- package/plugins/image/theme.css +43 -0
- package/plugins/include.ts +119 -0
- package/plugins/index.ts +91 -0
- package/plugins/input/authoring.ts +236 -0
- package/plugins/input/config.ts +85 -0
- package/plugins/input/index.ts +359 -0
- package/plugins/input/model.ts +173 -0
- package/plugins/input/state.ts +272 -0
- package/plugins/layout.ts +313 -0
- package/plugins/literate/client.ts +208 -0
- package/plugins/literate/index.ts +742 -0
- package/plugins/math/index.ts +140 -0
- package/plugins/meta.ts +42 -0
- package/plugins/navigation/index.ts +158 -0
- package/plugins/navigation/model.ts +364 -0
- package/plugins/preset.ts +65 -0
- package/plugins/query/index.ts +322 -0
- package/plugins/rss/index.ts +46 -0
- package/plugins/runtime/actions.ts +62 -0
- package/plugins/runtime/client.ts +8 -0
- package/plugins/runtime/document.ts +506 -0
- package/plugins/runtime/error.ts +154 -0
- package/plugins/runtime/heading-links.ts +31 -0
- package/plugins/runtime/index.ts +104 -0
- package/plugins/runtime/live.ts +51 -0
- package/plugins/runtime/navigation.ts +113 -0
- package/plugins/runtime/pending.ts +53 -0
- package/plugins/runtime/publication.ts +19 -0
- package/plugins/runtime/start.ts +177 -0
- package/plugins/runtime/static-client.ts +6 -0
- package/plugins/runtime/static-live.ts +188 -0
- package/plugins/runtime/view-transition.ts +72 -0
- package/plugins/runtime/viewport.ts +110 -0
- package/plugins/schema.ts +24 -0
- package/plugins/scripts.ts +140 -0
- package/plugins/source-preview/client.ts +256 -0
- package/plugins/source-preview/index.ts +132 -0
- package/plugins/source-preview/model.ts +11 -0
- package/plugins/source-preview/preview.css +107 -0
- package/plugins/styles.ts +145 -0
- package/plugins/theme/components.ts +10 -0
- package/plugins/theme/foundation.css +358 -0
- package/plugins/theme/index.ts +333 -0
- package/plugins/theme/prose.css +284 -0
- package/plugins/theme/scheme.js +63 -0
- package/plugins/theme/toggle.css +44 -0
- package/plugins/theme/view-transition.css +68 -0
- package/plugins/toc/client.ts +146 -0
- package/plugins/toc/index.ts +232 -0
- package/plugins/toc/theme.css +172 -0
- package/plugins/types.ts +538 -0
- package/problem.ts +41 -0
- package/project.ts +6 -0
- package/publish/build.ts +402 -0
- package/publish/concurrent.ts +32 -0
- package/publish/identity.ts +63 -0
- package/publish/index.ts +11 -0
- package/publish/location.ts +167 -0
- package/publish/model.ts +58 -0
- package/publish/plan.ts +300 -0
- package/publish/validate.ts +150 -0
- package/query/candidate-index.ts +101 -0
- package/query/dependency-index.ts +99 -0
- package/query/fields.ts +216 -0
- package/query/invalidation.ts +48 -0
- package/query/kernel.ts +776 -0
- package/query/model.ts +79 -0
- package/repository.ts +374 -0
- package/runtime-profile.ts +34 -0
- package/schema/model.ts +92 -0
- package/schema/registry.ts +642 -0
- package/schema/standard.ts +226 -0
- package/server/context.ts +94 -0
- package/server/css-imports.ts +66 -0
- package/server/http.ts +333 -0
- package/server/page.ts +21 -0
- package/server/protocols.ts +12 -0
- package/server/representations.ts +66 -0
- package/server/router.ts +131 -0
- package/server/routes.ts +202 -0
- package/server/serve.ts +5 -0
- package/server/watch.ts +120 -0
- package/source/blocks.ts +71 -0
- package/source/format.ts +164 -0
- package/source/index.ts +67 -0
- package/source/interpolate.ts +63 -0
- package/source/markdown/blocks.ts +46 -0
- package/source/markdown/comments.ts +32 -0
- package/source/markdown/fences.ts +67 -0
- package/source/markdown/headings.ts +88 -0
- package/source/markdown/index.ts +34 -0
- package/source/markdown/inline-code.ts +6 -0
- package/source/markdown/links.ts +326 -0
- package/source/markdown/lists.ts +223 -0
- package/source/markdown/prose.ts +74 -0
- package/source/markdown/scan.ts +79 -0
- package/source/markdown/text.ts +82 -0
- package/source/meta.ts +87 -0
- package/source/source.ts +121 -0
- package/source/topmatter.ts +57 -0
- package/source/utils.ts +21 -0
- package/source-diagnostic.ts +63 -0
- package/text-file.ts +26 -0
- package/util.ts +35 -0
- package/view/components.ts +499 -0
- package/view/model.ts +91 -0
- package/view/projection.ts +162 -0
- package/view/session.ts +146 -0
- package/view/syntax.ts +372 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { Plugin } from "../plugins/types.ts"
|
|
2
|
+
import { SourceDiagnosticError } from "../source-diagnostic.ts"
|
|
3
|
+
import type { Source } from "../source/index.ts"
|
|
4
|
+
import type {
|
|
5
|
+
Actor,
|
|
6
|
+
ActorProfile,
|
|
7
|
+
ActorProfileRef,
|
|
8
|
+
ActorProfiles,
|
|
9
|
+
AgentActor,
|
|
10
|
+
KnownActorIndex,
|
|
11
|
+
UserActor,
|
|
12
|
+
} from "./model.ts"
|
|
13
|
+
|
|
14
|
+
const USERNAME = /^[a-z0-9][a-z0-9._-]*$/
|
|
15
|
+
|
|
16
|
+
export function isActorSource(source: Source) {
|
|
17
|
+
return (
|
|
18
|
+
source.type === "user" ||
|
|
19
|
+
(source.type === "agent" && source.data.actor === true)
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ActorSourceError extends SourceDiagnosticError {
|
|
24
|
+
readonly sourceId: string
|
|
25
|
+
|
|
26
|
+
constructor(source: Source, message: string) {
|
|
27
|
+
super({
|
|
28
|
+
source: source.path,
|
|
29
|
+
plugin: "actors",
|
|
30
|
+
message: `Invalid Actor Source ${source.id}: ${message}`,
|
|
31
|
+
})
|
|
32
|
+
this.name = "ActorSourceError"
|
|
33
|
+
this.sourceId = source.id
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeEmail(value: string) {
|
|
38
|
+
return value.trim().toLowerCase()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requiredString(source: Source, field: string) {
|
|
42
|
+
const value = source.data[field]
|
|
43
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
44
|
+
throw new ActorSourceError(source, `${field} is required`)
|
|
45
|
+
}
|
|
46
|
+
return value.trim()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function optionalString(source: Source, field: string) {
|
|
50
|
+
const value = source.data[field]
|
|
51
|
+
if (value == null || value === "") return
|
|
52
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
53
|
+
throw new ActorSourceError(source, `${field} must be a non-empty string`)
|
|
54
|
+
}
|
|
55
|
+
return value.trim()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stringList(source: Source, field: string) {
|
|
59
|
+
const value = source.data[field]
|
|
60
|
+
if (!Array.isArray(value) || !value.length) {
|
|
61
|
+
throw new ActorSourceError(source, `${field} must be a non-empty list`)
|
|
62
|
+
}
|
|
63
|
+
const strings = value.map((item) =>
|
|
64
|
+
typeof item === "string" ? item.trim() : "",
|
|
65
|
+
)
|
|
66
|
+
if (strings.some((item) => !item)) {
|
|
67
|
+
throw new ActorSourceError(source, `${field} must contain only strings`)
|
|
68
|
+
}
|
|
69
|
+
return strings
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function profileFor(source: Source): ActorProfile {
|
|
73
|
+
const username = optionalString(source, "username") ?? source.name
|
|
74
|
+
if (!USERNAME.test(username)) {
|
|
75
|
+
throw new ActorSourceError(source, `invalid username: ${username}`)
|
|
76
|
+
}
|
|
77
|
+
const avatar = optionalString(source, "avatar")
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
name: requiredString(source, "name"),
|
|
80
|
+
username,
|
|
81
|
+
...(avatar ? { avatar } : {}),
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function homeFor(source: Source) {
|
|
86
|
+
const suffix = `.${source.type}`
|
|
87
|
+
if (!source.id.endsWith(suffix)) {
|
|
88
|
+
throw new ActorSourceError(
|
|
89
|
+
source,
|
|
90
|
+
`id must end in ${suffix} so its Home can be derived`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
return source.id.slice(0, -suffix.length)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function actorFor(source: Source): {
|
|
97
|
+
actor: Actor
|
|
98
|
+
emails: readonly string[]
|
|
99
|
+
} {
|
|
100
|
+
const profile = profileFor(source)
|
|
101
|
+
const base = {
|
|
102
|
+
id: source.id,
|
|
103
|
+
profile,
|
|
104
|
+
home: homeFor(source),
|
|
105
|
+
}
|
|
106
|
+
if (source.type === "agent") {
|
|
107
|
+
const actor: AgentActor = Object.freeze({ ...base, type: "agent" })
|
|
108
|
+
return { actor, emails: [] }
|
|
109
|
+
}
|
|
110
|
+
if (source.type !== "user") {
|
|
111
|
+
throw new ActorSourceError(source, `unsupported Actor type: ${source.type}`)
|
|
112
|
+
}
|
|
113
|
+
const authoredRole = source.data.role
|
|
114
|
+
const role =
|
|
115
|
+
authoredRole == null || authoredRole === "" ? "member" : authoredRole
|
|
116
|
+
if (role !== "member" && role !== "staff") {
|
|
117
|
+
throw new ActorSourceError(source, `role must be member or staff`)
|
|
118
|
+
}
|
|
119
|
+
const emails = stringList(source, "emails").map(normalizeEmail)
|
|
120
|
+
if (new Set(emails).size !== emails.length) {
|
|
121
|
+
throw new ActorSourceError(source, `emails must be unique`)
|
|
122
|
+
}
|
|
123
|
+
const actor: UserActor = Object.freeze({ ...base, type: "user", role })
|
|
124
|
+
return { actor, emails }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class ActorDirectory implements KnownActorIndex {
|
|
128
|
+
private actors = new Map<string, Actor>()
|
|
129
|
+
private usernames = new Map<string, Actor>()
|
|
130
|
+
private emails = new Map<string, UserActor>()
|
|
131
|
+
private homes = new Map<string, Actor>()
|
|
132
|
+
private sourceActors = new Map<string, string>()
|
|
133
|
+
private actorEmails = new Map<string, readonly string[]>()
|
|
134
|
+
|
|
135
|
+
readonly profiles: ActorProfiles = Object.freeze({
|
|
136
|
+
findById: (id: string) => this.profileRef(this.actors.get(id)),
|
|
137
|
+
findByUsername: (username: string) =>
|
|
138
|
+
this.profileRef(this.usernames.get(username)),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
findActorById(id: string) {
|
|
142
|
+
return this.actors.get(id)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
findHomeOwner(path: string) {
|
|
146
|
+
let candidate = path.replace(/\/+$/, "") || "/"
|
|
147
|
+
while (candidate) {
|
|
148
|
+
const actor = this.homes.get(candidate)
|
|
149
|
+
if (actor) return actor
|
|
150
|
+
const slash = candidate.lastIndexOf("/")
|
|
151
|
+
if (slash <= 0) return this.homes.get("/")
|
|
152
|
+
candidate = candidate.slice(0, slash)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
findById(id: string) {
|
|
157
|
+
return this.profileRef(this.actors.get(id))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
findByUsername(username: string) {
|
|
161
|
+
return this.profileRef(this.usernames.get(username))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
findUserByEmail(email: string) {
|
|
165
|
+
return this.emails.get(normalizeEmail(email))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
credentialUsers() {
|
|
169
|
+
return this.all()
|
|
170
|
+
.filter((actor): actor is UserActor => actor.type === "user")
|
|
171
|
+
.map((actor) => ({
|
|
172
|
+
actor,
|
|
173
|
+
emails: this.actorEmails.get(actor.id) ?? [],
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
all() {
|
|
178
|
+
return [...this.actors.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
index(source: Source) {
|
|
182
|
+
if (!isActorSource(source)) return
|
|
183
|
+
const { actor, emails } = actorFor(source)
|
|
184
|
+
const idOwner = this.actors.get(actor.id)
|
|
185
|
+
if (idOwner) {
|
|
186
|
+
throw new ActorSourceError(source, `duplicate Actor id: ${actor.id}`)
|
|
187
|
+
}
|
|
188
|
+
const usernameOwner = this.usernames.get(actor.profile.username)
|
|
189
|
+
if (usernameOwner) {
|
|
190
|
+
throw new ActorSourceError(
|
|
191
|
+
source,
|
|
192
|
+
`duplicate username: ${actor.profile.username}`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
if (this.homes.has(actor.home)) {
|
|
196
|
+
throw new ActorSourceError(source, `duplicate Home: ${actor.home}`)
|
|
197
|
+
}
|
|
198
|
+
for (const email of emails) {
|
|
199
|
+
if (this.emails.has(email)) {
|
|
200
|
+
throw new ActorSourceError(
|
|
201
|
+
source,
|
|
202
|
+
`duplicate normalized email: ${email}`,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.actors.set(actor.id, actor)
|
|
208
|
+
this.usernames.set(actor.profile.username, actor)
|
|
209
|
+
this.homes.set(actor.home, actor)
|
|
210
|
+
this.sourceActors.set(source.path, actor.id)
|
|
211
|
+
this.actorEmails.set(actor.id, emails)
|
|
212
|
+
if (actor.type === "user") {
|
|
213
|
+
for (const email of emails) this.emails.set(email, actor)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
unindex(sourcePath: string) {
|
|
218
|
+
const actorId = this.sourceActors.get(sourcePath)
|
|
219
|
+
if (!actorId) return
|
|
220
|
+
const actor = this.actors.get(actorId)
|
|
221
|
+
this.sourceActors.delete(sourcePath)
|
|
222
|
+
this.actors.delete(actorId)
|
|
223
|
+
if (actor) this.usernames.delete(actor.profile.username)
|
|
224
|
+
if (actor) this.homes.delete(actor.home)
|
|
225
|
+
for (const email of this.actorEmails.get(actorId) ?? []) {
|
|
226
|
+
this.emails.delete(email)
|
|
227
|
+
}
|
|
228
|
+
this.actorEmails.delete(actorId)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private profileRef(actor?: Actor): ActorProfileRef | undefined {
|
|
232
|
+
return actor ? { id: actor.id, profile: actor.profile } : undefined
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function ActorDirectoryPlugin(directory: ActorDirectory): Plugin {
|
|
237
|
+
return {
|
|
238
|
+
id: "actors",
|
|
239
|
+
name: "ActorDirectoryPlugin",
|
|
240
|
+
schema: {
|
|
241
|
+
sources: {
|
|
242
|
+
user: {
|
|
243
|
+
label: "User",
|
|
244
|
+
icon: "circle-user",
|
|
245
|
+
color: "blue",
|
|
246
|
+
},
|
|
247
|
+
agent: {
|
|
248
|
+
label: "Agent",
|
|
249
|
+
icon: "bot",
|
|
250
|
+
color: "purple",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
index(source) {
|
|
255
|
+
directory.index(source)
|
|
256
|
+
},
|
|
257
|
+
unindex(sourcePath) {
|
|
258
|
+
directory.unindex(sourcePath)
|
|
259
|
+
},
|
|
260
|
+
invalidate(transition) {
|
|
261
|
+
const actors = new Set<string>()
|
|
262
|
+
if (transition.before && isActorSource(transition.before)) {
|
|
263
|
+
actors.add(transition.before.id)
|
|
264
|
+
}
|
|
265
|
+
if (transition.after && isActorSource(transition.after)) {
|
|
266
|
+
actors.add(transition.after.id)
|
|
267
|
+
}
|
|
268
|
+
return actors.size ? { targets: [{ type: "actor", actors }] } : undefined
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AccessDeniedError } from "./access.ts"
|
|
2
|
+
import { createTransaction } from "../actions/transaction.ts"
|
|
3
|
+
import type { BlockDraft, BlockUpdate } from "../source/index.ts"
|
|
4
|
+
import type { Engine } from "../host/engine.ts"
|
|
5
|
+
import type { AgentActor, Principal } from "./model.ts"
|
|
6
|
+
import type { KnownActorIndex } from "./model.ts"
|
|
7
|
+
|
|
8
|
+
export class AgentAdmissionError extends Error {
|
|
9
|
+
constructor(message = "Agent job admission denied.") {
|
|
10
|
+
super(message)
|
|
11
|
+
this.name = "AgentAdmissionError"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type AgentJobContext = Readonly<{
|
|
16
|
+
actor: AgentActor
|
|
17
|
+
principal: Principal
|
|
18
|
+
sourceId: string
|
|
19
|
+
appendBlock(block: BlockDraft): void
|
|
20
|
+
updateBlock(id: string, update: BlockUpdate): void
|
|
21
|
+
}>
|
|
22
|
+
|
|
23
|
+
export type AgentJob = (context: AgentJobContext) => void | Promise<void>
|
|
24
|
+
|
|
25
|
+
export type AgentAdmission = Readonly<{
|
|
26
|
+
agentId: string
|
|
27
|
+
targetId: string
|
|
28
|
+
operationId: string
|
|
29
|
+
}>
|
|
30
|
+
|
|
31
|
+
export async function runAgentJob(
|
|
32
|
+
engine: Engine,
|
|
33
|
+
admission: AgentAdmission,
|
|
34
|
+
job: AgentJob,
|
|
35
|
+
) {
|
|
36
|
+
return engine.runAgentJob(admission, job)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runAgentJobWithIndex(
|
|
40
|
+
engine: Engine,
|
|
41
|
+
actors: KnownActorIndex,
|
|
42
|
+
admission: AgentAdmission,
|
|
43
|
+
job: AgentJob,
|
|
44
|
+
) {
|
|
45
|
+
const { agentId, targetId, operationId } = admission
|
|
46
|
+
if (!operationId.trim()) throw new AgentAdmissionError()
|
|
47
|
+
const agent = actors.findActorById(agentId)
|
|
48
|
+
if (!agent || agent.type !== "agent") throw new AgentAdmissionError()
|
|
49
|
+
|
|
50
|
+
return engine.commitPrepared(async () => {
|
|
51
|
+
const target = engine.repo.get(targetId)
|
|
52
|
+
if (!target) throw new AgentAdmissionError()
|
|
53
|
+
const principal: Principal = { type: "actor", actor: agent }
|
|
54
|
+
if (engine.readAccess(principal, target) === "denied") {
|
|
55
|
+
throw new AgentAdmissionError()
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
target.blocks.some((block) => block.data.operation_id === operationId)
|
|
59
|
+
) {
|
|
60
|
+
return { changes: [] }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tx = createTransaction(engine, agent)
|
|
64
|
+
const source = tx.source(target.id)
|
|
65
|
+
await job({
|
|
66
|
+
actor: agent,
|
|
67
|
+
principal,
|
|
68
|
+
sourceId: target.id,
|
|
69
|
+
appendBlock(block) {
|
|
70
|
+
source.appendBlock({
|
|
71
|
+
...block,
|
|
72
|
+
data: { ...block.data, operation_id: operationId },
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
updateBlock(id, update) {
|
|
76
|
+
source.updateBlock(id, (current) => {
|
|
77
|
+
const patch = typeof update === "function" ? update(current) : update
|
|
78
|
+
return {
|
|
79
|
+
...patch,
|
|
80
|
+
data: { ...patch.data, operation_id: operationId },
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
const changes = await tx.changeSet()
|
|
86
|
+
if (
|
|
87
|
+
changes.changes.some(
|
|
88
|
+
(change) => engine.repo.pathToId(change.path) !== target.id,
|
|
89
|
+
)
|
|
90
|
+
) {
|
|
91
|
+
throw new AccessDeniedError("agent-target-only")
|
|
92
|
+
}
|
|
93
|
+
return changes
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { CredentialSession, KnownActorIndex, UserActor } from "./model.ts"
|
|
2
|
+
|
|
3
|
+
export type CredentialLink<Method extends "get" | "post" = "get" | "post"> =
|
|
4
|
+
Readonly<{
|
|
5
|
+
method: Method
|
|
6
|
+
href: string
|
|
7
|
+
}>
|
|
8
|
+
|
|
9
|
+
export type ExternalIdentity = Readonly<{
|
|
10
|
+
provider: string
|
|
11
|
+
subject: string
|
|
12
|
+
email?: string
|
|
13
|
+
claims?: Readonly<Record<string, unknown>>
|
|
14
|
+
}>
|
|
15
|
+
|
|
16
|
+
export type ExternalIdentityHint = Readonly<{
|
|
17
|
+
provider: string
|
|
18
|
+
email?: string
|
|
19
|
+
}>
|
|
20
|
+
|
|
21
|
+
export type CredentialCandidate = Readonly<{
|
|
22
|
+
identity: ExternalIdentityHint
|
|
23
|
+
profile: Readonly<{ name: string; avatar?: string }>
|
|
24
|
+
}>
|
|
25
|
+
|
|
26
|
+
export type CredentialDirectory = Readonly<{
|
|
27
|
+
candidates(): readonly CredentialCandidate[]
|
|
28
|
+
allows(identity: ExternalIdentityHint): Promise<boolean>
|
|
29
|
+
admit(identity: ExternalIdentity): Promise<UserActor | undefined>
|
|
30
|
+
}>
|
|
31
|
+
|
|
32
|
+
export type ActorAdmission = Readonly<{
|
|
33
|
+
bind(actors: KnownActorIndex): CredentialDirectory
|
|
34
|
+
}>
|
|
35
|
+
|
|
36
|
+
export type CredentialProvider = Readonly<{
|
|
37
|
+
signIn: CredentialLink<"get"> | null
|
|
38
|
+
ready(directory: CredentialDirectory): Promise<void>
|
|
39
|
+
resolve(request: Request): Promise<
|
|
40
|
+
| Readonly<{
|
|
41
|
+
identity: ExternalIdentity
|
|
42
|
+
signOut: CredentialLink<"post"> | null
|
|
43
|
+
}>
|
|
44
|
+
| undefined
|
|
45
|
+
>
|
|
46
|
+
fetch(request: Request, directory: CredentialDirectory): Promise<Response>
|
|
47
|
+
close(): Promise<void>
|
|
48
|
+
}>
|
|
49
|
+
|
|
50
|
+
export type CredentialHost = Readonly<{
|
|
51
|
+
ready(actors: KnownActorIndex): Promise<void>
|
|
52
|
+
resolve(request: Request, actors: KnownActorIndex): Promise<CredentialSession>
|
|
53
|
+
fetch(request: Request, actors: KnownActorIndex): Promise<Response>
|
|
54
|
+
close(): Promise<void>
|
|
55
|
+
}>
|
|
56
|
+
|
|
57
|
+
/** Map verified provider identities to pre-authored User Actors by email. */
|
|
58
|
+
export function rosteredActorAdmission(): ActorAdmission {
|
|
59
|
+
return {
|
|
60
|
+
bind(actors) {
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
candidates: () =>
|
|
63
|
+
actors.credentialUsers().flatMap(({ actor, emails }) =>
|
|
64
|
+
emails.map((email) => ({
|
|
65
|
+
identity: { provider: "email", email },
|
|
66
|
+
profile: {
|
|
67
|
+
name: actor.profile.name,
|
|
68
|
+
...(actor.profile.avatar
|
|
69
|
+
? { avatar: actor.profile.avatar }
|
|
70
|
+
: {}),
|
|
71
|
+
},
|
|
72
|
+
})),
|
|
73
|
+
),
|
|
74
|
+
async allows(identity) {
|
|
75
|
+
return Boolean(
|
|
76
|
+
identity.email && actors.findUserByEmail(identity.email),
|
|
77
|
+
)
|
|
78
|
+
},
|
|
79
|
+
async admit(identity) {
|
|
80
|
+
return identity.email
|
|
81
|
+
? actors.findUserByEmail(identity.email)
|
|
82
|
+
: undefined
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Compose authentication protocol mechanics with application Actor policy. */
|
|
90
|
+
export function createCredentialHost(
|
|
91
|
+
provider: CredentialProvider,
|
|
92
|
+
admission: ActorAdmission = rosteredActorAdmission(),
|
|
93
|
+
): CredentialHost {
|
|
94
|
+
let directory: CredentialDirectory | undefined
|
|
95
|
+
const bound = () => {
|
|
96
|
+
if (!directory) throw new Error("CredentialHost is not ready.")
|
|
97
|
+
return directory
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
async ready(actors) {
|
|
101
|
+
directory = admission.bind(actors)
|
|
102
|
+
await provider.ready(directory)
|
|
103
|
+
},
|
|
104
|
+
async resolve(request) {
|
|
105
|
+
const authenticated = await provider.resolve(request)
|
|
106
|
+
if (!authenticated) {
|
|
107
|
+
return { type: "anonymous", signIn: provider.signIn }
|
|
108
|
+
}
|
|
109
|
+
const actor = await bound().admit(authenticated.identity)
|
|
110
|
+
return actor
|
|
111
|
+
? {
|
|
112
|
+
type: "authenticated",
|
|
113
|
+
actor,
|
|
114
|
+
signOut: authenticated.signOut,
|
|
115
|
+
}
|
|
116
|
+
: { type: "anonymous", signIn: provider.signIn }
|
|
117
|
+
},
|
|
118
|
+
fetch(request) {
|
|
119
|
+
return provider.fetch(request, bound())
|
|
120
|
+
},
|
|
121
|
+
async close() {
|
|
122
|
+
await provider.close()
|
|
123
|
+
directory = undefined
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const anonymousSession: CredentialSession = Object.freeze({
|
|
129
|
+
type: "anonymous",
|
|
130
|
+
signIn: null,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
export function anonymousCredentialHost(): CredentialHost {
|
|
134
|
+
return {
|
|
135
|
+
async ready() {},
|
|
136
|
+
async resolve() {
|
|
137
|
+
return anonymousSession
|
|
138
|
+
},
|
|
139
|
+
async fetch() {
|
|
140
|
+
return new Response("Not Found", { status: 404 })
|
|
141
|
+
},
|
|
142
|
+
async close() {},
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type ActorProfile = Readonly<{
|
|
2
|
+
name: string
|
|
3
|
+
username: string
|
|
4
|
+
avatar?: string
|
|
5
|
+
}>
|
|
6
|
+
|
|
7
|
+
type ActorBase = Readonly<{
|
|
8
|
+
id: string
|
|
9
|
+
profile: ActorProfile
|
|
10
|
+
home: string
|
|
11
|
+
}>
|
|
12
|
+
|
|
13
|
+
// Actor should be a schema
|
|
14
|
+
|
|
15
|
+
export type UserActor = ActorBase &
|
|
16
|
+
Readonly<{
|
|
17
|
+
type: "user"
|
|
18
|
+
role: "member" | "staff"
|
|
19
|
+
}>
|
|
20
|
+
|
|
21
|
+
export type AgentActor = ActorBase & Readonly<{ type: "agent" }>
|
|
22
|
+
|
|
23
|
+
export type Actor = UserActor | AgentActor
|
|
24
|
+
|
|
25
|
+
export type Principal =
|
|
26
|
+
Readonly<{ type: "anonymous" }> | Readonly<{ type: "actor"; actor: Actor }>
|
|
27
|
+
|
|
28
|
+
export type CredentialSession =
|
|
29
|
+
| Readonly<{
|
|
30
|
+
type: "anonymous"
|
|
31
|
+
signIn: Readonly<{ method: "get"; href: string }> | null
|
|
32
|
+
}>
|
|
33
|
+
| Readonly<{
|
|
34
|
+
type: "authenticated"
|
|
35
|
+
actor: UserActor
|
|
36
|
+
signOut: Readonly<{ method: "post"; href: string }> | null
|
|
37
|
+
}>
|
|
38
|
+
|
|
39
|
+
export type ActorProfileRef = Readonly<{
|
|
40
|
+
id: string
|
|
41
|
+
profile: ActorProfile
|
|
42
|
+
}>
|
|
43
|
+
|
|
44
|
+
export type CredentialUser = Readonly<{
|
|
45
|
+
actor: UserActor
|
|
46
|
+
emails: readonly string[]
|
|
47
|
+
}>
|
|
48
|
+
|
|
49
|
+
export type ActorProfiles = Readonly<{
|
|
50
|
+
findById(id: string): ActorProfileRef | undefined
|
|
51
|
+
findByUsername(username: string): ActorProfileRef | undefined
|
|
52
|
+
}>
|
|
53
|
+
|
|
54
|
+
export type KnownActorIndex = ActorProfiles &
|
|
55
|
+
Readonly<{
|
|
56
|
+
findActorById(id: string): Actor | undefined
|
|
57
|
+
findHomeOwner(path: string): Actor | undefined
|
|
58
|
+
findUserByEmail(email: string): UserActor | undefined
|
|
59
|
+
credentialUsers(): readonly CredentialUser[]
|
|
60
|
+
all(): readonly Actor[]
|
|
61
|
+
}>
|
|
62
|
+
|
|
63
|
+
export const anonymousPrincipal: Principal = Object.freeze({
|
|
64
|
+
type: "anonymous",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export function principalFor(session: CredentialSession): Principal {
|
|
68
|
+
return session.type === "authenticated"
|
|
69
|
+
? { type: "actor", actor: session.actor }
|
|
70
|
+
: anonymousPrincipal
|
|
71
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AccessDeniedError, type SharedSourceGrant } from "./access.ts"
|
|
2
|
+
import { canonicalPath, canonicalSourceId } from "../canonical.ts"
|
|
3
|
+
import type { Engine } from "../host/engine.ts"
|
|
4
|
+
import { createSource, patchFrontmatter } from "../source/index.ts"
|
|
5
|
+
import type { Actor } from "./model.ts"
|
|
6
|
+
|
|
7
|
+
export function setSharedGrants(
|
|
8
|
+
engine: Engine,
|
|
9
|
+
actor: Actor,
|
|
10
|
+
sourceId: string,
|
|
11
|
+
grants: readonly SharedSourceGrant[],
|
|
12
|
+
) {
|
|
13
|
+
canonicalSourceId(sourceId)
|
|
14
|
+
return engine.commitPrepared(async () => {
|
|
15
|
+
const before = engine.repo.get(sourceId)
|
|
16
|
+
if (!before) throw new AccessDeniedError()
|
|
17
|
+
const current = before.data.access
|
|
18
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
19
|
+
throw new AccessDeniedError("shared-source-required")
|
|
20
|
+
}
|
|
21
|
+
const owner = (current as Record<string, unknown>).owner
|
|
22
|
+
if (typeof owner !== "string") {
|
|
23
|
+
throw new AccessDeniedError("shared-source-required")
|
|
24
|
+
}
|
|
25
|
+
canonicalSourceId(owner)
|
|
26
|
+
const canonicalGrants = grants.map((grant) => ({
|
|
27
|
+
actor: canonicalSourceId(grant.actor),
|
|
28
|
+
can: [...grant.can],
|
|
29
|
+
}))
|
|
30
|
+
const canonicalActors = canonicalGrants.map((grant) => grant.actor)
|
|
31
|
+
if (new Set(canonicalActors).size !== canonicalActors.length) {
|
|
32
|
+
throw new AccessDeniedError("shared-members-must-be-unique")
|
|
33
|
+
}
|
|
34
|
+
const content = patchFrontmatter(
|
|
35
|
+
before.content,
|
|
36
|
+
{ access: { type: "shared", owner, members: canonicalGrants } },
|
|
37
|
+
engine.environment.yaml,
|
|
38
|
+
)
|
|
39
|
+
const after = createSource(before.path, content, engine.environment.yaml)
|
|
40
|
+
const decision = engine.authorizeChange(actor, {
|
|
41
|
+
type: "update",
|
|
42
|
+
operation: "grants",
|
|
43
|
+
sourceId,
|
|
44
|
+
before,
|
|
45
|
+
after,
|
|
46
|
+
})
|
|
47
|
+
if (decision.type === "denied") throw new AccessDeniedError(decision.code)
|
|
48
|
+
return {
|
|
49
|
+
changes: [{ type: "replace" as const, path: before.path, content }],
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setSourceAttachments(
|
|
55
|
+
engine: Engine,
|
|
56
|
+
actor: Actor,
|
|
57
|
+
sourceId: string,
|
|
58
|
+
attachments: readonly string[],
|
|
59
|
+
) {
|
|
60
|
+
canonicalSourceId(sourceId)
|
|
61
|
+
const canonicalAttachments = attachments.map(canonicalPath)
|
|
62
|
+
if (
|
|
63
|
+
canonicalAttachments.some((attachment) => attachment.endsWith(".md")) ||
|
|
64
|
+
new Set(canonicalAttachments).size !== canonicalAttachments.length
|
|
65
|
+
) {
|
|
66
|
+
throw new AccessDeniedError("invalid-attachment-binding")
|
|
67
|
+
}
|
|
68
|
+
return engine.commitPrepared(async () => {
|
|
69
|
+
const before = engine.repo.get(sourceId)
|
|
70
|
+
if (!before) throw new AccessDeniedError()
|
|
71
|
+
const content = patchFrontmatter(
|
|
72
|
+
before.content,
|
|
73
|
+
{ attachments: canonicalAttachments },
|
|
74
|
+
engine.environment.yaml,
|
|
75
|
+
)
|
|
76
|
+
const after = createSource(before.path, content, engine.environment.yaml)
|
|
77
|
+
const decision = engine.authorizeChange(actor, {
|
|
78
|
+
type: "update",
|
|
79
|
+
operation: "attachments",
|
|
80
|
+
sourceId,
|
|
81
|
+
before,
|
|
82
|
+
after,
|
|
83
|
+
})
|
|
84
|
+
if (decision.type === "denied") throw new AccessDeniedError(decision.code)
|
|
85
|
+
return {
|
|
86
|
+
changes: [{ type: "replace" as const, path: before.path, content }],
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|