Skip to content

Commit 6b1c5ee

Browse files
committed
feat(auth): add repository permission schema
- Implement RepoPermissionSchema with NSID validation - Support optional actions array - Transform to correct query string format - Add validation for collection names
1 parent 745b76f commit 6b1c5ee

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

packages/sdk-core/src/auth/permissions.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,51 @@ export const AccountPermissionSchema = z
183183
* Input type for account permission (before transform).
184184
*/
185185
export type AccountPermissionInput = z.input<typeof AccountPermissionSchema>;
186+
187+
/**
188+
* Zod schema for repository permission.
189+
*
190+
* Repository permissions control write access to records by collection type.
191+
* The collection must be a valid NSID or wildcard (*).
192+
*
193+
* @example Without actions (all actions allowed)
194+
* ```typescript
195+
* const input = { type: 'repo', collection: 'app.bsky.feed.post' };
196+
* RepoPermissionSchema.parse(input); // Returns: "repo:app.bsky.feed.post"
197+
* ```
198+
*
199+
* @example With specific actions
200+
* ```typescript
201+
* const input = {
202+
* type: 'repo',
203+
* collection: 'app.bsky.feed.post',
204+
* actions: ['create', 'update']
205+
* };
206+
* RepoPermissionSchema.parse(input); // Returns: "repo:app.bsky.feed.post?action=create&action=update"
207+
* ```
208+
*
209+
* @example With wildcard collection
210+
* ```typescript
211+
* const input = { type: 'repo', collection: '*', actions: ['delete'] };
212+
* RepoPermissionSchema.parse(input); // Returns: "repo:*?action=delete"
213+
* ```
214+
*/
215+
export const RepoPermissionSchema = z
216+
.object({
217+
type: z.literal("repo"),
218+
collection: NsidSchema.or(z.literal("*")),
219+
actions: z.array(RepoActionSchema).optional(),
220+
})
221+
.transform(({ collection, actions }) => {
222+
let perm = `repo:${collection}`;
223+
if (actions && actions.length > 0) {
224+
const params = actions.map((a) => `action=${a}`).join("&");
225+
perm += `?${params}`;
226+
}
227+
return perm;
228+
});
229+
230+
/**
231+
* Input type for repository permission (before transform).
232+
*/
233+
export type RepoPermissionInput = z.input<typeof RepoPermissionSchema>;

packages/sdk-core/tests/auth/permissions.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
MimeTypeSchema,
1111
NsidSchema,
1212
AccountPermissionSchema,
13+
RepoPermissionSchema,
1314
} from "../../src/auth/permissions.js";
1415

1516
describe("Permission Constants", () => {
@@ -228,3 +229,96 @@ describe("AccountPermissionSchema", () => {
228229
expect(() => AccountPermissionSchema.parse(input)).toThrow();
229230
});
230231
});
232+
233+
describe("RepoPermissionSchema", () => {
234+
it("should transform repo with NSID collection without actions", () => {
235+
const input = { type: "repo" as const, collection: "app.bsky.feed.post" };
236+
const result = RepoPermissionSchema.parse(input);
237+
expect(result).toBe("repo:app.bsky.feed.post");
238+
});
239+
240+
it("should transform repo with wildcard collection", () => {
241+
const input = { type: "repo" as const, collection: "*" };
242+
const result = RepoPermissionSchema.parse(input);
243+
expect(result).toBe("repo:*");
244+
});
245+
246+
it("should transform repo with single action", () => {
247+
const input = {
248+
type: "repo" as const,
249+
collection: "app.bsky.feed.post",
250+
actions: ["create" as const],
251+
};
252+
const result = RepoPermissionSchema.parse(input);
253+
expect(result).toBe("repo:app.bsky.feed.post?action=create");
254+
});
255+
256+
it("should transform repo with multiple actions", () => {
257+
const input = {
258+
type: "repo" as const,
259+
collection: "app.bsky.feed.post",
260+
actions: ["create" as const, "update" as const],
261+
};
262+
const result = RepoPermissionSchema.parse(input);
263+
expect(result).toBe("repo:app.bsky.feed.post?action=create&action=update");
264+
});
265+
266+
it("should transform repo with all three actions", () => {
267+
const input = {
268+
type: "repo" as const,
269+
collection: "com.example.record",
270+
actions: ["create" as const, "update" as const, "delete" as const],
271+
};
272+
const result = RepoPermissionSchema.parse(input);
273+
expect(result).toBe("repo:com.example.record?action=create&action=update&action=delete");
274+
});
275+
276+
it("should transform repo with wildcard and delete action", () => {
277+
const input = {
278+
type: "repo" as const,
279+
collection: "*",
280+
actions: ["delete" as const],
281+
};
282+
const result = RepoPermissionSchema.parse(input);
283+
expect(result).toBe("repo:*?action=delete");
284+
});
285+
286+
it("should handle empty actions array (no query params)", () => {
287+
const input = {
288+
type: "repo" as const,
289+
collection: "app.bsky.feed.post",
290+
actions: [],
291+
};
292+
const result = RepoPermissionSchema.parse(input);
293+
expect(result).toBe("repo:app.bsky.feed.post");
294+
});
295+
296+
it("should reject invalid NSID format", () => {
297+
const input = { type: "repo" as const, collection: "InvalidNSID" };
298+
expect(() => RepoPermissionSchema.parse(input)).toThrow();
299+
});
300+
301+
it("should reject invalid action values", () => {
302+
const input = {
303+
type: "repo" as const,
304+
collection: "app.bsky.feed.post",
305+
actions: ["invalid"],
306+
};
307+
expect(() => RepoPermissionSchema.parse(input)).toThrow();
308+
});
309+
310+
it("should reject missing type field", () => {
311+
const input = { collection: "app.bsky.feed.post" };
312+
expect(() => RepoPermissionSchema.parse(input)).toThrow();
313+
});
314+
315+
it("should reject wrong type value", () => {
316+
const input = { type: "account", collection: "app.bsky.feed.post" };
317+
expect(() => RepoPermissionSchema.parse(input)).toThrow();
318+
});
319+
320+
it("should reject missing collection field", () => {
321+
const input = { type: "repo" as const };
322+
expect(() => RepoPermissionSchema.parse(input)).toThrow();
323+
});
324+
});

0 commit comments

Comments
 (0)