@almadar/std 16.216.0 → 16.217.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/behaviors/lolo/infra/atoms/std-data-import.lolo +272 -0
- package/behaviors/lolo/infra/atoms/std-global-search.lolo +248 -0
- package/behaviors/lolo/infra/atoms/std-notification-preferences.lolo +526 -0
- package/behaviors/lolo/infra/atoms/std-permission-matrix.lolo +522 -0
- package/behaviors/lolo/infra/atoms/std-reminder-scheduler.lolo +11 -3
- package/behaviors/registry/infra/atoms/std-data-import.orb +1164 -0
- package/behaviors/registry/infra/atoms/std-global-search.orb +1417 -0
- package/behaviors/registry/infra/atoms/std-notification-preferences.orb +2404 -0
- package/behaviors/registry/infra/atoms/std-permission-matrix.orb +2229 -0
- package/behaviors/registry/infra/atoms/std-reminder-scheduler.orb +144 -43
- package/dist/behaviors/exports-reader.js +13208 -5484
- package/dist/behaviors/functions/index.d.ts +765 -7
- package/dist/behaviors/functions/index.js +13170 -5485
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +13209 -5485
- package/dist/behaviors/query.js +13208 -5484
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +17 -1
- package/dist/behaviors-registry.json +3496 -681
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13209 -5485
- package/dist/knob-embeddings.hash.json +3 -3
- package/dist/knob-embeddings.json +1 -1
- package/dist/registry/factory-signatures.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
app std-permission-matrix "1.0.0"
|
|
2
|
+
"std-permission-matrix — role × module permission grant table / capability matrix admin surface. Use when a user voices that permissions must be managed per role and module (permission matrix, role permissions, capability matrix, permission grants, access matrix, who can see finance, hide margins, module-level permissions, admin permission screen, grant management, finance visibility by role, restrict modules by role). Persists PermissionGrant rows (workspaceId + role + module + capability flags canReadFinance / canSeeOthersHours / canSeeMargins) with full CRUD on an admin page mounted at /admin/permissions — create/edit forms are the std-modal ModalRecordModal substrate (the corpus's two-trait CRUD shape, mirroring std-list), persists live in a sibling coordinator trait, and adminRoles is ENFORCED at the persist boundary (a viewer whose role is not listed cannot fire a write, from the button or from the bus). Ships a query trait other traits consult event-wise: CHECK_GRANT { role, module } fetches the matching grant(s) and re-emits GRANT_RESOLVED { role, module, canReadFinance, canSeeOthersHours, canSeeMargins, found }. Grant SURFACE only — entity-layer @read/@update gate integration is a recorded core gap (gates are pure @entity/@user S-expressions and cannot consult another entity's rows), so enforcement stays with the consumer until that core lands."
|
|
3
|
+
orbital PermissionMatrixOrbital {
|
|
4
|
+
uses Modal from "std/behaviors/std-modal"
|
|
5
|
+
uses Spinner from "std/behaviors/ui-spinner"
|
|
6
|
+
uses Typography from "std/behaviors/ui-typography"
|
|
7
|
+
uses Icon from "std/behaviors/ui-icon"
|
|
8
|
+
uses Divider from "std/behaviors/ui-divider"
|
|
9
|
+
uses Alert from "std/behaviors/ui-alert"
|
|
10
|
+
|
|
11
|
+
type PermissionGrantLoaded = Event { data : [PermissionGrant] } "Fired when the permission-grant collection finishes loading"
|
|
12
|
+
|
|
13
|
+
type PermissionGrantLoadFailed = Event { error : string, code : string } "Fired when the permission-grant collection fails to load"
|
|
14
|
+
|
|
15
|
+
;; Narrow id-only contract (std-list's ITEM_CREATED idiom), NOT a whole-row
|
|
16
|
+
;; @entity flatten: the composed std-modal traits carry the intrinsic
|
|
17
|
+
;; `seedRow` stash on this entity, which a row-flattened event would leak
|
|
18
|
+
;; into the payload schema (always empty on real persist echoes).
|
|
19
|
+
type PermissionGrantSaved = Event { id : string! } "Fired after a grant row persists (create / update); carries the persisted row's id"
|
|
20
|
+
|
|
21
|
+
type PermissionGrantSaveFailed = Event { error : string, code : string } "Fired when a grant persist (create / update) fails"
|
|
22
|
+
|
|
23
|
+
type PermissionGrantDeleted = Event { id : string, deleted : boolean } "Fired after a grant row is deleted — persist delete emits the deleted row's id and a deleted flag, not the row"
|
|
24
|
+
|
|
25
|
+
type PermissionGrantDeleteFailed = Event { error : string, code : string } "Fired when a grant delete fails"
|
|
26
|
+
|
|
27
|
+
type GrantCheckLoaded = Event { data : [PermissionGrant] } "Fired when the grant lookup for a CHECK_GRANT finishes loading"
|
|
28
|
+
|
|
29
|
+
type GrantCheckFailed = Event { error : string, code : string } "Fired when the grant lookup for a CHECK_GRANT fails"
|
|
30
|
+
|
|
31
|
+
;; Grant SURFACE only: entity-layer @read/@update gates are pure
|
|
32
|
+
;; S-expressions over @entity (candidate row) + @user and cannot consult
|
|
33
|
+
;; this table (orbital-core entity_access); gate-side enforcement is a
|
|
34
|
+
;; recorded core gap, owner decision pending. Consumers consult event-wise
|
|
35
|
+
;; via PermissionGrantQuery's CHECK_GRANT -> GRANT_RESOLVED below.
|
|
36
|
+
entity PermissionGrant [persistent: permission_grants] {
|
|
37
|
+
id : string!
|
|
38
|
+
workspaceId : string @description "Workspace the grant applies to; blank = applies to every workspace." @synonyms "workspace, tenant, org, organization" = ""
|
|
39
|
+
role : string! @description "The role the grant applies to (e.g. admin, manager, contractor)." @synonyms "user role, group, team role"
|
|
40
|
+
module : string! @description "The module or feature area the grant governs." @synonyms "feature, area, section, screen"
|
|
41
|
+
canReadFinance : boolean @description "Whether the role may read finance data in this module." @synonyms "see finance, read financials, view finance, finance access" = false
|
|
42
|
+
canSeeOthersHours : boolean @description "Whether the role may see other people's logged hours in this module." @synonyms "see others hours, view team hours, timesheet visibility, other people's time" = false
|
|
43
|
+
canSeeMargins : boolean @description "Whether the role may see margin / profit figures in this module." @synonyms "see margins, view profit, margin visibility, profitability" = false
|
|
44
|
+
createdAt : number @description "Timestamp indicating when the grant was created." @synonyms "creation time, created, date added" = 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
trait LoadingSpinner = Spinner.traits.SpinnerRender -> PermissionGrant {
|
|
48
|
+
config {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
trait ErrorSpinner = Spinner.traits.SpinnerRender -> PermissionGrant {
|
|
52
|
+
config {
|
|
53
|
+
size: sm
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
trait LoadingText = Typography.traits.TypographyRender -> PermissionGrant {
|
|
58
|
+
config {
|
|
59
|
+
color: muted
|
|
60
|
+
content: "Loading permission grants…"
|
|
61
|
+
variant: caption
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
trait ShieldCheckIcon = Icon.traits.IconRender -> PermissionGrant {
|
|
66
|
+
config {
|
|
67
|
+
name: shield-check
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
trait PermissionMatrixTitle = Typography.traits.TypographyRender -> PermissionGrant {
|
|
72
|
+
config {
|
|
73
|
+
content: "Permission matrix"
|
|
74
|
+
variant: h3
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
trait GrantDivider = Divider.traits.DividerRender -> PermissionGrant {
|
|
79
|
+
config {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
trait ErrorAlert = Alert.traits.AlertRender -> PermissionGrant {
|
|
83
|
+
config {
|
|
84
|
+
message: ?error
|
|
85
|
+
variant: error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
;; The admin browse surface: fetch + grid + the NEW/EDIT/DELETE broadcasts
|
|
90
|
+
;; the composed modals and the persistor listen for. No form rendering here
|
|
91
|
+
;; — the std-modal substrate owns open/submit/cancel/close (verified at
|
|
92
|
+
;; source); a hand-rolled modal here put a form-submit affordance in a
|
|
93
|
+
;; state this trait owned and stranded its cancel/close arms (runtime
|
|
94
|
+
;; verify walk, 2026-09-15).
|
|
95
|
+
trait PermissionMatrixAdmin -> @rebindable PermissionGrant [interaction, collection, access] {
|
|
96
|
+
initial: loading
|
|
97
|
+
|
|
98
|
+
state loading {
|
|
99
|
+
INIT -> loading
|
|
100
|
+
(fetch PermissionGrant {
|
|
101
|
+
emit: { failure: PermissionGrantLoadFailed, success: PermissionGrantLoaded }
|
|
102
|
+
})
|
|
103
|
+
(render-ui main {
|
|
104
|
+
align: center
|
|
105
|
+
children: [@trait.LoadingSpinner, @trait.LoadingText]
|
|
106
|
+
className: py-12
|
|
107
|
+
direction: vertical
|
|
108
|
+
gap: md
|
|
109
|
+
type: stack
|
|
110
|
+
})
|
|
111
|
+
PermissionGrantLoaded -> listing
|
|
112
|
+
(render-ui main {
|
|
113
|
+
children: [{
|
|
114
|
+
align: center
|
|
115
|
+
children: [{
|
|
116
|
+
align: center
|
|
117
|
+
children: [@trait.ShieldCheckIcon, @trait.PermissionMatrixTitle]
|
|
118
|
+
direction: horizontal
|
|
119
|
+
gap: sm
|
|
120
|
+
type: stack
|
|
121
|
+
}, {
|
|
122
|
+
action: NEW
|
|
123
|
+
icon: plus
|
|
124
|
+
label: "New grant"
|
|
125
|
+
type: button
|
|
126
|
+
variant: primary
|
|
127
|
+
}]
|
|
128
|
+
direction: horizontal
|
|
129
|
+
gap: sm
|
|
130
|
+
justify: between
|
|
131
|
+
type: stack
|
|
132
|
+
}, @trait.GrantDivider, {
|
|
133
|
+
cols: 1
|
|
134
|
+
entity: ?data
|
|
135
|
+
fields: [{
|
|
136
|
+
label: Role
|
|
137
|
+
name: role
|
|
138
|
+
variant: h4
|
|
139
|
+
}, {
|
|
140
|
+
label: Module
|
|
141
|
+
name: module
|
|
142
|
+
variant: badge
|
|
143
|
+
}, {
|
|
144
|
+
label: Workspace
|
|
145
|
+
name: workspaceId
|
|
146
|
+
variant: caption
|
|
147
|
+
}, {
|
|
148
|
+
format: boolean
|
|
149
|
+
label: "Read finance"
|
|
150
|
+
name: canReadFinance
|
|
151
|
+
variant: caption
|
|
152
|
+
}, {
|
|
153
|
+
format: boolean
|
|
154
|
+
label: "Others' hours"
|
|
155
|
+
name: canSeeOthersHours
|
|
156
|
+
variant: caption
|
|
157
|
+
}, {
|
|
158
|
+
format: boolean
|
|
159
|
+
label: Margins
|
|
160
|
+
name: canSeeMargins
|
|
161
|
+
variant: caption
|
|
162
|
+
}]
|
|
163
|
+
gap: sm
|
|
164
|
+
itemActions: [{
|
|
165
|
+
event: EDIT
|
|
166
|
+
icon: pencil
|
|
167
|
+
label: Edit
|
|
168
|
+
variant: ghost
|
|
169
|
+
}, {
|
|
170
|
+
event: DELETE
|
|
171
|
+
icon: trash
|
|
172
|
+
label: Delete
|
|
173
|
+
variant: danger
|
|
174
|
+
}]
|
|
175
|
+
type: data-grid
|
|
176
|
+
}]
|
|
177
|
+
direction: vertical
|
|
178
|
+
gap: md
|
|
179
|
+
type: stack
|
|
180
|
+
})
|
|
181
|
+
PermissionGrantLoadFailed -> error
|
|
182
|
+
(render-ui main {
|
|
183
|
+
children: [@trait.ErrorAlert]
|
|
184
|
+
type: stack
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
state listing {
|
|
189
|
+
;; Refetch target for the persistor's PermissionGrantSaved /
|
|
190
|
+
;; PermissionGrantDeleted broadcasts (listens maps them onto INIT).
|
|
191
|
+
;; Persist failures notify at the persistor itself — single notifier,
|
|
192
|
+
;; no duplicate toasts here.
|
|
193
|
+
INIT -> loading
|
|
194
|
+
(fetch PermissionGrant {
|
|
195
|
+
emit: { failure: PermissionGrantLoadFailed, success: PermissionGrantLoaded }
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
state error {
|
|
200
|
+
INIT -> loading
|
|
201
|
+
(fetch PermissionGrant {
|
|
202
|
+
emit: { failure: PermissionGrantLoadFailed, success: PermissionGrantLoaded }
|
|
203
|
+
})
|
|
204
|
+
(render-ui main {
|
|
205
|
+
align: center
|
|
206
|
+
children: [@trait.ErrorSpinner]
|
|
207
|
+
type: stack
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
emits {
|
|
212
|
+
NEW
|
|
213
|
+
@description "Requests the create-grant form (the create modal listens)."
|
|
214
|
+
@synonyms "add grant, new permission, create grant"
|
|
215
|
+
@tier "domain"
|
|
216
|
+
EDIT {
|
|
217
|
+
id : string!
|
|
218
|
+
row : PermissionGrant
|
|
219
|
+
}
|
|
220
|
+
@description "Requests the edit form for a grant row (the edit modal listens)."
|
|
221
|
+
@synonyms "modify grant, change permission, update grant"
|
|
222
|
+
@tier "domain"
|
|
223
|
+
DELETE {
|
|
224
|
+
id : string!
|
|
225
|
+
row : PermissionGrant
|
|
226
|
+
}
|
|
227
|
+
@description "Requests deletion of a grant row (the persistor listens; adminRoles-guarded there)."
|
|
228
|
+
@synonyms "remove grant, revoke permission, delete grant"
|
|
229
|
+
@tier "domain"
|
|
230
|
+
PermissionGrantLoaded {
|
|
231
|
+
data : [PermissionGrant]
|
|
232
|
+
}
|
|
233
|
+
@description "Indicates the permission-grant collection has been successfully loaded."
|
|
234
|
+
@synonyms "loaded, fetched, retrieved"
|
|
235
|
+
@tier "internal"
|
|
236
|
+
PermissionGrantLoadFailed {
|
|
237
|
+
error : string
|
|
238
|
+
code : string
|
|
239
|
+
}
|
|
240
|
+
@description "Indicates failure to load the permission-grant collection."
|
|
241
|
+
@synonyms "load error, failed load, request failure"
|
|
242
|
+
@tier "internal"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
listens {
|
|
246
|
+
PermissionGrantLoadFailed {
|
|
247
|
+
error : string
|
|
248
|
+
code : string
|
|
249
|
+
}
|
|
250
|
+
@description "Indicates failure to load the permission-grant collection."
|
|
251
|
+
@synonyms "load error, failed load"
|
|
252
|
+
@tier "internal"
|
|
253
|
+
PermissionGrantPersistor.PermissionGrantSaved -> INIT
|
|
254
|
+
PermissionGrantPersistor.PermissionGrantDeleted -> INIT
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
config {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
;; Create modal: configured via `config`, no effects overrides (std-list
|
|
261
|
+
;; idiom). SAVE is renamed GRANT_CREATED so the persistor distinguishes
|
|
262
|
+
;; Create-SAVE from Edit-SAVE without source-scoped listens.
|
|
263
|
+
trait PermissionGrantCreate = Modal.traits.ModalRecordModal -> PermissionGrant {
|
|
264
|
+
events {
|
|
265
|
+
OPEN: NEW
|
|
266
|
+
SAVE: GRANT_CREATED
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
config {
|
|
270
|
+
icon: plus-circle
|
|
271
|
+
title: "Create grant"
|
|
272
|
+
fields: (workspaceId role module canReadFinance canSeeOthersHours canSeeMargins)
|
|
273
|
+
mode: create
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
listens {
|
|
277
|
+
PermissionMatrixAdmin.NEW -> NEW
|
|
278
|
+
NEW
|
|
279
|
+
@description "Opens the create-grant form."
|
|
280
|
+
@synonyms "add grant, new permission, create grant"
|
|
281
|
+
@tier "domain"
|
|
282
|
+
SAVE {
|
|
283
|
+
data : @entity!
|
|
284
|
+
}
|
|
285
|
+
@description "The create-grant form committed; data carries the new grant's fields."
|
|
286
|
+
@synonyms "submit grant, confirm create, persist"
|
|
287
|
+
@tier "domain"
|
|
288
|
+
ModalRecordLoadFailed {
|
|
289
|
+
error : string
|
|
290
|
+
code : string
|
|
291
|
+
}
|
|
292
|
+
@description "Indicates a failure to load a record for a modal."
|
|
293
|
+
@synonyms "error, failure, problem, unsuccessful"
|
|
294
|
+
@tier "internal"
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
;; Edit modal: configured via `config`, no effects overrides. SAVE is
|
|
299
|
+
;; renamed GRANT_UPDATED.
|
|
300
|
+
trait PermissionGrantEdit = Modal.traits.ModalRecordModal -> PermissionGrant {
|
|
301
|
+
events {
|
|
302
|
+
OPEN: EDIT
|
|
303
|
+
SAVE: GRANT_UPDATED
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
config {
|
|
307
|
+
icon: pencil
|
|
308
|
+
title: "Edit grant"
|
|
309
|
+
fields: (workspaceId role module canReadFinance canSeeOthersHours canSeeMargins)
|
|
310
|
+
mode: edit
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
listens {
|
|
314
|
+
PermissionMatrixAdmin.EDIT -> EDIT
|
|
315
|
+
EDIT {
|
|
316
|
+
id : string
|
|
317
|
+
row : PermissionGrant
|
|
318
|
+
}
|
|
319
|
+
@description "Opens the edit form for a grant row; the grid supplies the row."
|
|
320
|
+
@synonyms "modify grant, change permission, update grant"
|
|
321
|
+
@tier "domain"
|
|
322
|
+
SAVE {
|
|
323
|
+
data : @entity!
|
|
324
|
+
}
|
|
325
|
+
@description "The edit-grant form committed; data carries the grant id + updated fields."
|
|
326
|
+
@synonyms "submit edit, confirm update, persist"
|
|
327
|
+
@tier "domain"
|
|
328
|
+
ModalRecordLoadFailed {
|
|
329
|
+
error : string
|
|
330
|
+
code : string
|
|
331
|
+
}
|
|
332
|
+
@description "Indicates a failure to load a record into a modal window."
|
|
333
|
+
@synonyms "error, failure, problem, unsuccessful"
|
|
334
|
+
@tier "internal"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
;; Coordinator trait: owns the persist side-effects triggered by the modal
|
|
339
|
+
;; emits and the grid's DELETE. adminRoles is ENFORCED here, at the persist
|
|
340
|
+
;; boundary — both execution paths honor transition guards, for the form
|
|
341
|
+
;; submit and for a bus-delivered event alike: empty = ungated, otherwise
|
|
342
|
+
;; the viewer's role must be listed or the write does not fire. (A blocked
|
|
343
|
+
;; viewer can still OPEN a form — the substrate modal owns open/close —
|
|
344
|
+
;; but their submit is a no-op; route-level gating of /admin/permissions
|
|
345
|
+
;; stays the host's job.)
|
|
346
|
+
trait PermissionGrantPersistor -> PermissionGrant [lifecycle, instance, access] {
|
|
347
|
+
initial: idle
|
|
348
|
+
|
|
349
|
+
state idle {
|
|
350
|
+
INIT -> idle
|
|
351
|
+
DO_CREATE -> idle when (or ["=", (array/len @config.adminRoles), 0] (array/includes @config.adminRoles @user.role))
|
|
352
|
+
(persist create PermissionGrant ?data {
|
|
353
|
+
emit: { failure: PermissionGrantSaveFailed, success: PermissionGrantSaved }
|
|
354
|
+
})
|
|
355
|
+
DO_UPDATE -> idle when (or ["=", (array/len @config.adminRoles), 0] (array/includes @config.adminRoles @user.role))
|
|
356
|
+
(persist update PermissionGrant ?data {
|
|
357
|
+
emit: { failure: PermissionGrantSaveFailed, success: PermissionGrantSaved }
|
|
358
|
+
})
|
|
359
|
+
DO_DELETE -> idle when (or ["=", (array/len @config.adminRoles), 0] (array/includes @config.adminRoles @user.role))
|
|
360
|
+
(persist delete PermissionGrant ?id {
|
|
361
|
+
emit: { failure: PermissionGrantDeleteFailed, success: PermissionGrantDeleted }
|
|
362
|
+
})
|
|
363
|
+
PermissionGrantSaveFailed -> idle
|
|
364
|
+
(notify error (str/concat "Couldn't save the permission grant — " ?error))
|
|
365
|
+
PermissionGrantDeleteFailed -> idle
|
|
366
|
+
(notify error (str/concat "Couldn't delete the permission grant — " ?error))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
emits {
|
|
370
|
+
PermissionGrantSaved -> external {
|
|
371
|
+
id : string!
|
|
372
|
+
}
|
|
373
|
+
@description "Signals that a permission grant has been persisted (create / update); carries the persisted row's id."
|
|
374
|
+
@synonyms "saved, created, updated, persisted"
|
|
375
|
+
@tier "domain"
|
|
376
|
+
PermissionGrantSaveFailed -> external {
|
|
377
|
+
error : string
|
|
378
|
+
code : string
|
|
379
|
+
}
|
|
380
|
+
@description "Indicates a failure to persist a permission grant."
|
|
381
|
+
@synonyms "save error, failed save, persist failure"
|
|
382
|
+
@tier "internal"
|
|
383
|
+
PermissionGrantDeleted -> external {
|
|
384
|
+
id : string
|
|
385
|
+
deleted : boolean
|
|
386
|
+
}
|
|
387
|
+
@description "Signals that a permission grant has been deleted."
|
|
388
|
+
@synonyms "deleted, removed, revoked"
|
|
389
|
+
@tier "domain"
|
|
390
|
+
PermissionGrantDeleteFailed -> external {
|
|
391
|
+
error : string
|
|
392
|
+
code : string
|
|
393
|
+
}
|
|
394
|
+
@description "Indicates a failure to delete a permission grant."
|
|
395
|
+
@synonyms "delete error, failed delete"
|
|
396
|
+
@tier "internal"
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
listens {
|
|
400
|
+
DO_CREATE {
|
|
401
|
+
data : PermissionGrant!
|
|
402
|
+
}
|
|
403
|
+
@description "Indicates a request to create a grant row."
|
|
404
|
+
@synonyms "add, new, insert"
|
|
405
|
+
@tier "domain"
|
|
406
|
+
DO_UPDATE {
|
|
407
|
+
data : PermissionGrant!
|
|
408
|
+
}
|
|
409
|
+
@description "Indicates a grant row has been modified."
|
|
410
|
+
@synonyms "modify, update, change"
|
|
411
|
+
@tier "domain"
|
|
412
|
+
DO_DELETE {
|
|
413
|
+
id : string!
|
|
414
|
+
}
|
|
415
|
+
@description "Indicates a request to delete a grant row."
|
|
416
|
+
@synonyms "remove, discard, erase"
|
|
417
|
+
@tier "domain"
|
|
418
|
+
PermissionGrantCreate.GRANT_CREATED -> DO_CREATE
|
|
419
|
+
PermissionGrantEdit.GRANT_UPDATED -> DO_UPDATE
|
|
420
|
+
PermissionMatrixAdmin.DELETE -> DO_DELETE
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
config {
|
|
424
|
+
adminRoles : [string] = []
|
|
425
|
+
@label "Who can manage grants?"
|
|
426
|
+
@description "Roles eligible to create, edit, or delete grants. ENFORCED by the DO_CREATE/DO_UPDATE/DO_DELETE transition guards: a viewer whose @user.role is not listed cannot fire a write, from the form submit or from the bus. Empty = every signed-in viewer may manage grants (ungated). Mounting the page is the opt-in — there is deliberately no separate enable flag (a default-off flag made the whole page's affordances dead on a fresh mount)."
|
|
427
|
+
@synonyms "admin roles, permission managers, who edits permissions, grant admins, permission matrix, role permissions, capability matrix, permission grants, access matrix, manage permissions"
|
|
428
|
+
@tier "policy"
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
trait PermissionGrantQuery -> @rebindable PermissionGrant [lifecycle, instance, access] {
|
|
433
|
+
initial: idle
|
|
434
|
+
|
|
435
|
+
state idle {
|
|
436
|
+
;; The fetch filter binds @entity to the candidate ROW, so the queried
|
|
437
|
+
;; role/module travel by payload (?role/?module) in the filter and are
|
|
438
|
+
;; remembered on the trait instance for the GRANT_RESOLVED echo.
|
|
439
|
+
CHECK_GRANT -> idle when @config.enabled
|
|
440
|
+
(set @entity.role ?role)
|
|
441
|
+
(set @entity.module ?module)
|
|
442
|
+
(fetch PermissionGrant {
|
|
443
|
+
filter: (and ["=", (object/get @entity role), ?role] ["=", (object/get @entity module), ?module])
|
|
444
|
+
emit: { failure: GrantCheckFailed, success: GrantCheckLoaded }
|
|
445
|
+
})
|
|
446
|
+
;; found: false = no matching row — whether that denies or permits is
|
|
447
|
+
;; the CONSUMER's choice (default-deny is the consumer's decision); this
|
|
448
|
+
;; atom reports, it never decides.
|
|
449
|
+
GrantCheckLoaded -> idle
|
|
450
|
+
(emit GRANT_RESOLVED {
|
|
451
|
+
role: @entity.role
|
|
452
|
+
module: @entity.module
|
|
453
|
+
canReadFinance: (if [">", (array/len ?data), 0] (object/get (array/first ?data) canReadFinance false) false)
|
|
454
|
+
canSeeOthersHours: (if [">", (array/len ?data), 0] (object/get (array/first ?data) canSeeOthersHours false) false)
|
|
455
|
+
canSeeMargins: (if [">", (array/len ?data), 0] (object/get (array/first ?data) canSeeMargins false) false)
|
|
456
|
+
found: [">", (array/len ?data), 0]
|
|
457
|
+
})
|
|
458
|
+
GrantCheckFailed -> idle
|
|
459
|
+
(notify error (str/concat "Couldn't resolve the permission grant — " ?error))
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
emits {
|
|
463
|
+
GRANT_RESOLVED -> external {
|
|
464
|
+
role : string
|
|
465
|
+
module : string
|
|
466
|
+
canReadFinance : boolean
|
|
467
|
+
canSeeOthersHours : boolean
|
|
468
|
+
canSeeMargins : boolean
|
|
469
|
+
found : boolean
|
|
470
|
+
}
|
|
471
|
+
@description "Answers a CHECK_GRANT: the capability flags of the first matching grant row, with found=false (and all flags false) when no row matches. Rename to wire the downstream listener; default-deny on found=false is the consumer's choice."
|
|
472
|
+
@synonyms "grant resolved, permission answer, capabilities returned, access decision data"
|
|
473
|
+
@tier "domain"
|
|
474
|
+
GrantCheckLoaded {
|
|
475
|
+
data : [PermissionGrant]
|
|
476
|
+
}
|
|
477
|
+
@description "Indicates the grant lookup for a CHECK_GRANT has been successfully loaded."
|
|
478
|
+
@synonyms "loaded, fetched, retrieved"
|
|
479
|
+
@tier "internal"
|
|
480
|
+
GrantCheckFailed {
|
|
481
|
+
error : string
|
|
482
|
+
code : string
|
|
483
|
+
}
|
|
484
|
+
@description "Indicates the grant lookup for a CHECK_GRANT failed."
|
|
485
|
+
@synonyms "lookup error, failed lookup, request failure"
|
|
486
|
+
@tier "internal"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
listens {
|
|
490
|
+
CHECK_GRANT {
|
|
491
|
+
role : string!
|
|
492
|
+
module : string!
|
|
493
|
+
}
|
|
494
|
+
@description "Asks which capabilities a role holds in a module; the answer arrives as GRANT_RESOLVED (asynchronous — correlate by the echoed role/module)."
|
|
495
|
+
@synonyms "check grant, lookup permission, resolve capabilities, can this role see"
|
|
496
|
+
@tier "domain"
|
|
497
|
+
GrantCheckLoaded {
|
|
498
|
+
data : [PermissionGrant]
|
|
499
|
+
}
|
|
500
|
+
@description "Indicates the grant lookup has been successfully retrieved."
|
|
501
|
+
@synonyms "loaded, fetched, retrieved"
|
|
502
|
+
@tier "internal"
|
|
503
|
+
GrantCheckFailed {
|
|
504
|
+
error : string
|
|
505
|
+
code : string
|
|
506
|
+
}
|
|
507
|
+
@description "Indicates the grant lookup failed."
|
|
508
|
+
@synonyms "lookup error, failed lookup"
|
|
509
|
+
@tier "internal"
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
config {
|
|
513
|
+
enabled : boolean = false
|
|
514
|
+
@label "Answer permission lookups"
|
|
515
|
+
@description "Answer CHECK_GRANT lookups with GRANT_RESOLVED from the persisted grant table. Off by default."
|
|
516
|
+
@synonyms "enable, turn on, activate, permission lookup, grant query, resolve capabilities"
|
|
517
|
+
@tier "policy"
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
page "/admin/permissions" as PermissionMatrixPage -> PermissionMatrixAdmin, PermissionGrantCreate, PermissionGrantEdit, PermissionGrantPersistor, PermissionGrantQuery
|
|
522
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
app std-reminder-scheduler "1.1.0"
|
|
2
|
-
"std-reminder-scheduler — deadline reminders for a target entity. A periodic tick scans the target for records whose `dateField` deadline (a ms-epoch number or an ISO datetime string) is within `offsetHours` of now and that have not been reminded yet, and emits `ReminderDue` for each — every matching record, via a self-driving scan loop. Marks each record `remindedAt` so it isn't reminded twice. Compose alongside std-notify-on-event to deliver the reminder."
|
|
2
|
+
"std-reminder-scheduler — deadline reminders for a target entity. A periodic tick scans the target for records whose `dateField` deadline (a ms-epoch number or an ISO datetime string) is within `offsetHours` of now and that have not been reminded yet, and emits `ReminderDue` for each — every matching record, via a self-driving scan loop. `direction` picks which side of now the scan matches: 'future' (default) fires for deadlines up to `offsetHours` ahead; 'past' fires once a record's date is at least `offsetHours` behind now. Marks each record `remindedAt` so it isn't reminded twice — and only when the record actually matched the scan, so records outside the window stay eligible for later passes. Compose alongside std-notify-on-event to deliver the reminder."
|
|
3
3
|
orbital ReminderSchedulerOrbital {
|
|
4
4
|
type ScanRecordsLoaded = Event { data : [@entity @config.targetEntity] } "Internal: target records due for a reminder."
|
|
5
5
|
|
|
@@ -36,6 +36,9 @@ orbital ReminderSchedulerOrbital {
|
|
|
36
36
|
emit: { failure: ScanFailed, success: ReminderStepped }
|
|
37
37
|
})
|
|
38
38
|
ScanRecordsLoaded -> idle when ["=", (array/len ?data), 0]
|
|
39
|
+
;; A disabled scan acknowledges the batch without marking anything —
|
|
40
|
+
;; the dedup write fires only for a record that actually matched.
|
|
41
|
+
ScanRecordsLoaded -> idle when ["=", @config.enabled, false]
|
|
39
42
|
ScanFailed -> idle
|
|
40
43
|
(notify error (str/concat "Couldn't scan for reminders — " ?error))
|
|
41
44
|
}
|
|
@@ -44,7 +47,7 @@ orbital ReminderSchedulerOrbital {
|
|
|
44
47
|
ReminderStepped -> scanning
|
|
45
48
|
(fetch @config.targetEntity {
|
|
46
49
|
emit: { failure: ScanFailed, success: ScanRecordsLoaded }
|
|
47
|
-
filter: (and ["=", (object/get @entity remindedAt), 0] [">", (time/parse (object/get @entity @config.dateField)), @now] ["<", ["-", (time/parse (object/get @entity @config.dateField)), @now], ["*", @config.offsetHours, 3600000]])
|
|
50
|
+
filter: (and ["=", (object/get @entity remindedAt), 0] (or (and ["=", @config.direction, "future"] [">", (time/parse (object/get @entity @config.dateField)), @now] ["<", ["-", (time/parse (object/get @entity @config.dateField)), @now], ["*", @config.offsetHours, 3600000]]) (and ["=", @config.direction, "past"] [">", ["-", @now, (time/parse (object/get @entity @config.dateField))], ["*", @config.offsetHours, 3600000]])))
|
|
48
51
|
})
|
|
49
52
|
ScanRecordsLoaded -> scanning when [">", (array/len ?data), 0]
|
|
50
53
|
(set @entity.headId (object/get (array/first ?data) id))
|
|
@@ -96,7 +99,7 @@ orbital ReminderSchedulerOrbital {
|
|
|
96
99
|
scanCycle every "*/15 * * * *"
|
|
97
100
|
(fetch @config.targetEntity {
|
|
98
101
|
emit: { failure: ScanFailed, success: ScanRecordsLoaded }
|
|
99
|
-
filter: (and ["=", (object/get @entity remindedAt), 0] [">", (time/parse (object/get @entity @config.dateField)), @now] ["<", ["-", (time/parse (object/get @entity @config.dateField)), @now], ["*", @config.offsetHours, 3600000]])
|
|
102
|
+
filter: (and ["=", (object/get @entity remindedAt), 0] (or (and ["=", @config.direction, "future"] [">", (time/parse (object/get @entity @config.dateField)), @now] ["<", ["-", (time/parse (object/get @entity @config.dateField)), @now], ["*", @config.offsetHours, 3600000]]) (and ["=", @config.direction, "past"] [">", ["-", @now, (time/parse (object/get @entity @config.dateField))], ["*", @config.offsetHours, 3600000]])))
|
|
100
103
|
})
|
|
101
104
|
|
|
102
105
|
}
|
|
@@ -122,6 +125,11 @@ orbital ReminderSchedulerOrbital {
|
|
|
122
125
|
@description "A reminder fires once the deadline is within this many hours of now."
|
|
123
126
|
@synonyms "advance notice, lead time, heads up, early warning hours, how far ahead"
|
|
124
127
|
@tier "infra"
|
|
128
|
+
direction : "future" | "past" = "future"
|
|
129
|
+
@label "Scan which side of now?"
|
|
130
|
+
@description "'future' (default) matches records whose dateField is still ahead, up to offsetHours out — upcoming-deadline reminders. 'past' matches records whose dateField is at least offsetHours behind now — act-after-the-fact policies. The emitted event and the dedup marker are identical either way."
|
|
131
|
+
@synonyms "direction, past due, overdue, after the deadline, look back, backward scan, past-window"
|
|
132
|
+
@tier "infra"
|
|
125
133
|
severity : string = info
|
|
126
134
|
@label "How urgent are the reminders?"
|
|
127
135
|
@description "Severity passed to the notifier: info / warning / error."
|