@oneteam-app/apps-cli 0.1.0-alpha.0 → 0.1.0-alpha.1
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.
- package/CHANGELOG.md +9 -0
- package/dist/index.js +115 -34
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
package/dist/index.js
CHANGED
|
@@ -9,13 +9,13 @@ import { basename, dirname, join, relative, sep } from "path";
|
|
|
9
9
|
import { validateManifest } from "@oneteam-app/apps/contracts";
|
|
10
10
|
|
|
11
11
|
// src/generated/instructions.ts
|
|
12
|
-
var AGENT_INSTRUCTIONS = "# Oneteam login and authorization\n\nApps built this way are opened from a Oneteam shortcut. Oneteam signs the\nemployee in (no extra password)
|
|
13
|
-
var CURSOR_RULE = "---\ndescription: Oneteam login and authorization rules for this app\nalwaysApply: true\n---\n\n## Never hand-roll this\n\nWriting the OAuth flow by hand is the main way apps get this wrong: PKCE with\n`plain`, no `state` check, an ID token read with `atob` instead of verified,\ntokens in `localStorage`. Use the package, which does the whole flow in one\ncall, or stop and say it isn't supported on this stack.\n\n## Rules\n\nThese are not style preferences. Breaking one creates a security hole.\n\n- **Deny by default.** `proxy.ts`
|
|
12
|
+
var AGENT_INSTRUCTIONS = "# Oneteam login and authorization\n\nApps built this way are opened from a Oneteam shortcut. Oneteam signs the\nemployee in (no extra password) and tells the app who they are and which\nOneteam permissions they hold. Authorization is RBAC on those permissions:\nadmins grant them through Oneteam roles, and the app checks them by name\n(`network::users.update`). Apps have no permissions or roles of their own.\n\n**Supported today**, with `@oneteam-app/apps`:\n\n- **TanStack Start** (Lovable's default, also on Cloudflare Workers): `@oneteam-app/apps/tanstack-start`\n- **Next.js App Router** (Next 15.2+, 16 recommended): `@oneteam-app/apps/next`\n\nOn any other stack, stop and tell the user it isn't supported yet.\n\n## Never hand-roll this\n\nWriting the OAuth flow by hand is the main way apps get this wrong: PKCE with\n`plain`, no `state` check, an ID token read with `atob` instead of verified,\ntokens in `localStorage`. Use the package, which does the whole flow in one\ncall, or stop and say it isn't supported on this stack.\n\n## Rules\n\nThese are not style preferences. Breaking one creates a security hole.\n\n- **Deny by default.** Next.js: `proxy.ts` runs `oneteam.proxy()` for every route.\n TanStack Start: `src/start.ts` has a request middleware that calls\n `oneteam.guard()`. Add to `publicPaths` only what anyone on the internet may\n open (health checks, a public landing page), and ask the user first.\n- **Never exclude routes from the gate by file extension.** `/api/receipts/1.jpg`\n is data. Keep the scaffolded matcher; put public files in `public/assets/`\n and add `'/assets/*'` to `publicPaths`.\n- **The gate is a safety net, not the check.** Server actions, server functions\n and route handlers still call `requirePermission` / `protectRoute` themselves.\n- **Check permissions on the server, every time.** Everything that reads or\n changes protected data runs on the server (Next.js server action or route\n handler, TanStack Start server function or server route) and calls\n `oneteam.requirePermission(...)` (or uses `oneteam.protectRoute`). `<Protected>`\n and `useOneteam().can` only hide UI.\n- **Never trust ids from the browser for authorization.** Load the record\n first, then check against *its* community/owner:\n `requirePermission('network::users.update', { communityId: expense.communityId })`.\n Scope list queries with `oneteam.communitiesFor(session, permission)`.\n- **Identify users by `session.user.id`** (Oneteam user id) or\n `session.user.externalId` (employee number). Not by email, not by `sub`,\n never by a user id sent in a request body.\n- **Secrets stay on the server.** Never prefix `ONETEAM_CLIENT_SECRET` or\n `ONETEAM_SESSION_SECRET` with `NEXT_PUBLIC_` or `VITE_`, never put them in code,\n never log tokens or sessions, never pass `getAccessToken()` results to the browser.\n TanStack Start: import `oneteam.server.ts` only from server functions, server\n routes and `src/start.ts`.\n- **Don't build your own login.** No custom JWT parsing, no reading tokens from\n query strings, no localStorage tokens, no second login screen.\n- **Authorize on Oneteam permissions only.** Not on a role, an admin flag, a\n function name, an email domain or a hard-coded list of user ids. Pick each\n permission from `node_modules/@oneteam-app/apps/docs/oneteam-permissions.md`\n (typed as `OneteamPermission`, so a wrong name doesn't compile) and name the\n ones the app uses once, e.g. in `lib/permissions.ts`. Never invent a name or\n match on a prefix.\n- **Community permissions are not organisation-wide.** A `network:` permission\n without `communityId` is only true for users who hold the same action\n organisation-wide. That's intended: don't \"fix\" it by dropping the community check.\n\n## The shape of a protected app\n\nTanStack Start (Lovable): full files in [references/tanstack-start.md](references/tanstack-start.md).\n\n```ts\n// src/lib/oneteam.server.ts\nimport { createOneteam } from '@oneteam-app/apps/tanstack-start';\nimport manifest from '../../oneteam.app.json';\nexport const oneteam = createOneteam({ scopes: manifest.scopes });\n\n// src/routes/api/oneteam/$.ts\nexport const Route = createFileRoute('/api/oneteam/$')({ server: { handlers: oneteam.handlers } });\n\n// src/start.ts \u2014 deny by default\nconst oneteamMiddleware = createMiddleware().server((ctx) => oneteam.guard(ctx, { publicPaths: ['/health'] }));\nexport const startInstance = createStart(() => ({ requestMiddleware: [oneteamMiddleware] }));\n\n// any server function that touches protected data\nexport const approveExpense = createServerFn({ method: 'POST' })\n .validator((id: string) => id)\n .handler(async ({ data: id }) => {\n const expense = await db.expense.findUniqueOrThrow({ where: { id } });\n await oneteam.requirePermission(APPROVE_EXPENSES, { communityId: expense.communityId });\n // ...\n });\n```\n\nNext.js:\n\n```ts\n// lib/oneteam.ts\nimport { createOneteam } from '@oneteam-app/apps/next';\nimport manifest from '../oneteam.app.json';\nexport const oneteam = createOneteam({ scopes: manifest.scopes });\n\n// proxy.ts \u2014 deny by default\nexport const proxy = oneteam.proxy({ publicPaths: ['/health'] });\n\n// app/api/oneteam/[...oneteam]/route.ts\nexport const { GET, POST } = oneteam.handlers;\n\n// lib/permissions.ts \u2014 the Oneteam permissions this app checks\nexport const APPROVE_EXPENSES = 'network::users.update' satisfies OneteamPermission;\n\n// any server action or route handler that touches protected data\nconst expense = await db.expense.findUniqueOrThrow({ where: { id } });\nawait oneteam.requirePermission(APPROVE_EXPENSES, { communityId: expense.communityId });\n```\n\n## Workflow\n\nDo these in order. Don't skip the checks at the end.\n\n1. **Confirm the stack.** TanStack Start: `package.json` has `@tanstack/react-start`\n and there is a `src/routes/` directory. Next.js: `package.json` has `next` and\n there is an `app/` or `src/app/` directory.\n2. **Scaffold.**\n ```bash\n npm install @oneteam-app/apps\n npx @oneteam-app/apps-cli init --url https://<production-domain>\n ```\n Ask the user for the production domain if you don't know it. Never guess one,\n and never use a preview URL (`*.vercel.app` previews, Lovable's editor preview)\n as the production domain; a published `https://<name>.lovable.app` is fine.\n `init` creates the setup files of [references/tanstack-start.md](references/tanstack-start.md)\n or [references/nextjs.md](references/nextjs.md), `oneteam.app.json` and\n `.env.example` entries. It won't overwrite an existing `src/start.ts`,\n `middleware.ts` or `proxy.ts`: add the gate to it yourself.\n **Without a terminal (Lovable):** add `@oneteam-app/apps` as a dependency and\n create those files yourself, exactly as in the reference.\n3. **Declare what the app needs** in `oneteam.app.json` `scopes`:\n `oneteam:permissions` (scaffolded) for permission checks, `oneteam:communities`\n for memberships and for permissions per community, `oneteam:functions` for\n functions, `oneteam:organisation` for the employee number. Request only what\n the app uses; admins see and approve each one. Then run\n `npx @oneteam-app/apps-cli validate`.\n4. **Choose the permissions.** For each action not every employee may do, pick\n the Oneteam permission whose holders should be allowed, from\n `node_modules/@oneteam-app/apps/docs/oneteam-permissions.md`: its \"Granted by\"\n column is the role editor toggle admins use. Use a `network:` permission when\n the action belongs to a store/location/team, an `organisation:` one when it is\n organisation-wide. Tell the user which permissions you chose and why.\n5. **Protect the app** following [references/tanstack-start.md](references/tanstack-start.md)\n or [references/nextjs.md](references/nextjs.md) and the rules above.\n6. **Register the app.** With the Oneteam MCP connector, an organisation admin\n can do it from this chat: call `register_app` with the app's production URL\n (and its localhost URL). Otherwise an admin registers it in the Oneteam\n dashboard. Either way the app starts in development status (only the person\n who registered it can sign in) and comes with a client id and secret. The user\n stores them as server-side secrets themselves (Lovable: project secrets;\n otherwise `.env.local` and the hosting provider's secret settings); never ask them to paste the secret into the chat, and never\n repeat a secret you were shown.\n7. **Verify:** `npx @oneteam-app/apps-cli doctor`, `npm run build`, then walk\n through [references/checklist.md](references/checklist.md) and report the\n result to the user.\n\n## What you cannot do for the user\n\nSay these plainly instead of reporting success:\n\n- Registering the app in Oneteam and getting it approved by an admin.\n- Putting the client id and secret into `.env.local` and the hosting provider.\n- Making sure the right people hold the chosen permissions through their Oneteam roles.\n\nUntil those happen, login will fail with `access_denied` or\n`not_a_oneteam_app_client`, however correct the code is.\n\n## Reference\n\n- [references/tanstack-start.md](references/tanstack-start.md): API and patterns for the TanStack Start adapter (Lovable)\n- [references/nextjs.md](references/nextjs.md): API and patterns for the Next.js adapter\n- [references/checklist.md](references/checklist.md): final security review\n- [references/oneteam-permissions.md](references/oneteam-permissions.md): every Oneteam permission name\n- Example app: `examples/next-app` in the oneteam-apps repository\n- Local testing without Oneteam: `pnpm dev:fake` in that repository";
|
|
13
|
+
var CURSOR_RULE = "---\ndescription: Oneteam login and authorization rules for this app\nalwaysApply: true\n---\n\n## Never hand-roll this\n\nWriting the OAuth flow by hand is the main way apps get this wrong: PKCE with\n`plain`, no `state` check, an ID token read with `atob` instead of verified,\ntokens in `localStorage`. Use the package, which does the whole flow in one\ncall, or stop and say it isn't supported on this stack.\n\n## Rules\n\nThese are not style preferences. Breaking one creates a security hole.\n\n- **Deny by default.** Next.js: `proxy.ts` runs `oneteam.proxy()` for every route.\n TanStack Start: `src/start.ts` has a request middleware that calls\n `oneteam.guard()`. Add to `publicPaths` only what anyone on the internet may\n open (health checks, a public landing page), and ask the user first.\n- **Never exclude routes from the gate by file extension.** `/api/receipts/1.jpg`\n is data. Keep the scaffolded matcher; put public files in `public/assets/`\n and add `'/assets/*'` to `publicPaths`.\n- **The gate is a safety net, not the check.** Server actions, server functions\n and route handlers still call `requirePermission` / `protectRoute` themselves.\n- **Check permissions on the server, every time.** Everything that reads or\n changes protected data runs on the server (Next.js server action or route\n handler, TanStack Start server function or server route) and calls\n `oneteam.requirePermission(...)` (or uses `oneteam.protectRoute`). `<Protected>`\n and `useOneteam().can` only hide UI.\n- **Never trust ids from the browser for authorization.** Load the record\n first, then check against *its* community/owner:\n `requirePermission('network::users.update', { communityId: expense.communityId })`.\n Scope list queries with `oneteam.communitiesFor(session, permission)`.\n- **Identify users by `session.user.id`** (Oneteam user id) or\n `session.user.externalId` (employee number). Not by email, not by `sub`,\n never by a user id sent in a request body.\n- **Secrets stay on the server.** Never prefix `ONETEAM_CLIENT_SECRET` or\n `ONETEAM_SESSION_SECRET` with `NEXT_PUBLIC_` or `VITE_`, never put them in code,\n never log tokens or sessions, never pass `getAccessToken()` results to the browser.\n TanStack Start: import `oneteam.server.ts` only from server functions, server\n routes and `src/start.ts`.\n- **Don't build your own login.** No custom JWT parsing, no reading tokens from\n query strings, no localStorage tokens, no second login screen.\n- **Authorize on Oneteam permissions only.** Not on a role, an admin flag, a\n function name, an email domain or a hard-coded list of user ids. Pick each\n permission from `node_modules/@oneteam-app/apps/docs/oneteam-permissions.md`\n (typed as `OneteamPermission`, so a wrong name doesn't compile) and name the\n ones the app uses once, e.g. in `lib/permissions.ts`. Never invent a name or\n match on a prefix.\n- **Community permissions are not organisation-wide.** A `network:` permission\n without `communityId` is only true for users who hold the same action\n organisation-wide. That's intended: don't \"fix\" it by dropping the community check.\n\n## The shape of a protected app\n\nTanStack Start (Lovable): full files in [references/tanstack-start.md](references/tanstack-start.md).\n\n```ts\n// src/lib/oneteam.server.ts\nimport { createOneteam } from '@oneteam-app/apps/tanstack-start';\nimport manifest from '../../oneteam.app.json';\nexport const oneteam = createOneteam({ scopes: manifest.scopes });\n\n// src/routes/api/oneteam/$.ts\nexport const Route = createFileRoute('/api/oneteam/$')({ server: { handlers: oneteam.handlers } });\n\n// src/start.ts \u2014 deny by default\nconst oneteamMiddleware = createMiddleware().server((ctx) => oneteam.guard(ctx, { publicPaths: ['/health'] }));\nexport const startInstance = createStart(() => ({ requestMiddleware: [oneteamMiddleware] }));\n\n// any server function that touches protected data\nexport const approveExpense = createServerFn({ method: 'POST' })\n .validator((id: string) => id)\n .handler(async ({ data: id }) => {\n const expense = await db.expense.findUniqueOrThrow({ where: { id } });\n await oneteam.requirePermission(APPROVE_EXPENSES, { communityId: expense.communityId });\n // ...\n });\n```\n\nNext.js:\n\n```ts\n// lib/oneteam.ts\nimport { createOneteam } from '@oneteam-app/apps/next';\nimport manifest from '../oneteam.app.json';\nexport const oneteam = createOneteam({ scopes: manifest.scopes });\n\n// proxy.ts \u2014 deny by default\nexport const proxy = oneteam.proxy({ publicPaths: ['/health'] });\n\n// app/api/oneteam/[...oneteam]/route.ts\nexport const { GET, POST } = oneteam.handlers;\n\n// lib/permissions.ts \u2014 the Oneteam permissions this app checks\nexport const APPROVE_EXPENSES = 'network::users.update' satisfies OneteamPermission;\n\n// any server action or route handler that touches protected data\nconst expense = await db.expense.findUniqueOrThrow({ where: { id } });\nawait oneteam.requirePermission(APPROVE_EXPENSES, { communityId: expense.communityId });\n```\n";
|
|
14
14
|
|
|
15
15
|
// src/templates.ts
|
|
16
16
|
var ENV_KEYS = [
|
|
17
17
|
{ key: "ONETEAM_CLIENT_ID", comment: "From Oneteam dashboard \u2192 Settings \u2192 Apps & Shortcuts \u2192 Apps \u2192 your app" },
|
|
18
|
-
{ key: "ONETEAM_CLIENT_SECRET", comment: "Server only. Never prefix with NEXT_PUBLIC_" },
|
|
18
|
+
{ key: "ONETEAM_CLIENT_SECRET", comment: "Server only. Never prefix with NEXT_PUBLIC_ or VITE_" },
|
|
19
19
|
{ key: "ONETEAM_SESSION_SECRET", comment: "At least 32 random characters: openssl rand -base64 32" },
|
|
20
20
|
{ key: "ONETEAM_APP_URL", comment: "Public base URL of this app; must match a registered redirect URI", value: "http://localhost:3000" }
|
|
21
21
|
];
|
|
@@ -58,14 +58,59 @@ function manifest(name, productionUrl) {
|
|
|
58
58
|
name,
|
|
59
59
|
login_uri: `${base}/api/oneteam/login`,
|
|
60
60
|
redirect_uris: [`${base}/api/oneteam/callback`],
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// oneteam:permissions: the user's Oneteam permissions, for can() / requirePermission().
|
|
62
|
+
scopes: ["openid", "profile", "email", "offline_access", "oneteam:permissions"],
|
|
63
63
|
development: {
|
|
64
64
|
login_uri: "http://localhost:3000/api/oneteam/login",
|
|
65
65
|
redirect_uris: ["http://localhost:3000/api/oneteam/callback"]
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
function tanstackLibFile(manifestImport) {
|
|
70
|
+
return `import { createOneteam } from '@oneteam-app/apps/tanstack-start';
|
|
71
|
+
import manifest from '${manifestImport}';
|
|
72
|
+
|
|
73
|
+
// Server only (.server.ts): TanStack Start keeps it out of the browser bundle.
|
|
74
|
+
// Request exactly the scopes the manifest declares (and the admin approves).
|
|
75
|
+
export const oneteam = createOneteam({ scopes: manifest.scopes });
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
function tanstackRouteFile() {
|
|
79
|
+
return `import { createFileRoute } from '@tanstack/react-router';
|
|
80
|
+
import { oneteam } from '../../../lib/oneteam.server';
|
|
81
|
+
|
|
82
|
+
// GET login \xB7 GET callback \xB7 GET session \xB7 POST logout
|
|
83
|
+
export const Route = createFileRoute('/api/oneteam/$')({
|
|
84
|
+
server: { handlers: oneteam.handlers },
|
|
85
|
+
});
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
function tanstackStartFile() {
|
|
89
|
+
return `import { createMiddleware, createStart } from '@tanstack/react-start';
|
|
90
|
+
import { oneteam } from './lib/oneteam.server';
|
|
91
|
+
|
|
92
|
+
// Deny by default: every page, server route and server function needs a
|
|
93
|
+
// Oneteam session unless listed in publicPaths. Only add paths that are safe
|
|
94
|
+
// for anyone on the internet to open. Inside .server() so the server-only
|
|
95
|
+
// module stays out of the browser bundle.
|
|
96
|
+
const oneteamMiddleware = createMiddleware().server((ctx) => oneteam.guard(ctx, { publicPaths: [] }));
|
|
97
|
+
|
|
98
|
+
export const startInstance = createStart(() => ({
|
|
99
|
+
requestMiddleware: [oneteamMiddleware],
|
|
100
|
+
}));
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
function tanstackFunctionsFile() {
|
|
104
|
+
return `import { createServerFn } from '@tanstack/react-start';
|
|
105
|
+
import { oneteam } from './oneteam.server';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Session without tokens, for <OneteamProvider session={...}>. Load it in the
|
|
109
|
+
* root route: beforeLoad: async () => ({ session: await fetchSession() }).
|
|
110
|
+
*/
|
|
111
|
+
export const fetchSession = createServerFn({ method: 'GET' }).handler(() => oneteam.getClientSession());
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
69
114
|
|
|
70
115
|
// src/commands.ts
|
|
71
116
|
var MANIFEST_FILE = "oneteam.app.json";
|
|
@@ -109,6 +154,15 @@ function detectNextApp(root) {
|
|
|
109
154
|
if (existsSync(join(root, "app"))) return { appDir: "app", srcPrefix: "" };
|
|
110
155
|
return null;
|
|
111
156
|
}
|
|
157
|
+
function hasDependency(root, name) {
|
|
158
|
+
const packagePath = join(root, "package.json");
|
|
159
|
+
if (!existsSync(packagePath)) return false;
|
|
160
|
+
const pkg = readJson(packagePath);
|
|
161
|
+
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
162
|
+
}
|
|
163
|
+
function detectTanstackStartApp(root) {
|
|
164
|
+
return hasDependency(root, "@tanstack/react-start") && existsSync(join(root, "src", "routes"));
|
|
165
|
+
}
|
|
112
166
|
var AGENTS_FILE = "AGENTS.md";
|
|
113
167
|
var CURSOR_RULE_FILE = ".cursor/rules/oneteam-apps.mdc";
|
|
114
168
|
var AGENTS_MARKER = "<!-- oneteam-apps:start -->";
|
|
@@ -138,28 +192,18 @@ ${section}
|
|
|
138
192
|
writeIfMissing(root, CURSOR_RULE_FILE, CURSOR_RULE, out2, force);
|
|
139
193
|
}
|
|
140
194
|
function init(root, options, out2) {
|
|
195
|
+
const force = options.force ?? false;
|
|
141
196
|
const next = detectNextApp(root);
|
|
142
|
-
if (
|
|
197
|
+
if (next) {
|
|
198
|
+
initNext(root, next, out2, force);
|
|
199
|
+
} else if (detectTanstackStartApp(root)) {
|
|
200
|
+
initTanstackStart(root, out2, force);
|
|
201
|
+
} else {
|
|
143
202
|
out2.error(
|
|
144
|
-
'No Next.js App Router
|
|
203
|
+
'No supported project found: Next.js App Router ("next" in package.json and an app/ or src/app/ directory) or TanStack Start ("@tanstack/react-start" in package.json and a src/routes/ directory). Other frameworks: use @oneteam-app/apps directly, see the README.'
|
|
145
204
|
);
|
|
146
205
|
return 1;
|
|
147
206
|
}
|
|
148
|
-
const force = options.force ?? false;
|
|
149
|
-
const libPath = `${next.srcPrefix}lib/oneteam.ts`;
|
|
150
|
-
const libImport = "@/lib/oneteam";
|
|
151
|
-
writeIfMissing(root, libPath, libFile(next.srcPrefix ? "../../oneteam.app.json" : "../oneteam.app.json"), out2, force);
|
|
152
|
-
writeIfMissing(root, `${next.appDir}/api/oneteam/[...oneteam]/route.ts`, routeFile(libImport), out2, force);
|
|
153
|
-
const existingGate = ["proxy.ts", "middleware.ts", "src/proxy.ts", "src/middleware.ts"].find(
|
|
154
|
-
(file) => existsSync(join(root, file))
|
|
155
|
-
);
|
|
156
|
-
if (existingGate && !force) {
|
|
157
|
-
out2.warn(
|
|
158
|
-
`found ${existingGate}: not replacing it. Call oneteam.proxy() from it so every route requires a session (see the example app).`
|
|
159
|
-
);
|
|
160
|
-
} else {
|
|
161
|
-
writeIfMissing(root, `${next.srcPrefix}proxy.ts`, proxyFile(libImport), out2, force);
|
|
162
|
-
}
|
|
163
207
|
writeAgentInstructions(root, out2, force);
|
|
164
208
|
if (!existsSync(join(root, MANIFEST_FILE)) || force) {
|
|
165
209
|
const name = options.name ?? readJson(join(root, "package.json")).name ?? basename(root);
|
|
@@ -182,11 +226,39 @@ ${key}=${value ?? ""}`).join("\n");
|
|
|
182
226
|
out2.info("");
|
|
183
227
|
out2.info("Next steps:");
|
|
184
228
|
out2.info(" 1. npm install @oneteam-app/apps");
|
|
185
|
-
out2.info(
|
|
186
|
-
out2.info(" 3. Put the client id and secret in .env.local, plus ONETEAM_SESSION_SECRET");
|
|
229
|
+
out2.info(" 2. Register the app in Oneteam: the register_app tool of the Oneteam MCP connector (as an admin), or the dashboard");
|
|
230
|
+
out2.info(" 3. Put the client id and secret in .env.local and your host's secret settings, plus ONETEAM_SESSION_SECRET");
|
|
187
231
|
out2.info(" 4. npx oneteam-apps doctor");
|
|
188
232
|
return 0;
|
|
189
233
|
}
|
|
234
|
+
function initNext(root, next, out2, force) {
|
|
235
|
+
const libPath = `${next.srcPrefix}lib/oneteam.ts`;
|
|
236
|
+
const libImport = "@/lib/oneteam";
|
|
237
|
+
writeIfMissing(root, libPath, libFile(next.srcPrefix ? "../../oneteam.app.json" : "../oneteam.app.json"), out2, force);
|
|
238
|
+
writeIfMissing(root, `${next.appDir}/api/oneteam/[...oneteam]/route.ts`, routeFile(libImport), out2, force);
|
|
239
|
+
const existingGate = ["proxy.ts", "middleware.ts", "src/proxy.ts", "src/middleware.ts"].find(
|
|
240
|
+
(file) => existsSync(join(root, file))
|
|
241
|
+
);
|
|
242
|
+
if (existingGate && !force) {
|
|
243
|
+
out2.warn(
|
|
244
|
+
`found ${existingGate}: not replacing it. Call oneteam.proxy() from it so every route requires a session (see the example app).`
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
writeIfMissing(root, `${next.srcPrefix}proxy.ts`, proxyFile(libImport), out2, force);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function initTanstackStart(root, out2, force) {
|
|
251
|
+
writeIfMissing(root, "src/lib/oneteam.server.ts", tanstackLibFile("../../oneteam.app.json"), out2, force);
|
|
252
|
+
writeIfMissing(root, "src/lib/oneteam.functions.ts", tanstackFunctionsFile(), out2, force);
|
|
253
|
+
writeIfMissing(root, "src/routes/api/oneteam/$.ts", tanstackRouteFile(), out2, force);
|
|
254
|
+
if (existsSync(join(root, "src", "start.ts")) && !force) {
|
|
255
|
+
out2.warn(
|
|
256
|
+
"found src/start.ts: not replacing it. Add a request middleware that calls oneteam.guard() so every request requires a session (see the TanStack Start example app)."
|
|
257
|
+
);
|
|
258
|
+
} else {
|
|
259
|
+
writeIfMissing(root, "src/start.ts", tanstackStartFile(), out2, force);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
190
262
|
function validate(root, file, out2) {
|
|
191
263
|
const path = join(root, file ?? MANIFEST_FILE);
|
|
192
264
|
if (!existsSync(path)) {
|
|
@@ -205,7 +277,7 @@ function validate(root, file, out2) {
|
|
|
205
277
|
for (const issue of result.errors) out2.error(`${issue.path}: ${issue.message}`);
|
|
206
278
|
return { code: 1 };
|
|
207
279
|
}
|
|
208
|
-
out2.info(`${MANIFEST_FILE} is valid (${result.manifest.
|
|
280
|
+
out2.info(`${MANIFEST_FILE} is valid (scopes: ${result.manifest.scopes.join(" ")})`);
|
|
209
281
|
return { code: 0, manifest: result.manifest };
|
|
210
282
|
}
|
|
211
283
|
function installedVersion(root, name) {
|
|
@@ -247,8 +319,8 @@ async function doctor(root, options, out2) {
|
|
|
247
319
|
if (!env[key]) fail(`${key} is not set (.env.local)`);
|
|
248
320
|
}
|
|
249
321
|
for (const key of Object.keys(env)) {
|
|
250
|
-
if (/^
|
|
251
|
-
fail(`${key} would ship a secret to the browser. Remove the NEXT_PUBLIC_ prefix.`);
|
|
322
|
+
if (/^(NEXT_PUBLIC|VITE)_ONETEAM_(CLIENT_SECRET|SESSION_SECRET)/.test(key)) {
|
|
323
|
+
fail(`${key} would ship a secret to the browser. Remove the ${key.startsWith("VITE_") ? "VITE_" : "NEXT_PUBLIC_"} prefix.`);
|
|
252
324
|
}
|
|
253
325
|
}
|
|
254
326
|
if (env.ONETEAM_SESSION_SECRET && env.ONETEAM_SESSION_SECRET.length < 32) {
|
|
@@ -263,13 +335,22 @@ async function doctor(root, options, out2) {
|
|
|
263
335
|
fail(`${callback} is not in ${MANIFEST_FILE} redirect_uris; Oneteam will reject the login`);
|
|
264
336
|
}
|
|
265
337
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
338
|
+
if (detectTanstackStartApp(root)) {
|
|
339
|
+
const start = join(root, "src", "start.ts");
|
|
340
|
+
if (!existsSync(start)) {
|
|
341
|
+
fail("no src/start.ts: requests are not protected by default. Run `oneteam-apps init`.");
|
|
342
|
+
} else if (!readFileSync(start, "utf8").includes("oneteam.guard")) {
|
|
343
|
+
fail("src/start.ts does not call oneteam.guard() from a request middleware: requests are not protected by default");
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
const gate = ["proxy.ts", "middleware.ts", "src/proxy.ts", "src/middleware.ts"].find(
|
|
347
|
+
(file) => existsSync(join(root, file))
|
|
348
|
+
);
|
|
349
|
+
if (!gate) {
|
|
350
|
+
fail("no proxy.ts: routes are not protected by default. Run `oneteam-apps init`.");
|
|
351
|
+
} else if (!readFileSync(join(root, gate), "utf8").includes("oneteam.proxy")) {
|
|
352
|
+
out2.warn(`${gate} does not call oneteam.proxy(); make sure it protects every route`);
|
|
353
|
+
}
|
|
273
354
|
}
|
|
274
355
|
const nextVersion = installedVersion(root, "next");
|
|
275
356
|
if (nextVersion && !isPatchedNext(nextVersion)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oneteam-app/apps-cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "Set up and check Oneteam login in an app: init, validate, doctor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oneteam",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"oneteam-apps": "./dist/index.js"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
+
"CHANGELOG.md",
|
|
27
28
|
"LICENSE",
|
|
28
29
|
"README.md",
|
|
29
30
|
"dist"
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"provenance": false
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@oneteam-app/apps": "0.1.0-alpha.
|
|
40
|
+
"@oneteam-app/apps": "0.1.0-alpha.1"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"build": "tsup",
|