···1import type { Route } from "@std/http/unstable-route";
2import { withAuth } from "../../../routes/middleware.ts";
3-import { atprotoClient } from "../../../config.ts";
4import { buildSliceUri } from "../../../utils/at-uri.ts";
5import { renderHTML } from "../../../utils/render.tsx";
6import { hxRedirect } from "../../../utils/htmx.ts";
···77 // Construct the URI for this slice
78 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId);
7980- // Get the current record first
81- const currentRecord = await atprotoClient.network.slices.slice.getRecord({
82 uri: sliceUri,
83 });
8400085 // Update the record with new name and domain
86- await atprotoClient.network.slices.slice.updateRecord(sliceId, {
87 ...currentRecord.value,
88 name: name.trim(),
89 domain: domain.trim(),
···115 }
116117 try {
000118 // Delete the slice record from AT Protocol
119- await atprotoClient.network.slices.slice.deleteRecord(sliceId);
120121 return hxRedirect(`/profile/${context.currentUser.handle}`);
122 } catch (_error) {
···1import type { Route } from "@std/http/unstable-route";
2import { withAuth } from "../../../routes/middleware.ts";
3+import { createSessionClient, publicClient } from "../../../config.ts";
4import { buildSliceUri } from "../../../utils/at-uri.ts";
5import { renderHTML } from "../../../utils/render.tsx";
6import { hxRedirect } from "../../../utils/htmx.ts";
···77 // Construct the URI for this slice
78 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId);
7980+ // Get the current record first (read operation, use public client)
81+ const currentRecord = await publicClient.network.slices.slice.getRecord({
82 uri: sliceUri,
83 });
8485+ // Create user-scoped client for write operation
86+ const sessionClient = createSessionClient(context.currentUser.sessionId!);
87+88 // Update the record with new name and domain
89+ await sessionClient.network.slices.slice.updateRecord(sliceId, {
90 ...currentRecord.value,
91 name: name.trim(),
92 domain: domain.trim(),
···118 }
119120 try {
121+ // Create user-scoped client for delete operation
122+ const sessionClient = createSessionClient(context.currentUser.sessionId!);
123+124 // Delete the slice record from AT Protocol
125+ await sessionClient.network.slices.slice.deleteRecord(sliceId);
126127 return hxRedirect(`/profile/${context.currentUser.handle}`);
128 } catch (_error) {
+2-2
frontend/src/features/slices/sync/handlers.tsx
···3import { requireAuth, withAuth } from "../../../routes/middleware.ts";
4import { getSliceClient } from "../../../utils/client.ts";
5import { buildSliceUri } from "../../../utils/at-uri.ts";
6-import { atprotoClient } from "../../../config.ts";
7import {
8 requireSliceAccess,
9 withSliceAccess,
···226227 // Get slice info for domain comparison
228 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId);
229- const sliceRecord = await atprotoClient.network.slices.slice.getRecord({
230 uri: sliceUri,
231 });
232 const sliceDomain = sliceRecord.value.domain;
···3import { requireAuth, withAuth } from "../../../routes/middleware.ts";
4import { getSliceClient } from "../../../utils/client.ts";
5import { buildSliceUri } from "../../../utils/at-uri.ts";
6+import { publicClient } from "../../../config.ts";
7import {
8 requireSliceAccess,
9 withSliceAccess,
···226227 // Get slice info for domain comparison
228 const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId);
229+ const sliceRecord = await publicClient.network.slices.slice.getRecord({
230 uri: sliceUri,
231 });
232 const sliceDomain = sliceRecord.value.domain;
+8-4
frontend/src/routes/middleware.ts
···1-import { atprotoClient, sessionStore } from "../config.ts";
2import { recordBlobToCdnUrl } from "@slices/client";
3import { getSliceActor } from "../lib/api.ts";
45export interface AuthenticatedUser {
06 handle?: string;
7 sub?: string;
8 isAuthenticated: boolean;
···19 const currentUser = await sessionStore.getCurrentUser(req);
2021 // If user is authenticated, try to fetch their profile data
22- if (currentUser.isAuthenticated && currentUser.sub) {
00023 try {
24 // Get the user's profile from network.slices.actor.profile
25- const profile = await getSliceActor(atprotoClient, currentUser.sub);
26 if (profile) {
27 currentUser.displayName = profile.displayName;
28 currentUser.avatar = profile.avatar;
···35 // Fallback to Bluesky profile for avatar if not found in slices profile
36 if (!currentUser.avatar) {
37 try {
38- const profileRecords = await atprotoClient.app.bsky.actor.profile
39 .getRecords({
40 where: {
41 did: { eq: currentUser.sub },
···1+import { sessionStore, createSessionClient } from "../config.ts";
2import { recordBlobToCdnUrl } from "@slices/client";
3import { getSliceActor } from "../lib/api.ts";
45export interface AuthenticatedUser {
6+ sessionId?: string;
7 handle?: string;
8 sub?: string;
9 isAuthenticated: boolean;
···20 const currentUser = await sessionStore.getCurrentUser(req);
2122 // If user is authenticated, try to fetch their profile data
23+ if (currentUser.isAuthenticated && currentUser.sessionId && currentUser.sub) {
24+ // Create session-scoped client
25+ const sessionClient = createSessionClient(currentUser.sessionId);
26+27 try {
28 // Get the user's profile from network.slices.actor.profile
29+ const profile = await getSliceActor(sessionClient, currentUser.sub);
30 if (profile) {
31 currentUser.displayName = profile.displayName;
32 currentUser.avatar = profile.avatar;
···39 // Fallback to Bluesky profile for avatar if not found in slices profile
40 if (!currentUser.avatar) {
41 try {
42+ const profileRecords = await sessionClient.app.bsky.actor.profile
43 .getRecords({
44 where: {
45 did: { eq: currentUser.sub },
+8-4
frontend/src/utils/client.ts
···1import { AtProtoClient } from "../client.ts";
2-import { atprotoClient } from "../config.ts";
3import { buildAtUri } from "./at-uri.ts";
45interface AuthContext {
6 currentUser: {
07 sub?: string;
8 };
9}
···30 });
3132 // Use authenticated client if user is authenticated, otherwise public client
33- return context.currentUser.sub
34- ? new AtProtoClient(API_URL, sliceUri, atprotoClient.oauth)
35- : new AtProtoClient(API_URL, sliceUri);
00036}
···1import { AtProtoClient } from "../client.ts";
2+import { createOAuthClient } from "../config.ts";
3import { buildAtUri } from "./at-uri.ts";
45interface AuthContext {
6 currentUser: {
7+ sessionId?: string;
8 sub?: string;
9 };
10}
···31 });
3233 // Use authenticated client if user is authenticated, otherwise public client
34+ if (context.currentUser.sessionId) {
35+ const sessionOAuthClient = createOAuthClient(context.currentUser.sessionId);
36+ return new AtProtoClient(API_URL, sliceUri, sessionOAuthClient);
37+ } else {
38+ return new AtProtoClient(API_URL, sliceUri);
39+ }
40}
···59 logger.error("--slice is required");
60 if (!slicesConfig.slice) {
61 logger.info(
62- "💡 Tip: Create a slices.json file with your slice URI to avoid passing --slice every time"
63 );
64 }
65 console.log("\nRun 'slices codegen --help' for usage information.");
···109 logger.success(`Generated client: ${outputPath}`);
110111 if (!excludeSlices) {
112- logger.result("Includes network.slices XRPC client methods");
113 }
114 } catch (error) {
115 const err = error as Error;
···59 logger.error("--slice is required");
60 if (!slicesConfig.slice) {
61 logger.info(
62+ "Tip: Create a slices.json file with your slice URI to avoid passing --slice every time"
63 );
64 }
65 console.log("\nRun 'slices codegen --help' for usage information.");
···109 logger.success(`Generated client: ${outputPath}`);
110111 if (!excludeSlices) {
112+ logger.info("Includes network.slices XRPC client methods");
113 }
114 } catch (error) {
115 const err = error as Error;
+13-21
packages/cli/src/commands/init.ts
···22 const projectName = parsed.name || parsed._[0] as string;
2324 if (!projectName) {
25- console.error("Error: Project name is required");
26- console.error("Usage: slices init <project-name> or slices init --name <project-name>");
27 Deno.exit(1);
28 }
2930 // Validate project name
31 if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
32- console.error("Error: Project name can only contain letters, numbers, hyphens, and underscores");
33 Deno.exit(1);
34 }
35···39 try {
40 const stat = await Deno.stat(targetDir);
41 if (stat.isDirectory) {
42- console.error(`Error: Directory '${projectName}' already exists`);
43 Deno.exit(1);
44 }
45 } catch {
···55 // Extract embedded templates
56 await extractEmbeddedTemplates(targetDir, projectName);
5758- logger.info("✅ Project created successfully!");
59 console.log(`
60-📁 Created project: ${projectName}
61-62-🚀 Get started:
63- cd ${projectName}
64- cp .env.example .env
65- # Edit .env with your OAuth configuration
66- deno task dev
6768-📖 Documentation:
69- - README.md: Setup instructions
70- - CLAUDE.md: Architecture guide for AI assistance
71- - See /auth/login for OAuth implementation
72-73-🔧 Available commands:
74- deno task dev # Start development server
75- deno task start # Start production server
76- deno fmt # Format code
77`);
78 } catch (error) {
79 const err = error as Error;
···22 const projectName = parsed.name || parsed._[0] as string;
2324 if (!projectName) {
25+ logger.error("Project name is required");
26+ console.log("Usage: slices init <project-name> or slices init --name <project-name>");
27 Deno.exit(1);
28 }
2930 // Validate project name
31 if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
32+ logger.error("Project name can only contain letters, numbers, hyphens, and underscores");
33 Deno.exit(1);
34 }
35···39 try {
40 const stat = await Deno.stat(targetDir);
41 if (stat.isDirectory) {
42+ logger.error(`Directory '${projectName}' already exists`);
43 Deno.exit(1);
44 }
45 } catch {
···55 // Extract embedded templates
56 await extractEmbeddedTemplates(targetDir, projectName);
5758+ logger.success(`Created project: ${projectName}`);
59 console.log(`
60+Get started:
61+ cd ${projectName}
62+ cp .env.example .env
63+ deno task dev
0006465+Available commands:
66+ deno task dev Start development server
67+ deno task start Start production server
68+ deno fmt Format code
0000069`);
70 } catch (error) {
71 const err = error as Error;
+11-5
packages/cli/src/commands/lexicon.ts
···1-import { importCommand } from "./lexicon/import.ts";
02import { listCommand } from "./lexicon/list.ts";
34function showLexiconHelp() {
···9 slices lexicon <SUBCOMMAND> [OPTIONS]
1011SUBCOMMANDS:
12- import Import lexicon files to your slice
013 list List lexicons in your slice
14 help Show this help message
15···17 -h, --help Show help information
1819EXAMPLES:
20- slices lexicon import --slice at://did:plc:example/slice
021 slices lexicon list --slice at://did:plc:example/slice
22 slices lexicon help
23`);
···5354 try {
55 switch (subcommand) {
56- case "import":
57- await importCommand(subcommandArgs, globalArgs);
00058 break;
59 case "list":
60 await listCommand(subcommandArgs, globalArgs);
···1+import { pushCommand } from "./lexicon/push.ts";
2+import { pullCommand } from "./lexicon/pull.ts";
3import { listCommand } from "./lexicon/list.ts";
45function showLexiconHelp() {
···10 slices lexicon <SUBCOMMAND> [OPTIONS]
1112SUBCOMMANDS:
13+ push Push lexicon files to your slice
14+ pull Pull lexicon files from your slice
15 list List lexicons in your slice
16 help Show this help message
17···19 -h, --help Show help information
2021EXAMPLES:
22+ slices lexicon push --slice at://did:plc:example/slice
23+ slices lexicon pull --slice at://did:plc:example/slice
24 slices lexicon list --slice at://did:plc:example/slice
25 slices lexicon help
26`);
···5657 try {
58 switch (subcommand) {
59+ case "push":
60+ await pushCommand(subcommandArgs, globalArgs);
61+ break;
62+ case "pull":
63+ await pullCommand(subcommandArgs, globalArgs);
64 break;
65 case "list":
66 await listCommand(subcommandArgs, globalArgs);
···33 const config = new ConfigManager();
34 await config.load();
3500036 if (config.isAuthenticated()) {
37 const authConfig = config.get().auth!;
3839+ console.log(`Authenticated as ${authConfig.did}`);
40+ console.log(`API: ${config.get().apiBaseUrl}`);
04142 if (authConfig.expiresAt) {
43 const expiresIn = Math.round((authConfig.expiresAt - Date.now()) / 1000);
44 if (expiresIn > 0) {
45 const hours = Math.floor(expiresIn / 3600);
46 const minutes = Math.floor((expiresIn % 3600) / 60);
04748 if (hours > 0) {
49+ console.log(`Token expires in ${hours}h ${minutes}m`);
50 } else if (minutes > 0) {
51+ console.log(`Token expires in ${minutes}m`);
52 } else {
53+ console.log(`Token expires in ${expiresIn}s`);
54 }
55 } else {
56+ console.log("Token has expired - run 'slices login' to re-authenticate");
057 }
0000000058 }
59 } else {
60+ console.log("Not authenticated - run 'slices login' to authenticate");
000000000061 }
00062}
+2-2
packages/cli/src/mod.ts
···20COMMANDS:
21 init Initialize a new Deno SSR project with OAuth
22 login Authenticate with Slices using device code flow
23- lexicon Manage lexicons (import, list)
24 codegen Generate TypeScript client from lexicon files
25 status Show authentication and configuration status
26 help Show this help message
···33EXAMPLES:
34 slices init my-app
35 slices login
36- slices lexicon import --path ./lexicons --slice at://did:plc:example/slice
37 slices lexicon list --slice at://did:plc:example/slice
38 slices status
39`);
···20COMMANDS:
21 init Initialize a new Deno SSR project with OAuth
22 login Authenticate with Slices using device code flow
23+ lexicon Manage lexicons (push, pull, list)
24 codegen Generate TypeScript client from lexicon files
25 status Show authentication and configuration status
26 help Show this help message
···33EXAMPLES:
34 slices init my-app
35 slices login
36+ slices lexicon push --path ./lexicons --slice at://did:plc:example/slice
37 slices lexicon list --slice at://did:plc:example/slice
38 slices status
39`);