@atproto/lex-schema 0.0.1 → 0.0.3

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 (289) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/core/$type.d.ts +6 -3
  3. package/dist/core/$type.d.ts.map +1 -1
  4. package/dist/core/$type.js +1 -0
  5. package/dist/core/$type.js.map +1 -1
  6. package/dist/core/record-key.d.ts +3 -3
  7. package/dist/core/record-key.d.ts.map +1 -1
  8. package/dist/core/record-key.js +12 -6
  9. package/dist/core/record-key.js.map +1 -1
  10. package/dist/core/result.d.ts.map +1 -1
  11. package/dist/core/result.js +6 -0
  12. package/dist/core/result.js.map +1 -1
  13. package/dist/core/string-format.d.ts +30 -27
  14. package/dist/core/string-format.d.ts.map +1 -1
  15. package/dist/core/string-format.js +56 -42
  16. package/dist/core/string-format.js.map +1 -1
  17. package/dist/core/types.d.ts +9 -1
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/core/types.js.map +1 -1
  20. package/dist/external.d.ts +31 -28
  21. package/dist/external.d.ts.map +1 -1
  22. package/dist/external.js +33 -17
  23. package/dist/external.js.map +1 -1
  24. package/dist/schema/_parameters.d.ts +2 -2
  25. package/dist/schema/_parameters.d.ts.map +1 -1
  26. package/dist/schema/array.d.ts +5 -6
  27. package/dist/schema/array.d.ts.map +1 -1
  28. package/dist/schema/array.js +5 -6
  29. package/dist/schema/array.js.map +1 -1
  30. package/dist/schema/blob.d.ts +2 -3
  31. package/dist/schema/blob.d.ts.map +1 -1
  32. package/dist/schema/blob.js +1 -2
  33. package/dist/schema/blob.js.map +1 -1
  34. package/dist/schema/boolean.d.ts +4 -5
  35. package/dist/schema/boolean.d.ts.map +1 -1
  36. package/dist/schema/boolean.js +2 -3
  37. package/dist/schema/boolean.js.map +1 -1
  38. package/dist/schema/bytes.d.ts +3 -4
  39. package/dist/schema/bytes.d.ts.map +1 -1
  40. package/dist/schema/bytes.js +2 -3
  41. package/dist/schema/bytes.js.map +1 -1
  42. package/dist/schema/cid.d.ts +13 -6
  43. package/dist/schema/cid.d.ts.map +1 -1
  44. package/dist/schema/cid.js +2 -4
  45. package/dist/schema/cid.js.map +1 -1
  46. package/dist/schema/custom.d.ts +3 -4
  47. package/dist/schema/custom.d.ts.map +1 -1
  48. package/dist/schema/custom.js +4 -3
  49. package/dist/schema/custom.js.map +1 -1
  50. package/dist/schema/dict.d.ts +3 -3
  51. package/dist/schema/dict.d.ts.map +1 -1
  52. package/dist/schema/dict.js +1 -1
  53. package/dist/schema/dict.js.map +1 -1
  54. package/dist/schema/discriminated-union.d.ts +15 -24
  55. package/dist/schema/discriminated-union.d.ts.map +1 -1
  56. package/dist/schema/discriminated-union.js +40 -64
  57. package/dist/schema/discriminated-union.js.map +1 -1
  58. package/dist/schema/enum.d.ts +8 -4
  59. package/dist/schema/enum.d.ts.map +1 -1
  60. package/dist/schema/enum.js +5 -3
  61. package/dist/schema/enum.js.map +1 -1
  62. package/dist/schema/integer.d.ts +3 -4
  63. package/dist/schema/integer.d.ts.map +1 -1
  64. package/dist/schema/integer.js +3 -4
  65. package/dist/schema/integer.js.map +1 -1
  66. package/dist/schema/intersection.d.ts +22 -14
  67. package/dist/schema/intersection.d.ts.map +1 -1
  68. package/dist/schema/intersection.js +12 -22
  69. package/dist/schema/intersection.js.map +1 -1
  70. package/dist/schema/literal.d.ts +8 -4
  71. package/dist/schema/literal.d.ts.map +1 -1
  72. package/dist/schema/literal.js +5 -3
  73. package/dist/schema/literal.js.map +1 -1
  74. package/dist/schema/never.d.ts +2 -2
  75. package/dist/schema/never.d.ts.map +1 -1
  76. package/dist/schema/never.js +1 -1
  77. package/dist/schema/never.js.map +1 -1
  78. package/dist/schema/null.d.ts +2 -3
  79. package/dist/schema/null.d.ts.map +1 -1
  80. package/dist/schema/null.js +1 -2
  81. package/dist/schema/null.js.map +1 -1
  82. package/dist/schema/nullable.d.ts +7 -0
  83. package/dist/schema/nullable.d.ts.map +1 -0
  84. package/dist/schema/nullable.js +19 -0
  85. package/dist/schema/nullable.js.map +1 -0
  86. package/dist/schema/object.d.ts +10 -44
  87. package/dist/schema/object.d.ts.map +1 -1
  88. package/dist/schema/object.js +13 -56
  89. package/dist/schema/object.js.map +1 -1
  90. package/dist/schema/optional.d.ts +7 -0
  91. package/dist/schema/optional.d.ts.map +1 -0
  92. package/dist/schema/optional.js +25 -0
  93. package/dist/schema/optional.js.map +1 -0
  94. package/dist/schema/params.d.ts +14 -19
  95. package/dist/schema/params.d.ts.map +1 -1
  96. package/dist/schema/params.js +10 -24
  97. package/dist/schema/params.js.map +1 -1
  98. package/dist/schema/payload.d.ts +4 -4
  99. package/dist/schema/payload.d.ts.map +1 -1
  100. package/dist/schema/payload.js.map +1 -1
  101. package/dist/schema/permission-set.d.ts +6 -6
  102. package/dist/schema/permission-set.d.ts.map +1 -1
  103. package/dist/schema/permission-set.js +1 -2
  104. package/dist/schema/permission-set.js.map +1 -1
  105. package/dist/schema/permission.d.ts +0 -1
  106. package/dist/schema/permission.d.ts.map +1 -1
  107. package/dist/schema/permission.js +0 -1
  108. package/dist/schema/permission.js.map +1 -1
  109. package/dist/schema/procedure.d.ts +8 -9
  110. package/dist/schema/procedure.d.ts.map +1 -1
  111. package/dist/schema/procedure.js +0 -1
  112. package/dist/schema/procedure.js.map +1 -1
  113. package/dist/schema/query.d.ts +7 -8
  114. package/dist/schema/query.d.ts.map +1 -1
  115. package/dist/schema/query.js +0 -1
  116. package/dist/schema/query.js.map +1 -1
  117. package/dist/schema/record.d.ts +34 -28
  118. package/dist/schema/record.d.ts.map +1 -1
  119. package/dist/schema/record.js +1 -2
  120. package/dist/schema/record.js.map +1 -1
  121. package/dist/schema/ref.d.ts +2 -3
  122. package/dist/schema/ref.d.ts.map +1 -1
  123. package/dist/schema/ref.js +1 -2
  124. package/dist/schema/ref.js.map +1 -1
  125. package/dist/schema/refine.d.ts +18 -0
  126. package/dist/schema/refine.d.ts.map +1 -0
  127. package/dist/schema/refine.js +33 -0
  128. package/dist/schema/refine.js.map +1 -0
  129. package/dist/schema/regexp.d.ts +7 -0
  130. package/dist/schema/regexp.d.ts.map +1 -0
  131. package/dist/schema/regexp.js +22 -0
  132. package/dist/schema/regexp.js.map +1 -0
  133. package/dist/schema/string.d.ts +4 -8
  134. package/dist/schema/string.d.ts.map +1 -1
  135. package/dist/schema/string.js +6 -3
  136. package/dist/schema/string.js.map +1 -1
  137. package/dist/schema/subscription.d.ts +7 -6
  138. package/dist/schema/subscription.d.ts.map +1 -1
  139. package/dist/schema/subscription.js.map +1 -1
  140. package/dist/schema/token.d.ts +2 -3
  141. package/dist/schema/token.d.ts.map +1 -1
  142. package/dist/schema/token.js +1 -2
  143. package/dist/schema/token.js.map +1 -1
  144. package/dist/schema/typed-object.d.ts +29 -27
  145. package/dist/schema/typed-object.d.ts.map +1 -1
  146. package/dist/schema/typed-object.js +1 -2
  147. package/dist/schema/typed-object.js.map +1 -1
  148. package/dist/schema/typed-ref.d.ts +2 -2
  149. package/dist/schema/typed-ref.d.ts.map +1 -1
  150. package/dist/schema/typed-ref.js +1 -1
  151. package/dist/schema/typed-ref.js.map +1 -1
  152. package/dist/schema/typed-union.d.ts +3 -4
  153. package/dist/schema/typed-union.d.ts.map +1 -1
  154. package/dist/schema/typed-union.js +3 -10
  155. package/dist/schema/typed-union.js.map +1 -1
  156. package/dist/schema/union.d.ts +2 -2
  157. package/dist/schema/union.d.ts.map +1 -1
  158. package/dist/schema/union.js +1 -1
  159. package/dist/schema/union.js.map +1 -1
  160. package/dist/schema/unknown-object.d.ts +2 -3
  161. package/dist/schema/unknown-object.d.ts.map +1 -1
  162. package/dist/schema/unknown-object.js +1 -2
  163. package/dist/schema/unknown-object.js.map +1 -1
  164. package/dist/schema/unknown.d.ts +2 -2
  165. package/dist/schema/unknown.d.ts.map +1 -1
  166. package/dist/schema/unknown.js +1 -1
  167. package/dist/schema/unknown.js.map +1 -1
  168. package/dist/schema.d.ts +4 -0
  169. package/dist/schema.d.ts.map +1 -1
  170. package/dist/schema.js +6 -1
  171. package/dist/schema.js.map +1 -1
  172. package/dist/util/array-agg.d.ts.map +1 -1
  173. package/dist/util/array-agg.js +1 -0
  174. package/dist/util/array-agg.js.map +1 -1
  175. package/dist/util/lazy-property.d.ts +2 -0
  176. package/dist/util/lazy-property.d.ts.map +1 -0
  177. package/dist/util/lazy-property.js +14 -0
  178. package/dist/util/lazy-property.js.map +1 -0
  179. package/dist/validation/schema.d.ts +24 -0
  180. package/dist/validation/schema.d.ts.map +1 -0
  181. package/dist/validation/schema.js +57 -0
  182. package/dist/validation/schema.js.map +1 -0
  183. package/dist/validation/validation-error.d.ts +3 -3
  184. package/dist/validation/validation-error.d.ts.map +1 -1
  185. package/dist/validation/validation-error.js +32 -4
  186. package/dist/validation/validation-error.js.map +1 -1
  187. package/dist/validation/validation-issue.d.ts +32 -24
  188. package/dist/validation/validation-issue.d.ts.map +1 -1
  189. package/dist/validation/validation-issue.js +136 -92
  190. package/dist/validation/validation-issue.js.map +1 -1
  191. package/dist/validation/validator.d.ts +20 -50
  192. package/dist/validation/validator.d.ts.map +1 -1
  193. package/dist/validation/validator.js +40 -134
  194. package/dist/validation/validator.js.map +1 -1
  195. package/dist/validation.d.ts +1 -0
  196. package/dist/validation.d.ts.map +1 -1
  197. package/dist/validation.js +1 -0
  198. package/dist/validation.js.map +1 -1
  199. package/package.json +8 -4
  200. package/src/core/$type.ts +7 -4
  201. package/src/core/record-key.ts +12 -5
  202. package/src/core/result.ts +6 -0
  203. package/src/core/string-format.ts +97 -61
  204. package/src/core/types.ts +12 -6
  205. package/src/external.ts +92 -70
  206. package/src/schema/_parameters.test.ts +416 -0
  207. package/src/schema/array.test.ts +237 -0
  208. package/src/schema/array.ts +17 -11
  209. package/src/schema/blob.test.ts +506 -0
  210. package/src/schema/blob.ts +3 -5
  211. package/src/schema/boolean.test.ts +116 -0
  212. package/src/schema/boolean.ts +5 -7
  213. package/src/schema/bytes.test.ts +226 -0
  214. package/src/schema/bytes.ts +4 -6
  215. package/src/schema/cid.test.ts +155 -0
  216. package/src/schema/cid.ts +14 -8
  217. package/src/schema/custom.test.ts +413 -0
  218. package/src/schema/custom.ts +10 -8
  219. package/src/schema/dict.test.ts +198 -0
  220. package/src/schema/dict.ts +6 -8
  221. package/src/schema/discriminated-union.test.ts +675 -0
  222. package/src/schema/discriminated-union.ts +68 -95
  223. package/src/schema/enum.test.ts +396 -0
  224. package/src/schema/enum.ts +12 -5
  225. package/src/schema/integer.test.ts +312 -0
  226. package/src/schema/integer.ts +5 -7
  227. package/src/schema/intersection.test.ts +32 -0
  228. package/src/schema/intersection.ts +37 -40
  229. package/src/schema/literal.test.ts +531 -0
  230. package/src/schema/literal.ts +12 -5
  231. package/src/schema/never.test.ts +174 -0
  232. package/src/schema/never.ts +3 -10
  233. package/src/schema/null.test.ts +79 -0
  234. package/src/schema/null.ts +3 -5
  235. package/src/schema/nullable.test.ts +480 -0
  236. package/src/schema/nullable.ts +23 -0
  237. package/src/schema/object.test.ts +47 -115
  238. package/src/schema/object.ts +23 -134
  239. package/src/schema/optional.test.ts +485 -0
  240. package/src/schema/optional.ts +31 -0
  241. package/src/schema/params.test.ts +582 -0
  242. package/src/schema/params.ts +37 -55
  243. package/src/schema/payload.test.ts +345 -0
  244. package/src/schema/payload.ts +5 -5
  245. package/src/schema/permission-set.test.ts +679 -0
  246. package/src/schema/permission-set.ts +6 -8
  247. package/src/schema/permission.test.ts +536 -0
  248. package/src/schema/permission.ts +0 -2
  249. package/src/schema/procedure.test.ts +443 -0
  250. package/src/schema/procedure.ts +11 -13
  251. package/src/schema/query.test.ts +408 -0
  252. package/src/schema/query.ts +9 -11
  253. package/src/schema/record.test.ts +694 -0
  254. package/src/schema/record.ts +38 -36
  255. package/src/schema/ref.test.ts +365 -0
  256. package/src/schema/ref.ts +8 -5
  257. package/src/schema/refine.test.ts +578 -0
  258. package/src/schema/refine.ts +85 -0
  259. package/src/schema/regexp.test.ts +580 -0
  260. package/src/schema/regexp.ts +22 -0
  261. package/src/schema/string.test.ts +612 -0
  262. package/src/schema/string.ts +11 -17
  263. package/src/schema/subscription.test.ts +689 -0
  264. package/src/schema/subscription.ts +13 -8
  265. package/src/schema/token.test.ts +428 -0
  266. package/src/schema/token.ts +3 -5
  267. package/src/schema/typed-object.test.ts +612 -0
  268. package/src/schema/typed-object.ts +23 -20
  269. package/src/schema/typed-ref.test.ts +823 -0
  270. package/src/schema/typed-ref.ts +10 -5
  271. package/src/schema/typed-union.test.ts +378 -0
  272. package/src/schema/typed-union.ts +6 -15
  273. package/src/schema/union.test.ts +200 -0
  274. package/src/schema/union.ts +5 -4
  275. package/src/schema/unknown-object.test.ts +592 -0
  276. package/src/schema/unknown-object.ts +3 -5
  277. package/src/schema/unknown.test.ts +312 -0
  278. package/src/schema/unknown.ts +3 -3
  279. package/src/schema.ts +7 -1
  280. package/src/util/array-agg.ts +1 -0
  281. package/src/util/lazy-property.ts +14 -0
  282. package/src/validation/schema.ts +92 -0
  283. package/src/validation/validation-error.ts +60 -9
  284. package/src/validation/validation-issue.ts +141 -144
  285. package/src/validation/validator.ts +67 -206
  286. package/src/validation.ts +1 -0
  287. package/tsconfig.build.json +12 -0
  288. package/tsconfig.json +7 -0
  289. package/tsconfig.tests.json +9 -0
