11export * as ConfigPermission from "./permission"
2- import { Schema } from "effect"
3- import { zod , ZodPreprocess } from "@/util/effect-zod"
2+ import { Schema , SchemaGetter } from "effect"
3+ import { zod } from "@/util/effect-zod"
44import { withStatics } from "@/util/schema"
55
66export const Action = Schema . Literals ( [ "ask" , "allow" , "deny" ] )
@@ -18,21 +18,19 @@ export const Rule = Schema.Union([Action, Object])
1818 . pipe ( withStatics ( ( s ) => ( { zod : zod ( s ) } ) ) )
1919export type Rule = Schema . Schema . Type < typeof Rule >
2020
21- // Captures the user's original property insertion order before Schema.Struct
22- // canonicalises the object. See the `ZodPreprocess` comment in
23- // `util/effect-zod.ts` for the full rationale — in short: rule precedence is
24- // encoded in JSON key order (`evaluate.ts` uses `findLast`, so later keys win)
25- // and `Schema.StructWithRest` would otherwise drop that order.
26- const permissionPreprocess = ( val : unknown ) => {
27- if ( typeof val === "object" && val !== null && ! Array . isArray ( val ) ) {
28- return { __originalKeys : globalThis . Object . keys ( val ) , ...val }
29- }
30- return val
31- }
32-
33- const ObjectShape = Schema . StructWithRest (
21+ // Known permission keys get explicit types — most are full Rule (either a
22+ // single Action or a per-pattern object), but a handful of tools take no
23+ // sub-target patterns and are Action-only. Unknown keys fall through the
24+ // Record rest signature as Rule.
25+ //
26+ // StructWithRest canonicalises key order on decode (known first, then rest),
27+ // which used to require the `__originalKeys` preprocess hack because
28+ // `Permission.fromConfig` depended on the user's insertion order. That
29+ // dependency is gone — `fromConfig` now sorts top-level keys so wildcard
30+ // permissions come before specifics, making the final precedence
31+ // order-independent.
32+ const InputObject = Schema . StructWithRest (
3433 Schema . Struct ( {
35- __originalKeys : Schema . optional ( Schema . mutable ( Schema . Array ( Schema . String ) ) ) ,
3634 read : Schema . optional ( Rule ) ,
3735 edit : Schema . optional ( Rule ) ,
3836 glob : Schema . optional ( Rule ) ,
@@ -53,24 +51,29 @@ const ObjectShape = Schema.StructWithRest(
5351 [ Schema . Record ( Schema . String , Rule ) ] ,
5452)
5553
56- const InnerSchema = Schema . Union ( [ ObjectShape , Action ] ) . annotate ( {
57- [ ZodPreprocess ] : permissionPreprocess ,
58- } )
54+ // Input the user writes in config: either a single Action (shorthand for "*")
55+ // or an object of per-target rules.
56+ const InputSchema = Schema . Union ( [ Action , InputObject ] )
5957
60- // Post-parse: drop the __originalKeys metadata and rebuild the rule map in the
61- // user's original insertion order. A plain string input (the Action branch of
62- // the union) becomes `{ "*": action }`.
63- const transform = ( x : unknown ) : Record < string , Rule > => {
64- if ( typeof x === "string" ) return { "*" : x as Action }
65- const obj = x as { __originalKeys ?: string [ ] } & Record < string , unknown >
66- const { __originalKeys, ...rest } = obj
67- if ( ! __originalKeys ) return rest as Record < string , Rule >
68- const result : Record < string , Rule > = { }
69- for ( const key of __originalKeys ) {
70- if ( key in rest ) result [ key ] = rest [ key ] as Rule
71- }
72- return result
73- }
58+ // Normalise the Action shorthand into `{ "*": action }`. Object inputs pass
59+ // through untouched.
60+ const normalizeInput = ( input : Schema . Schema . Type < typeof InputSchema > ) : Schema . Schema . Type < typeof InputObject > =>
61+ typeof input === "string" ? { "*" : input } : input
7462
75- export const Info = zod ( InnerSchema ) . transform ( transform ) . meta ( { ref : "PermissionConfig" } )
76- export type Info = Record < string , Rule >
63+ export const Info = InputSchema . pipe (
64+ Schema . decodeTo ( InputObject , {
65+ decode : SchemaGetter . transform ( normalizeInput ) ,
66+ // Not perfectly invertible (we lose whether the user originally typed an
67+ // Action shorthand), but the object form is always a valid representation
68+ // of the same rules.
69+ encode : SchemaGetter . passthrough ( { strict : false } ) ,
70+ } ) ,
71+ )
72+ . annotate ( { identifier : "PermissionConfig" } )
73+ . pipe (
74+ // Walker already emits the decodeTo transform into the derived zod (see
75+ // `encoded()` in effect-zod.ts), so just expose that directly.
76+ withStatics ( ( s ) => ( { zod : zod ( s ) } ) ) ,
77+ )
78+ type _Info = Schema . Schema . Type < typeof InputObject >
79+ export type Info = { - readonly [ K in keyof _Info ] : _Info [ K ] }
0 commit comments