@nmnmcc/ability 0.1.0 → 0.3.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 (85) hide show
  1. package/README.md +353 -170
  2. package/dist/Authorizer.d.ts +216 -0
  3. package/dist/Authorizer.d.ts.map +1 -0
  4. package/dist/Authorizer.js +55 -0
  5. package/dist/Authorizer.js.map +1 -0
  6. package/dist/Condition.d.ts +536 -0
  7. package/dist/Condition.d.ts.map +1 -0
  8. package/dist/Condition.js +141 -0
  9. package/dist/Condition.js.map +1 -0
  10. package/dist/Decision.d.ts +131 -0
  11. package/dist/Decision.d.ts.map +1 -0
  12. package/dist/Decision.js +83 -0
  13. package/dist/Decision.js.map +1 -0
  14. package/dist/Domain.d.ts +148 -0
  15. package/dist/Domain.d.ts.map +1 -0
  16. package/dist/Domain.js +34 -0
  17. package/dist/Domain.js.map +1 -0
  18. package/dist/HttpApi.d.ts +129 -0
  19. package/dist/HttpApi.d.ts.map +1 -0
  20. package/dist/HttpApi.js +95 -0
  21. package/dist/HttpApi.js.map +1 -0
  22. package/dist/Policy.d.ts +429 -0
  23. package/dist/Policy.d.ts.map +1 -0
  24. package/dist/Policy.js +162 -0
  25. package/dist/Policy.js.map +1 -0
  26. package/dist/Principal.d.ts +35 -0
  27. package/dist/Principal.d.ts.map +1 -0
  28. package/dist/Principal.js +37 -0
  29. package/dist/Principal.js.map +1 -0
  30. package/dist/Resource.d.ts +213 -0
  31. package/dist/Resource.d.ts.map +1 -0
  32. package/dist/Resource.js +71 -0
  33. package/dist/Resource.js.map +1 -0
  34. package/dist/Rule.d.ts +189 -0
  35. package/dist/Rule.d.ts.map +1 -0
  36. package/dist/Rule.js +50 -0
  37. package/dist/Rule.js.map +1 -0
  38. package/dist/_virtual/_rolldown/runtime.js +1 -2
  39. package/dist/index.d.ts +9 -4
  40. package/dist/index.js +9 -4
  41. package/dist/internal/authorizer.d.ts +28 -0
  42. package/dist/internal/authorizer.d.ts.map +1 -0
  43. package/dist/internal/authorizer.js +153 -0
  44. package/dist/internal/authorizer.js.map +1 -0
  45. package/dist/internal/condition.d.ts +11 -0
  46. package/dist/internal/condition.d.ts.map +1 -0
  47. package/dist/internal/condition.js +393 -0
  48. package/dist/internal/condition.js.map +1 -0
  49. package/dist/internal/domain.js +34 -0
  50. package/dist/internal/domain.js.map +1 -0
  51. package/dist/internal/httpApi.js +41 -0
  52. package/dist/internal/httpApi.js.map +1 -0
  53. package/dist/internal/policy.js +524 -0
  54. package/dist/internal/policy.js.map +1 -0
  55. package/dist/internal/resource.js +66 -0
  56. package/dist/internal/resource.js.map +1 -0
  57. package/dist/internal/rule.js +131 -0
  58. package/dist/internal/rule.js.map +1 -0
  59. package/package.json +61 -74
  60. package/src/Authorizer.ts +330 -0
  61. package/src/Condition.ts +706 -0
  62. package/src/Decision.ts +117 -0
  63. package/src/Domain.ts +189 -0
  64. package/src/HttpApi.ts +148 -0
  65. package/src/Policy.ts +575 -0
  66. package/src/Principal.ts +41 -0
  67. package/src/Resource.ts +290 -0
  68. package/src/Rule.ts +265 -0
  69. package/src/index.ts +57 -0
  70. package/src/internal/authorizer.ts +271 -0
  71. package/src/internal/condition.ts +607 -0
  72. package/src/internal/domain.ts +56 -0
  73. package/src/internal/httpApi.ts +84 -0
  74. package/src/internal/policy.ts +815 -0
  75. package/src/internal/resource.ts +98 -0
  76. package/src/internal/rule.ts +203 -0
  77. package/dist/Ability.d.ts +0 -516
  78. package/dist/Ability.js +0 -211
  79. package/dist/AbilityExtra.d.ts +0 -79
  80. package/dist/AbilityExtra.js +0 -195
  81. package/dist/AbilityRef.d.ts +0 -110
  82. package/dist/AbilityRef.js +0 -160
  83. package/dist/internal/ability.d.ts +0 -114
  84. package/dist/internal/ability.js +0 -624
  85. package/dist/internal/serializable.js +0 -22