@@ -0,0 +1,679 @@
1
+ import { asNsid } from '../core.js'
2
+ import { PermissionSet } from './permission-set.js'
3
+ import { Permission } from './permission.js'
4
+
5
+ describe('PermissionSet', () => {
6
+ describe('constructor', () => {
7
+ it('creates a PermissionSet instance with all parameters', () => {
8
+ const nsid = asNsid('app.bsky.oauth.permissions')
9
+ const permissions = [
10
+ new Permission('app.bsky.feed.post:read', {}),
11
+ new Permission('app.bsky.feed.post:write', {}),
12
+ ] as const
13
+ const options = {
14
+ title: 'Post Management',
15
+ detail: 'Allows reading and writing posts',
16
+ }
17
+
18
+ const permissionSet = new PermissionSet(nsid, permissions, options)
19
+
20
+ expect(permissionSet).toBeInstanceOf(PermissionSet)
21
+ expect(permissionSet.nsid).toBe(nsid)
22
+ expect(permissionSet.permissions).toBe(permissions)
23
+ expect(permissionSet.options).toBe(options)
24
+ })
25
+
26
+ it('creates a PermissionSet instance with minimal options', () => {
27
+ const nsid = asNsid('app.bsky.oauth.permissions')
28
+ const permissions = [
29
+ new Permission('app.bsky.feed.post:read', {}),
30
+ ] as const
31
+ const options = {}
32
+
33
+ const permissionSet = new PermissionSet(nsid, permissions, options)
34
+
35
+ expect(permissionSet).toBeInstanceOf(PermissionSet)
36
+ expect(permissionSet.nsid).toBe(nsid)
37
+ expect(permissionSet.permissions).toBe(permissions)
38
+ expect(permissionSet.options).toEqual({})
39
+ })
40
+
41
+ it('creates a PermissionSet instance with empty permissions array', () => {
42
+ const nsid = asNsid('app.bsky.oauth.permissions')
43
+ const permissions = [] as const
44
+ const options = {}
45
+
46
+ const permissionSet = new PermissionSet(nsid, permissions, options)
47
+
48
+ expect(permissionSet).toBeInstanceOf(PermissionSet)
49
+ expect(permissionSet.permissions).toEqual([])
50
+ })
51
+
52
+ it('creates a PermissionSet instance with title only', () => {
53
+ const nsid = asNsid('app.bsky.oauth.permissions')
54
+ const permissions = [
55
+ new Permission('app.bsky.feed.like:read', {}),
56
+ ] as const
57
+ const options = {
58
+ title: 'Like Management',
59
+ }
60
+
61
+ const permissionSet = new PermissionSet(nsid, permissions, options)
62
+
63
+ expect(permissionSet.options.title).toBe('Like Management')
64
+ expect(permissionSet.options.detail).toBeUndefined()
65
+ })
66
+
67
+ it('creates a PermissionSet instance with detail only', () => {
68
+ const nsid = asNsid('app.bsky.oauth.permissions')
69
+ const permissions = [
70
+ new Permission('app.bsky.feed.like:read', {}),
71
+ ] as const
72
+ const options = {
73
+ detail: 'Allows reading likes on posts',
74
+ }
75
+
76
+ const permissionSet = new PermissionSet(nsid, permissions, options)
77
+
78
+ expect(permissionSet.options.title).toBeUndefined()
79
+ expect(permissionSet.options.detail).toBe('Allows reading likes on posts')
80
+ })
81
+
82
+ it('creates a PermissionSet instance with localized titles', () => {
83
+ const nsid = asNsid('app.bsky.oauth.permissions')
84
+ const permissions = [
85
+ new Permission('app.bsky.feed.post:read', {}),
86
+ ] as const
87
+ const options = {
88
+ title: 'Post Management',
89
+ 'title:lang': {
90
+ es: 'Gestión de Publicaciones',
91
+ fr: 'Gestion des Publications',
92
+ },
93
+ }
94
+
95
+ const permissionSet = new PermissionSet(nsid, permissions, options)
96
+
97
+ expect(permissionSet.options.title).toBe('Post Management')
98
+ expect(permissionSet.options['title:lang']).toEqual({
99
+ es: 'Gestión de Publicaciones',
100
+ fr: 'Gestion des Publications',
101
+ })
102
+ })
103
+
104
+ it('creates a PermissionSet instance with localized details', () => {
105
+ const nsid = asNsid('app.bsky.oauth.permissions')
106
+ const permissions = [
107
+ new Permission('app.bsky.feed.post:read', {}),
108
+ ] as const
109
+ const options = {
110
+ detail: 'Allows reading posts',
111
+ 'detail:lang': {
112
+ es: 'Permite leer publicaciones',
113
+ fr: 'Permet de lire les publications',
114
+ },
115
+ }
116
+
117
+ const permissionSet = new PermissionSet(nsid, permissions, options)
118
+
119
+ expect(permissionSet.options.detail).toBe('Allows reading posts')
120
+ expect(permissionSet.options['detail:lang']).toEqual({
121
+ es: 'Permite leer publicaciones',
122
+ fr: 'Permet de lire les publications',
123
+ })
124
+ })
125
+
126
+ it('creates a PermissionSet instance with all options including localization', () => {
127
+ const nsid = asNsid('app.bsky.oauth.permissions')
128
+ const permissions = [
129
+ new Permission('app.bsky.feed.post:read', {}),
130
+ new Permission('app.bsky.feed.post:write', {}),
131
+ ] as const
132
+ const options = {
133
+ title: 'Post Management',
134
+ 'title:lang': {
135
+ es: 'Gestión de Publicaciones',
136
+ fr: 'Gestion des Publications',
137
+ de: 'Beitragsverwaltung',
138
+ },
139
+ detail: 'Allows reading and writing posts',
140
+ 'detail:lang': {
141
+ es: 'Permite leer y escribir publicaciones',
142
+ fr: 'Permet de lire et écrire les publications',
143
+ de: 'Ermöglicht das Lesen und Schreiben von Beiträgen',
144
+ },
145
+ }
146
+
147
+ const permissionSet = new PermissionSet(nsid, permissions, options)
148
+
149
+ expect(permissionSet.options.title).toBe('Post Management')
150
+ expect(permissionSet.options['title:lang']).toEqual({
151
+ es: 'Gestión de Publicaciones',
152
+ fr: 'Gestion des Publications',
153
+ de: 'Beitragsverwaltung',
154
+ })
155
+ expect(permissionSet.options.detail).toBe(
156
+ 'Allows reading and writing posts',
157
+ )
158
+ expect(permissionSet.options['detail:lang']).toEqual({
159
+ es: 'Permite leer y escribir publicaciones',
160
+ fr: 'Permet de lire et écrire les publications',
161
+ de: 'Ermöglicht das Lesen und Schreiben von Beiträgen',
162
+ })
163
+ })
164
+ })
165
+
166
+ describe('property immutability', () => {
167
+ it('options object itself is mutable', () => {
168
+ const nsid = asNsid('app.bsky.oauth.permissions')
169
+ const permissions = [
170
+ new Permission('app.bsky.feed.post:read', {}),
171
+ ] as const
172
+ const options = { title: 'Test' }
173
+
174
+ const permissionSet = new PermissionSet(nsid, permissions, options)
175
+
176
+ // The reference is readonly, but the object itself can be mutated
177
+ options.title = 'Updated Title'
178
+ expect(permissionSet.options.title).toBe('Updated Title')
179
+ })
180
+ })
181
+
182
+ describe('with multiple permissions', () => {
183
+ it('creates a PermissionSet with multiple read permissions', () => {
184
+ const nsid = asNsid('app.bsky.oauth.read')
185
+ const permissions = [
186
+ new Permission('app.bsky.feed.post:read', {}),
187
+ new Permission('app.bsky.feed.like:read', {}),
188
+ new Permission('app.bsky.feed.repost:read', {}),
189
+ new Permission('app.bsky.graph.follow:read', {}),
190
+ ] as const
191
+ const options = {
192
+ title: 'Read Access',
193
+ detail: 'Allows reading various resources',
194
+ }
195
+
196
+ const permissionSet = new PermissionSet(nsid, permissions, options)
197
+
198
+ expect(permissionSet.permissions).toHaveLength(4)
199
+ expect(permissionSet.permissions[0].resource).toBe(
200
+ 'app.bsky.feed.post:read',
201
+ )
202
+ expect(permissionSet.permissions[1].resource).toBe(
203
+ 'app.bsky.feed.like:read',
204
+ )
205
+ expect(permissionSet.permissions[2].resource).toBe(
206
+ 'app.bsky.feed.repost:read',
207
+ )
208
+ expect(permissionSet.permissions[3].resource).toBe(
209
+ 'app.bsky.graph.follow:read',
210
+ )
211
+ })
212
+
213
+ it('creates a PermissionSet with mixed read/write permissions', () => {
214
+ const nsid = asNsid('app.bsky.oauth.full')
215
+ const permissions = [
216
+ new Permission('app.bsky.feed.post:read', {}),
217
+ new Permission('app.bsky.feed.post:write', {}),
218
+ new Permission('app.bsky.feed.like:read', {}),
219
+ new Permission('app.bsky.feed.like:write', {}),
220
+ ] as const
221
+ const options = {
222
+ title: 'Full Access',
223
+ detail: 'Allows reading and writing posts and likes',
224
+ }
225
+
226
+ const permissionSet = new PermissionSet(nsid, permissions, options)
227
+
228
+ expect(permissionSet.permissions).toHaveLength(4)
229
+ })
230
+
231
+ it('creates a PermissionSet with a single permission', () => {
232
+ const nsid = asNsid('app.bsky.oauth.limited')
233
+ const permissions = [
234
+ new Permission('app.bsky.actor.profile:read', {}),
235
+ ] as const
236
+ const options = {
237
+ title: 'Profile Read',
238
+ detail: 'Allows reading user profiles only',
239
+ }
240
+
241
+ const permissionSet = new PermissionSet(nsid, permissions, options)
242
+
243
+ expect(permissionSet.permissions).toHaveLength(1)
244
+ expect(permissionSet.permissions[0].resource).toBe(
245
+ 'app.bsky.actor.profile:read',
246
+ )
247
+ })
248
+ })
249
+
250
+ describe('edge cases', () => {
251
+ it('handles very long NSID', () => {
252
+ const nsid = asNsid(
253
+ 'com.example.very.long.namespace.identifier.oauth.permissions',
254
+ )
255
+ const permissions = [new Permission('resource:action', {})] as const
256
+ const options = {}
257
+
258
+ const permissionSet = new PermissionSet(nsid, permissions, options)
259
+
260
+ expect(permissionSet.nsid).toBe(nsid)
261
+ })
262
+
263
+ it('handles long title strings', () => {
264
+ const nsid = asNsid('app.bsky.oauth.permissions')
265
+ const permissions = [
266
+ new Permission('app.bsky.feed.post:read', {}),
267
+ ] as const
268
+ const longTitle = 'A'.repeat(500)
269
+ const options = {
270
+ title: longTitle,
271
+ }
272
+
273
+ const permissionSet = new PermissionSet(nsid, permissions, options)
274
+
275
+ expect(permissionSet.options.title).toBe(longTitle)
276
+ expect(permissionSet.options.title?.length).toBe(500)
277
+ })
278
+
279
+ it('handles long detail strings', () => {
280
+ const nsid = asNsid('app.bsky.oauth.permissions')
281
+ const permissions = [
282
+ new Permission('app.bsky.feed.post:read', {}),
283
+ ] as const
284
+ const longDetail = 'B'.repeat(1000)
285
+ const options = {
286
+ detail: longDetail,
287
+ }
288
+
289
+ const permissionSet = new PermissionSet(nsid, permissions, options)
290
+
291
+ expect(permissionSet.options.detail).toBe(longDetail)
292
+ expect(permissionSet.options.detail?.length).toBe(1000)
293
+ })
294
+
295
+ it('handles multiple language codes in title:lang', () => {
296
+ const nsid = asNsid('app.bsky.oauth.permissions')
297
+ const permissions = [
298
+ new Permission('app.bsky.feed.post:read', {}),
299
+ ] as const
300
+ const options = {
301
+ title: 'Post Management',
302
+ 'title:lang': {
303
+ es: 'Gestión de Publicaciones',
304
+ fr: 'Gestion des Publications',
305
+ de: 'Beitragsverwaltung',
306
+ it: 'Gestione dei Post',
307
+ pt: 'Gerenciamento de Postagens',
308
+ ja: '投稿管理',
309
+ ko: '게시물 관리',
310
+ 'zh-CN': '帖子管理',
311
+ },
312
+ }
313
+
314
+ const permissionSet = new PermissionSet(nsid, permissions, options)
315
+
316
+ expect(
317
+ Object.keys(permissionSet.options['title:lang'] || {}),
318
+ ).toHaveLength(8)
319
+ })
320
+
321
+ it('handles undefined values in title:lang', () => {
322
+ const nsid = asNsid('app.bsky.oauth.permissions')
323
+ const permissions = [
324
+ new Permission('app.bsky.feed.post:read', {}),
325
+ ] as const
326
+ const options = {
327
+ title: 'Post Management',
328
+ 'title:lang': {
329
+ es: 'Gestión de Publicaciones',
330
+ fr: undefined,
331
+ de: 'Beitragsverwaltung',
332
+ },
333
+ }
334
+
335
+ const permissionSet = new PermissionSet(nsid, permissions, options)
336
+
337
+ expect(permissionSet.options['title:lang']?.es).toBe(
338
+ 'Gestión de Publicaciones',
339
+ )
340
+ expect(permissionSet.options['title:lang']?.fr).toBeUndefined()
341
+ expect(permissionSet.options['title:lang']?.de).toBe('Beitragsverwaltung')
342
+ })
343
+
344
+ it('handles undefined values in detail:lang', () => {
345
+ const nsid = asNsid('app.bsky.oauth.permissions')
346
+ const permissions = [
347
+ new Permission('app.bsky.feed.post:read', {}),
348
+ ] as const
349
+ const options = {
350
+ detail: 'Allows reading posts',
351
+ 'detail:lang': {
352
+ es: undefined,
353
+ fr: 'Permet de lire les publications',
354
+ },
355
+ }
356
+
357
+ const permissionSet = new PermissionSet(nsid, permissions, options)
358
+
359
+ expect(permissionSet.options['detail:lang']?.es).toBeUndefined()
360
+ expect(permissionSet.options['detail:lang']?.fr).toBe(
361
+ 'Permet de lire les publications',
362
+ )
363
+ })
364
+
365
+ it('handles special characters in title', () => {
366
+ const nsid = asNsid('app.bsky.oauth.permissions')
367
+ const permissions = [
368
+ new Permission('app.bsky.feed.post:read', {}),
369
+ ] as const
370
+ const options = {
371
+ title: 'Post Management: Read & Write (Full Access)',
372
+ }
373
+
374
+ const permissionSet = new PermissionSet(nsid, permissions, options)
375
+
376
+ expect(permissionSet.options.title).toBe(
377
+ 'Post Management: Read & Write (Full Access)',
378
+ )
379
+ })
380
+
381
+ it('handles special characters in detail', () => {
382
+ const nsid = asNsid('app.bsky.oauth.permissions')
383
+ const permissions = [
384
+ new Permission('app.bsky.feed.post:read', {}),
385
+ ] as const
386
+ const options = {
387
+ detail:
388
+ 'Allows reading posts, likes & reposts (includes all sub-resources)',
389
+ }
390
+
391
+ const permissionSet = new PermissionSet(nsid, permissions, options)
392
+
393
+ expect(permissionSet.options.detail).toBe(
394
+ 'Allows reading posts, likes & reposts (includes all sub-resources)',
395
+ )
396
+ })
397
+
398
+ it('handles unicode characters in title', () => {
399
+ const nsid = asNsid('app.bsky.oauth.permissions')
400
+ const permissions = [
401
+ new Permission('app.bsky.feed.post:read', {}),
402
+ ] as const
403
+ const options = {
404
+ title: '投稿管理 📝',
405
+ }
406
+
407
+ const permissionSet = new PermissionSet(nsid, permissions, options)
408
+
409
+ expect(permissionSet.options.title).toBe('投稿管理 📝')
410
+ })
411
+
412
+ it('handles empty strings in title', () => {
413
+ const nsid = asNsid('app.bsky.oauth.permissions')
414
+ const permissions = [
415
+ new Permission('app.bsky.feed.post:read', {}),
416
+ ] as const
417
+ const options = {
418
+ title: '',
419
+ }
420
+
421
+ const permissionSet = new PermissionSet(nsid, permissions, options)
422
+
423
+ expect(permissionSet.options.title).toBe('')
424
+ })
425
+
426
+ it('handles empty strings in detail', () => {
427
+ const nsid = asNsid('app.bsky.oauth.permissions')
428
+ const permissions = [
429
+ new Permission('app.bsky.feed.post:read', {}),
430
+ ] as const
431
+ const options = {
432
+ detail: '',
433
+ }
434
+
435
+ const permissionSet = new PermissionSet(nsid, permissions, options)
436
+
437
+ expect(permissionSet.options.detail).toBe('')
438
+ })
439
+
440
+ it('handles large number of permissions', () => {
441
+ const nsid = asNsid('app.bsky.oauth.permissions')
442
+ const permissions = Array.from(
443
+ { length: 100 },
444
+ (_, i) => new Permission(`resource${i}:action`, {}),
445
+ ) as any
446
+ const options = {}
447
+
448
+ const permissionSet = new PermissionSet(nsid, permissions, options)
449
+
450
+ expect(permissionSet.permissions).toHaveLength(100)
451
+ })
452
+ })
453
+
454
+ describe('real-world permission set examples', () => {
455
+ it('creates a feed management permission set', () => {
456
+ const nsid = asNsid('app.bsky.oauth.feed')
457
+ const permissions = [
458
+ new Permission('app.bsky.feed.post:read', {}),
459
+ new Permission('app.bsky.feed.post:write', {}),
460
+ new Permission('app.bsky.feed.like:read', {}),
461
+ new Permission('app.bsky.feed.like:write', {}),
462
+ new Permission('app.bsky.feed.repost:read', {}),
463
+ new Permission('app.bsky.feed.repost:write', {}),
464
+ ] as const
465
+ const options = {
466
+ title: 'Feed Management',
467
+ 'title:lang': {
468
+ es: 'Gestión de Feed',
469
+ fr: 'Gestion du Feed',
470
+ },
471
+ detail: 'Full access to manage posts, likes, and reposts in your feed',
472
+ 'detail:lang': {
473
+ es: 'Acceso completo para gestionar publicaciones, me gusta y reposts en tu feed',
474
+ fr: 'Accès complet pour gérer les publications, les likes et les reposts dans votre fil',
475
+ },
476
+ }
477
+
478
+ const permissionSet = new PermissionSet(nsid, permissions, options)
479
+
480
+ expect(permissionSet.nsid).toBe('app.bsky.oauth.feed')
481
+ expect(permissionSet.permissions).toHaveLength(6)
482
+ expect(permissionSet.options.title).toBe('Feed Management')
483
+ })
484
+
485
+ it('creates a read-only permission set', () => {
486
+ const nsid = asNsid('app.bsky.oauth.readonly')
487
+ const permissions = [
488
+ new Permission('app.bsky.feed.post:read', {}),
489
+ new Permission('app.bsky.feed.like:read', {}),
490
+ new Permission('app.bsky.actor.profile:read', {}),
491
+ new Permission('app.bsky.graph.follow:read', {}),
492
+ ] as const
493
+ const options = {
494
+ title: 'Read-Only Access',
495
+ detail: 'View posts, likes, profiles, and follows without modification',
496
+ }
497
+
498
+ const permissionSet = new PermissionSet(nsid, permissions, options)
499
+
500
+ expect(permissionSet.nsid).toBe('app.bsky.oauth.readonly')
501
+ expect(
502
+ permissionSet.permissions.every((p) => p.resource.endsWith(':read')),
503
+ ).toBe(true)
504
+ })
505
+
506
+ it('creates a profile management permission set', () => {
507
+ const nsid = asNsid('app.bsky.oauth.profile')
508
+ const permissions = [
509
+ new Permission('app.bsky.actor.profile:read', {}),
510
+ new Permission('app.bsky.actor.profile:write', {}),
511
+ ] as const
512
+ const options = {
513
+ title: 'Profile Management',
514
+ 'title:lang': {
515
+ es: 'Gestión de Perfil',
516
+ fr: 'Gestion du Profil',
517
+ de: 'Profilverwaltung',
518
+ },
519
+ detail: 'Read and update your profile information',
520
+ 'detail:lang': {
521
+ es: 'Leer y actualizar la información de tu perfil',
522
+ fr: 'Lire et mettre à jour les informations de votre profil',
523
+ de: 'Profilinformationen lesen und aktualisieren',
524
+ },
525
+ }
526
+
527
+ const permissionSet = new PermissionSet(nsid, permissions, options)
528
+
529
+ expect(permissionSet.nsid).toBe('app.bsky.oauth.profile')
530
+ expect(permissionSet.permissions).toHaveLength(2)
531
+ })
532
+
533
+ it('creates a minimal permission set', () => {
534
+ const nsid = asNsid('app.bsky.oauth.minimal')
535
+ const permissions = [
536
+ new Permission('app.bsky.actor.profile:read', {}),
537
+ ] as const
538
+ const options = {
539
+ title: 'Basic Access',
540
+ }
541
+
542
+ const permissionSet = new PermissionSet(nsid, permissions, options)
543
+
544
+ expect(permissionSet.nsid).toBe('app.bsky.oauth.minimal')
545
+ expect(permissionSet.permissions).toHaveLength(1)
546
+ expect(permissionSet.options.detail).toBeUndefined()
547
+ })
548
+ })
549
+
550
+ describe('permission validation', () => {
551
+ it('validates that permissions are Permission instances', () => {
552
+ const nsid = asNsid('app.bsky.oauth.permissions')
553
+ const permission1 = new Permission('app.bsky.feed.post:read', {})
554
+ const permission2 = new Permission('app.bsky.feed.post:write', {})
555
+ const permissions = [permission1, permission2] as const
556
+ const options = {}
557
+
558
+ const permissionSet = new PermissionSet(nsid, permissions, options)
559
+
560
+ expect(permissionSet.permissions[0]).toBeInstanceOf(Permission)
561
+ expect(permissionSet.permissions[1]).toBeInstanceOf(Permission)
562
+ })
563
+
564
+ it('preserves permission resource strings', () => {
565
+ const nsid = asNsid('app.bsky.oauth.permissions')
566
+ const permissions = [
567
+ new Permission('app.bsky.feed.post:read', {}),
568
+ new Permission('app.bsky.feed.like:write', {}),
569
+ new Permission('app.bsky.graph.follow:read', {}),
570
+ ] as const
571
+ const options = {}
572
+
573
+ const permissionSet = new PermissionSet(nsid, permissions, options)
574
+
575
+ expect(permissionSet.permissions[0].resource).toBe(
576
+ 'app.bsky.feed.post:read',
577
+ )
578
+ expect(permissionSet.permissions[1].resource).toBe(
579
+ 'app.bsky.feed.like:write',
580
+ )
581
+ expect(permissionSet.permissions[2].resource).toBe(
582
+ 'app.bsky.graph.follow:read',
583
+ )
584
+ })
585
+
586
+ it('preserves permission options', () => {
587
+ const nsid = asNsid('app.bsky.oauth.permissions')
588
+ const permissionOptions = { custom: 'value' }
589
+ const permissions = [
590
+ new Permission('app.bsky.feed.post:read', permissionOptions),
591
+ ] as const
592
+ const options = {}
593
+
594
+ const permissionSet = new PermissionSet(nsid, permissions, options)
595
+
596
+ expect(permissionSet.permissions[0].options).toBe(permissionOptions)
597
+ })
598
+ })
599
+
600
+ describe('option variations', () => {
601
+ it('accepts title without detail', () => {
602
+ const nsid = asNsid('app.bsky.oauth.permissions')
603
+ const permissions = [
604
+ new Permission('app.bsky.feed.post:read', {}),
605
+ ] as const
606
+ const options = {
607
+ title: 'Post Reading',
608
+ }
609
+
610
+ const permissionSet = new PermissionSet(nsid, permissions, options)
611
+
612
+ expect(permissionSet.options.title).toBe('Post Reading')
613
+ expect(permissionSet.options.detail).toBeUndefined()
614
+ expect(permissionSet.options['title:lang']).toBeUndefined()
615
+ expect(permissionSet.options['detail:lang']).toBeUndefined()
616
+ })
617
+
618
+ it('accepts detail without title', () => {
619
+ const nsid = asNsid('app.bsky.oauth.permissions')
620
+ const permissions = [
621
+ new Permission('app.bsky.feed.post:read', {}),
622
+ ] as const
623
+ const options = {
624
+ detail: 'Allows reading posts from the feed',
625
+ }
626
+
627
+ const permissionSet = new PermissionSet(nsid, permissions, options)
628
+
629
+ expect(permissionSet.options.title).toBeUndefined()
630
+ expect(permissionSet.options.detail).toBe(
631
+ 'Allows reading posts from the feed',
632
+ )
633
+ expect(permissionSet.options['title:lang']).toBeUndefined()
634
+ expect(permissionSet.options['detail:lang']).toBeUndefined()
635
+ })
636
+
637
+ it('accepts title:lang without title', () => {
638
+ const nsid = asNsid('app.bsky.oauth.permissions')
639
+ const permissions = [
640
+ new Permission('app.bsky.feed.post:read', {}),
641
+ ] as const
642
+ const options = {
643
+ 'title:lang': {
644
+ es: 'Gestión de Publicaciones',
645
+ fr: 'Gestion des Publications',
646
+ },
647
+ }
648
+
649
+ const permissionSet = new PermissionSet(nsid, permissions, options)
650
+
651
+ expect(permissionSet.options.title).toBeUndefined()
652
+ expect(permissionSet.options['title:lang']).toEqual({
653
+ es: 'Gestión de Publicaciones',
654
+ fr: 'Gestion des Publications',
655
+ })
656
+ })
657
+
658
+ it('accepts detail:lang without detail', () => {
659
+ const nsid = asNsid('app.bsky.oauth.permissions')
660
+ const permissions = [
661
+ new Permission('app.bsky.feed.post:read', {}),
662
+ ] as const
663
+ const options = {
664
+ 'detail:lang': {
665
+ es: 'Permite leer publicaciones',
666
+ fr: 'Permet de lire les publications',
667
+ },
668
+ }
669
+
670
+ const permissionSet = new PermissionSet(nsid, permissions, options)
671
+
672
+ expect(permissionSet.options.detail).toBeUndefined()
673
+ expect(permissionSet.options['detail:lang']).toEqual({
674
+ es: 'Permite leer publicaciones',
675
+ fr: 'Permet de lire les publications',
676
+ })
677
+ })
678
+ })
679
+ })
@@ -1,3 +1,4 @@
1
+ import { NsidString } from '../core.js'
1
2
  import { Permission } from './permission.js'
2
3
 
3
4
  export type PermissionSetOptions = {
@@ -8,15 +9,12 @@ export type PermissionSetOptions = {
8
9
  }
9
10
 
10
11
  export class PermissionSet<
11
- const Nsid extends string = any,
12
- const Permissions extends readonly Permission[] = any,
13
- const Options extends PermissionSetOptions = any,
12
+ const TNsid extends NsidString = any,
13
+ const TPermissions extends readonly Permission[] = any,
14
14
  > {
15
- readonly lexiconType = 'permission-set' as const
16
-
17
15
  constructor(
18
- readonly nsid: Nsid,
19
- readonly permissions: Permissions,
20
- readonly options: Options,
16
+ readonly nsid: TNsid,
17
+ readonly permissions: TPermissions,
18
+ readonly options: PermissionSetOptions = {},
21
19
  ) {}
22
20
  }