@@ -0,0 +1,98 @@
1
+ import * as Predicate from "effect/Predicate"
2
+ import * as Schema from "effect/Schema"
3
+ import type * as Resource from "../Resource.ts"
4
+
5
+ /** @internal */
6
+ export const TypeId = "~@nmnmcc/ability/Resource" as const
7
+
8
+ const fieldRefs = (fields: Schema.Struct.Fields) => {
9
+ const refs: Record<string, Resource.FieldRef<string>> = {}
10
+ for (const key of Object.keys(fields)) {
11
+ refs[key] = Object.freeze({ _tag: "Field", field: key })
12
+ }
13
+ return Object.freeze(refs)
14
+ }
15
+
16
+ const hasFields = (
17
+ schema: unknown,
18
+ ): schema is { readonly fields: Schema.Struct.Fields } =>
19
+ Predicate.hasProperty(schema, "fields") && Predicate.isObject(schema.fields)
20
+
21
+ /**
22
+ * Gives the paths of all data fields. A nested struct gives one path for each
23
+ * of its own fields, with a dot between the names.
24
+ */
25
+ const collectFieldPaths = (
26
+ fields: Schema.Struct.Fields,
27
+ prefix: string,
28
+ output: Array<string>,
29
+ ): void => {
30
+ for (const key of Object.keys(fields)) {
31
+ const schema = fields[key]
32
+ const path = prefix === "" ? key : `${prefix}.${key}`
33
+ if (hasFields(schema)) {
34
+ collectFieldPaths(schema.fields, path, output)
35
+ } else {
36
+ output.push(path)
37
+ }
38
+ }
39
+ }
40
+
41
+ const makeDescriptor = (
42
+ kind: string,
43
+ options: Resource.Options<ReadonlyArray<string>, Schema.Struct.Fields>,
44
+ schema: Schema.Top,
45
+ ): Resource.Descriptor => {
46
+ const fieldPaths: Array<string> = []
47
+ collectFieldPaths(options.fields, "", fieldPaths)
48
+ return Object.freeze({
49
+ kind,
50
+ actions: Object.freeze([...options.actions]),
51
+ path:
52
+ options.path === undefined
53
+ ? undefined
54
+ : Object.freeze([...options.path(fieldRefs(options.fields))]),
55
+ fieldPaths: Object.freeze(fieldPaths),
56
+ schema,
57
+ })
58
+ }
59
+
60
+ /** @internal */
61
+ export const Class =
62
+ (identifier?: string) =>
63
+ (
64
+ kind: string,
65
+ options: Resource.Options<ReadonlyArray<string>, Schema.Struct.Fields>,
66
+ ) => {
67
+ const Base = Schema.TaggedClass<any>(identifier)(kind, options.fields)
68
+ const descriptor = makeDescriptor(kind, options, Base)
69
+ return class extends Base {
70
+ static readonly [TypeId] = descriptor
71
+ static readonly kind = kind
72
+ static readonly actions = descriptor.actions
73
+ }
74
+ }
75
+
76
+ /** @internal */
77
+ export const make = (
78
+ kind: string,
79
+ options: Resource.Options<ReadonlyArray<string>, Schema.Struct.Fields>,
80
+ ) => {
81
+ const schema = Schema.TaggedStruct(kind, options.fields)
82
+ const descriptor = makeDescriptor(kind, options, schema)
83
+ return Object.freeze({
84
+ [TypeId]: descriptor,
85
+ kind,
86
+ actions: descriptor.actions,
87
+ schema,
88
+ make: (input: object) => schema.make(input as never),
89
+ })
90
+ }
91
+
92
+ /** @internal */
93
+ export const isResource = (u: unknown): u is Resource.Any =>
94
+ Predicate.hasProperty(u, TypeId)
95
+
96
+ /** @internal */
97
+ export const descriptor = (resource: Resource.Any): Resource.Descriptor =>
98
+ resource[TypeId]
@@ -0,0 +1,203 @@
1
+ import * as Equal from "effect/Equal"
2
+ import * as Formatter from "effect/Formatter"
3
+ import * as Hash from "effect/Hash"
4
+ import * as Inspectable from "effect/Inspectable"
5
+ import * as Pipeable from "effect/Pipeable"
6
+ import * as Predicate from "effect/Predicate"
7
+ import type * as Condition from "../Condition.ts"
8
+ import type * as Domain from "../Domain.ts"
9
+ import type * as Rule from "../Rule.ts"
10
+ import { builder as conditionBuilder } from "./condition.ts"
11
+ import { isResource, descriptor } from "./resource.ts"
12
+
13
+ /** @internal */
14
+ export const TypeId = "~@nmnmcc/ability/Rule" as const
15
+
16
+ /**
17
+ * The plain data of a rule.
18
+ *
19
+ * @internal
20
+ */
21
+ export interface RuleData {
22
+ readonly _tag: "Allow" | "Deny"
23
+ readonly actions: ReadonlyArray<string>
24
+ readonly resources: ReadonlyArray<string>
25
+ readonly when?: Condition.Ast
26
+ readonly fields?: ReadonlyArray<string>
27
+ readonly reason?: string
28
+ readonly id?: string
29
+ readonly meta?: unknown
30
+ }
31
+
32
+ /** @internal */
33
+ export const toData = (rule: RuleData): RuleData => {
34
+ const data: {
35
+ -readonly [K in keyof RuleData]: RuleData[K]
36
+ } = {
37
+ _tag: rule._tag,
38
+ actions: rule.actions,
39
+ resources: rule.resources,
40
+ }
41
+ if (rule.when !== undefined) data.when = rule.when
42
+ if (rule.fields !== undefined) data.fields = rule.fields
43
+ if (rule.reason !== undefined) data.reason = rule.reason
44
+ if (rule.id !== undefined) data.id = rule.id
45
+ if (rule.meta !== undefined) data.meta = rule.meta
46
+ return data
47
+ }
48
+
49
+ function* ruleIterator<A>(rule: A): Generator<A, A, unknown> {
50
+ yield rule
51
+ return rule
52
+ }
53
+
54
+ const Proto = {
55
+ ...Pipeable.Prototype,
56
+ [TypeId]: TypeId,
57
+ [Symbol.iterator](this: Rule.Any) {
58
+ return ruleIterator(this)
59
+ },
60
+ [Equal.symbol](this: RuleData, that: unknown): boolean {
61
+ return isRule(that) && Equal.equals(toData(this), toData(that))
62
+ },
63
+ [Hash.symbol](this: RuleData): number {
64
+ return Hash.structure(toData(this))
65
+ },
66
+ toJSON(this: RuleData) {
67
+ return toData(this)
68
+ },
69
+ toString(this: RuleData) {
70
+ return Formatter.format(toData(this), { space: 2 })
71
+ },
72
+ [Inspectable.NodeInspectSymbol](this: RuleData) {
73
+ return toData(this)
74
+ },
75
+ }
76
+
77
+ /**
78
+ * Makes a rule from plain data. It does not check the data.
79
+ *
80
+ * @internal
81
+ */
82
+ export const make = (data: RuleData): Rule.Any => {
83
+ const rule = Object.create(Proto) as {
84
+ -readonly [K in keyof RuleData]: RuleData[K]
85
+ }
86
+ Object.assign(rule, toData(data))
87
+ rule.actions = Object.freeze([...data.actions])
88
+ rule.resources = Object.freeze([...data.resources])
89
+ if (data.fields !== undefined) rule.fields = Object.freeze([...data.fields])
90
+ return Object.freeze(rule) as Rule.Any
91
+ }
92
+
93
+ /** @internal */
94
+ export const isRule = (u: unknown): u is Rule.Any =>
95
+ Predicate.hasProperty(u, TypeId)
96
+
97
+ const toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>
98
+ Array.isArray(value) ? value : [value as A]
99
+
100
+ const kindName = (target: unknown): string =>
101
+ isResource(target) ? descriptor(target).kind : (target as string)
102
+
103
+ const makeFromOptions =
104
+ (tag: "Allow" | "Deny") =>
105
+ (
106
+ action: string | ReadonlyArray<string>,
107
+ resource: unknown,
108
+ options?: {
109
+ readonly when?: (builder: unknown) => Condition.Ast
110
+ readonly fields?: ReadonlyArray<string>
111
+ readonly reason?: string
112
+ readonly id?: string
113
+ readonly meta?: unknown
114
+ },
115
+ ): Rule.Any => {
116
+ const data: {
117
+ -readonly [K in keyof RuleData]: RuleData[K]
118
+ } = {
119
+ _tag: tag,
120
+ actions: toArray(action),
121
+ resources: toArray(resource).map(kindName),
122
+ }
123
+ if (options?.when !== undefined)
124
+ data.when = options.when(conditionBuilder())
125
+ if (options?.fields !== undefined) data.fields = options.fields
126
+ if (options?.reason !== undefined) data.reason = options.reason
127
+ if (options?.id !== undefined) data.id = options.id
128
+ if (options?.meta !== undefined) data.meta = options.meta
129
+ return make(data)
130
+ }
131
+
132
+ const sharedBuilder = Object.freeze({
133
+ allow: makeFromOptions("Allow"),
134
+ deny: makeFromOptions("Deny"),
135
+ })
136
+
137
+ /** @internal */
138
+ export const builder = (_domain: Domain.Any): any => sharedBuilder
139
+
140
+ const patternCache = new Map<string, RegExp>()
141
+
142
+ const compilePattern = (pattern: string): RegExp => {
143
+ const cached = patternCache.get(pattern)
144
+ if (cached !== undefined) {
145
+ return cached
146
+ }
147
+ let source = ""
148
+ let index = 0
149
+ while (index < pattern.length) {
150
+ const char = pattern[index]!
151
+ if (char === "*") {
152
+ if (pattern[index + 1] === "*") {
153
+ source += ".*"
154
+ index = index + 2
155
+ continue
156
+ }
157
+ source += "[^.]*"
158
+ } else {
159
+ source += char.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
160
+ }
161
+ index = index + 1
162
+ }
163
+ const regex = new RegExp(`^${source}$`)
164
+ patternCache.set(pattern, regex)
165
+ return regex
166
+ }
167
+
168
+ /** @internal */
169
+ export const matchesFieldPattern = (pattern: string, field: string): boolean =>
170
+ pattern.includes("*")
171
+ ? compilePattern(pattern).test(field)
172
+ : pattern === field
173
+
174
+ /**
175
+ * Tests the parts of a rule that do not need the resource value: the action,
176
+ * the kind and the field.
177
+ *
178
+ * A rule with fields covers a request with no field only when it is an allow
179
+ * rule. So "may update some fields" is a yes. A deny rule for some fields does
180
+ * not stop a request for the whole value.
181
+ *
182
+ * @internal
183
+ */
184
+ export const matchesTarget = (
185
+ rule: RuleData,
186
+ action: string,
187
+ kind: string,
188
+ field: string | undefined,
189
+ ): boolean => {
190
+ if (!rule.actions.includes("*") && !rule.actions.includes(action)) {
191
+ return false
192
+ }
193
+ if (!rule.resources.includes("*") && !rule.resources.includes(kind)) {
194
+ return false
195
+ }
196
+ if (rule.fields === undefined) {
197
+ return true
198
+ }
199
+ if (field === undefined) {
200
+ return rule._tag === "Allow"
201
+ }
202
+ return rule.fields.some((pattern) => matchesFieldPattern(pattern, field))
203
+ }